ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent 技能工程化实践:Skill 拆分、测试与回归全指南

AI Agent 技能工程化实践:Skill 拆分、测试与回归全指南

做 AI Agent 的团队应该都有体会:写一个能跑通 Demo 的 Agent 很容易,但把它放进真实的工程环境、让它可以被维护、被测试、被团队其他人接手,完全是另一回事。这个Skills项目的初衷,就是想把 Agent 里的各种“能力单元”从散落的提示词和函数调用里抽出来,变成一套有目录、有规范、有验证、能迭代的工程资产。

这篇文章会拆解我在项目里踩过的坑和沉淀下来的打法,包括 Skill 的目录设计、输入输出约束、测试与回归、版本控制,以及最常见的失败模式。适合正在做 Agent 应用、或者想把原型系统往生产环境推的工程师参考,不是理论科普,是能直接拿去用的经验。

1. 先说清楚:Agent 里的 Skill 到底是什么

1.1 为什么不能继续用“一个大提示词 + 一堆函数”

早期做 Agent 应用,大家都习惯把所有指令塞进一个 system prompt,再把工具列表一股脑丢给模型。这个方案在最开始确实跑得通,因为场景单一、工具数量少,模型就算偶尔犯糊涂,人工兜底也能接受。

但项目业务一复杂,问题就接踵而至:

  • prompt 超过一定长度后,模型对指令的执行准确率明显下降,尤其是靠后的指令。
  • 工具函数之间没有清晰的语义边界,模型经常会选错工具,或者在同一个场景里反复调用不同工具。
  • 出了问题很难排查——你不知道模型是在哪一步做错了决定,因为整个调用链没有任何中间产物。

Skill 本质上是把“模型做某件事的能力”封装成独立单元。每个 Skill 有自己的说明文档、输入输出格式、执行逻辑和验证条件。模型不再直接面对一个巨大的工具列表,而是面对一组有清晰边界的技能模块,按需要激活和调用。

拿我自己的例子来说,之前做一个客服工单分类系统,老方案是一个超长 prompt + 6 个分类函数。分类准确率在测试集上看着还行,但一上真实数据就频繁出现离谱的分类结果。后来我把分类逻辑拆成独立的 Skill,每个 Skill 独立维护一份分类规则和边界条件的说明,准确率立马上了一个台阶。原因很简单,模型在单一上下文里处理的信息越聚焦,决策就越稳。

1.2 Skill 与普通工具函数的区别

很多人会把 Skill 理解成“带说明文档的工具函数”,但它们在工程上的差别非常大:

  • 普通工具函数是确定性的,输入合法就返回结果,不合法就报错。
  • Skill 内部可能包含多次模型调用,可能包含多个工具函数的组合,甚至可能包含对其他 Skill 的调用。

也就是说,Skill 不再是“一个函数”,而是一条可以自包含的工作流单元。它自己有输入、有输出、有内部状态、有终止条件。

举个实际的例子。在我最近做的一个数据报表 Agent 项目里,我需要 Agent 能够“按自然语言查询生成图表”。这个能力如果做成普通工具函数,你需要把自然语言转 SQL、SQL 执行、结果转图表配置全塞进一个函数里,代码极度耦合。但如果拆成三个 Skill:

  • 语义解析 Skill:把自然语言转成结构化查询意图。
  • 数据查询 Skill:执行查询,返回统一格式的数据集。
  • 图表生成 Skill:根据数据集和用户偏好生成图表配置。

每个 Skill 可以单独测试、单独替换、单独调优。这就是工程工作流的意义——不是让 Agent 变得更强更聪明,而是让它的能力变得可拆解、可演进。

1.3 Skills 项目要解决的四个工程问题

这个项目在实际落地过程中,我一直围绕四个问题在转:

第一,可发现性。Agent 怎么知道在什么场景下该用哪个 Skill?如果全靠模型从长列表里猜,那必然会出现选择困难。我们需要一段精简的说明索引,让模型在极短时间内判断是否激活某一个 Skill。

第二,可测试性。Skill 内部的每一条路径都要能被单独触发和验证。不能等到整个 Agent 跑挂了之后才去排查。

第三,可观测性。每次 Skill 调用的输入、输出、中间决策路径都要留痕,否则后续想优化都不知道改哪里。

第四,可演进性。改了一个 Skill,不能影响其他 Skill 的行为。换句话说,Skill 之间要尽量解耦,最好能支持独立的上线回滚。

这四个问题,本质上就是工程化改造的标准命题。只不过这一次,改造对象从传统服务变成了一个充满不可预测性的 AI 系统。

2. Skill 的目录结构与设计规范

2.1 推荐目录结构

在实际项目里,我们最后采用的目录结构长这样:

skills/ ├── manifest.yaml ├── query_understand/ │ ├── SKILL.md │ ├── schema.json │ ├── examples/ │ │ ├── valid_cases.md │ │ └── edge_cases.md │ ├── validator.py │ └── prompt.py ├── chart_render/ │ ├── SKILL.md │ ├── schema.json │ ├── examples/ │ └── validator.py └── share/ ├── common_types.py └── utils.py

这个结构看起来简单,但每一层都有讲究。

manifest.yaml是全局索引,声明所有可用的 Skill 以及它们的依赖关系。模型在启动时会先读这个文件来建立全局认知。它的核心作用是把 Skill 列表压缩成一份轻量的路由表,避免模型把每个 Skill 的详细文档都塞进上下文。

每个 Skill 目录里,SKILL.md是给模型看的,不是给人看的。它写清楚这个 Skill 擅长什么、不擅长什么、在什么情况下应该切换到其他 Skill。这部分的语气和格式要反复打磨到“一读就能懂”。

schema.json定义这个 Skill 的输入输出格式。它承担的是工程约束,在下游代码里会被拿来直接做校验。

validator.py是我们自己的校验逻辑。模型输出经常不严格遵循 JSON schema,光靠声明式校验不够,还得有代码层兜底。

examples/目录放正反案例。正面案例让模型知道正确路径长什么样,负面案例让它知道怎么避免踩坑。

2.2 SKILL.md 的写作规范

SKILL.md 的写法决定了 Skill 的使用率。我见过很多把 SKILL.md 写成产品需求文档的团队,写了几百行,模型根本读不进去。

一份好用的 SKILL.md 应该控制在 80 到 120 行,包含四个模块:

  • What:这个 Skill 做什么,一句话讲清楚。
  • When:什么时候该用它,什么场景不该用它。这个模块最重要,能极大降低模型选错 Skill 的概率。
  • How:操作步骤,但只写关键步骤,不写详细逻辑。
  • Output:输出格式和典型的返回值例子。

我举个例子,在报表数据查询这个 Skill 里,when 部分的写法是:

## When to use - 用户请求中包含明确的数据字段,比如"销售额""用户数""订单量"。 - 用户指定了时间范围、地域、渠道等筛选条件。 - 问题答案需要通过数据库查询获得,不能通过常识推理得到。 ## When NOT to use - 用户只是在询问图表样式,没有涉及具体数据。 - 数据来源是用户上传的临时文件,而不是数据仓库。 - 查询逻辑涉及跨多个数据源的复杂 join,此时应交给 data_service_skill。

写清楚 When NOT to use 比写 When to use 更能提升准确性。模型是概率决策,明确的负面边界能有效抑制乱用。

2.3 Schema 设计的三大原则

Skill 的输入输出 Schema 和普通 API 的 Schema 看起来差不多,但设计逻辑有差异。

第一个原则是输入必须有冗余信息。模型对隐式信息的理解不稳定,所以我们在输入里会同时放“用户原始描述”和“结构化解析结果”。一旦结构化结果解析错了,模型还有机会从原始描述里发现异常。

第二个原则是输出必须分隔人与机器路径。有些值是需要直接返回给用户的,有些是给下游 Skill 用的。混在一起的话,上游 Skill 的输出格式改了,下游直接崩。我们会在 Schema 里用summary字段存给用户看的内容,用data字段存结构化数据。

第三个原则是必须提供默认值或空值策略。模型不会每次都给齐所有字段。比如查询销售数据时用户没说时间范围,Schema 里就要定义默认是最近一个月还是全量。没有明确默认值的 Schema 设计,是 Agent 系统里大量运行异常的源头。

Schema 设计完了之后,要让 validator 在每一层跑一遍。模型输出必须经过校验才能传递到下一层,凡是校验不过的,宁可让这个 Skill 返回一个“能力不可用”的结果,也不能把脏数据往下游传。

