前阵子我们团队在给一个内部智能体平台做能力升级,遇到一个特别让人头大的问题:业务方接二连三提出新场景,每个场景我们都要往 system prompt 里塞一大段指令,再挂上一堆工具函数。结果不到两个月,主 prompt 就膨胀到四千多行,单次请求烧掉的 token 越来越多,模型的执行效果却越来越飘,经常出现"这个场景的规则压住了另一个场景的规则"这种诡异情况。后来我们做了一个决定:把智能体的外挂能力从"prompt 里手写指令"彻底改成"统一封装成技能包",项目代号就叫 agent-skills。这篇分享就是我在这套系统从设计到落地、再到线上跑稳定之后,想认认真真写下来的东西——包括技能规范怎么定、目录怎么搭、调度怎么做、评估怎么跑,以及上线后踩过的那些坑。
整个思路其实很直白:一个智能体要能干的事会越来越多,但模型上下文窗口是有限的,prompt 里塞不了无限规则,于是把能力沉淀成一个个可独立管理、可独立评估、可独立复用的技能单元,让 agent 在需要的时候按需加载、按需调用。这听起来不复杂,真正做起来之后才发现,坑全藏在细节里。这篇文章适合正在搭 agent 平台、想把智能体能力组件化的团队参考,也适合那些已经对接了不少 function calling、但感觉工具一多就开始失控的开发者。
1. 为什么我决定给智能体建"技能库"而不是继续堆 Prompt
1.1 从一段越改越乱的 system prompt 说起
我把时间线拉回去一点。最初我们的智能体只负责做一件事:根据用户的提问去查数据仓库、生成统计报表。那时候 system prompt 结构很清晰,规则就那么几条,工具也就三四个,模型跑得又稳又准。后来第二个场景进来了——要做定时邮件简报;接着第三个场景进来了——要在办公系统里创建审批流程。每个场景都不是简单的"调一个 API",而是有一套自己的业务规则、校验逻辑、边界条件。我们团队的做法很朴素:在 system prompt 里追加章节,把新场景的规则写进去,把新工具的函数签名挂上去。
问题就出在这里。当 prompt 里的规则超过一定数量后,模型的行为开始变得不可预测。我们复盘过几次线上事故,发现一个很典型的现象:用户明明在问邮件简报的事情,模型却去调用了建审批流的工具,甚至把邮件内容填进了审批意见里。某个规则的存在本身会污染其他规则的判断,尤其是在两个场景的描述里出现相似词语的时候。
这不是因为我们 prompt 写得不好,而是 LLM 的注意力机制天生就有短板——当上下文里塞了大量不相关的指令时,真正和当前任务有关的那部分指令会被"淹没"。这有点像你让一个实习生同时背十份操作手册,再让他现场判断眼前这个任务该按哪一份执行,他大概率会在相似流程之间犯迷糊。与其逼着模型每轮请求都处理全部规则,不如把规则拆开、封装成独立的技能包,让模型在需要时只加载相关的那一份。
1.2 skills 和函数调用、MCP 到底差在哪
很多人第一次听说 agent-skills,会直接把它理解成"换了个名字的 function calling"。这也是我们团队内部最初讨论时最大的迷思。函数调用(function calling / tool use)解决的是"模型该调用哪个工具"的问题,它有一个决定性的特点:工具是原子的、无状态的。一个函数执行完了就完了,函数本身不携带复杂的执行逻辑和判断规则,你给它什么参数,它返回什么结果。
但技能(skill)是一个更大的单元。它不仅包含"能对外提供什么操作",还包含"这个操作该怎么执行、什么情况不能执行、执行时要注意什么"。举个例子:函数调用层面,你只需要定义一个send_slack_message(channel, text)就够了;但技能层面,你还要告诉模型:
- 什么类型的消息适合发到哪个 channel;
- 消息长度超过多少需要截断;
- 哪些敏感内容必须经过脱敏再发送;
- 如果目标 channel 不存在,是自动创建还是直接报错。
这些执行规则如果都塞在 system prompt 里,会随调用场景膨胀;如果写死在代码里,模型在编排时又看不见这些约束,容易生成不合规的调用。技能的真正定位,是把"工具定义 + 执行指令 + 边界规则"打包成一套自包含的说明文件,让模型在执行前能通读这份说明,按说明行事。
至于 MCP(Model Context Protocol)这类协议,它更像是技能的上层运输标准——定义了工具怎么被发现、怎么被调用、怎么传参。skills 和 MCP 不是替代关系,而是不同抽象层的东西。我们最初没有强依赖任何平台协议,而是自己定义了一套 YAML 契约,等协议标准成熟后再做桥接。核心优先保证的是:先把能力沉淀和调度逻辑跑通,协议是后话。
1.3 什么时候该上技能库:三个判断条件
不是所有项目都需要从一开始就上技能库,我自己也不建议一上来就追求复杂的架构。但当你遇到下面三个信号中的至少两个,就该认真考虑这个方案了:
- 场景数量开始膨胀:智能体承担的职责超过三个以上,且每个职责都有自己的业务规则。这个数量级下,把规则全部写进 system prompt 已经是灾难的起点;
- 工具调用频繁出错:模型经常在相似工具之间选错,或者因为 prompt 里的规则互相冲突,导致同一个问题这次答对、下次答错;
- 上下文成本失控:为了兼顾所有场景,system prompt 越来越长,单轮请求的 token 消耗变得肉眼可见地高,而实际每个任务真正用到的指令片段只占很小一部分。
agent-skills 的核心理念就是按需加载:模型面对一个任务时,先检索最相关的技能,只把这几份技能说明注入上下文,而不是把全部规则常驻在 prompt 里。这样做的好处非常直接——prompt 短了、调用准了、规则之间不再相互干扰,每个技能也能被独立测试、独立升级,不再牵一发动全身。
2. 技能长什么样:一份能落地的 Skill 定义规范
2.1 描述是技能的"门面",也是最大的杠杆
在设计技能规范时,我们花了大量时间讨论一个看起来特别不起眼的字段:description。但后来所有实践都反复证明,这个字段才是整个技能定义里最重要的部分。道理很简单:在绝大多数 agent 架构里,模型判断"当前要不要调用这个技能"时,主要依赖的就是 name 和 description。真正到执行阶段才会去读完整的 instructions,而如果第一步的触发判断就错了,后面写得再详细也没有意义。
我们最早的版本里,有一个技能描述是这样写的:
这个技能用于处理数据库相关的事务,包括查询数据、统计数据、分析数据等。这种描述写了等于没写。它的覆盖范围太宽了,看不出和另一个"数据分析报表生成"技能有什么区别,模型在选择时只能靠猜。后来我们把描述改成了更接近"触发条件 + 典型场景 + 明确边界"的写法:
description: > 仅当用户需要直接对数据仓库执行只读 SQL 查询、探数、取数或 基于查询结果做简单统计时使用。典型问法包括"查一下最近7天的 订单量""各城市的销售额分布"。不适用于生成图表和PPT报表, 这类需求请交给 report_generator 技能。对比之下,后者的可辨识度要高得多。它告诉模型:什么情况下该用、典型问法长什么样、什么情况下不该用、该用别的什么技能。"不该用"这个信息尤其关键,它给模型一把尺子,能有效减少技能之间的误触发。
我强烈建议每个技能的描述都写成这种结构:触发场景 + 典型示例 + 明确边界。描述写不好,后面所有调度策略都是白搭。
2.2 参数设计:少而强,胜过多而全
技能要不要定义显式参数,其实是个很值得推敲的问题。最初我们倾向于把所有能被模型推断的信息都定义成参数,觉得这样调用起来更精确。结果上线后模型频繁出错——参数太多,模型在生成入参时产生了幻觉,经常编造出一些根本不在会话上下文里的值。
后来我们做了一个减法,把技能参数控制在 5 个左右,最多不超过 7 个,并且遵循一条铁律:参数必须是模型能从已有对话中直接提取或确认的信息,不能要求模型凭空推理出业务信息。比如一个"发送仓库到期提醒"的技能,我们最初定义了warehouse_id, warehouse_name, owner, expire_date, remind_channel, priority, remark七个参数,模型经常把仓库名字张冠李戴。后来我们把参数砍到三个:warehouse_id(必须来自会话上下文或用户明确指定)、remind_channel(枚举,默认走系统配置)、priority(枚举,默认 normal),执行时仓库名、负责人、到期时间全部由代码在获取warehouse_id后回源补齐。
这背后是模型行为的一个常识:模型擅长的部分是"理解意图、编排流程、生成文本",而不够擅长的是"精确记忆和复述数据"。既然不够擅长,就不要把容易出错的步骤暴露给它,凡是能回源查询、能用系统配置兜底的,都尽量让它做减法。参数层面的另一个重点是要为每个参数提供examples,尤其是字符串类型的参数,给一个标准样例能让模型快速理解格式要求,减少凭空发挥的空间。
2.3 指令部分的写法:给执行者足够的边界
skill 的核心是一段 instructions,这段内容会被注入到模型上下文里,指导它完成任务。我们最终采用的规范是三段式结构:前置校验、执行步骤、禁止事项。
前置校验解决的是"什么情况下不能做":比如执行只读 SQL 查询前,先判断语句里有没有 UPDATE、DELETE、DDL,有就拒绝执行并返回错误原因。执行步骤是把任务的典型流程写清楚:先做什么、再做什么、遇到什么情况走什么分支。禁止事项则是对模型行为的约束:不能臆造数据库表名、不能把聚合结果过度解释成趋势结论、不能输出原始敏感数据等。
这里有一个很关键的实操经验:instructions 不能太长。一个技能的 instructions 如果超过 1200 字,它对模型的约束力会明显下降,因为模型在长文本里的规则遵循度不是线性的。我们给每个技能的 instructions 设了硬性上限,超出就必须拆分逻辑,把一部分约束下沉到代码校验里完成,而不是全靠模型自觉。
name: sql_query_executor version: 1.4.0 capability: database description: > 仅当用户需要直接对已接入的数据仓库执行只读查询、探数、取数时使用, 支持 MySQL / PostgreSQL 数据源。典型问法包括"查一下最近7天的订单量" "各城市的销售额分布"。不适用于图表生成和PPT报表。 parameters: - name: query type: string description: 完整且可执行的只读 SQL 语句 required: true examples: ["SELECT count(*) FROM orders WHERE created_at >= now() - interval '7 day'"] - name: datasource type: enum values: [analytics_prod, analytics_dev] default: analytics_prod instructions: | 前置校验: 1. 必须检查SQL是否包含 UPDATE / DELETE / ALTER / DROP / INSERT 等非只读关键字,存在则直接拒绝执行,返回错误码 ERR_READONLY。 2. 必须确认 FROM 子句中引用的表都在已注册的表清单中,不确定时先执行 list_tables 查询。 执行流程: 1. 调用执行器运行 SQL,设置 5 分钟超时。 2. 查询结果超过 200 行时返回前 200 行并附上 total_count。 3. 查询失败时,用数据库返回的原始错误信息,不做任何美化或补全。 禁止事项: - 禁止编造或臆测不存在的表名和字段名。 - 禁止将查询结果直接包装为业务结论。可以呈现数据,但不应替用户做率化总结。 - 禁止在一条技能调用中执行多条 SQL。2.4 注册与组织:名称、版本、能力域的约束
技能的元信息同样需要约束,否则库一大了就是一团乱麻。我们给技能定了三件套:name 必须全局唯一且用蛇形命名;version 必须显式声明,agent 平台会锁定技能版本,不允许"漂移"到最新版;capability 必须归属于某个能力域,后面做路由和检索都依赖这个字段。
早期我们没管命名规范,团队里出现了send_email_to_owner和email_sender_v2这种两个名字风格完全不搭、功能却高度重叠的技能。后来我们在代码评审里把命名规范直接卡死了,并且定期跑技能清单去重,凡是 description 余弦相似度过高的技能,都要求合并或明确差异化定位。这个过程相当于是给技能库做"物业管理",脏乱差会直接传导到模型的选择准确率上。
3. 从零搭建可扩展的 skills 目录与发布流程
3.1 目录结构:按领域分层,不按接口平铺
技能的数量一旦上了几十个,目录组织就是第一个要解决的问题。我们最初把所有技能平铺在一个文件夹里,结果找一个技能要靠搜索,而技能之间的边界也变得越来越模糊。重构之后,我们采用了"领域 -> 技能"两级目录,每个领域内再按照"该领域的角色或任务类型"细分技能。
skills/ database/ sql_query_executor/ SKILL.md scripts/ execute.py tests/ cases.yml metadata.yaml table_metadata_explorer/ SKILL.md ... communication/ slack_notify/ SKILL.md ... email_digest/ SKILL.md ... approval/ create_approval_flow/ SKILL.md ...每一个技能目录都是自包含的:SKILL.md 是刚才那段 YAML 契约的完整扩展,scripts 目录放实际执行的代码,tests 目录放这个技能自己的评估用例,metadata.yaml 放负责人、创建时间、变更记录等管理信息。这样做的好处是:技能可以被独立检出、独立测试、独立回滚,发布时只需要打包这一个目录。
我特别想提醒一点:不要按接口来组织技能。一个后台管理系统可能有几百个 API,但真正能沉淀为技能的是"面对一个具体任务时的完整处置流程",它往往要编排多个 API。按接口平铺会让技能退化成函数清单,失去"技能"作为一层业务语义的意义。
3.2 skill 之间的依赖与复用
技能设计过程中必然会遇到重复:两个技能都要做"从企业微信发送消息"这样的底层动作。我们最初的想法是把这一步也做成一个技能,让高层的技能来调用它。实践之后发现过度抽象会带来调度成本——模型在一个任务里要依次选中两个技能,链路越长越容易在某一步选错。
后来我们的口径是:底层基础能力尽量下沉为内部函数,只有面向完整任务的工作流才暴露为技能。也就是说,"发企业微信消息"是一个内部函数,它被slack_notify、email_digest等技能各自引用;而"给用户发送每日仓库健康报告"才是一个技能。这个区分大大减少了模型需要抉择的技能数量,也避免了技能之间蜘蛛网一样的调用关系。
说得更直白一点:技能是给模型看的菜单,菜单上每一道菜都应该是完整的菜,而不是展示一堆半成品让模型自己去组合。过度拆分技能会让模型在组合时天马行空,反而把流程跑乱。
3.3 发布链路:变更、回归、灰度三步走
技能的本质是一份会直接影响模型行为的配置,它的变更风险比普通代码更高,因为同样的功能改动,在代码层面测试通过不代表在 agent 行为层面也通过了。我们最终定下的发布链路分三步:
第一步是单技能回归。每个技能在变更后,必须跑完它自己的 tests/cases.yml 里的全部用例。这些用例不仅验证结果正确,还验证"模型在给定 query 下是否选了该技能、是否生成了合法参数"。
第二步是端到端模拟。挑 10 到 20 个串联了多个技能的复杂任务,在模拟环境里整体跑一遍。这一步的目的不是测试技能本身,而是测试变更后的描述会不会影响到其他技能的触发率——这是技能库场景最隐蔽的破坏方式:A 技能的描述改了一个词,结果 B 技能的召回率掉了一截。
第三步是线上灰度。把新版本技能以一定比例切到真实流量里,观察调用率和失败率曲线,确认没有异常后再全网发布。这当然需要平台层面的流量染色能力,没有这套能力的团队,至少也要做 AB 对比测试。
4. Agent 如何"学会用"技能:三种调度方式的选择
4.1 静态挂载:小规模里的最优解
技能系统的第一步,是解决"模型怎么知道有哪些技能可用"。最简单的做法当然是静态挂载:把所有技能的定义全部塞进上下文,让模型在每一轮都能看到全部选项。这种做法的优点非常明显——实现简单、无额外检索延迟、模型是在全量信息下做决策。缺点同样直接:上下文长度不可控,技能一多,prompt 膨胀问题就又回来了。
在我们技能数少于 20 个的时候,静态挂载完全够用,而且准确率相当高。如果你刚起步,我建议就从静态挂载开始,不要一上来就搞向量检索。技能数量没到那个规模时,检索的收益微乎其微,反而多了一层可能出错的环节。
4.2 向量检索动态加载:技能库膨胀时的标配
当技能数超过 30,尤其是超过 50 之后,每轮请求把所有技能说明都读一遍就变得非常不划算。token 成本高不说,模型处理超长上下文时对选项的辨识度也会下降。这个阶段,我们的方案是把技能描述向量化,做成索引,在每次请求时先拿用户 query 做语义检索,召回 top 3 到 5 个候选技能,只把这几份技能说明注入上下文。
实操中有几个细节直接影响检索质量。第一,向量化一定不能只用 description 一个字段,要把"技能名称 + 描述 + 典型示例问法"拼起来一起向量化,尤其要强调在测试集上统计召回率。第二,top k 的选择很关键:k 太小召回容易漏,k 太大上下文还是会膨胀,我们实测 4 是一个不错的中间值,可以根据业务调整。第三,不要只信向量相似度,插件级别的候选集是向量召回 + 规则过滤的组合,比如用户问了邮件相关的问题,就至少要在候选集里保底挂上邮件技能。
4.3 规划器路由:复杂长链路任务的掌控力
对于那种要串起好多步骤的长链路任务——比如"每天早上9点统计前一天的库存数据、生成简报、分发给订阅用户"——动态检索 + 单轮判定就不够用了。这种情况下,我们引入了规划器路由:先由一个规划模块理解用户意图、拆解子任务,再为每个子任务选定技能。这相当于把"选择技能"这个动作从执行阶段提前到了规划阶段,让整体链路更可控。
规划器路由需要额外注意超时和兜底。规划器本身也是一次模型调用,它拆解出来的子任务可能超过 5 个,每个子任务的技能调用都有失败的可能,所以必须给整个链路准备好超时、重试和失败降级策略。我们从实践中得到的一个体会是:规划器路由适合任务编排,不适合简单问答。简单场景用它,反而引入无谓的延迟和判定风险。
4.4 三种方式的取舍与实测数据
| 调度方式 | 适用技能数量 | 单轮延迟 | 实现成本 | 长链路支持 | 我们的推荐场景 |
|---|---|---|---|---|---|
| 静态挂载 | < 20 | 最低 | 最低 | 中 | 初期搭建、任务类型固定 |
| 向量检索动态加载 | 20 ~ 200 | 低 | 中 | 中 | 技能库规模扩张后的标配 |
| 规划器路由 | 任意,适合复杂任务 | 较高 | 高 | 强 | 多步骤、强编排的复合任务 |
我们实际线上的架构是三层混合:轻量任务直接走全量静态挂载(因为我们保留了一个包含全部技能的"轻量索引"用于触发判断),多数任务走向量检索动态加载,少数高价值的复杂链路由规划器接管。这个组合跑了几个月,整体技能命中率稳定在 92% 以上,上下文 token 消耗比最初的"全量塞 prompt"时代下降了约 60%。
5. 技能评估:让"会不会用技能"变成可量化指标
5.1 评估集:三类样本缺一不可
技能评估最容易被忽视的部分是评估集的构成。我们最初给每个技能只写了正常用例,测试结果一片大好,上线后才发现模型经常乱触发技能。后来我们把评估集扩展为三部分:
正常路径用例是最基本的,描述这个技能的典型使用场景,预期是"必须调用该技能且参数正确"。边界路径用例是描述临界场景,比如"用户要的数据接近这个技能的能力边界但又不完全是",预期是"模型能判断是否该调用,或明确向用户确认,而不是强行调用"。反例路径用例尤其重要,预期是"模型绝不能调用该技能"。我们给每个技能配置了至少 5 条反例,比如对sql_query_executor技能来说,"帮我做一个汇报PPT"就是一条标准反例,模型应该把这个问题转给其他技能,而不是试图用 SQL 解决问题。
这三类样本合起来,构成一个技能模块的"行为底线测试"。任何技能变更都要保证这三类样本的表现不倒退,否则不允许合并发布。
5.2 核心指标:不止看成功率,还要看调用倾向
评估技能,光看执行成功与否远远不够。我们长期盯的指标有四个:
- 召回率:应当调用该技能的 query 中,模型实际触发该技能的占比;
- 误触发率:不该调用该技能的 query 中,模型错误触发该技能的占比;
- 参数准确率:调用时生成的参数能被目标系统直接接受的比例;
- 端到端成功率:一次任务从触发到最终结果符合用户预期,中间没有返工和中断的比例。
这四个指标放在一起,才能判断出一个技能有没有被"正确地使用"。有的技能功能本身写得很完善,但描述写得含糊,导致召回率低;有的技能功能没问题,但描述覆盖太宽,导致误触发率高。不对这两类问题进行分别度量,就无法定位问题到底出在技能实现还是技能触达。
5.3 评估怎么跑:离线回归、模拟环境、在线灰度
我们的做法分三层。离线回归跑的是技能自带的 tests 目录,每次 CI 变更自动触发,100 条以内的用例在分钟级跑完,这是第一道闸门。模拟环境是人工构造了十几个长链路任务,覆盖多技能协作、故障注入等场景,在发版前手动或半自动执行,这是第二道闸门。在线灰度就是前面提到过的流量染色,观察线上真实指标,特别是召回率和误触发率有没有漂移,这是第三道闸门。
这套评估体系建立之后,我们团队对技能改动的心态发生了根本变化。以前每次改一个技能描述都提心吊胆,怕影响其他场景;现在只要三道闸门全过,可以比较放心地发布。而事实上,线上出过的问题,基本都能倒推到某个环节的评估用例没补全——换句话说,只要评估集本身覆盖到位,绝大多数问题在离线阶段就能拦住。
6. 踩坑复盘:agent-skills 上线后我遇到的三类问题
6.1 技能描述冲突,导致 agent 随机选错
这是上线早期最头疼的问题。当时平台里有两个技能:"仓库到期提醒"和"仓库容量预警",描述里都写了"当仓库状态异常时发送通知给相关负责人"。结果线上经常出现用户明明问的是容量预警,模型却调用了到期提醒技能,通知内容驴唇不对马嘴。
排查之后我们发现,问题不在模型,而在我们的描述太像了。两个技能的差异点被淹没在相同的表达里,模型只能靠猜。解决思路是给每个技能强化一个"专属特征词":到期提醒的描述里强调"按时间节点触发、订阅关系到期、续费节点",容量预警则强调"按历史用量趋势、预测峰值超限概率、预警阈值"。改完之后,两个技能的召回率都回到了正常水平。
这件事给我一个教训:技能库的规模管理,本质是语义空间的区隔管理。当两个技能的语义在向量空间中距离过近,它们就会互相干扰。新技能设计阶段就要先跑一遍相似度检查,描述相似度过高的,要么合并,要么强行加上区分特征。
6.2 参数幻觉:模型会自己编造入参
另一个高频问题是参数幻觉。模型在生成技能入参时,会倾向于"把对话里没出现过的信息补全"。比如用户只说了一句"看看北京仓的情况",模型在执行某个仓库查询技能时,竟然生成了warehouse_name=北京一号仓,把这个从未出现的仓库名当成了事实。
我们做了三个层面的防御。第一,在技能指令里明确写一条规则:"所有参数值必须来源于用户会话原文,不得推断或补全缺失信息,无法确认时向用户提问。"第二,在参数 schema 中使用枚举和默认值压缩模型的自由发挥空间。第三,在代码执行层增加参数校验,凡是参数值查不到对应业务记录时,返回明确的参数不合法错误,而不带默认行为。这三层叠加之后,参数准确率从 87% 提到了 96% 左右。
6.3 技能超过 100 个之后,检索开始失灵
向量检索在技能数几十个的规模下表现不错,但当我们把线上技能推到 150 多个时,召回质量明显下滑。表现是:top 4 候选中常常混入一两个完全不相关的技能,而真正相关的技能排名反而落到了候选集之外。分析下来,原因是不同领域的技能描述在向量空间中存在"相互吸引"——比如数据库领域和报表领域的描述,都频繁出现"数据""统计""分析"这些词,语义中心靠得很近,检索时容易互相干扰。
我们的解法是引入两级路由:先用一个轻量的领域分类器把用户 query 分到 20 个左右的 domain(比如 database、communication、approval、report…),再在 domain 内部做向量检索。这一步把候选空间缩小到十几个技能,检索质量立刻恢复。更重要的是,domain 这个维度提供了一种自然的管理边界,新技能评审时只要确认它属于某个既有 domain,就不太会跨域干扰到其他领域的技能。
这一套走下来,agent-skills 已经成了我们团队构建智能体能力的默认底座。最让我感慨的不是这个方案有多先进,而是它把一个原本靠"大 prompt 碰运气"的事情,变成了可以测试、可以衡量、可以迭代的工程问题。如果你也在准备给自己的 agent 搭技能体系,我的建议很朴素:先从业务里出现频率最高的五件事开始,把五个技能写透,把评估集建到三十条以上,再谈规模扩张。技能库这个东西,扩张远比收敛容易,难的是让每一个技能都经得起"换个场景照样能用"的考验。