ACP 大模型应用开发 | 注册加载生成skill与社区复用skill(下)

承接:上篇:从审查痛点到高质量 Skill。

🚄 前言

上篇你已手动完成 Skill 从设计到「双 Agent」迭代的全过程。但 Skill 仍像躺在磁盘里的一组 Markdown——Agent 怎么发现它、加载它、在真实任务里调用它? 推广到团队后,如何避免 「一个人的成功,十个人的混乱」?

4 让你的 Agent 使用和生成 Skill

到目前为止,你手动完成了 Skill 从设计到迭代的全过程:用审查经验提炼检查清单,封装成 course-review/ 目录,又通过「双 Agent」模式反复打磨指令。但这个 Skill 还只是 一组 Markdown 文件——Agent 怎么发现它、加载它、在真实任务中调用它?

4.1 在 AgentScope 中加载 Skill

好消息是,你不需要自己写完整的加载逻辑。AgentScope Java 用 ClasspathSkillRepository 解析 SKILL.md,SkillBox 注册 Skill 并生成 Agent 可见的提示。

Python 课件 vs AgentScope Java

Python 课件 AgentScope Java
toolkit.register_agent_skill("course-review") skillBox.registerSkill(agentSkill)
toolkit.get_agent_skill_prompt() skillBox.getSkillPrompt()
按需读子文件 load_skill_through_path(框架内置)+ read_file
ReActAgent(..., toolkit=toolkit) ReActAgent.builder().skillBox(skillBox).toolkit(toolkit)

准备工作:SKILL.md + YAML frontmatter

框架要求入口文件名为 SKILL.md(不是 README.md),并从 YAML frontmatter 提取 name、description。上篇已完成升级;course-review 入口示例:

---
name: course-review
description: |
  审查课程内容的技术准确性、代码正确性和教学质量。当用户要求审查、审计或评估现有的课程或培训材料(含 Jupyter Notebook)时使用此技能。
owner: 教学设计团队
maintainers:
  - course-quality-lead
version: 2.1.0
last-reviewed: 2026-01-15
---

# CourseReviewSkill

## 审查流程
1. 使用 extract_notebook_toc 提取目录
2. 按需加载 code-quality.md、content-accuracy.md 等
3. 调用 validate_notebook_dependencies 做结构化检查
4. 按模板输出审查报告

装配 SkillBox(只注册 course-review)

package com.baoma.ai.debug.support;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.model.Model;
import io.agentscope.core.skill.AgentSkill;
import io.agentscope.core.skill.SkillBox;
import io.agentscope.core.skill.repository.ClasspathSkillRepository;
import io.agentscope.core.tool.Toolkit;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;

public final class CourseReviewSkillSupport {

    private CourseReviewSkillSupport() {
    }

    public static List<AgentSkill> loadSkillsFromClasspath(String skillsRoot) throws IOException {
        try (ClasspathSkillRepository repo = new ClasspathSkillRepository(skillsRoot)) {
            return repo.getAllSkills();
        }
    }

    public static SkillBoxAndToolkit createSkillBox(Path workspace) throws IOException {
        Toolkit toolkit = new Toolkit();
        SkillBox skillBox = new SkillBox(toolkit);

        AgentSkill courseReview = null;
        for (AgentSkill skill : loadSkillsFromClasspath("skills")) {
            if (!"course-review".equals(skill.getName())) {
                continue;
            }
            skillBox.registerSkill(skill);
            courseReview = skill;
        }

        if (courseReview != null) {
            skillBox.registerSkill(courseReview);
            // 注意:registration().tool() 每次赋值会覆盖前一个,不能链式挂两个 @Tool 类
            toolkit.registerTool(new CourseReviewFileTools(workspace));
            toolkit.registerTool(new CourseReviewValidateNotebookTool(workspace));
        }

        return new SkillBoxAndToolkit(skillBox, toolkit);
    }

    public static ReActAgent createSkillBoundReviewer(
        String apiKey,
        String modelName,
        Toolkit toolkit,
        SkillBox skillBox,
        int maxIters) {

        Model model = AgentScopeDashScopeModels.buildDashScopeChatModel(apiKey, modelName);

        return ReActAgent.builder()
            .name("CourseReviewer")
            .sysPrompt("你是教程审查员。根据系统注入的 Skill 元数据与 load_skill 工具完成审查,输出结构化报告。")
            .model(model)
            .toolkit(toolkit)
            .skillBox(skillBox)
            .memory(new InMemoryMemory())
            .maxIters(maxIters)
            .build();
    }

    public record SkillBoxAndToolkit(SkillBox skillBox, Toolkit toolkit) {
    }
}

具体路径:


image.png