2.4 原子 Skill 与复合 Skill

把 Skill 分为原子和复合两层,是我在这个项目里做得最正确的一个决定。

原子 Skill 做单一一件事,比如“解析自然语言日期”“计算两个时间段重叠天数”“把金额单位统一换算”。它们内部不调用其他 Skill,模型直接面向它们做最终决策。

复合 Skill 负责编排,内部会调用多个原子 Skill,并按一定顺序组织流程。比如“生成月度销售报表”这个复合 Skill 内部会依次调用数据查询、缺失值补全、图表生成三个原子 Skill。

这个分层最大的好处是测试非常干净。原子 Skill 出问题,范围一定很小;复合 Skill 出问题,大概率是编排逻辑不对,而不是底层的某一个能力失效。

在工程落地中,我强烈建议团队把 80% 的精力花在打磨原子 Skill 上。复合 Skill 的流程可以靠改配置调整,原子 Skill 的质量却决定了一个流程的天花板。

3. 实操环节:把一个工作流拆成可编排的 Skill 集合

3.1 从业务流到 Skill 图的拆解方法

这里分享一个比较好用的拆解法,我一般叫它“输入输出追踪法”。

第一步,写出当前的业务流程,把每一步的输入输出都列出来,哪怕这一步目前是人做的。比如做一个工单摘要工作流,流程就是:

  • 接收工单原始文本
  • 提取关键信息,包括客户、产品、问题类型、紧急程度
  • 检索历史工单
  • 生成摘要

第二步,对每一步问三个问题:这一步有几种输入类型?输出会被谁消费?如果这一步暂时做不了,有没有替代方案?

第三步,把依赖关系画成一张无环图,找到可以被独立封装的节点。凡是输入输出边界清晰、可以被其他流程复用的节点,就拆成原子 Skill。

以工单摘要为例,“提取关键信息”和“检索历史工单”都没有依赖前续步骤,边界又非常明确,就应该独立成 Skill。而“生成摘要”依赖前两个步骤的输出,但它的输出格式又非常稳定,所以适合作为复合 Skill 对外暴露。

3.2 一个完整的 Skill 实现样例

拿数据查询这个 Skill 举例,它的SKILL.md核心部分长这样:

--- name: data_query description: 根据结构化查询条件,从数据仓库读取数据并返回统一格式的数据集 input: table: string metrics: array dimensions: array filters: object time_range: object output: columns: array rows: array row_count: integer --- ## When to use - 收到来自 query_understand_skill 的结构化查询意图。 - 用户直接提供了完整的表名和字段名。 ## When NOT to use - 查询目标不是数据仓库,而是外部 API。 - 需要对多个表做复杂 join,应交给 data_service_skill。 ## Steps 1. 根据 input.table 检查表是否存在,不存在则返回 TABLE_NOT_FOUND。 2. 根据 input.metrics 和 input.dimensions 拼接查询字段,注意 metrics 必须配合聚合函数。 3. 将 input.filters 的对象结构转换成 SQL WHERE 子句,注意空值字段。 4. 执行查询并限制最大返回行数,默认 1000。 5. 返回统一格式的数据集。

对应的validator.py里会做一个重要检查:确保查询结果里的字段和采集时的字段完全一致。这个检查能在第一时间发现模型拼接查询字段时的幻觉。

实际跑主干流程的时候,我发现一个反复出现的问题:模型在拼接metrics和dimensions时经常混淆,比如把本应做聚合计算的sum(sales_amount)写成一个普通字段。为了堵住这个坑,我调整了 schema,在输入里明确标注每个字段的语义类型,并且给每个 metrics 字段强制配一个聚合函数模板。改完之后,错误率直接从原来的百分之二三十降到了个位数。

3.3 如何设置 Skill 的调用边界与退出条件

很多人会忽略:Skill 内部也必须有状态机和退出条件,否则 Agent 很容易在一个 Skill 内部死循环。

我在validator.py里总会写一个最大重试次数,比如同一个查询动作重试三次仍返回异常,就放弃本轮调用,返回一个降级结果,而不是无限循环。

