1. 从一个被问烂了的问题说起:Skill 和 Tool 到底差在哪
如果你最近半年在折腾 Agent 相关的东西,大概率会在某个群里看到有人问:“Skill 和 Tool 到底有什么区别?不都是给模型调用的能力吗?”这个问题看起来像概念辨析,实际上它决定了你整个 Agent 架构的走向。我自己在这个问题上踩过坑,早期做的一个客服 Agent,把所有能力都写成 Tool 注册进去,结果跑到第三周就崩了——不是代码崩了,是维护崩了。工具列表膨胀到四十多个,模型选工具的准确率断崖式下跌,改一个工具的描述还可能影响另外三个工具的选择。后来我把一部分能力重构成 Skill,情况才好转。
所以这篇东西不是概念科普,是我自己从 Tool 堆砌到 Skill 分层这个过程中的完整复盘。核心想讲清楚三件事:Skill 和 Tool 在机制上的本质差异、技能库为什么需要自演进、动态加载到底怎么落地。适合已经写过至少一个能跑的 Agent、正在被工具膨胀和上下文爆炸折磨的开发者。如果你还在“Hello World”阶段,建议先跑通一个最小 Agent 再回来看,会更有感觉。
先给一个我自己的定义,不一定标准但足够实用:Tool 是原子能力,Skill 是带上下文的能力编排。Tool 回答“能做什么”,Skill 回答“在什么情况下、按什么顺序、用什么参数做什么”。这个差别听起来很虚,但落到代码结构上,差别巨大。
2. Tool 的本质:一个被过度简化的抽象
2.1 Tool 的经典形态与它的隐含假设
绝大多数 Agent 框架里的 Tool,形态都差不多:一个函数,一段描述,一份参数 schema。模型看到的是描述和 schema,调用时输出参数,框架负责执行。这个设计非常优雅,优雅到让人误以为它能解决所有问题。
它背后有几个隐含假设,平时不会有人告诉你,但一旦规模上去就会暴露:
- 假设一:工具之间相互独立。你注册
search和summarize,框架默认它们互不干扰。但真实业务里,搜索的结果格式直接决定摘要工具能不能用。 - 假设二:工具描述足够让模型选对。工具少的时候成立,超过二十个之后,描述之间的语义重叠会让选择准确率明显下降。
- 假设三:工具是无状态的。每次调用都是全新开始,但很多任务需要跨调用保持中间状态,比如“先查订单再根据订单状态决定是否退款”。
这三个假设在小规模 demo 里都不成问题,一旦进入真实场景,它们就是三颗定时炸弹。
2.2 工具膨胀为什么必然发生
我观察过自己经手的几个项目,工具数量的增长曲线几乎都是一样的:第一周 5 个,第一个月 15 个,第三个月 40 个以上。原因很简单,业务需求是加法驱动的,每来一个新场景,最直觉的做法就是加一个 Tool。
问题在于,Tool 的边际成本不是线性的。我做过一个粗略统计,在一个 40 工具的系统里:
| 工具数量 | 工具选择准确率(我的实测) | 平均上下文占用 |
|---|---|---|
| 5 | 约 95% | 低 |
| 15 | 约 88% | 中 |
| 30 | 约 76% | 高 |
| 40+ | 约 65% | 很高 |
这个数据不是学术结论,是我在几个项目里用同一批测试用例跑出来的趋势,具体数值会因模型和任务而异,但趋势很稳定:工具越多,选得越不准,上下文越贵。这就是工具膨胀的代价。
2.3 一个具体的翻车现场
说个具体的。之前做一个内部知识助手,工具列表里有search_docs、search_wiki、search_tickets三个搜索类工具。模型经常在该用search_docs的时候选了search_wiki,因为两者的描述都写了“搜索内部知识”。我改描述改了三次,每次好一阵子又退化。
后来我意识到,这不是描述的问题,是抽象层级的问题。这三个工具在业务上其实是一个能力——“搜索内部知识”,只是数据源不同。把它们拆成三个 Tool,等于强迫模型在错误的层级上做决策。正确的做法是把它做成一个 Skill,内部根据查询意图路由到不同数据源,模型只需要决定“要不要搜索知识”,不需要决定“搜哪个库”。
这个认知转变,是我从 Tool 思维转向 Skill 思维的起点。
3. Skill 的本质:带上下文的能力编排单元
3.1 Skill 的构成要素
一个完整的 Skill,我通常会包含这几个部分:
- 触发条件:什么情况下这个 Skill 应该被激活。可以是语义描述,也可以是前置状态判断。
- 编排逻辑:内部调用哪些 Tool、按什么顺序、如何处理中间结果。
- 上下文契约:需要哪些输入上下文,产出哪些输出上下文,供后续 Skill 使用。
- 失败处理:某一步失败时是重试、降级还是终止。
- 元数据:版本、依赖、适用场景标签,这些是自演进的基础。
对比一下 Tool,Tool 只有“函数 + 描述 + schema”,Skill 多了编排、上下文和元数据。多出来的这些,正是解决工具膨胀的关键。
3.2 为什么 Skill 能缓解工具膨胀
核心在于抽象层级的提升。Tool 是扁平的,所有能力都在同一层,模型要在一堆平级选项里挑。Skill 是分层的,模型先决定用哪个 Skill(数量可控),Skill 内部再决定用哪个 Tool(由代码逻辑决定,不消耗模型的决策预算)。
打个比方,Tool 像是把家里所有工具摊在桌上让模型挑,Skill 像是把工具收进几个抽屉,抽屉上贴标签,模型先选抽屉,打开抽屉后里面的工具怎么用是固定的。桌子清爽了,选择也准了。
我实测下来,把 40 个 Tool 重构为 8 个 Skill(每个 Skill 内部平均编排 5 个 Tool)之后,顶层选择准确率从 65% 回升到 90% 以上,而且上下文占用降了将近一半,因为模型不需要同时看到所有 Tool 的完整 schema。
3.3 Skill 不是银弹:它引入的新问题
得说清楚,Skill 不是免费的。它引入了几个新问题:
- 编排逻辑的维护成本:Skill 内部的流程是代码,改流程要改代码,不像加 Tool 那么随意。
- 粒度的拿捏:Skill 太粗,内部逻辑复杂难维护;太细,又退化成 Tool。
- 调试难度上升:一个 Skill 出错,可能是编排问题,也可能是某个 Tool 问题,排查链路变长。
所以 Skill 和 Tool 不是替代关系,是分层协作关系。我的经验是:原子操作做 Tool,业务语义做 Skill。判断标准很简单——如果一段能力需要“先做 A 再做 B 根据 B 的结果决定做 C 还是 D”,它就是 Skill;如果它就是“输入 X 输出 Y”,它就是 Tool。
4. 技能库的自演进:让 Skill 自己长出来
4.1 为什么技能库需要演进
静态的技能库有个致命问题:它假设设计者能预判所有场景。但真实业务里,用户的需求是长尾的,你今天想不到的场景,明天就会出现。如果每次都要人工加 Skill,那技能库永远滞后于需求。
自演进要解决的就是这个滞后。它的目标是:让系统在运行过程中,从实际使用数据里发现“缺了什么 Skill”“哪个 Skill 该拆”“哪个 Skill 该合”,并给出可执行的调整建议,甚至自动完成调整。
4.2 演进的两个方向:增量和结构
我把自演进分成两类:
增量演进:发现缺失的 Skill。比如系统频繁出现“用户问 A,现有 Skill 都无法很好处理,最后靠模型硬答”的情况,这就是一个信号——需要一个新 Skill 覆盖 A 场景。
结构演进:调整现有 Skill 的边界。比如两个 Skill 的触发条件高度重叠,说明它们该合并;或者一个 Skill 内部逻辑分支过多,说明它该拆分。
增量演进相对好做,结构演进难得多,因为它涉及对现有 Skill 的修改,风险更高。
4.3 演进信号的采集
自演进的前提是有信号。我通常采集这几类:
- 未命中日志:用户请求没有匹配到任何 Skill,或者匹配到的 Skill 执行失败。
- 降级日志:Skill 执行中某步失败走了降级路径,说明这个 Skill 的编排不够健壮。
- 人工接管日志:模型处理不了转人工的 case,这是最强烈的信号。
- 执行耗时与成功率:某个 Skill 成功率持续偏低,可能是编排有问题。
这些信号采集起来不难,难的是从信号到决策。我的做法是定期(比如每周)跑一次分析,把信号聚类,人工review后决定是否调整。全自动的演进我试过,风险太大,一个错误的自动合并可能让整个技能库退化。
4.4 一个增量演进的实例
说个真实的。有个做文档处理的 Agent,运行两周后日志显示,大量请求是“把这份 PDF 转成结构化数据并校验”。当时技能库里只有parse_pdf和validate_data两个独立 Skill,模型经常只调一个就结束,导致结果不完整。
分析后我加了一个pdf_to_structured的 Skill,内部编排解析和校验两步,并强制校验不通过就重试解析。上线后这类请求的成功率从 60% 出头涨到 90% 以上。这个 Skill 不是我拍脑袋想的,是日志告诉我“用户实际在做的是一件事,而系统把它拆成了两件”。
这就是自演进的价值:它让技能库跟着真实使用走,而不是跟着设计者的想象走。
5. 动态加载:让技能库按需生长
5.1 为什么不能一次性全加载
技能库大了之后,全量加载有两个问题:上下文爆炸和启动变慢。假设你有 50 个 Skill,每个 Skill 的描述加元数据平均 200 token,全量注入就是 10000 token,还没开始干活就烧掉一大截预算。而且大部分请求只会用到其中两三个 Skill,剩下的纯属浪费。
动态加载的思路是:只加载当前请求可能用到的 Skill。这需要一套检索机制,根据请求内容召回相关 Skill,再注入上下文。
5.2 动态加载的三种实现路径
我试过三种,各有适用场景:
路径一:向量召回。把所有 Skill 的描述向量化,请求来了先做相似度检索,召回 top-k 个 Skill 注入。优点是实现简单,缺点是召回质量依赖描述质量,且对“需要组合多个 Skill”的场景不友好。
路径二:分层路由。先用一个轻量分类器判断请求属于哪个大类,再加载该大类下的 Skill。优点是可控,缺点是需要维护分类体系,新场景要更新分类器。
路径三:渐进式加载。先只加载最核心的几个 Skill,模型发现不够用时主动请求“加载更多”,系统再按需注入。这个最灵活,但对模型的元认知能力要求高,实现也最复杂。
我目前的主力方案是路径一 + 路径三的混合:默认向量召回 top-5,同时给模型一个load_skill的元工具,允许它主动加载。实测下来,召回覆盖了大部分场景,主动加载兜住了长尾。
5.3 动态加载的工程细节
这块坑不少,说几个关键的:
Skill 描述的写法直接影响召回质量。描述不能只写“这个 Skill 做什么”,还要写“什么情况下用”。我通常要求描述包含:能力概述、适用场景、不适用场景、典型输入示例。这样向量召回才能区分开相似 Skill。
召回数量要控制。召回太多等于没召回,召回太少容易漏。我的经验值是 top-5 到 top-8,具体看 Skill 的平均粒度和请求的复杂度。
要有兜底机制。召回失败或者召回的 Skill 都不适用时,要有降级路径,比如回退到全量加载或者直接让模型硬答。不能因为召回失败就让整个请求挂掉。
缓存召回结果。相似请求的召回结果往往一致,缓存能省不少向量检索的开销。我用的是请求指纹做 key,命中率大概能到 40%。
5.4 动态加载与自演进的配合
这两个机制是互相成就的。动态加载让技能库可以变大而不拖累性能,自演进让技能库可以变大而不失控。没有动态加载,自演进加进来的 Skill 会拖垮上下文;没有自演进,动态加载只是在管理一个僵化的库。
我的系统里,自演进产出的新 Skill 会先进入“候选池”,动态加载默认不召回候选池的 Skill,只有人工确认后才转正。这样既保证了演进速度,又控制了风险。
6. 实操:从零搭一个带自演进的技能库
6.1 整体架构
我用的架构分四层:
- 接入层:接收请求,做初步的意图识别。
- 召回层:根据请求召回相关 Skill,动态注入上下文。
- 执行层:运行 Skill 的编排逻辑,调用底层 Tool。
- 演进层:采集执行日志,定期分析,产出 Skill 调整建议。
这四层里,召回层和演进层是 Skill 体系区别于纯 Tool 体系的关键,也是我花时间最多的地方。
6.2 Skill 的数据结构
我用的是类似这样的结构(伪代码,具体语言随意):
skill = { "id": "pdf_to_structured", "version": "1.2.0", "description": "将 PDF 文档解析为结构化数据并校验完整性", "when_to_use": "用户提供 PDF 并要求提取结构化信息时", "when_not_to_use": "用户只是要 PDF 的文本内容,不需要结构化", "inputs": ["pdf_url"], "outputs": ["structured_data", "validation_report"], "orchestration": [ {"tool": "parse_pdf", "retry": 2}, {"tool": "validate_data", "on_fail": "retry_parse"}, ], "tags": ["document", "extraction"], "status": "active", # active / candidate / deprecated }几个设计要点:when_to_use和when_not_to_use是给召回用的,必须写清楚;orchestration是编排逻辑,用声明式而不是命令式,方便演进层分析和修改;status字段支撑候选池机制。
6.3 召回层的实现
召回层我做了两路并行:向量召回和标签过滤。向量召回负责语义相似,标签过滤负责硬性约束(比如某些 Skill 只在特定租户下可用)。两路结果合并去重后取 top-k。
向量化用的是通用的 embedding 模型,Skill 描述拼接description + when_to_use作为向量文本。这里有个细节:when_not_to_use不参与向量化,但会在召回后做二次过滤,把明确不适用的 Skill 剔除。
6.4 演进层的实现
演进层是个离线任务,每周跑一次。流程是:
- 拉取过去一周的执行日志。
- 聚类未命中和降级 case。
- 对每个聚类,判断是“缺 Skill”还是“现有 Skill 有问题”。
- 缺 Skill 的,生成候选 Skill 草稿;有问题的,生成调整建议。
- 人工 review,确认后写入技能库。
第 4 步的草稿生成我用了模型辅助,让它根据聚类里的请求样本,草拟 Skill 的描述和编排。但草稿必须人工确认,我试过全自动写入,出过一次事故——模型把一个本该拆分的 Skill 合并了,导致后续一周的相关请求全部走错路径。
6.5 上线后的效果
这套东西在一个文档处理 Agent 上跑了三个月,几个关键指标:
| 指标 | 上线前 | 上线后 |
|---|---|---|
| 顶层选择准确率 | 约 70% | 约 91% |
| 平均上下文 token | 约 8000 | 约 3500 |
| 未命中率 | 约 18% | 约 6% |
| 技能库 Skill 数 | 12(静态) | 23(含演进新增) |
Skill 数涨了将近一倍,但上下文反而降了,这就是动态加载的功劳。未命中率降下来,是自演进的功劳。
7. 踩过的坑与排查技巧
7.1 常见问题速查
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 召回总是漏掉某个 Skill | 描述里缺少场景词 | 检查 when_to_use 是否覆盖真实请求的表达 |
| 召回结果高度重复 | Skill 粒度过细 | 考虑合并语义重叠的 Skill |
| 模型不按 Skill 编排走 | 编排逻辑对模型不透明 | 检查是否需要把编排暴露给模型 |
| 自演进产出大量低质候选 | 信号聚类粒度过粗 | 调整聚类参数,增加人工过滤 |
| 动态加载后响应变慢 | 向量检索成为瓶颈 | 加缓存,或改用更轻量的召回方式 |
7.2 几个独家避坑经验
坑一:Skill 描述写得太“官方”。我一开始按写文档的习惯写 Skill 描述,结果召回效果很差。后来发现,描述要贴近用户的真实表达,甚至要把用户可能说的口语化说法写进去。比如“帮我看看这个 PDF”这种,比“解析 PDF 文档”召回效果好得多。
坑二:编排逻辑过度依赖模型。早期我让模型在 Skill 内部也做决策,结果 Skill 的执行路径变得不可预测。后来改成编排逻辑由代码固定,模型只在 Skill 之间做选择,稳定性大幅提升。Skill 内部要确定性,Skill 之间可以灵活。
坑三:自演进频率太高。我试过每天跑一次演进,结果技能库天天变,模型的行为跟着飘,用户反馈“这个助手怎么一天一个样”。后来改成每周一次,稳定多了。演进不是越快越好,要给系统留出稳定的窗口。
坑四:忽略 Skill 的版本管理。Skill 改了之后,历史请求的复现会出问题。后来我给每个 Skill 加了版本号,日志里记录用的是哪个版本,排查问题时能精确复现。
坑五:候选池没有过期机制。候选 Skill 如果一直没人确认,会越积越多。我加了个 30 天过期,过期自动归档,需要时再捞出来。
7.3 一个排查实例
有段时间发现某个 Skill 的成功率突然从 90% 掉到 70%。查日志发现,是它内部调用的一个 Tool 的返回格式变了(上游服务升级),导致编排里的解析逻辑失败。这个问题的教训是:Skill 的编排逻辑要对底层 Tool 的变化有感知。后来我在编排里加了输出 schema 校验,格式不对就报警,而不是静默失败。
8. 我对 Skill 体系的一些个人判断
折腾了这么久,有几个判断越来越清晰。
第一,Skill 和 Tool 会长期共存,不是替代关系。Tool 负责原子能力,Skill 负责业务编排,这个分层是稳定的。试图用 Skill 替代所有 Tool,会让 Skill 变得臃肿;试图用 Tool 解决所有问题,会陷入工具膨胀。
第二,自演进的重点是“发现”而不是“自动修改”。让系统自动发现“缺了什么”价值很大,让系统自动修改技能库风险很大。我现在把自演进的产出定位为“建议”,人工确认这一步短期内不会去掉。
第三,动态加载的瓶颈在召回质量,不在加载机制。加载本身不难,难的是召回得准。我在这上面花的时间,八成在优化 Skill 描述和召回策略,两成在写加载逻辑。
第四,技能库的规模不是越大越好。我见过有人追求“技能库上千个 Skill”,但实际用到的可能就几十个。技能库的价值在于覆盖真实场景,不在于数量。定期清理 deprecated 的 Skill,比不断新增更重要。
最后分享一个我最近在试的方向:把 Skill 的编排逻辑用声明式的 DSL 描述,而不是直接写代码。这样演进层可以直接分析和修改编排,而不需要人去读代码。目前还在早期,效果不好说,但方向我觉得是对的——让技能库的结构本身可被程序理解和操作,是自演进走向更深层的前提。
如果你也在做类似的东西,欢迎交流。这个领域变化太快,一个人踩坑太慢,互相抄作业才是正道。