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