大家好,我是一言,继续更新 Spring AI 2.0 系列。
前两篇,我们用 SKILL.md 编排了订单操作,又把退款规则、计算脚本和客服模板拆进了 Skill 目录。
这些 Skill 都放在项目本地。Agent 部署成多个实例后,规则更新要重新发版,各实例还可能加载不同版本。
Nacos 3.2 开始提供 Skill Registry,可以集中存储、审核、发布和分发 Skill。这篇继续订单退款案例,只跑 Skill 的交付链路。
整个测试过程中 Java 进程没有重启:

Nacos 管版本,SkillsTool 管加载#
Nacos Skill Registry 负责 Skill 包、版本和标签,SkillsTool 负责把 Skill 暴露给模型。两者之间的 Java 适配由本文实现:
Nacos → ZIP / 变更事件 → Java 适配 → SkillsTool → ChatClient实测版本为 Java 21、Spring AI 2.0.1、spring-ai-agent-utils 0.12.0 和 Nacos 3.2.3。
我发布了同一个 order-refund Skill 的两个版本:
| 版本 | 退款规则 |
|---|---|
| 1.0.0 | 统一进入标准审核 |
| 1.1.0 | 金额达到 200 元时进入 MANAGER_REVIEW |

stable 可以理解成一个“当前使用版本”的开关。把它从 1.0.0 拨到 1.1.0,应用会自动换上新规则;新规则有问题,再拨回 1.0.0。整个过程不用重启应用。
Spring 应用订阅 stable#
Properties properties = new Properties();
properties.setProperty(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:8848");
properties.setProperty(PropertyKeyConst.NAMESPACE, "public");
properties.setProperty("nacosAiTransportMode", "http");
AiService aiService = AiFactory.createAiService(properties);
ReloadableSkillTool reloadableSkill = new ReloadableSkillTool();
AbstractNacosSkillListener listener = new AbstractNacosSkillListener() {
@Override
public void onEvent(NacosSkillEvent event) {
reloadableSkill.replace(buildSkill(event.getZipBytes()));
}
};
byte[] initialZip = aiService.subscribeSkill(
"order-refund", null, "stable", listener);
if (initialZip == null) {
throw new IllegalStateException("stable Skill not found");
}
reloadableSkill.replace(buildSkill(initialZip));订阅成功时,subscribeSkill 返回当前 ZIP;Skill 不存在时可能返回 null。标签指向或包内容发生变化后,监听器会收到新 ZIP、resolvedVersion 和 md5。
完成本文的回滚测试后,两个版本仍处于上线状态,stable 已重新指向 1.0.0。

buildSkill 将 ZIP 解压到新目录,再构建一个 SkillsTool:
ToolCallback buildSkill(byte[] zipBytes) {
Path skillDirectory = unzipAndValidate(zipBytes);
return SkillsTool.builder()
.addSkillsDirectory(skillDirectory.toString())
.build();
}ChatClient 只注册一次 reloadableSkill:
ChatClient chatClient = builder
.defaultToolCallbacks(reloadableSkill)
.build();ReloadableSkillTool 内部用 AtomicReference<ToolCallback> 保存当前 SkillsTool。它的元数据和调用方法都转发给当前对象,版本变化时只执行一次 current.set(next)。
实测遇到两个坑#
第一个是传输模式。Nacos 3.2.3 的 AiService 默认选择 gRPC,这个版本的 Skill 查询会直接报错:
ErrCode:501
Skill query is only supported via HTTP transport.在 3.2.3 里要显式切到 HTTP:
properties.setProperty("nacosAiTransportMode", "http");第二个是 SkillsTool 的加载时机。Builder 会读取 Skill 元数据和正文,已构建的 Tool 会继续使用旧内容。收到新 ZIP 后,要重建 SkillsTool 并替换 delegate。
真实 Nacos 跑一次#
Nacos 控制台可以直接比较两个版本。这个案例只修改了 SKILL.md,Diff 标出了 refund-v1 到 refund-v2,以及新增的 200 元审核规则。

我在本地启动 Nacos 3.2.3,通过真实 Console API 发布两个版本,再用真实 Java SDK 订阅 stable:
RUN_NACOS_TEST=true mvn \
-Dtest=NacosSkillRegistryIntegrationTest test实测跑完了首次加载、热更新和回滚三步:


这次测试只验证 Nacos 发布、ZIP 分发、变更事件和 SkillsTool 刷新。模型选择 Skill 和调用 Tool 的链路,前两篇已经跑过。
最后#
单个应用、少量 Skill,继续放本地目录就够了。
多实例、多环境或多团队共用 Skill 时,Nacos 可以统一发布、按标签升级和快速回滚。更新 Skill 时,应用端不需重新打包。
Skill 的执行质量仍然取决于流程、Tool 和模型。Nacos 解决了一套 Skill 怎么稳定地交付给多个 Agent。
参考资料#
- Nacos 官方:Skill Registry
- Nacos 官方:Java SDK Skill 用法
- Spring Blog:Agent Skills 在 Spring AI 中的实现
- Nacos 3.2.3:NacosAiService
- Nacos 3.2.3:AiGrpcClient
