agent-skills 这个词最近频繁出现在各类智能体讨论里。第一次看到时可能觉得就是个普通项目名,其实背后的思想是把智能体拆成“会干一件明确事情的技能集合”。我在这块摸爬滚打了一段时间,发现很多团队卡住的地方不是模型能力不够,而是技能定义、注册、调度那一套工程细节没理顺。这篇内容想以 agent-skills 为主线,把技能体系从设计到落地的关键点完整拆一遍,适合正在做智能体平台、想要给 Agent 接入工具能力、或者被“工具调用不稳定”折磨过的开发者参考。
我最早接触这个问题,是因为一个内部项目需要让 Agent 自动处理日常报表汇总。最开始把所有操作说明都堆在 system prompt 里,功能越加越多,最后模型经常混淆、漏参数、甚至调用错函数。后来才意识到,真正该做的是把每个可复用的操作封装成独立单元,有名字、有描述、有输入输出约束,让模型像人类查阅工具手册一样去选。这个思路后来被我们收拢成了内部一个叫 agent-skills 的实践框架。下面所有内容,都是围绕这条主线展开的经验总结,不是理论推演,都是真刀真枪趟出来的。
1. 先把概念理顺:Agent Skill 到底是什么
1.1 一次真实场景里的“技能”长什么样
先看一个非常具体的场景:用户对 Agent 说“帮我汇总这周的日报,标出风险较大的几条”。如果按照传统方式,你会在 Prompt 里写一大堆“你是一个助手,你可以读取日报,你需要理解日报内容,然后总结……”最终模型能不能正确执行,完全靠运气。
换成 agent-skills 的思维,这个任务会被拆成几个独立的技能:
- list_daily_reports:获取指定日期范围内的日报列表。
- get_report_content:根据报告 ID 获取正文内容。
- extract_risk_items:从文本中抽取疑似风险语句。
- generate_summary:把汇总结果组织成结构化文本。
每个技能都像一个小工具,有自己的名称、功能描述、参数约束、执行逻辑。Agent 要做的事情不是“凭空理解”,而是先看有哪些技能可用,再根据用户的请求决定调用哪些、按什么顺序调。模型在这里扮演的更像调度中枢,真正干活的是技能。
这个转变很关键。它把“大而全的智能体”变成“小而专的技能库”。你可以为一个技能写单独的 prompt、单独的单元测试、单独的版本更新,甚至让不同团队各自维护自己的技能模块。这是 agent-skills 最核心的价值:让 Agent 的能力增长从改一个大 Prompt,变成新增一个可插拔模块。
1.2 为什么把能力拆成技能,而不是直接写进主流程
很多人第一反应是:直接给 Agent 传几十个函数定义不就行了?传统 function calling 确实也是这么做的,但问题在于,把能力定义和主流程混在一起会导致几个明显的副作用。
首先是 Prompt 膨胀问题。每个技能如果都有长描述、几十个参数、复杂的枚举值,把这些全塞进单次请求里,模型在理解和选择时会越来越失真。实测下来,工具定义超过一定数量后,模型会开始混淆相似功能的技能,甚至出现参数串场。把技能做成独立存储、按需加载,就能避免这种“广告太多,用户不知道选谁”的问题。
其次是可测试性。所有技能写死在主流程里,意味着每次改动都要回归整个 Agent 行为,风险极高。而独立技能可以针对“给定这个输入,技能应该返回什么结构”做专门测试。技能单元通过后,再放到集成环境里验证调度,问题的边界清晰很多。
第三是复用性。同一个“读取日报”技能,既可以被“日报汇总 Agent”用,也可以被“风险预警 Agent”用。如果不拆出来,你就要在两个 Agent 里各写一份重复逻辑。而 agent-skills 模式下,一个技能注册一次,多个 Agent 共享,维护成本直线下降。
把技能拆出来,表面上是工程结构更清晰了,本质上是让 Agent 具备了一种“可编排的肌肉记忆”。技能是稳定的“肌肉”,模型是负责调度的大脑。大脑不需要记住每个动作的细节,只需要知道有哪个动作可以用。
2. 给技能建一套标准:定义、输入、输出
2.1 一份可执行的技能定义长什么样
想要模型正确使用技能,光有函数实现不够,还得给函数一份“说明书”。在 agent-skills 体系里,这份说明书通常用 JSON Schema 描述。我的习惯是每个技能都包含四部分:名称、描述、参数、执行入口。
下面给一个最小可用的技能定义,以“查询天气”为例:
{ "name": "query_weather", "description": "查询指定城市、指定日期的天气情况,日期格式为 YYYY-MM-DD,支持未来 7 天。", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如 北京、上海、广州", "enum": ["北京", "上海", "广州"] }, "date": { "type": "string", "description": "查询日期,格式 YYYY-MM-DD" } }, "required": ["city", "date"] }, "execution": { "type": "local_function", "entry": "skills.weather.query_weather" } }这份定义里,name 和 description 是给模型看的,参数是模型需要生成的,execution 是给执行引擎用的。我见过很多团队把注意力全放在 execution 上,忽略了 description 的措辞。实际上 description 写得好不好,直接影响模型选不选这个技能、参数填得对不对。描述要写清楚“做什么、约束是什么、边界是什么”,不要写花哨的修辞。
参数部分尽量限制 enum 或格式,模型在输出 JSON 时越容易对齐规范。自由文本参数很容易被模型脑补出奇怪的取值。日期、城市这种常见字段,一定要写清楚格式,最好在 description 里给一个例子。
2.2 参数与上下文的设计原则
参数设计是整个技能体系里最容易翻车的地方。我总结出三个原则:少而明确、类型简单、必有默认。
少而明确是指一个技能的参数最好不要超过 5 个。参数越多,模型越容易漏填、错填。如果发现一个技能需要十来个参数才能干活,说明这个技能太粗了,应该拆成多个更细的技能,或者把一部分参数收纳进上下文。
类型简单是指尽量只用 string、number、boolean 和一层嵌套的 object。不要输出深层嵌套结构,因为模型一旦在深层字段上出错,你很难从日志里快速定位问题。
必有默认是指给每个可选参数都写默认值,而且默认值要符合真实业务场景。比如查询天气时,date 不填默认今天,city 不填默认用户所在城市。这样既降低模型生成参数的难度,也能减少执行失败的概率。
除了用户显式指定的参数,技能还需要一些“隐藏信息”。比如查询用户日报时,技能内部需要知道当前用户是谁、当前团队是什么、数据源的权限范围。这些信息不应该由模型提供,而应该由运行时通过 context 注入。
我在框架里专门实现了一个 context 对象,包含 user_id、tenant_id、timezone、request_id 等基础字段。技能执行时直接从 context 读取,不需要模型参与。这样做的最大好处是安全可控,模型永远拿不到它不该知道的数据,自然也不会把内部 ID 当作参数输出。
3. 技能生命周期管理:注册、发现、装载、更新
3.1 注册与发现机制
技能如果没有一套注册发现机制,很快就会变成一锅粥。agent-skills 的实践是:所有技能启动时统一注册到一个技能中心,运行时按需装载,而不是在进程启动时一次性加载全部技能。
我用过两种注册方式,各有优点。第一种是声明式注册,每个技能目录下放一个 manifest.yaml 文件,启动时扫描目录并加载。这个方式适合技能数量多、归属不同团队的项目。
name: query_weather description: 查询指定城市、指定日期的天气情况 version: 1.2.0 parameters: - name: city type: string required: true - name: date type: string required: false entry: skills.weather.query_weather第二种是装饰器注册,适合技能都写在同一个代码仓库里的情况:
@skill.register( name="query_weather", description="查询指定城市、指定日期的天气情况,日期格式为 YYYY-MM-DD", parameters={ "city": {"type": "string", "required": True}, "date": {"type": "string", "required": False} } ) def query_weather(city: str, date: str = None): # 执行真实查询逻辑 return {"city": city, "weather": "晴", "temperature": 25}无论用哪种方式,技能中心都要维护一份“技能目录”。模型每次规划时,不是直接看到所有技能,而是先通过检索机制把候选技能选出来。这个检索可以用关键词、向量相似度,或者基于用户当前任务的分类器。技能数量少的时候可以全量给模型,但一旦超过一定规模,就得上召回 + 粗排那套逻辑。
3.2 技能的热更新与版本管理
技能不是写完就永远不变的。业务逻辑变了、参数格式变了、底层接口换了,都需要更新。我强烈建议从第一天就做好版本管理,否则线上 Agent 可能今天正常、明天就莫名其妙出错。
每个技能定义里都加一个 version 字段,注册中心按照版本路由。新版本先在小流量里跑,确认稳定后再放量。如果参数结构变了,一定要做兼容层,让旧版本请求也能映射到新逻辑。不能假设所有调用方都会立刻升级。
热更新方面,不建议直接修改线上技能源代码。更稳妥的做法是:技能文件上传到对象存储,技能中心监听版本变更事件,新版本加载成功后,把运行中的 Agent 实例平滑切换到新版本。切换时要注意事务性:先存新版本配置,再加载新版本代码,最后更新路由表。任一步失败都要回滚。
技能更新还牵扯权限问题。不是所有人都能往线上技能库里塞代码。我在团队里定了规矩:每个技能必须有一个 owner,新版本提交要经过 owner 审核,用全局 request_id 把每次技能调用串起来,一旦线上出问题,可以立刻追溯到是哪个版本、哪个技能、哪个参数导致的。
4. 技能调度与组合:让 Agent 学会“用”技能
4.1 从用户意图到技能调用
有了技能库,接下来就是 Agent 的调度环节。简单说,就是把用户请求转化为一连串技能调用。整个流程我用一个循环概括:
- 把历史对话、当前用户请求、可用技能目录发给大模型。
- 大模型判断是否需要调用技能。如果要,输出一个结构化调用指令。
- 解析指令,校验参数,执行对应技能。
- 把技能返回结果放回上下文,再交给大模型。
- 重复,直到大模型判断任务完成,或者达到最大循环轮次。
这里的核心是“结构化调用指令”。我习惯用 JSON 格式定义:
{ "thought": "用户需要汇总本周日报并标记风险,我需要先获取日报列表", "tool": "list_daily_reports", "args": { "date_from": "2025-01-13", "date_to": "2025-01-17" } }注意 thought 这个字段,它记录模型为什么选择这个技能。调试 Agent 时极其有用,能分辨模型是理解错了任务,还是参数填错了。
循环轮次必须设置上限。我之前遇到一次线上事故,模型在一个失败技能上反复重试,把接口打到熔断。后来给所有 Agent 加了最大循环次数,默认 8 轮,超过就中断,并返回“任务过于复杂,建议补充信息”的兜底文案。
4.2 多技能组合与嵌套任务
真实业务里,很少有一个技能单打独斗的情况。更多时候是几个技能按顺序执行,前一技能的输出作为后一技能的输入。
拿“汇总周报并标记风险”来说,标准技能序列是:
- list_daily_reports 拿到本周日报列表。
- get_report_content 逐个读取日报正文。
- extract_risk_items 从正文中抽取风险句。
- generate_summary 汇总所有风险并输出结构化总结。
这套序列可以让大模型实时决定,也可以预置成工作流模板。我的建议是:稳定、重复性高的任务尽量预置模板,能大幅降低调度出错率;开放性、探索性任务才让模型自由编排。
技能组合时有个关键问题:一个技能能不能直接调用另一个技能?我曾经尝试在技能内部直接调用其他技能,结果非常难维护。因为每个技能都应该是原子操作,组合只应该发生在调度层。如果技能 A 的内部逻辑需要技能 B,那说明调度层应该先调 B、再调 A。把组合代码放在 Agent 循环里,而不是技能内部,可以最大限度保持技能复用性。
组合还需要考虑超时和重试。技能返回结果过大时,要给大模型截断,否则 token 会快速超限。技能失败时,不要盲目重试同一个技能,应该先把错误信息交给模型,让它判断是换一种方式,还是换参数重试。
5. 技能评测:到底好不好用,数据说话
5.1 评估维度和离线测试集
技能体系上线容易,但“技能到底帮 Agent 提升多少”很难拍脑袋说清。必须建立一套评测机制,否则你不知道新加一个技能是优化还是添乱。
我常用的离线评估维度有五个:
| 维度 | 含义 | 计算方式 |
|---|---|---|
| 技能选择准确率 | 模型选对技能的比例 | 选对技能数 / 总测试数 |
| 参数提取准确率 | 参数结构和取值是否合格 | 严格匹配或宽松匹配 |
| 执行成功率 | 技能内部逻辑正常返回的比例 | 成功执行数 / 总执行数 |
| 任务完成率 | 多步任务最终完成的比例 | 完成人次 / 总人次 |
| 单次任务成本 | 平均 token 消耗和延迟 | 总 token / 任务数 |
离线测试集不需要非常庞大,但一定要覆盖三种情况:正常请求、边缘请求、异常请求。每条测试记录包括用户输入、期望技能序列、期望参数、期望输出类型。这里的关键是不用要求模型输出和标准答案完全一致,只要结构正确、核心字段正确,就算通过。
还可以用“裁判模型”对输出做质量打分。把用户请求、真实输出、评测标准放到一个大模型里,让它按规范性、完整性、准确性打分。这个思路能处理很多结构化判断搞不定的开放性问题,但不能直接替代人工抽检,否则裁判模型会有自己的偏好偏差。
5.2 线上追踪与回归
离线测试只是及格线,真正让技能体系稳定的是线上追踪。我坚持给每一次 Agent 任务打 trace,完整记录从用户输入到每次技能调用的全过程。至少包括这些字段:
- task_id / request_id
- 大模型每次输出的 thought 和调用指令
- 技能名称、技能版本、参数、返回值摘要
- 每一步耗时、token 消耗、错误信息
- 最终用户反馈或人工接管标记
这些日志的价值在系统维护阶段会完全显现。哪天线上 Agent 表现变差,拉出 trace 就能看到是哪一步开始偏的。最常见的偏差点是技能返回了“看似成功但内容为空”的结果,模型可能会捏造内容。有了 trace,这种问题一秒定位。
有了线上日志,就必须有回归机制。我每周从线上失败 case 里抽一批加入离线测试集,然后统一跑一遍全部技能,防止某个新技能把旧技能挤坏。回归通过的标准是:新增测试错误率不高于基线,既有测试正确率不下降超过 1%。这个做法看起来笨,但比“感觉好像没问题”靠谱得多。
6. 踩坑记录与实操建议
6.1 我踩过的几个坑
先说一个最容易犯的错:技能描述写得太泛。我给某个技能写过“处理所有格式差异”的描述,结果模型遇到格式问题不去查原始数据,直接让这个技能“处理”,导致技能内部收到一堆稀奇古怪的输入。后来所有描述里必须带上边界条件,明确“不能做什么”。
第二个坑是在技能内部共享全局变量。多个并发请求同时执行同一个技能,共享变量被互相覆盖,导致输出串号。改成把上下文对象当作不可变数据传进技能后,这类问题才根治。凡是技能里需要读写任何状态,都要通过外部存储,绝不能放在进程内存里无保护共享。
第三个坑是热更新时没有保留旧版本。有一次新版本技能上线后,调度层还缓存着旧版本的参数描述,模型按旧格式传参,新版本执行器按新格式校验,结果全线失败。后来注册中心强制要求,更新必须先注册新版本,等活跃连接全部结束后再下线旧版本。
第四个坑是技能执行结果太大直接塞回上下文。有个技能原来会返回上万字的原始材料,大模型处理时 token 暴涨,成本翻了好几倍。后来给所有技能加了一个“返回摘要”的统一规则,技能内部自己决定返回全量还是摘要,并且限制 max_return_chars。
6.2 几个值得坚持的设计原则
踩了这些坑之后,我总结了五条做 agent-skills 时值得贯彻的原则,分享出来供参考。
第一,一个技能只做一件事。如果你描述技能时需要写“并且”,说明这个技能应该拆成两个。拆分的标准是:技能名能用一个动词短语说清,参数集合不交叉。
第二,描述里永远写清边界。“该技能仅适用于……”“该技能不包含……”这种话不能怕啰嗦。模型的调用权是在你手里,你不写边界,它就会自作主张。
第三,输出结构必须稳定。即使技能内部实现换了,输出 JSON 的字段名、类型、层级结构也不要轻易变。下游所有逻辑都依赖这个契约,变了就得全链路回归。
第四,所有技能都要带 trace 上下文。哪怕只是从外部 API 拉数据,也要把 request_id 传到对方服务的日志。没有全链路可观测性,Agent 出问题你只能靠猜。
第五,先小范围上线。新技能先只对 10% 流量生效,观察一天指标再放量。不要因为本地测了几次没问题就直接全量,Agent 的输入组合空间远比你想的大。
如果让我给 agent-skills 下一个定义,我更愿意把它看成智能体从 Demo 走向生产环境的分界线。模型负责聪明,技能负责稳定。把每一项可复用的能力都做成有名字、有契约、有版本、有日志的技能,整个 Agent 系统的可维护性和可控性会提升一个台阶。
最后再分享一个让我印象很深的小技巧。技能体系刚搭建时,我发现模型总是漏选其中一个关键技能,后来排查到原因是那个技能的名字和另一个相似技能太接近,模型辨认不出来。改完两个技能的名字,让它们从字面上就能区分使用场景,准确率立刻上来了。别小看命名这件事,它可能是你整个 agent-skills 体系里成本最低、收益最直观的一次优化。