🏠

别让 Skill 成为系统里最脆的那一环

基于 SkillsBench 的深度评测指南 —— 不谈空泛概念,用科学方法让你的 Skills 经得起复用

最近在工作中,我经常会想去沉淀一些自己的 Skills —— 把常用的逻辑封装起来,后面可以复用,像搭积木一样构建 Agent。但是每次写完,脑子里隐隐就会冒出一个问题:这个 Skill 在别人用的时候,会不会教错人了?会不会跑不通呢?

我相信不止我一个人有这种焦虑。在构建 AI Agent 或复杂自动化流程时,Skills(技能/能力)是我们绕不开的概念。无论是调用内部 API、查询数据库,还是封装复杂的业务 Prompt,我们的初衷都很美好:沉淀可复用逻辑,像搭乐高一样构建智能体

但现实往往很骨感。随着 Skills 越写越多,不少团队陷入了这样的困境:

我们不是不会写 Skills,而是不知道它什么时候会坏。

今天,我们不谈空泛的概念,而是结合斯坦福、CMU 等机构发布的重磅研究 SkillsBench,聊聊如何科学地评测 Skills,避免它们成为系统的"阿喀琉斯之踵"。


一、问题不是"不会写",而是"不知道它什么时候会坏"

在很多 AI 应用系统里,我们热衷于抽象 Skills。这样做的好处显而易见:解耦和复用

但做着做着,你会发现一些诡异的现象:

🐛 修改了一个 Skill,另一个看似无关的功能突然挂了
🐛 本地 Debug 一切正常,上线后行为却南辕北辙
🐛 没人敢重构旧 Skill,因为根本不知道会影响多少地方

这种状态的根源在于:我们只关注了 Skill 的"功能实现",却忽略了它的"安全性评估"

💡 根据 SkillsBench(首个 Agent Skills 基准测试)的研究显示,精心策划的 Skills 能带来显著收益,但模型自生成的 Skills 不仅无效,甚至可能导致性能下降。这意味着,我们不能盲目信任 AI 生成的逻辑,必须建立严格的评测标准。
graph LR
    A[Skills 来源] --> B[精心策划的 Skills]
    A --> C[模型自生成的 Skills]
    B --> D[✅ 显著收益提升]
    C --> E[❌ 性能下降 / 无效]
    D --> F[可信赖的系统]
    E --> G[隐性崩坏的系统]
            
图:Skills 来源对系统性能的影响(基于 SkillsBench 研究)

二、Skills 和普通代码最大的不同:它不是"纯"的

为什么传统的单元测试在 Skills 面前经常失效?因为 Skills 往往不是一个纯函数(Pure Function)

普通函数的输入是明确的参数,而 Skills 的输入通常包含大量隐式上下文

隐式输入 说明 影响程度
Context(上下文) 当前的对话历史、Agent 的内部状态 🔥🔥🔥
Prompt(提示词) 微小的变动可能导致截然不同的输出 🔥🔥🔥
Memory(记忆) 长期存储的状态信息 🔥🔥
External System(外部系统) 数据库、第三方 API 的实时响应 🔥🔥

这就导致了一个棘手的问题:

⚠️ 同一个 Skill,在不同环境、不同上下文下,行为可能完全不一样。

这也是为什么:

graph TB
    subgraph pure["纯函数"]
        A1["输入 (x, y)"] --> B1["f(x, y) = z"]
        B1 --> C1["输出 (z)"]
    end
    subgraph skill["Skill"]
        A2["显式参数"] --> B2["Skill()"]
        M2["上下文 Context"] --> B2
        N2["提示词 Prompt"] --> B2
        O2["记忆 Memory"] --> B2
        P2["外部系统 API"] --> B2
        B2 --> C2["输出 + 副作用"]
    end
            
图:纯函数 vs Skill 的输入对比

三、最容易被忽视的三个"隐形炸弹"

在评测 Skills 时,以下三个坑出现的频率最高,杀伤力也最大。

1. "看起来是读操作,其实在写"

很多 Skills 披着"查询"的外衣,却在背地里做"写入"的事。比如查询用户信息的同时,顺手更新了 last_active_time

问题在于:调用方对此一无所知

结果就是:一个看似无害的 Skill 调用,悄悄污染了后续的整个行为链。这是最难排查的一类 Bug

