☰
Agent技能体系实战:从工具堆砌到规范化调用
2026/10/13 11:09:32 网站建设 项目流程

最近几个月一直在调各种Agent应用,说实话,模型本身带来的惊喜已经越来越少,真正让我头疼的,是“技能”这件事。你可能也遇到过这种情况:一开始只是给Agent塞了三五个函数,它表现得还不错;等工具数量上了两位数,Prompt越来越长,模型开始频繁调用错函数、填错参数,甚至干脆不理工具自己瞎编。我后来把这一整套东西抽出来重新设计,名字就叫 agent-skills。你可以把它理解成一套“给Agent用的技能管理体系”,核心解决三个问题:技能怎么定义、怎么注册、怎么被可靠地调用。这篇文章我就把这个项目的完整思路、落地细节和踩过的坑展开聊聊,适合正在做Agent应用开发、或者想把工具调用做规范化的工程师参考。

1. 为什么Agent需要一套独立的“技能体系”

1.1 从“堆提示词”到“技能封装”的三个阶段

我最早做Agent的时候,采用的是最粗暴的方式:把所有工具函数一股脑写进系统提示词里,后面附上“如果你需要使用工具,请按以下格式输出”。效果怎么样呢?初期能用,后期一塌糊涂。

你可以把这种方式想象成让一个新员工上岗,不吃培训手册,直接塞给他一摞单据让他自己悟。单据少的时候没问题,单据一多,他就开始分不清哪张是报销单、哪张是申请单,填错栏位是必然的。模型也是这样:当函数列表超过一定规模,它对函数意图的区分能力会明显下降,最典型的表现就是——明明该调用A函数,它调了B,而且参数还是从上下文里猜的。

后来我进入第二个阶段:用现成的Function Calling能力,把工具以JSON Schema的格式传给模型。这个阶段解决了“格式混乱”的问题,模型知道该输出结构化的调用请求了,但新的问题又来了——我依然要把所有工具都塞到一次请求里,不管这个技能在本轮对话中是否用得上。结果是Prompt越来越长,响应越来越慢,模型在无关工具之间反复犹豫。

走到这一步我才意识到,真正需要的是第三个阶段:把每个工具从“函数的零散集合”升级为“独立的技能包”,每个技能包自带说明、参数协议、执行逻辑和测试用例。Agent侧只需要维护一个技能注册表,按需加载,动态调用。这就是 agent-skills 这个项目最初的动机。

1.2 技能体系的三根支柱:注册表、调用协议、生命周期

一套完整的技能体系,我拆成了三块,缺一块后面都会出问题。

第一根支柱是注册表。它解决的是“Agent到底有哪些能力可用”的问题。所有技能在上线之前都要登记,登记的内容包括技能名、用途描述、入参出参定义、版本号、负责人信息。这就像酒店的前台,住客不会直接冲进每个房间找人,而是先通过前台的房态表知道哪里有空房、能提供什么服务。没有注册表,Agent就是一个黑箱子,连开发者自己都不知道它现在能干什么。

第二根支柱是调用协议。模型输出一段结构化的调用意图,运行时把它翻译成真实的函数调用,再把结果按统一格式回传给模型。协议要足够严格,否则就会出现参数类型错乱、返回结构不统一的问题。在我的设计里,协议统一走JSON Schema,入口出口都是JSON,不许有“特殊通道”。

第三根支柱是生命周期。一个技能不是写完就结束的,它会经历开发、测试、上线、迭代、下线这几个阶段。如果没有生命周期管理,最典型的问题是:你改了技能A的参数结构,但忘了告诉模型侧,结果模型用旧参数去调用,全链路报错。所以每一次变更都要有版本记录,最好还能支持灰度。

1.3 什么算技能,什么不算

这里我想多说一句界定问题。很多人会把“技能”等同于“函数”,但它们其实是两个层次的抽象。函数是最小的执行单元,而技能是面向Agent的完整能力打包。