第一步:注册 Skill 并查看生成的 Prompt

package com.baoma.ai.debug;

import com.baoma.ai.debug.support.CourseReviewSkillSupport;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

import java.nio.file.Path;

class CourseReviewSkillPromptAgentScopeTest {

    @Test
    @DisplayName("SkillBox.getSkillPrompt 包含 course-review 的 name 与 description")
    void skillPromptContainsMetadata() throws Exception {
        var box = CourseReviewSkillSupport.createSkillBox(Path.of("."));

        String prompt = box.skillBox().getSkillPrompt();
        System.out.println("=== Skill Prompt(节选)===\n");
        System.out.println(prompt.length() > 2000
            ? prompt.substring(0, 2000) + "\n...(truncated)"
            : prompt);

        Assertions.assertTrue(prompt.contains("course-review") || prompt.contains("course_review"));
        Assertions.assertFalse(prompt.contains("反模式清单") && prompt.length() < 500,
            "完整 SKILL body 不应全部塞进 prompt(渐进式披露)");
    }
}

执行。

框架读取 frontmatter,生成类似结构(节选):

<available_skills>
  <skill>
    <name>course-review</name>
    <description>审查课程内容的技术准确性……</description>
    <owner>教学设计团队</owner>
    <version>2.1.0</version>
  </skill>
</available_skills>

提示告诉 Agent「这里有一个 Skill,要用时再读详情」——而不是把整个 SKILL.md 和所有子文件塞进 system prompt。

第二步:集成 ReActAgent 并执行审查

package com.baoma.ai.debug;

import com.baoma.ai.debug.support.CourseReviewScopeTestSupport;
import com.baoma.ai.debug.support.CourseReviewSkillSupport;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.skill.SkillBox;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;

class CourseReviewSkillBoxAgentScopeTest {

    @TempDir
    Path workspace;

    @BeforeEach
    void prepareWorkspace() throws Exception {
        try (var in = getClass().getClassLoader()
            .getResourceAsStream("agentscope-skill-demo/sample-course.ipynb")) {
            Assertions.assertNotNull(in);
            Files.copy(in, workspace.resolve("sample-course.ipynb"));
        }
    }

    @Test
    @DisplayName("AgentScope:SkillBox 加载 course-review 并审查 Notebook")
    void skillBoxCourseReview() throws Exception {
        String apiKey = CourseReviewScopeTestSupport.requireDashScopeApiKey();
        String modelName = CourseReviewScopeTestSupport.reviewModelName();
        CourseReviewScopeTestSupport.printApiKeyLoaded(apiKey);

        var box = CourseReviewSkillSupport.createSkillBox(workspace);
        SkillBox skillBox = box.skillBox();

        System.out.println("=== Skill 注入 Prompt 预览(节选)===\n");
        String skillPrompt = skillBox.getSkillPrompt();
        System.out.println(skillPrompt.length() > 1200
            ? skillPrompt.substring(0, 1200) + "\n...(truncated)"
            : skillPrompt);

        ReActAgent agent = CourseReviewSkillSupport.createSkillBoundReviewer(
            apiKey, modelName, box.toolkit(), skillBox, 28);

        Msg user = Msg.builder()
            .textContent("""
                请审查工作区中的 sample-course.ipynb,评估能否发布。
                使用 course-review 技能;按需加载子规则文件;输出结构化审查报告。
                """)
            .build();

        System.out.println("\n[user]: " + user.getTextContent());
        Msg reply = Objects.requireNonNull(agent.call(user).block(), "Agent 返回为空");
        String text = Objects.requireNonNullElse(reply.getTextContent(), "");
        System.out.println("\n=== 审查报告(SkillBox)===\n" + text);
        Assertions.assertFalse(text.isBlank());
    }
}

执行以上java

这次 Agent 不应再给出「整体不错,建议发布」的敷衍结论。它会按需加载 code-quality.md、content-accuracy.md、style-guide.md,并调用 validate_notebook_dependencies,点出 openpyxl 依赖、众数填充不一致、风格问题等。

三个设计决策

  • 为什么只注入 description 而非全文?
    上下文窗口是公共资源。框架只把 description(约数十 token)注入可见提示;工作流程、反模式清单等内容,由 Agent 决定使用 Skill 后再通过 load_skill / read_file 按需加载。

  • 为什么 Agent 需要文件读取工具?
    Agent 先从 description 判断是否需要这个 Skill,再读 SKILL.md 获取详细指令,需要更细节时再读子文件——渐进式披露在框架层的落地。

  • 为什么 description 字段的质量关键?
    当 Agent 装备了多个 Skill 时,它根据 description 判断当前任务应该使用哪一个。模糊描述(如「处理课程相关任务」)会导致匹配失败;具体描述(说明做什么、什么时候触发)才能确保正确激活。


