代理工程做到一定阶段,都会碰到同一个坎:单次模型调用在 demo 里什么都能干,可一旦对业务结果负责,模型本身的输出方差就成了最大的隐患。真正的可靠性往往不来自模型,而来自它周围那层被称为技能(skills)的工程框架。agent-skills 这个仓库名从一个内部代号慢慢变成了我们团队的核心理念:把模型从“什么都会一点”变成“有固定套路、可验证、可升级”的专家组合体。这篇文章我会把技能的几种定义方式、必须画清楚的边界、与记忆系统的关系、生产落地的编排方案,以及实测下的一些基线数据串起来讲,给还没搭过技能系统的同学一份可以直接照着思考的参考。
1. 先定个基调:代理的“技能”和传统“工具函数”到底差在哪
1.1 工具函数只是“能力”,技能是“可验收的行为契约”
如果你接触过 Function Calling,第一反应肯定是:技能不就是把一段提示词和API调用包一下,和工具函数有什么本质区别?我当初也这么想,直到第一次被线上问题打脸。
工具函数解决的是“模型能不能做”的问题,比如能不能查数据库、能不能发邮件。能力有了,但模型在什么条件下查库、查询结果怎么进上下文、失败之后要不要换一种方式,这些仍然是模型临场发挥。技能系统的核心恰恰是把这些临场发挥固定下来:它是一段可重复执行的行为流程,而不是一个孤立函数。
打一个比方:工具函数是给代理一把扳手,技能则是培养代理“拧这颗螺丝时要先检查螺纹、再对准角度、用力矩扳手分三次拧紧”的肌肉记忆。扳手可能被拿去砸核桃,但技能必须保证它在正确场景下做正确动作,而且做错时你能知道错在哪一步。
我一般用三个指标判断一个函数该不该升级成技能:是否被多个场景复用;是否涉及多步操作、中间状态或失败重试;是否对业务结果有可量化的要求,比如正确率、延迟、成本。三者只要命中任意两项,就应该走技能化改造。
1.2 技能给代理带来的三个收益:稳定性、可测试性、可观测性
技能带来的收益,我认为可以压成三个关键词。
第一是稳定性。模型每次调用输出方差很大,但技能通过约束提示词模板、校验输出结构、规定异常分支,把方差收敛到可控范围。稳定性不是靠提示词写得天花乱坠,而是靠“所有出口都被校验过”的设计。
第二是可测试性。单体模型调用只能做端到端测试,出了问题很难定位。而技能是一个有明确输入、输出和副作用声明的单元,你可以像测普通函数一样测它,可以构造 mock 外部服务做回归,提示词改完能立即看出对结构化输出的影响。
第三是可观测性。每个技能执行时有明确的开始、结束、成功率、耗时、token 消耗。这些指标拼起来,你才可能说出“周五下午代理失败率涨了,是因为天气技能依赖的第三方接口超时”,而不是对着黑盒瞎猜。
这三个收益不是白来的,代价是你要做额外的建模和工程工作,技能系统本身会引入一层调度开销。如果你的代理只是个简单 demo,不涉及多条业务线和真实用户,我反而不建议一上来就搭完整技能体系。过度设计比没有设计更容易拖垮项目,这个取舍要自己掂量。
2. 技能的定义方式:提示模板、工具与心智管道组成的“三明治”
2.1 技能注册表:所有技能要有统一的描述语言
技能化的第一个实操决定,是建立一个技能注册表。无论你底层用 LangChain、自研框架还是裸 SDK,注册表都应该是唯一可信的技能来源。每个技能在注册表里至少包含五个字段:
| 字段 | 含义 | 示例 |
|---|---|---|
| name | 全局唯一技能名 | weather_query |
| description | 给模型看的自然语言说明,包含使用场景和边界 | 查询指定城市当日天气;仅支持主要城市 |
| input_schema | JSON Schema 格式的输入校验 | 城市编码、日期 |
| output_schema | 结构化输出定义 | 天气代码、温度区间 |
| handler | 实际执行逻辑的引用 | 指向内部函数或外部服务 |
注册表的价值不只是文档,它承担着运行时路由:模型要调用技能时,系统根据 description 和 input_schema 做意图匹配与参数校验。我踩过的一个典型坑是 description 写得过于随意,结果模型经常把“查询天气”误配到“查询节假日”上。后来把所有 description 统一改成“场景 + 功能 + 边界 + 典型问法”四段式,误配率明显降了下来。
2.2 提示模板:技能里的“不变参数”与“可变参数”
技能内部最直观的部分是提示模板。以“总结对话并生成待办”这个技能为例:
SKILL_PROMPT = """ 你是对话待办提取器。 输入:以下是一段最近对话记录,过滤掉寒暄与无关信息,只保留动作性内容。 输出:JSON 数组,每项含 title、owner、due_at、priority。 对话记录: {{conversation_text}} 输出约束: - 未明确指定 deadline 的项,due_at 必须为 null,不得自行猜测。 - owner 无法判断时返回 unassigned。 - 只返回 JSON,不要额外解释。 """这里有几个关键点。第一,模板里的可变参数(如 conversation_text)一定要用占位符显式标注,并在外部完成插值,不要在模型上下文里现场拼一段不可控文字。第二,输出约束写在模板末尾、靠近模型生成位置,对稳定格式更有效。第三,提示模板必须带版本号,因为改提示词是最容易引发回归的操作,灰度章节我会详细讲。
2.3 工具封装与“心智管道”:技能不等于一个函数调用
如果一个技能只有提示词加一个函数,那它还称不上技能,顶多是“带提示的工具”。真正的技能通常包含一条心智管道,也就是把多个操作按固定顺序编排:
- 预处理:归一化输入,补齐缺失参数;
- 检索:从记忆库或知识库取相关上下文;
- 推理:调用模型生成中间结果;
- 执行:调用一个或多个外部工具;
- 校验:对结果做 schema 校验和业务规则校验;
- 回写:把关键结果写回记忆或日志。
举个例子,“帮用户查航班并提醒去机场时间”这个技能,内部其实串了航班查询、城市距离计算、交通耗时估计三个子模块。如果不包成一个技能,模型就要自己决定怎么拼接,这会带来灾难性的连续失败——某个中间工具返回格式稍有变化,整个推理就崩了。包成技能之后,中间过程对模型不可见,只有最终结果进入模型上下文,整体容错性会高一个量级。
这也是为什么我一直建议,技能要把“决策点”尽量收敛:模型只需要在技能边界做决策,技能内部尽量做成确定性流程。确定性越多,代理越可靠。
3. 设计技能时的三个边界:输入输出、失败边界、状态边界
技能系统里百分之八十的设计问题都出在边界没画清楚。边界画得好,技能就是乐高积木;画不好,技能就是一团乱麻。
3.1 输入输出边界:拒绝自由文本进技能
第一个教训是:不要让技能直接接受模型抛过来的自由文本参数。
最初我们的“查询天气”技能 input_schema 里城市字段是自由文本,模型可能传“北京”“北京市”“Beijing”“今天北京天气咋样”。同一个语义,五种写法,后续匹配和缓存全部紊乱。后来我强制所有文本参数进技能前先过一个轻量 NER 加归一化层,把实体映射到标准编码,比如城市映射到行政区划代码。代价是多一次模型或词典调用,收益是下游逻辑全部清爽了。
输出边界同理。技能返回给模型的内容必须是紧凑、结构化的结果描述,而不是把原始 API 返回的一大坨 JSON 原样丢回去。很多接口返回里有大量无用字段,轻则浪费 token,重则干扰模型推理。我习惯在技能内部做一次“投影”:只保留模型继续推理真正需要的字段,再附上必要的时间戳和置信度。
3.2 失败边界:技能必须能告诉代理“为什么没做成”
代理系统最怕的不是失败,而是失败得不明不白。模型如果只知道工具返回了个错误,它就会开始瞎猜,然后编造一个看似合理的替代结果,这是最危险的。
因此我对技能有个硬性要求:任何失败都必须归类,并且给出可执行的错误语义。至少分四类:
| 错误类型 | 含义 | 模型应该拿到的信息 |
|---|---|---|
| INPUT_INVALID | 输入参数非法 | 具体哪个字段不合规,合规范围是什么 |
| UPSTREAM_ERROR | 依赖的外部服务挂了 | 服务名、超时、是否可重试 |
| POLICY_REJECT | 业务规则不允许 | 触发了哪条规则,改什么输入才能通过 |
| SKILL_INTERNAL | 技能内部逻辑异常 | 大概哪个环节失败,能否降级 |
比如支付技能遇到余额不足时,返回 POLICY_REJECT 并附带“可用支付方式列表”,模型就能据此引导用户换一种方式,而不是反复调用同一技能。这个决策链条看起来很小,但在真实产品里决定了用户会觉得产品“挺聪明”,还是觉得你在调戏他。
3.3 状态边界:技能不得偷偷修改全局状态
第三个边界是状态。技能在承担多步任务时经常需要读写状态,但我的建议很明确:技能的写操作必须声明且收口,不允许顺手改全局会话状态。
简单来说,每个技能内部只能维护自己的局部状态,跨技能共享的信息必须显式通过返回值或专用记忆接口传递。如果让技能直接改全局状态,你很快就会看到两个技能之间的隐性耦合——比如订票技能改了日历技能依赖的时区设置,后面所有行为就不可预测了。
我实践过的一个土办法是:给技能上下文对象设置只读视图和写时复制快照。技能读到的永远是进入时刻的上下文,写完落库时统一提交并记录 diff。这么设计之后,跨技能排查问题的效率提升非常明显,因为每个技能看到了什么、改了什么都一目了然。
4. 技能和记忆是两码事:理不清这层关系,代理永远学不聪明
4.1 技能读取记忆的三种方式
这里想纠正一个常见误区:技能列表本身不是记忆。技能是“动作”,记忆是“事实”。如果一个代理只有技能而没有记忆系统,它就像精通各种拳法但没有战术意识的格斗新手,每次都从零理解局面。
技能读取记忆有三种典型方式:一次性注入,把和当前任务强相关的记忆片段直接拼进技能提示词,适合单轮技能;动态检索注入,先从记忆库按相似度召回 Top-K 片段再注入,适合知识密集型技能;工具式读取,技能内通过专门 API 查询记忆,适合需要精确数据的技能。
我在实践中发现,大多数人一上来就喜欢动态检索注入,因为听起来高级。但动态检索会引入噪声,检回来的片段一旦不准,对模型的误导比没有还严重。我的建议是:先做一次性注入,把会话级上下文整理清楚;跑通了,再按需引入检索。大多数场景下,一层精心整理的会话摘要比什么都管用。检索真正要解决的问题是跨会话的长期事实,比如“用户上周说过自己不吃辣”,而不是“用户刚才问了一句天气”。
4.2 技能之间的信息交接规范
技能之间怎么传信息,决定了一个多技能代理看起来是个整体,还是几个散兵各自为战。我坚持的规范是:技能之间不直接通信,要传消息必须经过一个运行上下文对象。
代理每次任务开始时会建立一个运行时上下文,包含用户画像摘要、会话摘要、当前任务目标、已执行技能列表。每个技能运行时读它,结束时把输出写回它。这样既避免状态边界问题,也让链路天然可追溯:每步之后上下文变成什么样,都能从日志重放出来。
这个设计思路简单,但建模要求很高。你需要为“任务”定义一个清晰抽象,而不是让每个技能自己拼凑信息。任务级上下文是技能间的共享白板,谁都能看,但写入必须按规范来。初期这个抽象可以很简陋,一个字典都行,但字段命名和写入时机一定要尽早统一,不然后期重构成本很高。
4.3 技能的回写策略:会改变记忆才叫学到东西
既然花了力气把技能做成可观测、可测试的单元,那它理应能为记忆系统提供素材。技能回写记忆主要有三种类型:事实回写,比如用户属性、偏好、明确声明过的信息;结果回写,比如上次任务结果摘要、执行成功或失败记录;过程回写,比如“用户对某一种表达方式有明显负面反馈”。
回写策略必须克制。我见过一个代理,每次技能执行完都把全过程写进长期记忆,结果记忆库迅速膨胀,检索时噪声爆表。我的习惯是给回写设白名单:哪些技能、哪些字段级别才能落长期记忆,其他只落短期会话上下文或干脆不落。
说到技能自省,有些团队在做模型复盘的尝试。自省不是不能做,但一定要有明确触发条件,比如失败之后或用户明确不满意时,并且限定复盘范围,否则每次技能完成都拉着模型自吹自擂一遍,token 成本高不说,还可能把本来正确的输出越改越错。
5. 技能编排的生产落地:顺序流、DAG 与规划器的取舍
当技能数量超过二十个,你就必须面对“谁决定技能调用顺序”的问题。这个问题的答案直接决定项目成败。
5.1 全交给 LLM 规划器:天花板高,但地板也很低
最理想的愿景是:模型看着用户需求,自己规划调用哪几个技能、按什么顺序。这种动态规划的天花板确实高,尤其适合开放性问题。
但落地时要注意,LLM 规划器的可靠性上限受制于意图识别和长链路错误累积。用户说一句“帮我安排明天下午的会议,并预订一个离公司近的餐厅”,模型要正确触发会议创建、会议地址解析、餐厅推荐、日历冲突检查等多个技能,中间任何一步的实体错误都会像滚雪球一样放大。我实测下来,超过三步的动态规划,端到端成功率大概在七成到八成五之间,而且很难单靠提示词提升,必须配合约束手段。
5.2 关键路径要确定性:把能固定的编排固化成 DAG
因此我强烈建议:业务关键链路不要完全交给模型即兴发挥,而是把主干流程固化成确定性 DAG,只在分支点给模型小范围选择权。
具体做法是定义技能流的 DSL,比如下面这样:
workflow: id: meeting_plus_restaurant steps: - id: s1 skill: calendar_query next: s2 - id: s2 skill: address_resolver next: s3 - id: s3 skill: restaurant_recommend next: s4 - id: s4 skill: calendar_create next: END branches: - from: s2 condition: "用户未提供理想位置" target: location_picker这个流程里,模型不是没有决策,而是决策被收敛到条件分支上。比如地址缺失时要不要触发 location_picker,这种决策交给模型没问题;但“解析完地址必须推荐餐厅”这种主干顺序,就不该让模型自由发挥。
我把这种设计叫“宽入口、窄决策”:任务入口尽量理解用户意图,内部流程尽量确定性,最后留少量小范围的选择点。它的可靠性,远高于全链路 LLM 自主规划。
5.3 运行时追踪是技能系统的仪表盘
没有追踪,技能系统就是个黑盒。我做技能调度的日志最少记录这些维度:技能 ID、版本、输入哈希;开始和结束时间、耗时、token 消耗;每一步子操作的耗时分布;最终状态和错误码;进入时的任务摘要和写回的关键字段。
有了这些数据,你才能做真正的调优。比如我发现某个技能经常在检索阶段花掉四成耗时,就去优化索引;发现另一个技能错误集中在 UPSTREAM_ERROR,就和外部服务方谈加超时、加缓存。没有观测数据,这些优化全是拍脑袋。
我还强烈建议对技能执行链路打上 trace id。一个用户请求可能跨五个技能,如果各技能日志彼此独立,排查跨技能问题时会非常痛苦。trace id 贯穿全链路之后,定位时间可以缩短一个数量级。
5.4 灰度与回滚:改提示词比改代码更容易翻车
提示词这东西,改一个词都可能改变输出概率分布。我见过不止一次,某团队改了一个技能的 system prompt,线上成功率瞬间掉了十个点,还没反应就已经影响一批用户。
所以技能系统必须有版本控制与灰度能力。我的做法是:每个技能的提示词、工具封装、流程编排都作为独立可发布的版本;灰度按 1%、10%、50%、100% 逐步放量;每一步对比老版本的成功率、延迟、token 消耗、用户反馈率;任何指标异常,立即回滚到稳定版本。
这个机制看起来重,但技能数量上到几十个之后,它能救你无数次。前阵子我们灰度新一版订单查询技能,总体成功率从 98.2% 跌到 96.0%,单看不算太糟,但按场景细分后,发现“查询历史订单”场景的成功率崩到了 78%。因为灰度只放量了 5%,影响面小,很快定位到是提示词里对“历史订单的时间范围描述”改出了歧义,回滚后恢复正常。这个案例很能说明灰度加细分监控的价值。
6. 技能安全与权限:被低估的一层防线
很多团队做代理时把精力全放在“能不能完成任务”上,却忘了问“这个技能是否被越权使用、是否泄漏了不该泄漏的信息”。技能化其实给权限控制提供了一个天然的抓手。
6.1 技能的最小权限原则
每个技能都应该只获得完成自身任务所需的最小权限。例如一个查询订单技能,从设计上就不应该能访问用户支付凭证;一个写日历技能,不应该有删除其他用户日历的权限。
实现上,我给技能上下文对象挂了一个权限令牌,技能在执行外部调用前必须校验令牌范围。令牌不由模型生成,而是由技能注册表在匹配技能时按预设策略签发。模型再怎么提示词注入,也无法绕过这层校验,因为权限判断发生在技能内部代码里。
这里有一个容易被忽略的细节:技能描述文本本身也可能被模型曲解为更高权限。比如模型可能把“查询”说成“导出”来试图触发不同技能,所以技能路由时除了匹配名称,还要校验意图与技能权限级别是否匹配。我们的方案是对每个技能记录一个 allowed_intents 列表,模型选择的技能如果不在允许意图范围内,直接拒绝。
6.2 技能调用的审计与熔断
技能系统还应该天然生成审计日志。谁触发了什么技能、传了什么参数、返回了什么结果、涉及哪些敏感字段,这些都要留痕。审计日志不需要每次排查都看,但出了安全事故时,它是唯一能说清楚“发生了什么”的证据。
熔断机制则用来防止技能在异常状态下反复冲击下游。比如某个外部接口连续失败了 5 次,自动熔断 30 秒,期间直接返回降级结果而不是继续烧 token 重试。这个策略对代理特别有效,因为模型默认是不会主动止损的,它只会不断尝试,直到把服务拖垮。
敏感操作类技能,比如删除、退款、对外发送消息,我建议做成双确认结构:第一阶段返回确认摘要,第二阶段要求模型以明确指令确认后再真正执行。虽然多一次交互,但对高风险动作是值得的。
7. 我实测过的一些基线数据和颗粒化准则
这一节分享一组我自己跑出来的数据和设计准则。不同团队场景不同,但作为起步基线是够用的。
7.1 技能调度的真实开销
技能系统是有成本的。我的实测数据(模拟项目,单技能平均步数四到六步)大致是这样:
| 环节 | 耗时占比 | 说明 |
|---|---|---|
| 模型推理 | 70%-80% | 技能的核心计算 |
| 工具外部调用 | 15%-25% | 依赖网络 IO,是延迟波动主因 |
| 技能调度与校验 | 2%-5% | 注册表路由、参数校验、日志写入 |
| 记忆读写 | 3%-7% | 检索和回写 |
调度与校验本身的开销很小,但如果每个技能都做检索,记忆环节就会明显膨大。所以我会在技能定义里显式声明是否需要检索,无检索需求的技能不默认加检索。
另一个值得注意的现象是输入压缩的效果。我们把技能返回给模型的长文本投影后,平均 token 消耗下降了大约三成,而任务成功率并没有下降。这印证了“给模型太多无关信息会干扰推理”的直觉。当然也别压缩太狠,丢掉关键上下文会让正确率掉下来,这个平衡点必须用实验调。
7.2 技能颗粒化的两个准则
技能粒度过大会增加复用难度和失败面,太小又会让调度与追踪成本压过收益。两个准则供参考:
- 准则一:以业务结果和失败模式为依据。一段流程如果有一种明确的主失败模式,就值得独立成技能;如果多种失败模式混杂且互相影响,则应继续拆细。
- 准则二:以复用频率为依据。多个场景都会用的原子操作,比如地址标准化、实体归一化,做成基础技能;只在特定流程里出现的组合,做成流程技能。
以酒店预订为例,我们拆成搜索酒店、价格比较、预订提交、取消预订四个技能。搜索、比较、提交的组合包另外封装了一个一键订房的高级技能,内部依然顺序调用前三者。这个组合牺牲了一点灵活性,但换来了更稳的用户体验,适合高频场景。
7.3 小心输入摘要折损输出质量
还有一个提醒:很多人为了省 token,会把用户输入先做一轮摘要再传给技能。摘要确实省 token,但会丢细节,尤其是在数字、时间、否定词上。模型摘要漏掉一个“不”字,整个技能输出方向就反了。
我踩过的坑是:某个技能做用户意见分类,为了省 token 把用户留言压缩成一句话摘要,结果把“我不喜欢这个功能”压成“用户提及此功能”,分类结果完全变了。从那以后,涉及否定语义、数值、时间实体的内容一律不做摘要,至少要保留原文关键片段。调度上的输入摘要要慎之又慎,省 token 的优先级永远低于正确性。
8. 从技能库到能力地图:往前一步还能做什么
8.1 能力地图:不再让模型盲选
技能多到一定规模,模型光靠注册表的 description 去选技能,会产生严重的选择疲劳。我现在在技能库之上建了一层能力地图,把技能按用户意图聚类映射。例如用户说“帮我订餐厅”,不会直接触发 restaurant_recommend,而是先进入 dining_intent 这个能力节点,由它调度位置偏好、口味偏好、时间约束、预订执行四个技能。
这一层带来的好处是,技能不再是孤岛,而是面对某类用户需求时的标准化组合。它同时成为产品能力的可视化地图,运营和产品可以直接看到代理目前能处理什么、不能处理什么,比甩一堆代码直观得多。
能力地图还有一个隐性的价值:它为技能迭代提供了方向。某个能力节点如果经常失败,说明这个能力还没被真正打磨好,应该优先补齐数据或重构流程;而不是继续增加新技能来掩盖旧技能的缺口。
8.2 技能的新建成本要持续下降
最后说个方向性观点。一个技能系统是否健康,不看它现在有多少技能,而是看“上线一个新技能”的成本。如果一个新技能从需求到上线要三周,那这套系统一定长得不健康。我见过比较健康的团队,一个新技能从编写、测试到灰度上线,周期压到一到两天,核心靠的是完善的脚手架和高度规范化的注册表。
技能库不是用来堆数量的,它是用来持续积累可复用经验的。随着业务理解加深,技能库会从几十个函数长成一组带边界、带契约、带观测的业务能力资产,模型越来越像一个跑在这些资产上的执行器。到那时,代理优秀与否,已经不取决于模型能不能灵光一现,而取决于你给它底座上的技能有多扎实。
最后分享一条个人经验:不要急着把什么都做成技能,先挑那个让你每天夜里睡不好的业务环节,把它做成第一个技能,跑出收益和标准流程,再逐步扩充。从一个技能开始,远比纸上规划二十个技能然后放弃更接近成功。技能系统最大的价值不是技术架构本身,而是它逼着你把不可控的模型行为,变成可依赖的业务能力。