举个例子,“获取天气”可以是一个函数,但“根据用户所在城市获取未来三天的天气并给出穿衣建议”才是一个技能。技能的粒度要控制在:模型看到描述之后,能在一次调用中完成目标,不需要多次来回试探。如果你把一个技能设计成“先查城市编码,再查天气,再算温差”,那就太细了,模型会晕,调用链也会变得脆弱。

反过来,如果技能描述里塞了一堆不相干的功能,比如“这个技能既能查天气又能订机票还能算汇率”,那也不行。模型分不清边界,迟早会拿错参数。

我自己的判断标准就一条:一个技能应该能用一句话说清楚它帮Agent完成了什么。说不清楚,说明粒度不对,要么继续拆,要么合并掉。

2. 技能设计实战:目录、参数和描述这三个地方别偷懒

2.1 标准目录结构:让每个技能开箱即用

我先给出我最终采用的目录结构,你可以直接抄:

skills/ image-resizer/ SKILL.md schema.json handler.py tests/ test_handler.py fixtures/ sample_input.json

每个技能包固定四类文件:描述文件、参数协议、执行代码、测试文件。看起来简单,但每一步都有讲究。

SKILL.md 是用自然语言写给模型“看”的说明文档。它描述的是这个技能在什么场景下应该被调用,需要注意什么边界。很多人不重视这个文件,觉得模型反正读JSON Schema就够了,实际上恰恰相反,模型理解技能的语义主要靠这段描述。写得好的SKILL.md能让模型在相似技能之间做出正确选择,写不好就等着模型乱调用。

schema.json 是参数协议,我用JSON Schema严格定义每个字段的类型、必填性、取值范围和默认值。handler.py 是真正的执行逻辑,它接收经过校验的参数,执行外部操作,然后返回统一结构的JSON。tests 目录放单元测试和模拟输入,每次改动都要能跑通。

有一点我特别想强调:handler.py 内部不要写“聪明”逻辑,只做参数到结果的映射。所有条件判断、状态管理都放到Agent层去处理。为什么?因为技能是要被模型编排的,你没法预料它会以什么顺序、什么参数来调用你,保持技能本身的纯粹,后面才不容易出Bug。

2.2 参数定义:最容易翻车的地方

整个技能体系里,参数定义是翻车率最高的地方,没有之一。我调过的Agent应用里,至少有三分之一的问题出在参数上:模型传了不存在的字段、类型对不上、枚举值超出范围、必填项被忽略。

JSON Schema能解决大部分问题。下面是我常用的一个示例:

{ "name": "resize_image", "description": "调整图片尺寸并返回处理后的图片URL", "parameters": { "type": "object", "properties": { "image_url": { "type": "string", "format": "uri" }, "width": { "type": "integer", "minimum": 16, "maximum": 4096 }, "height": { "type": "integer", "minimum": 16, "maximum": 4096 }, "mode": { "type": "string", "enum": ["fit", "fill", "stretch"], "default": "fit" } }, "required": ["image_url", "width", "height"] } }

这里有三个细节值得展开说说。

第一,枚举字段必须用 enum 把选项限定死。比如美颜等级、排序方向这类字段,如果不限定枚举,模型会凭感觉给出一个听起来合理的值,比如“big”或者“美丽模式”,然后你就得在handler里做兼容,兼容来兼容去代码就烂了。用enum之后,模型只能在给定选项里选,错误率直接归零。

第二,数值字段要给范围和语义单位。宽度、高度这类参数,如果只写integer,模型可能传负数或者一个离谱的大数。我在描述里还会补一句单位说明,比如“width表示输出图片的像素宽度,超过4096会自动截断”,这样模型在推理时有个度,不会放飞自我。

第三,required字段要克制。只把真正执行功能必需的东西设为必填,可选项不要放进required。我见过有人把十个字段全设为必填,结果模型为了凑齐参数开始编造内容,那比缺参数更可怕。参数缺了可以补对话,编造了没法补救。