sequenceDiagram
    participant Agent
    participant Skill as getUserInfo
    participant DB
    Agent->>Skill: 查询用户信息(以为是只读)
    Skill->>DB: SELECT * FROM users WHERE id = ?
    DB-->>Skill: 返回用户数据
    Skill->>DB: UPDATE users SET last_active = NOW() ❌
    Note right of Agent: 调用方完全不知道
发生了写操作! Skill-->>Agent: 返回用户数据
图:披着读操作外衣的隐式写入

2. 组合之后才出问题(1+1 < 0)

单个 Skill 的表现完美,但当它们串联起来却可能报错。这通常是因为:

⚠️ 这类问题,仅靠孤立地测试单个 Skill 永远测不出来。

3. 回归是"静默失败"的

最可怕的不是报错,而是无声的性能下降。你优化了一个 Skill:

💡 用户能感知到"不对劲",但系统监控却一片静好。

四、那到底应该怎么评测 Skills?

既然标准方法不管用,我们需要一套更贴近实战的评测策略。结合 OpenAI 和 LangChain 的最佳实践,以下是 5 个可落地的建议。

1. 建立"最小评测集"与 A/B 测试

不要只写 Demo,要为每一个核心 Skill 建立测试用例矩阵。更重要的是,引入 A/B 测试机制:对比 Agent 在"无 Skills"和"有 Skills"下的表现差异。

✅ SkillsBench 的研究表明,只有对比才能量化 Skill 的真实价值。如果加上 Skill 后性能没有显著提升,甚至下降,那么这个 Skill 就是无效的。
graph LR
    subgraph ab["A/B 测试对比"]
        A["Agent 无 Skills"] --> C["性能基准 Baseline"]
        B["Agent 有 Skills"] --> D["性能增量 Delta"]
        C --> E{Delta > 0 ?}
        D --> E
        E -->|是| F["✅ Skill 有效,保留"]
        E -->|否| G["❌ Skill 无效,移除"]
    end
            
图:通过 A/B 测试量化 Skill 的真实价值

2. 强制区分:纯逻辑 vs 有副作用

建议在代码层面或文档中给 Skills 打上标签:

类型 标签 特征 风险等级
Pure Skill pure 无副作用,只读,可放心缓存和重试 🟢 低
Side-effect Skill side-effect 会写 DB、写 Memory、调用支付接口等 🔴 高

并明确标注它会触碰哪些外部状态。这能让调用方在编排工作流时立刻意识到风险

// ✅ 推荐:显式标注 Skill 类型
interface SkillDefinition {
  name: string;
  type: 'pure' | 'side-effect';
  sideEffects?: string[];  // 声明会触碰哪些外部状态
  dependencies?: string[]; // 声明依赖的其他 Skills
}

const getUserInfo: SkillDefinition = {
  name: 'getUserInfo',
  type: 'pure',
};

const updateUserActive: SkillDefinition = {
  name: 'updateUserActive',
  type: 'side-effect',
  sideEffects: ['users.last_active_time'],
};

3. 做"前后状态对比"(State Diff)

这是排查隐式 Bug 的神器。在执行 Skill 前后,强制快照关键状态(Memory, Context, 全局配置),然后自动生成 Diff。

很多时候,你以为没改状态,Diff 会告诉你:"不,你改了"。
graph TD
    A["执行 Skill 前"] --> B["快照 State_before"]
    B --> C["执行 Skill"]
    C --> D["快照 State_after"]
    D --> E["Diff = State_after - State_before"]
    E --> F{有变化?}
    F -->|是| G["⚠️ 检查是否为预期变更"]
    F -->|否| H["✅ 确认为 Pure 操作"]
            
图:State Diff 排查隐式副作用

4. 引入"轨迹级"回归测试

不要只检查最终结果,要检查 Trajectory(轨迹),即 Agent 的整个决策路径。

利用 Record/Replay(记录/回放)技术,捕获一次成功的执行轨迹,后续每次修改都进行回放对比。这能有效防止因 Prompt 微调导致的逻辑路径改变。

📖 轨迹级回归测试的核心思想:不仅要验证"结果对不对",更要验证"路子对不对"。结果相同但路径不同,可能意味着 Agent 的推理过程发生了退化。

5. 日志一定要"可还原"

很多团队的日志只打了结果,这是不够的。一个合格的 Skill 日志应包含:

日志字段 说明 重要性
输入 含完整上下文(Context, Memory, Prompt) 🔴 必需
调用链 Tool 调用顺序、参数、响应 🔴 必需
输出 最终返回结果 + 状态变化 🔴 必需
状态变化 Memory/DB/外部系统的变更记录 🟡 强烈建议
✅ 目标是做到:线上出的任何问题,都能拿着日志在本地 100% 复现。

6. 给 Skill 写 Tests —— 像 Jest 一样定义输入输出

这是我在实践中摸索出来的一个想法:既然传统单测对 Skill 不完全管用,那我们能不能发明一种更适合 Skill 的"测试"方式?

灵感来自于前端开发中的 Jest 单元测试。我们写代码的时候,每个函数都会配上 describeit,明确定义"什么输入 → 什么输出"。Skill 其实也可以这么做 —— 虽然不能 100% 覆盖所有场景,但至少能保证:在已知的场景下,这个 Skill 是可用的

具体怎么做?每个 Skill 在定义时,强制要求附带一组 Examples(用例)

interface SkillExample {
  description: string;     // 用例描述:这个场景在做什么
  input: {                 // 输入:包含上下文、参数、记忆
    query: string;
    context?: Record;
    memory?: Record;
  };
  expectedOutput: {        // 期望输出:明确、可校验
    result: string;
    sideEffects?: string[]; // 预期的副作用
  };
}

interface SkillDefinition {
  name: string;
  type: 'pure' | 'side-effect';
  sideEffects?: string[];
  // ✨ 新增:必须附带至少一个 Example
  examples: SkillExample[];
}

写出来的效果类似这样:

const searchKnowledgeBase: SkillDefinition = {
  name: 'searchKnowledgeBase',
  type: 'pure',
  examples: [
    {
      description: '用户查询项目部署流程',
      input: {
        query: '项目怎么部署到生产环境?',
        context: { projectName: 'my-app' },
      },
      expectedOutput: {
        result: '从知识库检索到部署文档,返回步骤 1-5',
      },
    },
    {
      description: '查询不存在的内容',
      input: {
        query: '怎么发射火箭?',
        context: { projectName: 'my-app' },
      },
      expectedOutput: {
        result: '未检索到相关内容,返回兜底回复',
      },
    },
  ],
};
💡 这样做的核心价值不是"自动化测试",而是强制你作为 Skill 作者,把"我的 Skill 在什么场景下能用"写清楚。哪怕只有 1 个 Example,也比没有强 —— 至少别人调用时知道这个 Skill 是能跑通的。

更进一步,这些 Examples 还能发挥三个作用:

✅ 虽然它不能 100% 解决所有问题(毕竟 Skill 的输入包含隐式上下文),但能显著降低"写出来没人敢用"和"改了之后悄悄崩"的风险。至少,你要有一个 Example 来证明:这个 Skill,是真的能用的。

五、一个简单但有效的起步方案

如果你现在的项目处于"裸奔"状态,不要试图一步到位搭建复杂的评测平台。建议你今晚就做这三件事

挑出 Top 5 最核心的 Skill —— 不是所有的 Skill 都需要立刻评测,先聚焦最关键的
为每个 Skill 写至少 1 个 Example —— 定义输入、期望输出和预期副作用,像写 Jest 用例一样,至少证明"这个 Skill 能跑通"
在修改代码前后,跑一遍 Examples 并记录状态变化 —— 不需要自动化,手动跑一遍也能发现"改了之后悄悄崩"的问题

这三步不需要任何额外的基础设施,但能立刻让你的系统稳定性上一个台阶。

graph LR
    A["🔥 Top 5
核心 Skill"] --> B["📝 写至少 1 个
Example"] B --> C["🔍 修改前后
跑 Examples 对比"] C --> D["✅ 系统稳定性
显著提升"]
图:三步起步方案的执行路径

六、写在最后

Skills 的设计哲学很像城市建设:修路(写逻辑)很快,但维护下水道(副作用、状态管理)才是真正的挑战

Skills 的问题,从来不是"写出来",而是"写出来之后还能不能被信任"。

而评测的本质,就是在回答这三个灵魂拷问:

它什么时候会坏?
坏了我能不能第一时间发现?
改了之后会不会误伤别人?

如果这些问题没有答案,那么 Skills 写得越多,你的系统只会变得越脆弱

⚠️ 不要让可复用能力,最终变成系统性脆皮。
如果这篇文章对你有启发,欢迎分享给你的团队。让更多人意识到:评测不是可选项,而是 Skills 工程化的必选项