SkillBox

SkillBox居然没有类说明,但是看到一个@Deprecated的方法感觉还挺有意思的。

1
2
3
4
5
6
7
8
9
10
11
/**
* Creates a SkillBox without a toolkit.
*
* <p>This constructor will be removed in the next release. A SkillBox must hold a
* {@link Toolkit} to operate correctly. Relying on automatic toolkit assignment makes
* behavior less explicit and harder to reason about.
*/
@Deprecated
public SkillBox() {
this(null, null, null);
}

根据这段@Deprecated的描述可以看到,每个SkillBox都必须有一个Toolkit,或者说必须作为Toolkit存在,那是否说明其实在Agentscope框架中,Skill本身其实是作为一个Tool提供给Agent使用的呢,接着往下看。

参考Java-Agentscope官网的描述如下

渐进式披露机制
采用三阶段按需加载优化上下文:

  1. 初始化时仅加载元数据(~100 tokens/Skill)
  2. AI 判断需要时加载完整指令(<5k tokens)
  3. 按需访问资源文件。Tool 同样渐进式披露,仅在 Skill 激活时生效。

工作流程: 用户提问 → AI 识别相关 Skill → 调用 load_skill_through_path 工具加载内容并激活绑定的 Tool → 按需访问资源 → 完成任务

统一加载工具: load_skill_through_path(skillId, resourcePath) 提供单一入口加载技能资源

skillId 使用枚举字段, 确保只能从已注册的 Skill 中选择, 保证准确性

resourcePath 是相对于 Skill 根目录的资源路径(如 references/api-doc.md)

路径错误时会返回所有可用的资源路径列表,帮助 LLM 纠正

适应性设计
我们将 Skill 进行了进一步的抽象,使其的发现和内容加载不再依赖于文件系统,而是 LLM 通过 Tool 来发现和加载 Skill 的内容和资源。同时为了兼容已有的 Skill 生态与资源,Skill 的组织形式依旧按照文件系统的结构来组织它的内容和资源。

像在文件系统里组织 Skill 目录一样组织 Skill 的内容和资源吧!

以 Skill 结构 为例,这种目录结构的 Skill 在我们的系统中的表现形式就是:

1
2
3
4
5
6
7
8
9
AgentSkill skill = AgentSkill.builder()
.name("data_analysis")
.description("Use this skill when analyzing data, calculating statistics, or generating reports")
.skillContent("# Data Analysis\n...")
.addResource("references/api-doc.md", "# API Reference\n...")
.addResource("references/best-practices.md", "# Best Practices\n...")
.addResource("examples/example1.java", "public class Example1 {\n...\n}")
.addResource("scripts/process.py", "def process(data): ...\n")
.build();

SKILL.md 格式规范

name: skill-name # 必需: 技能名称(小写字母、数字、下划线)
description: This skill should be used when… # 必需: 触发描述,说明何时使用

技能名称

功能概述

[详细说明该技能的功能]

使用方法

[使用步骤和最佳实践]

可用资源

  • references/api-doc.md: API 参考文档
  • scripts/process.py: 数据处理脚本

那么根据官方的文档,所谓渐进式披露,我们也分三个阶段来看Skill是如何加载到Agent中的。

初始化

ReActAgent出发,看一下把Skill注册到Agent中发生了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Configures SkillBox integration.
*
* <p>This method automatically:
* <ul>
* <li>Registers skill load tool to the toolkit
* <li>Adds the skill hook to inject skill prompts and manage skill activation
* <li>Uploads skill files to the upload directory if auto upload is enabled
* </ul>
*/
private void configureSkillBox(Toolkit agentToolkit) {
skillBox.bindToolkit(agentToolkit);
// Register skill loader tools to toolkit
skillBox.registerSkillLoadTool();

// If auto upload is enabled, upload skill files
if (skillBox.isAutoUploadSkill()) {
skillBox.uploadSkillFiles();
}

hooks.add(new SkillHook(skillBox));
}

代码基本就集中在这个方法中了,我们依次来看。

bindToolkit