2.3 技能描述怎么写,模型才不瞎调用

参数定义之外,技能的名称和描述是另一个关键点。我把调模型比作和实习生合作:你交给他的每个工具都要有清晰的标签和使用说明,他才知道什么时候该用哪个。

首先是命名。技能名要符合模型对语义的直觉,不要搞缩写,不要用“Util_01”这种编号。我一开始为了省事,把一个查库存的技能命名成inv_q,结果模型在需要查库存的时候,完全没把它和“库存”这个词关联起来。改成query_inventory之后,调用准确率肉眼可见地上来了。

其次是描述。描述里至少包含三个维度:触发场景、功能范围、典型示例。我常用的模板是:

当用户需要调整图片尺寸时使用此技能。支持按宽高等比缩放、裁剪填充和拉伸三种模式。示例:用户说“把这张图缩小到800像素宽”,调用resize_image,image_url为原图地址,width为800,height按原图比例自动调整,mode传fit。

这个描述的作用是给模型建立“什么情况下该用我”的决策边界。遇到相似技能,比如图片压缩、图片裁剪、图片加水印,描述写得越清楚,模型选错的概率越低。我在实测中发现,加了典型示例之后,模型调用准确率能提升至少15个百分点,这个数字我测过多次,稳定有效。

3. 把技能挂到Agent上:注册、调用、回传的三条链路

3.1 注册与发现:启动扫描还是运行时注册

技能设计好之后,接下来就是怎么让Agent知道它们的存在。这里有两套方案,各有取舍。

方案A是启动时扫描目录。Agent启动时遍历skills目录,读取每个子目录下的schema.json,构建技能注册表,生成一个全部技能的索引。优点是实现简单、逻辑清晰,增删技能只要动文件系统就行。缺点是每次新增技能都要重启服务,不适合需要动态扩能力的场景。

方案B是运行时动态注册。通过注册中心或数据库登记技能,Agent在每次对话前按需拉取相关技能的清单。优点是灵活,技能可以热更新;缺点是复杂度上来了,还要考虑注册中心的可用性和一致性。

我在项目里先实现了方案A,跑通之后再扩展方案B。有一件事要特别注意:注册表里要维护技能名和handler的映射关系,技能名重复是绝对不允许的。我遇到过两个技能都叫search,结果Agent每次调用都路由到先注册的那个,另一个完全被无视。后来我在注册阶段加了一步冲突检测,发现重名直接报错并终止启动,宁可启动失败也不能带病运行。

另外,注册表里建议记录技能版本号。我在最开始没记录版本,上线一个新版本时模型缓存里还留着旧参数的描述,导致调用参数和实际handler不一致。加了版本号之后,至少可以通过日志快速定位是哪一版本的调用出了错。

3.2 调用分发:从模型输出到真实执行的完整通道

技能注册完成之后,核心环节就是调用分发。整个链路我理成了一条单向通道:模型输出结构化调用意图,解析器将其解析为调用请求,路由器找到对应handler,执行器带着校验过的参数去执行,最后把结果按统一格式压缩后回传模型。

用代码示意如下:

# 调用分发核心逻辑(简化版) def dispatch(model_output, registry): # 1. 解析模型输出中的调用意图 call_intent = parse_function_call(model_output) if not call_intent: return None # 2. 根据技能名查找handler skill = registry.get(call_intent.name) if skill is None: return build_error_response("unknown_skill") # 3. 参数校验 validated_params = validate_params( call_intent.arguments, skill.schema ) if validated_params.errors: return build_error_response("invalid_params", validated_params.errors) # 4. 执行技能,带超时控制 result = execute_with_timeout( skill.handler, validated_params.data, timeout=skill.config.timeout ) # 5. 回传统一格式 return build_success_response(result)

