最初让我决定正经搞一套 Agent 技能系统是因为线上客服机器人翻车了。当时大模型已经能回答得头头是道但只要牵扯到查订单、改地址这类实际操作它就开始自由发挥——明明没有调用任何接口的权限却一本正经地告诉我“已为您提交退款申请”。后来我给了它十几个工具定义问题更大工具一多模型要么在几个相近的工具之间反复横跳要么把参数填得离谱。那段经历让我意识到让 Agent 真正“会干活”需要的不是多塞几个函数定义而是一套完整的、可管理、可排查的技能系统。这也就是agent-skills这类设计要解决的核心问题。这篇文章我会从技能系统的概念讲起重点拆解技能 Schema 怎么写、技能怎么注册和检索、多个技能怎么编排协同最后把我踩过的坑和排查思路一并整理出来。无论你是在做大模型应用、智能客服、自动化助手还是在折腾个人 Agent 框架这套思路应该都用得上。1. 从“模型会聊天”到“Agent 会干活”技能系统解决的根本矛盾很多刚接触 Agent 的人会有一个误解给大模型接几个 API让它可以调用外部工具就算在做 Agent 了。我一开始也这么想但实际跑起来就发现问题远没有这么简单。1.1 为什么只靠 Function Calling 不够Function Calling 确实是一个里程碑式的能力它让模型不局限于文字输出而是可以输出结构化的调用意图。但如果你真在一个生产环境里跑过就会碰到这样几个扎心的问题模型不知道“什么时候该用什么工具”。你定义了查询天气和查询日历两个工具模型面对“帮我看看明天适合约几点开会”这种问题可能去调了天气接口也可能两个都想调结果一个都没调对。上下文窗口有限。Agent 能看到的工具定义是有预算的工具一多定义本身就占掉大量 token甚至影响对话质量。工具行为不可控。某个工具执行失败了模型会怎么处理它是如实告诉用户还是会基于自己的想象补一段“成功”的回复工具没有生命周期。上线的新工具、下线的旧工具、参数格式变了怎么让模型跟着更新手动改 prompt 不是不行但改完你就等着模型在别的地方出幺蛾子吧。1.2 技能系统的本质把“做什么”和“怎么做”分开agent-skills这类技能系统的核心思路是把每一个可执行的能力封装成一个独立的“技能”技能对外只暴露一个让模型理解的高层接口对内则隐藏具体的实现细节。模型不直接面对一堆零散的函数而是面对一组经过设计、描述清晰、有明确适用边界的技能。换个更直白的说法没有技能系统的 Agent像是一个空有蛮力、但不知道自己在哪、有什么工具可用的新手有技能系统的 Agent则像是手里拿着一份带注释的工具箱清单每件工具上还贴了“使用场景”和“注意事项”。对模型来说技能系统解决的是“决策”和“执行”的分离。模型负责根据用户的意图选择正确的技能、填入合理的参数而技能内部运行的逻辑、调用的服务、返回的数据格式完全不需要模型的参与。这样做的好处是模型的认知负担小了很多不需要理解每个业务系统的内部协议。技能的实现可以独立演进只要外部契约不变内部随便重构。排查问题时有清晰的边界模型选错技能是模型的问题技能执行失败是技能的问题不用混在一起查。1.3 一个可复用的技能系统包含哪几层我后来在实际项目中总结的技能系统架构大致分四层技能描述层给模型看的“说明书”包含技能名称、触发条件、参数说明、注意事项。技能注册层所有技能的统一登记处管理技能的版本、状态、可见性。技能执行层真正干活的代码接收结构化参数执行并返回结构化结果。技能编排层在多个技能之间定义调用顺序、分支条件、数据流向。这四层贯穿了下面要讲的每一个环节后面的内容基本都围绕这四层展开。2. 技能 Schema 设计模型能不能用对全看这一步我自己调试 Agent 时有个体会大部分“调用错工具”或者“参数填错”的问题根子都不在模型而在技能的 Schema 写得不够清楚。模型就像一个理解能力很强但缺乏常识的新实习生你给的说明书含糊他就只能靠猜。2.1 名称动词开头杜绝歧义技能名称是模型第一眼看到的信息命名规则非常重要。我的经验是用“动词 名词”的结构并且要能直接反映这个技能的职责边界。比如search_flights比flights清晰。submit_refund_request比refund清晰。get_order_status_by_id比order清晰。有个容易忽略的坑两个技能名称不能过于相似。我有一次在一个 Agent 里同时定义了query_weather和query_temperature模型经常在这两个之间切换出错。后来我把后者的名称改成了query_temperature_detail并在描述里明确“仅当用户需要具体温度数值时使用”问题才消停。2.2 描述写清触发条件、边界和负面规则如果说名称是技能的门牌号那描述就是技能的完整使用手册。描述里应该覆盖这几类信息这个技能是干什么的。在什么场景下应该被调用。什么时候不应该被调用。有没有什么特殊限制或前置条件。我看过一个很好的例子是某个支付系统里退款技能的描述大致是根据订单号为用户发起退款申请。仅当用户明确表达退款诉求且订单状态为已支付并可退款时调用。若订单状态为已发货必须先调用 check_delivery_status 确认物流流转情况后再判断。若用户仅咨询退款政策不得调用本技能直接根据知识库回答。这个描述把“触发条件”“前置依赖”“明确禁止”三件事说全了模型根本不用猜。对比很多项目里那种“用于退款操作”的敷衍描述效果天差地别。2.3 参数严格定义 JSON Schema参数是模型最容易翻车的地方。我的建议非常直接能用枚举就用枚举能标必填就标必填类型能具体就具体。下面是一个比较规范的技能定义示例以创建工单为例{ skill_name: create_support_ticket, description: 为用户创建一条新的支持工单。当用户描述遇到问题并要求解决或上报时调用。调用时若用户未提供优先级默认填 medium。创建成功后返回工单编号。, parameters: { type: object, properties: { title: { type: string, description: 工单标题必须一句话概括用户的问题通常不超过50个字符。 }, category: { type: string, enum: [billing, technical, account, other], description: 工单分类按用户问题归属选择。 }, priority: { type: string, enum: [low, medium, high, urgent], default: medium, description: 处理优先级默认 medium。仅当用户明确表示情况紧急时填 high 或 urgent。 }, contact_email: { type: string, format: email, description: 用户联系邮箱若用户未提供且历史会话中无法找到则留空并在返回结果中标记缺失。 } }, required: [title, category] } }这个例子里有几个值得说的设计点priority设了枚举还有默认值并且描述里约束了“什么时候才能填 high”这就能防止模型一看到用户说“比较急”就把所有工单都标成 urgent。contact_email虽然没设必填但是明确告知了模型拿不到时的兜底策略。这不是让模型自己编一个邮箱而是让它在执行层去处理缺失值。2.4 关于模型“理解”技能的几点心得描述里的否定词要谨慎。模型对“不要调用”的理解有时候会跑偏我试过的经验是与其说“不要在xxx时调用”不如说“仅在xxx时调用”正面引导的效果更稳定。参数描述不要复述参数名。date参数的描述写“日期”毫无意义要写“格式为 YYYY-MM-DD代表用户期望查询的日期”模型才不至于输入错误格式。技能数量多的时候给每个技能加一个“分组/域”的信息放在描述开头比如“订单域”或“支付域”能显著提升模型的区分度。3. 技能注册与动态发现让 Agent 知道自己在哪个工具箱里技能定义好之后下一步就是让 Agent 在合适的时机、以合适的方式知道这些技能。这里最大的矛盾在于上下文窗口是有限的技能库却可以很大。把几百个技能定义一股脑塞进 prompt既不现实也没必要。3.1 集中式注册表技能的“通讯录”我习惯用一张技能注册表来管理所有技能无论是放在内存里、配置文件里还是数据库里本质上它记录了每个技能的完整元数据技能名称、唯一 ID、版本号。技能描述和参数 Schema。技能当前状态可用、灰度中、已废弃。技能所属域、使用权限、调用统计。注册表最大的价值是让 Agent 运行时有能力查询“当前可用技能”。比如我在做智能助手时启动时把注册表里的技能按状态过滤一遍只把“可用”和“灰度中”的技能暴露给模型废弃的技能直接从候选列表里移除这就避免了模型冷不丁调一个已经下线的接口。3.2 动态发现不能把全部技能都塞进上下文当技能数量在二三十个以内时全量放入 prompt 还能勉强接受。超过这个数token 浪费和模型选择困难会同时爆发。这时候必须让 Agent 具备动态发现技能的能力。我比较常用的方案是“两段式”检索第一段从用户当前对话中抽取意图关键词在技能注册表里做一个粗筛。粗筛可以基于简单的关键词匹配也可以用 embedding 做语义相似度检索。第二段把候选技能控制在 58 个的完整定义交给模型让模型在这几个技能里做选择。这一步会大幅减少模型的选择困难。举一个实际场景用户说“我要把这两件商品一起结算”。粗筛阶段注册表里有几十个技能但语义相似度最高的可能是create_checkout_session、search_products、apply_coupon这几个。模型只需要在这几个里面做选择命中率当然要高得多。3.3 技能路由别把无关技能暴露给模型除了检索还要做一次权限和上下文的过滤。我管这叫技能路由规则不复杂但极其有用按对话上下文过滤用户正在咨询未发货订单就不把“申请退货”之外的技能暴露出来。按用户身份过滤普通用户看不到管理员技能普通商户看不到平台运营技能。按会话状态过滤一个客服会话如果还在身份核验阶段就不开放改地址、退款类技能。有些团队会把这套逻辑做成一个简单的“预路由模型”或者更轻量一点直接在代码里写规则。我的经验是先上规则规则覆盖不了的情况再考虑用模型判断。一来规则可解释、可排查二来复杂度可控。3.4 技能热更新与缓存失效技能定义不是一成不变的。在实际运营里我经常因为参数调整、描述优化而更新技能这时候有个很现实的麻烦——线上 Agent 可能缓存了旧版本的定义。我的做法是给技能注册表加一个“版本号 更新时间”并让运行时缓存设置一个 TTL比如 5 分钟。每次从缓存读取技能定义前都校验版本号如果发现注册表里的版本更新了就重新拉取并刷新缓存。这样新技能上线或者旧技能修订最多延迟几分钟就能全量生效不用重启服务。4. 技能编排与组合从孤立调用到多技能协作生产单个技能解决的问题有限。真实业务里用户一句话往往对应一条完整链路。比如“帮我取消订单并退款然后把优惠券还给我”这里就涉及查询订单、取消订单、发起退款、恢复优惠券四个动作。如果让模型一次性决定并调用四个技能风险不小因为中间任何一步失败都会影响后续。4.1 编排的几种形态我见过并且实际用过的编排形态大致有三类代码编排开发者用代码把调用顺序写死。比如cancel_order之后必须调用refund_order这就是一个硬编码的流程模型不参与决策。特点是稳定、可控但灵活度低。工作流编排用 DSL 或者可视化配置定义流程支持条件节点、并行节点、循环节点。模型可以决定进入哪个分支但分支内部的路径是预设好的。Agent 自主编排把几个技能暴露给模型让它在运行时动态决定调用顺序。灵活度最高但不确定性也最强适合探索性场景不太适合有强约束的流程。我的建议很明确凡是流程稳定的高频场景老老实实用代码编排或工作流编排凡是开放性任务比如“帮我把这个人的信息整理成周报并发送”才值得让 Agent 自主编排。4.2 编排中的上下文传递是核心多个技能协同工作时前一个技能的输出往往是后一个技能的输入。最容易踩的坑就是让模型在两个技能调用之间“人肉传递”数据这不仅浪费 token还会引入幻觉。可靠的做法是维护一个“技能执行上下文”对象由编排层统一管理class SkillExecutionContext: def __init__(self): self.results {} # skill_name - output def set_result(self, skill_name, output): self.results[skill_name] output def get_result(self, skill_name): return self.results.get(skill_name)执行cancel_order拿到订单信息后refund_order需要什么参数直接从上下文里取不需要模型重新输出一遍订单编号。这本质上是在 Agent 内部模拟了一个“管道”让数据在技能之间自动流转。4.3 编排的失败处理默认 fail-safe多技能编排里最怕的不是单个技能失败而是失败之后的连锁反应。比如取消订单成功但退款失败这时候如果流程直接中止用户会面临“订单没了钱也没退”的局面。我的处理原则是编排层要定义好失败补偿策略。最简单的是增加“补偿技能”的概念——当refund_order失败时自动触发refund_failed_notify通知人工介入或者cancel_order本身没成功时后续refund_order压根不允许执行。编排层在这里要做的是“防呆”而不是把失败留给模型去判断。4.4 并发与串行不是所有技能都能并行某些场景下并行调用多个技能能大幅提升 Agent 的响应速度比如用户同时问“今天天气怎么样、明天有什么会议、附近有什么餐厅”这三个技能互不依赖完全可以并发。但要注意并发调用必须确保技能的副作用互不影响都只读场景可以并行涉及写入或状态变更老老实实串行。我实现并发调用的方式很简单在编排层用异步任务池把互不依赖的技能丢进去并行执行再统一汇总结果返回给模型生成回复。实测下来这类场景的响应时间能从 6 秒降到 2 秒左右用户体验提升非常明显。5. 实测中的技能调用陷阱与排查经验技能系统搭好之后真正让我长经验的是上线后的排查过程。这里把我在实际项目中遇到的高频问题、完整的排查链路和最终的处理方法整理出来供你参考。5.1 高频陷阱一模型编造参数现象技能被正确选中了但传给技能的参数里有不存在的值。典型例子是get_order_status需要的订单号模型在用户根本没有提供的情况下自己“无中生有”编了一个。排查过程我先把技能执行层的入参全部打在日志里一比对就发现订单号根本不存在。然后我翻了对话历史用户确实没有给过订单号。解决思路单纯优化提示词让模型“不要编造”效果有限。最终我采用的方案是双管齐下一是在技能描述的参数后边补充“必须来自用户消息或上下文严禁自行生成”二是在执行层加参数校验如果必填字段缺失或校验失败直接返回一个专门的MISSING_PARAMETER_ERROR给模型要求它向用户确认信息而不是硬着头皮继续执行。5.2 高频陷阱二技能描述模糊导致选错技能现象同时定义了get_stock_info和get_product_info前者查库存后者查商品基本信息和价格。用户说“这个杯子还有吗”模型调了get_product_info返回了商品信息但没库存数据整个回答就废了。排查过程我看了模型调用的前后日志发现两个技能在语义上确实接近模型的内部评分可能相差不大。我又把两个技能的定义打印出来看发现get_product_info的描述里没有明确写“不含库存数据”模型完全有理由选它。解决思路它是在描述里增加“不适用”声明在get_product_info描述末尾加一句“若用户询问库存数量、是否有货请调用 get_stock_info”同时在get_stock_info描述里明确“杯子、鞋子等可用库存查询的唯一入口”。改完之后选错率从 20% 降到 3% 左右。5.3 高频陷阱三技能执行超时拖死整个 Agent现象某个技能内部调了一个第三方接口对方偶尔响应很慢技能默认没有超时控制导致整个 Agent 卡在原地转圈用户等得不耐烦。排查过程我打开链路追踪发现耗时全部堆积在技能内部的 HTTP 调用的等待上而 Agent 主流程在同步等地这个技能返回后续环节完全阻塞。解决思路我给技能执行层统一加了超时和降级机制。每个技能都必须设置超时阈值超时后返回一个SKILL_TIMEOUT结果同时记录日志并触发备用逻辑。比如查天气技能超时就直接返回“天气信息暂时获取失败”提示用户稍后再试而不是让整个 Agent 卡死。5.4 高频陷阱四上下文数据污染导致技能输出被模型“改写”现象技能返回了正确的结构化结果但在最后生成用户回复时模型没有照着结果说而是自己“润色”了一段导致关键数字错误。排查过程这是最隐蔽的一类问题。我在日志里看到技能明明返回了库存 5 件但模型回复里写的是“库存充足”。后来我发现是上下文里出现了互相矛盾的信息——技能返回“5 件”而更早的对话里模型自己说过“库存充足”后生成的回复就采用了更顺口的那个表述。解决思路这事的根子在“模型把技能结果当参考资料而不是事实来源”。我的处理方式是在给模型系统提示词里明确角色定位技能返回的数据是唯一可信事实来源生成回复必须严格引用不允许自行推断或补全数字。同时重要事实性信息从技能结果中提取后注入回复的 prompt 时用固定模板进行约束。5.5 建立一套可复用的排查链路以上这些坑让我总结出了一套排查链路遇到任何技能异常都能快速定位链路追踪给每个 Agent 请求分配一个 trace_id把模型调用、技能检索、技能执行、结果生成四段都串起来。日志结构化每个技能的入参、出参、耗时、状态全部 JSON 结构化记录方便检索。回放机制把线上失败的请求连同当时的技能注册表快照重新跑一遍用同一套输入复现问题。评估集回归每次修改技能描述或参数结构后跑一遍预先准备好的评测用例集看技能的调用准确率是否变化。这套链路建立之后技能系统的维护成本低了很多大多数问题都能在半小时内定位到具体环节而不是靠肉眼盯代码。写在最后的实操建议如果你正准备在自己的项目里搭一套agent-skills体系我个人的建议是不要一开始就追求全能。先把三五个最高频的技能做好Schema 反复打磨描述写到位注册表和简单的路由跑起来再慢慢扩展。等技能数量上了两位数再考虑动态检索、编排引擎和版本灰度。技能系统的核心不是技术炫技而是让 Agent 在真实业务里稳定、可靠、可排查这条路是一步一步走出来的不是设计出来的。