另一个边界是上下文隔离。复合 Skill 内部调用原子 Skill 时,要避免把所有中间结果都堆在上下文里继续传。中间结果该落盘就落盘,传给下一步的只保留 Schema 里声明的字段。这个操作能大幅减少模型“读到太多无关内容”造成的上下文污染。

做一个形象的类比,Skill 调用链如果设计得不好,就像一个没有任何人在中途把关的工厂流水线,上一环节的废料直接流到下一环节的加工台,到最后一环时,废料已经堆满了产线。合理的退出条件和上下文隔离,实际上是在每个环节设置了质检点。

3.4 本地调试工具:用回放代替真实调用

工程工作流里面一个非常提升效能的工具是回放日志。我们把每次生产环境中的 Skill 调用日志完整记录下来,存到本地。调试新版本 Skill 时,直接拿历史真实输入跑新的代码,对比输出差异。

这个方案比你自己编造测试用例强得多。因为真实用户的输入分布很难靠手工模拟出来。回放既有正常场景,也有各种出人意料的边界情况。

我自己的切身感受是,回放调试大概能帮你项目上线时少花至少一半的时间在做测试数据上,而且测试质量还更高。它不是用来替代测试集,而是作为回归测试的一个子集存在。

4. 常见问题与排查技巧实录

4.1 模型不按 Schema 输出怎么办

基本上每个人都会遇到这个问题。我的经验是分两层治理。

第一层是声明层,也就是把输出格式要求写进 prompt,这是基础但远远不够。

第二层是代码层,validator 对所有输出做防御性解析。不能只依赖模型遵守 prompt,要能识别偏离并自动修复。

自动修复的策略通常有三步:

  • 尝试把输出强转成目标类型,比如字符串数字转 int、时间字符串转日期对象。
  • 尝试从输出里做字段映射,比如模型把user_name写成了username,用常见别名表兜底。
  • 尝试裁剪。当模型在前面的总结文字里混入了 JSON,就提取 json 块的那一段再解析。

如果在代码层修复不了,就让这个 Skill 返回一个固定的OUTPUT_VALIDATION_FAILED状态,由上层流程决定是重试还是降级。

4.2 Skill 之间的冲突怎么处理

多个 Skill 之间出现边界重叠是很正常的。比如“图表生成”和“表格导出”都有可能处理用户“把数据给我”的诉求。

冲突处理的核心是优先级规则。我会在 manifest.yaml 里声明 Skill 的优先级,并且要求模型优先选择描述更精确、匹配度更高的 Skill。

同时会在 SKILL.md 的 when 部分增加反例来降低误命中率。比如图表生成 Skill 里明确写一句“当用户要求导出原始数据表格时,不要使用本 Skill,应切换到数据导出 Skill”。

如果两个 Skill 经常冲突,除了调整说明之外,还可以考虑检查模型路由逻辑里是不是缺少一个前置的意图分类。有时候问题不在 Skill 内部,而在 Agent 的调度层。

4.3 Schema 变更导致历史 Skill 失效

这是一个隐蔽的坑。线上 Skill 的 schema 变更,会导致旧版本 Agent 实例调出的数据和新版本校验器不匹配,从而返回大量异常。

我们项目里的解法是把 schema 也纳入版本管理,并在 manifest.yaml 里记录每个 Skill 版本依赖的 schema 版本号。部署上线时做自动比对,不一致就不允许滚动。

从工程角度看,schema 版本管理可以复用传统 API 兼容性的成熟经验。任何对外暴露的 Skill 变更 schema 时,必须先考虑向下兼容,无法兼容的就必须走新版本名,比如data_query_v2,而不是原地修改。

4.4 模型在 Skill 调用链中迷路

一个非常典型的错误行为是:Agent 在复合 Skill 内部已经拿到了正确的结果,但因为最后的回答阶段缺少明确的指令,又去额外调用了一个无关的 Skill,生成了一个错误的补充说明。

排查这种问题时,靠日志是最好的方式。我会在关键节点插入 log,记录每个 Skill 的激活时间、输入来源和输出去向,再用日志回溯现场,看模型的决策链是从哪一步开始跑偏的。

治本的方案通常是优化 prompt 的结束条件。明确告诉模型:当达到目标状态时,直接停止调用任何 Skill,进入最终回答。表达能力强的模型一旦把这个条件内化,迷路率会明显下降。