这里每一步都有坑。解析模型输出时,我最初直接用正则去抠函数名,后来发现模型有时候会输出Markdown包裹的JSON,有时候会带多余逗号,纯正则根本扛不住。换成一个容错性强的JSON解析器之后,解析失败率从5%降到了0.5%左右。

路由查找方面要注意大小写归一化。模型的输出稳定性其实比你想象的低,同一个技能有时候输出snake_case,有时候又变成CamelCase。我在注册和路由两层都强制小写统一,才能避免这种低级问题。执行环节最需要注意的是超时。技能里如果涉及外部API调用,你要预设超时时间,否则一个卡住的HTTP请求能把整个Agent的响应时间拖到用户无法接受。

超时之后怎么办?我采用的策略是:超时错误包装成一种“技能执行超时”的错误结果回传给模型,让模型判断是否需要重试或者换一种方式。你可能会想,为什么不直接自动重试?因为有些操作不是幂等的,自动重试可能造成重复扣费或者重复写入。

3.3 结果回传与错误处理:别让一次失败毁掉一次对话

调用成功之后,结果的回传质量直接决定了模型的下一步推理质量。如果返回结果是超大篇幅的文本,模型的注意力会被无关细节淹没,后续思考质量下降。所以我在回传前增加了一个“结果压缩”步骤,把非结构化的冗长文本截断、把无关字段去掉,只保留模型推理需要的最小信息集。

我做了一个简单的压缩规则:结果超过预设阈值(通常是2000字符)时,自动截断,并在末尾追加“结果过长已截断,如需完整信息请说明”。这样做的好处是模型不会被淹没,同时它也知道信息不完整,可以主动追问。

错误处理这块,我设计了一套错误码体系。错误分成三类:

  • RetryableError:网络抖动、服务临时不可用,模型可以重试
  • InvalidInputError:参数错误、内容不存在,重试也没用,模型需要修改输入或调整方案
  • UnknownError:未知异常,统一按“暂时无法完成”处理

错误码设计的意义在于给模型传递可行动的信号。如果什么错误都笼统地返回“调用失败”,模型不知道是该换参数、换技能还是直接放弃。有了明确的错误分类,模型的应对策略会清晰很多,整个对话的容错能力也会明显提升。

我还额外给每个技能配了一个“失败兜底文案”。技能可能因为各种原因执行不了,但Agent不能对着用户干瞪眼。兜底文案会让Agent说一句“当前暂时无法完成,你可以稍后再试”,而不是直接把一堆堆栈信息抛给用户,那体验太糟糕了。

4. 实测中的问题排查与避坑记录

4.1 模型填错参数:堪称最高频的事故

参数填错是我在实际运行中遇到的最高频事故,没有之一。不是说有了JSON Schema就能完全避免,模型在不理解业务语义的时候,依然会填出结构合法但语义错误的值。举个例子,你的技能要求传入时间范围,模型给你传了一个“last year”,幸好JSON Schema的type约束为string,它不会报错,但你handler里一解析就崩了。

针对这个问题,我的经验是在参数协议之外增加一层“语义校验”。时间类字段必须符合ISO 8601格式,URL字段必须能解析出有效域名和协议,枚举字段必须精确匹配。任何一层校验失败都会被捕获为InvalidInputError,而不会让错误语法渗透进handler内部。这层语义校验看着麻烦,但它是把模型的不确定性挡在系统外部的一道关键防线。

我还对参数来源加了记录。每次调用的参数都会以日志形式存储,包括模型最初输出的原始JSON、校验后的最终参数、执行结果。这个日志帮我发现了不少规律,比如某个模型在日期推算上特别容易错,那么针对这个模型,我会在技能描述里给出更明确的时间格式示例,相当于做了一层针对性的防御。

4.2 并发与状态:多个技能同时调用怎么不乱

Agent有时候会在一次回复里同时调用多个技能。比如用户问“对比这两款手机的相机参数和价格”,Agent可能同时调用参数查询、价格查询、对比分析三个技能。并发调用的顺序和结果归属如果不处理好,返回给模型的内容就是一团乱麻。

