1. 技能到底是什么:从"会聊天"到"会干活"的那道坎
过去一年我一直在折腾 AI 智能体的落地,最大的感受是:模型本身的智商已经不是瓶颈,真正卡住项目进度的是"skills"——也就是你喂给智能体的一套套可复用技能。模型再聪明,你不告诉它怎么调用工具、按什么格式输出、遇到异常怎么兜底,它照样会把简单任务做得乱七八糟。
有些朋友把 skills 理解成"给模型写个角色设定",其实差得很远。角色设定只是让模型"像谁",而技能是让模型"会做什么"。打个比方:你招了个实习生,人很聪明,但你得先给他一份操作手册,告诉他接到客户电话先说什么、工单系统怎么录入、遇到投诉升级给谁——这份操作手册就是技能。模型和实习生一样,缺的不是理解能力,而是稳定、可复用的操作流程。
以我自己的项目为例。我要做一个能自动整理周报的智能体,最开始只写了一句提示词"帮我汇总这周的进展",结果模型每次都自己发挥:有时输出表格,有时输出纯文本,有时连格式都对不上。后来我把它拆成了三个技能:collect_updates(拉取各渠道动态)、summarize_weekly(按固定模板生成摘要)、format_markdown(统一排版)。每个技能都有明确的输入输出约束,效果立刻稳定下来。
所以对"skills"最朴素的理解就是:把一类任务的操作经验固化下来,变成智能体可以反复调用的能力单元。它能解决的问题很具体——不用每次对话都重新解释任务背景、不用反复纠正输出格式、不用在多个工具之间手动搬运数据。适合所有正在做智能体应用、或者想把手头重复性工作自动化的开发者读一读,看完你就能按同样的思路把自己的任务拆成技能。
1.1 技能和提示词、插件到底有什么区别
很多人刚开始会混淆这三个概念,我把它们的边界理一下:
- 提示词(Prompt)是一次性的口头指令,写得好不好全看当场发挥,不可复用。
- 插件(Plugin)是外部工具的能力封装,比如能查天气、能发邮件,但插件本身不关心你的业务流程。
- 技能(Skill)介于两者之间:它规定了一个任务从输入到输出再到异常处理的完整路径,里面可以用提示词,也可以调用插件,但整体是一套可复用的流程。
所以技能更像一个"带流程的提示词 + 工具调用规则"。它不是让你少写提示词,而是让你把提示词升级成标准作业程序。
2. 给技能划边界:输入、输出与失败时怎么办
一个技能设计得好不好,关键看边界。我在第一版技能里最大的失误就是:只写清了"要做什么",没写清"输入是什么格式、输出必须长什么样、失败怎么办"。结果模型倒是按照要求去做了,但产出的东西五花八门,后处理代码根本没法统一接。
2.1 输入定义要严,但不能死
技能输入我建议用 JSON Schema 明确定义。拿上面的summarize_weekly技能举例,它的输入大概是这样一个结构:
{ "name": "summarize_weekly", "description": "将分散的周报素材整理为结构化周报摘要", "input_schema": { "type": "object", "properties": { "raw_materials": { "type": "array", "items": { "type": "string" }, "description": "本周的工作记录、会议纪要、进展片段" }, "focus": { "type": "string", "enum": ["progress", "risk", "all"], "description": "摘要侧重:进展、风险或全部" } }, "required": ["raw_materials"] } }这一层的目的不是给模型添麻烦,而是让模型在调用技能前就想清楚:我要传什么参数?参数有没有给全?如果模型发现自己手里的信息和required对不上,它就有理由主动向用户追问,而不是硬着头皮瞎编。
我在定义时踩过的一个坑是参数类型写得太宽。早期我把focus写成普通字符串,模型经常传"重点看风险"这种自然语言值,后段逻辑没法处理。改成枚举之后就老实了,只传progress、risk或all。
2.2 输出约束比输入约束更重要
输入是给模型看的,输出是给下游消费的。下游可能是另一段代码,也可能是另一个技能,格式乱一点就全乱套。所以我在输出定义里会写得很死:
{ "output_schema": { "type": "object", "properties": { "summary": { "type": "string", "description": "不超过200字的总体摘要" }, "highlights": { "type": "array", "items": { "type": "string" }, "description": "3到5条关键亮点" }, "risks": { "type": "array", "items": { "type": "string" }, "description": "风险列表,无风险时为空数组" } }, "required": ["summary", "highlights", "risks"] } }这里面有两件事容易被忽略。第一,必须要写"无风险时为空数组"这种边界说明,否则模型可能自作主张省略字段。第二,required必须写上所有字段,让模型无论如何都得给全,这样下游代码就不用做空值判断了。
2.3 失败路径是技能的灵魂
技能执行一定会遇到失败:工具没权限、网络超时、拿到的数据不完整。我在第一版技能里完全没设计这些,一旦失败模型就开始自由发挥,编造数据都是轻的,最怕它把失败原因伪装成正常输出。
后来我给每个技能都加了三条失败规则:
- 明确返回
error字段,而不是让输出硬套成功格式 - 如果一次失败后重试仍不成功,必须说明不确定,禁止编造
- 技能之间如果依赖前置数据,前置失败时直接终止,不做后续流程
这段经验真心建议写在每个技能里。见过太多智能体项目,前面跑得挺顺,一到异常场景就开始胡说八道。技能设计阶段多花十分钟把失败路径想清楚,后面能省几天的排查时间。
3. 技能之间的协作与冲突:当多个技能同时在线
单个技能好写,难的是让十几个技能在同一个智能体里和谐共处。技能多了以后,模型经常会犯选择困难症:明明该用 A 技能,它偏去调 B 技能;或者好几个技能都能干类似的事,它随机挑一个,结果输出风格完全不一致。
3.1 每个技能都要有"触发场景说明"
我给每个技能加了一个trigger_conditions字段,通常是一段两三行的自然语言,说明什么情况下用这个技能、什么情况下不要用。比如format_markdown技能的触发条件是:"当用户要求输出格式为 Markdown,或需要把已有内容排版为标题、列表、表格时使用;如果内容已经是 Markdown 则不要重复调用。"
这个字段非常管用。模型对"什么时候调用技能"的判断依据,主要是技能描述里的语义匹配。你把触发场景写清楚,它匹配得就准。相反,描述里如果全是"帮助用户格式化文本"这类泛泛的句子,模型一看好像哪个技能都沾边,自然就开始乱选。
3.2 技能命名与描述要像 API 文档一样严谨
技能多了以后,模型是靠"名字 + 一句话描述"来做路由的。我第一个版本吃过不小的亏:技能名起得太文艺,比如night_owl,模型根本猜不到它和"夜间自动备份"有什么关系。后来全部改成动词_对象的命名风格,准确率明显提升:
| 原技能名 | 修改后 | 调用准确率变化 |
|---|---|---|
| night_owl | backup_at_night | 提升约20% |
| little_helper | search_knowledge_base | 提升约25% |
| cleaner | cleanup_temp_files | 提升约15% |
描述部分就更关键了。我现在的经验是:描述里必须写明"输入是什么、执行什么操作、产出什么",三要素缺一不可。不要写"这个技能很棒""能极大提升效率"这类情绪化词汇,对模型没有意义。它需要的是冷静、可判断的语义信息。
3.3 冲突时的处理优先级
两个技能都匹配时怎么办?这个没法完全避免,但可以提前定一套优先级规则。我的做法是:每个技能加一个priority整数,数值越高越优先。当模型拿不准用哪个时,让它按这个值选。这招治好了"选择困难症"的大部分症状。
不过要注意,priority不能解决所有问题。有些技能冲突是语义层面的,比如"总结文档"和"提取要点"其实高度相似,靠优先级硬分效果有限。这种时候我的建议是合并技能,把相似任务收拢到一个技能里,通过参数区分不同场景。这也符合"技能要内聚"的原则——宁可技能数量少一点、每个技能职责大一点,也不要十几个小技能互相打架。
4. 实测中的翻车现场与完整排查链路
说一段真实经历。有次我把技能库一次性加到 20 个,结果模型开始频繁选错工具,明明用户只是问"现在几点了",它居然跑去调query_database(查数据库技能)。排查了一下午,最后定位到的原因让我很意外。
4.1 排查第一步:把所有调用的原始输入输出拉出来
智能体跑完任务之后,完整的调用链日志非常重要。我这里说的不是普通的运行日志,而是"模型在每个步骤看到了什么、选了什么、输出了什么"的全量记录。有一次用户反馈输出完全不对,我第一反应是查后处理代码,折腾了半天没发现问题,后来翻调用链日志才发现模型根本没走format_markdown,而是自己动手写了一段格式不完整的文本——问题出在路由,不在后处理。
所以我的项目里总会加一层请求日志拦截器,把每一次技能调用的skill_name、input、output记成 JSON 行,落本地文件。排查的时候先 grep,看看是哪一步开始不对的。这一步能省 80% 的排查时间。
4.2 排查第二步:检查技能描述是否被其他内容"带偏"
那次"现在几点了调数据库"的问题,根因其实很有意思:query_database技能的描述里有一句"可以回答用户的各种问题",这句话给模型释放了错误的信号。模型在不确定时,倾向选择一个描述范围"看起来很大"的技能。明白这个机制后,我把所有描述里的绝对化词汇清理了一遍,改成具体的、边界明确的表述,同时给current_time这个简单技能补了触发条件描述。之后同样的测试场景就恢复正常了。
这一类问题在排查链路里定位起来很痛苦,因为不是你代码写错,也不是模型抽风,而是描述语义空间重叠导致路由分岔。我的经验是:每次发现模型选了"语义范围更宽"的技能而不是"语义精确匹配"的技能,优先怀疑描述里的宽泛措辞。
4.3 排查第三步:用极端输入做回归测试
技能稳定性的验证,不能只测正常流程。我维护了一份测试用例,专门传一些刁钻输入:空字符串、极长文本、缺少required字段、带 SQL 注入特征的字符串等。每个技能至少跑一轮边界用例,把输出记录下来做 diff。
之前有个extract_contacts技能,正常文本跑得很好,一传入带大量电话号码的文本就爆炸,输出的联系人列表里 URL 和邮箱混在一起。如果没有边界测试,这类问题会在用户手里炸出来,那体验基本等于劝退。边界测试跑完虽然不能证明技能绝对可靠,但至少能把明显的问题堵在发布之前。
4.4 排查第四步:隔离变量做对照实验
实在排查不出来,就做隔离实验:只启用问题技能,其他全部关掉,看是否复现;再逐一加回其他技能,复现的那一刻你就知道是谁在干扰。这个方法简单粗暴,但对"技能间相互影响"类问题是唯一高效的定位手段。我曾经靠这个方法发现,某个技能表现不稳定是因为另一个技能共享了同一个临时文件命名空间,数据互相覆盖。这种问题靠读代码很难发现,必须靠隔离实验把交互干扰逼出来。
5. 从"单个技能"到"技能库":索引、发现与更新机制
当技能数量上了 30 个,你不能再靠"把所有技能描述一股脑塞进上下文"这种方式了,成本太高。我算过一笔账:平均一个技能描述约 200 token,30 个就是 6000 token,光是让模型"阅读技能列表"就占掉大量上下文,留给业务数据的位置就少了。这时候要做技能库治理。
5.1 技能清单与语义索引
我的方案是为所有技能维护一个统一清单文件(skills_index.yaml),里面只存精简信息:技能名、一句话描述、是否启用、优先级。模型先读这个清单,需要时再展开某个技能的完整定义。
更进一步的方案是语义检索:把技能描述向量化,用户请求进来后先做一次向量相似度检索,选出最相关的 3 到 5 个技能再展开给模型。我试下来 top-3 召回的准确率在 90% 左右,和全量展开差别不大,但上下文占用少了将近一半。如果你的项目对延迟敏感,这个方案很值得做。
# skills_index.yaml 示例片段 - name: summarize_weekly description: 将分散素材整理为结构化周报摘要 enabled: true priority: 10 - name: format_markdown description: 将内容按 Markdown 标题/列表/表格排版 enabled: true priority: 55.2 技能描述更新要克制
技能不可能一次写对,迭代更新是常态。但更新时必须克制:每次只改一个变量。比如这次想优化描述,就只动描述,不要顺手改输出 schema,否则你根本不知道提升是哪个改动带来的。
我用 Git 管理技能定义的整个目录,每次改动都是独立 commit,commit message 里写清楚"改了什么、为什么改、验证结果怎么样"。时间长了这个 commit log 就是一套天然的技能调优手册,回头看特别有价值。
5.3 灰度和回滚
技能和代码一样,也会有改坏了的时候。我现在的做法是给技能定义加version字段,发布时让每个技能保留最近两个版本。线上先切一部分流量到新版本,观察一小段时间,没问题再全量。一旦发现新版本输出质量下降,直接切回旧版本,整个过程不用改代码。
听起来有点重,但对生产环境的应用是必要的。毕竟技能不是代码,改坏了你没法编译报错,只有用户说"感觉不对了"你才知道出问题了。
6. 技能描述里的翻译学问:怎么"钓"出正确调用
技能描述是我和模型之间唯一的沟通语言,它的质量决定了整个路由的准确度。我归纳了一条实践经验:技能描述要写成"给一个聪明但固执的新同事看的操作说明",不是说给他听,而是给他照着做。语气要平实,边界要清晰,反面例子反而很有价值。
6.1 好的描述长什么样
拿search_knowledge_base来对比。粗糙版本是:
description: 搜索知识库,帮助用户找到相关信息问题很明显:什么算"相关信息"?什么时候该触发?输出长什么样?全没写。模型很可能在用户抱怨"找不到东西"的时候也调这个技能,但用户的本意其实是"页面出错了"。
改成这样就好多了:
description: 搜索内部知识库并返回匹配的知识条目。 触发条件:当用户明确询问知识库中的内容,或要求"查一下相关规定/文档/FAQ"时使用。 输入:search_query(搜索关键词),max_results(最多返回条数)。 输出:按相关性排序的知识条目列表,每条包含标题、摘要、原文链接。 不要用于:解答常识性问题、处理用户反馈、查询外部网络内容。这版描述包括了五个要素:技能功能、触发条件、输入、输出、不要用于。尤其是最后一点,很多人会忽略,但它恰恰是减少误调用的利器。
6.2 用"反面示例"训练路由
在技能描述里明确写出不该触发的情形,本质上是在给路由做负样本。我做过一次小实验:给两个经常被误触发的技能分别加了"不要用于"说明,一周下来误触发次数下降了四成以上。不要觉得写这些"废话"占 token,它换来的准确率提升远比那点 token 成本值。
6.3 定期复核调用记录
技能描述需要持续维护,不能写完就丢。我每两周会做一次调用记录复核:把"被调用但输出质量差"和"应该被调用但没被调用"两类情况各挑 10 条,逐一分析是描述问题还是逻辑问题。如果是描述问题,就按上面的方法修改;如果是逻辑问题,就要回到技能实现里看代码。
这种做法坚持下来之后,技能库会越来越稳,模型在具体任务上的表现也会稳步提升。一个技能库就像一个团队,磨合越久,配合越默契。
最后补一个实际操作中的小技巧:技能描述里尽量用动词开头。summarize、search、extract、backup、notify这类动作词,和模型预训练语料里的 API 文档风格高度一致,语义匹配时更容易命中。形容词、修饰词能省就省,描述写得像一份干净的接口文档,路由准确率往往比写"花哨文案"高得多。