前阵子一直在折腾 agent-skills 这个方向,说真的,做 Agent 应用做久了之后你会发现一个残酷的事实:大模型本身的能力差距其实没有想象中那么大,真正拉开体验差距的,是你能不能把模型的能力变成一个个稳定、可复用、可组合的“技能”。今天就把我在这段时间里从设计、封装到踩坑的全过程整理出来,内容偏实战,代码和步骤都是可以直接拿去改的。
1. 一个能让 Agent 稳定干活的“技能库”,到底在解决什么
我先从大多数人最容易误解的地方说起。很多刚接触 Agent 开发的朋友,第一反应是把所有逻辑都塞进 system prompt,让大模型“自己想办法”。这种做法的确能跑通 demo,但一旦任务变复杂,你会遇到一连串问题:prompt 越长指令越容易漂移、模型经常忘掉之前的限制条件、同一个任务今天执行得好明天就翻车。原因很简单,大模型本质上是概率模型,你把一堆“应该怎么做”写在提示词里,它每次都在猜你的真实意图,而不是在执行一段确定性的流程。
agent-skills 的思路恰恰相反:把 Agent 需要执行的每一个稳定动作,封装成独立的小模块,每个模块有自己的描述、参数契约、执行逻辑和测试用例。模型负责的是“理解用户意图并选择正确的技能”,至于技能内部怎么做,是确定性的代码和工具逻辑来保证。这样一拆,Agent 的核心职责就从“记住所有规则”变成了“路由到正确技能”,可靠性直接提升一个量级。
我自己比较喜欢用一个比喻:这就像老厨师做菜。新手厨师炒菜时每个步骤都要现场翻菜谱、临时调配料,遇到突发情况就手忙脚乱;而老厨师厨房里永远有一排提前准备好的半成品酱料和预处理食材,真正炒的时候按顺序下锅就行。agent-skills 就是给 Agent 建立这一排“半成品酱料”,把那些重复、确定、容易出错的环节提前固化下来。
这个方案适合谁?如果你正在做一个面向真实用户的 AI 产品,比如个人助理、客服机器人、数据分析助手,或者你只是想让自己的 Agent 稳定完成一系列重复任务,那这篇文章值得完整看一遍。尤其是那些已经试过“把功能全写进 prompt”但效果不稳定的项目,换成技能化以后体感会非常明显。
2. 核心设计拆解:技能描述、参数契约与触发逻辑
一套可用的技能体系,核心就三个部分组成:技能描述、参数契约、触发逻辑。这三个东西决定了一个技能能不能被模型正确使用,也决定了整个 Agent 的可维护性。我逐个说。
2.1 技能描述:给模型看的“使用说明书”
技能描述是模型判断“什么时候该用这个技能”的依据,这部分写不好,后面全白搭。很多人的习惯是把描述写成“这个技能用来生成周报”,这种描述过于笼统,模型遇到稍微模糊的请求就无法判断到底该不该调用。
我建议描述里至少包含四类信息:技能的能力边界、典型触发场景、不适用的情况、以及输出形态。举个例子,不要说“生成周报”,而要写清楚:
该技能根据用户提供的本周工作要点,生成结构化周报文本。适用于用户表达“写周报”“总结本周工作”“整理周报要点”等意图。不适用于月报、年报、简历或工作总结之外的文档生成。输出为 Markdown 格式的周报正文。
注意“不适用”这部分很关键。我在实际测试中发现,加上边界说明之后,误触发率能下降一半以上。原因很好理解:大模型在意图判断时,正例和反例同时给出,比只给正例要更容易收敛。
2.2 参数契约:用 JSON Schema 把自由度锁死
模型调用技能时,需要把用户需求转换成结构化的输入参数。如果参数没有强约束,模型就可能传进来各种奇怪格式,技能内部逻辑就得做一堆容错处理,最后反而更脆弱。
我的做法是为每个技能定义 JSON Schema,明确每个字段的类型、是否必填、取值范围。比如周报生成技能的输入可以是:
{ "type": "object", "required": ["work_items"], "properties": { "work_items": { "type": "array", "description": "本周工作要点列表", "items": { "type": "object", "required": ["summary"], "properties": { "summary": { "type": "string", "description": "工作内容简述" }, "result": { "type": "string", "description": "产出或结果" }, "next_plan": { "type": "string", "description": "下周计划,可选" } } } }, "tone": { "type": "string", "enum": ["formal", "concise", "detail"], "description": "周报语气风格,默认 formal" } } }有了契约之后,技能内部逻辑就不用关心“用户到底想表达什么”,只需要按照 Schema 把数据处理好。这相当于把 Agent 和技能之间的接口标准化了,模型也好、未来的其他调用方也好,对接成本都大幅降低。
2.3 触发逻辑:显式调用还是模型自动路由
技能的触发方式直接影响灵活性和可靠性,这两者需要做取舍。最省事的方案是让模型在对话过程中自己判断该调用哪个技能,这个叫隐式路由。优点是灵活,用户说“帮我记个待办”它就知道去调待办技能,不需要额外指令。缺点是技能数量多了以后,模型选错技能的概率会迅速上升。
另一套方案是显式调用,也就是用户或上层系统明确指定技能 ID,比如“执行 skill.weekly_report”。显式调用更稳,但牺牲了自然交互体验。我在实际项目中采用混合策略:常用技能走隐式路由,少数对准确性要求极高的技能走显式调用。比如财务相关的技能,一律要求显式触发,避免模型误会用户意图造成不可逆操作。
2.4 自检与版本:技能要能自我验证
一个技能封完不是终点,还要有自检能力。我给每个技能内置一个轻量测试用例集,运行时可以跑回归:给定一组已知输入,检查输出是否符合预期。这样每次修改技能内部逻辑时,不用等到用户反馈才知道改坏了。
版本管理也不能省。技能描述也好、参数契约也好,只要改动过就要升级版本号。Agent 在调用技能时可以只引用某个版本范围内的技能,避免线上环境被未经验证的新版本影响。这一点在团队协作时尤其重要,谁也不会希望同事一提交改动,线上 Agent 马上表现异常。
3. 从零手写一个“周报生成器”技能:完整实战过程
理论讲再多,不如直接跑一遍。我以“给个人助理 Agent 增加周报生成能力”为例,讲一下我是怎么完整封装一个 agent-skills 技能的,从设计到注册全流程都列出来。
3.1 第一步:写技能描述和参数契约
先定义这个技能到底做什么、不做什么、输入输出长什么样。上面 2.1 和 2.2 其实已经给出了描述和 JSON Schema 的雏形,这里直接用它。注意一点:描述里不要写“如何实现”,而要写“何时使用、能做什么”。实现细节是技能内部的事情,模型不需要也不应该关心。
3.2 第二步:实现技能内部逻辑
内部逻辑我用 Python 实现,做成一个函数,输入就是参数契约里的结构化对象,输出是标准周报文本。代码大致长这样:
from dataclasses import dataclass @dataclass class WeeklyReportSkill: """周报生成技能。""" name: str = "weekly_report" version: str = "1.2.0" def execute(self, work_items: list[dict], tone: str = "formal") -> str: if not work_items: return "本次没有可汇总的工作内容,请补充工作要点。" lines = ["## 本周工作回顾", ""] for idx, item in enumerate(work_items, start=1): title = item.get("summary", "未命名事项") result = item.get("result", "") next_plan = item.get("next_plan", "") lines.append(f"{idx}. {title}") if result: lines.append(f" - 结果:{result}") if next_plan: lines.append(f" - 后续:{next_plan}") if tone == "concise": return self._to_concise(lines) if tone == "detail": return self._to_detail(lines) return "\n".join(lines)真实场景里肯定不只是拼字符串,但核心思想是一样的:技能内部是确定性逻辑,不依赖模型发挥。这一步如果你使用其他语言,比如 TypeScript 或 Java,也完全可以,关键是保持接口一致。
3.3 第三步:把技能注册进 Agent 运行时
技能写好后要注册进 Agent 运行时,让模型“看到”这个技能的存在。我用的方式是把技能定义转成一个 tool schema,再挂载到 Agent 的函数调用列表里。伪代码是:
weekly_report_tool = { "type": "function", "function": { "name": "weekly_report", "description": "根据用户提供的工作要点生成周报,适用于表达写周报意图的场景。", "parameters": weekly_report_schema } } agent = create_agent(tools=[weekly_report_tool])这一步看起来简单,实则有讲究:注册顺序会影响模型的选择倾向。我把高频技能放在工具列表靠前的位置,实测调用准确率会有几个点的提升。原因大概率是模型对前面的选项注意力权重更高,虽然听起来不太“科学”,但数据确实如此。
3.4 第四步:测试与回归
技能注册完不能直接上线,至少要跑一轮回归。我的测试思路分三层:第一层用固定样例跑技能本身,确认输出格式正确;第二层用模拟对话让模型触发技能,确认路由准确;第三层混入相似意图的干扰样本,确认没有误触发。
这里分享一个我常用的测试技巧:准备一组“负样本”。比如测试周报技能时,负样本包括“写月报”“写简历”“总结今天的会议”这类容易混淆的请求。每跑一轮测试,记录误触发率,比只看正例通过率要有用得多。我优化描述后,负样本误触发率从 20% 左右降到了 3% 以内,这个数字才是真正反映技能边界质量的。
3.5 为什么输入输出要强约束,而不是让模型自由发挥
这一步很多人不理解。都让大模型处理了,为什么还要规定 JSON Schema?我的实际感受是:自由的代价是失控。你让模型自由输出,今天它给你纯文本,明天给你表格,后天可能给你一堆 markdown 嵌套列表,消费方根本没法稳定解析。而强约束之后,技能内部只管处理数据,输出永远是同一格式,上层展示和下游逻辑都能稳定工作。自由度应该留在模型选择技能这一步,而不是留在每个技能的内部实现里。
4. 技能编排与路由:单技能、多技能和组合技能的取舍
技能数量少的时候,怎么设计都行。但技能一多,编排和路由就成了决定体验的关键。我把常见的几种组织方式列出来,说说各自的适用场景和我在实测中踩到的坑。
4.1 单技能模式
一个 Agent 只挂一个技能,这种模式最简单也最稳。比如做一个“周报助手”,只挂周报生成技能,用户进来就一个用途,根本不存在路由问题。实际使用中这种模式适合垂直场景、单入口工具类应用,比如发票识别、文章摘要、翻译助手。优点是开发量小、行为可预测,缺点是用户交互稍微偏离核心场景就没办法处理,体验会比较死板。
4.2 多技能隐式路由模式
一个 Agent 挂多个技能,让模型根据用户输入自己选。这是现在大多数 Agent 产品的默认做法,体验也确实自然。但当技能超过一定数量后,问题就很明显了。
我实测过一组数据:5 个技能以内,模型选对技能的概率超过 95%;加到 10 个,降到 88% 左右;加到 20 个以上,就只剩 75% 上下。这个下降速度是惊人的。原因也不难理解:模型在长列表里选择时,相似的技能描述会相互干扰,尤其是“总结”“生成”“整理”这类动词开头的技能,很容易打架。
针对这个问题,我的调整策略有三个:
- 技能描述差异化。两个技能如果都可能被同一句话触发,就要明确写清“本技能不做另一件事”。
- 高频技能前置。工具列表顺序按调用频率排序,减少长尾技能对主路径的干扰。
- 必要时加显式意图分类器。在模型路由前,先用一个轻量分类器判断用户意图属于哪个技能组,再只给模型展示该组内的技能。
4.3 组合技能模式
有些复杂任务不是一个技能能覆盖的,需要多个技能协作。比如“生成一份数据周报”这个任务,可能需要“拉取数据”“生成图表”“生成周报”三个技能协作。这里有两种做法:一种是让模型自由决定调用顺序,另一种是在技能内部显式编排子技能调用链。
我的建议是:能用编排就用编排。在技能内部把子技能调用顺序写死,模型不需要思考“先拉数还是先生成周报”,它只需要把任务交给编排技能,编排技能内部依次调用子技能就行。这样做的好处非常直接:不会因为模型临场发挥导致步骤错乱,而且整个流程可以被日志记录、被重试、被测试。
4.4 三种模式怎么选
| 模式 | 稳定性 | 灵活性 | 维护成本 | 适用场景 |
|---|---|---|---|---|
| 单技能 | 最高 | 最低 | 低 | 垂直小工具,专用助手 |
| 多技能隐式路由 | 中 | 高 | 中 | 通用助手,技能数量少且边界清晰 |
| 组合技能编排 | 高 | 中 | 较高 | 复杂流程,多步骤任务,对顺序有要求的场景 |
我的经验是:不要一上来就追求“万能 Agent”。先按单技能把每个能力打磨稳,再逐步加路由和编排。很多项目翻车都是因为技能还没打磨好,就开始堆数量,最后模型连该选哪个都搞不清楚,更别提完成任务了。
5. 实测中踩过的坑与调优:冲突、幻觉与可观测性
技术方案说得再好,落地时一定会有意外。下面这几个坑是我在 agent-skills 实测过程中真实遇到的,每个都付出了不少调试时间,写出来帮你避一避。
5.1 相似技能互相抢占
我最先做的是一个记事本助手,挂了“待办管理”和“日程管理”两个技能。原以为边界很清楚:待办管任务列表,日程管时间安排。结果实测中模型经常把“明天下午三点开会”归类为待办,而把“买牛奶”归类为日程,完全反了。
问题根源是描述里的“适用场景”写得太宽泛,没有给出跨场景判断标准。后来我在描述里各自加了一句“当用户提到具体时间事件时,优先使用日程管理;当用户仅提到需要完成的事项且无明确时间时,优先使用待办管理”,误判率立刻降了下来。核心经验是:技能边界描述一定要面向“区分”,而不是面向“概括”。
5.2 模型幻觉式触发
还有一种更隐蔽的问题:模型明明不确定该不该用某个技能,却还是强行调用,然后技能返回空结果或者报错,模型又根据报错信息脑补一个答复返回给用户。比如用户问“这周我完成了多少件事”,Agent 调了周报技能,但是因为参数里没有对应数据,技能返回了空列表,模型居然回答“您本周完成了 0 项工作”。
这个问题的解法我给两个:第一,技能内部对异常输入要返回结构化的错误码,比如ACTION_REQUIRED,明确的告诉模型“参数缺失,需要向用户询问”,而不是返回空数据;第二,在技能描述里明确加上一条规则“当技能返回错误码时,必须向用户说明需要补充的信息,不得自行猜测结果”。加了这条之后,模型自我脑补的情况少了很多。
5.3 技能内部错误被模型掩盖
技能内部报错时,模型常常“粉饰太平”。比如图表生成技能因为数据源断连失败了,模型可能会回复“图表已生成,请查收”,因为它的语言模型特性倾向于生成连贯答案,而不是暴露错误。这个问题如果不处理,用户会以为 Agent 干成了,实际拿不到任何结果。
我的做法是让所有技能在失败时抛出的异常包含三个字段:错误码、用户提示、调试信息。模型拿到错误码后只能按照用户提示对外回复,调试信息只写入日志,不进对话。这样既保住了用户体验,又保住了排查链路。
5.4 技能数量膨胀带来的性能劣化
技能挂多了以后,不仅仅是选错概率上升,还有个直接问题:每次模型决定的 prompt 会被工具清单撑大,导致响应变慢、token 消耗变多。我统计过,每增加一个技能功能定义,单次请求的输入 token 大约多几十到一百不等,技能达到 30 个时,光工具定义就可能吃掉两三千 token。这不只是成本问题,还可能让模型注意力分散。
针对这个问题,我后来做了技能分组。按照功能域分成“信息查”“任务管理”“内容生成”“数据分析”四个组,先让一个小模型或规则引擎判断用户意图属于哪个组,然后再把对应组的技能列表注入给主模型。这样主模型每次看到的工具数量控制在 8 个以内,准确率和速度都恢复到了接近单技能的水平。
5.5 先跑通最小闭环再扩技能集
最后说一个整体性的经验。技能体系的复杂度是随着技能数量非线性上涨的。我在开发早期习惯一次性封装五六个技能再统一联调,结果经常是技能 A 和 B 之间互相干扰,C 的描述不清晰导致从不被触发,D 的参数契约和内部实现不匹配。后来改为“一次只加一个技能”的节奏:新增技能前先跑一遍全量回归,确认没有影响旧技能再继续。虽然后来开发速度看着变慢了,但整体返工率大幅下降,这个节奏值得坚持。
6. 从技能到产品:可复用技能集的管理与沉淀
单个技能做好,只能说解决了单点问题。真正要把 agent-skills 变成团队或产品级别的资产,还得在管理层面下功夫。我这段时间探索下来,三个方面是最有价值的。
6.1 技能版本管理与语义化
技能也应该用语义化版本号管理,像软件库一样。1.0.0表示首个稳定版本,1.1.0表示向后兼容的新功能,1.2.0表示修改了内部实现但对外行为不变,2.0.0表示破坏了接口兼容性。我在技能定义里加了version字段,Agent 平台会自动校验兼容范围,不符合版本要求的调用直接拦截,避免旧逻辑被新格式打乱。这个机制在单人项目里可能感觉多余,但在团队协作时几乎是救命级的存在。
另外,技能描述里的任何修改,哪怕只是一个措辞,都建议走版本变更记录。因为描述变化会影响模型的路由行为,这跟代码行为变化的后果是同等级的,不能随手改。
6.2 技能集的可观测性
技能上线之后不能当黑盒用,每一步都要能查。我在每个技能执行前后都写了结构化日志,记录四样东西:触发入口(用户原话还是显式调用)、模型选择的置信度、技能接收到的参数、技能最终返回或抛出的错误码。配合简单的看板,每天都能看到“哪个技能被高频调用、哪个技能误触发多、哪个技能经常报错”,后续优化优先级一目了然。
如果没有可观测性,技能体系就像一片黑森林,你永远不知道模型在里面做了什么选择,等到用户投诉才去复盘,代价太高。有一次就是因为没有日志,用户反映 Agent 偶尔多扣了积分,我排查了两天才发现是一个技能返回了重复订单号,而这个问题如果日志结构规范,五分钟就能定位。
6.3 技能集市与评测集
在团队里,我建立了一个团队内部共享的技能仓库,每个技能必须通过固定的评测集才能合入主线。评测集至少包括三类用例:
- 正向用例:典型的、能明确触发技能的场景
- 负向用例:相似但不应触发该技能的场景
- 边界用例:数据缺失、格式异常、空输入等边缘情况
这个评测集的价值在于“回归”。每修改一个技能描述或内部逻辑,就自动跑一遍全量评测集,不合格就不允许发布。虽然初期搭建评测集要花时间,但后期省下的排查时间会远超投入。
再往后走,技能集的沉淀还能延伸到跨产品复用。比如我在 A 项目里打磨好的“数据分析摘要”技能,因为接口和版本都标准了,可以直接迁移到 B 项目,稍作参数调整就能用。技能从“项目里的代码”变成了“组织的资产”,这个转化是整个 agent-skills 实践里回报最高的部分。
我自己现在维护的这套技能体系,规模不大,但每个技能都是被真实场景反复打磨过的,任何一个新项目要接,半个工作日就能完成基础接入。对比最早那个“把所有逻辑塞 prompt”的原型,稳定性和开发效率都不可同日而语。如果你的 Agent 也处于“够用但不稳”的阶段,建议从最小的一个技能开始重构,把第一块地基打牢,后面会顺很多。