☰
agent-skills技能库实战:从散装函数到可靠工具调用的工程路径
2026/10/13 5:14:27 网站建设 项目流程

agent-skills这标题,在智能体开发圈子里出现得越来越频繁。乍一看像个新框架,其实它是个挺务实的工程实践:把大模型能执行的每个动作,收敛成有统一契约的技能包,再挂到一个共享的技能库上,让 Agent 既能调外部系统,也能把多步骤任务拆成一条清晰的执行链。我这次想聊的,不只是一个概念,而是从设计思路、代码骨架到线上踩坑的完整过程,给正准备把智能体从“demo 能跑”推到“真在干活”的同学一条可复制的路径。

很多团队做到这一步会卡住:模型接口接好了,提示词写得也算细,但一到真业务里就发现,模型需要的不是“更长的提示”,而是一套它能理解、能调用、能纠错的能力接口。agent-skills 解决的就是这件事。它适合自己做智能体应用、想引入工具调用机制、或者正在维护多个机器人能力的开发者,哪怕你目前只跑过一个最小的 Chat 示例,这篇文章也能帮你把技能的注册、调用、观测、治理这四件事一次理顺。

1. agent-skills 技能库到底解决什么问题

1.1 模型的“会聊天”和业务的“要干活”之间,隔着一条执行鸿沟

大模型本身不执行动作。你说“帮我查一下明天下午会议室有没有空闲”,模型能给你写一段 JSON,或者用自然语言回复一个查询建议,但它自己不会真的去打开某办公系统,也不会去查内部日历。对话能力再强,落地到业务里,总需要一层能把“意图”翻译成“函数调用”的中间层。

这个中间层早期大家都是临时写的:在提示词里塞一段“你可以调用如下函数”,然后把用户输入和几个函数的说明一起丢给模型,解析模型的输出,再做一次函数分发。问题在于,这种临时做法只在两三个函数的时候很好用,一旦函数变成十几个、几十个,调用关系开始交叉,参数格式五花八门,错误处理各写各的,维护成本就迅速失控。

agent-skills 想做的事,就是把这一层从“散装代码”提升到“技能库”。一个技能的本质是:给模型一段足够清晰的能力描述,配上一个严格约束的入参协议,背后挂一个真正干活的函数。模型只负责决定调哪个技能、填什么参数,具体执行细节被封装在技能内部,技能库负责统一调度、校验、返回和观测。

这很像一个团队里来了个什么都懂的新人,但他不知道公司内部系统的口令和操作方式,你得先把一堆系统能力整理成清晰的接口文档给他。技能库就是这个接口文档的工程化版本,而且接口文档本身还要能被模型动态查询、组合和使用。

1.2 散装函数与技能库:差的不是代码,是契约

我见过不少项目,早期也用函数调用的方式接过大模型,表面看起来也能跑。但你把代码摊开看,会发现每个人都按自己的习惯写:有的人把参数直接拼在自然语言里,有的人把返回结果直接丢给模型,有的人在技能内部偷偷改全局变量。三个函数时还能凑合,到第八个函数时,光是字段命名就够吵一架的。

散装函数的核心问题是没有“契约”。模型侧拿到的是一堆参差不齐的描述,有的写得很详细,有的只有一句话,前端调用代码和后端实现之间没有统一的校验层,出错之后要么模型瞎编一个结果,要么技能内部抛出异常把整个 Agent 流程打崩。

技能库提供的契约至少包含几层:一层是功能的唯一标识,也就是技能名;一层是面向模型的能力说明,也就是描述文本;一层是参数的 JSON Schema,严格约束字段名、类型、必填项和枚举;最后一层是执行返回的结构化结果,以及异常情况下的错误码和错误描述。

有了这层契约之后,Agent 主循环不再关心具体技能怎么实现,只关心技能库里有哪些名、描述是什么、入参规则是什么。这就把“模型能力”和“业务系统”彻底拆开,两边可以独立演进。某办公系统接口变了,只需要改对应的技能内部实现;模型版本升级了,那边对新工具的处理能力提升了,技能库本身的注册和契约不用大改。

