 Tool Calling(函數(shù)調(diào)用)實戰(zhàn))
在 AI 應(yīng)用開發(fā)中讓大模型調(diào)用外部工具、訪問實時數(shù)據(jù)或執(zhí)行業(yè)務(wù)邏輯是常見需求。Spring AI Alibaba 結(jié)合阿里云 DashScope通義千問提供了簡潔的 Tool Calling函數(shù)調(diào)用能力模型可以自動識別用戶意圖并調(diào)用注冊的 Java 方法再將結(jié)果融入對話。本文以“查詢天氣”為案例完整演示在 Spring Boot 項目中集成 DashScope并分別使用注解Tool、接口Function以及FunctionTool.builder Lambda三種方式定義工具。同時針對每種工具定義方式均展示基于ChatModel底層手動循環(huán) 和ChatClient自動工具閉環(huán) 的調(diào)用實現(xiàn)共六種組合并解決ChatClient無法自動注入的問題。重要前置提示底層ChatModel#call()只負(fù)責(zé)和大模型網(wǎng)絡(luò)通信不會自動執(zhí)行工具。如果直接調(diào)用收到模型返回FunctionCall后直接返回JSON結(jié)構(gòu)體不會執(zhí)行業(yè)務(wù)邏輯。想要完整工具調(diào)用閉環(huán)方案A推薦使用ChatClient內(nèi)部ToolCallingAdvisor自動完成工具執(zhí)行多輪對話方案B底層API使用ChatModelDefaultToolCallingManager手寫while循環(huán)驅(qū)動工具調(diào)用。2. 環(huán)境準(zhǔn)備2.1 添加依賴在pom.xml中引入 Spring AI Alibaba 的 DashScope 起步依賴dependencygroupIdcom.alibaba.cloud.ai/groupIdartifactIdspring-ai-alibaba-starter-dashscope/artifactId!-- 請使用最新版本例如 1.0.0-M3 --/dependency提示建議在dependencyManagement中引入 Spring AI Alibaba BOM 統(tǒng)一管理版本。2.2 配置 application.propertiesserver.port8013# 設(shè)置全局編碼格式server.servlet.encoding.enabledtrueserver.servlet.encoding.forcetrueserver.servlet.encoding.charsetUTF-8spring.application.nameSAA-13ToolCalling# SpringAIAlibaba Configspring.ai.dashscope.api-key${aliQwen-api}請?zhí)崆霸诎⒗镌崎_通 DashScope 服務(wù)并獲取 API Key設(shè)置環(huán)境變量aliQwen-api你的key。3. 定義工具三種方式3.1 方式一使用 Tool 注解聲明式工具使用Tool注解標(biāo)記 Java 方法Spring AI 會自動解析方法簽名、參數(shù)和描述生成可供大模型調(diào)用的工具元數(shù)據(jù)。import org.springframework.ai.tool.annotation.Tool;public class WeatherTools {/*** 查詢指定城市的天氣* returnDirect false 表示工具結(jié)果會再次交給大模型由大模型組織最終回復(fù)*/Tool(description 查詢指定城市的天氣情況, returnDirect false)public String getWeather(String city) {// 實際項目中可調(diào)用第三方天氣 API這里用模擬數(shù)據(jù)演示return String.format(%s晴氣溫 25℃濕度 40%%, city);}}關(guān)鍵參數(shù)說明description工具的描述信息大模型會根據(jù)它判斷是否以及何時調(diào)用該函數(shù)。returnDirecttrue工具返回后直接作為最終響應(yīng)不再調(diào)用大模型。false工具結(jié)果會送回給大模型由大模型結(jié)合上下文生成更自然的回答。3.2 方式二實現(xiàn) Function 接口編程式工具通過實現(xiàn)java.util.function.FunctionT, R接口并包裝為FunctionTool可以更靈活地控制工具邏輯適合復(fù)雜業(yè)務(wù)場景便于做代理、鑒權(quán)、單元測試。① 定義入?yún)?recordpublic record WeatherRequest(String city) {}② 實現(xiàn) Function 接口import org.springframework.stereotype.Component;import java.util.function.Function;Componentpublic class WeatherTool implements FunctionWeatherRequest, String {Overridepublic String apply(WeatherRequest request) {// 實際項目中可在此調(diào)用天氣 APIString city request.city();return String.format(%s多云氣溫 22℃風(fēng)力 3 級, city);}}③ 包裝為 FunctionTool??不推薦直接new FunctionTool(weatherTool)無元數(shù)據(jù)構(gòu)造必須通過builder設(shè)置name、description大模型才能識別工具。生產(chǎn)最佳實踐不要在Controller方法內(nèi)每次請求構(gòu)建FunctionTool統(tǒng)一在配置類注冊為Bean復(fù)用。3.3 方式三FunctionTool.builder Lambda 編程構(gòu)建進(jìn)階動態(tài)工具這種方式不需要編寫注解也不需要實現(xiàn)Function接口直接使用 Lambda 表達(dá)式定義函數(shù)邏輯并通過FunctionTool.builder構(gòu)建工具。它最大的優(yōu)勢是靈活可以動態(tài)生成工具、臨時定義邏輯尤其適合需要根據(jù)運(yùn)行時條件生成不同工具的場景。這里繼續(xù)復(fù)用 3.2 中定義的WeatherRequestrecord 作為入?yún)⒛P?。import org.springframework.ai.tool.function.FunctionTool;import java.util.function.Function;public class WeatherToolLambda {/*** 使用 Lambda 定義天氣查詢邏輯*/public static final FunctionWeatherRequest, String WEATHER_FUNCTION request - {String city request.city();return String.format(%s陰氣溫 18℃風(fēng)力 2 級, city);};/*** 通過 FunctionTool.builder 構(gòu)建 FunctionTool*/public static FunctionTool createWeatherTool() {return FunctionTool.builder(getWeather, WEATHER_FUNCTION).description(查詢指定城市的天氣情況).inputType(WeatherRequest.class).returnDirect(false).build();}}關(guān)鍵參數(shù)說明name(getWeather)工具名稱模型返回的函數(shù)調(diào)用請求會使用該名稱。description(...)工具描述用于模型判斷是否調(diào)用。inputType(WeatherRequest.class)指定入?yún)㈩愋蚐pring AI 會據(jù)此生成 JSON Schema。returnDirect(false)工具結(jié)果是否直接返回默認(rèn)false。4. 使用 ChatModel 進(jìn)行 Tool Calling底層手動循環(huán)使用底層ChatModel必須引入DefaultToolCallingManager手動驅(qū)動工具執(zhí)行循環(huán)否則只能拿到FunctionCall JSON不會執(zhí)行業(yè)務(wù)工具。4.1 手動配置 ChatClient解決自動注入問題在當(dāng)前 Spring AI Alibaba 版本中ChatClient默認(rèn)不會自動注入需要通過Configuration顯式注冊 Bean。同時將Function接口包裝后的FunctionTool注冊為BeanController直接注入復(fù)用避免每次請求重復(fù)構(gòu)建對象。import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.tool.function.FunctionTool;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;Configurationpublic class SaaLLMConfig {Beanpublic ChatClient chatClient(ChatModel chatModel) {return ChatClient.builder(chatModel).build();}/*** 將Function接口實現(xiàn)包裝為FunctionTool注冊為單例Bean復(fù)用*/Beanpublic FunctionTool queryWeatherFunctionTool(WeatherTool weatherTool){return FunctionTool.builder(weatherTool).name(queryWeather).description(查詢指定城市天氣情況).build();}}4.2 注解方式 ChatModel手動循環(huán)import com.example.study.tools.WeatherTools;import jakarta.annotation.Resource;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.chat.prompt.Prompt;import org.springframework.ai.model.tool.ToolCallingChatOptions;import org.springframework.ai.support.ToolCallbacks;import org.springframework.ai.tool.ToolCallback;import org.springframework.ai.tool.manager.DefaultToolCallingManager;import org.springframework.ai.tool.manager.ToolCallingManager;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;RestControllerpublic class ToolCallingController {Resourceprivate ChatModel chatModel;private final ToolCallingManager toolCallingManager new DefaultToolCallingManager();GetMapping(/toolcall/chat-annotation)public String chatWithAnnotation(RequestParam(name msg, defaultValue 北京天氣怎么樣) String msg) {// 1. 將注解式工具注冊到回調(diào)數(shù)組ToolCallback[] tools ToolCallbacks.from(new WeatherTools());// 2. 構(gòu)建帶有工具回調(diào)的 ChatOptionsvar options ToolCallingChatOptions.builder().toolCallbacks(tools).build();// 3. 組裝 PromptPrompt prompt new Prompt(msg, options);var response chatModel.call(prompt);// 手動驅(qū)動工具調(diào)用循環(huán)最大循環(huán)次數(shù)防止死循環(huán)int maxRound 5;int round 0;while (response.hasToolCalls() round maxRound) {var execResult toolCallingManager.executeToolCalls(prompt, response);prompt execResult.conversationHistory();response chatModel.call(prompt);round;}return response.getResult().getOutput().getText();}}4.3 接口方式 ChatModel手動循環(huán)import jakarta.annotation.Resource;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.chat.prompt.Prompt;import org.springframework.ai.model.tool.ToolCallingChatOptions;import org.springframework.ai.tool.ToolCallback;import org.springframework.ai.tool.function.FunctionTool;import org.springframework.ai.tool.manager.DefaultToolCallingManager;import org.springframework.ai.tool.manager.ToolCallingManager;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;RestControllerpublic class ToolCallingController {Resourceprivate ChatModel chatModel;// 直接注入配置類構(gòu)建完成的FunctionTool Bean不再重復(fù)builderResourceprivate FunctionTool queryWeatherFunctionTool;private final ToolCallingManager toolCallingManager new DefaultToolCallingManager();GetMapping(/toolcall/chat-function)public String chatWithFunction(RequestParam(name msg, defaultValue 上海天氣如何) String msg) {ToolCallback[] tools new ToolCallback[]{queryWeatherFunctionTool};var options ToolCallingChatOptions.builder().toolCallbacks(tools).build();Prompt prompt new Prompt(msg, options);var response chatModel.call(prompt);int maxRound 5;int round 0;while (response.hasToolCalls() round maxRound) {var execResult toolCallingManager.executeToolCalls(prompt, response);prompt execResult.conversationHistory();response chatModel.call(prompt);round;}return response.getResult().getOutput().getText();}}4.4 Lambda 方式 ChatModel手動循環(huán)import com.example.study.tools.WeatherToolLambda;import jakarta.annotation.Resource;import org.springframework.ai.chat.model.ChatModel;import org.springframework.ai.chat.prompt.Prompt;import org.springframework.ai.model.tool.ToolCallingChatOptions;import org.springframework.ai.tool.ToolCallback;import org.springframework.ai.tool.function.FunctionTool;import org.springframework.ai.tool.manager.DefaultToolCallingManager;import org.springframework.ai.tool.manager.ToolCallingManager;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;RestControllerpublic class ToolCallingController {Resourceprivate ChatModel chatModel;private final ToolCallingManager toolCallingManager new DefaultToolCallingManager();GetMapping(/toolcall/chat-lambda)public String chatWithLambda(RequestParam(name msg, defaultValue 杭州天氣怎么樣) String msg) {FunctionTool functionTool WeatherToolLambda.createWeatherTool();ToolCallback[] tools new ToolCallback[]{functionTool};var options ToolCallingChatOptions.builder().toolCallbacks(tools).build();Prompt prompt new Prompt(msg, options);var response chatModel.call(prompt);int maxRound 5;int round 0;while (response.hasToolCalls() round maxRound) {var execResult toolCallingManager.executeToolCalls(prompt, response);prompt execResult.conversationHistory();response chatModel.call(prompt);round;}return response.getResult().getOutput().getText();}}5. 使用 ChatClient 進(jìn)行 Tool Calling自動閉環(huán)推薦ChatClient內(nèi)置ToolCallingAdvisor自動完成工具調(diào)用循環(huán)不需要手動寫while循環(huán)支持流式返回。5.1 注解方式 ChatClient 調(diào)用import com.example.study.tools.WeatherTools;import jakarta.annotation.Resource;import org.springframework.ai.chat.client.ChatClient;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;RestControllerpublic class ToolCallingController {Resourceprivate ChatClient chatClient; // 注入手動配置的 BeanGetMapping(/toolcall/chatclient-annotation)public FluxString chatClientWithAnnotation(RequestParam(name msg, defaultValue 廣州天氣怎么樣) String msg) {return chatClient.prompt(msg).tools(new WeatherTools()) // 直接傳入注解工具對象.stream() // 啟用流式調(diào)用.content(); // 返回文本內(nèi)容的 Flux}}5.2 接口方式 ChatClient 調(diào)用import jakarta.annotation.Resource;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.tool.function.FunctionTool;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;RestControllerpublic class ToolCallingController {Resourceprivate ChatClient chatClient;// 直接復(fù)用配置類中已經(jīng)構(gòu)建好的FunctionTool BeanResourceprivate FunctionTool queryWeatherFunctionTool;GetMapping(/toolcall/chatclient-function)public FluxString chatClientWithFunction(RequestParam(name msg, defaultValue 深圳天氣如何) String msg) {return chatClient.prompt(msg).tools(queryWeatherFunctionTool) // 傳入已經(jīng)構(gòu)建完成的FunctionTool.stream().content();}}5.3 Lambda 方式 ChatClient 調(diào)用import com.example.study.tools.WeatherToolLambda;import jakarta.annotation.Resource;import org.springframework.ai.chat.client.ChatClient;import org.springframework.ai.tool.function.FunctionTool;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import reactor.core.publisher.Flux;RestControllerpublic class ToolCallingController {Resourceprivate ChatClient chatClient;GetMapping(/toolcall/chatclient-lambda)public FluxString chatClientWithLambda(RequestParam(name msg, defaultValue 成都天氣如何) String msg) {FunctionTool functionTool WeatherToolLambda.createWeatherTool();return chatClient.prompt(msg).tools(functionTool) // 傳入 FunctionTool.stream().content();}}6. 測試與效果啟動項目后分別測試六個接口。底層ChatModel接口返回完整文本ChatClient系列接口為SSE流式輸出瀏覽器直接訪問即可看到逐字輸出。① ChatModel 注解工具curl http://localhost:8013/toolcall/chat-annotation?msg北京天氣怎么樣響應(yīng)示例北京晴氣溫 25℃濕度 40%② ChatModel 接口工具curl http://localhost:8013/toolcall/chat-function?msg上海天氣如何響應(yīng)示例上海多云氣溫 22℃風(fēng)力 3 級③ ChatModel Lambda 工具curl http://localhost:8013/toolcall/chat-lambda?msg杭州天氣怎么樣響應(yīng)示例杭州陰氣溫 18℃風(fēng)力 2 級④ ChatClient 注解工具流式 SSE瀏覽器打開http://localhost:8013/toolcall/chatclient-annotation?msg廣州天氣怎么樣會看到文本逐漸輸出例如廣州晴氣溫 25℃濕度 40%⑤ ChatClient 接口工具流式 SSE瀏覽器打開http://localhost:8013/toolcall/chatclient-function?msg深圳天氣如何會看到文本逐漸輸出例如深圳多云氣溫 22℃風(fēng)力 3 級⑥ ChatClient Lambda 工具流式 SSE瀏覽器打開http://localhost:8013/toolcall/chatclient-lambda?msg成都天氣如何會看到文本逐漸輸出例如成都陰氣溫 18℃風(fēng)力 2 級注意以上示例中工具返回結(jié)果后因為returnDirect false注解方式默認(rèn)或FunctionTool默認(rèn)行為大模型會再次加工生成自然語言回復(fù)。若需直接返回工具結(jié)果可調(diào)整配置或使用returnDirect選項。7. 原理與關(guān)鍵點解析7.1 Tool Calling 工作流程用戶提問→ 攜帶已注冊工具的元數(shù)據(jù)發(fā)送給 DashScope 大模型。模型判斷→ 如果需要調(diào)用某個工具返回一個“函數(shù)調(diào)用請求”包含工具名和參數(shù)。框架執(zhí)行→ Spring AI 根據(jù)返回的工具名找到對應(yīng) Java 方法并執(zhí)行獲取結(jié)果。 ChatClientAdvisor自動執(zhí)行原始ChatModel必須通過ToolCallingManager手動執(zhí)行。二次生成→ 若returnDirect false框架將工具返回結(jié)果重新提交給模型模型結(jié)合上下文生成最終回復(fù)若為true則直接返回工具結(jié)果。7.2 三種工具定義方式對比特性Tool 注解方式Function 接口方式Lambda FunctionTool.builder定義方式在方法上添加注解實現(xiàn)FunctionT, R接口Lambda 表達(dá)式 builder 構(gòu)建參數(shù)傳遞方法參數(shù)自動映射通過 record 封裝入?yún)⑼ㄟ^ record 封裝入?yún)nputType指定靈活度簡單快速適合單一方法更靈活適合復(fù)雜業(yè)務(wù)邏輯、代理鑒權(quán)、單元測試最靈活可動態(tài)構(gòu)建無需類定義注冊方式ToolCallbacks.from(obj)或.tools(obj)配置類Bean注冊Controller直接注入復(fù)用FunctionTool.builder(...).build()或.tools(functionTool)推薦場景輕量級工具快速接入需要依賴注入、復(fù)雜過濾或自定義邏輯動態(tài)工具、臨時 Lambda、避免編寫類7.3 為什么 ChatClient 不能自動注入目前 Spring AI Alibaba 的自動配置還未將ChatClient納入標(biāo)準(zhǔn) Bean 管理因此需要我們在Configuration類中手動創(chuàng)建并返回。隨著版本迭代這個問題很可能會被解決留意官方更新即可。7.4 returnDirect 的選擇需要大模型潤色結(jié)果例如“北京今天天氣晴朗溫度 25℃建議穿短袖” → 設(shè)為false。工具結(jié)果已是最終答案例如查詢用戶余額后直接返回數(shù)字 → 設(shè)為true可以節(jié)省一次模型調(diào)用成本。對于Function接口方式通過builder設(shè)置returnDirectFunctionTool functionTool FunctionTool.builder(weatherTool).name(queryWeather).description(查詢指定城市天氣情況).returnDirect(true).build();對于FunctionTool.builder Lambda方式可在 builder 中設(shè)置FunctionTool functionTool FunctionTool.builder(getWeather, WEATHER_FUNCTION).description(查詢指定城市的天氣情況).inputType(WeatherRequest.class).returnDirect(true) // returnDirect true.build();8. 總結(jié)本文以查詢天氣為例完整演示了 Spring AI Alibaba 中 Tool Calling 的六種實現(xiàn)組合注解工具 ChatModel底層手動循環(huán)接口工具 ChatModel底層手動循環(huán)Lambda 工具 ChatModel底層手動循環(huán)注解工具 ChatClient自動閉環(huán)流式接口工具 ChatClient自動閉環(huán)流式Lambda 工具 ChatClient自動閉環(huán)流式同時解決了ChatClient無法自動注入的問題并說明了流式返回的實現(xiàn)方法。生產(chǎn)優(yōu)化點靜態(tài)工具對象不要在Controller接口方法內(nèi)重復(fù)構(gòu)建統(tǒng)一在Configuration注冊單例Bean復(fù)用減少對象創(chuàng)建開銷。生產(chǎn)建議業(yè)務(wù)開發(fā)優(yōu)先選擇ChatClient避免手寫工具循環(huán)只有需要完全接管工具執(zhí)行流程、自定義鑒權(quán)攔截、需要用戶確認(rèn)后再執(zhí)行工具場景才使用原始ChatModel DefaultToolCallingManager編程式Function工具優(yōu)先用builder不要直接無元參數(shù)構(gòu)造FunctionTool安全不要只在工具內(nèi)部鑒權(quán)優(yōu)先外層動態(tài)裁剪ToolCallback工具內(nèi)部做兜底校驗。Tool注解適合快速接入簡單工具Function接口適合需要依賴注入或復(fù)雜業(yè)務(wù)邏輯的場景FunctionTool.builder Lambda適合動態(tài)構(gòu)建、臨時定義工具避免編寫額外類。希望這篇教程能幫助你快速上手 Spring AI Alibaba 的函數(shù)調(diào)用功能為構(gòu)建智能體應(yīng)用打下堅實基礎(chǔ)。