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