1. 技能化设计:Agent 工程的必然走向
接手 Agent 项目的朋友,大概率都经历过这个阶段:一开始,Agent 的能力逻辑直接写在主流程代码里,判断条件、调用分支、临时工具散落得到处都是。跑通 Demo 没问题,一旦往真实场景推,要加新能力、要适配不同业务线,代码就开始失控。我最近在重构一套智能客服 Agent,核心目标就是把它改造成技能化、可插拔的架构,"agent-skills" 这个项目名称,说白了就是这套改造的代号。
项目要解决的问题很直接:怎么让一个 Agent 具备"按需调用"不同能力,而不是把所有逻辑揉成一团。传统的写法是"如果用户说 A,就调 A 接口;如果用户说 B,就调 B 接口",业务规则多了以后,条件分支层层嵌套,维护成本指数级上升。技能化设计的思路完全反过来——把每个能力封装成一个独立技能,Agent 的调度器根据用户意图,动态决定调哪个技能、怎么组合技能。改动一个能力,只动对应技能;新增一个能力,只加一个技能包,主流程一行代码都不用改。
这个方向不是凭空想出来的。过去一年,整个 Agent 生态都在做类似的事:OpenAI 的 Function Calling 让模型学会调用外部工具,Anthropic 的 Tool Use 也在做函数级抽象。但工具调用只是技能化的底层原语,"技能"是更高一层的语义封装。两者的区别用生活类比来解释:工具是螺丝刀、扳手,技能是"修理漏水水龙头"这件完整的事——它包含工具选型、操作顺序、异常处理等一系列决策。
这篇文章适合正在做 Agent 应用落地、或者准备从单体架构转向模块化架构的开发者。不管你是用 LangChain、CrewAI 这类框架,还是自己写调度逻辑,技能化的设计思想都是通用的。接下来,我会把整套设计方法、封装原则、编排策略和踩坑记录拆开来讲,每一步都附上可以直接抄的模板和反例。
2. 核心拆解:技能的定义、封装与描述策略
2.1 技能三件套:描述、参数 Schema、执行逻辑
一个规范的技能,无论用什么框架实现,本质上都是三元组:描述(Description)、参数 Schema、执行逻辑(Execution)。三者缺一不可,而且相互制约。
描述是给大模型"看"的。模型通过技能描述来判断:当前用户的问题,是不是该调用这个技能。描述写得准不准确,直接决定模型调得准不准。参数 Schema 是给大模型"填"的。模型根据用户请求,从自然语言里抽取关键信息,填到技能要求的参数里。执行逻辑是给程序"跑"的。拿到完整参数后,技能真正干活的代码——调 API、查数据库、算结果。
我见过不少团队在第一步就栽了跟头。初期做 Agent 时,技能描述写得非常随意,比如"查询用户信息",结果用户问"帮我查一下张三的等级",模型调了查询用户信息技能,但技能内部根本没有等级字段,返回结果就有偏差。描述里没写清楚"能查什么、不能查什么、参数从哪里来",模型只能瞎猜。
实操上,我用一套固定的描述模板来约束:
技能名称:查询订单物流 触发场景:用户询问订单的物流轨迹、快递进度、包裹位置 不触发场景:用户咨询运费标准的,应该调用【查询运费规则】技能 参数来源说明:订单号通常从用户消息中提取;如用户仅提供手机号,需要先调用【查询订单列表】技能获取订单号 返回格式说明:返回物流节点数组,每个节点包含时间、地点、状态描述这段话看起来啰嗦,但每条信息都在帮模型做决策。触发场景是正向信号,不触发场景是负向信号,信号越清晰,模型误调用的概率越低。参数来源说明是给模型"铺路"的——我自己测试过的结果,加了这行字,多技能协作场景下调用准确率能提升 10% 到 15%,因为模型知道下一步该干嘛,而不是拿到参数缺口就卡住。
2.2 技能描述怎么写,模型才不误解
写技能描述,核心原则就一条:站在模型视角,把话说死,不要留想象空间。
第一,名称要具体到动作级别。"查询"不如"查询订单物流","处理"不如"创建售后工单"。模糊的动词模型没法判断边界。第二,场景描述要包含正反两面。只写"什么时候用",模型在边缘场景会乱来;写清楚"什么时候不用",模型就有了排除依据。第三,用祈使句描述技能行为。"根据用户提供的订单号,调用快递 100 API,返回最近 5 条物流记录",这是明确的指令;"本技能可用于查询物流信息"则是对模型的弱提示。第四,明确参数缺口的处理策略。如果模型抽不到必要参数,是直接拒绝还是引导用户补充,必须在描述里写清楚,否则模型可能编造参数,这是实战中最危险的行为。
为了验证描述质量,我做了一个最笨但最有效的测试:把技能描述发给一个完全不知道项目背景的同事,让对方根据描述判断"用户说某句话时该不该调这个技能",如果不确定,说明描述还有歧义。这个测试成本极低,但对质量提升帮助很大。
2.3 参数 Schema 设计的原则与反例
参数 Schema 是技能的"接口契约",设计好坏直接影响两件事:模型能不能抽准参数,执行层能不能跑通逻辑。
设计原则第一条:参数能少就少。每个参数都是模型的一个决策点,参数越多,填错概率越高。能通过查询获取的信息,不要设计成必填参数;能从其他参数推导的值,不要单独设参数。我见过一个极端反例:一个"查询订单"技能,要求必填订单号、用户 ID、下单时间、渠道来源四个参数,模型每轮都在反反复复追问用户补信息,逼得用户想摔手机,而实际只需要订单号一个参数就能查到全部订单信息。参数冗余直接毁掉了用户体验。
原则第二条:用 enum 约束枚举值。凡是取值在有限集合里的字段,比如订单状态、支付方式、物流公司,统统列成 enum,给模型一个封闭选项列表,绝对不给自由发挥的自由。自由文本输入是最容易出错的地方。
原则第三条:类型要严格,避免 string 滥用。日期就要用 date 格式,数值就要用 number,布尔值就用 boolean。有团队把"是否加急"设计成 string,模型填了"要的"、"是的"、"真真的",后端解析直接崩。类型一严格,模型就只能填 true 或 false。
原则第四条:参数描述要附带示例。给模型一个价值千金的示例,比如"订单号格式:23 位数字,通常以 2024 开头",模型抽参的准确率会明显提升——因为模型见过这个格式了,能更准地识别它。
反例场景来自我的实际经历。有一个"创建工单"的技能,初始参数 Schema 设计了 9 个字段,嵌套了两层对象。模型经常漏填嵌套字段,还出现过把"问题描述"填进"用户昵称"字段的低级错误。后面我把 Schema 砍到 4 个必填字段,合并了两个对象,删掉了三个可以从上下文推导的字段,漏填率降了一半以上。设计 Schema 不是从数据完整性出发,而是从模型能力边界出发——模型能轻松完成的留,模型费力才能完成的砍或改写。
3. 技能编排:从单技能到复合技能协作
3.1 原子技能与复合技能的划分逻辑
技能不是越多越好,也不是越"大"越好。我把技能分成两个层级:原子技能和复合技能。
原子技能是单一职责的最小可执行单元,比如"查询快递轨迹""计算运费""校验地址有效性"。判断标准有三个:单一职责——只做一件清晰的事;无状态——不依赖上一次调用的结果;输入输出明确——参数和返回值边界清楚。原子技能的设计目标就是"高内聚、低耦合",被复用的次数越多,设计就越成功。
复合技能是编排方案,把多个原子技能按业务逻辑串起来。比如"处理退货申请"就是一个复合技能,它内部会调用【校验订单状态】【生成退货单】【通知仓库】三个原子技能,并且包含决策逻辑——只有订单状态为"已签收"且签收时间在 7 天内,才允许继续走退货流程。
为什么要做这个区分?因为模型对长链路的控制能力有限。如果让模型在一步之内完成退货判定、生成单据、通知仓库三个动作,中间任何一步出错,模型很难自行纠正,而且这些错误往往还难以察觉。复合技能将判断和编排固化到代码层面,模型只需要传"用户要退货"的意图,具体流程由复合技能执行。模型更轻松,行为也更稳定。
3.2 四种调度模式:顺序、分支、并行与重试
复合技能的编排方式有规律可循,主要就是四种模式。
顺序模式最简单,A -> B -> C,上一步输出作为下一步输入。典型场景是"下单流程":校验库存 -> 扣减库存 -> 生成订单。任何一步失败,直接短路,不执行后续步骤。
分支模式按条件走不同路径。典型场景是根据订单状态走不同处理分支,或者根据用户等级分配差异化策略。分支条件需要写成明确的规则,且分支路径必须有兜底 else 分支。我吃过一次亏,条件分支漏写了"订单类型为其他"的兜底,结果模型遇到未知类型时,走了一条不该走的路径,生成了错误的售后单。之后所有分支都强制加 else。
并行模式适合多个无依赖的技能同时执行。比如处理"退货申请 + 退款申请 + 补发申请"这种多意图场景。并行要注意一点:多个技能同时操作数据库时,先做了一次事务版本号检查,避免并发写冲突。
重试模式处理可恢复的失败。调用第三方 API 超时、网络抖动,这类失败不需要动整体流程,重试 1~2 次往往就够了。要注意的是,重试必须配合幂等设计——同一个请求重复执行多次,效果和一次完全一样。有个典型教训:重试退款接口时,因为没做幂等处理,用户被重复退款两次,这问题相当严重。
这些模式不复杂,但实际项目里容易走偏的是:第一次设计就直接上最复杂的编排,搞出一个"万能调度器",实际上大部分场景只需要最简单的顺序执行。编排复杂度要跟着业务复杂度走,不要为了架构上的美观去过度设计。
3.3 技能冲突与优先级策略
技能数量一多,必然遇到冲突:两个技能都能响应用户请求,模型怎么选择?
先分析冲突的类型。最常见的是职责重叠,比如"查询订单"和"查询售后退款进度",某些场景下非常相似,模型拿到用户消息,很难判断该调哪个。解决办法是明确职责边界,给每个技能增加"不触发场景"的负向提示。我之前写过:"本技能不处理退款进度查询,该类请求请调用【查询售后退款进度】技能",冲突率明显下降。
第二种是上下文优先级冲突。同样的用户请求,在不同业务上下文里应该走向不同技能。比如"我要改地址",如果用户订单未发货,应该调用【修改收货地址】技能;如果已发货,应该调用【登记地址变更请求】技能。这时候靠模型自己判断就不够稳定了。更可靠的做法是在调度层增加上下文识别逻辑,先识别订单状态,再决定调用哪个技能。这类场景靠"技能本身"解决不了。
第三种是组合冲突。多个技能同时执行时,相互之间的副作用冲突。典型例子是"修改地址"和"重新发货"是两个技能,如果同时触发,前者改了地址,后者按旧地址发货。解决方案是引入冲突检测——在同一轮复合技能编排里,涉及同一资源(比如订单、地址)的技能互斥执行,只允许串行。
4. 实操实录:一个客服 Agent 的技能化改造全过程
4.1 第一阶段:技能盘点与边界梳理
我拿手头的客服 Agent 举例说明整套实施路径。这个 Agent 要处理 13 类核心业务场景、6 类辅助场景,第一版逻辑全部写成 if-else,分布在 8000 行的主文件里。
改造第一步是盘点,把散落在各处的功能分支提炼成技能清单。从需求文档和代码分支里提取出所有"动作",每个动作如果具备独立输入输出,就标记为一个候选技能。候选技能有 22 个,比如"查询订单""修改地址""退款申请""投诉建议"等。
第二步是划边界。22 个候选技能里有几个高度重叠。比如"查询订单物流"和"查询订单详情"都涉及订单查询,前者关注物流轨迹,后者关注商品明细和金额,但用户往往一句话同时问两者:"我的订单到哪了,一共多少钱?"模型选技能时犹豫不决,会频繁出现误选。处理办法是把两个技能合并成一个复合技能"查询订单全貌",内部先查详情,再查物流,最后统一组装返回。合并后技能数量减到 19 个,误调用率下降了一截。
第三步是定级。19 个技能里,10 个是原子技能,9 个是复合技能。每个复合技能都明确编排链路,比如"退款申请"链路是:查订单状态 -> 验签收时间 -> 生成退款单 -> 通知财务。链路固化到代码里,不让模型自由发挥。
4.2 第二阶段:Schema 与描述的迭代打磨
技能清单定稿后,最耗时的是 Schema 和描述。每个技能都按前面说的模板写描述、按四条原则设计 Schema。我拿三个典型技能实际对比一下打磨前后的效果。
第一个技能"查询订单物流"。初始 Schema 是order_id一个参数,描述只有一句"查询订单物流信息"。实测发现,用户经常不直接提供订单号,只给手机号。打磨后:描述增加"如用户只提供手机号,先调用【查询订单列表】技能获取订单号",Schema 增加可选参数phone,同时在描述里说明:phone 有值时,执行层自动用 phone 查出最近一笔订单的物流。打磨后,物流查询的完整成功率从 62% 提升到 88%。
第二个技能"修改收货地址"。初始 Schema 有order_id、new_address、old_address三个参数,其中old_address是冗余参数——执行层根本不需要旧地址。模型为了填 old_address,经常向用户反问"您的旧地址是?",非常烦人。砍掉后,模型只抽 order_id 和 new_address,交互顺畅了很多。
第三个技能"退款申请"。最大问题是没有写负向条件,用户说"帮我看看怎么退货"这种咨询类请求,模型也直接调了退款技能,生成了退款单。在描述里明确"本技能仅在有明确退款意图时调用;如用户仅为询问政策,调用【查询售后政策】技能",问题就解决了。还有一个细节:退款原因字段从自由文本改成 enum 列表,包含"七天无理由""商品破损""配送错误"等 8 个选项,模型抽参准确率大幅提高。
4.3 第三阶段:调用链路与返回格式的规范化
技能执行完,返回格式如果不统一,Agent 上层的大模型就没法稳定组装最终答复。我的经验是统一用结构化的 JSON 作为所有技能的输出,并且每个输出都带status和message字段。实战中,status只有三个取值:success、fail、need_input。
success表示技能正常执行,执行结果放在data字段里。fail表示业务上不可执行,比如订单号不存在、已经退过款,此时message字段放给用户看的提示语。need_input表示参数缺失,message字段放引导用户补充信息的追问。
当时我的系统里做过一次统计,技能输出不规范导致上层模型组织回复时"一本正经地胡说八道",例:技能返回了原始堆栈信息,结果模型直接把堆栈贴给用户当答案。还有人遇到过技能成功标志是布尔值 false,但模型把它当成执行失败,本来正常的结果被重新执行了一次。统一 JSON 结构后,这类低级错误一劳永逸地解决了。
所有技能共享一套返回规范,还有个额外收益:上层大模型组织最终回复时,只需要处理三种固定模式,心智负担大大降低。这在多技能协作的并行场景里尤为重要——模型要合并多个技能的返回结果,统一结构让它能清楚地知道哪些是数据、哪些是提示语。还有一点,message字段我要求所有技能在写提示语时使用"口语化、温和、给出下一步可操作建议"的风格,避免技能直接暴露系统内部错误信息给用户。
5. 常见问题速查表与独家避坑经验
5.1 问题速查:现象、原因、解决方案
这一节把我在 agent-skills 项目里遇到过的最典型的六个问题整理成速查表。
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 技能 A 被模型频繁误调用,经常替代技能 B 响应 | 描述中未写明"不触发场景" | 在 A 的描述中增加负向条件,明确"何时不该调用" |
| 模型把枚举值填成了自由文本 | enum 类型未约束,或描述中未列出候选值 | Schema 用 enum 列出全部合法值;描述中给一个示例值 |
| 多技能并行时数据互相覆盖 | 技能间共享了同一个全局变量或缓存键 | 为每个技能增设独立命名空间;涉及同一实体时改为串行 |
| 用户问常识问题,Agent 非要调用技能 | 技能列表中混入了不适合当前场景的技能 | 在调度层做前置过滤,或为技能组配置白名单场景 |
| 技能执行成功但回复内容与用户诉求偏差大 | 技能返回数据未经过上层模型摘要,直接透传 | 统一用 JSON 结构化输出,data与display_message分离 |
| 同一个用户问题,不同时间调用不同技能 | 调度层缺少上下文记忆,模型每次都重新判断 | 引入会话级技能记忆,用户本轮意图确定后缓存决策结果 |
避坑的第一条经验:不要在调度层堆规则。早期我把所有技能冲突都用 if-else 规则解决,结果规则越加越多、越加越乱,最后出现了规则间相互打架的情况。真实场景应该把认知负担交给模型——描述写够了,模型自己会判断;只有模型确实无法判断的场景,才值得写规则。
避坑的第二条:每个技能都要有可观测性。接入了简单的日志体系,记录每次调用的技能名称、参数、返回值、耗时、模型决策置信度。上线前两周每天检查一次决策日志,发现了大量"模型想调 A 技能却写成了 B 技能"的隐蔽问题。没有日志,这些问题可能过很久才会被用户投诉。
避坑的第三条:技能数量控制在 30 个以内。我在十几个不同类型 Agent 项目上得到的经验一致:技能超过 30 个,模型的选择准确率会明显下降。如果业务确实需要更多技能,应该拆分成多个子 Agent,每个子 Agent 只关注自己的技能集。这是架构层面最有效的"减负"手段。
5.2 上下文窗口消耗的控制技巧
技能描述和参数 Schema 是要占用上下文窗口的。早期我把 19 个技能的完整描述全部挂载到每次请求里,光技能元数据就吃掉了接近 4000 token。随着技能增多,这个问题会更突出,所以有必要做裁剪。
我的做法是分层挂载:所有技能分为全局常驻和局部动态两组。全局常驻技能不超过 8 个,是用户高频使用的核心技能,每次请求都会带上。剩余技能按会话状态动态注入——当调度层判断当前会话涉及"售后"主题时,才把售后相关的 5 个技能描述追加到上下文中。用户意图一旦偏离该主题,这些技能描述就会被移除。
实现这个机制其实不难,核心在于一个轻量级的意图预判器,用规则或者小模型判断用户消息属于哪个主题域,然后决定加载对应的技能组。这个设计让每次请求的平均 token 消耗降了接近一半,决策效率和准确率反而提升了——因为模型看的候选少了,选择噪音也少了。
5.3 技能版本管理与灰度发布
技能不是写完就完事的。它上线之后一定要改,而且改坏了要有回滚能力。我的方案很朴素:给每个技能单独做版本号,技能配置和代码都走版本管理。
版本管理的关键是"配置与代码分离"。技能的描述、参数 Schema 这些文本配置,我放在独立的配置文件里,跟执行代码分开管理。改描述不需要重新发代码,直接改配置就能灰度。这在调整技能描述时极其方便——改个词、加句负向提示,完全是热更新。代码层面如果涉及执行逻辑变更,就要走正常的发版流程,不能在配置里塞逻辑。
灰度发布的方法则是按会话比例逐步放量。新版本技能先服务于 5% 的会话,观察决策准确率和用户满意度,确认没问题再逐步提升到 10%、50%、100%。灰度期间,新旧版本并行共存,日志里记录版本号,方便回溯问题。有一次新版本技能把"送达"误识别为"签收",就是灰度阶段发现的,及时回滚只影响了一小部分用户。
从实际经验来看,大多数技能问题都出在描述和 Schema 上,而不是执行代码上——代码跑不跑得通很容易测出来,描述有没有歧义却要上线后才知道。所以配置和代码分离这套机制,我认为是技能化管理必不可少的一环。
6. 从工具调用到技能化,是必经之路
写到最后,再分享一点我在这个项目里最深的体会。刚做 Agent 时,任何人都会先沉迷于 Function Calling 或 Tool Use 带来的"模型会调用工具了"的快感,但工程化到一定程度,如果不做技能化的抽象,系统一定会走向失控。技能化不是要为架构而架构的去制造复杂度,而是把 Agent 的能力收敛到"一个可描述、可测试、可版本管理、可灰度发布"的工程事物。
在我自己实际操作中,每次新建一个技能,都要重新审视三个问题:描述站在模型视角是否足够清楚?Schema 是否把模型决策点降到最低?返回格式是否让上层模型能直接组装回复?这三个问题过完,技能质量基本就有保障了。这个做法如果让我概括成一句话,我会说:Agent 的能力不是靠堆砌,而是靠设计。技能化设计,就是让每一个能力都"有边界、有说明书、有质量基线"。
最后再留给正在做 Agent 工程化的朋友一个建议:如果你的 Agent 已经超过 10 个能力点,赶紧停下来做一次技能盘点。先把能力清单列出来,划清边界,再考虑描述和 Schema。这个动作早做一天,后面重构的成本就能少一大截。