戰(zhàn):Tavily 搜索接入 TaoToken 統(tǒng)一通道)
1. 為什么要在 Spring AI 1.1.2 里折騰 MCP 和 Tavily如果你正在用 Spring Boot 寫(xiě) AI 應(yīng)用大概率遇到過(guò)這種局面項(xiàng)目里同時(shí)接了 OpenAI、DeepSeek、通義千問(wèn)好幾個(gè)模型每個(gè)模型的 Key 散落在不同的配置文件里Base URL 各不相同測(cè)試環(huán)境切到生產(chǎn)環(huán)境要改一堆東西。更麻煩的是當(dāng)你想讓模型具備聯(lián)網(wǎng)搜索能力時(shí)又得單獨(dú)寫(xiě)一套工具調(diào)用邏輯代碼越堆越厚。Spring AI 1.1.2 引入的 MCPModel Context Protocol支持恰好能解決這兩個(gè)痛點(diǎn)。MCP 是一種讓大模型與外部工具、資源交互的標(biāo)準(zhǔn)化協(xié)議你可以把它理解成AI 世界的 USB 接口——只要工具實(shí)現(xiàn)了 MCP Server任何支持 MCP Client 的框架都能即插即用。Tavily 是一個(gè)專(zhuān)為 AI 應(yīng)用設(shè)計(jì)的搜索 API每月有 1000 次免費(fèi)額度非常適合做搜索增強(qiáng)問(wèn)答。這篇內(nèi)容聚焦一條完整鏈路Spring Boot 3.5 Spring AI 1.1.2 通過(guò) MCP 接入 Tavily 搜索同時(shí)把模型調(diào)用的 endpoint 統(tǒng)一指向 TaoToken 通道解決多模型 Key 分散、Base URL 切換繁瑣的問(wèn)題。適合已經(jīng)寫(xiě)過(guò) Spring Boot、想快速給 AI 應(yīng)用加上聯(lián)網(wǎng)搜索能力的后端開(kāi)發(fā)者。跟著做下來(lái)你會(huì)得到一份可復(fù)制的application.yml、一個(gè) MCP 客戶(hù)端 Bean 定義以及把 endpoint 改到 TaoToken 后的連通性驗(yàn)證步驟。先說(shuō)清楚 MCP 的工作方式。MCP Server 把工具能力搜索、查庫(kù)、讀文件等以統(tǒng)一格式暴露出來(lái)MCP Client 負(fù)責(zé)連接 Server、拉取工具定義并在需要時(shí)轉(zhuǎn)發(fā)工具調(diào)用LLM 通過(guò) Spring AI 的 tool-calling 能力在對(duì)話(huà)過(guò)程中自動(dòng)決定是否調(diào)用工具。在 Spring AI 1.1.2 之前給模型接外部工具需要手寫(xiě)Tool注解或FunctionCallback現(xiàn)在直接復(fù)用社區(qū)已有的 MCP Server配置即集成。TaoToken 在這里扮演的角色是統(tǒng)一通道。它兼容 OpenAI 協(xié)議提供模型對(duì)話(huà)、Coding Plan、API Keys 管理等能力。你不需要為每個(gè)模型單獨(dú)維護(hù)一套 Base URL 和 Key把 Spring AI 的 OpenAI Starter 指向 TaoToken 的 API 地址再通過(guò)模型 ID 區(qū)分不同模型即可。這樣 MCP 負(fù)責(zé)工具擴(kuò)展TaoToken 負(fù)責(zé)模型接入兩者職責(zé)清晰。2. 前置準(zhǔn)備依賴(lài)、版本與 TaoToken 通道配置動(dòng)手之前先把版本對(duì)齊。Spring AI 1.1.2 對(duì) Spring Boot 版本有要求建議用 3.5.x。Java 版本至少 17。MCP Server 這邊用tavily-mcp通過(guò)npx拉起所以機(jī)器上要有 Node.js建議 18 以上。先看 Maven 依賴(lài)。父工程的pom.xml里聲明版本號(hào)然后引入 Spring AI BOM 統(tǒng)一管理properties spring-ai.version1.1.2/spring-ai.version /properties dependencyManagement dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-bom/artifactId version${spring-ai.version}/version typepom/type scopeimport/scope /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId version${spring-ai.version}/version /dependency /dependencies /dependencyManagementAI 框架模塊里引入實(shí)際使用的依賴(lài)。這里用 OpenAI Starter因?yàn)?TaoToken 兼容 OpenAI 協(xié)議dependencies dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-model-openai/artifactId /dependency dependency groupIdorg.springframework.ai/groupId artifactIdspring-ai-starter-mcp-client/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-webflux/artifactId /dependency /dependenciesspring-ai-starter-mcp-client會(huì)自動(dòng)引入 MCP 協(xié)議實(shí)現(xiàn)和 stdio/SSE 傳輸層不需要額外依賴(lài)。接下來(lái)是 TaoToken 通道的準(zhǔn)備。訪(fǎng)問(wèn)官網(wǎng) https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注冊(cè)后進(jìn)入控制臺(tái)創(chuàng)建 API Key??刂婆_(tái)地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite API Keys 管理頁(yè)在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。創(chuàng)建好 Key 之后模型調(diào)用的 Base URL 統(tǒng)一用 https://taotoken.net/api 注意這個(gè)地址不加 UTM 參數(shù)。Tavily 這邊去 tavily.com 注冊(cè)登錄拿到TAVILY_API_KEY。免費(fèi)額度每月 1000 次個(gè)人開(kāi)發(fā)和小規(guī)模測(cè)試夠用。環(huán)境變量建議這樣組織避免 Key 硬編碼進(jìn)代碼export TAOTOKEN_API_KEYsk-你的TaoToken密鑰 export TAVILY_API_KEYtvly-你的Tavily密鑰 export OPENAI_BASE_URLhttps://taotoken.net/apiWindows 下用set或者直接在 IDE 的 Run Configuration 里配。把 Key 放在環(huán)境變量里application.yml通過(guò)${}引用這樣不同環(huán)境切換只改環(huán)境變量配置文件不用動(dòng)。3. 可復(fù)制配置application.yml 與 MCP 客戶(hù)端 Bean這一節(jié)是核心配置寫(xiě)對(duì)了后面基本就通了。先看application.yml的完整片段spring: ai: openai: api-key: ${TAOTOKEN_API_KEY} base-url: ${OPENAI_BASE_URL} chat: options: model: gpt-4o-mini temperature: 0.7 embedding: options: model: text-embedding-3-small mcp: client: type: SYNC request-timeout: 60s initialized: true stdio: connections: tavily: command: cmd.exe args: - /c - npx - -y - tavily-mcplatest env: TAVILY_API_KEY: ${TAVILY_API_KEY}逐項(xiàng)解釋關(guān)鍵參數(shù)。type: SYNC表示同步模式適配傳統(tǒng) Servlet 應(yīng)用如果你的項(xiàng)目是全響應(yīng)式 WebFlux改成ASYNC。request-timeout: 60s是工具調(diào)用超時(shí)時(shí)間Tavily 搜索有時(shí)耗時(shí)較長(zhǎng)默認(rèn)值可能不夠。initialized: true非常重要它讓?xiě)?yīng)用啟動(dòng)時(shí)立即初始化 MCP 連接并拉取工具列表如果設(shè)為false第一次調(diào)用時(shí)才初始化容易出現(xiàn)首次響應(yīng)慢或工具未生效的問(wèn)題。stdio.connections.tavily定義了一個(gè)名為 tavily 的連接。command加args拼起來(lái)就是cmd.exe /c npx -y tavily-mcplatest通過(guò) npx 拉取并運(yùn)行 tavily-mcp。env里注入的TAVILY_API_KEY只對(duì)子進(jìn)程可見(jiàn)不會(huì)暴露給模型。Linux 或 Mac 用戶(hù)把command改成npxargs改成[-y, tavily-mcplatest]即可。多個(gè) MCP Server 直接在stdio.connections下繼續(xù)加比如同時(shí)接入文件系統(tǒng)stdio: connections: tavily: command: cmd.exe args: [/c, npx, -y, tavily-mcplatest] env: TAVILY_API_KEY: ${TAVILY_API_KEY} filesystem: command: cmd.exe args: [/c, npx, -y, anthropic/mcp-filesystemlatest, D:/docs]所有連接的工具會(huì)自動(dòng)合并模型可以同時(shí)使用多個(gè) MCP Server 提供的工具。然后是 Java 側(cè)的 Bean 定義。spring-ai-starter-mcp-client會(huì)自動(dòng)完成啟動(dòng) MCP Server 子進(jìn)程、拉取工具列表、把 MCP tools 轉(zhuǎn)換成 Spring AI 的ToolCallback、注冊(cè)ToolCallbackProviderBean 這幾件事。你要做的只有把ToolCallbackProvider掛到ChatClient上。先看自動(dòng)配置類(lèi)Configuration public class AiAutoConfiguration { Bean public ChatMemory chatMemory() { return MessageWindowChatMemory.builder() .maxMessages(20) .build(); } Bean public VectorStore vectorStore(EmbeddingModel embeddingModel) { return SimpleVectorStore.builder(embeddingModel).build(); } }核心是動(dòng)態(tài)構(gòu)建ChatClient的工廠類(lèi)Component RequiredArgsConstructor public class DynamicChatClientFactory { private final ChatMemory chatMemory; private final ToolCallbackProvider toolCallbackProvider; public ChatClient buildDefaultClient(ChatModel chatModel) { String systemPrompt 你是一個(gè)智能助手遇到實(shí)時(shí)信息需求時(shí)主動(dòng)調(diào)用搜索工具。; return ChatClient.builder(chatModel) .defaultSystem(systemPrompt) .defaultAdvisors( MessageChatMemoryAdvisor.builder(chatMemory).build() ) .defaultToolCallbacks(toolCallbackProvider) .build(); } }關(guān)鍵就一行.defaultToolCallbacks(toolCallbackProvider)。這行代碼讓模型每次對(duì)話(huà)時(shí)都能看到所有 MCP Server 暴露的工具定義模型根據(jù)用戶(hù)問(wèn)題自主決定是否調(diào)用工具工具調(diào)用的請(qǐng)求和響應(yīng)由 Spring AI 加 MCP Client 自動(dòng)處理。ChatModel的構(gòu)建這里簡(jiǎn)化了實(shí)際項(xiàng)目里你可以通過(guò)策略模式支持多個(gè)模型。用 TaoToken 通道時(shí)構(gòu)建OpenAiChatModel的配置如下OpenAiApi openAiApi OpenAiApi.builder() .baseUrl(https://taotoken.net/api) .apiKey(System.getenv(TAOTOKEN_API_KEY)) .build(); OpenAiChatOptions options OpenAiChatOptions.builder() .model(gpt-4o-mini) .temperature(0.7) .build(); ChatModel chatModel OpenAiChatModel.builder() .openAiApi(openAiApi) .defaultOptions(options) .build();換模型只改.model()里的 IDBase URL 和 Key 不用動(dòng)這就是統(tǒng)一通道的價(jià)值。4. 驗(yàn)證請(qǐng)求curl 連通性與日志斷言配置寫(xiě)完別急著寫(xiě)業(yè)務(wù)代碼先驗(yàn)證鏈路通不通。分兩步先驗(yàn) TaoToken 通道再驗(yàn) MCP 工具是否掛載成功。第一步用 curl 直接打 TaoToken 的 API確認(rèn) Key 和 Base URL 沒(méi)問(wèn)題curl -X POST https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: 你好}], stream: false }返回里能看到choices數(shù)組和content字段說(shuō)明通道正常。如果返回 401檢查 Key 是否復(fù)制完整、有沒(méi)有多余空格。如果返回local proxy failed之類(lèi)的錯(cuò)誤檢查 Base URL 是不是寫(xiě)成了https://taotoken.net/api注意結(jié)尾不要多加/v1OpenAI Starter 會(huì)自動(dòng)拼接路徑。第二步啟動(dòng) Spring Boot 應(yīng)用觀察控制臺(tái)日志。MCP 初始化成功會(huì)打印類(lèi)似這樣的內(nèi)容i.m.client.transport.StdioClientTransport:106 - MCP server starting. i.m.client.transport.StdioClientTransport:137 - MCP server started如果看到MCP server started說(shuō)明 tavily-mcp 子進(jìn)程拉起來(lái)了。接著確認(rèn)工具列表是否拉取成功可以在啟動(dòng)類(lèi)里加一段臨時(shí)日志Bean public CommandLineRunner logTools(ToolCallbackProvider provider) { return args - { ToolCallback[] callbacks provider.getToolCallbacks(); System.out.println(已加載 MCP 工具數(shù)量: callbacks.length); for (ToolCallback cb : callbacks) { System.out.println(工具名: cb.getToolDefinition().name()); } }; }正常應(yīng)該看到tavily_search之類(lèi)的工具名。如果數(shù)量為 0說(shuō)明 MCP 連接沒(méi)初始化成功回到第 5 節(jié)排查。第三步發(fā)一個(gè)真實(shí)請(qǐng)求測(cè)試搜索增強(qiáng)。寫(xiě)個(gè)簡(jiǎn)單的 ControllerRestController RequestMapping(/chat) RequiredArgsConstructor public class ChatController { private final DynamicChatClientFactory factory; private final ChatModel chatModel; GetMapping(value /stream, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxString chatStream(String message, String conversationId) { ChatClient client factory.buildDefaultClient(chatModel); return client.prompt() .advisors(a - a.param(ChatMemory.CONVERSATION_ID, conversationId)) .user(message) .stream() .content(); } }啟動(dòng)后請(qǐng)求curl -N http://localhost:8080/chat/stream?message今天杭州天氣怎么樣conversationIdtest1模型會(huì)先判斷天氣是實(shí)時(shí)信息決定調(diào)用tavily_search工具M(jìn)CP Client 通過(guò) stdio 把搜索請(qǐng)求發(fā)給 tavily-mcp 子進(jìn)程子進(jìn)程調(diào)用 Tavily API 拿到結(jié)果結(jié)果返回給模型模型基于搜索結(jié)果生成最終回答并流式輸出。整個(gè)過(guò)程模型自主決策你不需要寫(xiě)任何 if-else 判斷什么時(shí)候該搜索。日志里能看到工具調(diào)用的痕跡類(lèi)似Tool execution request和Tool execution response。如果模型直接回答而沒(méi)有調(diào)用工具檢查defaultToolCallbacks是否掛上、initialized是否為true。5. 常見(jiàn)報(bào)錯(cuò)排查401、local proxy failed、reading choices、OAuth這一節(jié)把實(shí)際踩過(guò)的坑列出來(lái)對(duì)照?qǐng)?bào)錯(cuò)找原因。401 Unauthorized。最常見(jiàn)的是 Key 問(wèn)題。先確認(rèn)TAOTOKEN_API_KEY環(huán)境變量在當(dāng)前 shell 里能echo出來(lái)。如果用的是 IDE檢查 Run Configuration 的 Environment variables 有沒(méi)有配。還有一種情況是 Key 復(fù)制時(shí)帶了換行或空格用curl單獨(dú)測(cè)一下就能定位。TaoToken 的 Key 在 API Keys 頁(yè)面管理如果懷疑 Key 失效去 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 重新生成一個(gè)。local proxy failed。這個(gè)報(bào)錯(cuò)通常出現(xiàn)在 Base URL 配置不對(duì)的時(shí)候。檢查spring.ai.openai.base-url是不是https://taotoken.net/api不要寫(xiě)成https://taotoken.net/api/v1OpenAI Starter 會(huì)自己拼/v1/chat/completions。另外確認(rèn)網(wǎng)絡(luò)能正常訪(fǎng)問(wèn)該地址用curl -I https://taotoken.net/api看返回狀態(tài)碼。Error reading choices。這個(gè)報(bào)錯(cuò)說(shuō)明請(qǐng)求發(fā)出去了但響應(yīng)體解析失敗。常見(jiàn)原因是模型 ID 寫(xiě)錯(cuò)了TaoToken 通道不認(rèn)這個(gè)模型名。去模型對(duì)話(huà)頁(yè)面 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 確認(rèn)可用的模型 ID然后改spring.ai.openai.chat.options.model。還有一種可能是響應(yīng)被截?cái)鄼z查request-timeout是否太短。OAuth 相關(guān)報(bào)錯(cuò)。如果你用的是需要 OAuth 的 MCP Serverstdio 模式下通常不需要 OAuth但 SSE 模式可能需要。Tavily 的 MCP Server 用 API Key 就夠了不需要 OAuth。如果看到 OAuth 報(bào)錯(cuò)先確認(rèn)你連的是哪個(gè) Server是不是配置里混入了其他連接。Windows 下進(jìn)程啟動(dòng)失敗。npx在 Windows 下實(shí)際是.cmd腳本不能直接作為command啟動(dòng)必須通過(guò)cmd.exe /c npx ...。報(bào)錯(cuò)通常是Cannot run program npx。按第 3 節(jié)的配置寫(xiě)就沒(méi)問(wèn)題。工具列表為空。檢查initialized是否為true。如果設(shè)為false第一次調(diào)用時(shí)才初始化啟動(dòng)日志里看不到工具數(shù)量。另外確認(rèn) Node.js 和 npx 可用node -v npx -v版本建議 18 以上。如果 npx 拉取 tavily-mcp 很慢可以先用npx -y tavily-mcplatest手動(dòng)跑一次把包緩存下來(lái)。SYNC 還是 ASYNC。項(xiàng)目里同時(shí)用了spring-boot-starter-webServlet就選SYNC純 WebFlux 響應(yīng)式應(yīng)用選ASYNC混合使用比如引入 webflux 做流式但主體是 Servlet也選SYNC。選錯(cuò)了會(huì)出現(xiàn)工具調(diào)用阻塞或響應(yīng)異常。工具調(diào)用超時(shí)。Tavily 搜索偶爾慢默認(rèn)超時(shí)可能不夠。設(shè)request-timeout: 60s或更大。如果還是超時(shí)檢查網(wǎng)絡(luò)到 Tavily API 的連通性。排查的時(shí)候有個(gè)技巧把 Spring AI 和 MCP 的日志級(jí)別調(diào)成 DEBUG能看到完整的請(qǐng)求響應(yīng)過(guò)程logging: level: org.springframework.ai: DEBUG io.modelcontextprotocol: DEBUG這樣工具調(diào)用的入?yún)⒑统鰠⒍紩?huì)打出來(lái)定位問(wèn)題快很多。6. 把通道固定下來(lái)長(zhǎng)期編碼與 Agent 場(chǎng)景的接入建議配置跑通之后建議把 TaoToken 通道的接入方式固定成項(xiàng)目規(guī)范避免每個(gè)開(kāi)發(fā)者各寫(xiě)一套。核心原則是 Base URL 和 Key 走環(huán)境變量模型 ID 走配置中心或數(shù)據(jù)庫(kù)代碼里只讀不寫(xiě)死。對(duì)于長(zhǎng)期做編碼輔助或 Agent 開(kāi)發(fā)的場(chǎng)景可以考慮用 Coding Plan。它面向持續(xù)性的編碼任務(wù)和 Agent 調(diào)用在額度管理和通道穩(wěn)定性上比按次調(diào)用更合適。具體可以看 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。如果你的場(chǎng)景是偶爾驗(yàn)證模型效果用模型對(duì)話(huà)頁(yè)面就夠了如果是接入到 CI 或自動(dòng)化流程里Coding Plan 更省心。接入文檔在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各語(yǔ)言的調(diào)用示例和參數(shù)說(shuō)明。Claude Code 相關(guān)的接入可以參考 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecodeutm_campaignrewrite 如果你用 Claude Code 做開(kāi)發(fā)這個(gè)頁(yè)面有專(zhuān)門(mén)的配置說(shuō)明?;氐?Spring AI 這邊有幾個(gè)工程化建議。第一把ChatModel的構(gòu)建封裝成工廠模型 ID 從配置讀取這樣換模型不用改代碼。第二MCP 連接配置放在application.yml里但 Key 用環(huán)境變量注入不要把 Key 提交到 Git。第三給工具調(diào)用加監(jiān)控記錄每次調(diào)用的工具名、耗時(shí)、是否成功方便排查線(xiàn)上問(wèn)題。第四request-timeout根據(jù)實(shí)際工具調(diào)整搜索類(lèi)工具給足時(shí)間本地文件類(lèi)工具可以短一些。最后說(shuō)一個(gè)實(shí)際經(jīng)驗(yàn)MCP 工具掛載后模型的決策質(zhì)量跟 system prompt 有關(guān)系。如果發(fā)現(xiàn)模型該搜索的時(shí)候不搜索可以在 system prompt 里明確寫(xiě)遇到實(shí)時(shí)信息、新聞、天氣、股價(jià)等問(wèn)題時(shí)優(yōu)先調(diào)用搜索工具。如果發(fā)現(xiàn)模型濫用搜索就加一句對(duì)于常識(shí)性問(wèn)題直接回答不需要搜索。這個(gè)平衡需要根據(jù)你的業(yè)務(wù)場(chǎng)景調(diào)。整套鏈路跑通后你得到的是一個(gè)可擴(kuò)展的架構(gòu)MCP 負(fù)責(zé)工具生態(tài)想加新工具就加一個(gè) connectionTaoToken 負(fù)責(zé)模型通道想換模型就改一個(gè) ID。兩者解耦維護(hù)成本低。