我采取的方案是给每个调用请求生成一个唯一的 call_id。模型在发起多个调用时,每个调用都带着自己的call_id,执行完成后按call_id把结果装配回对话上下文。这有点像快递单号,包裹再多,单号一对就能定位。

并发还需要考虑共享状态的问题。技能本身要尽量设计成无状态的,所有输入输出都通过参数传递。如果一个技能内部维护了全局变量,两个调用同时进来,互相污染几乎是必然的。为了让技能可并发,我会强制要求handler内部不写全局可变状态,所有临时状态用局部变量,需要跨调用保存的数据统一交给外部存储。

如果你的技能涉及外部存储的写入,还要注意冲突处理。比如两个调用同时写同一个文件,用“读改写”的方式很容易丢数据。我在底层存储层加了按key粒度的锁,虽然牺牲了一点性能,但换来了确定性,做Agent场景值得。

4.3 技能更新之后,老对话怎么办

版本管理是个容易被忽略但实际很致命的问题。Agent的对话是有状态的,一段长对话可能已经持续了十几轮。如果在这期间,技能A从V1升级到了V2,参数结构变了,那么模型在后续轮次里可能会用新技能定义发起调用,但对话历史里还残留着旧调用的记录。这会导致一种很尴尬的情况:前后调用使用的协议不一致,上下文里的中间结果没法对齐。

我的处理策略是:每次技能升级,版本号递增,并在注册表里保留最近两个可兼容版本的协议。新发起的调用统一走新版本协议,但历史会话中已经产生的技能调用结果继续以“快照”形式存在上下文里,不被新版本影响。这样可以避免升级导致的长对话中断。

快照机制还有个好处:当技能执行涉及外部状态变化时(比如调用了支付、修改了数据库记录),模型后续推理需要依赖那个操作的结果。如果因为版本升级导致快照丢失,模型就会基于错误前提继续推理。所以我会在回传结果时同时写入一个快照字段,这个字段在后续轮次中对模型可见,保证了长对话的连贯性。

4.4 快速排查清单:从现象到根因

最后整理一份排查清单,遇到问题可以按顺序过一遍:

现象可能原因排查动作
模型从不调用某个技能描述不清晰或触发场景没写明白检查SKILL.md是否包含触发场景和典型示例,重写描述
模型调用了错误技能技能名称歧义、相似技能太多在描述里增加区分性说明,必要时合并技能
参数频繁报错语义校验缺失或参数描述不足补充JSON Schema约束,增加语义校验逻辑
调用超时外部API响应慢或超时设置过短检查外部服务耗时,调整超时参数
并发结果错乱缺少call_id或请求隔离给每次调用分配唯一ID,校验回传归属
升级后老对话出错版本协议不一致保留兼容协议,对历史结果做快照
技能执行成功但模型胡编结果未回传或回传被截断确认回传链路完整,检查结果压缩策略

这套排查清单看起来简单,但它是我把几十个线上异常案例归类汇总之后的结果。你先定位现象,再沿着对应行去找可能原因,基本能覆盖绝大多数问题。

说到这,我有点感慨。当初做 agent-skills,我以为自己是在写一套工具框架,后来才发现它更像是在给Agent设计一套“职业培训体系”——每个技能都是岗位说明书加操作手册,注册表是人事档案,调用协议是跨部门协作的标准接口,错误码是应急预案。把这些东西做扎实,Agent才真正算是一个可靠的执行者,而不是一本什么都会一点、但什么都不精的百科全书。

如果你也在做Agent相关的事情,我建议你哪怕不搞这么重的框架,也从目录结构、参数校验、调用日志这三件事做起。先把最容易翻车的地方守住,再逐步补上生命周期和协议规范。技能体系的搭建不是一天能完成的,但每多做一层,后面省下的排查时间都是成倍的。

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

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

立即咨询