4.2 让 Agent 自动生成 Skill

手动编写 Skill 是必要的学习过程。熟练之后,可以让 Agent 辅助完成初稿:本仓库提供 skill-creator 元 Skill(保留意图捕获、SKILL.md 规范、渐进式披露;去掉与 Java 环境无关的 CLI 链)。

元 Skill:skill-creator.md 的具体内容

---
name: skill-creator
description: 帮助用户创建新的 Agent Skill,包括需求分析、SKILL.md 编写和结构设计。当用户想要从零创建一个 Skill、改进现有 Skill 的结构或内容时使用。
---

# Skill Creator

一个帮助你创建高质量 Agent Skill 的工具。

创建 Skill 的核心流程:

1. 明确 Skill 的目标和触发场景
2. 收集需求细节和边界条件
3. 编写 SKILL.md 初稿
4. 创建测试用例验证效果
5. 根据反馈迭代改进

你的职责是判断用户当前处于哪个阶段,然后帮助他们推进。如果用户已经有了初稿,可以直接进入改进阶段。

---

## 创建 Skill

### Capture Intent(捕获意图)

首先理解用户的意图。当前对话中可能已经包含了用户想要封装的工作流(比如他们说"把这个流程变成一个 Skill")。如果是这样,先从对话历史中提取信息——使用了哪些工具、步骤顺序、用户做了哪些修正、观察到的输入输出格式。用户可能需要补充细节,确认后再进入下一步。

1. 这个 Skill 要让 Agent 完成什么任务?
2. 什么情况下应该触发这个 Skill?(哪些用户提问/场景)
3. 期望的输出格式是什么?
4. 是否需要设置测试用例来验证 Skill 的效果?有客观可验证输出的 Skill(文件转换、数据提取、代码生成、固定流程步骤)适合测试用例;主观输出的 Skill(写作风格、设计)通常不需要。根据 Skill 类型建议合适的默认方案,但让用户决定。

### Interview and Research(访谈与调研)

主动询问边界情况、输入输出格式、示例文件、成功标准和依赖关系。在搞清楚这些之前,不要急着写测试用例。

如果有可用的工具(搜索文档、查找类似 Skill、查阅最佳实践),先做调研,带着充分的上下文来减轻用户的负担。

### Write the SKILL.md(编写 SKILL.md)

基于用户访谈,填写以下组成部分:

- **name**:Skill 标识符
- **description**:触发条件和功能描述。这是主要的触发机制——既要说明 Skill 做什么,也要说明何时使用。所有"何时使用"的信息都放在这里,而不是放在正文中。注意:为了确保触发率,description 应该适度"积极"。例如,与其写"构建数据可视化面板",不如写"构建数据可视化面板。当用户提到仪表盘、数据展示、指标监控,或想要展示任何类型的数据时都应使用此 Skill,即使他们没有明确要求'面板'。"
- **compatibility**:所需工具、依赖(可选,很少需要)
- **正文内容**

---

## Skill 编写指南

### Skill 的结构


skill-name/
├── SKILL.md (必需)
│   ├── YAML frontmatter (name, description 必填)
│   └── Markdown 指令
└── Bundled Resources (可选)
    ├── scripts/    - 用于确定性/重复性任务的可执行代码
    ├── references/ - 按需加载到上下文中的文档
    └── assets/     - 输出中使用的文件(模板、图标、字体)


### Progressive Disclosure(渐进式披露)

Skill 使用三级加载系统:

1. **元数据**(name + description)- 始终在上下文中(约 100 词)
2. **SKILL.md 正文** - Skill 触发时加载(理想情况 <500 行)
3. **捆绑资源** - 按需加载(不限大小,脚本可以在不加载的情况下执行)

这些字数是近似值,如果需要可以更长。

**关键模式:**
- 保持 SKILL.md 在 500 行以内;如果接近这个限制,添加层级结构并指明下一步应该阅读哪些文件
- 在 SKILL.md 中清晰引用文件,并说明何时应该读取它们
- 对于大型参考文件(>300 行),包含目录

**领域组织**:当 Skill 支持多个领域/框架时,按变体组织:


cloud-deploy/
├── SKILL.md (工作流 + 选择逻辑)
└── references/
    ├── aws.md
    ├── gcp.md
    └── azure.md


Agent 只读取相关的参考文件。

### 安全原则

Skill 不得包含恶意软件、利用代码或任何可能危害系统安全的内容。Skill 的内容在被描述时不应让用户感到意外。不要创建误导性的 Skill 或旨在促进未授权访问的 Skill。

### 编写模式

在指令中优先使用祈使句。

