1. 项目缘起与核心设计思路
1.1 agent-skills 到底在解决什么问题
先聊一个我在多个项目里反复撞上的痛点:模型本身的能力很强,但一旦要把 agent 放到真实业务里,它总是缺那"最后一公里"的落地能力。模型知道怎么调用 API、怎么写查询语句,可真正要让它稳定地完成一次信息检索、数据处理或报表生成,缺的不是大模型参数,而是把"知识"变成"可执行动作"的那层封装。
agent-skills 这个项目,核心就是把 agent 的"技能"抽象成一种可注册、可发现、可复用、可评测的标准化模块。你可以把它理解成给 agent 装了一个"工具箱":每个技能就是一个带有清晰说明书和使用接口的工具,agent 在接到任务时先翻箱倒柜找到合适的工具,再按规矩使用,用完归位。这样做的直接收益有三点:第一,模型不再凭感觉"临场发挥",而是走成熟路径;第二,新增能力不用反复改 prompt,往技能库里加一个模块即可;第三,每个技能都可以单独测试、单独计费、单独降级。
我在项目里最常拿来打比方的场景是"组织一场多人协作":如果没有明确分工,大家只能靠默契瞎忙;而 agent-skills 提供的就是岗位说明书、标准作业流程和验收标准。模型是那个项目经理,技能库则是它手底下每个成员的"工作手册"。
1.2 为什么把"技能"当作一等公民,而不是塞进 prompt
很多人刚开始做 agent 时,习惯把所有指令、背景知识、工具说明一股脑写进 system prompt。短期看没什么问题,但一旦技能数量超过五个,prompt 就会变得臃肿不堪,每次调用都要传一大段上下文,不仅费 token,还经常出现指令互相打架的情况。
把技能从 prompt 里抽离出来,本质上是做了一个"关注点分离"。prompt 只负责表达目标和约束,技能库负责承载"怎么做"。agent 在运行过程中按需拉取技能说明,而不是每条消息都背着全部技能清单跑。这样做之后,上下文长度能降一个量级,指令遵循的成功率也明显提升,因为模型面对的不再是一堆堆叠的规则,而是一个简洁的意图加一个精确的接口说明。
另外一个关键原因是"可评测性"。技能一旦模块化,就可以为每个技能单独构造评测集,比如"调用该技能完成 50 个用例,统计成功率、耗时、Token 消耗"。这种细粒度的观测在 prompt 大杂烩模式下根本做不到,你很难判断一次失败到底是意图理解错了、参数提取错了,还是工具本身出了问题。技能化之后,问题边界清晰,排查效率翻倍。
1.3 设计原则:模块化、可组合、可治理
在动手写 agent-skills 之前,我给自己定了三条铁律,后面所有设计决策都围绕这三条展开。
第一条是模块化。每个技能必须是一个自洽的单元,包含描述、输入输出定义、实现逻辑、测试用例、权限声明和错误处理策略。这意味着拿走任何一个技能,系统其余部分不受影响;加一个新技能,也不用改老代码。
第二条是可组合。单一技能只做一件小事,复杂任务由多个技能编排完成。比如"写一份行业周报"这个任务,可以拆成"搜索行业新闻"、"提取关键信息"、"按模板生成 Markdown"三个技能,前一个的输出喂给后一个。技能之间不直接依赖,而是通过标准格式交换数据。
第三条是可治理。技能必须有版本、有作者、有调用统计、有风险等级。我不能允许一个内部测试用的半成品技能被生产环境误调用,所以技能库必须支持环境隔离、灰度发布和权限控制。这一点在多人协作的项目里尤其重要——别人能一眼看出哪个技能是稳定的、哪个是实验性的。
2. 技能库的架构设计与数据建模
2.1 技能描述规范:一份能看懂又能执行的说明书
技能描述是整个 agent-skills 体系的基石。我参考了 OpenAPI 和 JSON Schema 的思路,定义了一套轻量的技能清单(Skill Manifest)格式,用 YAML 编写,包含以下几个关键字段:
name:技能唯一标识,采用命名空间加动词的格式,比如web.search、data.extract。description:一句话说明技能用途,要求写清楚"什么时候该用、什么时候不该用",这直接决定模型能否正确选中技能。input_schema与output_schema:分别定义入参和出参的 JSON Schema 结构。run:执行入口,指向实际处理函数。permissions:技能运行所需的权限声明,比如网络访问、本地文件读写。version与tags:版本号与分类标签,方便检索和灰度。test_suite:指向该技能的评测用例集。risk_level:low / medium / high,用来标识技能可能造成的风险。
这个清单看起来简单,但实际写的时候很容易踩坑。最典型的问题就是 description 写得太抽象。我曾经写过一句"查询天气信息",结果模型在"查询上海明天适不适合户外跑步"这种任务里根本不调用它,因为描述里没提到"适不适合"这种判断语义。后来我把描述改成"根据指定城市和日期返回天气实况及综合出行建议,适合用于行程规划、户外活动决策等场景",命中率立刻上来了。所以描述里一定要写清楚适用场景、输入约束和典型用法。
2.2 输入输出 Schema 的设计要点
Skill Manifest 里最容易被忽略、却最影响稳定性的就是输入输出 Schema。模型是靠这个来生成调用参数的,Schema 写得越精确,参数提取越准。
我设计输入 Schema 时坚持三条原则:
第一,必填参数尽量少。每多一个必填项,就多一层失败风险。能用默认值兜底的,就设置默认值;能从上下文推断的,就标记为可选。比如搜索技能里的"语言"参数,我默认按用户输入的语言推断,不需要单独传。
第二,字段类型和格式要尽量严格。能用 enum 枚举的,就不要开放字符串自由填写。比如排序方式,与其让模型填一个"按时间从新到旧",不如给它enum: [relevance, date_desc, date_asc]三个选项,模型选起来又快又准。
第三,关键字段要写注释说明取值规则。JSON Schema 支持description字段,我会在参数描述里写清格式约束,比如"日期格式为 YYYY-MM-DD"、"数量必须是大于 0 的整数"。模型对明确规则的遵循度,远高于对隐含共识的理解度。
输出 Schema 我会设计得稍微宽松一些,因为下游技能可能只关心其中的部分字段。统一约定所有输出都带一个meta对象,记录来源、耗时、token 消耗等元信息,方便后续追踪和审计。
2.3 技能依赖与版本管理
技能不是一定孤立存在的。一个"生成销售周报"的技能可能要依赖"查询销售数据"和"生成图表"两个子技能。这就引出依赖管理问题。
我采用了一种类似 npm 的轻量依赖声明方式:每个技能的 Manifest 里可以写dependencies字段,声明它运行前需要哪些技能已经注册。系统在加载技能时先做依赖解析,保证被依赖的技能先注册。如果依赖缺失,该技能会被标记为"不可用",而不是加载时报错,这样其他技能不受影响。
版本管理上,我严格遵循语义化版本规则:主版本号变更表示接口不兼容,次版本号变更表示向后兼容的功能新增,补丁版本号表示 bug 修复。技能之间的依赖必须锁定最小版本,比如web.search: ">=1.3.0",避免下游技能因为上游行为变化而悄悄出问题。
另外一个容易被忽略的细节是技能运行时的隔离。不同的技能可能依赖不同版本的 Python 包或 Node 模块,我最终选择了把技能放进独立的进程或容器里跑,宿主 agent 通过标准输入输出与它通信。虽然这么做会增加一点调用开销,但换来的稳定性和可运维性非常值得。
3. 核心实现环节:注册、调度与编排
3.1 技能注册与热加载机制
技能注册中心(Skill Registry)是 agent-skills 的心脏。它维护着当前环境中所有可用技能的内存索引,支持三级结构:技能集合(skill set)-> 单个技能 -> 技能版本。程序启动时从配置目录扫描所有 Manifest 文件,解析后构建索引,之后对技能文件的变更提供热加载能力。
热加载实现起来不复杂,核心是文件监听加原子替换。我用了 watchdog 类似的机制监听技能目录,当 Manifest 文件或实现代码发生变化时,重新解析并替换内存中的技能对象。这里有个关键细节:加载新版本技能时,不能影响正在执行中的旧版本实例。我的做法是保留旧版本句柄直到它运行结束,新请求一律路由到新版本,这种"边跑边切"的策略在线上环境里非常实用。
注册中心还承担着技能发现(Skill Discovery)的职责。当模型拿到一个用户任务时,我会先从技能索引里做一次关键词检索,得到一个候选技能列表,再让模型从候选列表里做精确选择。这个"两段式检索"比直接把全部技能描述丢给模型要高效得多。实测下来,当技能数量超过 20 个时,先检索再选择的方式能把命中准确率提升至少 15 个百分点,同时大幅降低 token 消耗。
3.2 调用路由与上下文注入
技能选好之后,就进入调用路由环节。agent-skills 的调用路由不是简单的"传参数 -> 执行函数",而是做了一层标准化的请求上下文封装。每个 skill 调用请求会带上一个Context对象,里面包含:
user_intent:用户的原始意图文本,方便技能内部做细粒度判断。conversation_history:与当前任务相关的对话片段,经过截断处理。parameters:模型提取的结构化入参,已经过 schema 校验。environment:环境信息,包括当前时间、用户 ID、请求 ID、可用的 API 密钥引用等。trace_id:全链路追踪 ID,用来串联一次任务中的所有技能调用。
上下文注入的原则是"按需给,不超量"。技能 Manifest 里可以声明它需要哪些上下文字段,路由层只注入被声明的字段。比如一个纯粹的数据计算技能,就不需要拿到对话历史;而一个需要个性化输出的技能,则必须拿到用户画像。这种声明式上下文有一个好处:技能的输入边界清晰,驱动开发者养成"不依赖上下文隐式信息"的好习惯。
路由层还有一个重要职责是权限校验。每次技能调用都会先检查调用方是否有权限使用该技能,以及技能自身声明的权限是否与当前运行环境匹配。比如一个标记为risk_level: high的技能,如果运行环境是生产环境,必须经过二次确认才允许执行。
3.3 多技能组合编排的两种方式
在 agent-skills 项目里,我同时实现了两种技能编排方式,分别适用于不同场景。
第一种是"意图直连"(Intent Routing)。用户意图比较明确时,模型直接从技能库里选择一个技能并调用。这种方式简单直接、延迟低,适合那些"一问一答"型的任务,比如查天气、算税率、翻译文本。整个链路就是:用户输入 -> 意图识别 -> 技能选择 -> 参数提取 -> 执行 -> 返回。
第二种是"任务分解"(Task Decomposition)。用户目标比较复杂、需要多步处理时,由 agent 自主规划调用序列。比如"分析这份 PDF 的核心观点并生成一页摘要",需要先调用document.parse提取文本,再调用text.summarize生成摘要,最后调用markdown.render格式化输出。规划器根据候选技能的能力描述,生成一个执行计划,然后按计划逐个调用。
实现任务分解时我遇到的最大挑战是"部分失败后的恢复策略"。早期版本一旦中间某一步失败,整个任务就死掉。后来我引入了重试、降级和重规划三种策略:技能自身有重试机制;如果重试失败,检查是否存在替代技能可以完成相同目标;如果替代技能也没有,则允许规划器重新生成后续步骤。经过这一轮优化,复杂任务的完成率从 61% 提升到了 83%,提升非常明显。
3.4 失败回退与幂等设计
代理系统跑在生产环境,最怕的就是技能调用"半成功半失败"——比如写数据库时网络超时,实际数据可能写入了,也可能没写入。这时如果我们直接重试,可能造成重复数据。所以技能设计规范里有一条硬性要求:涉及写入、发送等有副作用的操作,必须实现幂等。
幂等实现通常有两种思路。一种是引入幂等键(Idempotency Key),每次调用带上全局唯一的请求 ID,服务端记录已处理的 ID,重复请求直接返回上次结果。另一种是状态机校验,操作执行前先查当前状态,只有处于"待执行"状态时才继续。我在技能框架里默认提供了第一种方案,开发者只需在 Manifest 里声明idempotent: true并指定幂等字段,框架会自动处理重复调用。
失败回退的另一个关键是"错误分类"。我会把技能错误分为两类:可重试错误和不可重试错误。可重试错误包括超时、限流、上游服务 5xx 等,这类错误值得用指数退避的方式重试几次;不可重试错误包括参数校验失败、权限不足、业务规则不满足等,这类错误重试一百次也没用,应该直接返回给上层做降级处理。错误分类写进技能实现规范里,每个技能必须返回结构化错误码,而不是只给一条报错文本。
4. 从零搭建 agent-skills 的实操过程
4.1 最小可运行环境与目录结构
纸上谈兵讲了这么多,下面直接上一套最小可运行的实现。我用 Python 3.11 加 FastAPI 写了一个轻量框架,目录结构如下:
agent-skills/ ├── skills/ │ ├── web.search/ │ │ ├── manifest.yaml │ │ ├── run.py │ │ └── tests/ │ └── text.summarize/ │ ├── manifest.yaml │ ├── run.py │ └── tests/ ├── registry.py ├── router.py ├── planner.py ├── context.py └── main.py启动流程分三步:注册中心扫描skills/目录,逐个解析 Manifest,构建索引;路由模块加载上下文注入规则和权限配置;然后启动一个 HTTP 服务,暴露/invoke和/list_skills两个接口,前者用于技能调用,后者返回当前可用技能列表。
我强烈建议一开始就把tests目录建好。很多人会觉得测试是后置工作,但对于技能系统来说,测试用例本身就是"技能说明书"的一部分,它们能告诉后来者这个技能预期地行为是什么。后面讲评测时会详细展开。
4.2 定义一个实用技能:以"联网搜索并总结"为例
这里我用一个日常开发最常用的技能做完整示例:web.search_and_summarize,它的职责是接收一个查询词,抓取前几条搜索结果,过滤正文,并生成一份要点总结。
Manifest 文件长这样:
name: web.search_and_summarize description: > 根据用户提供的查询词执行联网搜索,从搜索结果中提取高价值内容并生成结构化总结。 适用于需要获取最新信息、事实核查、资料调研等场景。 input_schema: type: object properties: query: type: string description: 搜索查询词,建议不超过 50 个字符 max_results: type: integer description: 返回的搜索结果条数 default: 5 minimum: 1 maximum: 10 language: type: string enum: [zh, en] description: 搜索结果语言偏好 default: zh required: [query] output_schema: type: object properties: summary: type: string description: 综合多个摘要合成的结构化总结 references: type: array items: type: object properties: title: { type: string } url: { type: string } snippet: { type: string } version: 1.0.0 risk_level: low permissions: network: true idempotent: true对应的run.py里,核心逻辑是先用搜索引擎 API 拿到结果列表,再对每个结果做正文抽取,把正文块送入摘要模型,最后把各条摘要合成一个结构化输出。这里有一个工程细节:正文抽取阶段不要贪多,每个页面只保留前 3000 字符的有效文本,避免把广告和导航文本喂给摘要模型。
这个技能定义里有几个细节是我反复打磨过的。description里明确写了"适用于需要获取最新信息、事实核查、资料调研等场景",这是因为模型在意图识别时非常依赖这类场景化描述。另外max_results的上下限约束也是有意为之,搜索太多页面会显著增加耗时和成本,默认 5 条往往已经足够。
4.3 技能评测与回归测试
技能系统的评测不能只看"这次调通没有",要建立可重复的回归基线。我在每个技能的tests/目录下放了一组 JSON 测试用例,每个用例包含输入、预期输出特征、可接受的耗时上限。
评测分三个维度:
- 功能正确性:输出是否满足格式要求,关键字段是否齐全。
- 稳定性:同一输入连续调用 10 次,结果波动是否在可接受范围内。
- 性能:平均耗时、P95 耗时、Token 消耗是否达标。
对于生成式输出,我不会做"逐字比对",而是做"要点覆盖率"评估——人工预先把每个测试用例应该覆盖的 3 到 5 个要点写下来,跑完技能后用语言模型判断输出中是否覆盖了这些要点。这个方案比 BLEU、ROUGE 这类指标更适合业务场景,因为它关心的是"该说的说了没有",而不是"表达得一样不一样"。
回归测试则是每次修改技能后自动跑全量用例,只要有一个用例从通过变成失败,就阻断上线。这一步刚开始会让人觉得繁琐,但一旦技能数量超过 10 个,它就是防止"改一个技能弄挂三个下游技能"的唯一防线。
4.4 性能与成本控制实操
技能系统上线后,最让人头疼的往往不是功能问题,而是成本和延迟。我有几个经过实测的优化手段。
第一是结果缓存。同一参数和上下文的技能调用,在短时间内大概率会返回相似结果。我在路由层加了一层 LRU Cache,key 是"技能名 + 参数哈希 + 上下文摘要",缓存时间根据技能类型设定,搜索类 5 分钟,计算类可以到 1 小时。实测下来,缓存命中率能做到 30% 左右,直接砍掉了将近三分之一的重复调用成本。
第二是模型分级。不是所有技能都需要用最强的模型来跑。我给框架加了一个model_hint字段,在 Manifest 里指定这个技能倾向使用的模型档位。简单的信息抽取用小型模型,复杂的推理合成用大型模型。这套"按技能定档"的策略,比全局统一用大模型能省一半以上的成本。
第三是并行调度。任务分解产生的多个技能调用,如果相互之间没有数据依赖,尽量并行执行。比如分析一份多章节报告时,每个章节的摘要可以并发跑,最后再合并。并行度从 1 调到 4,整体耗时能下降 50% 以上,代价只是多占一点并发配额。
5. 常见问题与排查技巧实录
5.1 技能调用失败排查清单
我整理了项目上线半年里遇到频率最高的几类故障,做成一张排查表:
| 症状 | 可能原因 | 排查步骤 |
|---|---|---|
| 模型不调用技能 | 技能描述与用户意图匹配度低 | 检查 description 是否写了适用场景和典型用法 |
| 参数频繁提取错误 | Schema 约束不明确 | 检查参数类型、枚举、格式注释是否完整 |
| 技能执行超时 | 上游 API 响应慢或超时阈值太小 | 先看监控里的上游 P95 耗时,再调超时配置 |
| 输出格式不合预期 | 输出 Schema 与实际返回不一致 | 用 Schema 校验工具跑一遍,检查是否有额外字段 |
| 同一问题时好时坏 | 技能依赖了外部非确定性因素 | 检查是否用了默认排序、是否依赖了上下文中的模糊信息 |
| 部署后技能不可用 | Manifest 解析失败或依赖缺失 | 看注册中心的日志,确认加载阶段是否有报错 |
这张表的核心理念是"先定位环节,再处理问题"。技能调用链路分成意图识别、技能选择、参数提取、权限校验、执行、输出校验六段,每类故障在链路上都有典型的位置,不要一上来就怀疑模型能力,多数时候问题出在描述和 Schema 上。
5.2 技能上下文冲突与污染
上下文冲突是技能系统特有的坑,我踩了好几次才彻底摸清规律。常见场景是这样的:技能 A 在实现里偷偷往上下文的某个字段里塞了中间状态,下一个技能 B 读取这个字段时拿到了污染后的值,导致行为异常。
解决办法是强制在路由层做上下文隔离。每个技能调用拿到的Context对象都是拷贝出来的副本,技能内部怎么改都只影响自己这一份;技能返回的输出结果再作为新上下文传给下一个技能。这样就从机制上杜绝了"A 的副作用污染 B"的可能性。
另一个相关问题是上下文中携带的敏感信息泄漏。早期版本的日志功能会把整个 Context 对象打印出来,有一次评测时发现用户邮箱出现在了日志里,虽然是无心之失,但这类问题在真实业务里就是安全事故。后来我加了一层脱敏组件,日志打印时自动把邮箱、电话、身份证之类的字段替换成掩码,这才放心。
5.3 技能膨胀与维护治理
技能数量增长到一定规模后,会出现一个很有意思的现象:多个技能功能高度重叠,但参数和行为细节各不相同。比如团队里有人写了web.search,有人写了news.search,还有人写了google.search,实际都是搜索,但是返回格式不一样,模型经常选错。
针对这个问题,我建立了技能治理评审机制。每月做一次技能清单审计,统计每个技能的调用次数、失败率、平均延迟、最近上线时间。调用次数连续一个月为 0 的技能会被标记为"僵尸技能",发送通知给维护人,要么补充用例重新激活,要么归档下架。功能重叠的技能会被合并,保留描述更清晰、调用更稳定的一方。
最后再分享一个我在实际使用中摸索出的经验:技能质量的提升,不能光靠写代码时的自觉,一定要有数据反馈闭环。每个技能调用都要记录完整的 trace 信息,定期抽样回看"模型为什么这么选、技能为什么这么答"。很多看似是模型不聪明导致的问题,追下去会发现其实是技能描述有歧义或 Schema 约束不足。把这些问题反馈给技能开发者,技能质量和模型表现都会同步提升。这个反馈循环,才是 agent-skills 真正价值的来源。