上周有个项目把我折腾得够呛。一个基于大模型做的客服知识库Agent,用户问“怎么退款”它答得挺好,换个说法问“钱什么时候能到账”,它就开始搬出另一个技能的内容,东拉西扯,最后还给出一段完全无关的售后政策。我盯着对话日志看了半天,问题不在模型本身,出在技能的组织方式上——所有逻辑挤在一起,职责边界模糊,模型一遇到语义相近的问题就开始“乱串门”。
这其实是做Agent落地时最常踩的坑。你精心编排的Prompt、精心挑选的工具,一旦技能数量超过5个,行为就开始不可控。今天这篇就聊聊我实际项目里沉淀下来的5种Agent Skill设计模式。它们不是什么高深理论,都是解决具体问题的手段:让技能边界清晰、让复用不靠复制粘贴、让模型每一步都有据可依。如果你正在做Agent开发,或者被“模型又答偏了”折磨得头疼,这篇应该能帮上忙。
1. 先看看Agent为什么“不稳定”
想理解设计模式的价值,得先搞清楚Agent为什么会不稳定。很多情况下,问题不在模型推理能力,而在我们把任务交给模型的方式太粗糙了。
1.1 病根拆解:三个跑偏的常见原因
第一个原因是上下文污染。模型每次调用都在处理整个对话历史加系统提示词,一旦技能定义(Tool Schema)和用户问题语义有重叠,模型就容易选错工具。比如你同时定义了“查询订单状态”和“查询物流轨迹”两个技能,前者针对订单内部流转,后者针对运输环节,但在模型看来,两者都跟“我的快递到哪了”这种问题沾边,它就随机挑一个执行。
第二个原因是技能粒度过大。一个技能里塞了太多逻辑分支,比如“处理售后”这一个技能,内部包含了退款、换货、维修、投诉、进度查询五个子任务。且不说模型能不能准确命中分支,光是技能的Description写得过长,模型读起来都费劲。上下文窗口有限,描述越长,模型越容易忽略关键约束。
第三个问题是输出不可控。很多技能让模型自由发挥输出格式,结果模型偶尔会冒出JSON也不像、自然语言也不像的内容。一旦下游程序解析失败,整个流程就断了。或者模型在推理过程中“过度发挥”,在结论里擅自加了推测性内容,这在金融、医疗、法律这类严谨场景里是灾难性的。
1.2 Skill设计模式是什么,不是什么
这里说的Skill,你可以理解成“赋予大模型的一项可复用能力单元”。形式上它可以是一个函数定义(Function Calling中的Tool),也可以是一个带输入输出约束的Prompt模板,甚至可以是一段独立封装的后端服务调用。设计模式,就是给这个能力单元定的一套“标准动作”。
一个好模式至少要解决三个问题:可复用性——换个项目、换个模型,这套设计还能直接用;稳定性——同样的输入,多次调用的结果一致,不会因为措辞微调就行为漂移;可诊断性——出问题时,你能快速定位是模型选错了、工具报错了,还是参数传错了。这三个目标,正好对应上面说的三个病灶。
它不是什么?不是万能的架构方案。别指望套个模式,Agent就不会犯错了。模式只是降低犯错概率和缩小排查范围的手段,最终效果还要看后面的工程细节。
2. 模式一:原子技能模式
这是最基础、也最容易被忽视的模式。核心一句话:一个技能只做一件事,并且把这件事做到干净利落。
2.1 核心思路:一个Skill只做一件事
我见过太多项目把“万事通”型技能当成默认设计。写一个process_order函数,既能查订单,又能改地址,还能申请发票,看起来功能强大,实际上就是把复杂度全部抛给了模型。模型需要自己判断:用户当前这句话到底对应哪个子操作?这个判断本身就容易出错。
原子技能模式要求你反着来:拆。把“大而全”拆成“小而专”。比如订单域,就拆成search_order(按关键词查单)、get_order_detail(查详情)、update_order_address(改地址)、apply_invoice(申请发票)。每项技能只对应一个清晰的用户意图,参数尽量少,描述尽量短。
这样做的直接好处是:模型不需要在同一个技能内部做复杂分支判断,它只需要从若干个意图明确的技能里做选择就行。选项越清晰,选择越准确。这跟给菜单排序是一个道理——一个餐厅提供200道菜的菜单,顾客点餐的决策成本远比只有20道菜的菜单高得多。
2.2 代码示例与契约定义
拿订单查询来举例,一个原子化的技能定义大概是这样的:
{ "skill_name": "search_order", "description": "根据订单号或商品关键词查询订单基本信息。只能用于查询,不能用于修改订单。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "用户提供的完整订单号,必须精确匹配" }, "keyword": { "type": "string", "description": "用于模糊搜索的商品名称关键词" } }, "minProperties": 1, "additionalProperties": false } }注意几个细节:Description里明确写了“只能用于查询,不能用于修改订单”,这等于给模型划了一条行为红线,防止它在能力边界外发挥;参数里加了additionalProperties: false,杜绝模型乱传没用字段;minProperties=1保证至少有一个查询条件,否则模型啥都不传就调用,后端查个寂寞。
2.3 适用场景与注意事项
原子技能最适用的场景是:任务边界清晰、输入输出可枚举、单一操作后无复杂状态流转。比如查天气、算运费、查库存、发验证码这类。
要提醒几个坑。第一,不要为了原子而原子,拆太碎会让模型要在一大堆技能里做选择,选择成本反而升高。经验值是控制在15-20个技能以内,超过这个数量就要考虑模式四的路由机制。第二,技能Description不要写废话。模型对描述文本的注意力是有限的,把“该技能用于查询订单信息,查询订单是用户在下单后最常进行的操作之一,本技能适用于所有订单查询场景”这种绕圈子的话删掉,直接写清楚功能和边界就好。第三,注意词性统一。所有技能的Description都用陈述句,保持结构一致,模型不需要切换理解模式。
3. 模式二:结构化输出与校验模式
模型乱输出,是Agent不稳定的最大来源之一。这个模式专门治这个问题。
3.1 为什么要给Skill套上Schema
很多开发者在设计Function Calling时,对输入参数要求严格,但对输出格式放任自由。模型返回一段自然语言,程序拿到手没法直接操作,还得再做一轮解析,解析失败了还得让模型重新生成。来回折腾,稳定性直线下降。
结构化输出模式要求:每个Skill的输出都有严格的JSON Schema约定,并且在下游增加一道校验闸门。这等于强制模型按契约办事——你说返回JSON,就必须是JSON,少一个字段、多一个字段、类型不对,都算调用失败。
实际操作中,我在定义技能时会给每个技能增加一个response_schema字段,描述返回结果的预期结构。同时在下游代码里,用JSON Schema校验库或者手写校验逻辑做双重保险。手写校验逻辑其实也不复杂,关键字段逐项检查就行。
3.2 正反例对比与校验兜底
举个例子。我做过一个发票信息抽取的Agent技能,输入一段客户提供的开票信息文本,输出结构化内容。刚上线时让模型自由发挥,结果就是下面这样:
{ "company_name": "某某科技有限公司", "tax_id": "91310000MA1K35WXXX", "address": "上海市某某区某某路100号", "phone": "13800138000", "bank": "中国银行上海分行", "account": "34567890123" }看起来还行是不是?问题在于,不是每次模型输出都这么规整。有时候它会多出一个bank_name字段,有时候会把phone写成数字类型,有时候直接返回一段带解释的文本:“以下是提取的发票信息:{...}”。一次一个样,下游解析器写得再健壮也扛不住。
后来改成强制response_schema约束,并在技能描述里写明“必须返回JSON对象,字段严格遵循schema定义,禁止添加额外字段”,再加一道后端校验,解析失败就自动重试一次。从那以后,这个技能的解析成功率从不到80%提到了99%以上。剩下的1%基本是原始文本信息缺失导致的字段为空,能被校验逻辑准确捕获并进入人工处理流程。
3.3 参数设计细节
除了输出Schema,这个模式还要求你在设计阶段就想清楚:哪些字段必填?哪些字段允许为空?枚举值的取值范围是什么?这些都要写进Schema里。
我常用的校验方法是:先做类型校验,再做枚举值校验,最后做语义合理性校验。比如“退款金额必须为正数”“日期格式必须满足YYYY-MM-DD”“状态只能取pending、processing、done之一”。语义校验放在最后是因为它最消耗时间和token,能给前面两道拦住的问题就不要留给它。
这个模式的核心理念是“不信任模型”。不是说模型不可信,而是你要把模型当能力很强但有随机性的同事看待——你跟他反复对齐输出格式,他确实能做好;但如果你不规定清楚,他每次交上来的东西格式都带点个人风格。校验就是那个击碎风格差异的机制。
4. 模式三:自我纠错模式
模型一次做对很难,但让它自己发现自己错了、然后重做一遍,成功率会大幅上升。这就是自我纠错模式的核心逻辑。
4.1 一次做对很难,两次做对没那么难
大模型有个很有意思的特性:自由回答时容易出错,但如果你让它对自己的回答做评审,它往往能准确指出问题。这不是玄学,而是因为“评审”这个任务的语言模式更简单——它不需要生成完整答案,只需要做判断题和找错题。
自我纠错模式就是利用这个特性。流程分三步:模型先基于用户输入生成一份初稿;然后一个评审角色(通常是同一个模型,用另一段Prompt激活评审视角,或者是另一个更小的模型)检查初稿是否符合技能约束和用户真实意图;最后把评审意见和初稿一起回传给生成模型,让它修改。
这个模式对“跑偏”问题的针对性极强。比如你让Agent总结一段会议纪要里的行动项,它第一次可能漏掉了某个负责人。如果直接把它生成的总结拿给用户看,可能没人发现遗漏;但如果让评审人去核对“每个行动项是否都包含负责人字段”,缺失就立刻暴露,修改后的结果就会完整很多。
4.2 具体实现:轻量评审循环
实现上,这个模式的核心是评审Prompt。我常用的评审模板长这样:
你是一名严格的评审员。请根据以下规则检查模型输出是否满足要求: 1. 是否包含所有必要字段({必要字段列表}) 2. 是否直接回答了用户的原始问题(用户原始问题是:{问题原文}) 3. 是否有多余的推测性内容,如果有请明确指出 4. 输出格式是否符合约束:{格式要求} 要检查的输出内容如下: {模型初稿} 请输出一份JSON格式的评审报告,包含字段:issues(问题列表,每条包含类型和说明),passed(布尔值)。拿到评审报告后,如果passed为false,就把初稿、评审报告和“请根据以上问题重新生成”的指令一起传给生成模型。重试一到两次。一般第一次重试后的通过率就能达到95%以上,第二次重试后再提升就有限了,没必要无限循环。
4.3 防死循环的阈值设计
自我纠错模式有个致命隐患:死循环。模型每次评审都能挑出新问题,每轮修改都可能引入新错误,Agent就卡在循环里出不来。
我的标准做法是设重试上限,最多两轮重试。连续两次评审都判定需要修改,就不让模型继续折腾,直接把当前最佳版本交给用户,并附上一句“结果可能存在不完整之处,请核查”。把失败控制在有限范围内,好过无限重试消耗时间和token。
另外要注意,自我纠错模式不适合所有场景。如果是时效性要求极高的对话(比如用户在线咨询),每轮纠错都意味着多一次模型调用,多几百毫秒延迟。这种情况更适合只做格式化校验(模式二),不做内容级评审。如果是对准确率要求极高的离线任务(批量信息抽取、日报生成),那自我纠错就是标配。
5. 模式四:按需路由与动态分发模式
当技能数量超过一定规模,让模型每次面对完整技能列表做选择就是一场灾难。这时需要把决策拆成两级:先决定“走哪个方向”,再决定“调用哪个具体技能”。
5.1 问题场景:大而全的Skill为什么会跑偏
在某个项目中,我一度给Agent配了30多个技能。结果模型在工具选择上的错误率明显上升,典型的误选比如:
用户问:“你们有没有线下门店?”
模型选中了search_product_availability技能,而不是get_store_list技能。为什么?因为search_product_availability的描述里包含了“查询某商品在哪个门店有货”这个短语,描述语义跟我实际想问的门店信息沾边了。模型看到“门店”两个字就兴奋了。
这就是大而全的问题:技能越多,描述之间的语义边界越模糊。模型不是理解能力不够,而是面对30个相似选项时,它自己也分不清哪个才是用户真正要的。
5.2 路由器怎么设计
按需路由模式的核心思路:不把所有技能一次性暴露给模型,而是用一个“路由器”技能做粗粒度分类。
路由器本身也是一个技能,它的作用不是完成用户任务,而是通过分析用户输入,决定下一步调用哪个技能组。比如按业务域划分:订单域、商品域、售后域、支付域、账号域。路由器只需要从这几个域里选一个,选择范围从30个技能骤降到5个域。
选完域之后,再把该域相关的5、6个技能细节填入上下文,让模型做第二轮精确选择。这个两级选择机制,跟搜索引擎先分领域再找具体页面的思路如出一辙。优点是模型每次面对的选项都少而精,选择的准确率大幅提升。
5.3 分发策略与兜底逻辑
路由器的实现,我们项目里试过两种。简单场景,直接用当前模型加一个route_domain函数定义:
{ "skill_name": "route_domain", "description": "分析用户意图,判断该问题属于哪个业务域。只能输出一个域名,不要尝试回答问题。", "parameters": { "type": "object", "properties": { "domain": { "type": "string", "enum": ["order", "product", "after_sale", "payment", "account", "other"] } }, "required": ["domain"] } }域名用英文枚举值,避免中文分词歧义。另外加一个other兜底,模型判断不了就直接走人工或通用问答流程,不要硬选。
复杂场景,建议换一个更小的模型专门做路由。小模型速度快、成本低,路由任务对推理能力要求其实不高,大材小用纯属浪费。实测下来路由用一个小模型,精确执行用一个大模型,整体成本和速度都更优。
路由模式还有个额外好处:日志里第一行就是路由器选的域,排查问题时你不用翻遍整个对话记录,一眼就能看出模型是否从一开始就跑偏了。
6. 模式五:状态隔离与记忆持久化模式
Agent跟普通API调用的最大区别在于:它是有状态的。用户说“刚才那个订单”,指的是对话历史里上一轮提到的订单。状态管理不好,Agent就会“失忆”,或者更糟——“串记忆”。
6.1 为什么Skill要自己的“小账本”
先想一个问题:技能执行过程中产生的中间数据,应该放在哪?很多人直接把所有中间数据全堆在对话上下文里,模型每轮调用都重新读一遍。上下文越长,模型对早期信息的注意力越弱,执行结果越容易飘。
状态隔离模式要求:每个Skill在执行时,只能访问自己需要的局部状态,不要把所有信息都倒进公共上下文里。打个比方,你去银行办事,柜员只需要知道你办这项业务要用的资料,而不是把你从出生到现在的全部档案都摊在桌上。
比如一个多轮对话场景:用户先查询了“某商品的详细信息”,接着又问“它有没有大号的”。第二个问题只依赖第一个问题里锁定的商品ID,商品的完整详情没必要再次塞进模型上下文。正确的做法是:在第一个技能执行完成后,提取并把商品ID存入会话状态(可以是一个变量,也可以是外置键值存储),第二个技能从状态里读ID,再针对尺寸字段做专项查询。
6.2 上下文清洗与外置记忆
具体实现上,我习惯把会话状态和阅读型上下文分开。阅读型上下文是指模型生成回答时需要参考的对话历史;会话状态是用户在多轮交互中锁定的关键信息(订单号、商品ID、收货地址等)。前者可以精简,后者必须保值。
每当技能执行完,我会从返回结果中提取关键实体,写入会话状态对象。下一轮用户提问时,先做一次“状态注入”:
当前会话中已知信息:{状态对象} 用户最新提问:{用户输入} 请结合已知信息回答。如果已知信息不足以回答问题,明确告知用户需要补充哪些信息。这相当于给Agent配了一个便签本。它的优势是:即使对话历史很长,关键信息也不会丢失;同时由于上下文被清洗过,模型每轮需要考虑的“杂音”少了很多,输出质量更稳定。
6.3 状态冲突与并发处理
状态隔离模式有个需要注意的边界问题:多个技能同时对同一个会话状态字段写入,后者覆盖前者,但前者可能才是用户真正想要的。比如用户先选了默认收货地址,后来又问“北京那个地址能送到吗”,按逻辑应该临时切换到北京地址,但如果技能写入逻辑不分优先级,默认地址可能被误改。
我的解决方案是给状态字段增加来源标记:user_explicit(用户明确指定)、system_inferred(系统根据上下文推断)、default(默认值)。在写入时,user_explicit优先级最高,不会被子级覆盖;system_inferred若与已有user_explicit冲突,则丢弃并提示用户确认。这个优先级规则简单可靠,实测里避免了绝大多数状态错乱问题。
还要提醒并发场景。如果你的Agent是异步多轮处理的,同一会话可能有多个技能同时在跑,状态写后必读的竞态条件就可能出现。解决方案很老套但有效:给每个技能调用加一个递增的sequence number,状态写入时带上序号,读取时只接受比当前序号更早的写入结果,确保逻辑顺序清晰。
7. 常见问题与排查技巧实录
模式讲完了,实际操作中光知道模式还不够,你得会诊断问题。下面这节是把项目里踩过的坑和排查方法做个汇总。
7.1 典型问题速查表
| 症状 | 可能原因 | 涉及模式 | 排查路径 |
|---|---|---|---|
| 模型答非所问 | 技能粒度太粗,或路由判断错误 | 模式一、模式四 | 查路由日志,确认域名是否选对;查技能列表,看是否有更贴近的选项 |
| 输出格式频繁解析失败 | 没有输出Schema约束,或模型被允许自由发挥 | 模式二 | 检查输出是否符合Schema;给下游加校验与自动重试 |
| 一次错、次次错,重试无效 | 缺少评审环节,模型在同一个思维路径上打转 | 模式三 | 加入评审角色,从外部视角纠正思路 |
| 多轮对话丢失关键信息 | 状态直接堆在上下文,没有隔离 | 模式五 | 检查会话状态对象是否在每轮后更新;确认关键实体是否已提取 |
| 技能列表太长,选择困难 | 选项过多,语义边界重叠 | 模式四 | 引入路由器,先粗分再细分 |
这个表本身也可以作为你项目里的排查SOP,遇到异常先跑一遍,绝大多数问题都能定位到具体环节。
7.2 调试Skill的三个实用技巧
第一,日志要记录“模型实际看到的技能定义”。很多问题不是模型不行,而是模型看到的技能描述跟你想象中的不一样。所以每次带技能的调用请求,把技能清单原文落到日志里,出问题先看这一层。
第二,每次只改一个变量。想验证“技能描述改了之后选择准确率是否提升”,就只改描述,别同时动参数结构和回调逻辑。否则出了问题你都不知道是哪一项改动引发的。
第三,为每个技能准备三条黄金测试用例。一条正中靶心、一条擦边球、一条完全无关。每次改动后先跑这三条,能快速回归出这个技能的行为是否符合预期。很多Agent开发团队犯的错误就是只测“正中靶心”那一条,擦边球场景下跑偏了要到了生产环境才暴露。
7.3 从模式到框架:组织你的Skill清单
最后给个落地建议:把上面的五种模式当成检查清单,每新增一个技能,都过一遍。
我目前的习惯是在项目管理文档里维护一张技能登记表,字段包括:技能ID、所属域、原子化检查结果(一个技能是否只做一件事)、输出Schema是否定义、是否受路由保护、是否有状态依赖、是否有黄金测试用例。新增或修改技能时,逐一核对这些项。
这样做的好处是,Agent开发从“靠感觉调Prompt”变成了“按清单填表”。新同学接手项目,不需要靠口口相传了解坑在哪里,看一遍这张表,能快速知道现状哪项没做、做了哪项、为什么做。稳定性和可维护性都靠这套流程支撑起来。
另外想说一点:这五种模式不是互相排斥的。一个成熟的Agent系统通常是同时用它们的——按域拆路由,路由底下挂原子技能,技能输出走结构化校验,校验失败进自我纠错,执行过程全程状态隔离。模式是骨架,怎么组合、怎么取舍,还得看你实际业务的具体约束。