做 AI Agent 工程化这件事,做得越深越会发现一个尴尬的现状:模型的能力上限其实没那么容易触到,真正决定一个 Agent 能干什么、干得多稳的,反而是你给它配的那套“技能”体系。
这里的“技能”不是指模型参数里隐含的推理能力,而是指你通过代码、提示词、工具定义、执行流程,明确告诉 Agent“你能调什么、按什么顺序调、调完结果怎么处理”的那套外部配置。我习惯把它称为 Skills 配置。最近在重构一版内部 Agent 框架时,我正好把整套技能工作流从“拍脑袋加工具”改成了“按工程规范配置”,整个过程踩了不少坑,也沉淀出一套可复用的方法论,这篇文章就完整拆一遍。
先抛结论:Skills 的本质,是把一个通用模型变成领域专用执行器的那层“胶水”。它解决的核心问题不是“模型懂不懂”,而是“模型能不能稳定地按预期干活”。这篇内容适合两类人看:一类是刚上手做 Agent 应用,需要系统理解技能怎么配的开发者;另一类是已经在用 Agent 框架,但觉得工具越加越多、行为越来越不可控,想重新梳理工程化思路的同学。
1. 先搞清楚:Skills 到底在配什么
很多人的第一个误区,是把 Skills 等价于“注册几个函数接口”。我一开始也这么干过,写几个 Python 函数挂到 Agent 上,发现效果还行,但一上复杂场景就原形毕露。后来想明白了:Skills 是给 Agent 配一套完整的“执行上下文”,不只是工具,还包括工具的约束、动作的编排、异常的处理,以及整个流程的状态管理。
1.1 从“会聊天”到“会干活”
聊天式交互和任务执行式交互有个本质区别:聊天只需要“答得对”,执行则需要“办得成”。你问模型“帮我查一下订单状态”,模型可以给你一段标准答案;但你要是让模型真的去查,它就得知道查哪个接口、带什么参数、返回结果怎么解析、查不到的时候怎么办。
这些信息如果全部塞进对话上下文,会出现两个问题:一是上下文长度飞速膨胀,模型处理不过来;二是每次对话都要重复描述规则,既浪费 token 又容易冲突。Skills 就是把这个“怎么办”的部分从对话里剥离出来,做成一套独立的、可复用、可版本管理的执行规范。
你可以把模型想象成一个能力很强但对业务一无所知的新人。你给他一份详细的公司 SOP 手册,告诉他遇到什么情况翻哪一章、按什么流程操作,这就是给 Agent 配 Skills。手册越清晰、流程越固定,新人出错的概率就越低。Skills 本质上就是这份 SOP 手册的工程化形态。
1.2 为什么不能把技能直接写进 Prompt
我见过不少项目把工具描述、调用规则、注意事项全部拼到一个超长 System Prompt 里,效果当然能用,但维护成本极其痛苦。原因有三:
第一,Prompt 是线性文本,表达能力有限。工具与工具之间的依赖关系、条件分支、循环逻辑,用自然语言写出来非常绕,模型理解起来也容易出现歧义。而 Skills 可以用结构化的流程定义(比如状态机、DAG、顺序编排)来表达这些关系,语义清晰得多。
第二,Prompt 的修改成本和风险不成比例。你改一句话,可能影响所有场景。而 Skills 配置可以按模块独立调整,还能做线上灰度对比,回滚也只是切一个版本的事。
第三,Prompt 里的技能描述是“建议性”的,模型可以自由发挥;而 Skills 通过函数调用的约束,把模型的行为限制在“可执行”的边界内。你要让 Agent 调一个接口,就得用函数调用的格式把参数 Schema 定义清楚,模型就算想自由发挥也发挥不出去。
1.3 Skills 的三层结构:意图、动作、流程
在实践中,我把一个完整的 Skill 拆成三个层级,互相配合:
- 意图层:描述这个技能是干什么的、什么场景下触发、什么情况下不触发。模型靠这层做决策,决定当前任务是否需要调用这个技能。
- 动作层:定义具体执行的原子操作,比如调一个 API、查一次数据库、发一封邮件。每个动作必须有清晰的输入输出 Schema,以及失败时的降级方案。
- 流程层:把多个动作按业务逻辑串起来,定义顺序、条件分支、循环、超时、回滚等行为。
三个层级各司其职。意图层解决“该不该做”,动作层解决“怎么做”,流程层解决“先做什么后做什么”。这样拆分以后,你排查问题时能精准定位:模型不调用技能,是意图描述不清;调用了但参数错,是动作 Schema 有问题;调用了也执行了但结果不对,大概率是流程编排逻辑出了问题。
2. 技能体系的整体设计:从能力清单到可编排动作
设计一套 Skills 配置,第一步绝不是写代码,而是做能力盘点。先把业务场景里需要 Agent 完成的所有任务列出来,然后逐个拆解:这个任务需要哪些数据、哪些外部操作、哪些判断逻辑。
2.1 先列能力清单,再谈工具
我建议用一张表格把自己的场景需求列出来,类似下表的逻辑:
| 场景任务 | 需要的数据 | 需要的外部操作 | 涉及的判断逻辑 | 优先级 |
|---|---|---|---|---|
| 查询订单状态 | 订单号、用户身份 | 调用订单查询接口 | 校验订单归属、判断状态语义 | P0 |
| 生成周报 | 本周数据、模板 | 读取数据库、生成文件 | 汇总统计、格式化 | P1 |
| 定时提醒 | 时间、消息内容 | 调用消息推送服务 | 时区换算、频次控制 | P2 |
这张表做完以后,你才能判断哪些能力应该做成“通用工具”,哪些应该做成“组合 Skill”。比如“查询订单状态”可能只是一个原子工具,而“生成周报”需要读取数据、统计汇总、套用模板三个动作串联,就应该做成一个 Workflow。
这里有个容易忽略的点:不要为了炫技而堆砌工具数量。我见过一个项目给 Agent 配了三十多个工具,看似能力强大,实际模型根本分不清在什么场景该用哪个,决策准确率反而下降。工具数量控制在 8~12 个以内,模型的选择准确率通常是最高的。如果确实需要很多能力,优先考虑把相关操作合并成更粗粒度的 Skill,而不是暴露一堆碎片化接口。
2.2 工具定义:质量和数量同样重要
工具定义的核心是描述质量。还是用“函数调用”来理解:你给模型的函数描述写得越准确,模型传参的准确率就越高。写工具描述的时候,有几个实操要点:
- 参数描述要带业务语义,比如“user_id”不能只写“用户 ID”,要写“用户唯一标识,用于查询用户创建的订单,必填,格式为 UUID”。
- 枚举值要明确列举,不要让模型自由发挥。比如订单状态字段,就应该明确列出“pending / paid / shipped / completed / cancelled”。
- 要写负向约束,也就是什么情况不适用。比如一个查询天气的工具,要写明“不支持未来 15 天之外的预报查询”;一个发送邮件的工具,要写明“单封邮件正文不超过 10MB,附件不超过 3 个”。
- 返回结果的 Schema 也要定,这是很多人的盲区。模型需要知道工具会返回什么,才能决定下一步动作。返回结构不稳定,流程编排就没法写。
另外强烈建议在工具层做一层轻量校验:入参格式不正确时,工具内部先用规则引擎做一次拦截,返回结构化的错误信息,而不是直接把异常抛给模型。这样模型可以根据错误信息自行修正参数重试,不需要你额外写重试逻辑。
2.3 动作编排:把单个工具串成流程
单个工具能做的事有限,真正体现工程价值的是编排。我在实践里把编排模式总结成四类:
- 顺序执行:动作 A 完成后必然执行动作 B,比如“查库存 → 生成订单 → 发送确认”。
- 条件分支:根据前置结果决定后续路径,比如“查库存充足则下单,否则推荐替代商品”。
- 循环重试:某个动作失败后按规则重试,但要设置最大次数和退避策略,避免死循环。
- 并行分发:多个互相独立的动作同时执行,再汇总结果,比如“同时查三个数据源,合并去重后输出”。
这四类模式可以组合出绝大多数业务场景。关键在于,编排逻辑要尽可能显式化,不要依赖模型自己推理下一步做什么。如果某条业务路径是固定的,就把它写成硬编码流程,让模型沿着流程执行;只有真正需要模型做判断的节点,才开放给模型决策。这样既保证稳定性,又保留灵活性。
状态管理也是编排里绕不开的问题。一个运行中的 Agent 任务,中间可能跨越多次模型调用和工具调用,每次调用之间不能丢状态。我建议把所有中间状态统一存到一个状态对象里,包括当前步骤、已收集的数据、待处理事项、错误计数等。模型每次执行前,把状态摘要注入上下文;执行完一步,立刻更新状态。这就避免了模型“失忆”导致的重复操作或漏操作。
2.4 状态与上下文:别让 Agent 每次失忆
Agent 的上下文窗口是有限的,而一个长任务的执行过程会产生大量中间信息。如果不加管理,上下文会越撑越满,模型不知道哪些信息重要,最后要么漏掉关键约束,要么被无关信息干扰判断。
我的做法是做一个“上下文裁剪器”,每次进入新步骤前,从状态对象里提取当前步骤最关键的信息子集,拼成精简摘要供模型参考。历史细节不删,但也不全量进入上下文,而是放在一个可由模型中转访问的存储里。这样上下文长度保持可控,模型的行为一致性也更好。
3. 实操:给 Agent 配一套可运行的 Skills 配置
理念讲再多,不如直接上一份能跑的配置。下面我用一个“订单售后处理”的常见业务场景,完整拆一遍配置流程。这个案例不是玩具级的 hello world,而是我在模拟生产环境里实际调过的一套方案。
3.1 场景定义与能力清单
场景需求:用户申请订单退款,Agent 需要校验订单信息、判断退款条件、计算退款金额、执行退款操作,最后生成处理结果。
我把这个场景拆成四个原子动作:
- 查订单详情
- 查退款规则
- 计算退款金额
- 执行退款
再定义两个判断节点:
- 订单是否满足退款条件
- 退款金额是否在免审批额度内
整个流程用顺序 + 条件分支的组合就能描述完整,不需要并行,逻辑非常清晰。
3.2 工具 Schema 定义
先看最核心的“查订单详情”工具,它的函数定义大致长这样(用常见的 Agent 函数调用格式示意,换成你用的框架也能对上):
{ "type": "function", "function": { "name": "query_order_detail", "description": "根据订单号查询订单完整信息。仅支持查询本人创建的订单,不支持跨用户查询。订单号格式:ORD 开头后面接 12 位数字。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式为 ORD 开头 + 12 位数字,必填" }, "user_token": { "type": "string", "description": "当前操作用户的身份令牌,用于校验订单归属,必填" } }, "required": ["order_id", "user_token"] } } }这里有两个细节值得琢磨。一是描述里加了负向约束“不支持跨用户查询”,这能避免模型在后续对话中被诱导查询他人订单,属于安全边界。二是把 user_token 显式作为参数传入,而不是让工具内部从全局变量里取,这样每个动作的输入输出都完全确定,既方便校验,也方便排查问题。
退款规则工具的返回结构我也定得比较细,它直接决定了后续判断节点怎么计算,所以我设计成一个结构体,包含退款上限、退款比例、特殊条件三个字段:
{ "rule_id": "RULE_2025_001", "refund_limit": 0.9, "refund_ratio": 1.0, "special_conditions": ["vip_user_double_refund"] }把规则做成结构化数据而不是自然语言描述,是因为后续的判断逻辑做数值比较时,结构化数据可以直接参与运算,避免模型在“规则理解”上产生偏差。
3.3 流程编排配置
接下来把动作串成流程。我用一个类似 JSON 的 DSL 来定义流程,换成用代码硬编码也是一样的效果,重点是逻辑结构:
{ "steps": [ { "id": "step_1", "type": "tool_call", "tool": "query_order_detail", "next": "step_2" }, { "id": "step_2", "type": "tool_call", "tool": "query_refund_rule", "next": "step_3" }, { "id": "step_3", "type": "judge", "condition": "order.status == 'completed' && order.paid_amount > 0", "if_true": "step_4", "if_false": "step_deny" }, { "id": "step_4", "type": "tool_call", "tool": "calc_refund_amount", "next": "step_5" }, { "id": "step_5", "type": "judge", "condition": "refund_amount <= 500", "if_true": "step_execute", "if_false": "step_manual_review" }, { "id": "step_execute", "type": "tool_call", "tool": "execute_refund", "next": "step_done" }, { "id": "step_done", "type": "end_with_result", "result": "退款完成,返回回执单号" }, { "id": "step_deny", "type": "end_with_message", "result": "订单不满足退款条件,返回具体原因" }, { "id": "step_manual_review", "type": "handoff", "result": "退款金额超出免审批额度,转人工处理" } ] }这段配置表达的核心思想是:能确定的分支全部写死,不给模型自由发挥的空间。整个流程里模型需要做决策的只有两个判断节点,其他步骤都是确定性的工具调用。这样做的好处非常明显——线上行为可预期,出现问题时能精准定位是哪一步出了差错,而不是让模型“背锅”然后又说不清楚错在哪。
流程里专门安排了一个“handoff”类型,这是我在实践里强烈建议加的一个能力。当 Agent 判断自己处理不了(比如金额超限、命中风控规则)时,清清楚楚地转交人工,而不是硬着头皮再调一次工具或者给出一个看似合理的错误结论。人工兜底是所有 Agent 工程化方案的必选项,不存在什么时候都 100% 可靠的 Agent。
3.4 参数与阈值的计算逻辑
流程里出现的两个关键阈值,很多同学会好奇怎么定出来的,我详细说明一下计算逻辑。
第一个是退款审批线 500 元。这个值不能拍脑袋拍出来,它来源于对业务数据的统计分析。我拉取了历史三个月的退款订单数据,做了个小统计:
- 订单金额在 500 元以下的退款请求,纠纷率极低(低于 0.5%),且退款金额基本都在订单实付金额的 80% 以内;
- 订单金额在 500~2000 元的,纠纷率明显上升;
- 2000 元以上的,即使规则允许全额退款,也会有人工审核介入的客诉需求。
于是我把自动执行阈值定在 500 元,留出充足的安全边界。等线上运行一段时间后,再根据实际的客诉率和人工介入率数据,动态调整这个值。
第二个是特殊的 Vip 用户双倍退款规则,这是我在配置“calc_refund_amount”工具时考虑到的一个细节。规矩本身并不难,难点在于你要在配置里把“特殊规则”和“通用规则”的优先级定义清楚。我的做法是:通用规则配在流程层,特殊规则配在工具层。算钱的时候,工具内部先按通用规则算一遍,再检查用户是否命中特殊条件,命中就覆盖计算结果。
这种“通用流程 + 工具内特例”的组合,比在流程层面做复杂的条件嵌套要清晰得多,也更容易测试。因为你可以在工具单元测试里把特殊规则全部覆盖掉,流程层只管通用的逻辑。
3.5 效果验证与迭代
配置写完后不能直接上生产,我先用一批历史工单做了验证。操作方式是:把历史真实订单数据灌进去,让 Agent 模拟处理,再用人工核对结果。
这个环节我最大的体会是:第一次跑通不代表配置好了,后面至少要迭代两到三轮。第一轮跑完,我发现“查退款规则”这个工具经常被模型调用两次。本来流程里只要调一次,但模型可能因为上下文太长忘了前面已经拿到规则,又调了一次。排查后发现是步骤之间的状态摘要没做好,状态对象里没有记录“规则已获取”这个标志。修复方式很简单:在判断节点后面加一个状态标记,后续模型再看到这个标记,就不会重复调用。
另外我还监控了一个指标:工具调用的 parse error 率。这个指标反映了模型生成工具参数时,有多少次没通过你 Schema 的格式校验。最初版本里这个比率高达 8%,后来优化了参数描述、给枚举值加了明确约束后,降到了 1.5% 以下。如果你的 Agent 经常出现“工具调用格式错”的情况,优先检查的不是模型本身,而是你的参数描述够不够具体。
4. 这份配置上线后:运行观察与一次真实翻车记录
线上跑了三周后,整体效果符合预期,但我必须承认,中间出过一次印象极为深刻的翻车事故。当时没有走人工兜底路径,而是走到了一个预期之外的路径里。
4.1 事故复盘:Skill 描述里的“效率优化”竟然闯了祸
具体场景是这样的:一个用户申请退款,订单金额 368 元,命中自动退款条件,流程执行到“step_execute”,即调用执行退款工具。执行完成后,按流程应该走到 step_done 结束,并把回执单号返回给用户。
但那次操作中,模型没有结束对话,而是在拿到执行结果后,又多看了几遍工具描述里的一个词“尽量为用户提供高效的服务体验”。它认为这是系统允许它“帮忙代发一条优惠券给用户”的依据,于是接着去调用了一个不在当前流程里的“发优惠券”工具。结果因为该工具要求的参数格式和模型生成的格式不一致,返回了一个错误。模型又尝试修正重试,反复了三次,导致整个处理过程拖长了将近 20 秒,用户体验大打折扣。
问题不在模型本身胡编乱造,而在于我配置的工具描述里那句“提供高效的服务体验”属于表达不明确的“开放授权”。模型把它理解成了“可以额外做一些提升体验的动作”,而这个动作并没用她当前链条上。
这次事故给我提了很大一个醒:Skill 描述里只能写“能做什么”和“不能做什么”,永远不要写“为了达到某个抽象目标你可以灵活发挥”这类话。抽象目标是需要在代码层和流程层体现的,而不是交给模型去自由理。
从那以后,我修改了全部工具描述,删掉了所有“若可能”“尽量”这类模糊授权词汇,取而代之的是明确的参数约束和“超出范围请转人工”的兜底提示。修复后,同样场景的误调用率从 7% 降到了接近为零。
4.2 工具调用频次监控
除了翻车事故,我还想分享一个数据观察。上线后,我把每一次用户会话中工具调用的次数做了统计,发现一个很有意思的分布:
- 70% 的会话只需要调用 1~2 次工具就能解决;
- 20% 的会话需要调用 3~5 次;
- 10% 的会话会出现 8 次以上的工具调用,而且这一部分会话的最终满意率明显更低。
后面这个“长调用链低满意率”的现象,经过仔细分析,往往不是任务本身复杂,而是流程编排里缺少“终止条件”。模型在一个分支里反复尝试不同的参数、不同的工具,始终没有遇到“提前终止”的判断节点,最后把用户和模型都拖得疲惫不堪。
所以我在所有流程配置里都加了一个通用规则:工具调用超过 N 次(按场景不同取 5~10 次)必须主动调用人工转交接口,或者给用户返回一个明确的“处理需要时间”的提示。不要让 Agent 无限重试,这对用户体验的伤害远超直接坦白。
4.3 用回放对比法做回归验证
每次改完 Skill 配置,我都不允许直接上线。我先做一次“回放”验证:把历史会话记录里的用户输入重新灌入新配置的 Agent,得到新输出,再和人工标注的期望结果做对比。
这个方法看起来很简单,但极其有效。它能捕捉到两类问题:一类是流程边界问题(比如新配置把一个本来该自动处理的单子错误地转给了人工),另一类是工具选择问题(比如新配置在某个场景没调用预期工具,却调了另一个相关性较差的工具)。
回放验证有一个关键点:要充分覆盖边界场景,而不是只选正常场景。正常流程跑通一百次不一定能发现问题,但离场场景试一次就足够暴露缺陷。比如退款场景里的边界就包括:已经退过款的订单再次请求退款、订单金额为负数(数据异常)、用户身份无效、订单号格式不合法。这些边界用例每个都要单独跑一遍,并且要检查 Agent 的反应是否符合预期——既不能对非法请求执行操作,也不能给用户返回没头没尾的技术错误。
4.4 当 Skill 数量膨胀后怎么办
最后补充一个组织层面的问题。当你的 Skill 数量从几个涨到几十个之后,会出现一个新的挑战:模型在几十个工具里做选择,准确率会明显下降,有时候会“眼花”——明明该用 A 工具,却选了名字相似的 B 工具。
我目前用的一套解决办法是“Skill 分组路由”:不把几十个工具一次性全部暴露给模型,而是先根据用户意图做一次粗分类,只把当前分类下的那组 Skill 注入模型上下文。比如用户表达的是“我要退货”,我就只注入“售后处理组”的 4 ~ 5 个 Skill,其余组的技能平时根本不出现在模型的可见范围内。
这套方式完美继承了模块化的思想,也极大降低了模型的“选择困难”。不过有一个前提必须做到:对用户意图的粗分类要足够准。分类错了,后面负荷再大都没用。所以我的实现里,粗分类这一步不是让模型自己判断,而是用一个更快、更窄的分类规则引擎先做一轮过滤,分类的粒度不需要细,只需要能大致对到几大类场景即可。旁边放一个低置信度兜底,如果分类引擎的置信度不够高,就把全部技能暴露给模型,让模型自己挑。
5. (结尾)一些踩过坑之后沉淀的原则
这套 Skills 工程工作流,我在两个不同的项目里完整落地过,反复调整了很多轮,沉淀出几条在配置时最值得坚持的原则:
第一,不确定的事让模型判断,确定的事一律写死。流程编排的价值就在于此。模型的优势是理解模糊意图,而不是执行确定性逻辑。你把一个“金额大于 500 就走人工”的规则交给模型判断,就是在透支可靠性;把它写进流程定义,就是免费的确定性。
第二,工具描述是代码,值得被 Review 和测试。我见过太多团队把工具描述当注释随便写写,结果线上模型行为飘忽不定。描述里每一个词都在影响模型的行为,参数约束、负向约束、返回结构、边界情况,都要像对待对外 API 文档一样认真对待。我甚至会把工具描述拿给团队里另一个同事看,问问他看完能不能准确说出“什么时候该用这个工具、什么时候不该用”——如果他说不清楚,模型大概率也说不清楚。
第三,永远安排人工兜底。不管你的流程多完善,工具多丰富,Agent 一定会在某个角落碰到它理解不了的任务,或者走到规则边界之外。这不是失败,而是工程边界。把边界内的自动化和边界外的人工接管配合好,整个系统才真的像一个工程化的产品,而不是一个会把自己绕进去的 demo。
第四,回归验证是底线。每次改配置都是引入风险的过程,不做回放对比就是盲改。把历史会话变成回归测试集,让每次改动都有数据说话,这个习惯能帮你免去绝大多数线上事故。
所以如果你正准备给 Agent 配一套细致好用的 Skills,我建议你就从“能力清单 → 工具 Schema → 流程编排 → 状态管理 → 回归验证”这条主线往下走。一开始不用贪多,先拿一个高频场景完整跑下来,把这套链路摸熟,再横向复制到其他场景。比起一上来就追求大而全的技能矩阵,把一个场景做深做稳,才是把 Agent 推向工程化的正确姿势。