说到这个bindToolkit,btw,看到ReActAgentbuild方法,发现所有的features本质上都是tool

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Builds and returns a new ReActAgent instance with the configured settings.
*
* @return A new ReActAgent instance
* @throws IllegalArgumentException if required parameters are missing or invalid
*/
public ReActAgent build() {
// Deep copy toolkit to avoid state interference between agents
Toolkit agentToolkit = this.toolkit.copy();
if (enableMetaTool) {
agentToolkit.registerMetaTool();
}
// Configure long-term memory if provided
if (longTermMemory != null) {
configureLongTermMemory(agentToolkit);
}
// Configure RAG if knowledge bases are provided
if (!knowledgeBases.isEmpty()) {
configureRAG(agentToolkit);
}
// Configure PlanNotebook if provided
if (planNotebook != null) {
configurePlan(agentToolkit);
}
// Configure SkillBox if provided
if (skillBox != null) {
configureSkillBox(agentToolkit);
}
return new ReActAgent(this, agentToolkit);
}

不过今天的重点是skill,别的部分就暂时不管了。

从代码里看,主要依托于SkillToolFactory类进行skill的绑定。

1
2
this.skillPromptProvider = new AgentSkillPromptProvider(skillRegistry, instruction, template);
this.skillToolFactory = new SkillToolFactory(skillRegistry, toolkit);

那就先看一下这个skillRegistry.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* Registry for managing skill registration and activation state.
*
* <p>This class provides basic storage and retrieval operations for skills.
*
* <p><b>Responsibilities:</b>
* <ul>
* <li>Store and retrieve skills
* <li>Track skill metadata and activation state
* </ul>
*
* <p><b>Design principle:</b>
* This is a pure storage layer. All parameters are assumed to be non-null
* unless explicitly documented. Parameter validation should be performed
* at the Toolkit layer.
*/
class SkillRegistry {
private final Map<String, AgentSkill> skills = new ConcurrentHashMap<>();
private final Map<String, RegisteredSkill> registeredSkills = new ConcurrentHashMap<>();
...
}

重点在这一段:

设计原则:

这是一个纯存储层。所有参数均假定为非空值。
除非另有明确说明。参数验证应在工具包层执行。

那么暂时其实也不用太管,毕竟这里只是一个存储层。

回到这个类:AgentSkillPromptProvider,你就可以发现其中skill的奥秘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public static final String DEFAULT_AGENT_SKILL_INSTRUCTION =
"""
## Available Skills

<usage>
Skills provide specialized capabilities and domain knowledge. Use them when they match your current task.

How to use skills:
- Load skill: load_skill_through_path(skillId="<skill-id>", path="SKILL.md")
- The skill will be activated and its documentation loaded with detailed instructions
- Additional resources (scripts, assets, references) can be loaded using the same tool with different paths

Path Information:
When you load a skill, the response will include:
- Exact paths to all skill resources
- Code examples for accessing skill files
- Usage instructions specific to that skill

Template fields explanation:
- <name>: The skill's display name
- <description>: When and how to use this skill
- <skill-id>: Unique identifier for load_skill_through_path tool
</usage>

<available_skills>

""";

// skillName, skillDescription, skillId
public static final String DEFAULT_AGENT_SKILL_TEMPLATE =
"""
<skill>
<name>%s</name>
<description>%s</description>
<skill-id>%s</skill-id>
</skill>

""";

然后就到了把上述的Skill作为Tool绑定到Toolkit的方法bindToolkit

1
2
3
4
/**
* Factory for creating skill access tools that allow agents to dynamically load and access skills.
*/
class SkillToolFactory {}

所以本质上所有的skill逻辑都在这里了。

registerSkillLoadTool

