
1. 从一次“技能改了但模型还在背旧说明书”说起Spring AI Alibaba Skills 是给智能体挂载可复用指令包的一套机制你可以把它理解成给模型准备了一排“说明书抽屉”平时只把抽屉标签技能名、一句话描述、路径贴在系统提示里模型真要用某个技能时再调用read_skill(skill_name)把对应SKILL.md的正文拉进上下文。这个“先给目录、后给正文”的做法就是渐进式披露它最直接的好处是省 Token、防误触也让技能目录可以越挂越多而不至于把上下文撑爆。但真正落到工程里麻烦往往不在第一次加载而在“改完技能文件之后”。我试过在本地调试一个库存管理技能把SKILL.md里的表结构从三张表改成四张表重启应用、重新跑 Agent结果模型回答里还是老三张表。排查半天才发现FileSystemSkillRegistry在构建时已经把技能扫描进内存SkillsAgentHook注入系统提示的也是那份快照文件改了但注册表没重载模型自然读的是旧内容。这就是热更新要解决的问题——让SkillRegistry在不重启进程的前提下重新扫描、重新注册并且保证同一次 Agent 执行内的行为连续。这篇就围绕SkillRegistry的注册、加载与动态刷新展开给出可复制的配置片段、渐进式披露的分层示例以及热更新触发与回滚的验证步骤。适合已经在用 Spring AI Alibaba 搭 Agent、想把 Skills 从“能跑”推进到“能改”的同学。核心检索词先摆在这Spring AI Alibaba Skills 渐进式披露与热更新重点就是SkillRegistry配置和SkillsAgentHook的autoReload。先说清楚 Skills 的目录约定不然后面配置对不上。每个技能一个子目录目录名建议和技能name一致里面必须有一个SKILL.mdskills/ ├── inventory_management/ │ ├── SKILL.md # 必需 │ ├── references/ # 可选放参考资料 │ ├── examples/ # 可选放示例 │ └── scripts/ # 可选放脚本 └── test-reload/ └── SKILL.mdSKILL.md用 YAML front matter 声明元信息正文写功能说明、使用方法、可用资源列表--- name: inventory_management description: This skill should be used when the user asks about warehouse inventory, stock levels, or inventory database operations. --- # 库存管理技能 ## 功能说明 管理仓库库存支持查询、更新、盘点。 ## 数据库表结构 - inventory_items库存主表 - inventory_logs操作日志 - warehouses仓库信息 - suppliers供应商信息 ## 使用方法 调用 execute_inventory_script 执行具体操作。name建议小写字母、数字、连字符最长 64 字符description超长会被截断所以要把“什么时候该用这个技能”写清楚因为渐进式披露第一阶段模型只能看到它。这里有个容易踩的坑description写得太泛比如“库存相关”模型判断不出该不该读就会漏掉技能写得太长又被截断关键触发词可能正好在截断之外。我的做法是把触发场景前置像上面那样把“asks about warehouse inventory, stock levels”放最前面。2. TaoToken 前置把模型通道和 Key 准备好Skills 本身不绑定具体模型供应商但ReactAgent需要一个chatModel才能跑起来。本地调试时我习惯把模型通道统一走 TaoToken这样换模型只改配置不改代码也方便对比不同模型在渐进式披露下的表现——有的模型很听话会主动read_skill有的会硬答这个差异只有真跑才看得出来。TaoToken 的定位是给开发者提供统一的模型调用入口兼容 OpenAI 风格的接口所以 Spring AI Alibaba 里用 OpenAI 兼容的ChatModel就能接。你需要先拿到 API Key入口在控制台的 API Keys 页面https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewriteBase URL 用https://taotoken.net/api注意这个地址不带 UTM 参数配置里原样填就行。模型 ID 按你实际要用的填比如claude-sonnet-4-5这类具体可用列表可以在模型对话页面试https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite如果你后面要长期跑编码类 Agent、频繁调 Skills可以看下 Coding Plan它更适合这种持续调用的场景https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite接入文档在这里遇到参数对不上可以翻https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite把 Key 和 Base URL 准备好之后先别急着写 Skills先用一个最小请求确认模型通道是通的。这一步能帮你把“模型不通”和“Skills 没加载”两类问题分开不然混在一起排查很痛苦。确认通道没问题再往下配SkillRegistry。3. 可复制配置SkillRegistry 与 SkillsAgentHook 三件套这一节给的是能直接抄的配置。Spring AI Alibaba 里 Skills 的核心是SkillRegistry和SkillsAgentHook两个东西前者负责扫描、加载、管理技能后者负责把技能列表注入系统提示、注册read_skill工具、处理渐进式工具披露和自动重载。先看模型配置用 application.yml 走 OpenAI 兼容通道spring: ai: openai: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} chat: options: model: claude-sonnet-4-5 temperature: 0.3对应的环境变量在启动前设好export TAOTOKEN_API_KEY你的Key然后是SkillRegistry的两种构建方式。开发期技能放在src/main/resources/skills下用ClasspathSkillRegistry最省事打包后也能读到SkillRegistry registry ClasspathSkillRegistry.builder() .classpathPath(skills) .build();如果你需要在不重新打包的情况下改技能文件就得用FileSystemSkillRegistry指向磁盘上的真实目录SkillRegistry registry FileSystemSkillRegistry.builder() .projectSkillsDirectory(D:/java/IdeaProjects/agent-cloud/skills-demo/src/main/resources/skills/) .build();两者的区别很关键ClasspathSkillRegistry读的是 classpath 里的资源改文件后通常要重新编译或重新打包才生效FileSystemSkillRegistry读的是磁盘路径配合autoReload才能做到改完文件下次调用就生效。热更新场景基本都用后者。接着是SkillsAgentHook这是把注册表和 Agent 连起来的桥SkillsAgentHook hook SkillsAgentHook.builder() .skillRegistry(registry) .autoReload(true) .build();autoReload(true)是热更新的开关。它的行为是每次 Agent 执行前调用一次registry.reload()但只在同一次 Agent 执行的第一次推理时执行后续多轮推理不再重载。这个设计是为了保证同一次执行内行为连续——如果模型第一轮读了技能 A 的旧内容第二轮技能 A 被重载成新内容模型前后看到的东西不一致回答就会自相矛盾。最后把 hook 挂到ReactAgent上ReactAgent agent ReactAgent.builder() .name(skills-agent) .model(chatModel) .saver(new MemorySaver()) .hooks(List.of(hook)) .build();注意这里没有在.tools()里注册任何业务工具。这是渐进式工具披露的前提业务工具要通过groupedTools绑定到技能上由 hook 在模型读取技能后才动态暴露。如果你在全局.tools()里注册了那就变成全量披露模型一上来就能看到所有工具渐进式就白做了。三件套凑齐就是Base URLhttps://taotoken.net/api KeyTAOTOKEN_API_KEY Model IDclaude-sonnet-4-5。这三个在模型配置里SkillRegistry和 hook 负责技能侧两边都配好才能跑通完整链路。4. 验证请求从技能列表到 read_skill 再到工具披露配置写完得用请求验证每一层是否按预期工作。渐进式披露分三层我建议一层一层验别一次全上。第一层验证技能列表是否注入系统提示。先只挂 hook不绑工具发一个“请介绍你有哪些技能”AssistantMessage result agent.call(请介绍你有哪些技能); System.out.println(result.getText());预期日志里能看到文件扫描成功Loaded skill: inventory_management from .../skills/inventory_management Skills reloaded: 1 total skills模型回复里应该明确列出inventory_management及其描述。如果你去翻发给模型的ChatCompletionRequest会看到系统消息里有一段类似### Available Skills **Project Skills:** - **inventory_management**: Manages the inventory of the warehouse...到这一步模型只知道“有个叫 inventory_management 的技能”不知道具体怎么执行。这就是渐进式披露的第一阶段只给目录不给正文。第二层验证模型是否主动调用read_skill。换个问题问具体操作步骤AssistantMessage result agent.call(请详细介绍一下 inventory_management 技能的具体操作步骤。);预期日志里出现工具调用toolCalls: [ToolCall[id..., functionChatCompletionFunction[nameread_skill, arguments{skill_name: inventory_management}]]]系统把SKILL.md正文注入后模型才能输出详细的表结构和 SQL 示例。如果模型没调read_skill而是硬答通常是description没写清楚触发场景或者模型本身对工具调用不敏感换个模型试试。第三层验证渐进式工具披露。把业务工具绑定到技能名上ToolCallback dummyTool FunctionToolCallback.builder(execute_inventory_script, (MapString, Object args) - { System.out.println( 工具被调用了执行库存脚本...); System.out.println( 收到的参数: args); return SUCCESS: 库存已更新; }) .description(执行库存管理的 Python 脚本参数为操作类型) .inputType(Map.class) .build(); MapString, ListToolCallback groupedTools Map.of( inventory_management, List.of(dummyTool) ); SkillsAgentHook hook SkillsAgentHook.builder() .skillRegistry(registry) .groupedTools(groupedTools) .build();groupedTools的 key 必须和SKILL.md里的name完全一致差一个字符就绑不上。然后发一个诱导性 PromptString prompt 请详细介绍一下 inventory_management 技能。 如果该技能支持通过脚本自动化操作请使用相关工具执行一个“检查库存”的操作。 ; AssistantMessage result agent.call(prompt);看第一次请求模型只拿到read_skill工具看不到execute_inventory_script。模型调用read_skill(inventory_management)后框架在运行时把execute_inventory_script绑定进当前会话。看第二次请求工具列表里出现了execute_inventory_script模型用它生成调用指令。这个“工具跟着技能走”的机制就是渐进式工具披露好处是省 Token、防误触、按需授权。5. 常见错排查401、local proxy failed、reading choices、OAuth跑这套东西报错基本集中在模型通道和技能加载两块。下面按真实报错对照排查。401 Unauthorized或invalid_api_key模型通道的 Key 不对。先确认环境变量TAOTOKEN_API_KEY真的被读到了Spring 里${TAOTOKEN_API_KEY}如果没解析到会传空字符串。再确认 Base URL 是https://taotoken.net/api别多写或少写路径。Key 可以在 API Keys 页面重新生成https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewritelocal proxy failed或连接超时通常是 Base URL 写错或网络出口有问题。检查配置里有没有多余的斜杠、有没有把/api写成/v1。TaoToken 的地址就是https://taotoken.net/api原样填。reading choices相关报错比如Cannot read field choices because response is null一般是响应体不是预期的 OpenAI 格式可能是模型 ID 写错导致返回了错误结构或者请求被中间层拦截返回了 HTML。先单独用 curl 打一次确认返回结构curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d {model:claude-sonnet-4-5,messages:[{role:user,content:hi}]}如果 curl 正常但 Spring 里报错检查 Spring AI 的版本和 OpenAI 兼容配置是否匹配。OAuth相关报错如果你用的是 Claude Code 这类工具接进来可能会碰到 OAuth 认证流程的问题。Claude Code 接入的配置在文档里有说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteClaude Code 的接入配置三件套同样是 Base URL Key Model IDBase URL 填https://taotoken.net/apiKey 用 API Keys 页面生成的Model ID 按实际填。如果 OAuth 流程卡住先确认是不是把 API Key 和 OAuth 两种认证方式混用了。技能加载侧的报错UnsupportedOperationException出现在registry.reload()时说明你用的SkillRegistry实现不支持重载。ClasspathSkillRegistry通常不支持换FileSystemSkillRegistry。如果 hook 捕获了这个异常并打了 debug 日志把日志级别调到 DEBUG 就能看到。技能没被加载检查classpathPath或projectSkillsDirectory是否指向了包含技能子目录的父目录而不是某个技能目录本身。SKILL.md的 front matter 格式错了也会导致解析失败name和description必须存在。热更新不生效确认autoReload(true)开了且用的是FileSystemSkillRegistry。还要注意reload()只在同一次 Agent 执行的第一次推理时触发如果你在同一个agent.call()里改文件本次执行不会重载得等下一次call()。6. 热更新触发与回滚的验证步骤热更新的验证要能观察到“改前”和“改后”的差异还要能回滚。我用一个test-reload技能来演示。先准备技能文件skills/test-reload/SKILL.md初始内容写个明显的标记--- name: test-reload description: Use this skill when the user asks to read the test-reload skill content. --- # 测试重载技能 当前版本V1 内容这是第一版内容。用FileSystemSkillRegistry加autoReload构建 AgentSkillRegistry registry FileSystemSkillRegistry.builder() .projectSkillsDirectory(path/to/skills/) .build(); 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();第一次调用让模型读技能内容AssistantMessage result1 agent.call(请调用 read_skill 读取 test-reload 技能的内容并告诉我里面写了什么。); System.out.println(Agent 回复: result1.getText());预期回复里出现“当前版本V1”。然后手动改文件把 V1 改成 V2当前版本V2 内容这是第二版内容用于验证热更新。保存后等几秒再发第二次调用AssistantMessage result2 agent.call(请再次读取 test-reload 技能的内容。); System.out.println(Agent 回复: result2.getText());如果热更新生效第二次回复里应该是“当前版本V2”。如果还是 V1检查autoReload是否真的开了、reload()是否被调用日志里搜Skills reloaded、以及文件路径是否指向了正确的目录。回滚验证把文件改回 V1再调一次确认回复回到 V1。这一步能证明重载是双向的不是只加载一次新内容就锁死。有个细节要注意reload()只在同一次 Agent 执行的第一次推理时执行。如果你在agent.call()内部改文件本次执行不会重载因为第一次推理已经过了。所以验证时要分两次call()中间改文件。这个设计是为了保证同一次执行内模型看到的内容一致避免前后矛盾。另外MemorySaver会保留会话历史。如果你在同一个会话里连续调用模型可能从历史里读到旧内容而不去重新read_skill。验证热更新时建议每次用新的会话或者清掉 saver 里的历史确保模型真的重新读了技能文件。跑通这套之后你可以把技能目录挂到 CI 流程里改完技能文件自动触发一次验证调用确认新内容生效。长期跑编码类 Agent 的话配合 Coding Plan 会更顺https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite模型对话页面可以用来快速对比不同模型对同一技能的理解差异https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite最后留一个我踩过的坑groupedTools的 key 和SKILL.md的name不一致时工具不会报错只是永远不披露模型会一直说“我没有这个工具”。排查时先打印registry.listAll()确认技能名再核对groupedTools的 key两边对上才行。