1.3 一个技能库要服务好哪几类人

设计技能库之前,先想清楚用户是谁。第一个使用者当然是智能体本身,也就是大模型,它靠技能描述来做选择,靠参数 Schema 来生成入参。第二个使用者是开发技能的工程师,他们希望写技能时只关注业务逻辑,不用关心调度、日志、校验这些事情,最好定义好一个技能文件就能自动注册。第三个使用者是排查问题的运维和调试者,他们需要看到每一次技能调用的入参、出参、耗时和错误信息。

这三个角色的利益并不总是一致。给模型看的描述越详细,工程师维护起来越繁重;Schema 约束越严格,模型生成参数的难度越大;日志记录越全面,调试越方便,但上下文和存储成本也越高。技能库的工程价值,就在于把这三方需求放进同一套抽象里,让每一方只面对自己需要的那块表面。我在后面的设计里也始终围绕这条主线:模型侧要友好,工程师侧要简洁,可观测侧要完整。

2. 技能库的核心设计:描述、Schema、注册与发现

2.1 一个技能的四要素,缺一不可

我建议每个技能都由四个部分组成,少一个后面都会难受。第一是技能名,必须全局唯一,且最好能概括动作,比如“query_free_rooms”,不要起那种只有自己看得懂的缩写。第二是描述,面向模型的文本,交代清楚这个技能能做什么、什么时候用它、有什么边界。第三是参数 Schema,定义入参结构,包括类型、必填、枚举和默认值。第四是执行函数,也就是真正的业务逻辑。

四要素听起来不复杂,但落地时很多人只在代码里写了一个函数,描述和 Schema 都靠临时拼。临时拼的问题在于:模型看到的工具列表和背后实际执行的函数之间,可能已经悄悄不一致了。比如函数改了入参,但描述里还在写旧参数,模型按旧参数调用,执行层直接报错。

所以技能库的核心层应该严格统一这些要素的入口。每个技能包在注册时把四要素一并提交,库里做一致性校验,参数 Schema 与实际函数签名不匹配就直接拒绝注册。这一步检查能挡住后面大量运行期问题,属于典型的“前置成本,后置收益”。

2.2 技能描述不是写给人看的,是写给模型看的

写技能描述是整件事里最需要抠细节的地方。很多工程师写描述时,习惯性按内部接口文档的方式来,堆一堆实现细节和技术名词,结果模型根本抓不住重点。技能描述的目标,是让模型在好几屏的工具列表里,以极小的代价判断“这个技能是否适合当前任务”,并正确构造参数。

一个比较通用的写法是四句话结构:先一句话点出这个动作是什么,再补一句“什么场景下你应该调用它”,然后写清楚输入的关键信息,最后标出边界,比如“只返回今天的数据”“不修改状态”“只支持工作日”。听起来像产品文案,但对着模型说话就这感觉。

我举一个实际对比。差的描述:“会议室查询模块,支持对会议室资源进行可用性检查,入参为date、startTime、endTime,函数内部会访问数据库并返回匹配记录。”信息全吗?信息都在,但对模型不友好。

更好的描述:“用于查询某日期时间段内哪些会议室空闲,适用于用户问‘明天下午有空的会议室’时。入参需要完整日期和起止时间,返回空闲会议室列表和状态字段。只做查询,不预约、不修改。”后者没有增加任何代码信息,但模型理解“什么时候用它”这件事变得极其容易。

我后来还养成了一个习惯:如果同一个技能被模型反复误用,先不急着改代码,先把描述当作文案重新写一遍,往往比改校验逻辑更有效。成本低,见效快。

2.3 参数 Schema 怎么定才算“既严格又灵活”