随后就可以在这个方法中看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* Registers skill access tools to the provided toolkit.
*
* <p>This method registers the following tool:
* <ul>
* <li>load_skill_through_path - Load skill resources or SKILL.md content. When a resource
* is not found, it automatically returns a list of available resources with SKILL.md
* as the first item.</li>
* </ul>
*
* @throws IllegalArgumentException if toolkit is null
*/
public void registerSkillLoadTool() {
if (toolkit == null) {
throw new IllegalArgumentException("Toolkit cannot be null");
}

if (toolkit.getToolGroup("skill-build-in-tools") == null) {
toolkit.createToolGroup(
"skill-build-in-tools",
"skill build-in tools, could contain(load_skill_through_path)");
}

toolkit.registration()
.agentTool(skillToolFactory.createSkillAccessToolAgentTool())
.group("skill-build-in-tools")
.apply();

logger.info("Registered skill load tools to toolkit");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* Creates the load_skill_through_path agent tool.
*
* <p>This tool allows agents to load and activate skills by their ID and resource path.
* It supports loading SKILL.md for skill documentation or other resources like scripts,
* configs, and templates.
*
* @return AgentTool for loading skill resources (including SKILL.md)
*/
AgentTool createSkillAccessToolAgentTool() {
return new AgentTool() {
@Override
public String getName() {
return "load_skill_through_path";
}

@Override
public String getDescription() {
return "Load and activate a skill resource by its ID and resource path.\n\n"
+ "**Functionality:**\n"
+ "1. Activates the specified skill (making its tools available)\n"
+ "2. Returns the requested resource content\n"
+ " usage instructions)\n"
+ "- 'SKILL.md': The skill's markdown documentation (name, description,"
+ "- Other paths: Additional resources like scripts, configs, templates, or"
+ " data files";
}

@Override
public Map<String, Object> getParameters() {
// Get all available skill IDs
List<String> availableSkillIds =
new ArrayList<>(skillRegistry.getAllRegisteredSkills().keySet());

return Map.of(
"type", "object",
"properties",
Map.of(
"skillId",
Map.of(
"type",
"string",
"description",
"The unique identifier of the" + " skill.",
"enum",
availableSkillIds),
"path",
Map.of(
"type",
"string",
"description",
"The path to the resource file within the"
+ " skill (e.g., 'SKILL.md,"
+ " references/references.md')")),
"required", List.of("skillId", "path"));
}

@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
try {
Map<String, Object> input = param.getInput();

// Validate parameters
String skillId = (String) input.get("skillId");
if (skillId == null || skillId.trim().isEmpty()) {
return Mono.just(
ToolResultBlock.error(
"Missing or empty required parameter: skillId"));
}

String path = (String) input.get("path");
if (path == null || path.trim().isEmpty()) {
return Mono.just(
ToolResultBlock.error("Missing or empty required parameter: path"));
}

String result = loadSkillResourceImpl(skillId, path);
return Mono.just(ToolResultBlock.text(result));
} catch (IllegalArgumentException e) {
logger.error("Error loading skill resource", e);
return Mono.just(ToolResultBlock.error(e.getMessage()));
} catch (Exception e) {
logger.error("Unexpected error loading skill resource", e);
return Mono.just(ToolResultBlock.error(e.getMessage()));
}
}
};
}

这样就把一个skill转化为了一个tool.

uploadSkillFiles

这个方法有点意思,在你没有文件markdown的时候,如果你用代码注册了一个skill,打开isAutoUploadSkill开关,那么在每次build的时候就会把skill创建markdown文件写到resource文件夹中。

SkillHook

最后来看看这个hook.

逻辑非常的简单,但是确实有效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
// Inject skill prompts
if (event instanceof PreReasoningEvent preReasoningEvent) {
String skillPrompt = skillBox.getSkillPrompt();
if (skillPrompt != null && !skillPrompt.isEmpty()) {
List<Msg> inputMessages = preReasoningEvent.getInputMessages();
int systemIndex = findFirstSystemMessageIndex(inputMessages);
if (systemIndex >= 0) {
// Merge skill prompt into existing system message in-place (structural)
Msg existingSystem = inputMessages.get(systemIndex);
List<ContentBlock> mergedContent = new ArrayList<>(existingSystem.getContent());
mergedContent.add(TextBlock.builder().text(skillPrompt).build());
Msg mergedMsg =
Msg.builder()
.id(existingSystem.getId())
.role(MsgRole.SYSTEM)
.name(existingSystem.getName())
.content(mergedContent)
.metadata(existingSystem.getMetadata())
.timestamp(existingSystem.getTimestamp())
.build();
List<Msg> newMessages = new ArrayList<>(inputMessages);
newMessages.set(systemIndex, mergedMsg);
preReasoningEvent.setInputMessages(newMessages);
} else {
// No existing system message, add one at the beginning
List<Msg> newMessages = new ArrayList<>(inputMessages.size() + 1);
newMessages.add(
Msg.builder()
.role(MsgRole.SYSTEM)
.content(TextBlock.builder().text(skillPrompt).build())
.build());
newMessages.addAll(inputMessages);
preReasoningEvent.setInputMessages(newMessages);
}
}
return Mono.just(event);
}

