自从我把手头的 Agent 项目从"一个 Prompt 打天下"改成"skill 集合驱动"之后,项目进展快了很多,也少了很多来回调试的重复劳动。这篇文章想聊聊 agent-skills 这个项目——准确地说,是聊聊我们围绕 agent-skills 做的一整套技能化改造:为什么做、怎么设计、具体怎么落地,以及中间踩过的那些值得记录的坑。
如果你正在做 LLM Agent 相关的东西,比如让模型学会调用工具、按流程完成任务、或者想解决"提示词越写越长但效果越来越不稳定"的困境,这篇文章应该能给你一些可以抄作业的思路。整篇内容不依赖任何特定的 Agent 框架,核心一套设计思路,你拿到自己项目里也能套用。
1. agent-skills 是什么,我们为什么需要它
1.1 先把问题说透:Agent 为什么需要"技能库"
很多人在刚接触 Agent 时会有一个错觉:只要把任务描述得足够详细,大模型就能自己搞定一切。早期我也这么干过,结果就是一段核心 Prompt 被不断打补丁——今天加一句"遇到 XX 情况要这样处理",明天补一条"如果返回格式不对就重试"。到最后那几段 Prompt 又长又脆弱,换一个场景就跑偏,排查起来极其痛苦。
问题的根源在于:Agent 不是一个"全知全能"的执行体,它本质上是"一个大脑 + 一堆可以调用的能力",而这些能力如果没有被结构化地组织起来,大脑就只能靠猜。
agent-skills 做的事情,就是把这堆能力变成一套可以注册、可以检索、可以被调度的"技能组件"。每一个技能都是一个独立的功能单元,有自己的描述、输入输出约定、触发条件、执行逻辑。Agent 在拿到任务后,先去技能库里面找匹配项,而不是靠泛化的推理去猜流程。
这个思路听起来简单,但带来的改变是很直接的:
- 每个技能可以单独测试、单独迭代,不污染其他逻辑。
- 技能描述可以写得非常聚焦,模型的调用准确率明显提升。
- 新场景接入时,不需要重写 Prompt,只要往技能库里增加新技能。
1.2 agent-skills 解决的真实痛点
我们项目初期的状态大概是这样的:一个主控 Agent 负责理解用户意图,然后靠手写的 if-else 去调几个内部函数。一开始还好,但技能一多,主控部分的代码肉眼可见地膨胀。每加一个能力就要改主控逻辑,回归测试成本也不低。
后来我们把技能独立出来,用一组标准的描述信息登记到技能库,主控部分只负责"选技能、传参数、拿结果"。用了一段时间之后,我发现它实际上解决了三件事。
第一是解耦。技能之间的依赖关系被显式声明,不再寄生于主控代码。想停用某个技能,直接在注册表里删掉这一条,不会产生连锁反应。
第二是可控。每一个技能可以先单独验证精度和稳定性,通过之后再放进技能库。之前那种"加了新功能结果旧场景挂了"的问题,出现频率低了很多。
第三是可观测。技能调用的输入、输出、耗时都记录在案,出了问题能快速定位是哪个环节,而不是在长 Prompt 里大海捞针。
1.3 这套方案适合谁
如果你正在做以下这些工作,agent-skills 的思路会比较直接适用:
- 你的 Agent 需要调用多个工具或 API 完成复杂任务。
- 你希望不同项目之间能复用一套能力定义,而不是每个项目各写一遍 Prompt。
- 你在团队里负责 Agent 的基础建设,想让后续接入的新场景都走同一套注册方法。
当然,所有方案都有代价。技能库会带来一定的抽象成本,初期需要花时间设计"技能的描述格式"和"调度的触发逻辑"。如果你的应用只有两个简单函数,直接写 if-else 可能更快——技能库的优势从技能数量超过 10 个之后才开始体现。
2. 技能库的顶层设计与分层逻辑
2.1 技能的最小单位:一份标准定义
在做 agent-skills 的时候,我们最先定的不是代码结构,而是"一个技能长什么样"。因为技能库的入口是描述信息,这些信息最终要被模型读到,所以定义格式直接决定了调用质量。
我把一个技能拆成了几个字段:name(技能名)、description(功能描述)、input_schema(输入参数定义)、output_schema(输出结果定义)、trigger_hint(什么场景下优先用我)。前面两个好理解,后面三个细节上花的功夫不少。
description不是写给人看的,是写给模型看的。所以我们后来统一改成"当用户想要 XX 时,使用该技能,该技能可以 YY,特别适用于 ZZ 场景"这样的句式。'目标'越明确,模型在选择时越不容易跑偏。
input_schema和output_schema严格对齐 JSON Schema 的写法。这样做的原因很现实:模型拿到的是一串 JSON 定义,而我们要保证执行时拿到的参数不会缺字段、不会错类型。用标准 Schema 定义,校验逻辑可以直接复用现成库,不需要自己手写一堆判断。
trigger_hint是我们的额外补充。它不承担主要描述,而是专门写"边界"。比如某技能在"数据量小于 100 条时推荐使用",或者"当请求来自非技术用户时优先提示使用该技能"。这个字段在模型模糊选错技能的时候特别管用。
2.2 技能注册:把能力变成"可被检索"的条目
技能的注册机制其实很像服务注册中心。每个技能在被加载时,会向技能库提交一份自己的元信息。这个元信息不仅仅包含上面说的字段,还会有一些运行时数据,比如技能执行的依赖组件、是否需要在特定环境下运行、超时阈值等等。
我们在实现时,用了一个类似"技能清单"的概念:项目启动时扫描所有技能目录,逐个读取技能定义文件,统一注册到内存中的一个槽位表里面。每一条记录都有一个全局唯一的技能 ID,后续的调用全部按 ID 走,而不是按技能名匹配。这样做的考虑是:技能名可能调整,但 ID 稳定,日志和追踪记录不会因改名而断掉。
注册阶段还有一个容易被忽略的环节:技能依赖校验。有些技能不是"开箱即用"的,它们需要外部服务在线,或者需要某个配置文件存在。启动注册时我们会做一次体检,把不满足条件的技能标记为disabled而不是直接剔除。这样做的意义在于,当故障恢复后,技能可以自动从禁用恢复为可用,而不需要重启整个应用。
2.3 技能编排:不是所有任务都很简单
一个技能解决一个原子问题,但用户的实际任务往往是多个原子问题的组合。所以技能库上面一定要有一层编排逻辑,否则 Agent 会面对"同时检索到好几个可用技能"的困惑。
编排这部分,我们走了两阶段路线。第一阶段是"意图 → 技能列表",也就是根据用户的初始目标,先粗筛出候选技能;第二阶段是"候选技能 → 执行序列",这一步让模型基于候选技能的描述设计执行顺序。
这里有一个很关键的经验:不要让模型从全量技能库里直接选执行顺序。全量技能可能有一两百条描述,模型处理长列表时很容易"选择疲劳",出现漏选或误选。先粗筛成 Top 5 左右,再让模型做序列规划,实测准确率有非常明显的提升。
编排结果会产出一份结构化的执行计划,格式大概是这样的:
{ "task_id": "xxx", "steps": [ {"skill": "web_search", "params": {"query": "..."}}, {"skill": "extract_key_points", "params": {"top_n": 5}}, {"skill": "generate_report", "params": {"format": "markdown"}} ] }这个执行计划会被传给执行引擎,引擎根据技能 ID 逐个调用,并把每一步的结果缓存下来。这样即使后续某一步失败了,回溯时还能看到中间产物,而不是只能看到最终报错。
2.4 技能的失败处理与回退
技能执行不可能是 100% 成功的。网络超时、输入参数不合法、外部依赖返回异常数据……这些情况在实战中太常见了。设计技能库时如果不把失败处理想清楚,线上出问题的时候会非常狼狈。
我们在每个技能定义里加了两类失败描述:一类是"预期内失败",比如输入参数校验失败,这种失败通常是因为模型生成的参数不符合 Schema;另一类是"预期外失败",比如调用外部服务超时。针对预期内失败,执行引擎会拿到详细的校验错误信息,然后把错误信息反馈给 Agent,让模型重新生成参数;针对预期外失败,则走重试或降级逻辑。
回退策略也很有讲究。每个技能都可以配置一个"备选技能",比如主技能是"实时汇率查询",备选技能是"缓存汇率查看",当主技能不可用时,Agent 可以直接降级到备用逻辑。这个机制在大部分在线服务多多少少存在不稳定的前提下,算是一个保命功能。
3. 实操记录:从零搭一套 agent-skills 技能库
3.1 技能目录怎么组织
项目里我们先建了一个技能根目录,每个技能一个小文件夹,文件夹名就是技能 ID。目录结构大概是这样的:
skills/ ├── skill_registry.py ├── definitions/ │ ├── time_report.yaml │ ├── database_query.yaml │ └── ... ├── runtimes/ │ ├── time_report.py │ ├── database_query.py │ └── ... └── shared/ ├── utils.py └── validators.pydefinitions里面放的是技能的 YAML 描述文件,runtimes里面放的是技能的执行函数。我们把定义与实现分离,是因为在调用链路上,模型只与definitions中的描述打交道,而执行引擎才需要去加载runtimes。这样也方便以后把技能描述展示给模型做训练。
每个 YAML 文件的格式是固定的。举个例子,一个"数据库查询"的技能定义大概长这样:
name: database_query description: 当用户需要从业务数据库中查询数据时使用该技能,支持按字段过滤和聚合统计。 input_schema: type: object properties: table: { type: string, description: "表名" } filters: { type: object, description: "过滤条件,键是字段名,值是期望值" } required: ["table"] output_schema: type: object properties: rows: { type: array } total: { type: integer } trigger_hint: 适合有明确表名或字段关键词的查询请求;当用户只描述模糊业务问题时不要使用。 timeout_seconds: 5 fallback_skill: cache_query这里timeout_seconds和fallback_skill都是运行时配置,执行引擎在做调度时直接读取。实际执行的时候,我们还会根据这个配置动态决定超时。
3.2 技能注册表:一个带索引的加载器
注册表的核心实现其实不复杂,就是一个SkillRegistry类,在初始化时递归扫描目录,读取所有 YAML 定义,并做三件事:校验定义格式合法性、注册运行时函数、建立技能描述的索引。
为了弥补我的经验空白,我做了一个"技能描述增强"的小技巧:注册时,除了原始 description 之外,我们还会用关键词模板扩充一份"检索索引"。比如技能描述里有"数据库"这个词,索引里就加入"DB、查询、表、数据量"。这样在粗筛阶段,可以先用 BM25 或者词频匹配,把明显不相关的技能过滤掉,减轻模型要读的描述数量。
索引匹配在技能库规模变大之后非常关键。技能数量超过 30 个时,我建议一定要加这一层,否则每轮调用都把全部技能描述塞给模型,成本和时延都不可控。我们用了一个简单的词频计算就够用,没必要上太重的向量检索;技能描述都是短文本,词频匹配效果已经足够。
3.3 技能调用链路:从任务到结果
接下来是整个链路的核心部分,一个完整调用流程大概是这样的:
- 系统接收用户任务,先做一次任务预处理,提取目标关键词。
- 用关键词匹配技能索引,粗筛候选技能列表(Top 5)。
- 把候选技能的完整定义打包,连同任务描述一起发给模型。
- 模型输出结构化执行计划。
- 执行引擎解析执行计划,按顺序调用技能运行时函数。
- 每步执行结果都做 Schema 校验,不合法则反馈给模型重新生成参数。
- 所有步骤完成后,汇总输出。
这里第 4 步和第 6 步之间会有一个循环:模型生成参数 → 执行技能 → 校验结果 → 不通过则带着错误信息重新生成参数。我们在循环上加了一个最大重试次数(通常 3 次),避免模型陷在错误参数里无限重试。
SkillRegistry里还维护了一个简单的调用审计,记录每个技能被调用的次数、失败率、平均耗时。这些数据后面对优化技能描述和触发词特别有用——你总能发现某个技能描述在真实请求里经常被踩,其描述大概率写得不够清晰。
3.4 一个具体的组合技能案例
为了把上文的东西串起来,我分享一个实际做过的模拟需求:内部运营部门希望 Agent 每天自动生成一份"产品动态简报"。这个任务天然是多技能组合的,我们拆解成四个技能:
fetch_news:从内部平台拉取产品相关新闻。filter_by_keywords:按关键词过滤掉无关内容。summarize_batch:对过滤结果做批量摘要。format_brief:将摘要内容按简报模板格式化。
这四项分别注册后,在技能编排阶段让模型设计顺序。运行几次后发现,fetch_news偶尔会把无关业务线的内容也抓进来,于是我们在filter_by_keywords的trigger_hint里补充了"当新闻源包含多个业务线时,必须使用本技能过滤"。这个小小的调整,让整体的简报准确率从 78% 提升到了 91%。这再次印证了上一篇里那个观点:技能描述要写得非常明确,特别是触发边界。
3.5 运行时与模型的选择
最后说一下模型选型这块的经验。我们测试过用不同模型来承担"技能选择"和"技能编排",发现它们的能力差异主要体现在第 4 步(生成执行计划)。简单模型能正确选出一个技能,但面对多步骤任务时,输出的计划经常漏步骤或顺序颠倒。复杂模型在这个环节要稳健很多。
所以如果你的场景以单一技能调用为主,小模型就够了;一旦涉及多技能编排,建议用一个能力更强的模型专门负责规划。这种"规划用大模型、执行用小函数"的混合架构,在成本和效果之间是相对均衡的。
4. 常见问题与排查实录
4.1 模型总是选错技能怎么办
这个问题是我们项目早期最明显的痛点。排查后的原因通常集中在描述不清晰和候选过多这两件事上。
如果是描述问题,看的是技能描述里是否写清了"适用场景"和"不适用场景"。模型是很吃边界描述的,你只写"该技能可以查天气"远远不够,要写"当用户提到降水量、风速、紫外线指数时,使用此技能;当用户询问季节性气候趋势时,不要使用"。
如果是候选过多,检查粗筛索引是否生效。有些技能的关键词模板覆盖不到位,导致粗筛后仍然保留了十几个候选。这时候要回头补技能索引的关键词表。
4.2 输出参数不符合 Schema
输出校验报错特别常见,尤其是技能返回的数据结构稍微复杂一点,模型配参数就容易漏字段。
我们的做法是:在执行引擎里加一个"结果规范化"的步骤——不为难模型,让它自由输出原始结果,再由技能的运行时函数负责把结果转换成标准 Schema。比如模型返回了一段文本而不是结构化的{"rows": [...]},运行时函数里就写一个解析器去适配。这比死磕模型的输出格式更省力。
4.3 技能编排结果死循环或超时
如果模型生成执行计划时出现了循环依赖,或者某个技能调用一直失败重试,会导致整条链路超时。
我们的解决方案有三层:第一层是编排阶段给模型一个强约束——计划中同一技能最多出现一次;第二层是执行阶段设置单步超时,而不是整体超时;第三层是增加"熔断"逻辑,同一个技能连续失败超过 5 次,本次任务直接标记为失败,不再继续重试,避免拖垮后续任务。
4.4 常见问题速查表
| 现象 | 可能原因 | 处理建议 |
|---|---|---|
| 模型选错技能 | 描述边界不清楚 | 在trigger_hint里补充不适用的场景 |
| 候选技能太多 | 粗筛索引失效 | 扩充关键词表,或调整粗筛阈值 |
| 参数频繁报错 | 输入 Schema 描述过粗 | 给各个参数补充详细的语义说明 |
| 结果无法解析 | 模型输出格式不稳定 | 在运行时里做结果规范化解析 |
| 链路总是超时 | 单步超时设置过长 | 细化到每个技能的超时配置 |
| 新增技能不被调用 | 描述没有突出差异化 | 对比现有技能,在描述里明确不同点 |
这个速查表是我实际调试过程中沉淀出来的,不一定覆盖所有场景,但排查方向基本够用。
5. 进一步扩展:从技能库到技能网络
5.1 技能之间的组合与复用
当技能数量积累到一定程度,我发现单纯靠"一个技能一个函数"的模式有点不够用了。有些技能彼此之间存在依赖关系,比如"生成简报"必然依赖"拉取内容",而"拉取内容"又可以服务很多不同的上层任务。
这带来一个新的思路:技能不应该是扁平的一堆函数,而应该允许技能之间互相调用。好比我做一个report_generator技能,其内部不是直接写业务逻辑,而是声明依赖fetch_news、summarize_batch和format_brief这三个原子技能。这样技能库从"一层函数集合"慢慢长成了"一层技能调技能的网络"。
实现上其实不复杂,就是在技能定义里增加一个depends_on字段,注册时做依赖解析和拓扑排序。调用某个复合技能时,执行引擎先把它依赖的原子技能按顺序执行一遍,再把各步骤结果作为参数传给复合技能的逻辑。这样避免我们在上层重复编排相同的执行计划。
5.2 给技能加一层"意图路由缓存"
另一个我觉得很有意思的扩展方向,是给技能的编排结果做缓存。同样的任务模式在真实业务里会反复出现——比如"每天生成简报"和"每周整理周报"本质上是同一套技能组合,只是参数不同。
我们在编排引擎上加了一个"执行计划缓存":按任务类型的哈希值存最近一次成功执行计划。下一次遇到同类任务时,先查缓存有没有匹配的计划模板,有就直接复用,只更新参数部分,不再让模型生成计划。这样能省掉不少模型推理开销,响应速度也更快。
当然这个缓存只适合高频重复的场景,长尾任务没必要做缓存,否则模板多了反而难维护。
5.3 未来的技能自动生成路线
写到最后,顺手聊聊我在这个项目里看到的一个潜在的更远方向:用模型自己去生成技能。我们目前的所有技能都是人工定义、人工写运行时逻辑的,成本不低。但如果技能定义足够标准化,理论上模型可以读一份新工具的 API 文档,自动产出技能描述文件、参数 Schema,甚至生成简单的运行时适配函数。
我之前做了一个小实验:给模型一份内部工具的接口文档,让它生成技能定义的 YAML 初稿。生成的内容七七八八能跑通,但在边界情况和异常描述上面还是需要人工兜底。这个方向对长期维护技能库的人来说是挺值得期待的,可以显著降低技能扩展的成本。
6. 写在最后的经验总结
agent-skills 这个项目做下来,给我最大的感受是:Agent 系统的复杂度和技能定义的标准化程度呈强烈的负相关关系。技能定义越规范,Agent 系统整体越简单——因为模型不需要花力气去理解各种非结构化的能力描述,执行引擎也不需要针对每个技能单独写调度逻辑。
如果你想在自己的项目里落地这套思路,我的建议是:先别急着把大量技能一股脑注册进去,而是从两个高复用性的技能开始,把定义格式、调用链路、校验逻辑都跑通,再逐步扩容。技能库是一个越用越顺手、但也越积越难改的组件,早期的格式决策会直接影响后面所有技能的扩展体验。
另外一个小建议:给每个技能定义加一个"负责人"字段,哪怕是自己做的小项目也加上。当技能数量变多以后,你会发现追踪哪些技能描述需要更新、哪些技能因为需求变化已经过时,单看代码很难判断。一条简单的负责人标记,能省去不少复盘时的沟通成本。
这套东西后续我还会继续往"技能自动生成"和"技能效果评估"两个方向迭代。如果你也在做类似的 Agent 技能库,欢迎在评论区聊聊你踩过的坑——尤其是技能描述这种看似简单、实际能磨掉人一层皮的细节,多看看别人的经验能少踩很多坑。