news 2026/10/28 3:15:24

Agent技能系统设计指南:从Schema定义到多技能编排

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Agent技能系统设计指南:从Schema定义到多技能编排

最初让我决定正经搞一套 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 一个可复用的技能系统包含哪几层

我后来在实际项目中总结的技能系统架构,大致分四层:

  1. 技能描述层:给模型看的“说明书”,包含技能名称、触发条件、参数说明、注意事项。
  2. 技能注册层:所有技能的统一登记处,管理技能的版本、状态、可见性。
  3. 技能执行层:真正干活的代码,接收结构化参数,执行并返回结构化结果。
  4. 技能编排层:在多个技能之间定义调用顺序、分支条件、数据流向。

这四层贯穿了下面要讲的每一个环节,后面的内容基本都围绕这四层展开。

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 做语义相似度检索。

第二段,把候选技能(控制在 5~8 个)的完整定义交给模型,让模型在这几个技能里做选择。这一步会大幅减少模型的选择困难。

举一个实际场景,用户说“我要把这两件商品一起结算”。粗筛阶段,注册表里有几十个技能,但语义相似度最高的可能是create_checkout_session、search_products、apply_coupon这几个。模型只需要在这几个里面做选择,命中率当然要高得多。

3.3 技能路由:别把无关技能暴露给模型

除了检索,还要做一次权限和上下文的过滤。我管这叫技能路由,规则不复杂但极其有用:

  • 按对话上下文过滤:用户正在咨询未发货订单,就不把“申请退货”之外的技能暴露出来。
  • 按用户身份过滤:普通用户看不到管理员技能,普通商户看不到平台运营技能。
  • 按会话状态过滤:一个客服会话如果还在身份核验阶段,就不开放改地址、退款类技能。

有些团队会把这套逻辑做成一个简单的“预路由模型”,或者更轻量一点,直接在代码里写规则。我的经验是先上规则,规则覆盖不了的情况再考虑用模型判断。一来规则可解释、可排查,二来复杂度可控。

3.4 技能热更新与缓存失效

技能定义不是一成不变的。在实际运营里,我经常因为参数调整、描述优化而更新技能,这时候有个很现实的麻烦——线上 Agent 可能缓存了旧版本的定义。

我的做法是给技能注册表加一个“版本号 + 更新时间”,并让运行时缓存设置一个 TTL,比如 5 分钟。每次从缓存读取技能定义前都校验版本号,如果发现注册表里的版本更新了,就重新拉取并刷新缓存。这样新技能上线或者旧技能修订,最多延迟几分钟就能全量生效,不用重启服务。

4. 技能编排与组合:从孤立调用到多技能协作生产

单个技能解决的问题有限。真实业务里,用户一句话往往对应一条完整链路。比如“帮我取消订单并退款,然后把优惠券还给我”,这里就涉及查询订单、取消订单、发起退款、恢复优惠券四个动作。如果让模型一次性决定并调用四个技能,风险不小,因为中间任何一步失败都会影响后续。

4.1 编排的几种形态

我见过并且实际用过的编排形态大致有三类:

  1. 代码编排:开发者用代码把调用顺序写死。比如cancel_order之后必须调用refund_order,这就是一个硬编码的流程,模型不参与决策。特点是稳定、可控,但灵活度低。

  2. 工作流编排:用 DSL 或者可视化配置定义流程,支持条件节点、并行节点、循环节点。模型可以决定进入哪个分支,但分支内部的路径是预设好的。

  3. 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 建立一套可复用的排查链路

以上这些坑让我总结出了一套排查链路,遇到任何技能异常都能快速定位:

  1. 链路追踪:给每个 Agent 请求分配一个 trace_id,把模型调用、技能检索、技能执行、结果生成四段都串起来。
  2. 日志结构化:每个技能的入参、出参、耗时、状态全部 JSON 结构化记录,方便检索。
  3. 回放机制:把线上失败的请求,连同当时的技能注册表快照,重新跑一遍,用同一套输入复现问题。
  4. 评估集回归:每次修改技能描述或参数结构后,跑一遍预先准备好的评测用例集,看技能的调用准确率是否变化。

这套链路建立之后,技能系统的维护成本低了很多,大多数问题都能在半小时内定位到具体环节,而不是靠肉眼盯代码。

写在最后的实操建议

如果你正准备在自己的项目里搭一套agent-skills体系,我个人的建议是:不要一开始就追求全能。先把三五个最高频的技能做好,Schema 反复打磨,描述写到位,注册表和简单的路由跑起来,再慢慢扩展。等技能数量上了两位数,再考虑动态检索、编排引擎和版本灰度。技能系统的核心不是技术炫技,而是让 Agent 在真实业务里稳定、可靠、可排查,这条路是一步一步走出来的,不是设计出来的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 3:14:37

Next.js 与 LangGraph.js 构建简历分析 AI Agent 实战

1. 为什么我选择用 Next.js 加 LangGraph.js 来做简历工具 Agent1.1 从一份简历的痛点说起做招聘的朋友应该都有体会,筛简历这件事本质上是个信息匹配问题,但真正干起来远没有说起来那么轻松。一份简历里藏着教育背景、工作经历、项目经验、技能栈、时间…

作者头像 李华
网站建设 2026/10/28 3:14:35

claude-mem:解决Claude Code上下文丢失问题的持久记忆工具

如果你重度使用 Claude Code 做开发,大概率经历过这个场景:上午刚让 Claude 搭好项目的目录结构和 API 约定,下午开个新会话让它继续写某个模块,结果它连项目的技术栈都要重新问一遍。你不得不把上午说过的约束、偏好、踩过的坑重…

作者头像 李华
网站建设 2026/10/28 3:14:21

用餐厅后厨类比讲透LLM、RAG、ReAct、Memory与Tool Use

1. 为什么用餐厅后厨来讲AI技术概念第一次听到“LLM、RAG、ReAct、Memory、Tool Use”这一串词,很多人的反应是:每个字母都认识,连起来完全不知道在说什么。我刚开始接触大模型应用开发的时候也一样,看了一堆文档,概念…

作者头像 李华
网站建设 2026/10/28 3:14:20

Rspack 1.7惰性编译技术解析与性能优化实战

1. Rspack 1.7版本深度解析:构建速度的革命性突破上周五深夜,当我正被一个大型前端项目的构建速度折磨得焦头烂额时,Rspack 1.7的发布公告突然弹了出来。看到"Lazy Compilation终于稳了"这句话,我立刻放下手中的咖啡&am…

作者头像 李华
网站建设 2026/10/28 3:14:20

创意工作者如何通过场景练习提升专业技能

1. 场景练习的价值与意义作为一名从业十年的创意工作者,我深刻理解场景练习在职业发展中的重要性。很多人误以为创意是天赋使然,但实际上,持续的场景化训练才是提升专业能力的核心路径。就像运动员需要反复练习基础动作一样,创意工…

作者头像 李华
网站建设 2026/10/28 3:13:05

AI编程助手导致技能衰退?程序员如何避免技术肌肉萎缩

1. 当补全代码比思考更快时,我在担心什么最近半年,我身边不少写了十年代码的朋友都在经历一种很微妙的状态:编辑器里的代码产出速度肉眼可见地变快了,但心里却越来越没底。以前遇到一个复杂需求,脑子里会先跑一遍数据流…

作者头像 李华