Administrator
发布于 2026-04-05 / 484 阅读
10

Agent Skills 规范:能力复用标准化的尝试

我们内部有 34 个 Agent,每家一套工具定义

今年年初做资产盘点,我发现一件事:公司里跑着 34 个 Agent(客服、运营、数据分析、运维助手……),它们调用的工具加起来有 210 多个,其中功能重叠的大概 60 个。

最典型的是「查订单」。客服 Agent 有一份实现,运营 Agent 有一份,数据分析 Agent 还有一份。三份代码逻辑基本一样,但参数名不同、错误处理方式不同、返回的字段不同。改一次订单查询逻辑,要改三个地方,而三个地方由三个人维护。

Agent Skills 这个规范出来的时候,我第一时间去看了。这篇记录我们试用了两个月的结论,以及它在整个能力复用体系里到底该放哪。

Agent Skills 想解决的问题

先说清楚这个规范是什么。它不是协议,也不是框架,是一个能力描述格式。核心思路是:把一个 Agent 能做的事,用一份结构化的描述文件表达出来,让不同的 Agent 运行时都能加载。

最小形态大概是这样的:

my-skills/
├── SKILL.md              # 必需:技能说明 + 元数据
├── scripts/
│   └── sync_order.py     # 可选:可执行脚本
├── references/
│   └── api_doc.md        # 可选:参考文档,按需加载
└── assets/
    └── template.xlsx     # 可选:模板文件

SKILL.md 的头部是 YAML 元数据,正文是给模型看的说明:

---
name: order-refund-analysis
description: 分析某时间段内的退款数据,识别异常退款模式并生成报告。
             当用户要求分析退款、排查退款异常、生成退款报表时使用。
version: 1.2.0
allowed-tools:
  - read_file
  - execute_sql
  - write_file
model-hints:
  min-context: 32000
---

## 用途
分析指定时间窗口内的退款订单,输出退款原因分布、金额分布、异常模式识别。

## 执行流程
1. 用 `execute_sql` 查询退款订单主表,条件见 references/sql_template.md
2. 按 reason 字段聚合,计算各原因的占比和金额
3. 检查是否存在异常模式(同一用户高频退款、金额异常集中等)
4. 生成报告,格式见 assets/template.xlsx

## 注意事项
- 单用户退款次数 > 5 次需要单独列出,可能涉及风控
- 不要输出用户的手机号和身份证号
- 数据量超过 5 万行时先聚合再分析,不要全量拉到上下文

关键设计是渐进式披露(progressive disclosure):Agent 启动时只加载所有技能的 name + description(几百 token),判断要用哪个技能时才加载完整的 SKILL.md 正文,再需要时才去读 references/ 里的文档。

这个设计直接解决了一个我们很头疼的问题:工具多了之后,光是工具定义就吃掉大量上下文。我们客服 Agent 有 41 个工具,工具定义占了 8,400 token,每次请求都要带上。加上系统提示词和历史消息,上下文很快就不够用了。

和 MCP、A2A 的分工

这三个东西经常被混在一起讨论,我一开始也搞混过。试用之后我的理解是这样:

维度MCPAgent SkillsA2A
解决什么怎么调用外部能力怎么描述「做一件事的方法」Agent 之间怎么对话
传输层有(JSON-RPC,Streamable HTTP / stdio)无,纯文件有(HTTP + JSON-RPC)
内容工具签名(强类型)自然语言流程 + 脚本 + 参考资料任务、消息、工件
执行方远端 MCP ServerAgent 自己(或沙箱里的脚本)另一个 Agent
粒度原子操作(查订单、发退款)完整任务(分析退款异常)委派任务
类比函数调用 / RPC操作手册 / SOP同事间的工单往来

这个类比是我觉得最清楚的:

  • MCP 是「手」——Agent 用来操作外部系统的能力,每个工具是一个原子动作;
  • Skills 是「脑子里的经验」——告诉 Agent 做某件事的标准流程,包括先做什么后做什么、有什么坑、格式要求;
  • A2A 是「嘴」——Agent 之间派活和汇报的通道。

它们是可以叠加的,不是互斥的。一个 Skill 内部完全可以调用 MCP 提供的工具。我们实际就是这么用的:

// Skill 描述流程,MCP 提供原子能力
// SKILL.md 里写:
//   1. 用 mcp://order-tools/queryOrders 查询订单
//   2. 用 mcp://risk-tools/checkUserRisk 检查风控
//   3. 综合判断后输出结论

反过来说,如果你只有 MCP,Agent 知道「能查订单」但不知道「分析退款异常该怎么查」;如果只有 Skills,Agent 知道该怎么做但没有实际的操作能力。这两个是互补的。

实际试用:把退款分析流程做成 Skill

我拿运维场景做了试验。我们有个「线上故障初筛」的流程,原来写在一份 12 页的 Wiki 里,新人照着做要 40 分钟,老手也要 15 分钟。

把它做成 Skill:

fault-triage/
├── SKILL.md
├── scripts/
│   ├── collect_logs.sh
│   └── check_metrics.sh
├── references/
│   ├── error_code_map.md
│   ├── escalation_policy.md
│   └── postmortem_template.md
└── assets/
    └── triage_report.md

SKILL.md 正文(节选):

---
name: online-fault-triage
description: 线上故障初筛。当用户报告服务异常、收到告警、或要求排查线上问题时使用。
version: 2.1.0
allowed-tools:
  - execute_shell
  - read_file
  - write_file
  - query_metrics
---

## 第一步:确认影响范围(3 分钟内完成)

执行 `scripts/check_metrics.sh <service>` 获取最近 30 分钟的核心指标。
重点关注三项:QPS、错误率、P99 延迟。

判断标准:
- 错误率 > 1% 且持续 5 分钟以上 → P2,立即进入第二步
- 错误率 > 0.1% 但 < 1% → P3,进入第二步但不用叫人
- 仅 P99 升高、错误率正常 → 大概率是容量问题,跳到第四步

## 第二步:拉日志

执行 `scripts/collect_logs.sh <service> --since 30m --level ERROR`。
日志量超过 2000 行时,先按异常类型聚合,只把 top 5 的异常堆栈给出来分析。

## 第三步:对照错误码

查 `references/error_code_map.md`。这个表维护了常见错误码的含义和处理方式。
表里没有的错误码,不要猜,标注「未知错误码,需人工判断」。

## 第四步:输出初筛结论

按 `assets/triage_report.md` 模板输出,必须包含:
影响范围、初步判断、已排除的可能性、建议的下一步、是否需要升级。

## 硬约束
- 不要执行任何写操作(重启、扩缩容、改配置),只做只读排查
- 涉及数据库的排查一律用只读账号
- 不确定时标「需人工确认」,不要给确定性结论

最后那段「硬约束」是我们加的,规范里没有要求,但我觉得必须有。

Java 侧的支持现状

这块要说清楚,因为和 Python 侧的成熟度差距不小。

我们用的是 Spring AI。截至 2026 年 4 月,Spring AI 1.x 里没有官方的 Skill 加载器。2.0 的里程碑版本里我看到了相关的 API 雏形,但还没稳定。所以现在只能用自定义的方式接。

我们自己写了一个约 300 行的加载器,逻辑不复杂:

@Component
public class SkillRegistry {

    private final Map<String, SkillDefinition> skills = new ConcurrentHashMap<>();
    private final Path skillRoot = Path.of("/etc/agent/skills");

    @PostConstruct
    void load() throws IOException {
        try (var stream = Files.walk(skillRoot)) {
            stream.filter(p -> p.getFileName().toString().equals("SKILL.md"))
                  .forEach(this::register);
        }
        log.info("loaded {} skills", skills.size());
    }

    private void register(Path skillFile) {
        String content = Files.readString(skillFile);
        SkillDefinition def = parseFrontMatter(content);

        if (def == null || def.name() == null || def.description() == null) {
            log.warn("skill file missing name or description: {}", skillFile);
            return;                            // 元数据不全的直接跳过
        }
        skills.put(def.name(), def);
    }

    /** 给系统提示词用的:只有 name + description,token 开销很小 */
    public String buildIndex() {
        return skills.values().stream()
            .sorted(Comparator.comparing(SkillDefinition::name))
            .map(s -> "- %s: %s".formatted(s.name(), oneLine(s.description())))
            .collect(Collectors.joining("\n"));
    }