**定义输出格式**——可以这样做:

markdown
## 报告结构
始终使用这个模板:
# [标题]
## 摘要
## 关键发现
## 建议


**示例模式**——包含示例很有用:

markdown
## Commit 消息格式
**示例 1:**
Input: Added user authentication with JWT tokens
Output: feat(auth): implement JWT-based authentication


### 编写风格

尽量向模型解释"为什么"事情很重要,而不是堆砌强硬的 MUST/NEVER。利用心智理论,让 Skill 通用化而非局限于特定示例。先写初稿,然后用新鲜的眼光审视并改进。

---

## 迭代改进

### 如何思考改进方向

1. **从反馈中泛化。** 我们创建的 Skill 将被在各种不同的场景中使用。不要针对测试用例过度拟合,而是理解用户反馈背后的通用模式。与其添加僵硬的限制条件,不如尝试不同的表达方式或工作模式。

2. **保持指令精简。** 移除没有发挥作用的内容。如果 Agent 在执行 Skill 时浪费时间在无用的步骤上,考虑删除导致这种行为的指令部分。

3. **解释为什么。** 尽量解释每条指令背后的原因。当前的大模型很聪明,给出好的上下文后能超越机械执行。如果你发现自己在写 ALWAYS 或 NEVER(全大写),这是一个信号——重新表述,解释推理过程,让模型理解为什么这很重要。

4. **寻找重复工作。** 如果多个测试用例中 Agent 都独立编写了类似的辅助脚本,这强烈暗示 Skill 应该捆绑该脚本。写一次,放在 `scripts/` 中,让 Skill 指引 Agent 使用它。

---

## 测试用例

编写 Skill 初稿后,构思 2-3 个真实的测试提示——真正的用户会实际说的话。与用户分享:"这是我想测试的几个用例,看起来合理吗?需要补充吗?"

好的测试用例是具体的、有细节的,而不是抽象的请求。

不好:`"格式化这个数据"`, `"从 PDF 中提取文本"`

