Spring AI Alibaba Skills 的渐进式披露与热更新实战

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 成功将技能的 NameDescription 注入到了系统提示词中。

阅读日志中发送给大模型的 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 工具已经成功出现在工具列表中。模型利用这个新出现的工具,生成了调用指令。

这个测试完美演示了"渐进式披露"的三个核心优势:

  1. Token 节省 :第一次请求时,模型不需要阅读 execute_inventory_script 的描述,节省了上下文 Token。
  2. 防误触 :如果模型在第一步判断这不是一个库存任务,它永远不会看到 execute_inventory_script,从而避免了模型在没有阅读说明书的情况下乱调用脚本。
  3. 按需加载 :只有当模型明确调用 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());
    }

观察日志输出:

相关推荐
netyeaxi2 小时前
Spring:如何查看Spring应用对外提供了哪些API接口?
java·spring
一只大袋鼠2 小时前
MySQL 事务从入门到精通(上):概念、操作、特性、隔离级别全解析
java·mysql·事务
若鱼19192 小时前
JPA/Hibernate中一对一关联时不持有外键方的属性延迟加载为什么不生效?
java·spring
凯尔萨厮2 小时前
创建SpringWeb项目(Spring2.5)半注解
spring·mvc
砍材农夫2 小时前
spring-ai 第八模型介绍-图像模型
java·人工智能·spring
rrrjqy2 小时前
深入浅出 RAG:万物皆可向量化 (Embedding) 与 Spring AI + pgvector 实战
人工智能·spring·embedding
金融数据出海2 小时前
java对接美股股票api涵盖实时行情、K 线、指数等核心接口。
后端
认真的小羽❅2 小时前
从入门到精通:Spring Boot 整合 MyBatis 全攻略
spring boot·后端·mybatis
橘子hhh2 小时前
Netty基础服务器实现
java·nio