    /** 命中后才加载正文,并替换脚本路径为绝对路径 */
    public String loadFull(String name) {
        SkillDefinition def = skills.get(name);
        String body = def.body();
        // 把相对路径替换掉,否则 Agent 找不到脚本
        return body.replace("scripts/", def.baseDir().resolve("scripts") + "/")
                   .replace("references/", def.baseDir().resolve("references") + "/");
    }
}

然后在 CallAdvisor 里做渐进式加载:

@Component
public class SkillAdvisor implements CallAdvisor {

    @Override
    public ChatClientResponse adviseCall(ChatClientRequest req, CallAdvisorChain chain) {
        String userText = req.prompt().getUserMessage().getText();

        // 第一轮:只给索引,让模型自己判断要不要用技能
        SkillDecision decision = decide(userText, skillRegistry.buildIndex());
        if (decision == null) {
            return chain.nextCall(req);
        }

        // 第二轮:命中了,把完整 SKILL.md 塞进系统提示词
        return chain.nextCall(req.mutate().system(s -> s + """

            ## 当前激活的技能:%s

            %s
            """.formatted(decision.name(), skillRegistry.loadFull(decision.name())))
            .build());
    }
}

这套东西能用,但我得说清楚它的局限:

  • 脚本执行是沙箱外的问题,得自己解决。规范里说脚本由 Agent 运行时执行,但怎么隔离、怎么限权,Java 侧没有任何现成方案。我们的做法是只允许执行白名单里的脚本,且跑在一个只读挂载的容器里;
  • 没有版本协商机制。Skill 文件改了,正在跑的会话用的是旧版本,这个我们踩过一次;
  • 没有依赖声明。Skill 之间如果有依赖(比如「故障初筛」依赖「日志查询」),规范里没法表达,我们只能在 description 里用文字说明;
  • Java 侧的生态基本为零。没有中央仓库、没有包管理、没有版本发布流程。我们是用 Git 仓库存 Skill 文件,靠 CI 同步到各个环境,很土但能用。

效果:值不值得做

用了两个月,说说实际收益。

最明显的收益是上下文省下来了。我们的运维 Agent 原来把 12 页 Wiki 的内容全塞进系统提示词,占了 14,200 token。改成 Skill 按需加载之后,平时只占 680 token(技能索引),用到时才加载 2,100 token。

改造前改造后
系统提示词 token18,4004,900
单次调用成本¥0.086¥0.041
故障初筛耗时(新人)40 分钟11 分钟
初筛结论准确率78%(老手是 91%)

成本减半,这个是实打实的。新人的初筛时间也确实降下来了。

但准确率 78% 这个数字要诚实地说:它比老手差不少。我们分析过差距在哪,主要是「需要跨系统关联判断」的场景,Agent 做得不好。比如同时要看监控、日志、最近的发布记录才能判断的情况,它经常只看一半就下结论。

第二个收益是流程终于有地方沉淀了。以前这些 SOP 写在 Wiki 里,写完就没人维护,三个月后和实际操作完全脱节。现在 Skill 文件是被 Agent 实际执行的,流程错了马上就能发现。我们这两个月改了 17 次 fault-triage 的 SKILL.md,Wiki 那版一年改了 2 次。

我自己的判断

说几点比较主观的看法。

一、这个规范的价值主要在「约定」,不在「技术」。 它的技术含量很低,就是一个 Markdown 文件加 YAML 头。但正因为简单,它有可能被大家接受。MCP 之所以能起来,也不是因为技术多先进,是因为它足够简单且解决了一个真问题。

二、现在(2026 上半年)还不适合大规模投入。 规范本身还在快速变化,我们这两个月就遇到两次不兼容的调整(allowed-tools 的字段名改过一次,元数据的必填项变过一次)。我的建议是:做 POC 可以,别把核心流程绑上去。

三、和 MCP 的关系不用担心。 一开始我担心 Skills 会取代 MCP,现在看不会。它们解决的是不同层次的问题,实际用起来是组合关系。真正需要担心的是 Skill 内部调用工具时的权限传递——这个规范里没说清楚,是最大的空白。

四、Java 侧现在要做的话,得接受自己造轮子。 上面那个 300 行的加载器,加上沙箱、版本管理、CI 同步,我们总共投入了约 18 人日。如果你的团队主要用 Python,现在生态成熟度高很多。

留个问题

关于《Agent Skills 规范:能力复用标准化的尝试》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。

参考