🚄 前言
Memory 让 Agent 记住了你的偏好,但具体的工作方法每次还是要在对话里重新交代。Skill 就是解决这个问题的:把「在什么情况下,正确做法是什么」固化为可触发的专属流程。本系列上篇先走完从 Prompt 到可复用知识库的路径——这是写出高质量 Skill 的基础。
🍁 文章目标
你将学到:
- 理解从「临时 Prompt」到「可复用 Skill」的演进逻辑(上篇:知识库阶段)
- Skill 的结构:触发条件、工作流、知识库(下篇展开 AgentSkill)
- 如何写出高质量的 Skill(五步编写法,下篇)
- Skills-as-Code:让 Skill 进入版本控制和 CI/CD(下篇)
- Skill 生态:社区共享与复用(下篇)
回顾:Agent 能力已完备,但「标准」在人脑子里
你先为 Agent 装上了工具调用、反思和记忆等单体能力。你大概试过让 AI 审代码、改文章、检查文档——反馈却常隔靴搔痒:要么太泛(「建议增加注释」),要么抓不住重点。
问题不在于 Agent 能力不够,而在于它不知道你的标准是什么。
问题引入:
假设你收到一个任务:审查一份同事写的技术教程。这份教程是 docs 目录下的《Python 数据分析实战》——一个完整的 Jupyter Notebook,包含近 300 个单元格,从环境准备、数据清洗、特征工程一直到可视化和综合业务分析。你最容易想到的方法就是:用你已有的 Agent,直接给它一句话。
1 用 Prompt 做教程审查
最容易想到的办法:给已有的 ReActAgent 装上 read_file,然后一句话交代任务。
Java 单测:通用 Prompt 审查
文件:module-ai/src/test/java/com/baoma/ai/debug/CourseReviewGenericPromptAgentScopeTest.java
package com.baoma.ai.debug;
import com.baoma.ai.debug.support.CourseReviewAgentSupport;
import com.baoma.ai.debug.support.CourseReviewFileTools;
import com.baoma.ai.debug.support.CourseReviewScopeTestSupport;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.tool.Toolkit;
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;
/** §1 用通用 Prompt 审查教程:反馈偏泛,难以命中团队细分标准。 */
class CourseReviewGenericPromptAgentScopeTest {
@TempDir
Path workspace;
@BeforeEach
void copySampleNotebook() throws Exception {
Files.createDirectories(workspace);
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:无审查标准时报告偏通用")
void genericPromptReview() {
String apiKey = CourseReviewScopeTestSupport.requireDashScopeApiKey();
String modelName = CourseReviewScopeTestSupport.reviewModelName();
CourseReviewScopeTestSupport.printApiKeyLoaded(apiKey);
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new CourseReviewFileTools(workspace));
ReActAgent agent = CourseReviewAgentSupport.createReviewer(
apiKey,
modelName,
toolkit,
"你是一个 AI 助手,能够帮助用户完成各种任务。",
10);
String notebook = workspace.resolve("sample-course.ipynb").toString();
Msg user = Msg.builder()
.textContent("帮我审查一下 " + notebook + ",看看质量怎么样、能不能发布。"
+ "你可以用 read_file 读取该文件。")
.build();
System.out.println("[user]: " + user.getTextContent());
Msg reply = Objects.requireNonNull(agent.call(user).block(), "Agent 返回为空");
String text = Objects.requireNonNullElse(reply.getTextContent(), "");
System.out.println("\n=== 审查报告(通用 Prompt)===\n" + text);
Assertions.assertFalse(text.isBlank());
System.out.println("\n[观测] 若报告多为「补充注释/异常处理」等泛化建议,而未点出 openpyxl/众数/俏皮话,即符合课件预期。");
}
}
运行:
mvn -pl module-ai test -Dtest=CourseReviewGenericPromptAgentScopeTest
运行后你会看到什么
Agent 往往给出「放之四海而皆准」的建议:补充注释、增加异常处理、优化图表等——换成任何教程都成立,是正确的废话。
而人工抽查样例 Notebook 会发现它没提的、仅对本教程成立的问题:
| 问题类型 | 样例中的表现 |
|---|---|
| 代码跑不通 | 使用 pd.ExcelWriter(..., engine='openpyxl'),环境准备却未安装 openpyxl |
| 说一套做一套 | 计划写「众数填充」,代码却 fillna('未知')
|
| 讲解风格 | 开篇小说式描写、俏皮话(「数据自带惊喜」) |
团队真正在乎的标准(通用模型不知道)
- 代码可执行性:依赖、路径、自上而下能否执行
- 内容准确性:API 与文档一致、文字与代码一致
- 学习曲线:渐进式编排、有无跳跃
- 讲解风格:禁止俏皮话与多余情景描写
小贴士:审查任务要获得有针对性的反馈,关键不是措辞技巧,而是提供明确的审查标准。
把标准写进 Prompt 仍会遇到的麻烦
即便写出很长的 Prompt(含 .ipynb JSON 结构说明、四项标准、输出格式),仍会面临:
| 痛点 | 说明 |
|---|---|
| 找不到 | 标准散落在聊天记录、文档或脑子里 |
| 每次重贴 | 新会话要复制整段 Prompt |
| 口径不一 | 同事各写一版,结果不可比 |
| 版本混乱 | 上次补充的规则不知进了哪一版 |
根源:Prompt 是会话级的,对话结束知识就散。你需要的是可复用、基于文件的专业知识模块——写一次、进 Git、按需加载。
2 把审查经验沉淀下来
2.1 从聊天记录到独立文件
把审查标准保存为 course-review-single.md,Agent 用 read_file 按文件审查。
知识库文件(classpath):module-ai/src/test/resources/course-review/course-review-single.md
单测:module-ai/src/test/java/com/baoma/ai/debug/CourseReviewWithSingleGuideAgentScopeTest.java
package com.baoma.ai.debug;
import com.baoma.ai.debug.support.CourseReviewAgentSupport;
import com.baoma.ai.debug.support.CourseReviewFileTools;
import com.baoma.ai.debug.support.CourseReviewKnowledgeBaseWriter;
import com.baoma.ai.debug.support.CourseReviewScopeTestSupport;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.tool.Toolkit;
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 CourseReviewWithSingleGuideAgentScopeTest {
@TempDir
Path workspace;
@BeforeEach
void prepareWorkspace() throws Exception {
CourseReviewKnowledgeBaseWriter.copyKnowledgeBaseTo(workspace);
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:按 course-review-single.md 审查样例 Notebook")
void reviewWithSingleGuideFile() {
String apiKey = CourseReviewScopeTestSupport.requireDashScopeApiKey();
String modelName = CourseReviewScopeTestSupport.reviewModelName();
CourseReviewScopeTestSupport.printApiKeyLoaded(apiKey);
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new CourseReviewFileTools(workspace));
ReActAgent agent = CourseReviewAgentSupport.createReviewer(
apiKey, modelName, toolkit,
"你是教程审查员。必须先 read_file 读取审查标准与 Notebook,再按标准输出结构化报告。",
16);
Msg user = Msg.builder()
.textContent("""
按照 course-review/course-review-single.md 的标准,
审查工作区中的 sample-course.ipynb。
先用 read_file 读取标准与 Notebook;输出:状态/位置/说明。
""")
.build();
Msg reply = Objects.requireNonNull(agent.call(user).block(), "Agent 返回为空");
System.out.println(Objects.requireNonNullElse(reply.getTextContent(), ""));
Assertions.assertFalse(Objects.requireNonNullElse(reply.getTextContent(), "").isBlank());
}
}
运行:
mvn -pl module-ai test -Dtest=CourseReviewWithSingleGuideAgentScopeTest
核心用户消息:
按照 course-review/course-review-single.md 的标准,审查 sample-course.ipynb。
先用 read_file 读取标准与 Notebook。
收益:文件有固定位置、可 Git 管理、团队可共享。代价:单文件会继续膨胀。
2.2 从单文件到知识库目录
主文件保持精简,细节拆到子文件:
course-review/
├── README.md # 审查流程入口
├── code-quality.md # 代码可执行性
├── content-accuracy.md # 内容准确性
├── style-guide.md # 讲解风格正/反例
├── outdated-api.md # pandas/numpy 过时 API
└── course-review-single.md
Java 侧用 CourseReviewKnowledgeBaseWriter 将 classpath 资源复制到单测工作区(对应 Python 课件「脚本生成目录」):
文件:module-ai/src/test/java/com/baoma/ai/debug/support/CourseReviewKnowledgeBaseWriter.java
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;
/** 将 classpath 下 course-review/ 复制到磁盘工作区。 */
public final class CourseReviewKnowledgeBaseWriter {
private static final String[] KB_FILES = {
"README.md", "code-quality.md", "content-accuracy.md",
"style-guide.md", "outdated-api.md", "course-review-single.md"
};
public static Path copyKnowledgeBaseTo(Path targetDir) throws IOException {
Path kbRoot = targetDir.resolve("course-review");
Files.createDirectories(kbRoot);
ClassLoader cl = CourseReviewKnowledgeBaseWriter.class.getClassLoader();
for (String name : KB_FILES) {
String cp = "course-review/" + name;
try (InputStream in = cl.getResourceAsStream(cp)) {
if (in == null) {
continue;
}
Files.copy(in, kbRoot.resolve(name), StandardCopyOption.REPLACE_EXISTING);
}
}
return kbRoot;
}
}
2.3 应对长文档:分段检查
Notebook 近 300 单元格时,不宜一次性塞进上下文(Lost in the Middle:中段信息易被忽略)。
方法一:提取目录 — Java 实现 NotebookTocExtractor(对应 Python extract_toc.py):
文件:module-ai/src/test/java/com/baoma/ai/debug/NotebookTocExtractorTest.java
package com.baoma.ai.debug;
import com.baoma.ai.debug.support.NotebookTocExtractor;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
class NotebookTocExtractorTest {
@Test
@DisplayName("从样例 Notebook 提取 Markdown 标题目录")
void extractTocFromSampleNotebook() throws Exception {
String toc = NotebookTocExtractor.extractTocFromClasspath(
"agentscope-skill-demo/sample-course.ipynb");
System.out.println("=== Notebook 目录 ===\n" + toc);
Assertions.assertTrue(toc.contains("[Cell 0]"));
Assertions.assertTrue(toc.contains("环境准备") || toc.toLowerCase().contains("环境"));
}
}
运行(无需 API Key):
mvn -pl module-ai test -Dtest=NotebookTocExtractorTest
示例输出:
[Cell 0] # Python 数据分析实战(样例)
[Cell 1] ## 环境准备
Agent 还可通过工具 extract_notebook_toc 在审查流程中调用(见 CourseReviewFileTools)。
方法二:jupytext 转 Markdown(课件建议):适合人工批量编辑;Java 单测未封装,可在 CI 中用 shell 调用 jupytext --to md。
2.4 按需加载:不要一次塞全部
README.md 用表格索引子文件,Agent 按检查阶段 read_file:
| 检查阶段 | 建议加载 |
|---|---|
| 代码质量 |
code-quality.md + outdated-api.md
|
| 讲解风格 | style-guide.md |
| 长 Notebook | 先 extract_notebook_toc,再按章节细读 |
Java 单测:知识库 + 按需加载
文件:module-ai/src/test/java/com/baoma/ai/debug/CourseReviewKnowledgeBaseAgentScopeTest.java
package com.baoma.ai.debug;
import com.baoma.ai.debug.support.CourseReviewAgentSupport;
import com.baoma.ai.debug.support.CourseReviewFileTools;
import com.baoma.ai.debug.support.CourseReviewKnowledgeBaseWriter;
import com.baoma.ai.debug.support.CourseReviewScopeTestSupport;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.tool.Toolkit;
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;
/** §2.2 + §2.4:审查知识库目录 + 按需加载子文件。 */
class CourseReviewKnowledgeBaseAgentScopeTest {
@TempDir
Path workspace;
@BeforeEach
void prepareWorkspace() throws Exception {
CourseReviewKnowledgeBaseWriter.copyKnowledgeBaseTo(workspace);
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:知识库按需加载审查")
void reviewWithKnowledgeBaseOnDemand() {
String apiKey = CourseReviewScopeTestSupport.requireDashScopeApiKey();
String modelName = CourseReviewScopeTestSupport.reviewModelName();
CourseReviewScopeTestSupport.printApiKeyLoaded(apiKey);
Toolkit toolkit = new Toolkit();
toolkit.registerTool(new CourseReviewFileTools(workspace));
ReActAgent agent = CourseReviewAgentSupport.createReviewer(
apiKey,
modelName,
toolkit,
"""
你是教程审查员。工作流:
1. read_file course-review/README.md;
2. extract_notebook_toc sample-course.ipynb;
3. 按需 read_file 子规则文件,不要一次读完所有文件;
4. read_file sample-course.ipynb;
5. 输出结构化报告。
""",
24);
Msg user = Msg.builder()
.textContent("请审查工作区 sample-course.ipynb,使用 course-review/ 知识库,按需加载规则。")
.build();
Msg reply = Objects.requireNonNull(agent.call(user).block(), "Agent 返回为空");
String text = Objects.requireNonNullElse(reply.getTextContent(), "");
System.out.println("\n=== 审查报告(知识库按需加载)===\n" + text);
Assertions.assertFalse(text.isBlank());
}
}
运行:
mvn -pl module-ai test -Dtest=CourseReviewKnowledgeBaseAgentScopeTest
2.5 对比:改造前 vs 改造后
| 维度 | 改造前(一次性 Prompt) | 改造后(知识库目录) |
|---|---|---|
| 存放 | 散落在聊天记录 | 固定路径,可版本管理 |
| 结构 | 所有内容混在一起 | README 精简,子文件拆分 |
| 长 Notebook | 易截断 / 漏中间段 | 先 TOC,再分段检查 |
| 加载 | 每次塞全部 | 按需 read_file |
| 协作 | 一人一套标准 | 团队共享、可 PR 评审 |
flowchart LR
A[用户:审查教程] --> B[ReActAgent]
B --> C[read_file README]
C --> D[extract_notebook_toc]
D --> E[按需 read 子规则]
E --> F[read_file .ipynb]
F --> G[结构化审查报告]
共享工具类(Support)
审查相关单测共用以下类(完整源码见仓库):
| 文件 | 职责 |
|---|---|
CourseReviewFileTools.java |
read_file、extract_notebook_toc 工具 |
CourseReviewAgentSupport.java |
创建 CourseReviewer ReActAgent
|
CourseReviewScopeTestSupport.java |
API Key、模型名 |
NotebookTocExtractor.java |
解析 .ipynb 提取标题目录 |
与 Python 对照:
await agent(msg)→agent.call(msg).block();toolkit.register_tool_function→toolkit.registerTool(new CourseReviewFileTools(...))。