参数这块是最容易走极端的地方。太松,模型会自由发挥,给你传一些业务世界根本不存在的枚举;太紧,模型动不动就凑不齐必填字段,调用成功率直线下降。我的经验是:把“模型必须知道且只有模型才能确定的字段”设为必填,把“可以由默认值填充、或者在执行层自动推导的字段”全部不要暴露给模型。

比如查询空闲会议室,模型必须知道用户要的时间段,所以日期和起止时间是必填;但“只查会议室还是也查工位”这种场景如果默认只开会话,就可以先不暴露,让它走默认值。模型只需要做“唯一正确”的选择题,不要做开放性填空题。

另一个有用的技巧是尽量给参数提供枚举值。模型从你给的候选项里挑一个,远比让它凭空生成一个靠谱。比如“状态”字段直接限制为 available、booked、maintenance 三个值,模型就算想编也只能在这三个里面选。而对那些本身是外部系统唯一标识的字段,比如某个内部编号,模型不可能知道,正确做法是先给模型提供“可选项列表”,让它从中引用,而不是让它自己编造。

Schema 里还建议明确标注格式约束。日期时间统一成字符串,格式固定为“YYYY-MM-DD HH:mm”,不要给模型同时提供多种格式选项,选择越多,越容易错。

2.4 注册与发现:给 Agent 一份可用的“技能清单”

技能注册机制很容易被忽视,但它决定了技能库的扩展体验。早期做法是在 Agent 启动时手工拼一个列表,新增技能就要改主流程代码,久而久之主流程文件里堆满了技能实例化代码。更合理的方式是设计一个注册中心,技能模块通过装饰器或者显式注册接口把自己挂进来,主流程只依赖注册中心的抽象接口。

注册信息里除了技能四要素,还可以附加一些运行时元数据:技能所属的领域分组、依赖的外部系统标识、是否幂等、超时时间建议、优先级权重等。这些信息当前调度逻辑不一定用得上,但等到要做技能排序、分组、灰度时会非常有用。

发现机制决定 Agent 怎么拿到技能清单。最简单的是启动时全量读一次,把清单转换成模型工具接口需要的结构。稍微复杂一点的是支持运行时动态加载,添加一个技能包不用重新部署 Agent。我自己更倾向保守方案:先保证全量注册,把动态加载放到后续迭代再做。全量注册的模型工具列表确实会占比特数,但它足够简单可靠,等你真正碰到 token 压力时再上分组筛选也不迟。

注册时记得做重名校验,同名的技能直接拒绝覆盖,否则调试时会遇到“模型调用的到底是哪个实现”这种很伤脑筋的问题。

3. 手把手搭一套最小可用的 agent-skills

3.1 项目结构先想清楚,后面少走弯路

先给一个最小目录结构,后面扩展不会乱:

agent-skills/ core/ registry.py schema.py runner.py skills/ calendar.py notify.py todo.py examples/ simple_agent.py config/ skills.json

core 放调度核心,它们不依赖任何具体业务技能,只对“技能”这个概念做抽象。skills 按领域放具体技能,每个技能文件只暴露注册函数和一个可执行入口。examples 放 Agent 主循环的示例,演示技能库怎么被模型调用。config 里的 skills.json 用来维护技能清单,包括哪些技能默认启用、哪些技能灰度开启。

这个结构的关键是依赖方向:skills 依赖 core,core 不反向依赖 skills。这样新增技能就只是一个新增文件加一行注册的过程,不会碰到公共调度代码。很多项目做乱了,往往是依赖方向反了,core 层写了一大堆具体业务逻辑,最后新增一个技能改三处代码。

3.2 定义一个标准技能:完整代码与逐段讲解

先看 core 层一个简化但完整的技能基类,以及注册中心:

