Agent 技能化改造一直是个热门话题,但真正把 agent-skills 落到生产环境的人,大多经历过一段"工具调用一时爽,排错火葬场"的时期。我自己的项目从最早硬编码几十个 function call,到后来重构为完整的技能注册与编排体系,中间踩过的坑、推倒重来的设计,比写业务代码的时间多得多。这篇内容就当是给自己的项目做一次复盘,也希望能给正在做 Agent 技能层设计的各位一些参考。
先说结论:agent-skills 不是简单地把工具函数注册给模型就完事,它是一整套关于"如何让模型正确理解、选择、执行、编排能力"的工程方法论。
1. 为什么智能体需要"技能"而不是"工具函数"
1.1 从一次失败的天气查询说起
我最早做 Agent 原型时,参考网上大部分教程的做法——定义几个 JSON Schema,塞进 OpenAI 的functions参数。当时跑通了一个 Demo,能查天气、能设闹钟,感觉很顺利。但放到真实业务场景里,问题立刻炸开:
用户问:"明天去杭州出差,适合穿什么衣服?"
我注册的天气查询函数有两个参数:city和date。模型确实调用了这个函数,但传参是city="杭州"、date="2025-07-15"(假设今天 7 月 14 日)。看起来没错,可返回的温度是 22°C~28°C,模型最终回答"建议穿短袖"——杭州夏季室内外温差大、公共场所冷气足,一件单薄短袖其实不够。
问题不在模型,而在于我的"工具函数"只解决了"获取原始数据",没有解决"如何针对用户意图做更完整的推理"。这催生了我对技能的重新理解:工具函数是能力的最小原子,但不该是 Agent 暴露给模型的最小单元。
1.2 技能是"目标导向的行为封装"
我后来定义的技能,必须包含三个层次:
- 意图匹配层:这个技能解决什么问题、在什么场景下被唤起
- 参数与依赖层:需要什么输入、依赖哪些其他技能或数据源
- 执行与反馈层:如何执行、失败后的兜底方式、结果如何翻译成自然语言回答
举上面穿衣服的例子,我不该直接暴露"天气查询",而是暴露"穿衣建议"技能。这个技能内部封装两步:先调用天气数据源拿温度和降水概率,再结合季节、地域温差、用户出行场景做一层规则判断或模型二次推理,最后输出建议。
这就是工具函数和技能的差别:技能是为某个完整任务目标服务的,它可以内部串联多个基础能力,并且承担一小部分决策逻辑。
1.3 从模型视角理解技能的价值
如果你只从工程视角看,技能和工具函数的代码差异无非是多包了一层。但站在模型的角度,差别非常大。
模型的推理过程是逐 token 进行的,它的注意力是有限资源。当一个模型面对 30 个扁平注册的 function,它需要先从所有 function 里挑一个或几个,再组织参数。但如果你把 30 个函数归类、封装成 8 个技能,每个技能的描述是"什么情况下用它、它内部会做什么、最终返回什么形态的结果",模型的决策路径就从"30 选 1"简化成"8 选 1",准确率显然会提升。
更重要的一点:扁平工具列表经常导致模型把多个步骤的复杂任务拆解成连续多次工具调用,中间的上下文开销和错误风险都成倍增加。而技能化封装后,一个复杂任务变成一次调用,后续执行逻辑在引擎内部完成。这既减少了模型的推理负担,也降低了系统整体延迟和出错概率。
2. 技能的结构化表达:从自由文本到可校验的 Schema
2.1 技能描述是给模型看的"产品说明书"
技能化改造初期,我犯过一个典型错误:把技能描述写得很工程师化,里面全是技术术语,比如"调用 travel_advisor_service 获取目的地 рекомендации"。结果模型经常学不会在正确的时机使用技能。
后来我请教了一位做 NLP 的老前辈,他一句话点醒我:"你写的描述是给模型看的说明书,不是给代码看的技术文档。模型理解自然语言,你要用场景化语言描述,而不是接口文档语言。"
现在我的技能描述模板是这样的:
- 技能名字:简短、语义化,动词开头
- What(做什么):一句话说明技能产出什么结果
- When(何时用):两到三句话描述典型触发场景,覆盖常见用户表达
- When not(何时不用):明确排除易混淆但不应使用该技能的场景
- Input(输入需求):需要模型从对话中提取的关键实体/参数
- Output(输出约定):返回给用户的回答风格、结构
以我之前做的"会议纪要技能"为例,它的描述片段是:
技能名称: generate_meeting_minutes What: 根据会议的原始转写文本,生成结构化会议纪要,包含议题、决议、待办事项。 When: 用户说"把刚才的会议整理成纪要""帮我记一下这次讨论的重点""会议记录发我一份",或检测到会议结束后的整理诉求。 When not: 如果用户只是询问会议时间、会议地点,不要使用该技能。如果会议尚未发生,不应调用,应告知用户需要会后整理。 Input: 需要从对话或上下文中获取会议转写全文或关键片段;若缺失,应主动向用户索要。 Output: 严格按照 议题-讨论过程-决议-责任人-截止日期 五段式输出。这一段文字看起来简单,但对模型行为的影响非常大。改完描述后,会议纪要技能在盲测数据上的触发准确率从 72% 提升到 91%。原因是之前描述里写的是"提取并总结对话内容,生成 JSON 格式数据",模型理解的目标是"产出结构化数据",而不是"生成一份用户能直接阅读的纪要"。
2.2 参数设计的"最少必需"原则
技能参数设计遵循一条核心原则:能推断的绝不要求显式传入,能合并的绝不分拆。
很多 Agent 工程师是从 Django 写 CRUD 转过来的,习惯把参数设计成 RESTful 风格——id、type、status 各种字段一应俱全。但技能参数是给模型看的,模型需要从对话里抽取实体进行映射,参数越多,抽取出错率越高。
我设计参数时用了一个"三重问法":
- 这个参数会被执行器真的用到吗?如果只是作为透传值传到下一个工具,那说明技能拆分有问题
- 模型能稳定地从对话中抽取这个参数吗?抽不出来的参数,宁可设计成可选+兜底逻辑
- 参数能否合并成更高层的语义对象?比如"城市+日期"可以合并为"出行计划"对象,尽管后端会拆开用,但模型只需要理解一个输入
同样以"穿衣建议"技能为例,第一版我定义了city、date、occasion(场合)、activity(活动类型)四个参数。实际测试中,模型经常从"出差"里推断出occasion="商务"、activity="会议",但这两个字段后端根本用不上,它们的真实作用是帮助模型理解用户需求,然后由技能内部的规则模块根据季节数据做判断。所以最终版本我把参数削成两个:destination(目的地城市)和travel_date(出行日期),occasion和activity降级为可选提示项,不列入必传参数。
2.3 输出 Schema 的稳定性大于一切
技能的输出格式一旦被 Agent 主流程消费,就必须保持高度稳定。我在项目里统一使用 JSON 作为技能输出格式,每个技能的输出 Schema 都有严格的 JSON Schema 定义,并加上了版本号管理。
为什么这么较真?因为技能的输出会被上游的 Agent 编排器读取并决定下一步动作。如果某个技能今天输出{"status": "success", "data": ...},明天改成{"code": 0, "result": ...},编排器里的判断逻辑就全乱了。我在项目里吃过这个亏——一个第三方技能包升级后,把原有的orders字段名改成了order_list,导致整个订单同步流程静默失败了两天,直到用户投诉才发现。
现在我的做法是:
- 所有技能输出必须通过 JSON Schema 校验,不通过直接标记技能调用失败
- Schema 变更必须走版本升级流程,旧版本至少并行运行一个迭代周期
- 技能内部字段命名遵循统一规范:状态用
status,数据主体用data,错误信息用message,关联资源用resource_id
3. 技能注册与发现的工程实践
3.1 技能注册中心:一份集中式的技能清单
技能多了以后,最怕的就是"哪哪都有技能定义,但谁也说不清一共多少个"。
我第一版技能代码是散在项目各个目录里的,有的技能写在services/skill_weather.py,有的写在handlers/skill_calendar.py。结果到后期想盘点能力时,只能靠人肉 grep,还经常漏掉。
重构后,我加了一个技能注册中心——本质上是一份集中式的技能清单,包含:
skills: - name: generate_meeting_minutes version: 2.1.0 enabled: true owner: team_productivity entrypoint: skills.meeting_notes.execute schema_ref: schemas/meeting_minutes_output.json tags: [meeting, productivity, text_summary] timeout_ms: 8000 retry_count: 2这份清单的核心作用有三点:
- 技能入口统一:编排器只认注册中心里的
entrypoint,不关心技能代码在哪个目录 - 版本与归属清晰:每个技能有明确的负责人、版本号,出现问题能快速找到维护方
- 启停可控:线上如果某个技能异常,可以在注册中心直接
enabled: false摘除,不等发版
3.2 技能发现:Agent 主流程如何找到正确的技能
注册中心只是让技能"被管理起来",真正难的是让 Agent 主流程在运行时"找到该用的技能"。
我的方案是把技能发现分成两个阶段:
第一阶段:基于意图的粗筛
当用户请求进来,首先跑一个轻量的意图分类器,把请求映射到一至三个候选技能域。这一步我用的是 embedding 相似度 + 关键词规则的双路召回。Embedding 负责语义泛化(比如用户说"帮我总结一下上午开会聊的内容"能匹配到会议纪要技能),关键词规则负责精准触发(比如用户说"会议纪要"直接命中)。
粗筛的目的是把大技能库缩小到一个小候选集,降低后续精确匹配的干扰。
第二阶段:基于 LLM 的精确选择
对粗筛得到的候选技能,把它们的描述拼接起来,连同用户请求一起发给 LLM,让模型选出最合适的技能并填充参数。这一步我在 prompt 里做了严格的输出约束:
SYSTEM_PROMPT = """ 你是技能调度器,负责从候选技能中选择最合适的技能。 候选技能如下: {skills_schema} 用户请求如下: {user_request} 你必须仅输出 JSON,格式为: {"skill_name": "...", "parameters": {...}} 如果不确定用户意图,选择 skill_name 为 "fallback_ask_clarify"。 """这里有个非常关键的细节:给 LLM 的候选技能描述必须保留原始场景描述,不能只给技能名和参数列表。一旦精简了描述,模型的精确选择准确率会下降一截,因为它缺少了"When use / When not use"这类关键的决策上下文。
3.3 兜底策略:找不到技能时怎么办
技能发现系统不可能做到 100% 覆盖。用户可能提出超出技能库范围的请求,也可能是技能描述写得不够好导致模型没召回。
我的兜底策略分三级:
- 一级兜底:把请求交给一个通用的问答技能,由模型直接基于自身知识回答,同时明确告知用户"这项能力我没有专用的工具,以下回答基于通用知识"
- 二级兜底:主动向用户澄清需求,而不是强行瞎猜。这个策略对应上面的
fallback_ask_clarify技能 - 三级兜底:记录失败样本,定期人工分析。如果某类请求频繁走兜底分支,说明技能库里缺了一个本该有的技能,或者现有技能的描述覆盖不足
三级兜底里最容易被忽略的是"向用户澄清"。不少 Agent 产品发现模型选不出技能时,会让模型硬答,往往答得又空又错。宁可多问一句"您是想查天气还是想规划行程",让用户明确意图,也好过硬着头皮给错误答案。
4. 技能编排:从单个技能到复合任务
4.1 为什么要引入编排层
如果你的 Agent 只是做"回答天气""查询余额"这类单技能任务,确实不需要编排层。但真实业务里绝大多数请求都是复合的:
"帮我把下周的会议安排生成一份简报,发给所有参会人并同步到群里。"
这个请求里至少包含三件事:读取日历条目、生成简报内容、发送消息。如果把它们封装成一个巨大的"会议简报推送"技能,也不是不行,但技能的复用性会变得很差——"读取日历"本身就值得单独成为一个技能,它能服务"提醒我下午的会""帮我看看周五有没有空档"等更多场景。
所以我引入了一个独立的编排层,它根据用户复杂请求,生成一条技能调用链。这里的核心问题是:编排器怎么决定技能的先后顺序和数据流向?
4.2 编排策略:顺序执行、条件分支与并行
我目前实现的编排器支持三种技能执行策略:
顺序执行
技能 A 的输出作为技能 B 的输入,适合强依赖场景。比如"查询订单状态"技能输出订单号,"查询物流轨迹"技能需要使用这个订单号。数据流是线性的,执行顺序不能颠倒。
条件分支
根据某个技能的输出结果,决定下一步走哪条分支。比如"检查库存"技能返回in_stock: false,则走"生成缺货通知"分支,否则走"创建订单"分支。条件分支的逻辑放在编排器的流程定义里,而不写死在技能内部。
并行执行
多个技能互不依赖时,可以并发跑。比如"获取用户画像""获取本周消费记录""获取优惠券列表"三者没有依赖关系,可以同时发起。并行能显著降低整体响应时间,我实测一个三技能并行的场景,总耗时从串行的 2.8 秒降到了 1.1 秒。
编排器的流程定义我是用 YAML 描述的,结构大致如下:
workflow: meeting_brief_and_notify steps: - id: fetch_calendar skill: get_calendar_events params: time_range: "next_week" - id: generate_brief skill: generate_meeting_minutes params: source: "{{steps.fetch_calendar.result}}" depends_on: [fetch_calendar] - id: notify_group skill: send_group_message params: content: "{{steps.generate_brief.result}}" group_id: "{{context.param.group_id}}" depends_on: [generate_brief]为了让编排器能正确填充参数,每个技能暴露的输入输出接口必须遵守统一协议。技能之间的数据传递,我统一用 JSON path 表达式引用,避免写一坨get_result()[0]["data"]["items"]这种脆弱代码。
4.3 人工编排 vs 模型自动编排
这里想聊一个偏思辨但工程上很重要的问题:编排流程到底应该人肉写在 YAML 里,还是让模型在运行时自动生成调用链?
我的测试结论是:对稳定性要求高的生产场景,优先人工编排;对探索性、开放性的场景,可以让模型自动编排,但必须有护栏。
人工编排的好处是流程可控、错误可预期、问题可复现。我把常用的 20 条流程预先定义好,覆盖公司业务的 80% 场景。剩下 20% 长尾场景,我让模型自由编排,但设置了两个硬性限制:
- 技能调用次数上限:一次自由编排最多调用 5 个技能,超过视为失败,转入人工兜底通道
- 敏感操作白名单:涉及对外发送消息、修改数据、删除资源的技能,即使被编排器选中,也必须经过二次确认
有人可能觉得人工编排 YAML 太重,不如全让模型来。我理解这个倾向,但真实生产里,你无法接受模型随机把两个不相干技能串在一起还自信满满地告诉用户"已完成"。
4.4 编排失败的回滚机制
技能编排执行到一半,某个技能失败了,是整个流程直接放弃,还是尝试恢复?
我目前的策略是分情况处理:
- 可重试型:网络超时、服务临时不可用,直接重试该技能,最多重试两次
- 可降级型:如生成简报技能失败,但原始会议记录已拿到,可以直接把原文附上发给用户,并注明"简报生成为降级输出"
- 不可恢复型:如关键技能 (比如用户身份鉴权) 失败,立即终止流程,向用户说明失败原因,并保证不留下半截脏数据
回滚逻辑中最麻烦的是"副作用已产生"的场景。比如"发送消息"技能已经发出去了,但后面的"生成简报"技能失败了,用户收到消息但没有简报。我在实际项目里的处理是:这不属于技术回滚,而需要做业务补偿——在失败分支里调用一个"发送补充说明"技能,告知用户"简报生成失败,以下是关键内容摘要,完整版稍后补发"。
5. 技能化改造的实测心得与避坑指南
5.1 参数幻觉:模型自信地传错了值
技能化改造上线后,我观察到一个高频问题:模型有时候会"脑补"参数值。用户只说"帮我查下上海的天气",模型会把date参数填充为当前日期——这个还算合理。但有时用户说"告诉我要不要带伞",模型居然给city填了"深圳",因为上下文里早前出现过"在深圳出差"的表述。
这类错误排查起来特别费劲,因为参数值是合法的,技能也正确执行了,就是结果不符合用户隐含预期。
我的应对之策是引入"参数确认"机制:当技能检测到关键参数不是用户显式提供,而是模型推断得到的,执行前主动向用户确认一次。这个机制只对高成本或高风险技能开启——比如"订机票""发起支付""给外部系统发通知"——低风险的查询类技能不做确认,以免过度打扰用户。
5.2 技能描述更新后要重新跑回归测试
技能描述是对模型行为影响最大的单一因素,没有之一。我在项目里每次改技能描述,都会重新跑一遍该技能的回归测试集——事先准备 100 条典型用户请求,统计技能触发准确率、参数抽取完整率、输出格式合法率。
有次我把"生成会议纪要"技能描述里加了一句"如果会议时长低于 5 分钟,直接返回一句话摘要",结果触发准确率从 91% 掉到 84%。原因是模型对"低于 5 分钟"这个条件产生了困惑,开始在"是否为会议场景"的判断上犹豫。回滚描述后准确率恢复。
这个教训让我建立了严格的管理流程:技能描述变更必须走 review + 回归测试,不能因为改几个字就随手提交上线。
准确率对比表格(摘录自内部测试集):
| 技能 | 描述版本 | 触发准确率 | 参数完整率 | 输出合法率 |
|---|---|---|---|---|
| 生成会议纪要 | v1(接口文档风格) | 72% | 65% | 88% |
| 生成会议纪要 | v2(场景化描述) | 91% | 83% | 96% |
| 生成会议纪要 | v3(新增时长分支) | 84% | 79% | 93% |
| 穿衣建议 | v1(四参数全必传) | 68% | 55% | 91% |
| 穿衣建议 | v2(最少必传参数) | 87% | 81% | 95% |
5.3 技能之间的隐性冲突
技能库到了一定规模,会出现"隐性冲突"。两个技能描述里对同一个场景都有"何时使用"的描述,模型就会陷入两难。
我在项目里遇到过一个真实案例:"查询附近的餐厅"和"规划周末行程"两个技能在"用户问周末去哪吃"这个请求上发生了冲突。前者认为应该找出餐厅,后者认为应该输出完整行程。模型时而选前者,时而选后者,结果完全不一致。
这个问题的解法是回归到技能描述层面:在"查询附近的餐厅"的When not use里明确写上"如果用户意图是规划包含餐厅在内的完整行程,应使用规划周末行程技能而非本技能";同时在"规划周末行程"技能描述里增加"如果用户只关注餐饮,优先返回餐厅推荐而不需完整行程"。
技能冲突排查起来很隐蔽,因为单个技能测试都是通过的,只有在组合场景里才暴露。我现在每两周做一次跨技能的路由冲突分析,用一个专门的数据集覆盖常见易混淆场景,防止某个技能改描述后悄悄"越界"。
5.4 线上监控:技能层的可观测性设计
技能化改造后,我有一个很深的感触:技能层必须有自己的可观测性体系,不能只依赖框架层面的日志。
目前我在每个技能入口和出口各埋一个关键点,记录四类数据:
- 调用链路:从用户请求到技能选型到执行到反馈,完整 trace
- 参数快照:模型抽取后的参数原样记录(包含数据脱敏后的版本),用于事后回溯参数幻觉问题
- 选型置信度:模型/排序器给出的候选技能打分,看低分被选中是不是高失误率来源
- 执行耗时分位数:每个技能的 P50/P95/P99 耗时,敏感技能单独告警
有了这些数据,再遇到线上问题,我能很快定位是"技能没被选上""参数抽错了""技能执行报错""反馈格式异常"这四种情况中的哪一种,排查效率大幅提升。
举一个具体例子:有段时间用户反馈"设置提醒不生效"。从链路 trace 上看,用户请求成功触发了"创建提醒"技能,参数也正常,但最后的"反馈话术"误导了用户——技能执行成功,但模型回答"已为你设置提醒",实际上提醒的触发时间被技能内部的时区转换逻辑调整成了 UTC 时间,和用户理解的中国时间差了 8 小时,导致提醒晚了两小时才触发。这种问题不加链路监控根本查不出来。
6. 未来的扩展方向:技能的市场化与跨 Agent 共享
沿着 agent-skills 这条路继续走,我目前比较看好的方向是跨 Agent、跨组织的技能共享。每个团队积累的技能资产,完全可以像"能力插件"一样被其他 Agent 实例动态加载。前提是技能定义标准化做到位——Schema 版本、描述规范、输入输出协议、安全边界全部对齐。
我自己的项目已经做了一个轻量版的内部技能市场,团队里任何人贡献的新技能经过 review 后,可以上架共享。其他项目组的 Agent 可以通过技能清单发现并按其声明的接口进行调用,互相之间不需要共享业务代码,只通过技能注册中心与执行协议交互。这个模式跑通后,各项目组的重复建设明显减少,新的业务场景往往有一半以上的能力可以直接从技能市场里"点菜"。
当然,跨 Agent 共享的技能涉及数据权限隔离问题,不是简单把技能注册中心公开就能做好的。我的下一步工作是给技能市场接入统一的权限网关,让技能在执行用户请求时,能够识别调用方身份并做细粒度数据权限控制。
回到最开始的结论:agent-skills 做得好不好,核心看三点——技能描述是否让模型明确决策、参数设计是否减轻模型负担、编排层是否让复合任务稳定执行。这三点我都有过实际教训,对应地给出了具体设计和优化方法,希望能给同行们省下一些试错成本。技能层的架构本身不复杂,复杂的全在细节里。