4.5 性能与成本的平衡

Skill 拆细了之后,调用链路变长,模型调用次数变多,延迟和成本都会上升。这里面有一个非常值得重视的取舍问题。

我的经验是:不是所有步骤都需要大模型。原子 Skill 当中很多步骤其实是规则逻辑,比如单位换算、时间归一化,这些完全可以用传统代码实现,不必过模型。

另一个值得考虑的方向是,常见路径可以走缓存。比如相同用户、相同查询意图的请求,短时间内可以复用上一次的结果。对于查询类 Skill,缓存命中率往往能到百分之三四十,能省下一大笔模型调用成本。

如果某个复合 Skill 频繁出现在高延迟链路里,可以考虑把这个链路整体固化成一个专用策略,跳过部分中间 Skill 的模型调用。核心思路始终是:模型调用是资产,也是成本,精确地用在必要的地方才划算。

5. 工作流的演进后续

5.1 从 Skill 到可观测评估体系

当 Skill 数量到了两位数,你就需要一套自动评估机制,而不是每次新版本上线都靠人工抽查。

我们在项目里搭建了一个简易评估池,把每个 Skill 的历史真实输入和期望输出沉淀成用例集,每次更新 Skill 之后自动跑一遍全量用例,看准确率指标是否有回退。

这个评估池目前在行业内有一定共识,但底层逻辑并不复杂:你需要一组足够贴近线上分布的测试样本,它们最好是真实流量的采样,而不是人工编造的理想样本。

评估的不只是模型输出的准确率,还要评估路径选择正确率。也就是说,Agent 有没有在正确的位置激活了正确的 Skill。这一点往往比最终输出结果更容易隐藏问题。

5.2 团队协作与 Code Review

当多个工程师一起维护 Skill 集时,最怕的是互相之间改了彼此的边界。我们后来约定了一个规则:每个 Skill 都有唯一的负责人,改动 Skill 说明文档和 schema 都必须通过 review。

Review 时关注的顺序是:边界描述是否清晰、Schema 是否向后兼容、validator 是否覆盖了足够多的异常路径、示例用例是否反映了真实线上分布。这个顺序和我们平时做代码 review 关注点很不一样,但更贴合 AI 系统的特殊性。

5.3 经验的沉淀与固化

做 Skill 体系的时间越长,越能体会到它带来的一个隐藏收益:经验本身开始变得可沉淀、可复用。

以前做一个新的 Agent 应用,总要从头开始写 prompt、调函数、试错。现在不同,基于已有的 Skill 资产池,新应用只需要做两件事:

  • 从资产池里挑选可复用的 Skill,重置参数。
  • 针对新业务场景,补齐缺失的原子 Skill。

这其实是把 AI 开发从“艺术家模式”推向“工程模式”的关键一步。它还远不完美,但至少团队不必每次都从零开始。

6. 最后的一些真心话

在这个项目上踩了大半年的坑之后,我最大的体会是:给 AI Agent 配工程工作流,真正的难点不是智能,而是确定性。你没办法让模型每次输出完全一样,但你可以通过 Skill 的边界、校验、验证器和回滚机制,把不确定性控制在一个可控的范围里。

做这套体系从初期到成熟,经历过很长一段时间的怀疑——投入了大量时间在编排设计上,却觉得 Agent 的“智能感”反而不如一个乱写大 prompt 的版本来的灵敏。后来我才想明白,那种灵敏不过是脆弱的表象,场景稍一扩展就塌了。真正能在生产环境里稳定服役的 Agent,靠的就是这种看似保守的工程化约束。

如果你也正在做 Agent 应用,我的建议很简单:先把 Skill 拆分做薄,让每个 Skill 的意图边界尽可能精确;给每个 Skill 配好 validator 和日志;然后建立一套基于真实流量的回归机制。这会让你后续的所有优化都变得有的放矢。

这个 Skill 工作流的方向还有很大的扩展空间,包括 Skill 自动生成、Skill 组合的自动优化、以及跨 Agent 共享的 Skill 仓库。我相信未来这会成为 Agent 基建里一个核心的基础单元。当下如果你正好在面临同样的困惑,希望这篇分享能给你一些方向和信心。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进