# core/registry.py from dataclasses import dataclass, field from typing import Callable, Any, Dict @dataclass class AgentSkill: name: str description: str parameters: Dict[str, Any] # JSON Schema 风格的入参定义 handler: Callable[..., Dict[str, Any]] group: str = "default" # 领域分组,后续治理用 idempotent: bool = False # 是否幂等 class SkillRegistry: def __init__(self): self._skills: Dict[str, AgentSkill] = {} def register(self, skill: AgentSkill): if skill.name in self._skills: raise ValueError(f"技能名重复:{skill.name}") self._skills[skill.name] = skill def get(self, name: str) -> AgentSkill: return self._skills[name] def list_schemas(self): schemas = [] for skill in self._skills.values(): schemas.append({ "type": "function", "function": { "name": skill.name, "description": skill.description, "parameters": skill.parameters, }, }) return schemas def execute(self, name: str, arguments: Dict[str, Any]) -> Dict[str, Any]: skill = self._skills.get(name) if not skill: return {"ok": False, "error": f"技能 {name} 不存在"} try: return {"ok": True, "data": skill.handler(**arguments)} except Exception as exc: return {"ok": False, "error": f"{skill.name} 执行失败:{exc}"}

这个注册中心的 execute 统一把异常包成结构化返回,这是关键设计。异常信息不能直接暴露给模型,但也不能完全吞掉。结构化返回里有 ok 标记和 error 描述,模型收到后可以据此调整参数、换技能,或者向用户反馈错误,而不是让整个对话流程崩溃。

对应的一个具体技能示例,以查询空闲会议室为例:

# skills/calendar.py from core.registry import AgentSkill def query_free_rooms(date: str, start_time: str, end_time: str): # 这里调用真实的日历或办公系统接口 # 开发期可以先返回模拟数据 return { "rooms": [ {"id": "A101", "name": "会议室A101", "status": "available"}, {"id": "B202", "name": "会议室B202", "status": "available"}, ] } def register(registry): registry.register(AgentSkill( name="query_free_rooms", description=( "查询指定日期、时间段内哪些会议室空闲。" "适用于用户询问“明天下午3点到4点有哪个会议室能用”时。" "入参需要完整日期和起止时间,返回空闲会议室列表。" "只做查询,不预约、不修改数据。" ), parameters={ "type": "object", "properties": { "date": {"type": "string", "description": "日期,格式YYYY-MM-DD"}, "start_time": {"type": "string", "description": "开始时间,格式HH:mm"}, "end_time": {"type": "string", "description": "结束时间,格式HH:mm"}, }, "required": ["date", "start_time", "end_time"], }, handler=query_free_rooms, group="calendar", idempotent=True, ))

这里的 description 里特意写了“不预约、不修改数据”,很多人觉得这句话多余,但它对模型非常有用。模型知道这是个只读操作之后,一方面会更放心地调用,另一方面不会错误地用它去完成预约任务,而是会继续找预约相关的技能。边界描述能显著减少模型的对齐误差。

3.3 调度循环:模型下指令,技能库执行,结果回填

技能库本身不跑业务,跑业务的是 Agent 调度循环。设计好循环是让整个系统稳定运转的核心。我用一个极简的循环示例说明每一轮发生了什么:

# examples/simple_agent.py def run_agent(model, registry, user_message) -> str: messages = [{"role": "user", "content": user_message}] for step in range(5): # 限制最大迭代步数,防止死循环 tools = registry.list_schemas() response = model.chat(messages=messages, tools=tools) if not response.tool_calls: return response.content messages.append(response.message) for tool_call in response.tool_calls: result = registry.execute(tool_call.name, tool_call.arguments) messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": json.dumps(result, ensure_ascii=False), }) return "已经尝试多次,仍未能完成完整操作,请简化需求后重试。"

整个过程就是一个不断循环的“问模型-执行技能-把结果回填-再问模型”的闭环。第一轮模型可能决定先查会议室,执行完把结果回填;第二轮模型看到没有空闲的时段,可能再查另一个日期,或者直接请求用户修改需求;第三轮可能调用另一个技能去订会议。

