最近好几个做Agent开发的同行都跑来问我同一个问题:明明给模型配了不少Skills,为什么它还是该动手时不动手,不该动时瞎指挥?我看了他们的代码发现,问题基本不在模型,而在于那套Skills根本不像一个工程产物,倒更像是往提示词里塞了一堆功能说明书。
标题里说“给AI Agent配一套工程工作流”,这句话的真正含义不是让你多写几个函数、多挂几个插件,而是把Skill当成一个需要设计、开发、测试、监控、迭代的完整模块来对待。我在这篇文章里不会只讲概念,会把我在实际项目里落过地的设计方法、实现细节、调试手段、测试思路和踩坑记录全部抛出来,适合正在做Agent应用开发、被模型乱调用工具折磨过的开发者参考。
1. Skills到底是个什么东西,先把它和看似很像的东西撇清
1.1 Skills不是高级提示词,也不是传统意义上的工具调用
很多人理解Skills时,容易把它和三项东西混为一谈:Function Calling、Prompt模板、RAG。先说说它们的关系。
Function Calling是模型的一种输出能力,它让模型在对话中决定“我需要调哪个函数、传什么参数”,本质上是模型与外部系统之间的“握手协议”。Skills是在这个协议之上封装出来的、面向特定场景的完整能力单元。举个例子,一个查天气的Function只是告诉你“传入城市名就返回天气数据”,而一个“天气查询Skill”则可能包含:确认城市、处理模糊地名、判断是查实时还是查预报、对极端天气做额外提醒、输出格式规范。这些都是Function本身不负责的事情。
Prompt模板更不用说,它只是一段文字。Skill里面虽然也可以包含Prompt模板,但它更多是执行逻辑的载体之一。RAG则是给模型提供被动检索的知识背景,而Skill是让模型去做事、去操作、去执行。
我用一句话总结自己的理解:Skills是给模型配的“可复用行为模块”,它必须包含触发条件、执行逻辑、输入输出约束和失败处理,缺一个都不算工程化。
1.2 散装Skills和工程化Skills的差别,就差在生命周期上
我在某个跨平台系统项目里第一次尝试给Agent加技能时,做法非常原始:写了十几个Python函数,然后在系统提示词里把函数说明一股脑塞进去,让模型“看着用”。第一轮效果还可以,但越往后越失控——模型开始把不相关的技能也编进回答里,参数缺东少西,错误信息直接抛给用户。
后来我重新梳理了一套工程化方案,把每个Skill当成一个独立交付物来管理:有版本、有元信息、有测试用例、有监控日志。同样是这些功能,效果完全不一样。
这里做一张对比表,方便你直观看清差别:
| 对比维度 | 散装Skills | 工程化Skills |
|---|---|---|
| 触发方式 | 靠模型自觉 | 靠元信息+上下文约束+校验兜底 |
| 参数质量 | 模型随便编 | Schema校验+默认值+重试机制 |
| 失败处理 | 直接抛异常 | 有降级策略和用户可读的错误信息 |
| 调试手段 | 看完整对话日志 | 单技能独立日志+调用链追踪 |
| 迭代周期 | 改提示词碰运气 | 改版本跑测试集看回归 |
| 可复用性 | 换项目重写 | 包成包/目录,直接搬家 |
这张表基本就是我踩坑总结的核心。工程化的本质不是“写得规范”,而是让每个Skill从出生到退役都有明确的轨迹。
1.3 一个工程化Skill的标准组成
我在项目里规定的Skill结构包含了六个部分:
- 元信息:技能名称、版本、负责人、一句话描述、适用场景、不适用场景。
- 触发条件:什么情况下模型应当考虑调用它,什么情况下绝对不要调用。
- 输入Schema:每个参数的名称、类型、取值范围、是否必填、示例值。
- 执行逻辑:可以是代码函数,可以是一段多步Prompt,也可以是内部再编排其他Skill的指挥逻辑。
- 输出规范:返回值结构、错误码、给用户呈现的内容格式。
- 测试用例:至少3个以上正向、负向、边界测试样例。
这个结构看着不复杂,但80%的团队连一半都没做全。尤其“不适用场景”这一项,几乎没人写。我建议你从下一个Skill开始一定加上,它能救你无数次——模型太容易在“有点像但根本不是”的情况下乱调技能了。
2. 动手写Skill前,最关键的是先把能力地图画出来
2.1 别急着写功能,先顺着用户旅程倒推技能清单
每次我接手一个新Agent项目,第一件事不是看代码,而是把使用流程从头到尾走一遍,把“用户在哪个环节需要Agent做什么”列成清单。
假设我在做一个面向售后客服的Agent。用户旅程大概是:描述问题、查订单、判断责任、出解决方案、跟进结果。光这一条线就能拆出十几个潜在技能点,比如订单查询、退换货政策查询、物流信息追踪、赔偿计算、工单创建、人工转接条件判断、话术生成等。
拆完之后我会做一次筛选:哪些是Agent必须亲自动手的,哪些只是需要查询资料给出答案的,哪些直接交给规则引擎就行的。筛选标准很简单——如果这个动作不需要上下文感知、不需要多步推理,那就别做成Skill,做成普通接口调用反而更稳。
2.2 技能描述真的不是写得越多越好
我见过有人把一个Skill的描述写到四五百字,把模型的内部逻辑都写上去了,结果该触发的时候不触发。模型对描述的理解能力并没有你想象的那么强,关键是让它“一眼认出来”。
一套我在项目中反复打磨后的描述范式是这样:
- 第一句:一句话说清这个技能是干什么的,直截了当。
- 第二句:说明调用它最典型的前置场景,用用户原话风格表述。
- 第三句:明确什么情况下不能调它,可以列举一两个反例。
- 然后给一个“假设你正在处理XXX场景,请调用此技能”的示例对话片段。
这里用“订单查询Skill”做示范:
description: 查询用户订单的最新状态,包括待付款、待发货、运输中、已完成状态。当用户询问自己订单走到哪一步、何时发货、物流到哪了时使用。如果用户没有提供订单号且通过对话无法确认身份,请先引导说“我需要先确认你的订单信息”,不要尝试用猜测的订单号查询。
你看,“先引导确认”这种话放进去后,实际触发准确率明显上升了,因为模型有了明确的行为指引,而不是模棱两可的自由发挥。
2.3 技能粒度要拆到“原子操作”这个级别,编排交给更上层的Skill
在另一个做内容创作助手的项目里,我一开始把“生成一篇文章”做成了一个巨型Skill,里面包含了选题分析、大纲生成、段落撰写、配图建议、SEO优化。听起来很完整,实际跑起来灾难级——模型在“写正文”环节反复触发“选题分析”,上下文全部跑偏。
后来我学乖了,把Skill拆成两层:底层是一批原子技能,比如“生成大纲”“写开头段落”“生成标题方案”“提取关键词”;上层再做一个编排型Skill,它的本职工作不是执行具体内容,而是决定“当前这个任务需要按什么顺序调用哪些底层技能”。这种分层结构有几个明显好处:单个技能职责单一,模型不容易搞混;出故障时定位快;底层技能可以复用到别的编排策略里。
粒度判断标准我个人总结得很简单:如果这个技能里还有“如果情况是A就做X,情况是B就做Y”这类分支,那它就不够原子,继续拆。
2.4 技能之间的依赖关系必须显式声明,不然协作就是靠运气
我最初做技能编排时压根没考虑过依赖问题,后来出现一个让人哭笑不得的bug:Agent在“查询库存”之前先调用了“生成订单确认文案”,给用户发了一堆虚构的确认信息。这个问题的根源是模型不知道技能之间的先后约束。
解决方案也不复杂:在编排型Skill的元信息里增加“依赖声明”字段,写明它执行前需要哪些前置技能的结果完成,执行过程中如果某些中间技能失败应该中止还是降级。同时在代码层面做了依赖检查,如果前置技能没有正确返回,就直接拦截调用,不让它往下走。
提示:如果你发现模型经常把技能调用顺序搞乱,不要只想着优化提示词,先去检查有没有显式的依赖声明。依赖关系是代码逻辑层面的约束,不该全靠模型的“常识”。
3. Skill的构建与调试全流程实操
3.1 一套清晰可复制的目录结构,是工程化的地基
我的个人项目里现在统一用这套结构,复制到新项目基本不用改:
skills/ ├── order_query/ # 技能目录名,建议全小写下划线 │ ├── SKILL.md # 技能元信息+描述+触发条件 │ ├── schema.json # 输入输出参数定义 │ ├── execute.py # 执行逻辑入口 │ ├── prompts/ # 需要用到的提示词模板 │ ├── tests/ # 测试用例集 │ └── examples/ # 少样本示例 ├── order_refund/ └── README.md这个目录结构本身没有多深奥,但它把“描述”“参数”“实现”“测试”“示例”这五要素物理隔离了。改描述不用动代码,跑测试不用碰线上逻辑。
3.2 元信息(SKILL.md)的写法模板
我写SKILL.md有一套固定模板,每次新建技能都是复制改字段,这里直接放出来:
--- name: order_query version: 1.3.0 description: | 查询用户订单的最新状态。 当用户询问订单进度、发货时间、物流位置时使用。 如果用户未登录或无法提供订单号,请先引导用户提供必要信息,禁止猜测。 trigger: | 用户表达了对订单状态的查询意图。 用户提供了订单号或已明确身份。 not_trigger: | 用户只是提到订单但没有问状态。 用户在投诉物流,但没有查询意图。 $schema: "./schema.json" --- # 执行说明补充 在调用订单接口前,先根据上下文提取订单号。 若订单号缺失,输出“请提供订单号”的引导话术。这个模板里我最想强调的是trigger和not_trigger。在真实项目里,not_trigger写清楚后,误调用率至少降一半。有一次我们把某技能的not_trigger里补了一句“用户只是在调侃要投诉,不是真要投诉”,之后这个技能在闲聊场景下的触发次数骤降。
3.3 实现逻辑的三种形态,按场景选型
执行逻辑不只有“写代码”一种做法,我一般根据技能类型分三种形态:
第一种是代码直调型:适合确定性操作,比如查数据库、调用第三方接口,返回结构固定。注意点是要做参数清洗,模型传来的参数经常是字符串带多余空格,或者把“今天”这种相对时间直接传进来,前置解析很有必要。
第二种是提示词执行型:适合没有固定逻辑、但规则清晰的任务,比如“给一段文本打分”“判断退货是否合理”。核心在提示词模板里把评价维度、分数范围、输出JSON格式写死。
第三种是编排指挥型:适合复杂任务,它不直接干活,而是分析任务、拆解步骤、调用底层原子技能、汇总结果。这种实现里要特别注意每一步结果的传递,我一般会在内部维护一个简单的上下文状态对象,让每一步技能结果写入其中,而不是靠把前面所有输出重新塞给模型去理解。
3.4 本地调试的三种实用姿势,别只靠端到端跑对话
调试Skills最大的误区就是每次改动都跑一遍完整的Agent对话。那样效率太低,而且看不到问题在哪一层。
我实际使用中效率最高的三种调试方式:
- 单技能离线验证:写一个小脚本,手动模拟模型传入的参数,直接调用execute.py,查看返回值是否符合schema。这样调试不经过模型,定位逻辑bug非常快。
- 回放式调试:把真实对话日志里模型生成的“调用参数”原样提取出来,重新执行技能,看是否复现问题。这个方式专门解决“线上复现不了”的烦恼。
- 上下文模拟调试:为编排型技能构造一段模拟的对话上下文,里面故意设置一些边界条件,比如参数不全、用户改口、中间技能失败,检验技能的容错能力。
这三种方式我都写成了命令行工具,统一入口是python debug_skill.py --name order_query --case 001。整个调试链路不依赖大模型API,日常开发调试基本不烧token。
4. 测试与评估:没有指标的Skills迭代全是玄学
4.1 先定指标:你用什么标准判断一个Skill是好是坏
我自己常用的评估维度是六个:
| 指标 | 计算方式 | 我的合格线 |
|---|---|---|
| 触发准确率 | 该触发时触发+不该触发时不触发的总数/测试轮次数 | ≥90% |
| 参数完整率 | 正确填写全部必填参数的轮次/总触发轮次 | ≥95% |
| 执行成功率 | 返回正确的执行结果,无异常无错误码的轮次/总轮次 | ≥95% |
| 结果相关率 | 返回内容与用户问句相关的轮次/总轮次 | ≥90% |
| 平均响应延迟 | 技能执行耗时(不含模型对话耗时) | P95<800ms |
| 上下文污染率 | 执行后导致后续对话出现无关内容的轮次/总轮次 | <5% |
前三个指标是最基础的,后三个是我被现实教育之后加的。上下文污染率尤其重要——一个技能执行之后把之前的话题带跑,这种问题只跑单技能测试完全发现不了,必须放回完整对话里评估。
4.2 测试集怎么构造:别光拿标准样例测,要拿用户吵架记录测
我建测试集的一个原则:真实对话永远是测试集最大的来源。每次跑线上Agent,我都会把调用日志存下来,定期抽一批做标注,把“触发对/错、参数全/缺、结果好/坏”打标放进回归集。
除了真实数据,我也会故意构造三类刁钻样例:
- 措辞变体类:用户不会按教科书说话,你得测“帮我瞅瞅快递到哪了”“那个订单怎么样了”这种野路子表述。
- 相似意图混淆类:把两个描述相近的技能放在一起,故意用一些两边都能沾边的句子测模型会不会选错。
- 边界参数类:缺参数、超长文本、生僻实体名、表情符号混合,这种输入最容易崩。
测试集不用一开始就做很大,我的建议是每新增一个Skill,至少配10个正向、5个负向、5个边界样例。跑完一轮标识成基线,后续每一次改版本都要全量回归。
4.3 回归测试怎么落地:哪怕只是改了一个标点
分享一个真实教训:有一次我犯懒,觉得某个Skill的描述只改了一个词,不会影响任何东西,没跑回归直接上线。结果当天误触发率翻了三倍,原因是我改的那个词刚好让模型把另一个相似的技能给“吞并”了,非常魔幻。
从那以后我定了死规矩:任何改动,哪怕只是删个空行,都要跑回归测试集。回归测试的逻辑写成脚本,触发和参数校验全自动。只要触发准确率或参数完整率掉超过2个百分点,这次改动就要被标记为风险变更,不能自动合并。
跑回归这套动作听起来重,其实一次性搭好之后,每次跑只需要几分钟,对比起来特别值。
4.4 可观测性:不记录调用日志的Skills等于活在黑盒里
我给每个Skill接入了一套调用日志方案,记录的内容包括:
- 触发时的用户原始问题。
- 模型生成的完整调用参数。
- 执行过程中的关键步骤耗时。
- 返回值前200个字符摘要。
- 错误堆栈和降级动作。
- 最终结果标识。
这套日志的核心目的是输出一张“调用追踪列表”,每个技能每次调用都能定位到具体输入输出和出错位置。如果某天线上出了问题,我可以直接搜索日志回溯完整链路,而不是对着对话记录猜。
注意:给调用日志加采样率是非常糟糕的做法。Agent的问题是低频偶发的,你砍掉50%的日志,出问题时排查成本直接翻倍。宁愿存得久一点,也别省这个量。
5. 落地后的效果与我踩过的几个典型坑
5.1 工程化改造前后的真实效果对比
我在某跨平台系统项目里做的这套改造,前后对比数据非常有说服力。改造前模型真是“自由发挥”,改造后完全不一样:
| 指标 | 改造前 | 改造后 |
|---|---|---|
| 技能误触发率 | 每周约25次 | 每周约3次 |
| 参数完整率 | 78% | 96% |
| 用户投诉中“答非所问”类占比 | 41% | 11% |
| 单个功能点从改描述到上线耗时 | 半天以上 | 30分钟 |
这个数据不是我吹出来的,是线上真实统计。最让人欣慰的是线上报错日志大幅度变少了,很多以前需要人工盯着的异常现在能自动降级,用户感知不明显。
5.2 高频问题速查表:遇到症状直接对照找对策
我在维护多个Skill之后,整理了一张速查表,你遇到类似问题可以直接按图索骥:
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 模型该调用Skill时不调用 | 描述太混乱,触发条件写得抽象 | 精简trigger,添加典型示例 |
| 模型不该调用时偏要调用 | 缺not_trigger,与其他Skill相似度太高 | 明确负例场景,与相似Skill做区分测试 |
| 参数经常传错或缺漏 | Schema定义不清晰,未提供示例值 | 每个参数写示例,设置必填校验 |
| 调用顺序错乱 | 依赖关系没声明,编排逻辑薄弱 | 增加依赖声明,加编排层拦截 |
| 技能执行时间太长 | 实现逻辑里包含大段LLM递归 | 精简单步逻辑,增加超时熔断 |
| 执行结果与问题相关性差 | 返回内容未做后处理 | 增加输出规范和后处理清洗 |
| 改了描述后效果严重波动 | 缺少回归测试防线 | 恢复上一个版本,跑测试集找出差异 |
这张表不是一次性写出来的,基本是四五个月踩坑记录的浓缩。每次遇到新问题我都会补一行进去,现在它已经成了团队内部的标准排查手册。
5.3 三个有代表性的踩坑实录
第一个坑是把“超时处理”丢给模型自己编。早期某个查询类Skill接口偶尔会变慢,模型在超时后竟然自己虚构了一个失败的订单状态告诉用户“订单异常”。这个问题的根源是技能实现里没做超时降级,接口一慢模型只能瞎编。后来加了固定超时和统一降级文案,问题直接消失。
第二个坑是过度优化描述导致技能“狭窄化”。有一次为了让某个技能的触发准确率达到100%,我把描述写满了各种边界条件,结果稍微换个说法用户意图就触发不了。后来我才意识到回归测试不只要测“别乱触发”,还得测“该触发的一定要触发”,两者平衡才是正确方向。
第三个坑是拼技能时把上下文状态封装得太死。做编排型Skill时,我把每一步的中间结果都存进了内部状态对象,但那层状态跟主对话上下文是割裂的,导致一个技能要参考上一步结果时拿不到。后来我把内部状态对象和对话上下文做了双向同步,这个问题才算解决。这提醒我,编排的本质不是把信息藏起来,而是让信息在该出现的地方流动起来。
6. 后续还可以怎么扩展这套工作流
按我个人经验,如果你已经把前面这套流程跑顺了,接下来可以往三个方向延伸。
第一个方向是给Skill做版本间的自动化对比评测。把每次变更前后的Skill跑同一批测试集,自动生成指标差异报告。这样新人和老人写出来的改动都能用一个标准衡量,不用依赖经验判断。
第二个方向是沉淀一套“技能设计规范文档”。把描述怎么写、参数怎么定义、测试集怎么构造这些规则变成团队README,新成员照着文档走一遍就能独立交付合格技能,不用你反复口头讲解。
第三个方向是把单个Skill的调用链做成真正的可观测产品。我现在已经能基于日志生成完整的调用图谱,是在普通日志基础上做的聚合追查。每个技能被谁触发、经过哪些步骤、结果去向何处,一图看全,排查问题的时间大幅缩短。这个方向的技术含量不低,但带来的工程价值非常可观。
这套工作流不是写完就完事,它得跟着线上效果持续迭代。从我自己的体会来说,给AI Agent配Skills,本质上是在教一个“很聪明但没常识的实习生”按流程办事。你前期多花的时间,后期都会从调试里省回来。希望我上面这些整理,能让你少走几段弯路。