Skills 是可复用的指令与上下文包,智能体在相关任务时会自动发现并使用。通过 SkillRegistry 管理技能、SkillsAgentHook 注册
read_skill工具并注入技能列表到系统提示,模型在需要时调用read_skill(skill_name)按需加载完整内容。
核心概念
渐进式披露
系统提示中先只注入技能列表(name、description、skillPath);模型判断需要某技能时调用 read_skill(skill_name) 加载完整 SKILL.md;再按需访问技能目录下的资源或使用与该技能绑定的工具。
Skill 目录结构
每个技能一个子目录,必须包含 SKILL.md:
text
skill-name/
├── SKILL.md # 必需
├── references/ # 可选
├── examples/
└── scripts/
SKILL.md 格式规范
yaml
---
name: skill-name
description: This skill should be used when...
---
# 技能名称
正文:功能说明、使用方法、可用资源列表等。
必需字段 :name(建议小写字母、数字、连字符,最长 64 字符)、description(超长会被截断)。
在 Agent 中使用 Skills
使用 FileSystemSkillRegistry和 ClasspathSkillRegistry
智能体支持从本地文件系统中加载 skills 技能,以下示例假设 skills 在进程工作目录(技能放在 src/main/resources/skills),如:
bash
skills/
├── inventory_management/
├── SKILL.md
java
@Test
void testReadSkills() throws GraphRunnerException {
// SkillRegistry registry = FileSystemSkillRegistry.builder()
// .projectSkillsDirectory("D:\\java\\IdeaProjects\\agent-cloud\\skills-demo\\src\\main\\resources\\skills\\")
// .build();
SkillRegistry registry = ClasspathSkillRegistry.builder()
.classpathPath("skills")
.build();
System.out.println("开始加载Skills");
registry.listAll().forEach(skill -> System.out.println("Loaded skill: " + skill.getName()));
System.out.println("结束加载Skills");
SkillsAgentHook hook = SkillsAgentHook.builder()
.skillRegistry(registry)
.build();
ReactAgent agent = ReactAgent.builder()
.name("skills-agent")
.model(chatModel)
.saver(new MemorySaver())
.hooks(List.of(hook))
.build();
AssistantMessage result = agent.call("请介绍你有哪些技能");
System.out.println(result.getText());
}
观察日志:
首先是文件扫描成功:
properties
Loaded skill: inventory_management from .../skills/inventory_management
然后是技能被注册到了 Hook 中:
properties
Skills reloaded: 1 total skills
大模型回复中明确列出了 inventory_management 及其描述。这证明 SkillsAgentHook 成功将技能的 Name 和 Description 注入到了系统提示词中。
阅读日志中发送给大模型的 ChatCompletionRequest 部分:
properties
"content": "## Skills System ... (省略) ... \n\n### Available Skills\n\n**Project Skills:**\n- **inventory_management**: Manages the inventory of the warehouse. ..."
SkillsAgentHook 拦截了请求,将所有加载的技能列表(仅包含 Name 和 Description)拼接成了一段 Markdown 文本,作为 System Message 发送给了大模型。
大模型知道它有一个叫 inventory_management 的工具可以用,但不知道具体怎么执行,这就是"渐进式披露"。
虽然现在加载成功了,但目前的测试只走完了第一步(模型知道有 inventory_management),还需要测试当模型决定使用该技能时,会调用 read_skill 工具去读取 SKILL.md 的正文内容。
更换问题:
java
AssistantMessage result = agent.call("请详细介绍一下 inventory_management 技能的具体操作步骤。");
观察日志:
模型最初只知道 inventory_management 这个名字和一句简短的描述("Manages the inventory...")。它并不知道具体的数据库表结构。
当你问它"具体操作步骤"时,模型意识到仅凭简短描述无法回答,于是它主动调用 了 read_skill 工具。
properties
"toolCalls": [ToolCall[id=..., function=ChatCompletionFunction[name=read_skill, arguments={"skill_name": "inventory_management"}]]
系统将 SKILL.md 的正文内容(包含 Tables 和 Business Logic)注入给模型后,模型成功输出了详细的数据库结构和 SQL 示例。
渐进式工具 Tool 披露
接下来通过将工具与 Skill 技能名绑定,可以做到工具跟随 Skill 实现渐进式披露:仅当模型对该技能调用了 read_skill 后,对应工具才会加入当次请求,实现按需暴露。激活后该技能的工具在会话后续轮次中仍可用。
java
@Test
void testProgressiveToolDisclosure() throws GraphRunnerException {
// 1. 初始化注册表
SkillRegistry registry = ClasspathSkillRegistry.builder()
.classpathPath("skills")
.build();
// 2. 创建一个模拟工具
// 假设这个工具是用来"执行 Python 脚本"的,或者任何具体的业务工具
ToolCallback dummyTool = FunctionToolCallback.builder("execute_inventory_script", (Map<String, Object> args) -> {
System.out.println(">>> 工具被调用了!执行库存脚本...");
System.out.println(">>> 收到的参数: " + args);
return "SUCCESS: 库存已更新";
})
.description("执行库存管理的 Python 脚本,参数为操作类型")
.inputType(Map.class)
.build();
// 3. 【关键步骤】配置绑定关系 (Grouped Tools)
// 只有当技能名是 'inventory_management' 时,才暴露 'execute_inventory_script'
Map<String, List<ToolCallback>> groupedTools = Map.of(
"inventory_management", // 这里必须和 SKILL.md 中的 name 完全一致
List.of(dummyTool)
);
// 4. 构建 Hook (注入绑定配置)
SkillsAgentHook hook = SkillsAgentHook.builder()
.skillRegistry(registry)
.groupedTools(groupedTools) // <--- 注入绑定
.build();
// 5. 构建 Agent (注意:不要在全局 .tools() 中注册 dummyTool,否则就变成全量披露了)
ReactAgent agent = ReactAgent.builder()
.name("skills-agent")
.model(chatModel)
.saver(new MemorySaver())
.hooks(List.of(hook)) // 仅通过 Hook 管理工具
.build();
// 6. 发起调用
// 这里的 Prompt 设计很关键:先让模型读技能,再诱导它使用工具
String prompt = """
请详细介绍一下 inventory_management 技能。
如果该技能支持通过脚本自动化操作,请使用相关工具执行一个"检查库存"的操作。
""";
AssistantMessage result = agent.call(prompt);
System.out.println(result.getText());
}
运行后分析下工具是如何"渐进"出现的:
查看第一次 Writing 请求

在模型读取技能详情之前,框架只把 read_skill 工具给了模型。此时模型"不知道"有脚本可以执行,符合"按需暴露"的第一步。
然后模型根据 Prompt 决定先读取技能详情,调用了 read_skill("inventory_management"),SkillsInterceptor 捕获到了这个动作。

这一行日志证明了框架在运行时动态地将 execute_inventory_script 绑定到了当前会话中。
再查看第二次 Writing 请求

在第二次请求中,execute_inventory_script 工具已经成功出现在工具列表中。模型利用这个新出现的工具,生成了调用指令。
这个测试完美演示了"渐进式披露"的三个核心优势:
- Token 节省 :第一次请求时,模型不需要阅读
execute_inventory_script的描述,节省了上下文 Token。 - 防误触 :如果模型在第一步判断这不是一个库存任务,它永远不会看到
execute_inventory_script,从而避免了模型在没有阅读说明书的情况下乱调用脚本。 - 按需加载 :只有当模型明确调用
read_skill读取了inventory_management的说明书后,框架才"授予"它使用脚本的权限。
测试自动重载技能
每次 Agent 执行前会调用 registry.reload()(若实现支持;不支持则抛 UnsupportedOperationException,Hook 会捕获并打 debug 日志)。
注意,每次 Agent 执行可能包含多次模型推理,registry.reload() 仅会在第一次推理时执行并加载最新的 skills,这样能保证同一次 Agent 执行时行为的连续性。
java
@Test
void testAutoReloadSkill() throws GraphRunnerException, InterruptedException {
SkillRegistry registry = FileSystemSkillRegistry.builder()
.projectSkillsDirectory("path\\to\\skills\\")
.build();
// 构建 Hook 并开启 autoReload
SkillsAgentHook hook = SkillsAgentHook.builder()
.skillRegistry(registry)
.autoReload(true)
.build();
ReactAgent agent = ReactAgent.builder()
.name("reload-agent")
.model(chatModel)
.saver(new MemorySaver())
.hooks(List.of(hook))
.build();
System.out.println("=== 测试开始 ===");
// 第一次调用(此时应该加载旧内容)
System.out.println("\n--- 第 1 次调用 Agent ---");
AssistantMessage result1 = agent.call("请调用 read_skill 读取 test-reload 技能的内容,并告诉我里面写了什么。");
System.out.println("Agent 回复: " + result1.getText());
System.out.println("现在去修改 skills/test-reload/SKILL.md 的文件内容。");
System.out.println("修改完保存文件,等待 20 秒...");
Thread.sleep(20000);
// 第二次调用(此时 SkillsAgentHook 应该会触发 reload,加载新内容)
System.out.println("\n--- 第 2 次调用 Agent ---");
AssistantMessage result2 = agent.call("请再次读取 test-reload 技能的内容。");
System.out.println("Agent 回复: " + result2.getText());
}
观察日志输出:

