一、什么是 Skills
Skills 是可复用的指令与上下文包 ,Agent 在遇到相关任务时自动发现并使用。
三个核心组件:
| 组件 | 职责 |
|---|---|
SkillRegistry |
管理技能(加载、列举、读取内容) |
SkillsAgentHook |
注册 read_skill 工具 + 把技能列表注入系统提示 |
| 模型 | 判断需要时调用 read_skill(skill_name) 按需加载完整内容 |
二、核心机制:渐进式披露(Progressive Disclosure)
这是 Skills 的灵魂设计 ------ 不把所有内容一次性塞进上下文:
① 系统提示只注入「技能列表」:name + description + skillPath
② 模型判断需要某技能 → 调用 read_skill(skill_name)
③ 加载完整 SKILL.md 正文
④ 按需访问技能目录下的 references/ examples/ scripts/,或使用绑定的工具
价值:技能再多也不撑爆上下文;只有被真正用到的那部分才进入窗口。这与 ReAct、上下文工程的思路一脉相承。
三、Skill 目录结构
每个技能一个子目录,必须包含 SKILL.md:
skill-name/
├── SKILL.md # 必需
├── references/ # 可选:参考资料
├── examples/ # 可选:示例
└── scripts/ # 可选:脚本(Python/Shell 等)
SKILL.md 格式规范
markdown
---
name: skill-name
description: This skill should be used when...
---
# 技能名称
正文:功能说明、使用方法、可用资源列表等。
| 字段 | 要求 |
|---|---|
name |
必需。建议小写字母、数字、连字符,最长 64 字符 |
description |
必需。超长会被截断,所以要精炼 ------ 它决定模型是否会选中这个技能 |
description的写法直接决定技能能否被正确激活,和工具调用里@Tool(description=...)同理。
四、在 ReactAgent 中使用 Skills
4.1 FileSystemSkillRegistry(本地文件系统)
skills/ # 位于进程工作目录
├── pdf-extractor/
│ ├── SKILL.md
│ ├── references/
│ └── scripts/
java
SkillRegistry registry = FileSystemSkillRegistry.builder()
.projectSkillsDirectory(System.getProperty("user.dir") + "/skills")
.build();
SkillsAgentHook hook = SkillsAgentHook.builder()
.skillRegistry(registry)
.build();
ReactAgent agent = ReactAgent.builder()
.name("skills-agent")
.model(chatModel)
.saver(new MemorySaver())
.hooks(List.of(hook))
.build();
agent.call("请介绍你有哪些技能");
目录配置规则
| 配置项 | 说明 | 默认值 |
|---|---|---|
| `userSkillsDirectory(String | Resource)` | 用户级 |
| `projectSkillsDirectory(String | Resource)` | 项目级 |
同名技能:项目级覆盖用户级。
4.2 ClasspathSkillRegistry(打包进 JAR)
技能放在 src/main/resources/skills,随 JAR 分发。
java
SkillRegistry registry = ClasspathSkillRegistry.builder()
.classpathPath("skills")
.basePath("/tmp") // 可选:JAR 内资源复制目标目录,默认 /tmp
.build();
SkillsAgentHook hook = SkillsAgentHook.builder().skillRegistry(registry).build();
ReactAgent agent = ReactAgent.builder()
.name("skills-agent").model(chatModel)
.hooks(List.of(hook))
.build();
五、完整集成:Skills + Python + Shell
技能通常需要配合脚本执行才能落地。经典四件套组合:
java
// 1. 技能注册表:classpath:skills
SkillRegistry registry = ClasspathSkillRegistry.builder()
.classpathPath("skills")
.build();
// 2. Skills Hook:注册 read_skill + 注入技能列表
SkillsAgentHook skillsHook = SkillsAgentHook.builder()
.skillRegistry(registry)
.build();
// 3. Shell Hook:提供 Shell 命令执行
ShellToolAgentHook shellHook = ShellToolAgentHook.builder()
.shellTool2(ShellTool2.builder(System.getProperty("user.dir")).build())
.build();
// 4. 构建 Agent:Skills Hook + Shell Hook + Python 工具
ReactAgent agent = ReactAgent.builder()
.name("skills-integration-agent")
.model(chatModel)
.saver(new MemorySaver())
.tools(PythonTool.createPythonToolCallback(PythonTool.DESCRIPTION))
.hooks(List.of(skillsHook, shellHook))
.enableLogging(true)
.build();
AssistantMessage response = agent.call("请从 " + skillFilePath + " 文件中提取关键信息。");
要点
ShellTool2.builder(workDir):workDir是 Shell 执行的工作目录,常传System.getProperty("user.dir")。PythonTool.createPythonToolCallback(PythonTool.DESCRIPTION)即可用;自定义描述可传第二参数。- 技能列表里包含每个技能的
skillPath→ 模型用它拼出技能目录下文件的绝对路径,再交给 Python/Shell 处理。这是 Skills 能"带着资源干活"的关键。
六、高级用法
6.1 渐进式工具披露(groupedTools)
把工具绑定到技能名 上:只有模型对该技能调用过 read_skill 后,对应工具才加入当次请求;激活后在会话后续轮次中仍可用。
java
Map<String, List<ToolCallback>> groupedTools = Map.of(
"pdf-extractor", // 必须与 SKILL.md 的 name 一致
List.of(myTool)
);
SkillsAgentHook hook = SkillsAgentHook.builder()
.skillRegistry(registry)
.groupedTools(groupedTools)
.build();
这解决了"工具太多导致模型选择困难"的问题:工具跟随技能按需暴露。
6.2 自动重载(开发环境)
java
SkillsAgentHook hook = SkillsAgentHook.builder()
.skillRegistry(registry)
.autoReload(true)
.build();
- 每次 Agent 执行前调用
registry.reload(); - 不支持的 registry 会抛
UnsupportedOperationException,Hook 会捕获并打 debug 日志(不会崩); - ⚠️
reload()仅在第一次推理时执行一次,保证同一次 Agent 执行内行为连续 ------ 避免中途技能列表变化导致上下文错乱。
6.3 用户级 + 项目级共存
java
SkillRegistry registry = FileSystemSkillRegistry.builder()
.userSkillsDirectory("/home/user/saa/skills")
.projectSkillsDirectory("/app/project/skills")
.build();
// 同名技能:项目级优先
6.4 自定义系统提示模板
内置模板用于引导渐进式披露;可按自己的组织方式定制:
java
SystemPromptTemplate customTemplate = SystemPromptTemplate.builder()
.template("## 可用技能\n{skills_list}\n\n## 加载说明\n{skills_load_instructions}")
.build();
FileSystemSkillRegistry registry = FileSystemSkillRegistry.builder()
.projectSkillsDirectory("./skills")
.systemPromptTemplate(customTemplate)
.build();
模板变量只有两个:{skills_list} 和 {skills_load_instructions}。
6.5 扩展 SkillRegistry
实现接口即可接入自己的技能体系(数据库、对象存储、远程配置中心等):
get、listAll、contains、size、readSkillContent、
getSkillLoadInstructions、getRegistryType、getSystemPromptTemplate
(可选 reload())
SkillMetadata 需包含 name、description、skillPath(可选 source)。
参考实现:AbstractSkillRegistry、FileSystemSkillRegistry、ClasspathSkillRegistry。
七、在 Graph / ChatClient 链路中使用
非 ReactAgent 场景下,用 SkillPromptAugmentAdvisor (spring-ai-alibaba-graph-core)实现「技能发现」部分。
java
// 方式一:直接指定技能目录(内部自动创建 FileSystemSkillRegistry)
SkillPromptAugmentAdvisor skillAdvisor = SkillPromptAugmentAdvisor.builder()
.projectSkillsDirectory("./skills") // 或绝对路径
// .userSkillsDirectory("~/saa/skills") // 可选,默认 ~/saa/skills
.lazyLoad(false) // true = 首次请求时再加载
.build();
// 方式二:复用已有 SkillRegistry
SkillPromptAugmentAdvisor skillAdvisor = SkillPromptAugmentAdvisor.builder()
.skillRegistry(registry)
.build();
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultAdvisors(skillAdvisor)
.build();
String response = chatClient.prompt().user("请介绍你有哪些技能").call().content();
⚠️ 重要限制 :SkillPromptAugmentAdvisor 只注入技能列表与加载说明,不注册 read_skill 工具 。需要模型读取完整 SKILL.md 时,必须额外注册 read_skill(用带 SkillsAgentHook 的 Agent 节点,或单独注册 ReadSkillTool)。
选型对照
| 场景 | 方案 | 是否含 read_skill |
|---|---|---|
| ReactAgent | SkillsAgentHook |
✅ 自动注册 |
| ChatClient / Graph 节点 | SkillPromptAugmentAdvisor + 自行注册 ReadSkillTool |
❌ 需手动补 |
八、最佳实践与性能建议
- 控制 SKILL.md 大小 :单文件建议 1.5k--2k tokens ;长内容放
references/,正文只列路径。 - 技能名称保持一致 :
SKILL.md的name=read_skill参数 =groupedTools的 key。 - 按需使用 groupedTools :只有需要"随技能激活"的工具才放进去,其余走 Agent 的
.tools()。 - 技能通常需要配脚本:生产环境一般需要 Python / Shell 配合才能真正干活。
description要精炼:会被截断,且决定模型是否选中该技能。
常用 API
java
hook.listSkills(); // 列出所有技能
hook.hasSkill(name); // 是否存在
hook.getSkillCount(); // 技能数量
registry.reload(); // 重新加载(ClasspathSkillRegistry 支持)
九、速查卡
java
// 注册表
FileSystemSkillRegistry.builder().projectSkillsDirectory("./skills").build()
ClasspathSkillRegistry.builder().classpathPath("skills").basePath("/tmp").build()
// Hook
SkillsAgentHook.builder().skillRegistry(registry)
.groupedTools(Map.of("skill-name", List.of(tool)))
.autoReload(true)
.build()
// 挂载
ReactAgent.builder().hooks(List.of(skillsHook, shellHook))
.tools(PythonTool.createPythonToolCallback(PythonTool.DESCRIPTION))
// 非 Agent 链路
SkillPromptAugmentAdvisor.builder().projectSkillsDirectory("./skills").lazyLoad(false).build()
ChatClient.builder(chatModel).defaultAdvisors(skillAdvisor).build()
// 查询
hook.listSkills() / hook.hasSkill(name) / hook.getSkillCount() / registry.reload()
七条铁律
- 每个技能目录必须有
SKILL.md,缺少即视为无效技能。 name≤ 64 字符;description会被截断,务必精炼。groupedTools的 key 必须与SKILL.md的 name 完全一致。- 项目级技能覆盖用户级同名技能。
autoReload的reload()每次执行只跑一次(首次推理),不支持的 registry 会被 Hook 捕获为 debug 日志。SkillPromptAugmentAdvisor不提供 read_skill,需自行注册。- SKILL.md 建议 1.5k--2k tokens,长了拆到
references/。
十、对照本项目(xs-interview-agent)
有意思的是,项目的包名本身就是 skillsagentexample,出题提示词里也已经把 Skills 列为考察点之一:
| AI | AI应用 | tools、RAG、Agent、Skills |
但代码里只有三次裸 chatModel.call(),没有用到任何 Skills 机制。这是最值得改造的方向 ------ 而且 Skills 恰好是本项目的理想形态:
把三类面试模式改造成技能
src/main/resources/skills/
├── java-backend-interview/ # 现有:Java 后端
│ ├── SKILL.md
│ └── references/
│ └── resume-analysis-system.st # 直接搬现有 4 个 prompt
├── frontend-interview/ # 新增
├── algorithm-interview/ # 新增
└── ai-agent-interview/ # 新增
收益 :README 里 TODO 的"更多面试模式(前端、后端、算法等)"不用改一行业务代码,加个技能目录就完成扩展 ------ 这正是 Skills「复用指令包」的价值所在。
落地示例
java
SkillRegistry registry = ClasspathSkillRegistry.builder()
.classpathPath("skills")
.build();
SkillsAgentHook skillsHook = SkillsAgentHook.builder()
.skillRegistry(registry)
.groupedTools(Map.of(
"java-backend-interview", List.of(questionBankTool, saveReportTool)
))
.autoReload(true) // 开发期热更新面试题技能
.build();
ReactAgent interviewAgent = ReactAgent.builder()
.name("interview-agent")
.model(chatModel)
.hooks(List.of(skillsHook))
.saver(new RedisSaver(redissonClient))
.build();
RunnableConfig config = RunnableConfig.builder()
.threadId(resumeId)
.addMetadata("user_id", currentUserId)
.build();
迁移建议
- 现有四个
.st模板 → 作为对应技能的references/,SKILL.md里写清何时用、资源路径在哪。 description要写准,例如:"This skill should be used when the user uploads a resume for Java backend positions and needs scoring, question generation, or answer evaluation."- 题库/报告工具放
groupedTools,避免未激活时干扰模型选择。 - 保持
SKILL.md在 1.5k--2k tokens 内(现有resume-analysis-system.st约 5KB,需要瘦身或拆分到 references)。 - 成本优化:
autoReload只在开发环境开,生产用ClasspathSkillRegistry随 JAR 打包,避免热重载副作用。