有几个点需要特别强调。第一个是工具列表每轮都要传,因为模型每一轮都要重新看到可用的工具集;但这个列表本身占用 token,技能多了之后需要做瘦身,我后面会专门讲。第二个是结果回填必须是结构化 JSON,而不是生产环境执行的完整日志,模型只关心业务结果。第三是迭代步数上限很重要,没有上限的循环可能失控,我一般设 5 到 8 轮,超过就直接告知用户需要人工介入。

3.4 多技能编排:拿“组织一场跨部门周会”练手

单个技能调通之后,一定会遇到多个技能串起来完成的场景。我用一个常见任务做例子:“帮我把周一下午 3 点那场周会安排到空闲会议室,然后通知项目组五个人。”

这个任务最少要三个技能:查询空闲会议室、预约会议室、发送通知。模型会自己编排调用顺序,先调用查询,再调用预约,再调用发送。技能库要做的事情是确保每步调用都能拿到下一步需要的信息。

比如第一步 query_free_rooms 返回的房间列表,必须包含房间的唯一标识。第二步预约接口需要房间ID和会议时间,如果第一步模拟数据里只返回房间名而没返回 ID,模型就卡在“该传哪个标识”上。所以技能库设计前,最迟也得在写第一个技能时想好返回结果如何为后续技能服务。这不是漂亮的设计,而是真实运行的硬约束。

还有一种场景是固定流程的编排。比如某些操作不允许模型自由发挥顺序,必须先查、再确认、再下单。这种安全敏感的链路不适合全交给模型自由编排,我建议单独提供一个 workflow 技能,它内部把原子技能按固定顺序串起来,对整个外部世界只暴露一个“安排跨部门周会”入口。模型不需要知道中间有三个调用,它只需要传入会议名称、时间、参与人,剩下的由 workflow 内部完成。两种模式各有应用场景:用户意图开放时让模型编排,步骤固定且安全要求高时用代码编排。

4. 上线前必看的踩坑记录与排查清单

4.1 技能描述写得天花乱坠,模型就是不调用

我见过最典型的空转场景:技能注册没问题,Schema 也没错,日志里一只调用记录都没有,模型全程在用自己的知识回答。第一反应往往是“是不是模型不支持函数调用”,但大多数情况根本不是模型的问题,而是技能描述没能在提示语境里建立可用性。

常见原因有几个。技能描述和当前用户提问的措辞差得太远,用户问“明天开会的地方定哪儿”,技能描述里却写“查询会议室资源”,模型不一定能建立起关联。另外技能描述里没有给出“何时调用”的显式信号,模型不确定这个操作是否合适,宁可保守不调用。

排查时我会做一个很笨但很有效的动作:把技能描述和用户的原始提问放在一起,闭上眼睛想象自己是模型,判断该不该调用。如果我觉得含糊,那模型大概率也含糊。这时候要做的不是调代码,而是改描述中的触发条件,明确写出“当用户提到开会、预约、会议室时,请先调用此技能”这类显式信号。在系统提示里也可以补一句“你拥有会议室查询和预约工具,遇到相关需求时优先使用工具而非自行推断”。实测下来,描述里增加触发词,比反复调整模型参数更管用。

4.2 参数幻觉:模型在虚拟一个不存在的会议室ID

技能调用成功率高并不代表结果可靠,模型参数字段里的幻觉问题很隐蔽。比如预约会议室,模型没查到具体的房间编码,就直接编了一个“room_88”,而系统里根本没有这个房间,预约请求被外部服务拒掉。这类问题在枚举字段、唯一标识字段上特别容易爆发。

治理经验是分层的。凡是“模型不可能凭空知道”的标识,接口设计上就应该让模型从候选列表里引用,比如先查询返回一个含 ID 的列表,预约时把 ID 一并传进去;而不是让模型无中生有生成 ID。如果无法避免生成标识,就在 Schema 里加 pattern 或者枚举约束,尽可能圈定范围。执行层再做一次守卫式的后置校验,参数里出现未知标识,就返回错误并提示“该 ID 不存在,请先调用查询技能获取有效的 ID”。

