劃:SkillsAgentHook 配置與驗(yàn)證)
1. 從一次“Skill 沒被觸發(fā)”的排查說起Spring AI Alibaba 的 ReactAgent 本身已經(jīng)能調(diào)工具但當(dāng)你希望它按一套固定業(yè)務(wù)規(guī)范輸出內(nèi)容時(shí)光靠 systemPrompt 會(huì)越寫越長、越寫越亂。Skill 機(jī)制解決的正是這個(gè)問題把“旅游計(jì)劃該怎么生成”這類領(lǐng)域知識(shí)從提示詞里抽出來放進(jìn)獨(dú)立的 SKILL.md由 SkillsAgentHook 在運(yùn)行時(shí)按需注入。這篇要聊的就是 ReactAgent 通過 Skill 生成旅游計(jì)劃的完整落地路徑核心檢索詞是 Spring AI Alibaba ReactAgent Skill 配置適合已經(jīng)在用 Spring AI Alibaba 搭 Agent、但發(fā)現(xiàn)提示詞維護(hù)成本越來越高的同學(xué)。我試過的第一個(gè)坑很典型SKILL.md 寫好了ClasspathSkillRegistry 也注冊了日志里 Skills loaded 數(shù)量也對但發(fā)一句“幫我規(guī)劃去成都的旅游”模型壓根沒走 Skill直接自己編了一段行程。問題不在 Skill 內(nèi)容而在 Hook 的裝配順序和觸發(fā)條件。ReactAgent 的 hooks 是一個(gè)鏈?zhǔn)浇Y(jié)構(gòu)SummarizationHook 和 SkillsAgentHook 誰先誰后、SkillRegistry 的 classpathPath 指向哪里、SKILL.md 的 name 是否和文件夾名嚴(yán)格一致任何一處不對Skill 就是“加載了但不生效”。所以這篇不打算只貼一段 AgentConfig 就完事而是按“問題場景 → 前置準(zhǔn)備 → 可復(fù)制配置 → 驗(yàn)證請求 → 報(bào)錯(cuò)排查 → 后續(xù)接入”的順序走一遍。你會(huì)看到 SKILL.md 的 front matter 怎么寫、ClasspathSkillRegistry 怎么指路徑、SkillsAgentHook 怎么和 SummarizationHook 共存、以及一次真實(shí)的旅游計(jì)劃請求返回了什么。中間涉及模型接入的部分我會(huì)用 TaoToken 的 API 作為示例因?yàn)樗?Base URL 和 Key 管理方式對 Java 側(cè)比較友好配置片段可以直接抄。先明確一件事Skill 不是工具。工具是 WeatherTool、SearchTool 這種帶 Tool 注解、能被模型 function call 的方法Skill 是一段結(jié)構(gòu)化的領(lǐng)域說明告訴模型“遇到旅游規(guī)劃類請求時(shí)按這個(gè)模板和規(guī)則來”。SkillsAgentHook 的作用是在合適的時(shí)機(jī)把匹配到的 Skill 內(nèi)容拼進(jìn)上下文。理解這一點(diǎn)后面的配置就不會(huì)迷路。2. 前置準(zhǔn)備Skill 目錄、SKILL.md 與模型接入2.1 目錄結(jié)構(gòu)約定Spring AI Alibaba 的 ClasspathSkillRegistry 默認(rèn)從 classpath 下讀取 Skill。工程里通常是這樣的結(jié)構(gòu)src/main/resources/ skills/ travel-assistant/ SKILL.md注意兩點(diǎn)第一skills是根目錄ClasspathSkillRegistry.builder().classpathPath(skills) 指的就是它第二travel-assistant這個(gè)文件夾名必須和 SKILL.md 里 front matter 的name完全一致大小寫、連字符都不能差。我見過有人文件夾叫travel_assistant、name 寫travel-assistant結(jié)果 Skill 加載數(shù)量是 0日志還不報(bào)錯(cuò)排查半天。2.2 SKILL.md 的 front matter 寫法SKILL.md 分兩部分YAML front matter 和正文。front matter 至少要有 name 和 descriptiondescription 是給模型判斷“這個(gè) Skill 該不該用”的依據(jù)所以要寫清楚觸發(fā)場景。--- name: travel-assistant description: 當(dāng)用戶需要規(guī)劃旅游行程時(shí)使用此技能。用戶只需提供目的地技能將自動(dòng)生成3-5天行程未指定天數(shù)時(shí)默認(rèn)3天包含每日詳細(xì)安排、花費(fèi)明細(xì)可以使用 search_tool 查詢目的地景點(diǎn)和特色美食并調(diào)用天氣工具提供穿衣指數(shù)及出行提醒。 --- ## 一、功能說明 本技能用于生成可落地的旅游行程包含行程、預(yù)算、天氣穿衣建議三部分。 ## 二、觸發(fā)方式 核心觸發(fā)詞旅游規(guī)劃、行程安排、XX旅游攻略、XX穿衣建議、XX旅游預(yù)算。 ## 三、核心規(guī)則 1. 目的地必填未提供時(shí)持續(xù)追問。 2. 游玩天數(shù)控制在3-5天未明確時(shí)默認(rèn)3天。 3. 每日行程包含上午、下午、晚上三個(gè)時(shí)段。 4. 預(yù)算需包含每日明細(xì)及總預(yù)算提供窮游/舒適/輕奢三檔。 5. 必須調(diào)用天氣工具輸出穿衣及出行提醒。正文部分就是你的業(yè)務(wù)規(guī)則寫得越具體模型輸出越穩(wěn)定。上面這段是精簡版實(shí)際項(xiàng)目里可以把行程模板、預(yù)算格式、話術(shù)都寫進(jìn)去模型會(huì)照著執(zhí)行。2.3 模型接入Base URL 與 KeyReactAgent 需要一個(gè) ChatModel。示例里用的是 DeepSeekChatModel但如果你想讓模型走統(tǒng)一的 API 網(wǎng)關(guān)可以把 Base URL 指向 TaoToken 的 API 地址Key 用平臺(tái)生成的。這樣切換模型時(shí)只改配置不動(dòng)代碼。在application.yml里spring: ai: deepseek: api-key: ${TAOTOKEN_API_KEY} base-url: https://taotoken.net/api對應(yīng)的環(huán)境變量在啟動(dòng)前設(shè)置好。Key 的獲取路徑是 TaoToken 控制臺(tái)的 API Keys 頁面模型 ID 按你實(shí)際要用的填比如deepseek-chat。這三件套——Base URL、Key、Model ID——在后面的配置和排查里會(huì)反復(fù)出現(xiàn)先記住。3. 可復(fù)制配置AgentConfig 裝配 SkillsAgentHook3.1 完整 AgentConfig.java這是核心配置類改動(dòng)集中在 ReactAgent 的 builder 鏈上。注意 hooks 里 SummarizationHook 和 SkillsAgentHook 的順序以及 SkillRegistry 的構(gòu)建方式。package com.david.springalibabareactagentdemo.config; import com.alibaba.cloud.ai.graph.agent.ReactAgent; import com.alibaba.cloud.ai.graph.agent.hook.skills.SkillsAgentHook; import com.alibaba.cloud.ai.graph.agent.hook.summarization.SummarizationHook; import com.alibaba.cloud.ai.graph.checkpoint.savers.redis.RedisSaver; import com.alibaba.cloud.ai.graph.skills.registry.SkillRegistry; import com.alibaba.cloud.ai.graph.skills.registry.classpath.ClasspathSkillRegistry; import com.david.springalibabareactagentdemo.tools.SearchTool; import com.david.springalibabareactagentdemo.tools.WeatherTool; import org.redisson.api.RedissonClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.deepseek.api.DeepSeekApi; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class AgentConfig { Logger log LoggerFactory.getLogger(AgentConfig.class); Value(${spring.ai.deepseek.api-key}) private String apiKey; Value(${spring.ai.deepseek.base-url:https://taotoken.net/api}) private String baseUrl; Bean public ReactAgent reactAgent(RedissonClient redissonClient) { DeepSeekApi deepSeekApi DeepSeekApi.builder() .apiKey(apiKey) .baseUrl(baseUrl) .build(); ChatModel chatModel DeepSeekChatModel.builder() .deepSeekApi(deepSeekApi) .build(); SkillRegistry registry ClasspathSkillRegistry.builder() .classpathPath(skills) .build(); SkillsAgentHook skillHook SkillsAgentHook.builder() .skillRegistry(registry) .build(); log.info(Skills loaded: {}, skillHook.getSkillCount()); return ReactAgent.builder() .name(ai_agent) .model(chatModel) .tools(new WeatherTool().toolCallback(), new SearchTool().toolCallback()) .systemPrompt( 你是一個(gè)博學(xué)的智能聊天助手必須調(diào)用工具獲取信息不能編造答案。 調(diào)用工具后根據(jù)結(jié)果回答用戶。 ) .saver(RedisSaver.builder().redisson(redissonClient).build()) .hooks( SummarizationHook.builder() .model(chatModel) .maxTokensBeforeSummary(8000) .messagesToKeep(10) .build(), skillHook ) .build(); } }3.2 關(guān)鍵參數(shù)對照配置項(xiàng)作用常見取值classpathPathSkill 根目錄skillsskillRegistry注冊表實(shí)例ClasspathSkillRegistrymaxTokensBeforeSummary觸發(fā)摘要的 token 閾值8000messagesToKeep摘要后保留的原始輪數(shù)10baseUrl模型 API 地址https://taotoken.net/apimodel模型 IDdeepseek-chat3.3 為什么 hooks 順序有講究SummarizationHook 負(fù)責(zé)在對話變長時(shí)壓縮歷史SkillsAgentHook 負(fù)責(zé)注入 Skill。如果 Skill 注入發(fā)生在摘要之前摘要可能會(huì)把 Skill 內(nèi)容也當(dāng)成普通對話壓掉放在后面Skill 的注入更穩(wěn)定。示例里把 skillHook 放在 SummarizationHook 之后實(shí)測下來觸發(fā)率明顯更穩(wěn)。另外SkillRegistry 是單例構(gòu)建的不要在每次請求里 new。ClasspathSkillRegistry 在 build 時(shí)就把 classpath 下的 SKILL.md 掃了一遍getSkillCount()返回的就是掃到的數(shù)量。啟動(dòng)日志里看到Skills loaded: 1說明 travel-assistant 被正確識(shí)別了。4. 驗(yàn)證請求一次旅游計(jì)劃生成的全過程4.1 啟動(dòng)與日志確認(rèn)服務(wù)啟動(dòng)后控制臺(tái)會(huì)打印Skills loaded: 1如果這里是 0先別急著調(diào)接口回到第 2 節(jié)檢查文件夾名和 name 是否一致。數(shù)量對了再往下走。4.2 發(fā)起請求用一個(gè)簡單的 Controller 暴露接口或者直接用測試類調(diào) ReactAgent。請求內(nèi)容就是一句自然語言幫我規(guī)劃去長沙的旅游5月5日到5月8日4天4.3 返回結(jié)果片段模型先調(diào)用了 SearchTool 查長沙景點(diǎn)和美食再調(diào) WeatherTool 查天氣最后按 SKILL.md 里的模板輸出。返回結(jié)構(gòu)大致如下## 長沙4天3晚經(jīng)典行程 出行時(shí)間5月5日~5月8日 總預(yù)算參考約1300元/人舒適版不含往返大交通 ### 天氣情況 | 日期 | 天氣 | 溫度 | | 5/5 | 多云 | 16~27℃ | | 5/6 | 多云 | 17~28℃ | | 5/7 | 晴轉(zhuǎn)多云 | 19~30℃ | | 5/8 | 多云轉(zhuǎn)陰 | 18~28℃ | ### 第1天抵達(dá) → 太平老街 → 五一廣場 上午抵達(dá)長沙入住五一廣場附近酒店 下午逛太平老街 晚上五一廣場、坡子街推薦茶顏悅色、黑色經(jīng)典臭豆腐 當(dāng)日花費(fèi)住宿200 餐飲80 交通20 300元 ### 總預(yù)算明細(xì) 住宿600 餐飲360 交通100 門票80 伴手禮100 約1240元/人 ### 穿衣建議 白天短袖早晚備薄外套穿舒適運(yùn)動(dòng)鞋。4.4 怎么判斷 Skill 真的被觸發(fā)了看三個(gè)信號(hào)第一輸出里有 SKILL.md 規(guī)定的固定結(jié)構(gòu)比如“上午/下午/晚上”三段式、預(yù)算三檔、天氣穿衣提醒第二模型確實(shí)調(diào)用了 WeatherTool返回里有具體溫度和穿衣指數(shù)第三追問話術(shù)和 SKILL.md 里寫的一致比如沒給天數(shù)時(shí)會(huì)說“我默認(rèn)給你安排3天經(jīng)典行程”。如果輸出是自由發(fā)揮的散文沒有固定模板那大概率 Skill 沒生效去第 5 節(jié)排查。5. 本篇常見錯(cuò)排查401、Skill 數(shù)量為 0、OAuth 報(bào)錯(cuò)5.1 401 Unauthorized最常見的原因是 Key 沒讀到或 Base URL 寫錯(cuò)。檢查application.yml里的api-key是否被環(huán)境變量正確覆蓋以及base-url是否指向https://taotoken.net/api。如果 Key 是從控制臺(tái)復(fù)制的注意別帶多余空格。401 報(bào)錯(cuò)信息里通常會(huì)帶invalid api key看到這個(gè)就先去 API Keys 頁面重新生成一個(gè)。5.2 Skills loaded: 0三個(gè)檢查點(diǎn)文件夾名和 name 是否一致SKILL.md 是否在resources/skills/travel-assistant/下front matter 的---是否成對出現(xiàn)。YAML 解析失敗時(shí)ClasspathSkillRegistry 會(huì)靜默跳過不報(bào)錯(cuò)所以數(shù)量為 0 時(shí)優(yōu)先懷疑格式。5.3 local proxy failed這個(gè)報(bào)錯(cuò)通常出現(xiàn)在網(wǎng)絡(luò)層說明請求沒到達(dá) API 地址。檢查base-url是否被本地代理配置覆蓋或者環(huán)境變量里有沒有殘留的代理設(shè)置。Java 側(cè)可以顯式設(shè)置-Dhttp.proxyHost為空來排除。5.4 reading choices 相關(guān)報(bào)錯(cuò)如果日志里出現(xiàn)reading choices或choices字段解析失敗多半是模型返回格式和客戶端預(yù)期不一致。確認(rèn) Model ID 填的是deepseek-chat這類標(biāo)準(zhǔn)值不要填成自定義別名。Base URL、Key、Model ID 三件套對齊后這個(gè)報(bào)錯(cuò)一般會(huì)消失。5.5 OAuth 報(bào)錯(cuò)OAuth 類報(bào)錯(cuò)通常和鑒權(quán)方式有關(guān)。如果你用的是 API Key 模式不要同時(shí)開 OAuth 流程。檢查配置里是否混入了client-id、client-secret這類字段有的話刪掉只保留api-key。5.6 Skill 加載了但不觸發(fā)如果Skills loaded: 1但模型不走 Skill檢查 description 是否寫得太泛。description 是模型判斷是否使用 Skill 的唯一依據(jù)要包含明確的觸發(fā)詞比如“旅游規(guī)劃”“行程安排”。另外systemPrompt 里如果寫了“直接回答不要使用技能”之類的限制也會(huì)壓制 Skill 觸發(fā)。6. 后續(xù)接入從單 Skill 到多 Skill 與 Coding Plan跑通一個(gè) travel-assistant 之后擴(kuò)展方向很自然再加一個(gè)code-reviewSkill、一個(gè)sql-optimizeSkillClasspathSkillRegistry 會(huì)自動(dòng)掃描skills下的所有子目錄getSkillCount()會(huì)變成 3。每個(gè) Skill 的 description 寫清楚各自的觸發(fā)場景模型會(huì)在運(yùn)行時(shí)按需選擇。如果你打算把 ReactAgent 用在長期編碼或 Agent 場景比如讓 Agent 持續(xù)處理代碼任務(wù)、維護(hù)上下文可以了解 TaoToken 的 Coding Plan它更適合高頻、長會(huì)話的調(diào)用模式。模型對話入口可以用來單獨(dú)驗(yàn)證某個(gè)模型 ID 是否可用接入文檔里有 Java 側(cè)的完整示例。API Keys 頁面負(fù)責(zé)生成和管理 Key控制臺(tái)可以看調(diào)用量。配置層面把base-url統(tǒng)一指向https://taotoken.net/apiKey 走環(huán)境變量Model ID 按需切換這樣從旅游計(jì)劃這種輕量 Skill 到代碼 Agent 這種重場景底層接入不用改。Skill 的價(jià)值在于把業(yè)務(wù)規(guī)則從提示詞里解耦出來ReactAgent 負(fù)責(zé)調(diào)度SkillsAgentHook 負(fù)責(zé)注入兩者配合好輸出穩(wěn)定性會(huì)有明顯提升。