好:`"我老板刚发了一个 xlsx 文件给我(在我的下载目录里,叫'Q4销售数据_最终版v2.xlsx'),她让我加一列利润率百分比。营收在 C 列,成本在 D 列。"

write_file 工具(与 read_file 对称)

package com.baoma.ai.debug.support;

import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

public class CourseReviewWriteFileTools {

    private final Path workspaceRoot;

    public CourseReviewWriteFileTools(Path workspaceRoot) {
        this.workspaceRoot = workspaceRoot;
    }

    @Tool(name = "write_file", description = "将内容写入工作区相对路径;目录不存在则自动创建")
    public String writeFile(
        @ToolParam(name = "path", description = "相对路径,如 skills/api-doc-review/SKILL.md")
        String path,
        @ToolParam(name = "content", description = "文件全文")
        String content) throws IOException {

        Path target = workspaceRoot.resolve(path).normalize();
        if (!target.startsWith(workspaceRoot.normalize())) {
            return "错误:路径越界";
        }
        if (target.getParent() != null) {
            Files.createDirectories(target.getParent());
        }
        Files.writeString(target, content);
        return "已写入 " + path;
    }
}

装配 skill-creator + 创建 Agent

package com.baoma.ai.debug.support;

import io.agentscope.core.ReActAgent;
import io.agentscope.core.memory.InMemoryMemory;
import io.agentscope.core.model.Model;
import io.agentscope.core.skill.AgentSkill;
import io.agentscope.core.skill.SkillBox;
import io.agentscope.core.skill.repository.ClasspathSkillRepository;
import io.agentscope.core.tool.Toolkit;

import java.io.IOException;
import java.nio.file.Path;

public final class SkillCreatorSupport {

    private SkillCreatorSupport() {
    }

    public static SkillBoxAndToolkit createCreatorSkillBox(Path workspace) throws IOException {
        Toolkit toolkit = new Toolkit();
        SkillBox skillBox = new SkillBox(toolkit);

        AgentSkill creator = null;
        try (ClasspathSkillRepository repo = new ClasspathSkillRepository("skills")) {
            for (AgentSkill skill : repo.getAllSkills()) {
                if ("skill-creator".equals(skill.getName())) {
                    creator = skill;
                    skillBox.registerSkill(skill);
                }
            }
        }

        if (creator != null) {
            skillBox.registerSkill(creator);
            toolkit.registerTool(new CourseReviewFileTools(workspace));
            toolkit.registerTool(new CourseReviewWriteFileTools(workspace));
        }

        return new SkillBoxAndToolkit(skillBox, toolkit);
    }

    public static ReActAgent createSkillCreatorAgent(
        String apiKey,
        String modelName,
        Toolkit toolkit,
        SkillBox skillBox) {

        Model model = AgentScopeDashScopeModels.buildDashScopeChatModel(apiKey, modelName);

        return ReActAgent.builder()
            .name("SkillCreator")
            .sysPrompt("你是一个 Agent Skill 设计师。遵循 skill-creator 技能指引生成符合规范的 Skill 目录。")
            .model(model)
            .toolkit(toolkit)
            .skillBox(skillBox)
            .memory(new InMemoryMemory())
            .maxIters(16)
            .build();
    }

    public record SkillBoxAndToolkit(SkillBox skillBox, Toolkit toolkit) {
    }
}

加载 skill-creator 并生成新 Skill

package com.baoma.ai.debug;

import com.baoma.ai.debug.support.CourseReviewScopeTestSupport;
import com.baoma.ai.debug.support.SkillCreatorSupport;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Objects;

class SkillCreatorAgentScopeTest {

    @TempDir
    Path workspace;

    @Test
    @DisplayName("AgentScope:skill-creator 生成 api-doc-review Skill 目录")
    void generateApiDocReviewSkill() throws Exception {
        String apiKey = CourseReviewScopeTestSupport.requireDashScopeApiKey();
        String modelName = CourseReviewScopeTestSupport.reviewModelName();
        CourseReviewScopeTestSupport.printApiKeyLoaded(apiKey);

        var box = SkillCreatorSupport.createCreatorSkillBox(workspace);
        ReActAgent creator = SkillCreatorSupport.createSkillCreatorAgent(
            apiKey, modelName, box.toolkit(), box.skillBox());

        Msg user = Msg.builder()
            .textContent("""
                帮我创建一个审核 REST API 文档的 Skill,目录名 api-doc-review。
                要检查:1. 所有端点是否有示例请求和响应;
                2. 错误码是否有说明;3. 参数是否有类型标注。
                请用 write_file 写入 skills/api-doc-review/SKILL.md 及必要的子文件。
                """)
            .build();

        System.out.println("[user]: " + user.getTextContent());
        Msg reply = Objects.requireNonNull(creator.call(user).block());
        System.out.println("\n=== Skill Creator 回复 ===\n" + reply.getTextContent());

        Path skillMd = workspace.resolve("skills/api-doc-review/SKILL.md");
        if (Files.isRegularFile(skillMd)) {
            System.out.println("\n=== 已生成 SKILL.md ===\n" + Files.readString(skillMd));
            Assertions.assertTrue(Files.readString(skillMd).contains("api-doc-review")
                || Files.readString(skillMd).contains("API"));
        } else {
            System.out.println("\n[提示] 模型可能未调用 write_file;请检查回复或重跑。");
            Assertions.assertFalse(Objects.requireNonNullElse(reply.getTextContent(), "").isBlank());
        }
    }
}

执行上面的最终方法。

Agent 可能生成完整的 api-doc-review/ 目录;也可能只回复计划而未调用 write_file——可多跑几次。

这是加速,不是替代。 你仍需理解上篇五步编写法,才能判断 Agent 生成的内容是否合理。专家用 skill-creator 快速产出高质量 Skill;不理解设计原则的新手,即使有元 Skill 的创建的skill也很难产出可用结果。

小贴士:生成的 Skill 初稿一定要配合上篇 「双 Agent」评测 + 本篇中的skill的规范。自动生成只解决「写出来」;评测解决「写得对不对」。


4.3 Skill 市场:复用社区的专家知识

前两节介绍了注册和生成自建 Skill。在更广的 Coding Agent 生态中,社区已形成 Skill 共享平台。

发现 Skill

# 按关键词搜索
npx skills find typescript

# 输出示例:
# Install with npx skills add <owner/repo@skill>
# vercel-labs/agent-skills@typescript-best-practices

不带参数运行 npx skills find 可进入交互式模糊搜索。找到感兴趣的 Skill 后,在平台详情页查看完整内容与安全审计评级。

安装和使用

npx skills add vercel-labs/agent-skills --skill vercel-react-best-practices
npx skills update

安装后,Skill 就是一组 Markdown 文件,与上篇结构一致。Java 侧将目录放入工程的 skills/ 资源,用 ClasspathSkillRepository(开发)或 FileSystemSkillRepository(部署)加载。

社区 Skill 与自建 Skill 的分工

场景 推荐方式
通用技术最佳实践(React 性能、代码规范) 社区 Skill
成熟的行业标准流程(安全审计、SEO) 社区 Skill
企业内部业务流程和质量标准 自建 Skill
团队特有的代码规范和架构约定 自建 Skill
领域专家的隐性知识(如课程审核) 自建 Skill

使用时的注意事项

  • 避免 Skill 过多:过多 Skill 会撑满 system prompt,稀释核心指令
  • 注意版本匹配:依赖的工具或框架更新后,Skill 内命令可能过时
  • 合理预期:Skill 提升的是下限(少犯低级错误),不是无限抬高上限

4.4 本节回顾

本节围绕 Skill 的「用起来」展开三个层次:

层次 要点
注册与加载(§4.1) SkillBox + getSkillPrompt 只注入 description;Agent 按需读详细指令——渐进式披露在框架层的落地
自动生成(§4.2) skill-creator 元 Skill + write_file 生成初稿;仍需评测验证
社区复用(§4.3) npx skills 发现/安装;与自建 Skill 互补

5 当 Skill 遇上团队

5.1 一个人的成功,十个人的混乱

你在 §4 中成功构建了 course-review,把课程审核时间从 2 小时缩短到约 20 分钟。主管很满意,让你把这个 Skill 推广给整个教学设计团队——10 个人。你把 SKILL.md 发到群里,附了一段简短的使用说明。

一个月后,问题接踵而至。

版本混乱:你根据最新的课程标准更新了评分权重,但有 3 位同事还在用旧版 Skill。同一门课程被审出矛盾的结论——你的报告说「结构合理」,同事的报告说「章节划分不当」。团队不得不花半天时间排查,才发现是 Skill 版本不一致。

误修改:实习生觉得 SKILL.md 中「避免使用 print() 进行调试」这条规则太严格,自行改成了「允许在开发阶段使用 print()」。结果 Agent 开始把所有 print() 调试代码标记为「符合规范」,三周后才被发现。

知识断档:你休假两周,团队遇到 3 个审核边界问题——「代码示例中的注释算不算教学内容?」「引用外部 API 文档需不需要检查链接有效性?」——没人能回答,因为这些判断逻辑只存在于你的经验中,没有写进 Skill。

这三个问题有一个共同点:CourseReviewSkill 作为一个人的作品运转良好,但它没有为团队协作而设计。


5.2 朴素修补:为什么「群里发一下」行不通

面对这些问题,你可能会想到几个直觉性的修补方案。

统一存放位置 — 把 Skill 放到共享网盘。但谁有权修改?如果 10 个人都能改,就等于没人负责。

发一份使用说明文档 — 文档和 Skill 分离,两周后 Skill 迭代了,文档还停留在旧版本。

由你一个人负责所有修改 — 你成了瓶颈,且你不是所有审核领域的专家。

这些修补失败的根本原因在于:Skill 不是普通配置文件,而是封装了专家知识的可执行资产。管理它需要明确的所有权和工程化的生命周期。

换一个角度:Skill 是能力放大器——资深设计师一周审 10 门课,封装成 Skill 后,10 名初级成员都能调用。但要让放大器在团队中稳定运转,需要解决 所有权、协作流程、质量保障。

image.png


5.3 领域团队所有权:让最懂业务的人管 Skill

谁最有能力定义和维护 course-review?教学设计团队——他们最了解「什么样的课程才算好」。

新的分工模式

角色 职责
专家 (Expert) 定义标准、封装 Skill、审核变更
团队成员 (Member) 调用 Skill、反馈问题、提交改进建议

专家从「亲自执行每一次审核」转变为「维护审核标准」,影响力从线性增长变为可复制的规模化。

不同领域的 Skill 由对应业务团队负责

领域 所有权团队 维护的 Skill 示例
课程质量 教学设计团队 CourseReviewSkill
数据分析 数据团队 DataCleaningSkill
安全合规 安全团队 CodeSecurityScanSkill

业务专家 + 技术人员协作

角色 职责
业务专家 (SOP Owner) 制定流程标准与边界条件
技术人员 (Agent Architect) Skill 结构调优与工具挂载

协作流程分三步:

  • 1、业务专家用自然语言描述"一个优秀的审核员会怎么做"
  • 2、技术人员将其转化为结构化的 Skill 格式
  • 3、双方一起测试、迭代、根据 Agent 实际产出调整指令

为了在 Skill 文件中明确所有权信息,可以在 YAML frontmatter 中添加管理元数据:
在 frontmatter 中明确所有权

---
name: course-review
description: |
  审核课程内容的质量和结构完整性。当用户要求检查、审核或评估课程材料时触发。
owner: 教学设计团队
maintainers:
  - zhang-wei
  - li-na
version: 2.1.0
last-reviewed: 2026-01-15
---

这些字段让任何人打开文件就能知道:有问题找谁、当前是什么版本、上次审查是什么时候。

小贴士:GitHub 可配置 CODEOWNERS,例如 /skills/course-review/ @teaching-design-team,对该路径的修改自动要求教学设计团队审批。

/skills/course-review/    @org/course-quality
/skills/skill-creator/    @org/agent-platform

5.4 Skills-as-Code:用工程方法管理 Skill 资产

所有权明确后,下一个问题是:团队如何协作迭代 Skill?答案是把 Skill 当作代码来管理——version control、code review、CI/CD。这些软件工程的成熟实践同样适用于 Skill。

Skills-as-Code 生命周期

image.png

阶段 说明
1、本地迭代 维护者在本地修改 Skill,用几个代表性案例快速验证效果
2、PR / Review 提交 Pull Request,由团队其他成员(尤其是业务专家)审查变更
3、CI 评测 自动化流水线运行评测用例,确保修改没有引入回归问题
4、部署上线 合并到主分支后,所有使用该 Skill 的 Agent 自动获取最新版本
5、反馈收集 跟踪使用情况,驱动改进

将 classpath 中的 skills 部署到磁盘(模拟 CI 构建拷贝)

package com.baoma.ai.debug.support;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;

public final class SkillsAsCodeDeploySupport {

    private SkillsAsCodeDeploySupport() {
    }

    public static Path deployClasspathSkillsTo(Path targetSkillsRoot) throws IOException {
        Files.createDirectories(targetSkillsRoot);
        ClassLoader cl = SkillsAsCodeDeploySupport.class.getClassLoader();
        deploySkillDir(cl, targetSkillsRoot, "course-review");
        deploySkillDir(cl, targetSkillsRoot, "skill-creator");
        return targetSkillsRoot;
    }

    private static void deploySkillDir(ClassLoader cl, Path targetRoot, String skillName) throws IOException {
        Path skillDir = targetRoot.resolve(skillName);
        Files.createDirectories(skillDir);
        String prefix = "skills/" + skillName + "/";
        copyResource(cl, prefix + "SKILL.md", skillDir.resolve("SKILL.md"));
        copyIfExists(cl, prefix + "code-quality.md", skillDir.resolve("code-quality.md"));
        copyIfExists(cl, prefix + "content-accuracy.md", skillDir.resolve("content-accuracy.md"));
        copyIfExists(cl, prefix + "style-guide.md", skillDir.resolve("style-guide.md"));
        copyIfExists(cl, prefix + "outdated-api.md", skillDir.resolve("outdated-api.md"));
    }

    private static void copyResource(ClassLoader cl, String cp, Path dest) throws IOException {
        try (InputStream in = cl.getResourceAsStream(cp)) {
            if (in == null) {
                throw new IOException("classpath 资源不存在: " + cp);
            }
            Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
        }
    }

    private static void copyIfExists(ClassLoader cl, String cp, Path dest) throws IOException {
        try (InputStream in = cl.getResourceAsStream(cp)) {
            if (in != null) {
                Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
            }
        }
    }

    public static void copyDirectoryFromClasspath(Path targetSkillsRoot) throws IOException {
        deployClasspathSkillsTo(targetSkillsRoot);
    }
}

从磁盘加载:FileSystemSkillRepository

package com.baoma.ai.debug;

import com.baoma.ai.debug.support.SkillsAsCodeDeploySupport;
import io.agentscope.core.skill.AgentSkill;
import io.agentscope.core.skill.repository.FileSystemSkillRepository;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Path;

class FileSystemSkillRepositoryAgentScopeTest {

    @TempDir
    Path skillsRoot;

    @Test
    @DisplayName("FileSystemSkillRepository 从磁盘加载 course-review")
    void loadSkillFromFilesystem() throws Exception {
        SkillsAsCodeDeploySupport.deployClasspathSkillsTo(skillsRoot);

        try (FileSystemSkillRepository repo = new FileSystemSkillRepository(skillsRoot)) {
            Assertions.assertTrue(repo.skillExists("course-review"));

            AgentSkill skill = repo.getSkill("course-review");
            Assertions.assertEquals("course-review", skill.getName());
            Assertions.assertTrue(skill.getDescription().contains("审查"));
            Assertions.assertTrue(skill.getSkillContent().contains("审查流程"));

            System.out.println("Repository source: " + repo.getSource());
            System.out.println("Skill version metadata: " + skill.getMetadataValue("version"));
        }
    }
}

frontmatter CI 校验器

package com.baoma.ai.debug.support;

import io.agentscope.core.skill.util.MarkdownSkillParser;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

public final class SkillFrontmatterValidator {

    private static final Set<String> REQUIRED_FIELDS = Set.of(
        "name", "description", "owner", "version", "last-reviewed"
    );

    private SkillFrontmatterValidator() {
    }

    public static List<String> validateSkillFile(Path skillMdPath) throws IOException {
        List<String> errors = new ArrayList<>();
        if (!Files.isRegularFile(skillMdPath)) {
            errors.add("文件不存在: " + skillMdPath);
            return errors;
        }
        String content = Files.readString(skillMdPath);
        var parsed = MarkdownSkillParser.parse(content);
        Map<String, Object> meta = parsed.getMetadata();
        for (String field : REQUIRED_FIELDS) {
            if (!meta.containsKey(field) || meta.get(field) == null
                || meta.get(field).toString().isBlank()) {
                errors.add("缺少必需字段: " + field);
            }
        }
        if (parsed.getContent() == null || parsed.getContent().isBlank()) {
            errors.add("Markdown body 为空");
        }
        return errors;
    }
}

单测:PR 阶段 frontmatter 门禁

package com.baoma.ai.debug;

import com.baoma.ai.debug.support.SkillFrontmatterValidator;
import com.baoma.ai.debug.support.SkillsAsCodeDeploySupport;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;

import java.nio.file.Path;
import java.util.List;

class SkillFrontmatterValidationTest {

    @TempDir
    Path skillsRoot;

    @Test
    @DisplayName("course-review SKILL.md 通过 frontmatter 校验")
    void courseReviewSkillPassesValidation() throws Exception {
        SkillsAsCodeDeploySupport.deployClasspathSkillsTo(skillsRoot);

        Path skillMd = skillsRoot.resolve("course-review/SKILL.md");
        List<String> errors = SkillFrontmatterValidator.validateSkillFile(skillMd);

        System.out.println("校验结果: " + (errors.isEmpty() ? "通过" : errors));
        Assertions.assertTrue(errors.isEmpty(), () -> String.join("; ", errors));
    }
}

GitHub Actions 示例(Maven 评测)

name: validate-skills
on:
  pull_request:
    paths:
      - 'module-ai/src/test/resources/skills/**'
      - 'skills/**'
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Check SKILL.md metadata
        run: mvn -pl module-ai test -Dtest=SkillFrontmatterValidationTest
      - name: Run skill evaluation
        env:
          DASHSCOPE_API_KEY: ${{ secrets.DASHSCOPE_API_KEY }}
        run: mvn -pl module-ai test -Dtest=CourseReviewSkillBoxAgentScopeTest

废弃与清理(建议每季度)

  • 过去 3 个月未被调用 → 考虑归档
  • 对应的业务流程已取消 → 立即删除
  • 与其他 Skill 功能重叠 → 合并精简

核心度量

指标 定义 意义
复用率 Skill 被不同成员调用的频次 只有被反复调用的 Skill 才是有效资产
交付标准一致性 新手使用 Skill 后的产出 vs 专家基线 衡量 Skill 是否真正传递了专家能力

5.5 落地路径:从种子团队到全面推广

理解了所有权和工程化管理之后,推荐 「种子 Skill」 策略,不要一步全员铺开:

步骤 动作
1 选 3~5 人先锋队
2 聚焦一个高价值痛点(课程审核、Code Review、数据清洗)
3 跑通闭环:识别专家知识 → 封装 Skill → 评测验证 → 团队试用
4 用数据说服其余团队(审核时间缩短 X%、质量达专家水平 Y%)

4 周落地清单

周次 动作 产出
W1 课程审查场景 course-review + 至少 1 个 @Tool
W2 frontmatter + CODEOWNERS frontmatter 单测进 CI
W3 2 个黄金 Notebook 样例 SkillBox 审查回归门禁
W4 skill-creator 第二个 Skill api-doc-review 初稿 + 人审 + 评测

治理演进

维度 试点期 推广期
谁写 技术骨干 + 业务专家 任何成员可提交
谁审 同行 Code Review Skill 审核组 / CODEOWNERS
谁批准 团队负责人 领域负责人

引入 Skill 初期(1~3 个月)效率可能不升反降——学习规范、调试 Agent、建评测都需要时间。让管理层有预期,用试点数据建立信心。

责任归属:Agent 调用 Skill 产出经人合并上线时,Merge 即负责——点合并的人对结果承担最终责任。

小结回顾:

1、推广困境:个人构建的 Skill 在团队中使用时,会遇到版本混乱、误修改和知识断档三类问题.
2、能力放大器:Skill 的核心价值是将专家能力规模化复制,但需要所有权和工程化流程来保障
3、领域团队所有权:最有度量能力的团队拥有并维护对应领域的 Skill,通过 YAML 元数据和 CODEOWNERS 明确权责。

4、Skills-as-Code:将 Skill 当作代码管理,走 PR → Review → CI 评测 → 部署的完整工程生命周期。
5、落地策略:从种子团队开始,聚焦高价值痛点,用数据说话,逐步推广到全组织。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容