另外,涉及下单、删除、发送这类不可逆操作,强烈建议加一层人工确认。模型执行预约技能前,先调一个“生成预约确认单”的技能,把房间、时间、参与者展示给用户,用户确认后再执行最终预约。这层确认在外面看来多了一次交互,但对减少事故非常重要。

4.3 多实例共享与状态隔离:技能别带“私货”

技能本质是被多个会话共享的函数。非常容易踩的坑是为了图省事,在技能模块里放全局变量,把“当前用户”“当前选中的会议室”挂在模块级。一旦多个用户同时使用,各自会话的数据就会互相污染,出现“A 用户订的房间跑到了 B 用户头上”的诡异问题。

正确姿势是技能尽可能做无状态设计。所有需要的数据都从入参里带,没有入参的信息不要偷偷塞进模块。真有跨步骤的中间状态,比如“刚才选了哪个房间”,应该由 Agent 调度层保存在会话上下文里,而不是由技能自己保存。这一步想明白之后,多用户并发、多机器人共享技能库才不会翻车。

我在代码审查时看到技能文件里有模块级 mutable 对象,会要求改成函数内局部变量,或显式传入上下文对象。虽然从单元测试角度看没什么区别,但一旦并发起来,差异就是线上事故。

4.4 日志与追踪:没 tracing,排查的时候真要命

技能调用的日志必须从第一天就做,否则后面根本没法排查。最开始的技能库可能只有三个技能,出了问题肉眼能定位,等技能涨到十几个,用户问一句“为什么那个 Agent 一直订错会议室”,你连它调用了哪个技能、传了什么参数都不知道,体验会很崩。

每一条技能调用日志应该至少包含:请求追踪 ID、技能名、完整入参、出参摘要、耗时、返回状态。追踪 ID 贯穿整个 Agent 会话,这样一问一答里涉及的多轮技能调用就能串联起来。日志格式建议用结构化文本,一行一个事件,方便搜出同一追踪 ID 下的所有调用历史。

也要留意日志里的敏感信息,会议室名称、人员邮箱、会议主题这些尽量不要全量落盘。要么做字段脱敏,要么只记录摘要级别的内容。排查时缺信息会难受,但泄露内部信息带来的麻烦更大,两害相权取可控的边。

4.5 技能数量膨胀之后,模型反而开始“选择困难”

技能从 10 个涨到 40 个以后,会出现一个反直觉的现象:可用技能越全,模型选错的概率越高。原因很简单,模型在巨大的工具列表中做选择,注意力被稀释了,描述写得再规范,也架不住几十个选项的认知负担。

治理手段我推荐三个。第一个是分组标签,注册时给技能打上“calendar”“notify”“todo”之类的组别,调度时可以根据任务类型先筛一组再进模型,而不是把全部工具一股脑塞进去。第二个是合并同类项,把动作类似、只在参数细节上有差异的技能合并成一个技能,用 action 参数区分。第三个是组合技能,将高频率使用的原子技能序列封装成一个 workflow 技能,让模型少做几步决策。

定期翻调用日志也很重要,连续一个季度零调用的技能,说明模型找不到调用它的理由。要么重写它的描述和入口,让它更容易被发现;要么直接下线,给技能库减负。技能库不是越满越好,克制才是维护成本的关键。

说回我自己的体感,技能库这东西收益是滞后的。刚开始只有一两个技能时,你会觉得多了一层接口是种负担,不直接调函数不也挺快吗?但当技能数超过两位数、多个人共享同一套能力、线上问题开始出现时,core 层那套注册、校验、追踪机制的价值才会真正体现。如果让我重来一次,我会把技能的粒度写得更小、调度循环跑得更早、技能描述改得更频繁。技能库没有一锤定音的完美设计,它是在一次次模型误用、参数幻觉和并发事故里,慢慢被磨出来的工程产物。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询