
我最早写 Agent Skill 的时候其实完全把它当成一个高级 Prompt 来用——写一大段详细的“你是一个擅长某某的助手请按照以下步骤执行”然后塞给模型。结果上线之后状况百出任务做到一半突然跳步骤明明只让它查天气它顺手把明天的日历也建了换一个 Agent 框架整套描述又要重新调。后来我才意识到Skill 不是“写出来的”是“设计出来的”。这个标题里提到的“稳定、可复用、不跑偏”本质上对应 Agent 工程化落地时最头疼的三个问题模型乱调用工具、技能换个场景就不能用、多步任务执行到一半偏题。我这一年多在几个项目里反复踩坑、重构最后沉淀出 5 种比较通用的 Skill 设计模式。这篇文章我会把这 5 种模式逐个拆开讲配合代码示例和踩坑记录适合正在做 Agent 应用、尤其是工具型 Agent 的开发者参考。先说明一下为了不让文章绑定某个特定框架下面的示例我用的是通用 Python 伪代码核心思想在任何支持函数调用的 Agent 平台上都适用。1. 先搞明白 Skill 为什么会“跑偏”三个常见根源在聊设计模式之前得先把问题定义清楚。很多人一看到 Agent 输出不对就怪模型不行、换更大的模型但实际上大部分“跑偏”和模型聪明程度没关系是 Skill 的结构设计有漏洞。第一个根源是技能边界模糊。一个 Skill 如果描述里同时写了“查询订单、修改订单、取消订单”模型大概率会在该查询的时候顺手把取消的逻辑也带出来。模型不是人它不会主动判断“这里我只能做查询”它倾向于把所有看起来相关的能力都用上。你给它越大的职责范围它跑偏的空间就越大。第二个根源是没有显式约束。自然语言描述天生是软约束模型可以理解也可以“发挥”。比如你写“输出格式要规范”但什么叫规范模型有自己的理解。真正可靠的约束是结构化的比如 JSON Schema 规定死字段名和类型让校验逻辑在模型之外强制执行。凡是放在自由文本里的要求都等于没要求。第三个根源是缺失中间校验环节。很多 Agent 的设计是“模型输出什么系统就执行什么”。模型输出一段调用参数系统直接照做参数错了好坏全凭模型自觉。但凡在执行前后加一道校验把非法参数、不合理请求拦截下来跑偏的概率立刻降一个量级。这三个根源决定了 5 种设计模式各自要解决的问题边界模糊用原子化解决软约束用结构化 Schema 解决缺校验用护栏模式解决。而可复用性、多步任务失忆、高风险操作失控则分别对应路由分发、状态跟踪、人机确认这三种模式。下面逐个展开。2. 模式一原子工具封装——一个 Skill 只干一件事这个模式是全部设计模式的地基。所谓原子工具封装就是把一个 Skill 的职责收敛到“单一动作”并且用严格的输入输出 Schema 把它包起来像一个函数一样对外提供服务。2.1 为什么“大而全”的 Skill 注定不稳定我最开始写过一个“全能助理 Skill”里面包含查天气、设提醒、发邮件、查日历四个功能。测试的时候单问每一项都挺好但只要问题稍微复杂一点比如“明天下午三点提醒我开会顺便帮我看看那天天气怎么样”它就经常把提醒和天气的参数搞串甚至有次真的发了一封邮件出去。事后我复盘问题很明确模型需要在一次调用里同时理解四个子功能的触发条件、参数格式、输出规范理解负担太重混淆是必然的。原子化之后每个 Skill 只有三样东西一个明确的触发描述、一个严格的输入 Schema、一个无状态的执行函数。模型每次只需要判断“当前该不该调用我、参数是什么”决策空间小准确率自然高。2.2 输入输出 Schema 的写法要点先说输入 Schema。必须用 JSON Schema不要用自然语言描述参数格式。JSON Schema 的价值在于它是机器可校验的模型输出之后系统可以直接用校验器检查不符合就直接拒绝调用。一个典型的原子 Skill 结构长这样{ skill: query_weather, description: 查询指定城市在指定日期的天气情况。只能用于查询天气不能执行其他操作。, parameters: { type: object, properties: { city: {type: string, description: 城市名称如北京、上海}, date: {type: string, format: date, description: 日期格式为YYYY-MM-DD} }, required: [city, date], additionalProperties: false } }这里有两个细节特别重要都是我踩过坑才注意到的。第一additionalProperties: false一定要写。不写的话模型经常自作主张往参数里塞额外字段比如它会在查天气的时候加一个unit: celsius之类的自定义字段你的执行函数如果没处理就会报错。加上这个约束所有不在 Schema 里的字段都会被校验器拦下来从源头杜绝了模型的“自由发挥”。第二description 的写法有讲究。很多教程让你把描述写得很详细恨不得把所有用法都写进去。我的实测经验是描述越短、越聚焦模型调用越准。一个 Skill 的描述只应该回答两个问题它做什么什么时候不该调用它比如上面那个描述里的“只能用于查询天气不能执行其他操作”就是显式画了一条边界模型很吃这一套。2.3 执行函数怎么设计才能“可复用”执行函数要做到无状态。所谓无状态就是函数的执行结果只取决于当前传入的参数不依赖任何外部会话变量、临时文件或者内存里的中间值。为什么要这样因为一旦有状态这个 Skill 在不同 Agent、不同会话里复用时你就得去维护那套状态复用成本极高。我一般把执行函数设计成“入参即全部、出参即结果”的形态内部只做三件事调用底层 API 或数据源、清洗结果、返回结构化 JSON。底层数据源怎么连、用什么鉴权全部封装在函数内部外部不感知。这里还要提一个容易被忽视的点返回值也要有 Schema。输出 Schema 的作用是约束后续流程让下一个 Skill 或模型知道能拿什么字段。很多时候 Agent 跑偏不是因为调错工具而是因为工具的返回值太随意模型拿到乱七八糟的文本只能瞎解读。2.4 原子化的边界把握原子化也不是越细越好。如果每个 Skill 只做“把两个数相加”那 Agent 要完成一个任务得调用十几次反而增加了出错概率和 token 消耗。我自己的判断标准是一个 Skill 的执行过程是否能在“一个自然步骤”内描述完。查天气是一个步骤分析天气并生成穿衣建议是另一个步骤分开而“打开网页、定位输入框、输入关键词、点击搜索按钮”这种自动化流程本质上就是“执行一次网页搜索”这一个步骤不用拆。原子工具封装模式解决的问题是单个技能的可复用性。但一个 Agent 往往有十几个 Skill怎么让它们各司其职不打架这就轮到第二种模式上场了。3. 模式二意图路由分发——Skill 多了以后怎么不打架当 Agent 的 Skill 数量超过 5 个模型的选择正确率会肉眼可见地下降。尤其当两个 Skill 的触发条件有重叠时比如“查物流”和“查订单”模型经常选错。这个时候就需要一个“路由 Skill”把决策层和执行层拆开。3.1 路由层存在的意义路由层其实就是一个特殊的 Skill它的职责只有一个分析用户的输入决定把任务分发给哪一个子 Skill。它本身不执行任何具体功能只做决策。这个设计解决了两个问题。第一模型不需要在十几个 Skill 里逐一比较谁的描述更匹配它只需要在四五条路由规则里做选择题准确率大幅提升。第二新增 Skill 时不需要改动其他 Skill 的定义只需要在路由表里加一条映射可维护性很好。3.2 路由表的设计与兜底路由表的设计我建议用“意图分类 分发表”的结构。先让模型把用户输入归到某一个意图类别再根据意图类别找到对应的 Skill。ROUTING_TABLE { weather_query: skill_query_weather, alert_create: skill_create_reminder, mail_send: skill_send_mail, calendar_query: skill_query_calendar } def route_intent(user_input): intent classify_intent(user_input) # 由模型判断意图类别 if intent in ROUTING_TABLE: return ROUTING_TABLE[intent] else: return skill_fallback_unknown # 兜底那段“兜底”代码是整个路由模式里最重要的部分。没有兜底模型面对一个它不认识的问题时要么随机选一个 Skill跑偏要么直接抛错体验差。我通常会设计一个skill_fallback_unknown它的作用是回复用户“抱歉这个需求我暂时无法处理你可以尝试……”而不是硬凹。3.3 路由决策的准确率怎么提升路由层的准确率直接决定了整个 Agent 的上限。我试过几种做法按效果排序第一种做法是“给每个意图类别配 3 到 5 个典型示例”。比如alert_create这个意图示例写“提醒我下午开会”“明早 8 点叫我起床”“设个闹钟明天六点”。模型的 few-shot 学习能力很强几个例子比一大段规则描述管用得多。第二种做法是“让模型先说出意图再调路由”。也就是强制模型在调用路由 Skill 之前先输出一段“意图分析”文本。这样做的目的是让模型多思考一步不要直接跳到结果。实测加了这一步之后路由准确率能提升 5 到 8 个百分点代价是每次多消耗几十个 token性价比很高。第三种做法是“子 Skill 之间不设置重叠触发词”。路由规则和子 Skill 的触发词如果重叠模型会混乱。我维护了一张触发词冲突自查表每次新增 Skill 先检查有没有跟现有意图重叠。3.4 路由模式的复用价值路由分发模式的复用点在于决策逻辑。你有一个场景是“查天气、设提醒、发邮件”换一个场景是“查库存、下订单、生成报表”两套子 Skill 完全不同但路由表的骨架、兜底策略、意图分类方法可以原样搬过去。我后来把路由层做成了一个可配置的模块意图分类逻辑、分发表全部外部注入换项目的时候只需要改配置不用动代码。但路由只能解决“选对 Skill”的问题。选对了不等于执行得好。执行过程中模型随时可能产生幻觉输出非法参数、漏掉必填字段甚至把两步操作合并成一步。这就要靠护栏模式来兜底了。4. 模式三护栏校验——执行前拦截执行后复检护栏模式的核心思路是永远不要相信模型的输出永远在校验通过之后再执行。这个模式理念上很像后端开发里的参数校验中间件只不过校验的对象变成了模型生成的结构化参数。4.1 三层的护栏设计我在实际项目里把护栏拆成了三层每一层拦截不同类型的问题。第一层是输入护栏也就是在 Skill 执行之前校验模型生成的参数是否符合 JSON Schema。这一层最容易实现用现成的 jsonschema 库就能做。它在两个地方重复使用一是模型先给出“我打算调用哪些 Skill、参数是什么”系统校验通过之后再真正执行二是每个 Skill 的内部入口再校验一次防止路由层漏进来的脏数据。第二层是业务护栏校验参数之间的业务逻辑关系。比如一个“转账” Skill输入参数是转出账户和金额如果模型生成的金额是负数或者超过账户余额这种问题 JSON Schema 是拦不住的需要在业务层写规则。常见规则包括金额范围、日期不能早于今天、IP 格式合法等等。第三层是输出护栏对 Skill 的返回结果做类型检查和格式检查。这一层的目的是防止底层 API 返回异常数据污染后续流程。我记得有次第三方接口返回了空字符串模型拿这个空字符串去生成了摘要结果摘要完全是编的。有了输出护栏空结果直接触发重试或降级流程不会传给模型。4.2 拦截住之后系统怎么做比拦截本身更重要护栏校验必然会拦截掉一部分请求。被拦下来之后是直接抛错让用户重来还是自动重试还是把请求降级处理这个决策流程往往决定了整个 Agent 的可用性。我常用的做法是三级响应如果校验失败的原因是参数缺字段系统自动补充默认值后重试如果参数类型对不上把错误信息和正确的 Schema 一起回传给模型让它自己修正最多重试一次如果两次都失败就走降级路径返回一段预设的兜底文案而不是让用户面对一个报错。这里有一个小技巧回传给模型修错时把错误信息写得非常具体。不要说“parameters invalid”要说“字段 amount 必须是数字你提供了字符串 abc请重新生成合法的调用参数”。模型看到具体错误修正成功率很高我实测大概有七成左右。4.3 护栏模式的常见误区和平衡一个容易犯的错是把护栏做成“事后批评师”。也就是让模型自己检查自己的输出发现不对就改。这种方式我试过效果很差模型对自己的错误往往视而不见会一本正经地说“我的输出没问题”。护栏必须放在模型之外由确定性代码来执行不能依赖模型自省。另一个平衡点是护栏的严格度不能一刀切。比如一个“生成文案”的 Skill输出是开放性的没法用 Schema 严格约束那就只做基础的长度、空值检查。而“发起支付”“发送邮件”这类操作型 Skill护栏必须拉满。我建议给每个 Skill 配一个risk_level属性低风险技能走宽松校验高风险技能走全量校验。有了护栏单次调用是稳了。但 Agent 的很多任务是多步骤的比如“对比三款手机的配置推荐一款再帮我设置一个明天抢购的提醒”。这种任务涉及多次调用、多个 Skill 协作如果每一步之间不传递上下文模型很快会忘记前面做了什么开始胡说。这就是状态记忆模式要解决的问题。5. 模式四状态记忆跟踪——多步任务最怕“失忆”“失忆”是 Agent 多步任务跑偏的头号原因。模型每轮对话都有上下文窗口但它的记忆力天然是脆弱的——几千字的对话历史里关键信息可能早就被淹没了。而且很多 Agent 的执行过程是异步的一步执行完下一步触发时可能已经过了好几分钟上下文都换了。5.1 把任务状态从“模型脑子里”搬进“系统存储里”解决失忆的基本思路是凡是重要信息一律不放在模型的上下文里放到系统端的结构化状态存储里。模型需要的时候去查不需要的时候不占记忆。我通常会为每个多步任务创建一个任务对象包含四个字段任务 ID、步骤列表、已完成步骤记录、关键数据字典。步骤列表在任务启动时就由路由层规划好已完成步骤记录用来跟踪进度关键数据字典则存放各步骤产生的中间结果比如查询到的订单号、计算出的推荐结果等。task_state { task_id: task_2025_001, steps: [compare_phone, analyze_result, create_reminder], completed: [compare_phone], data: { phone_models: [A, B, C], recommended: B, reminder_time: 2025-07-01 10:00 } }这样设计之后模型每一步要做的事情变得极其简单读取steps里未完成的第一项执行对应 Skill把结果写回data在completed里标记完成。它不需要记住上一轮说了什么因为一切都在任务对象里。5.2 状态读写的两个关键细节第一个细节是“写入时覆盖还是追加”。很多中间结果是有版本迭代的比如用户第一版说“预算 5000”后来又说“预算提到 7000”。如果写回时直接覆盖历史版本丢失如果全部追加数据会越来越臃肿。我的做法是每个data字段都保存{value, updated_at}结构默认读取最新值需要时能追溯历史。这样既不影响模型取值的简洁性又保留了变更记录。第二个细节是“状态存储的持久化等级”。如果 Agent 是单机同步执行内存 Map 就够了。但如果任务是异步的、跨服务节点的或者需要支持断点恢复就必须用外部存储比如 Redis 或者数据库。我建议从一开始就按可替换接口设计状态管理器不要贪方便直接把状态写死在进程内存里。亲历过一个教训本地测试一直没问题部署到集群后任务一多状态互相覆盖排查了半天才发现是内存状态没有做到按任务 ID 隔离。5.3 状态与 Skill 的交互模式状态记忆模式和前面的原子 Skill 结合起来有个经典套路原子 Skill 只负责“算”不负责“记”状态管理器负责“记”。Skill 算完结果返回给编排层编排层决定要不要把结果写进状态。Skill 本身不感知状态的存在这样 Skill 依然是无状态的可复用性不受影响。我经常看到有人把状态写进 Skill 内部的全局变量里这是非常危险的做法。两个任务并行跑的时候全局变量一混A 任务的订单号就跑到 B 任务里去了。记住一条原则状态只存在于编排层不进 Skill。前面四种模式已经能覆盖大部分场景了原子化保证单点正确路由保证选对方向护栏拦截非法操作状态维持多步连续性。但我发现有些场景仍然不能让人完全放心——比如“自动发通知给所有客户”“批量修改配置”“执行退款”这类操作的共同点是一旦做错代价极大且很难挽回。对待这类操作正确的姿势不是靠模型更聪明而是主动把一部分控制权交还给人类。6. 模式五人机确认——高风险操作前的最后一道闸门人机确认模式说起来很简单在 Agent 执行高风险操作之前先暂停把“即将执行的动作和影响范围”告知用户得到确认后再真正执行。这个模式的实现难度低但很多人会忽略它因为觉得“加一步确认会降低自动化程度”。我的观点是自动化程度再高也抵消不了一次严重事故带来的损失。6.1 什么情况下必须加确认点我总结了一套判断标准满足任何一条就应该加确认点操作不可逆删除、覆盖、发送后不可撤回操作有财务影响支付、退款、扣费操作涉及外部系统副作用通知一批用户、修改线上配置操作金额或数量超过某个阈值。这些条件可以配置化每个 Skill 的risk_level直接决定确认策略低风险无需确认高风险强制确认。有次我负责的一个 Agent 接入了群发通知能力。测试阶段有一轮对话里用户说“顺便让大家把周报都提交了”模型居然真的调用了群发接口给全部门发了一封邮件。虽然内容是合理的但时机完全是错的——它没有任何确认步骤。那次之后我立刻把群发操作加上了确认点把模型直接调用的路径改成了“先生成草稿、待用户点击发送”的两段式。6.2 确认点的实现方式两段式调用实现上我会把操作拆成两段预执行和最终执行。预执行阶段模型生成操作参数但系统不真正执行而是生成一条“确认申请”包含操作类型、关键参数、影响范围回复给用户。用户通过明确指令“确认”“可以”“执行吧”或者点击按钮系统收到确认信号后拿着之前预存的参数去执行第二个阶段。def prepare_hazard_operation(operation, params): # 阶段一只生成确认单不执行 confirmation { confirm_id: uuid4(), operation: operation, preview: build_preview(operation, params), status: pending } save_confirmation(confirmation) return confirmation def confirm_hazard_operation(confirm_id): confirmation load_confirmation(confirm_id) if confirmation.status confirmed: raise AlreadyConfirmed() confirmation.status confirmed # 阶段二仅在这里执行真正的操作 result execute_operation(confirmation.operation, confirmation.params) return result注意预执行阶段生成的参数必须存起来最终执行时直接使用不要重新让模型生成一遍参数。原因是模型两次生成的结果可能不一样万一第二次生成了一个新的、仍然合理的参数但已经不是用户确认过的那个了就等于让用户为 A 操作做了确认实际执行的是 B 操作。所有确认流程都必须绑定一个确定的、不可变的参数快照。6.3 确认点的体验优化人机确认模式最大的敌人是“打扰感”。如果每一步都要求确认用户会烦然后干脆关掉确认功能风险反而更大。我的优化实践经验包括批量操作合并成一个确认点比如“给 20 个客户发通知”不要 20 次确认一次确认展示完整清单提供“本次会话默认确认同类操作”的选项让用户主动选择降低确认频率确认单里用一眼能看懂的方式展示影响范围比如数量、金额、接收方名单而不是罗列原始参数。我还遇到过一种特殊情况模型生成的操作和用户的意图完全一致但用户的确认方式比较隐晦比如用户说的是“嗯”“行”“来”系统容易漏判。建议确认信号的匹配规则宁可保守一点识别不明确时按“未确认”处理多问一句“我没有听清请明确回复‘确认’或‘取消’”也不要冒险去执行。到这里5 种模式单独都讲完了。但实际项目里很少只用一种更多时候是几种模式嵌套搭配这就有个选型和组合的问题。接下来给一张决策表和一个组合案例方便照搬到自己项目里。7. 五个模式怎么选决策表加一个组合实战案例7.1 选型参考先判断你的 Agent 属于哪种类型每种模式解决的问题不一样选型之前先想清楚当前最大的痛点是下面的哪一类。我做了一张简表说明各模式对应的痛点和适用条件。设计模式解决的问题落地前置条件典型案例原子工具封装单个技能边界模糊、不稳定技能有明确的单一动作边界查天气、查订单、算运费意图路由分发多技能之间选择出错、扩展困难技能数量超过 5 个触发条件有关联个人助理、客服机器人护栏校验模型幻觉导致非法参数、错误执行技能是操作型有强参数结构发邮件、改配置、下单状态记忆跟踪多步任务失忆、跨步骤结果丢失任务天然是多步骤的、有中间状态对比购物、行程规划人机确认高风险操作失控造成不可逆损失操作不可逆或高影响群发消息、退款、删除如果你拿不准该优先上哪个我给出的参考顺序是先做原子工具封装把每个 Skill 的参数和边界收窄再做护栏校验至少保证模型输出合法技能多了加路由任务复杂了加状态有高风险操作加确认。这个顺序基本符合一个 Agent 从 Demo 到上生产的演进路径。7.2 组合实例一个“智能采购助手”的设计拆解为了让你更直观地看到五种模式怎么配合我拆解一个我实际做过模拟项目 X 的采购助手。这个助手负责处理采购申请员工提交需求它判断是否合规查库存生成订单通知审批人。我把它拆分成了几个原子 Skillcheck_requirement校验需求的预算和合规、query_stock查库存、create_order生成采购订单、notify_approver通知审批人。每个 Skill 都有独立的 JSON Schema 和输出结构谁都不会越界去干别的活。任务进来之后路由分发 Skill 先判断这是“采购申请”“库存查询”还是“订单状态咨询”走不同分支。所有 Skill 外层都挂了护栏校验预算金额不能为负、日期不能乱填、库存查询的 SKU 必须是合法格式。整个采购流程是典型的多步任务每一步的状态需求单号、预算余额、库存数量、生成的订单号都写进任务状态对象里模型每一步只读状态里最新值从不用靠记忆猜。最后create_order和notify_approver都标成高风险操作。生成订单时模型先输出订单草稿系统展示给员工确认“采购 100 个 A 配件单价 50总金额 5000预算 8000”确认后下单通知审批人则是先展示“将要通知审批人张三内容包括订单号、金额、申请理由”确认后才发。上线跑了一个多月跑偏率从之前的 20% 左右降到不到 3%而且新增业务线时只需要新增原子 Skill 和路由表条目主流程结构完全不用动。7.3 模式之间如何迁移和演进最后想说一下模式之间的演进关系。不需要一上来就全上五件套那是过度设计。我会根据项目阶段逐步加码MVP 阶段有原子工具封装就够了功能增多后上路由出现误操作后补护栏任务步骤变长后加状态涉及钱、通知、删除等高影响操作后加确认。每一种模式都是在你真实遇到痛点的那个节点才引入不要提前为想象出来的问题买单。这套模式的另一个好处是每个模式独立实现、依赖低。换一个 Agent 框架、换一个模型厂商原子 Skill、路由表、护栏校验器、状态管理器、确认机制这些代码几乎可以原封不动平移。我实测过把用整套模式写好的技能从一个框架迁到另一个框架迁移成本主要花在框架 API 适配上而不是业务逻辑重写这大概就是“可复用”这三个字最实在的体现。如果你正在做的 Agent 恰好也遇到了“调用不准、换个场景就废、任务做着做着就跑偏”的困扰我建议先从最轻的原子工具封装开始改把其中一个经常出错的 Skill 拆开加严格的 JSON Schema跑一周看看效果。你会发现很多“模型不够聪明”的问题其实是设计不到位的问题。