做 Agent 开发这几年,我最大的一个体会是:决定一个智能体上限的,往往不是模型有多聪明,而是挂在它身上的那串 agent-skills 设计得有多稳。把 prompt 写得更长、换更大的模型,短期内确实能见效,但越往后瓶颈越在技能层——怎么定义、注册、路由、观测、降级、迭代,每一个环节都能决定生产环境是稳定运行还是频繁翻车。这篇文章会把我从第一个原型翻车到生产环境跑通的完整过程沉淀下来,把想清楚的部分和踩过的坑都讲透,适合正在搭 Agent 技能系统、或者想把自己手头那堆"工具集合"升级成"技能体系"的团队参考。
1. 一次失败的原型:为什么工具的集合不等于技能体系
我第一次给 Agent 加"技能"是在一个内部客服助手上。当时的需求很朴素:用户会问订单状态、退换货政策、物流时效,偶尔还有发票补开的申请。第一版实现得特别天真——我把十几个 API 工具的定义全部塞进了 system prompt,然后用 ReAct 循环让模型自己决定调哪个、按什么顺序调。
1.1 prompt 越堆越长,模型选择却越来越飘
刚开始只有几个工具,模型表现还算像模像样。等到工具数量超过二十个,问题就开始集中爆发了。prompt 里工具说明文本越来越长,光描述部分就占了三四千个 token;模型开始出现"拿错工具"的情况——用户明明在问退换货政策,模型偏偏调用了订单修改接口。最折磨人的是,每加一个新工具都要重新调试好几轮,新工具的表述总会和已有工具产生语义干扰,按下葫芦浮起瓢。
那段时间我一度以为是模型理解力不行,换了当时更强的模型,效果确实有改善,但 token 成本直接翻倍,而且只要两个工具描述稍微沾点边,选择不稳定的问题照样复现。这个阶段让我非常受挫,感觉自己在不断给一座摇摇欲坠的房子打补丁。
1.2 问题出在"把技能当成了工具的集合"
后来我停下来重新想这件事,发现根子不在模型,而在我自己对"技能"这个概念的认知。工具是什么?工具是单一动作的封装,比如"查询订单接口""计算运费";技能是什么?技能是带边界的、可以被独立触发和编排的能力单元,它不止包含"能做什么",还要覆盖"什么时候该用""执行失败怎么处置""用完之后会产生什么副作用"。
一个很直观的例子:查天气可以是一个工具,但"帮用户规划出行"是一个技能——它内部可能要查天气、查路线、算耗时、甚至查目的地附近有没有停车场。如果只把底层工具暴露给 Agent,模型就得自己组合好几个动作,任何一环的描述不清晰都会崩。可如果把"规划出行"沉淀成一个技能,模型只需要触发一次,内部流程由系统编排好,模型的负担小得多,稳定性也高得多。
这次复盘让我确定了一个方向:做 agent-skills 不是"给 Agent 多挂几个 API",而是围绕能力单元建立一套完整的设计规范、注册机制、路由策略和治理手段。后面所有的工作,本质上都是在补这套体系。
2. 技能定义的核心抽象:能力描述、输入输出 Schema 与副作用声明
想清楚"技能不等于工具集合"之后,第一件事就是把技能的定义标准化。这一节我重构过至少三版描述结构,最后沉淀下来一套相对稳定、也能支撑后续做权限和降级的字段体系。
2.1 一条完整的技能描述应该包含什么
我现在要求团队里每个技能都必须用结构化对象来定义,而不是散落在注释和文档里。一个实用的技能定义大概是这个样子:
# skill_schema.py from typing import TypedDict, Literal, List class SkillIO(TypedDict): name: str description: str schema: dict # JSON Schema class SkillDef(TypedDict): name: str # 技能唯一标识,如 order_refund summary: str # 一句话概述,给模型快速了解用途 description: str # 完整触发条件与能力说明 input_io: SkillIO # 输入定义 output_io: SkillIO # 输出定义 side_effects: List[Literal["no_op", "write", "notify", "costly"]] timeout: int # 超时时间(秒) fallback: str # 降级策略描述这套字段里,side_effects和fallback是最容易被新手忽略的,但它们在权限控制和异常处理这两个后续环节里几乎是救命的。技能定义不只是一份"给模型看的说明",它更应该是一份"系统在运行时可以做决策的依据"。
2.2 description 才是模型做决策的"判断依据"
很多人写技能描述时喜欢写"调用订单模块的 refund 接口",这个写法对工程师友好,对模型完全不友好。应该翻译成模型能理解的自然语言:"当用户表达退款或退货意图,且已确认订单信息时,使用本技能发起退款申请。如果用户只是询问退款政策,不要调用本技能,请改用政策查询技能。"
有几个细节值得展开:
- 描述里要写清楚"什么时候应该用",同时还要写清楚"什么时候不应该用",排他性信息对模型帮助极大。
- 描述里要写清楚"用户完整表达了什么意图才触发",防止只凭一个词就触发高风险动作。
- 模糊的对话场景下,宁可引导模型触发一个只读查询技能,也不要让它触发写操作技能。
写操作技能(退款、下单、改密码)如果没有清晰的触发边界,模型极容易只凭一句话里的疑似意向就提前执行,一旦执行就是不可逆的,后果很严重。我在这个点上吃过很大的亏,后面第 6 节会详细讲。
2.3 输入输出 Schema 不要直接抄接口参数
技能暴露给模型的 Schema,不需要和底层 API 的入参完全一致。技能层要做的是"语义化入参"——把底层接口零散的参数打包成模型容易填写的字段。
举个例子。底层订单查询接口需要传merchant_id、channel_code、biz_order_id、user_token四个参数,对模型来说它只知道用户说了一个订单号。那么在技能层,输入 Schema 就只定义成:
{ "type": "object", "properties": { "order_id": { "type": "string", "description": "用户在对话中提供的订单号或电商单号" } }, "required": ["order_id"] }模型能正确填出order_id的比率,远比让它同时填四个底层参数的比率高得多。至于内部怎么根据order_id查出merchant_id和user_token,那些都是技能内部实现,不该暴露给模型。
2.4 副作用声明如何影响调度与安全控制
我在side_effects里定义了四类:no_op(只读查询)、write(会写数据)、notify(会给用户发消息)、costly(昂贵调用)。模型侧其实不需要看到完整的副作用枚举,但它需要从技能描述里感知到"这是一个读操作还是写操作"。
在调度侧,副作用声明有两个用处:一是决定能不能走结果缓存——no_op类技能可以做缓存,write类技能绝对不能;二是决定是否需要插入二次确认——write和notify类技能在真正执行前,系统可以自动加一个确认环节。这个设计让"技能层"和"安全控制层"彻底解耦,权限规则不再散落在各个技能内部,审计起来也干净得多。
3. 技能注册与分发机制:从字典硬编码到运行时技能总线
定义完技能结构,下一个问题是:这些技能怎么被系统发现、怎么被 Agent 拿到、怎么在几十个技能并存时选出正确的那一个。这块的演进路径,我建议你按自己的实际阶段来,不用一上来就追求最复杂的方案。
3.1 第一版:字典注册,简单但迟早不够用
最朴素的做法是写一个注册表,把技能名映射到处理函数:
# registry_v1.py SKILL_REGISTRY = { "order_query": order_query_handler, "refund_apply": refund_apply_handler, "policy_query": policy_query_handler, } def dispatch(name, params, context): handler = SKILL_REGISTRY.get(name) if not handler: return SkillResult.fail(f"skill {name} not found") return handler(params, context)这种方案在技能数量少于十个时完全够用,但它的问题很快会暴露:技能元数据(描述、副作用、超时时间)散落在代码的各个角落,每次想调整技能描述都要改代码发版;没有统一的加载时机和热更新机制;想给技能做多版本、灰度发布、A/B 测试更是无从下手。
3.2 第二版:装饰器驱动的声明式注册
我很快把注册方式改成了声明式,用装饰器把"技能定义"和"处理函数"绑定在一起:
# skill_registry.py _skills = {} def skill_register(skill_def): def decorator(fn): skill_def.handler = fn _skills[skill_def.name] = skill_def return fn return decorator @skill_register(SkillDef( name="refund_apply", summary="处理用户退款申请", description="当用户表达退款或退货意图,且已确认订单信息时,使用本技能发起退款申请。...", input_io=SkillIO(...), output_io=SkillIO(...), side_effects=["write"], timeout=10, fallback="refund_apply_fallback", )) def refund_apply(params, context): ...这种方式比字典硬编码好了不少:技能定义和处理逻辑放在一起,查起来方便,也更容易做静态检查。但到这一步,它其实还只是一个"更好维护的字典",真正让它变成"技能总线"的,是后面三件事:运行时元数据加载、技能发现接口、以及按需组装技能视图。
3.3 第三版:运行时技能总线与按需裁剪的技能视图
最终版本里,我把技能仓库设计成一个独立的运行时组件,它负责四件事:注册、校验、发现、导出。
- 注册:启动时加载所有技能定义,做 schema 合法性校验、副作用枚举校验、技能名唯一性校验。
- 校验:确认输入输出定义是合法 JSON Schema,
fallback指定的降级技能真实存在,依赖的技能也在技能池里。 - 发现:根据当前对话上下文,从技能池中召回一部分候选技能。
- 导出:把候选技能的描述文本拼进 prompt,或者转成 function calling 的 tools 参数。
这里有两个细节对线上效果影响非常大。
第一,技能视图必须是按需裁剪的。把全部技能都塞给模型,一方面 token 成本高,另一方面技能一多模型的选择精度就会掉。我在导出前加了一个召回层——用"当前意图分类 + 关键词匹配 + 少量向量检索"从技能池里筛出 Top K 候选,再交给模型。实测技能池从 40 个技能缩小到每次 8~10 个候选之后,工具选择的准确率提升非常明显,token 消耗也降了将近一半。
第二,技能描述导出时要有统一的胶水格式。我建议所有技能导出给模型时都统一成"技能名 + 一句话 summary + 何时触发与何时不触发的说明"。这个固定格式可以让模型更快地适应"在多个技能之间做决策"这个任务,减少不同技能描述风格差异带来的干扰。
3.4 技能数量的临界点:从全量注入到按需召回
如果你现在只有五六个技能,全量注入完全没问题,不用追求复杂设计,那是过度工程。我的经验是:当技能数量超过 15 个,或者单条技能描述超过 300 个 token,就该认真考虑引入召回机制了。
给你一个更直观的类比:全量注入是把一整本电话簿递给模型,按需召回是只把当前对话最可能用到的几页递给模型。电话簿越厚,模型翻错页的概率越大,决策时间也越长。
4. 多技能协同编排:意图路由、依赖处理与上下文传递
技能注册机制建立之后,真正复杂的工作才刚刚开始——多个技能凑在一起,怎么让它们协作得好。这个阶段处理不好,技能再多也只是"看起来丰富",用起来还是四处漏风。
4.1 显式编排与动态规划:两条路线如何取舍
我见过两类典型方案。一类是"全动态路由",让模型自由选择技能并自行决定调用顺序,所有流程都写在模型脑子里。另一类是"全固定流程",预先用代码写死编排模板,技能触发序列完全固定,比如先查订单再查物流再给结论。
我的实践结论是:关键业务路径必须显式编排,探索型场景可以动态规划。
举两个真实场景。用户说"帮我看看我那个订单什么时候到",如果走全动态路由,模型可能要依次触发订单查询、物流查询、时效计算三个技能,任何一步选错,整体结果就歪了。但如果我们预先定义好一个"订单时效查询"技能,内部固定编排好三步,模型只需要触达一次,成功率会高很多。
反过来,用户说"我想去厦门玩三天,帮我想想怎么安排",这种开放式任务你很难预先把所有组合写成模板,动态规划反而更合适——模型可以在交通、住宿、景点、美食这几个技能之间自由跳转,生成一个组合方案。
所以我的编排设计原则是:业务确定性越高,编排越往代码侧下沉;开放性越高,编排越往模型侧上浮。两种能力都要有,而不是只押注其中一种。
4.2 技能依赖处理:A 技能的产出如何成为 B 技能的输入
多技能协作时最容易被忽视的是依赖关系。比如"取消订单并退款"这个组合动作里,退款技能必须先拿到订单查询技能产出的订单金额和支付流水号。如果这些数据要模型自己记,prompt 会变得异常复杂,而且容易漏。
我推荐在技能定义里显式声明依赖,而不是让模型现场发挥:
class SkillDef(TypedDict): ... dependencies: List[str] # 前置技能名列表 injects: List[str] # 从前置技能输出中注入到本技能上下文的字段调度器在执行某个依赖型技能前,先检查前置技能输出是否存在;不存在就先触发前置技能,并把需要的数据注入到当前技能上下文。这一步做扎实之后,模型完全不需要在上下文里"记住"上一轮技能输出,因为编排器已经把数据接好了。
4.3 上下文传递的三类高频陷阱
技能之间的上下文传递,我在这上面翻过不少车,总结出三类高频问题。
第一类是对话历史的过度携带。有些技能其实只需要当前这轮用户输入,开发时却图省事把整整十几轮对话历史都传给了技能内部调用的大模型,结果技能执行变慢、成本变高。我的做法是给每个技能定义一个"上下文窗口",通常只注入最近 2~3 轮对话加上与该技能相关的系统状态,其余历史不传。
第二类是隐式状态的丢失。技能 A 执行完把结果写进了内存,但技能 B 所在的计算单元如果是无状态的,B 就拿不到 A 的结果。这个问题在微服务化之后尤其明显,我最后的解法是在编排层引入一个显式的会话快照,技能产生的结构化产物都落到快照里,B 再从中取值。
第三类是时区、单位、货币等隐含语义没有对齐。模型在技能 A 里输出了订单金额,技能 B 里却把币种当错了,这种错用户一眼就能看出来,体验极差。技能层应该强制要求:跨技能传递的一切数值字段必须携带单位、币种和时区声明,不要在描述里默认"大家都懂"。
4.4 超时与中断:执行到一半用户反悔了怎么办
模型调度技能执行期间,用户随时可能插话或者修改意图。技能执行到一半,新意图已经很明确不想继续了——这时如果继续硬跑完,是浪费资源和成本;如果直接中断,又可能留下脏数据。
我在编排器里设计了"检查点"机制:每个技能内部划分成多个可中断步骤,每个步骤执行前先检查当前会话是否有新的意图请求,如果有就标记当前步骤为"已取消",并执行该技能定义里声明的 cancel 策略——回滚、等待完成、或者直接丢弃。
这个机制看着简单,但对线上体验的提升非常大,尤其适用于耗时较长的技能,比如生成式报告、批量处理任务。用户以为自己在"打断"系统,实际上系统确实响应了打断,而不是继续闷头跑完再给出一个无人关心的结果。
5. 生产环境里的技能治理:权限边界、可观测性与降级演练
技能系统一旦上了生产环境,你很快会意识到"能跑"和"能长期稳定跑"是两码事。这一节要讲的治理事项,每一项都是线上事故换来的教训。
5.1 权限边界:技能能做什么,必须在配置层显式声明
很多团队在原型阶段让 Agent 使用一把全局 API Key,所有技能共用同一套凭证。这个做法在原型阶段确实省事,但生产环境必须拆开,原因很简单:你没办法在一个共享凭证上做审计,也没办法限制某个技能不能访问另一个系统的数据。
我的做法是每个技能单独声明所需的权限,维度至少包含:目标系统、操作类型(读/写/删除)、配额上限、敏感字段脱敏策略。权限校验发生在技能注册阶段和调用入口,而不是在技能内部自行判断。这样审计的时候可以很清楚地回答"这个技能到底被授权了什么"。
我踩过最疼的一个坑是:某个查询类技能因为共用凭证,误触发了另一个系统的删除操作。那次事故之后,我强制要求所有技能必须显式声明最小权限,甚至把权限声明纳入技能上线的 CI 检查——权限声明不通过,技能就不允许注册。这个改动看着不起眼,但它把"人靠自觉"变成"流程强制"。
5.2 可观测性:技能调用需要全链路追踪
技能层的可观测性和普通 API 的可观测性不太一样:你不仅要看耗时和错误率,还要看"这次调用的触发理由是什么"。也就是说,每次技能调用至少要记录三部分数据:模型决策前看到的上下文摘要、模型选择该技能时输出的原始片段、以及技能入参与出参的摘要。
我建议至少按这张表的结构来记录:
| 维度 | 记录内容 | 目的 |
|---|---|---|
| 触发 | 技能名、触发时的意图分类、模型原始输出片段 | 定位误触发 |
| 输入 | 入参摘要、上下文快照引用 | 复现问题 |
| 输出 | 出参摘要、是否降级、是否中断 | 评估效果 |
| 性能 | 耗时、token 消耗、错误类型 | 成本与稳定性 |
这些数据直接决定了你能不能回答"这个技能真的被正确触发了吗",而不是只能回答"这个技能被调用了几次"。前者能帮你持续优化技能描述,后者只能帮你做汇报。
5.3 降级策略:每个技能都要有 Plan B
线上服务没有不挂的。第三方天气接口会限流,订单系统会超时,向量数据库也会有毛刺。你的技能系统如果没有降级方案,一个底层服务抖动就会让整个 Agent 表现得像个傻子——要么一直报错,要么模型自己编一个答案。
我给每个技能都要求写fallback字段,明确"主能力不可用时怎么办"。降级方案通常有三个层次:直接返回可理解的失败话术,比如"暂时无法查询物流信息,请稍后再试";切换到同义能力的替代技能;或者降级到半自动人工处理通道。
关键点是:降级不能是代码里偶发的 try-except,而要在设计阶段就规划好。每次新技能上线前,我会让团队成员手动模拟底层服务挂掉,观察 Agent 的表现是否符合预期。这个"降级演练"应该像做备份恢复演练一样常态化,而不是等出了事故才想起来。
5.4 灰度发布:新技能先进影子模式
新技能上线最怕什么?怕模型在还不了解它边界的时候被误触发,导致线上用户遭遇不可预期的操作。我的做法是引入"影子模式":新技能在技能池里正常注册,但调用时只记录"如果当时不是在影子模式,我会执行什么"的日志,不真正执行。观察一段时间,统计它的触发频率、触发理由以及和已有技能是否产生抢占,确认无误后才转为正式执行。
这一招对写操作类技能尤其重要。影子模式本质上是在给技能做一场"无人受伤的预演",让误触发问题在造成真实影响之前就暴露出来。
6. 我在生产环境里踩得最深的三类技能坑
前面讲的是方法论,这一节讲具体翻车现场。这三类问题几乎每个做 agent-skills 的人都会遇到,而且踩坑路径高度相似,我把排查和修复过程完整写出来。
6.1 技能描述太"技术化",模型根本听不懂
有个查库存的技能,我第一版描述写的是"通过 inventory service 的 get_stock 接口查询 sku 的可用库存"。上线后马上发现问题:用户说"这个还有货吗""这个还能买吗""什么时候补货",模型完全不触发这个技能,反而去触发了商品查询技能。
排查链路是这样的:我先看调用日志,确认模型确实收到了技能列表,然后查看模型的原始输出,发现模型在理解get_stock、sku这类词上明显犹豫,最后选择了名称上更像"查商品"的技能。
修复方式很简单:把描述彻底改写成人话。"当用户询问某商品的现货数量、是否有货、能否购买、何时到货时,使用本技能查询实时库存。注意:查询商品基本信息和价格时不要使用本技能。"改完之后误触发率立刻降了一个量级。这件事给我的教训是:技能描述是写给模型读的,不是写给工程师读的,所有内部项目代号、接口名、技术缩写,都要翻译成模型能理解的自然语言。
6.2 技能异常处理太笼统,模型陷入重试死循环
有一次线上事故让我印象特别深。某个技能因为底层服务限流返回了一个通用的system error,模型拿到这个错误后,以为是自己的参数填得不对,于是反复重试同一个技能,一连重试了五次,把底层服务彻底打到熔断。
问题根源有两个。一是技能返回的错误信息没有区分"参数错误"和"服务暂不可用";二是模型缺少"遇到这类错误时应该停止还是换一个方案"的指导。
修复方案我做了三层。第一层:技能内部统一封装错误码,至少区分input_invalid、service_unavailable、timeout、forbidden四类。第二层:在技能描述或系统提示里明确告诉模型,"如果收到 service_unavailable,不要重试,直接向用户说明服务繁忙,请稍后再试"。第三层:在调度器里做重试熔断,同一技能连续失败 N 次就暂停该技能一段时间,从机制上杜绝异常循环。三层叠加之后,类似事故再没有出现过。
6.3 技能职责重叠,模型选择不稳定
技能一多,职责边界天然会模糊。举个例子,我有"订单查询"和"售后进度查询"两个技能,用户问"我的退款到哪一步了",两个技能看起来都能答,模型每次随机选一个,返回值的风格还不一样。
这个问题排查起来最费劲,因为不是报错,而是"不稳定"。用户不会说你错了,只会觉得这个助手时灵时不灵。最后靠统计技能触发日志和模型交付结果对比,才定位到职责重叠。
根治办法是给技能画一张"决策边界矩阵":列出所有高频用户意图,逐个确认该意图应该固定路由到哪个技能,不允许出现"都行"的灰色地带。两个技能如果覆盖了同一个意图,要么合并,要么在描述里明确排他关系——"当用户询问退款或售后进度时,不要使用订单查询技能,请使用售后进度查询技能。"做完这轮梳理后,模型选择稳定性提高了非常多,而且这张矩阵本身也成了团队新人的入门文档。
7. 技能系统还可以往哪里走:组合模板、用户自定义与效果闭环
聊完踩坑,再说几个我们正在推进的扩展方向,这些方向不一定每个团队都需要,但可以给你一个参考坐标。
7.1 把高频编排逻辑沉淀成组合模板
显式编排和动态规划之间,有一个经常被忽略的中间地带——组合模板。比如"订单全流程查询"可以是"订单查询 + 物流查询 + 售后状态查询"的组合,"出行助手"可以是"天气 + 路线 + 停车场"的组合。把高频出现的技能调用序列固化成模板,既能提高成功率,也方便后续做 A/B 测试,比较不同组合对用户满意度的影响。
7.2 让部分用户自定义技能
再往后,可以尝试把技能系统的使用边界从开发者扩展到更有技术背景的用户。提供一套可视化的技能编辑器,让用户自己配置"触发条件 + 调用动作 + 返回话术",本质上是在复用同一套技能注册、校验、路由、观测的基础设施,只是换了一个入口。这个方向对平台型产品尤其有价值,能让 Agent 的边界从团队自己扩展到整个用户生态。
7.3 建立技能效果的反馈闭环
每个技能上线后,都应该形成效果闭环:触发准确率、误触发率、降级率、单次调用的 token 成本、用户对返回结果的满意度。这些指标反哺到技能描述改写和决策边界矩阵的调整上,才能让技能系统越跑越稳。技能系统不是一锤子买卖,它需要像产品一样持续运营。
我在实际运营中最大的感受是:agent-skills 的价值,一半在初期的设计规划,另一半在长期的迭代治理。技能写得再好,没有观测、没有降级、没有边界梳理,早晚会在某个线上角落爆雷;反过来,只要治理到位,即使技能数量增长很快,系统也依然可控。希望这些从实战里长出来的经验,能帮你少踩几个我已经踩过的坑。