1. 先搞清楚:agent skills 不是"给 Agent 加个插件"
1.1 从一次需求沟通说起
上个月团队里来了个新同学,接手一个客服问答 Agent 的优化任务。他跑过来问我:"我要给这个 Agent 加一个'查订单'的技能,是不是直接接一个订单查询 API 就行?"
这个问题听起来简单,但恰恰是很多人对agent-skills最大的误解所在。把 Agent 的开发想成"调 API + 拼 prompt"的人不在少数,但凡是真正在生产环境里跑过一段时间 Agent 的人都会告诉你,技能的难点从来不在"调通一个接口",而在"让大模型在正确的时间、用正确的方式、把接口调对"。
我记得特别清楚,最早我们内部尝试做 Agent 时,第一版就是把各种功能函数一股脑塞给模型,告诉它"这些工具你都能用"。结果模型在简单场景下表现还行,一旦任务复杂,就开始频繁选错工具、传错参数、在不需要调用的场景瞎调用。后来我们才意识到,问题不是模型不够聪明,而是我们根本没有把"技能"当成一个需要体系化设计的对象来对待。
这篇文章我想从实操角度,把 agent skills 这件事拆开聊透:它到底是什么、怎么设计、怎么落地、怎么评估,以及我在反复试错中总结出的一套方法论。内容偏向工程实践,适合正在做 AI Agent 应用开发的工程师、技术负责人,以及对 Agent 底层机制感兴趣的读者。
1.2 技能、工具、插件:三个容易混淆的概念
先把概念对齐。很多讨论里"技能""工具""插件"混着用,但实际上它们是完全不同层级的东西。
| 概念 | 本质 | 举例 | 关键特性 |
|---|---|---|---|
| 工具(Tool) | 单一可执行函数/API | 查询天气、计算器、数据库查询 | 无状态、单一职责、可独立调用 |
| 技能(Skill) | 面向目标的封装能力 | "处理订单退款"、"生成周报" | 有状态、可包含多个步骤、内部可调度多个工具 |
| 插件(Plugin) | 技能的打包分发载体 | 企业微信插件、Notion 插件 | 包含技能定义 + 配置 + 依赖,可安装可卸载 |
直白一点说:工具是"手",技能是"一套操作流程",插件是"把流程打包好的安装包"。
一个 Agent 技能,本质上是对"某个目标能不能完成"这件事的完整封装。它告诉模型三件事:这个技能是干什么的、什么时候该用、用的时候要注意什么。我在设计技能时,经常用一句话自检:如果把这个技能的描述和参数丢给一个完全不懂业务的人,他能不能凭这份说明正确使用?如果答案是否定的,说明这个技能的定义还不够完善。
1.3 为什么技能体系是 Agent 从"能用"到"好用"的分水岭
我见过很多团队把 Agent 做成了"大号聊天机器人":模型能力很强,上下文窗口很大,什么都能聊,但一旦涉及严肃的业务操作,立刻露怯。原因就是模型的海量参数里,其实并没有你的业务规则和操作规范。
举个例子。你让 Agent 帮用户办理"发票寄送"这个动作,业务上有规定:金额超过 5000 元的发票必须走财务审批流,且必须使用顺丰到付。这些规则大模型并不知道。如果不把它固化成技能逻辑,模型就会自由发挥——可能在用户仅是询问时就触发了寄送,可能完全无视审批规则。
这就是技能体系存在的意义:把不确定的模型行为,约束在确定性的能力边界内。技能不是用来"增强"模型的,而是用来"规范"模型的。它把业务流程中的确定性逻辑沉淀下来,让模型只需要负责"理解和决策",不需要负责"记忆和执行细节"。
所以回到开头的那个问题——"接一个 API 是不是就等于加了一个技能"?我的回答是:接 API 只是技能的第一步,甚至连第一步都不算,你先得把业务规则写清楚。
2. 一个优秀 Agent 技能的核心设计原则
2.1 技能的边界要窄,意图要清晰
我早期犯过一个大错误:总想做一个"全能技能",比如"订单管理技能",试图把查订单、改订单、退订单、催发货全部塞进去。结果模型经常选错子功能,明明用户想退货,它却去调了改地址的接口。
后来我总结出一个规律:技能边界越窄,模型的选择准确率越高。一个技能对应一个明确的用户意图,这是最稳的配置。
比如"订单管理"这个需求,如果拆开,实际上是这几个独立意图:
- 查询订单状态
- 修改收货地址
- 申请退货退款
- 催发货
每个意图都是一个独立技能,它们的参数、响应逻辑、可能出现的异常情况完全不同。混在一起的结果是参数空间交叉,模型在有限示例下很难学会精确匹配。
做技能设计时,我会用"一个人能听懂的最小任务单元"来检验拆分粒度:如果一个新员工只需要你一句话就能独立完成的动作,才能算一个原子技能。凡是需要你补充第二句、第三句的,都应该继续拆。
2.2 技能描述是给模型的"使用说明书"
这是整个技能设计中最关键、也最容易被忽视的部分。很多人写技能描述就一行字:"查询订单状态。"模型确实能理解,但理解得极其粗糙。
我给团队定的标准是:一个技能的描述必须包含以下信息——
- 技能职责:一句话说明这个技能做什么,不能做什么
- 触发条件:什么场景下必须使用、什么场景下禁止使用
- 行为规则:调用时有哪些必须遵守的约束
- 与相似技能的区分:避免模型混淆时的引导
我看过不少真实的技能定义,很多description写成这样:
获取商品评价信息这个描述给到模型,等于什么都没说。模型不知道什么时候该用,不知道评价信息包含哪些字段,不知道是获取单个商品还是某个店铺的评价列表。实际开发中,技能描述应写成面向模型的结构化指令,而不是给人类看的功能注释。
给一个我实际用过的描述示例:
技能名称: query_product_reviews 技能职责: 查询指定商品的历史用户评价摘要,用于帮助用户决策是否购买。 触发条件: - 用户明确询问"评价怎么样""口碑如何""别人用着怎么样"等意图 - 用户在商品详情页发起咨询 禁止场景: - 用户询问的是商品规格、库存、价格,禁止调用本技能 - 用户只是提及"评价"二字但与购买决策无关时,禁止调用 行为约束: - 调用前必须确认商品ID有效 - 若商品无评价数据,明确告知用户"暂无评价",不得编造 区分指引: - 与 query_product_detail 的区别:后者返回规格参数,本技能只返回用户评价一段好的技能描述,其实是在帮模型做"决策边界划分"。它的最终目标不是让模型"更懂这个技能",而是让模型"在无数个可选项里,精准地命中该用这个技能的那个场景"。
2.3 参数设计决定技能的上限
参数是模型与真实世界交互的接口。参数设计得好不好,直接决定了技能在真实场景中能否被正确调用。
我见过最典型的反例是:参数设计得过于宽松,大量字段都是可选,导致模型经常漏传关键参数。另一个极端是参数过于严格,要求用户必须先提供某个必须字段否则无法调用,导致模型在信息不足时频繁问用户,体验极差。
好的参数设计遵循三个原则:
最小必需原则:只保留完成该任务真正必需的参数。比如"查询订单状态"技能,只需要 order_id 和 user_token 两个参数就够了。什么历史记录、用户备注这些字段,能不加就不加。
默认值兜底:非核心参数尽量给出 default 值。比如"生成周报"技能,时间范围如果没有指定,默认本周一至今。模型就不用每次追问用户。
参数描述要写清楚:给每个参数写一行"模型看得懂"的说明,说明参数的格式要求、取值范围、与其它参数的关联。比如:
"time_range": { "type": "string", "description": "统计时间范围,格式为 YYYY-MM-DD 至 YYYY-MM-DD,不传则默认最近一个自然周", "required": False }参数设计的本质,是在给模型"画格子"。格子画得太粗,模型会乱跑;格子画得太细,模型会被卡住。找到那个"刚刚好"的尺度,需要靠线上数据不断校准。
3. 从零搭建一个技能的实际过程
3.1 需求拆解:把业务任务分解成原子能力
理论说完了,来看实操。假设我们的业务场景是:做一个企业内部的"行政服务 Agent",员工可以找它申请会议室、报修设备、查询年假余额。
第一步永远不是写代码,而是做需求拆解。找业务方聊清楚,哪些操作是员工高频需要的,哪些是只读查询,哪些是涉及审批流的写操作。这里有个关键判断标准:凡是涉及资金、审批、权限变更的操作,必须拆成独立技能,并强制加入确认环节。比如"申请会议室"和"取消会议室"是反操作,也必须拆成两个技能,不能靠传一个 action 参数复用同一套逻辑。
我当时画的拆解结果是这样的(节选):
| 原子能力 | 类型 | 是否需要审批 | 数据依赖 |
|---|---|---|---|
| 查询会议室空闲状态 | 只读 | 否 | 会议室系统 |
| 预订会议室 | 写操作 | 否 | 会议室系统、员工日历 |
| 取消预订 | 写操作 | 否 | 会议室系统、订单号 |
| 报修申请 | 写操作 | 是 | 工单系统、资产信息 |
拆完之后,每一个原子能力对应一个技能,职责边界非常清楚。这里我特别想强调一个经验:拆解时多花一小时,后面调试少花一整天。技能的划分本质上是在定义 Agent 的能力边界,这一阶段的混乱会直接传导到线上的每一个对话里。
3.2 技能的注册与编排配置
拆解完需求就可以进入开发了。技能注册的第一步,是把你定义好的技能信息告诉 Agent 的运行框架。以我们团队使用的 Python + LangChain 栈为例,一个技能通常是一个 BaseTool 类的子类:
from langchain.tools import BaseTool from pydantic import BaseModel, Field class MeetingRoomArgs(BaseModel): date: str = Field(description="查询日期,格式YYYY-MM-DD") campus: str = Field(description="园区编码,如 HQ-01", default="HQ-01") class QueryMeetingRoomTool(BaseTool): name = "query_meeting_room" description = "查询指定日期和园区的会议室空闲列表,用于预订前确认可用状态。禁止用于直接预订。" args_schema = MeetingRoomArgs def _run(self, date: str, campus: str = "HQ-01") -> str: # 调用会议室系统API,返回空闲会议室列表 return query_room_availability(date, campus)注意我这段代码里,description 写的是"用于预订前确认可用状态。禁止用于直接预订。"。这个"禁止用于直接预订"就是边界声明。模型非常依赖这种边界信息来决策,你写得越清楚,它就越不容易误用。
注册完成之后,技能就进入了 Agent 的候选调用池。但一个技能池里挂几十个技能时,模型选错技能的概率会显著上升。所以下一步不是急着上线,而是做"技能闸门"——在 Agent 入口处加一层基于规则的意图预判,把明显不相关的技能先过滤掉。比如用户说的是"报修",就没必要把所有技能都送入模型上下文。
3.3 单技能验证:先跑通,再谈优化
技能开发完,先别急着接编排,先做单技能验证。我所说的"单技能验证",是指在一个隔离环境里,只把这个技能暴露给模型,然后构造大量这个技能应该被触发的测试用例,看模型能不能准确调用、正确传参。
这一步有个很容易忽略的点:不仅仅要测"该触发时能不能触发",更要测"不该触发时会不会乱触发"。很多人只做正向测试,结果上线后模型在一些边缘场景里乱调用技能,把好好的对话搞得一团糟。
单技能验证我用的是这种测试表:
| 测试用例 | 用户输入 | 是否应触发 | 实际行为 | 参数是否正确 | 结论 |
|---|---|---|---|---|---|
| 正常触发 | 帮我订明天下午三点的会议室 | 是 | 触发 | 是 | 通过 |
| 相似意图 | 帮我看看明天下午有没有会 | 是 | 触发 | 是 | 通过 |
| 边界排除 | 会议室在几楼? | 否 | 未触发 | — | 通过 |
| 参数缺失 | 帮我订个会议室 | 是 | 触发但缺少时间 | 否 | 不通过 |
参数缺失那一条,是我们非常容易在初期踩坑的地方。用户说"帮我订个会议室"时,模型确实识别出意图触发了技能,但没法提供具体时间和日期。这时候技能内部必须有一套"追问机制",引导模型向用户索要缺失参数,而不是报错。
基于这套验证流程,一个技能从开发到稳定,通常需要 3 到 5 轮迭代,核心是打磨描述和参数提示,让模型"理解得更准"。
4. 技能编排:让多个技能协作完成复杂任务
4.1 从单技能到多技能协作的链路设计
真实业务场景往往不是一个技能能搞定的。比如员工说:"帮我明天下午订个大会议室,顺便给参会的人发个通知。"这里涉及查询空闲房间、预订、查询参会人信息、发通知四个环节。
单技能验证通过后,就要考虑如何编排。我的原则是:尽量让编排发生在技能之外的流程层,而不是让模型自由串联。什么意思?就是把这些技能的调用顺序、依赖关系,用代码写成一条明确的任务流。模型只负责在每个节点做触发决策,不负责决定执行顺序。
如果你完全依赖模型来编排,它确实也能做,但稳定性就不可控了。可能这次它先查了房间再发通知,下次它先发通知再查房间,或者中间漏了一步也不自知。在生产环境里,这种不确定性是无法接受的。
所以我的做法是:定义一个编排器(Orchestrator),它接收用户输入,匹配业务场景,然后按预设流程依次调用技能。遇到分支条件时,调用一次模型来做路由决策,但流程骨架是确定的。
4.2 状态和上下文:编排中最容易翻车的地方
多技能协作时,最大的技术难点不是调用技能本身,而是状态维护。每个技能都是一个独立的调用,它不记得上一个技能产生了什么结果。比如"查询会议室空闲列表"返回了一个 room_id,下一步"预订会议室"需要用到这个 room_id。如果编排器不处理好状态的传递,预订技能就会因为缺参而报错。
我常用的方案是引入一个共享状态容器,在流程中逐步累积上下文:
state = { "user_id": "u_10086", "meeting_room_id": None, "participants": [] } # 第一步:查空闲 rooms = query_meeting_room(date="2025-07-10", campus="HQ-01") state["candidate_rooms"] = rooms # 模型选择房间 selected_room = route_choice(rooms, user_input) state["meeting_room_id"] = selected_room.id # 第二步:预订 booking = book_meeting_room(room_id=state["meeting_room_id"], ...)这里有个经验:不要把所有状态都堆在内存里。真实线上环境,一次编排可能持续几分钟,用户可能中途离开,进程重启后状态就丢了。我建议把关键状态持久化到 Redis 或数据库中,key 用一个 session id 关联。这样用户中断后还能恢复流程。
4.3 容错和兜底:编排不是强流程
再讲讲容错。编排能解决问题,但也会引入新的问题——链路中任何一环失败,整个任务就卡住了。
比如预订会议室成功了,但发通知失败,这时候员工实际感受是"会议没订上"还是"通知没发出去"?严谨一点的做法是:在编排层为每个技能节点加上独立的成功/失败记录,失败时自动触发补偿。
实操中,我给每个编排流程定义了三种补偿策略:
- 自动重试:网络类错误直接重试两到三次
- 跳过:非关键节点失败时,记录日志并继续后续流程(比如通知失败不影响预订结果)
- 挂起转人工:关键节点失败时,把任务转入人工处理队列,由运营人员介入
这张决策表一定要在设计编排时提前写好,不要等问题出现了再临时想。我见过太多线上故障,就是因为没有预定义补偿策略,结果编排流程卡死在一个失败的 API 调用上,半天没人发现。
5. 技能评估与迭代:好技能是改出来的
5.1 建立评估集:从"感觉好用"到"数据说话"
很多团队做技能优化,完全靠拍脑袋——"最近模型好像不太聪明,老是选错技能"。这种模糊的反馈没法指导改进。
我的做法是建立一份技能评估集(Eval Set),里面每一个测试用例都包含:用户输入、期望触发技能、期望参数名。每次迭代后,跑一遍评估集,算准确率。这样每次改动是变好还是变差,一目了然。
评估集怎么建?我从线上日志里随机抽取真实对话,再人工标注正确行为。初期至少准备 200 条,覆盖常规、边界、异常三类场景。常规场景占 60%,边界占 30%,异常占 10%。特别注意,边界和异常场景才是评估集真正的价值所在,因为它们才是区分"能用"和"好用"的试金石。
一个典型的评估集条目长这样:
{ "input": "帮我把会议时间改到下周一下午四点半", "expected_skill": "reschedule_meeting", "expected_params": { "new_time": "2025-07-13 16:30" }, "should_not_trigger": ["cancel_meeting", "book_meeting_room"] }跑完评估,你会得到一组数据:触发准确率、参数正确率、误触发率。这三个指标就是技能健康度的核心 KPI。
5.2 从失败案例中提炼改进动作
评估集只是发现问题的手段,真正的功夫在于针对失败案例做归因和改进。我通常把失败分成三类:
第一类是描述不清导致的决策失败。比如模型在"改时间"和"取消重订"之间摇摆。这种问题的解法不是调整模型,而是改技能描述:明确写出"改时间必须调 reschedule_meeting,禁止先取消再重新预订"。
第二类是参数映射失败。用户说"下周一",模型不知道怎么转成日期。这类问题需要你在技能参数描述里加入解析规则,或者引入外部的时间解析器。
第三类是边界溢出。用户输入其实不在这两个技能能处理的范围内,但模型强行触发了一个。这说明你的技能描述里缺少"禁止场景"说明。把"非XX场景禁止调用"写得再显眼一点,这类问题通常会明显减少。
我在实践中发现,绝大多数问题通过调整技能定义就能解决,根本不需要换模型。这个认知很重要,因为它避免了很多团队一遇到问题就想着换更强的模型,结果成本和复杂度上去了,问题依旧。
5.3 技能版本管理:线上变更的灰度策略
技能迭代跑起来了,随之而来的问题是——技能的修改什么时候上线?怎么保证不会改崩线上服务?
我踩过一个大坑:有一次优化了一个技能的描述,直接上线,结果触发准确率反而下降了 8 个百分点。原因是新描述和另一个技能产生了语义重叠,模型开始混淆。这让我意识到,技能文件也必须做版本管理,并且要走灰度发布流程。
当前我的做法是:每个技能定义对应一个独立文件,放在 Git 仓库里,修改走 MR 评审。上线时通过配置中心做灰度切流,先让 10% 的流量使用新版本技能,观察触发准确率和任务成功率,确认无异常后逐步放开到 50%、100%。如果出现问题,直接一键回滚到上一个版本。
这个流程听起来笨重,但对于生产环境来说,稳妥永远是第一位的。
6. 我在实际项目中踩过的几个坑(避坑指南)
6.1 坑一:把"技能描述"写成了"给人看的文档"
这是我们最早犯的错误。团队之前一位同学写技能描述,写得跟产品需求文档一样长,大段讲背景,讲用户价值。但模型的注意力是有限的,一长串无关内容直接稀释了核心指令。
后来我把"技能描述"强制控制在一个"结构化的简洁区间"——职责一句话,触发条件分列,行为约束一条一条列清楚,禁止场景单独列出,和相似技能的区分再单独一段。总共控制在 200 字以内。
这是我在实践中反复验证后有效的模板:
<技能职责> 一句话说明。 </技能职责> <触发场景> - 场景1 - 场景2 </触发场景> <禁止场景> - 场景1 - 场景2 </禁止场景> <行为约束> - 约束1 - 约束2 </行为约束>6.2 坑二:没有给技能设置"否定提示"
早期设计技能时,我只写"这个技能能做什么",不写"不能做什么"。结果模型在用户输入模棱两可时,倾向于把所有看起来沾边的技能都试一遍。比如用户说"这个月工资怎么还没发",模型居然去触发了"会议室预订"技能——因为它在语义上匹配到了"时间"相关的参数,这种乱触发打击了整个 Agent 的信任感。
解决方式也很直接:给每个技能加上显式的"禁止场景"描述,并且在编排层做规则拦截,双重保险。
6.3 坑三:忽略技能内部异常状态的返回
最后一个坑,是技能内部逻辑本身的健壮性。早期我们写的技能,一旦后端服务报错,直接抛异常。模型收到异常信息后通常会"胡言乱语"——它会把异常信息当作正常结果,接着用户的问题继续生成内容。
后来我把技能内部统一改成了"结构化返回 + 兜底话术"。无论底层 API 是否成功,都必须返回一段模型能理解的结果文本。成功时返回数据,失败时返回"查询失败 + 可能原因 + 下一步建议"。这样模型就能根据返回内容向用户解释,而不是凭空编造。
比如预订会议室技能,失败时的返回文本是这样设计的:
抱歉,预订未成功。原因是会议室冲突。建议:换个时间段或联系会议室管理员(分机:8001)。这段文本看起来是给用户看的,但它同时也是给模型看的。模型读到这个结果,就知道怎么回复用户,并且知道可以建议用户换时间——这比让模型自己编说法靠谱得多。
7. 最后分享一点体会
Agent 技能的打磨是一个滚雪球的过程。第一版技能池上线时,触发准确率可能只有六成,也别慌——准确率是靠一轮轮评估、改描述、调参数堆上去的。只要评估集在沉淀、方法论在迭代,技能质量就会稳步上升。
我现在的习惯是每周固定抽出半天时间,跑一遍全量评估集,看 diff,改技能定义,更新版本。这个节奏不快,但非常稳。实践了几个月后,我们线上 Agent 的技能触发准确率稳定在了 95% 以上,误触发率降到了 1% 以下。
Agent 技能这条路没有终点,业务一变,技能就要跟着变。但只要你搭好了设计、评估、迭代这套循环,任何业务变化都不会让你手忙脚乱。