
前阵子我在做一个客服工单自动处理Agent一开始把所有业务规则、话术模板和公共能力全塞进一个大Prompt里让模型自由发挥。结果不出所料模型经常把“查订单状态”和“修改收获地址”搞混参数漏传、流程跑到一半不知道该调用哪个原子能力气的我差点想把这套方案整个推倒重来。后来我把思路彻底换了一遍——不去“调教”模型理解一堆杂乱无章的能力而是把所有功能抽象成一组边界清晰、描述精确、可独立调用的技能包agent-skills让模型像查字典一样“看见”每个技能是干什么的、什么场景该用、需要什么参数。效果几乎是立竿见影。这篇文章就把我这几个月在技能系统上踩过的坑、总结出的设计思路和落地方法完整写出来给正在做Agent开发的同行做个参考。1. 一批技能如何改变Agent的行为方式从大Prompt到可编排能力单元先说一个反直觉的结论决定Agent好用不好用的一半是底模型本身的悟性另一半是你给它准备的那批技能长什么样。我在初版方案里回答“为什么原生的Function Calling不够用”这其实是个伪命题——问题不在工具调用框架而在于我根本没有按“技能”的颗粒度去组织能力。1.1 技能、插件、工作流、RAG这几个概念到底怎么分很多人一听到agent-skills就会问这跟插件Plugin、工作流Workflow、RAG检索有什么本质区别在工程上这几个东西经常混着用但边界其实完全可以分清楚插件本质上是一段代码包提供新的能力入口但它往往只负责“能不能调”不负责“何时调、为何调”的决策。如果给插件配了严格的描述和参数Schema它就变成了技能的一部分。工作流编排的成分更多常常是固定顺序的步骤串联比如“先查询订单、再判断是否超时、然后发起退款”。工作流适合流程稳定、不需要模型中途重定向的场景。RAG解决的是“知识从哪里来”的问题返回的是检索片段。技能解决的是“做什么动作”的问题执行后返回的是结果或者副作用两者的任务性质完全不同。技能Skill把描述、参数定义、执行函数、返回解析、甚至附属的提示词绑在一起由模型根据用户意图和上下文动态挑选是真正意义上的“模型可主动调度的能力单元”。在项目里我把它们的关系总结成一句话RAG负责喂知识工作流负责定流程技能负责给Agent一双手。这双手就是“能做什么”的集合剩下“该做哪件事、按什么顺序做”由Agent大脑去决策。1.2 技能化改造后的效果差异对比拿我那套工单Agent举例改造前后对比非常直观对比维度改造前巨型Prompt 零散工具改造后技能库 轻量Prompt模型误选工具率36%左右降到9%单轮查询耗时4-6秒每次请求带全部工具定义稳定在2秒左右新增业务规则改Prompt容易污染其它行为新增一个技能文件零侵入排障方式靠读对话日志猜看技能调用记录即可定位复用性几乎为零新Agent直接挂载同一批技能这个表不是我编的是上线后从日志里统计的真实数据。尤其是“新增业务规则零侵入”这一点对产品迭代速度的价值远大于数字本身。1.3 理解Agent技能调度的完整循环一个标准技能调用循环是这样的用户说一句话 → Agent理解意图 → 在技能列表里匹配最合适的技能 → 解析出参数 → 执行技能函数 → 把结果返回给Agent → Agent组织自然语言回复用户。整个过程里模型需要动态决策而技能的描述质量和参数清晰度直接决定决策准确率。所以我反复跟团队强调一句话别把技能库当成一个函数列表把它当成一本给模型读的操作手册而且是一本需要反复修订的操作手册。2. 技能设计的三个层面描述层、执行层、校验层各自要做什么一个真正合格的生产级技能绝对不是一个函数加了句注释那么简单。我拆成三层来做缺一层后面都会在线上蹦出幺蛾子。2.1 描述层模型眼里的说明书决定第一次选择对不对描述层包括技能名称name、技能描述description和参数定义parameters。这一层直接决定了Agent“会不会选它”以及“选完之后会不会用对”。我吃过最大的亏就是技能描述写得太抽象。比如我最早给“查快递”技能写的描述是这样的查询用户的快递物流信息。看起来很无害对吧但线上模型经常在用户问“我的包裹到哪了”“东西发货没有”“订单显示已签收”这类问题时完全不触发这个技能而是生硬地根据训练知识瞎编状态。后来我把描述改成这样当用户询问任何关于包裹、快递、物流、发货状态、配送进度、签收情况的问题时调用此技能。 输入参数为订单号或快递单号如果用户只提供手机号允许额外调用手机号查询订单技能获取运单信息后再查询。 注意此技能只做物流轨迹查询不做退货、修改地址等操作。改完之后命中率直接上了两个台阶。核心经验就是描述里要包含触发条件、边界约束、以及预期行为反例。让模型理解“什么时候该用”比让它理解“这是什么”重要得多。参数Schema也属于描述层的范围。千万不要只写字段名和类型要把取值范围、默认值、单位、缺省行为都标出来。比如时间参数我会写“ISO8601格式时区为UTC8如果用户说‘明天’则由模型结合当天日期自行换算”否则模型给你传个“明天”字符串进来函数侧根本没法解析。2.2 执行层不只是一个函数而是带业务语义的薄封装执行层是真正干活的地方。我在项目里的习惯是一个技能对应一个Python模块内部可以做任意事情——调外部API、查数据库、发消息、操作文件——但暴露给Agent的边界一定要薄。薄封装的意思是说技能函数不做高层的状态决策只做单点动作。比如“修改收获地址”这个技能它应该只负责接收“订单号 新地址”然后调修改接口至于这个订单是否允许修改、修改后要不要通知仓库那是别的技能或编排层去完成的。如果你把一大堆逻辑塞进一个技能里它就不再是技能而是一个隐藏的工作流模型会失去中间决策能力灵活性大打折扣。另外执行层一定要做到幂等。幂等的意思是同一个技能用同样的入参执行多次结果是一致的不会产生重复副作用。对用户来说模型如果超时重试了一次导致同一个工单被创建了两遍这种事故我在早期踩过后来所有写操作技能的代码里都强制要求带上幂等键服务端靠幂等键去重。2.3 校验层入参校验和返回归一化技能不被模型带偏的关键这一层是我加得最晚、却让我睡得最安稳的一层。校验层做两件事入口校验和出口归一。入口校验是指在技能函数真正执行之前先核对参数。比如“创建工单”技能必须校验工单标题不能为空、优先级字段只能是low/medium/high、邮箱字段要符合格式。如果校验不过不是抛异常让Agent懵而是返回一段结构化的错误说明比如{status: invalid_params, field: priority, message: priority 只接受 low/medium/high 三个值收到 unexpected_value}这种结构化的错误信息是给模型看的它能在下一步自己修正参数重新调用而不是报一串Traceback让模型对着堆栈瞎猜。出口归一是指无论内部调用的外部API返回什么乱七八糟的结构都要统一成一套标准格式返回给模型status成功还是失败data结构化结果数据字段名要直白比如logistics_status、estimated_delivery_timemessage可读的补充说明供模型组织话术suggestion可选字段告诉模型“接下来可以考虑做什么”相当于给模型的隐性引导我几乎在所有技能的返回里都刻意加了message这个字段哪怕只是“查询成功共返回3条物流记录”。因为模型在组织自然语言回复时直接引用这个字段里的信息比它自己根据数据字段瞎编要靠谱得多。3. 技能注册表从命名、版本到依赖关系的一次成型方案当技能数量超过几十个以后管理它们就成了一个新问题。你不能让每个技能都孤零零躺在目录里必须有一个注册表来统一维护元数据、版本和依赖关系。3.1 命名规范技能名的本质是给模型一个最短路径技能命名必须遵循一个原则一看就懂不易混淆。项目里我统一用“动词业务对象”的格式比如query_order_status、modify_ship_address、create_refund_ticket。名词永远放在动词后面动词一律用“query / create / modify / delete / send / notify”这类无歧义动作词避免用“handle”、“process”、“deal”这类含义模糊的词。我还做了一道防线构建同义词冲突检测。注册表里每个技能都有一个synonyms字段列出这个技能可能被说到的其它叫法。系统在启动时会检查有没有两个技能的synonyms互相覆盖有的话直接警告。比如cancel_order和refund_order如果synonyms里都写了“退单”模型就会犯迷糊这两个技能必须明确区分职责边界。3.2 版本管理给模型一个不混乱的技能版本空间技能不是写完了就永久不变的。业务规则更新、API参数调整、返回字段加值都会带来技能版本演进。我在注册表里给每个技能加version字段并且坚持少改动、多追加的设计能不修改现有技能就不修改需要变化就新增一个V2技能把旧的标记为deprecated。理由很简单正在线上的长对话里模型可能已经在较早轮次里摸清了旧技能的用法你突然把一个技能的入参从A改成B模型在下一次调用时会按照上下文推断出错误的调用方式。版本转换的机制也要留好。注册表里可以给新技能加一条supersedes字段指向被替代的旧版本同时旧技能的描述首行加一句“此版本已废弃仅用于兼容历史对话新对话请优先使用 query_order_status_v2”。模型看到这句话一般会自然转向新版实测效果不错。3.3 依赖关系发挥组合能力而不是把每个技能都做成全栈技能之间是可以互相调用的但这种调用关系需要在注册表里显式声明而不是在执行代码里偷偷import。我在技能配置里加了depends_on列表比如query_logistics依赖get_order_by_phone系统加载时会把依赖关系解析成一张有向图。这样做有实际的工程价值冷启动预热加载技能库时按依赖顺序做冒烟测试确保基础技能是健康的下游技能才能正常注册。循环依赖检测如果A依赖B、B又依赖A注册表启动时直接拒绝加载避免线上出现死循环。编排辅助依赖图还可以给模型提供路径参考比如模型想执行query_logistics但只有手机号可以看依赖图知道应该先调get_order_by_phone。不过我也要提醒一句技能之间的互相调用最好控制在两层以内超过两层会让调用链变深、单次请求耗时暴涨而且中间任何一层出错都难排查。我现在的要求是——一个技能最多依赖一个下级技能不能再多。4. 让模型真的“会用”技能描述编写、参数解析与多技能编排实践很多团队做技能系统把函数写出来、描述填上、注册表跑通就算完了。但真正上线后才发现模型怎么选技能、怎么填参数、怎么组合技能才是决定体验的胜负手。4.1 技能描述的一句话公式触发条件 行为边界 明确参数来源根据我的实践技能描述可以套用一个固定公式当[触发情境]如果[限制条件]则调用此技能参数来源于[具体指明]完成后返回[预期输出类型]。举一个我改好的真实例子当用户直接或间接表达希望查询订单物流状态时调用此技能。支持通过订单号或快递单号查询。如果用户只提供了手机号参数 order_id 可暂时留空优先调用 get_order_by_phone 解析出 order_id 后再次调用。此技能仅返回物流轨迹数据不执行发货、不处理修改地址、不发起退款。这个描述有几个要素都很关键触发情境写了“直接或间接表达”防止模型在用户说“我的东西到哪了”这种间接表达时不触发。参数来源写了“从手机号转换”这是给模型的执行策略提示避免缺参就瞎猜。行为边界列了三个“不”杜绝模型用这个技能去做超出职责的操作。4.2 参数解析陷阱模型漏参、错参、多参的三类实操处理参数解析是所有Agent项目里最让工程师头疼的环节之一。我遇到过的坑基本可以归成三类漏参——用户没提模型就不知道该填什么。处理方式除了在描述里写明“如果某参数未提供先尝试从上下文中提取提取不到则向用户反问”还可以在参数Schema里把字段设置成required: false但描述里明确说“一旦缺失会降低执行成功率建议追问”。错参——模型把语义相近但实际指代不同的内容填到了不存在的字段里。比如用户说“改成电话号码138开头的那个订单”模型可能把“138开头”当成订单号。这类问题光靠描述很难根治我在校验层做了参数类型与格式二次校验不对就返回invalid_params并附加“正确的字段示例”让模型自己纠正。多参——请求里塞了多余参数。这需要在执行层做白名单过滤只从请求参数里取Schema里声明过的键其它一律忽略。否则模型可能脑补字段名把外部API调得面目全非。4.3 多技能编排一个复合任务如何拆成多个技能步进执行Agent的强项是它可以把复杂任务拆成一串技能调用。举一个我线上稳定运行的案例——用户说“帮我查一下上周买的那个手机壳到哪了到了的话提醒我一声”。这个需求拆开其实是两件事查询物流、订阅送达提醒。第一轮模型先调用query_logistics_by_order_time_range定位具体订单拿到运单号后再调query_logistics_events看轨迹。如果发现未签收再调create_delivery_notification创建订阅。整条链路是模型自己一步一步走的而不是预先用工作流写死的。我能让模型这么顺畅走完这条链路靠的还是技能返回里的suggestion字段。查询技能返回时带了suggestion: 可以检查 delivery_notification 是否已为当前运单创建如未创建可调用相关订阅技能相当于在技能的出口处给模型递了一张小纸条它顺着纸条走就不会跑偏。经验单个技能不要试图解决用户的全部诉求把它做成一个“推进器”每执行一步返回一个“下一步建议”Agent自然就学会多技能组合了。5. 线上踩过的坑技能误选中招、参数错位、新旧版本冲突与恢复策略不管是解释原理还是给设计建议都不如直接把我踩过的坑列出来有说服力。这里挑四个真实发生过的线上事故每一个都花了不小代价才搞定。5.1 模型为什么总把“修改地址”和“取消订单”搞混现象是用户说“把订单的地址改成公司”模型一半概率会调用cancel_order。查日志我才发现原因出在技能描述里我写了“如果用户希望调整订单信息可调用此技能”。模型一看到“调整订单信息”觉得取消订单也是调整订单信息。解决方式是把所有技能描述的边界话说得非常死不要出现“调整”、“处理”、“操作”这类宽泛动词而是要直接写出用户视角的行为。“用户说改、说换、说换成指的是修改收货信息本身不涉及删除或取消订单。”另外我在两个技能的描述末尾刻意加了互相排除语句比如cancel_order的描述里写“此技能与 modify_ship_address 无关仅在用户明确表达退掉订单时使用”。这种描述层面的微小调整带来的是十几个百分点的准确率提升比我换模型、换温度参数都有效。5.2 参数解析错位时间格式和枚举值是最容易翻车的两处有一次用户问“这个订单什么时候发货”模型调用查询技能时把时间参数填成了当前时间导致系统一直查“现在的物流轨迹”什么也查不出来。根因是参数Schema里我写了date: string没告诉模型这个字段应该填订单创建时间的范围起点而不是当前时刻。参数Schema的字段说明必须回答五个问题这个参数的单位是什么、格式是什么、从哪里取比较合理、如果缺失会怎样、有没有候选值。我在项目里用JSON Schema的description字段把这五条全部写全宁可啰嗦也不能留白。对于枚举值比如排障级别、退款原因我都会在Schema的enum之外额外写一条“如果用户表达的意思不在候选值内选择最接近的候选值并诚实告知”。5.3 技能冲突两个技能都能回答同一问题时如何在编排层解决技能多了以后一定会出现多个技能可以处理同一类请求的情况。比如“查订单”和“查物流”用户说“帮我看看我的单子”两个技能的相似度评分可能都超过阈值。我给注册表加了一个优先级评分机制每个技能带一个priority字段如果模型返回的候选技能里有两个以上得分接近系统会综合优先级做仲裁。同时我重写了两个技能的描述明确划分“订单查询只返回订单金额、商品列表、订单状态不包含物流轨迹物流轨迹统一由物流技能返回”。让彼此互斥而不是靠模型临场猜。5.4 执行失败的恢复策略重试、降级、换技能的三步走技能执行不可能永远成功接口超时、上游报错、参数被服务端拒绝都可能发生。我现在的恢复策略是三层递进重试一次如果返回错误类型是timeout或transient_error校验层自动让同一个技能带同样参数重试一次间隔500ms。修正参数重试如果错误类型指向参数问题就返回结构化错误给模型让它根据错误信息修正参数后再次调用。技能切换如果重试两次仍失败就交给一个兜底的fallback_skill或者由Agent明确告知用户当前不可用并给出替代建议。这套策略上线之后用户侧感觉到的“技能失效”次数大幅减少。最关键的认知是技能挂了不可怕可怕的是挂了以后模型不知道该告诉用户什么系统设计一定要给模型一条明确的后路。6. 用小评测集和日志反哺迭代技能库的质量到底怎么度量最后一个部分聊度量。技能库不是写完就结束的它需要持续迭代。而迭代的前提是你能量化评估一版技能比上一版好在哪里。6.1 三个最常用的指标选中准确率、无效调用率、任务完成率我的面板上放三个核心指标全部可以通过日志自动计算技能选中准确率模型调用的技能是否与真实用户意图一致。评估方式是小样本标注每天抽100条调用记录人工判定是否选对。无效调用率技能被成功调用但并没有对最终回复产生价值。比如用户问“能退货吗”模型调用query_product_rule查了很久但最终只是回答了一个常识性问题这个调用就是无效的。任务完成率用户原始诉求在N轮对话内是否得到妥善解决。这个指标最难自动化目前靠会话结束后用户是否有新问题来间接估算。6.2 日志里怎么看技能质量微线索往往比大错误更值得关注有一次我发现某个技能的调用量极少但所有调用的result都是成功。点开详细日志才发现模型只有在用户连续追问两次时才会触发它说明技能描述里的触发条件写得不够前置模型把它当成了兜底技能而不是首选技能。通过日志反哺描述迭代我已经形成固定习惯每周把所有技能按调用次数排序重点看那些调用率低但相关提问多的技能以及选中后却频繁走到异常分支的技能。前者说明描述不够吸引模型后者说明技能内部实现和描述的预期不一致。6.3 如何搭建一个小规模的技能评测集防止改A坏B技能库迭代最大的风险是回归——改了B技能结果A技能的行为变了。因为模型是统一决策的技能描述之间会相互影响。我从一开始就搭了一个技能评测回归集收集200个真实历史用户问题按业务场景分成30组每组覆盖了这个场景应有的技能调用链路。每次修改任何技能描述或参数Schema都用这200个问题重新跑一遍。统计每个场景的技能选中准确率和调用路径与上次跑的结果做diff。这个评测集跑一轮大概需要10到20分钟成本完全可控。它不一定能帮你提升效果但绝对能在你上线新技能之前拦住那些“改A坏B”的意外。做技能库稳住基本盘比创造惊喜更重要。最后分享一个小小的实战心得我在技能系统的迭代路上犯过最大的错就是总想着让模型“聪明一点”其实模型已经很聪明了真正笨的是我给它的那本技能说明书写得不够好。把说明书改成用户视角、把边界写死、把参数说透Agent的行为立刻变得规规矩矩。建议每个正在做Agent的团队都认真花两周时间去打磨自己的技能库描述层你会回来感谢这个决定的。