return Mono.just(event);
}

在推理前的hook中,merge原来的SystemMessage,如果没有的话就自己创建一个放在index0的位置。
然后其实只有一个要点:skillPrompt是取自哪里。

AgentSkillPromptProvider

简单来说就是把所有注册的skilltemplate拼在一起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
* Gets the skill system prompt for the agent.
*
* <p>Generates a system prompt containing all registered skills.
*
* @return The skill system prompt, or empty string if no skills exist
*/
public String getSkillSystemPrompt() {
StringBuilder sb = new StringBuilder();

// Check if there are any skills
if (skillRegistry.getAllRegisteredSkills().isEmpty()) {
return "";
}

// Add instruction header
sb.append(instruction);

// Add each skill
for (RegisteredSkill registered : skillRegistry.getAllRegisteredSkills().values()) {
AgentSkill skill = skillRegistry.getSkill(registered.getSkillId());
sb.append(
String.format(
template, skill.getName(), skill.getDescription(), skill.getSkillId()));
}

// Close available_skills tag
sb.append("</available_skills>");

return sb.toString();
}

INSTRUCTION + skill0-Template + skill1-Template + …

注意点:instructiontemplate 在一个 agent 中只会有一份,不会因skill不同而改变。默认的在上面也贴了。

可以理解为这里的template本质上就是初始化的披露。

完整指令

那么从上面基本上可以猜到,在SystemPrompt初始化披露之后,要进一步拿到完整指令,就是在Agent调用tool的时候。这里就不进一步展开了。

访问资源文件

那么换句话说,skill是通过什么访问资源文件的呢。是如何通过tool的调用访问的呢,那就需要进一步看一下这个tool是怎么声明的了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/**
* Implementation of skill resource loading logic.
*
* @param skillId The unique identifier of the skill
* @param path The path to the resource file
* @return The formatted resource content or error message with available resources
* @throws IllegalArgumentException if skill doesn't exist or resource not found
*/
private String loadSkillResourceImpl(String skillId, String path) {
AgentSkill skill = validatedActiveSkill(skillId);

// Special handling for SKILL.md - return the skill's markdown content
if ("SKILL.md".equals(path)) {
return buildSkillMarkdownResponse(skillId, skill);
}

// Get resource
Map<String, String> resources = skill.getResources();
if (resources == null || !resources.containsKey(path)) {
// Resource not found, return available resource paths
throw new IllegalArgumentException(
buildResourceNotFoundMessage(skillId, path, resources));
}

String resourceContent = resources.get(path);
return buildResourceResponse(skillId, path, resourceContent);
}

根据在SkillBox中配置的资源文件地址以及资源文件的内容进行提取。
但是根据代码中可以发现,resource是不可变的,在代码中实现了深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* Creates an AgentSkill with explicit parameters and custom source.
*
* <p>Use this constructor when you want to create a skill directly without parsing
* markdown. The source parameter indicates where the skill originated from.
*
* @param name Skill name (must not be null or empty)
* @param description Skill description (must not be null or empty)
* @param skillContent The skill implementation or instructions (must not be null or empty)
* @param resources Supporting resources referenced by the skill (can be null)
* @param source Source identifier for the skill (null defaults to "custom")
* @throws IllegalArgumentException if name, description, or skillContent is null or empty
*/
public AgentSkill(
String name,
String description,
String skillContent,
Map<String, String> resources,
String source) {
if (name == null || name.isEmpty() || description == null || description.isEmpty()) {
throw new IllegalArgumentException(
"The skill must have `name` and `description` fields.");
}
if (skillContent == null || skillContent.isEmpty()) {
throw new IllegalArgumentException("The skill must have content");
}

this.name = name;
this.description = description;
this.skillContent = skillContent;
this.resources = resources != null ? new HashMap<>(resources) : new HashMap<>();
this.source = source != null ? source : "custom";
}
1
this.resources = resources != null ? new HashMap<>(resources) : new HashMap<>();

那基本可以了解具体是怎么实现的Skill了,今天就先到这里好了。bye~