1.工具調用介紹
工具調用是現代大語言模型(LLM)的一項重要能力,允許模型在生成回復時“決定”是否需要調用某個外部函數來獲取信息或執行操作。例如:
- 聯網搜索 (實現查詢到大模型未學習和RAG知識庫中不存在的數據)
- 網頁抓取(給大模型一個鏈接地址 讓大模型進行分析網頁)
模型會返回一個結構化的調用請求(如函數名和參數),由框架負責執行該函數并將結果返回給模型,最終生成自然語言回復。
工具調用 是一個能解決大模型非常多能力的方法,使得原先只有對話能力的大模型依靠工具能夠生成 PDF 或者 根據你當前的位置信息進行附近公園 或者 游玩景點進行推薦,有或者聯網搜索
2.工具調用的流程
流程解讀:
-
用戶提問?→?應用發送:將用戶問題和可用工具列表一起發送給LLM
-
LLM分析?→ 判斷是否需要調用工具
-
需要工具?→?返回調用?→?執行工具?→?返回結果
-
LLM生成最終自然語言回答
-
返回用戶最終結果
3.編寫工具
3.1 網頁抓取工具
引入Maven坐標 , 這個解析器主要用于把你傳入的鏈接解析成HTML
<!--jsoup HTML 解析庫網頁抓取工具--> <dependency><groupId>org.jsoup</groupId><artifactId>jsoup</artifactId><version>1.19.1</version> </dependency>
編寫網頁抓取工具代碼
返回值是String 是為了告訴大模型
package com.xiaog.aiapp.tools;import org.jsoup.Connection;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;/*** 網頁抓取工具*/
public class WebScrapingTool {@Tool(description = "網頁抓取工具,用于抓取網頁內容")public String scrapeWebPage(@ToolParam (description = "要抓取的網頁URL") String url){try {Connection connect = Jsoup.connect(url); //鏈接至url網址Document elements = connect.get();return elements.html();} catch (Exception e) {return "Error 網頁抓取失敗"+e.getMessage() ;}}}
就是這么簡單,那么我們編寫一下測試吧
我傳入的Url是博主自己的主頁
package com.xiaog.aiapp.tools;import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class WebScrapingToolTest {@Testpublic void testScrapeWebPage() throws Exception {WebScrapingTool webScrapingTool=new WebScrapingTool();String result = webScrapingTool.scrapeWebPage("https://blog.csdn.net/typeracer/article/details/140711057");System.out.println( result);}}
返回的結果是博主主頁被解析成為HTML的結構
3.2 聯網搜索工具
通過HTTP遠程調用方式訪問 通曉WebSearch服務
具體文檔地址:
通用搜索-通曉統一接口_信息查詢服務(IQS)-阿里云幫助中心
3.2.1?響應數據結構 - PageItem
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;// 響應數據結構 - PageItem@Data@JsonIgnoreProperties(ignoreUnknown = true)public class PageItem {//網站標題@JsonProperty("title")private String title;//網站地址@JsonProperty("link")private String link;//網頁發布時間,ISO時間格式//2025-04-27T20:36:04+08:00@JsonProperty("publishedTime")private String publishedTime;@JsonProperty("hostname")private String hostname;@JsonProperty("summary")private String summary;/*** 解析得到的網頁全正文,長度最大3000字符,召回比例超過98%*/@JsonProperty("mainText")private String mainText;/*** 網頁動態摘要,匹配到關鍵字的部分內容,平均長度150字符*/@JsonProperty("snippet")private String snippet;/*** 解析得到的網頁全文markdown格式*/@JsonProperty("markdownText")private String markdownText;@JsonProperty("hostLogo")private String hostLogo;@JsonProperty("rerankScore")private Double rerankScore;@JsonProperty("hostAuthorityScore")private Double hostAuthorityScore;// Getter 方法public String getTitle() { return title; }public String getLink() { return link; }public String getPublishedTime() { return publishedTime; }public String getHostname() { return hostname; }public String getSummary() { return summary; }public String getMainText() { return mainText; }public String getSnippet() { return snippet; }public String getMarkdownText() { return markdownText; }public String getHostLogo() { return hostLogo; }public Double getRerankScore() { return rerankScore; }public Double getHostAuthorityScore() { return hostAuthorityScore; }@Overridepublic String toString() {return "PageItem{" +"title='" + title + '\'' +", link='" + link + '\'' +", publishedTime='" + publishedTime + '\'' +", hostname='" + hostname + '\'' +'}';}}
3.2.2 響應數據結構 - SceneItem (垂直領域的結構)
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;/*** 垂類場景結果類型*/
// 響應數據結構 - SceneItem@JsonIgnoreProperties(ignoreUnknown = true)public class SceneItem {/*** 垂類場景結果類型(如天氣、時間、日歷等)*/@JsonProperty("type")private String type;/*** 返回的是一個json 類型字符串*/@JsonProperty("detail")private String detail;public String getType() { return type; }public String getDetail() { return detail; }@Overridepublic String toString() {return "SceneItem{" +"type='" + type + '\'' +", detail='" + detail + '\'' +'}';}}
? ? ? ?3.2.3 完整的相應結構類型 - SearchResponse?
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;import java.util.List;// 完整的響應數據結構@JsonIgnoreProperties(ignoreUnknown = true)public class SearchResponse {@JsonProperty("requestId")private String requestId;@JsonProperty("pageItems")private List<PageItem> pageItems;@JsonProperty("sceneItems")private List<SceneItem> sceneItems;@JsonProperty("searchInformation")//搜索消耗時間private Object searchInformation;@JsonProperty("queryCo ntext")private Object queryContext;@JsonProperty("costCredits")/*** 計算費用*/private Object costCredits;public String getRequestId() { return requestId; }public List<PageItem> getPageItems() { return pageItems; }public List<SceneItem> getSceneItems() { return sceneItems; }public Object getSearchInformation() { return searchInformation; }public Object getQueryContext() { return queryContext; }public Object getCostCredits() { return costCredits; }@Overridepublic String toString() {return "SearchResponse{" +"requestId='" + requestId + '\'' +", pageItems=" + pageItems +", sceneItems=" + sceneItems +'}';}}
3.2.4 完整的請求類型 - SearchRequest?
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import com.fasterxml.jackson.annotation.JsonProperty;// 請求數據結構public class SearchRequest {@JsonProperty("query")/*** 搜索內容*/private String query;@JsonProperty("numResults")/*** 返回條數*/private Integer numResults;public SearchRequest() {}public SearchRequest(String query) {this.query = query;}public SearchRequest(String query, Integer numResults) {this.query = query;this.numResults = numResults;}public String getQuery() { return query; }public void setQuery(String query) { this.query = query; }public Integer getNumResults() { return numResults; }public void setNumResults(Integer numResults) { this.numResults = numResults; }}
3.2.5 通曉客戶端的編寫 - TongXiaoSearchClient?
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpRequest.BodyPublishers;
import java.time.Duration;import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;public class TongXiaoSearchClient {private static final String API_URL = "https://cloud-iqs.aliyuncs.com/search/llm";private final String apiKey ;;private final HttpClient httpClient;private final ObjectMapper objectMapper;public TongXiaoSearchClient(String apiKey) {this.apiKey=apiKey;this.httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build();this.objectMapper = new ObjectMapper();}/*** 執行搜索請求* @param request 搜索請求對象* @return 搜索響應對象* @throws Exception 如果請求失敗或解析錯誤*/public SearchResponse executeSearch(SearchRequest request) throws Exception {// 將請求對象轉換為JSON字符串String jsonBody;try {jsonBody = objectMapper.writeValueAsString(request);} catch (JsonProcessingException e) {throw new RuntimeException("Failed to serialize request to JSON", e);}// 構建HTTP請求HttpRequest httpRequest = HttpRequest.newBuilder().uri(URI.create(API_URL)).header("Authorization", "Bearer " + apiKey).header("Content-Type", "application/json").POST(BodyPublishers.ofString(jsonBody)).timeout(Duration.ofSeconds(30)).build();// 發送請求并獲取響應HttpResponse<String> response = httpClient.send(httpRequest,HttpResponse.BodyHandlers.ofString());// 檢查HTTP狀態碼if (response.statusCode() != 200) {throw new RuntimeException("HTTP error: " + response.statusCode() + ", body: " + response.body());}// 解析JSON響應try {return objectMapper.readValue(response.body(), SearchResponse.class);} catch (JsonProcessingException e) {throw new RuntimeException("Failed to parse response JSON: " + response.body(), e);}}/*** 簡化搜索方法* @param query 搜索查詢詞* @param numResults 返回結果數量(可選,最大10)* @return 搜索響應對象* @throws Exception 如果請求失敗或解析錯誤*/public SearchResponse search(String query, Integer numResults) throws Exception {SearchRequest request;if (numResults != null) {request = new SearchRequest(query, numResults);} else {request = new SearchRequest(query);}return executeSearch(request);}/*** 最簡單的搜索方法,只使用查詢詞* @param query 搜索查詢詞* @return 搜索響應對象* @throws Exception 如果請求失敗或解析錯誤*/public SearchResponse search(String query) throws Exception {return search(query, 5);}}
3.2.6 將客戶端封裝 成為 - WebSearchTool 工具
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import jakarta.annotation.Resource;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
import org.springframework.beans.factory.annotation.Autowired;/*** 給AI提供網頁搜索的功能*/
public class WebSearchTool {String apiKey;public WebSearchTool(String apiKey) {this.apiKey = apiKey;}@Tool(description = "網頁搜索工具,用于互聯網搜索")public String webSearch(@ToolParam(description = "需要搜索的內容") String query){try{TongXiaoSearchClient tongXiaoSearchClient = new TongXiaoSearchClient(apiKey);//進行問題查詢返回前 5條查詢到的信息SearchResponse response = tongXiaoSearchClient.search(query, 5);StringBuilder result = new StringBuilder();// 處理網頁搜索結果if (response.getPageItems() != null && !response.getPageItems().isEmpty()) {result.append("網頁搜索結果數量: ").append(response.getPageItems().size()).append("\n");for (PageItem item : response.getPageItems()) {result.append("標題: ").append(safeToString(item.getTitle())).append("\n");result.append("鏈接: ").append(safeToString(item.getLink())).append("\n");result.append("摘要: ").append(safeToString(item.getSummary())).append("\n");result.append("片段: ").append(safeToString(item.getSnippet())).append("\n");result.append("發布時間: ").append(safeToString(item.getPublishedTime())).append("\n");result.append("重新排名分數: ").append(safeToString(item.getRerankScore())).append("\n");result.append("---\n");}} else {result.append("網頁搜索結果數量: 0\n");}// 處理垂類場景結果if (response.getSceneItems() != null && !response.getSceneItems().isEmpty()) {result.append("垂類場景結果數量: ").append(response.getSceneItems().size()).append("\n");for (SceneItem item : response.getSceneItems()) {result.append("類型: ").append(safeToString(item.getType())).append("\n");result.append("詳情: ").append(safeToString(item.getDetail())).append("\n");result.append("---\n");}} else {result.append("垂類場景結果數量: 0\n");}return result.toString();}catch (Exception e){return "請求 出現 問題 !!"+e.getMessage() ;}}// 輔助方法:防止 null 值導致輸出 "null" 字符串private static String safeToString(Object obj) {return obj == null ? "" : obj.toString();}}
編寫測試(apiKey 更換成為自己的)?
apikey 申請地址 :?信息查詢服務控制臺
package com.xiaog.aiapp.tools.tongxiaoWebSearch;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;import static org.junit.jupiter.api.Assertions.*;@SpringBootTest
class WebSearchToolTest {@Value("${tongxiao.apiKey}")String apiKey;@Testvoid webSearch() {WebSearchTool webSearchTool = new WebSearchTool(apiKey);String result = webSearchTool.webSearch("今天柳州天氣如何呢?");System.out.println(result);}}
測試結果: 聯網搜索成功了
4.將編寫好的工具集中注冊
創建一個集中注冊工具的配置類,別忘記apikey 更換成為自己的
@Configuration
public class ToolRegistration {@Value("${tongxiao.apiKey}")String apiKey;@Beanpublic ToolCallback[] tools() {/*** 網頁搜索工具類(用于互聯網搜索)*/WebSearchTool webSearchTool = new WebSearchTool(apiKey);/*** 網頁抓取工具類(用于抓取網頁內容)*/WebScrapingTool webScrapingTool = new WebScrapingTool();return ToolCallbacks.from( webSearchTool, webScrapingTool);}
編寫一個chatClient
自動注入
@Component
public class LoveApp {ChatClient chatClient;private static final String SYSTEM_PROMPT = "扮演深耕戀愛心理領域的專家。開場向用戶表明身份,告知用戶可傾訴戀愛難題。" +"圍繞單身、戀愛、已婚三種狀態提問:單身狀態詢問社交圈拓展及追求心儀對象的困擾;" +"戀愛狀態詢問溝通、習慣差異引發的矛盾;已婚狀態詢問家庭責任與親屬關系處理的問題。" +"引導用戶詳述事情經過、對方反應及自身想法,以便給出專屬解決方案。";public LoveApp(ChatModel dashscopeChatModel){// 初始化基于內存的對話記憶ChatMemory chatMemory = new InMemoryChatMemory();// String basePath = System.getProperty("user.home")+ "/.tmp/chat-memory";
// System.out.println("basePath:"+basePath);
// //初始化基于文件的對話記憶
// ChatMemory chatMemory = new FileBasedChatMemory(basePath);chatClient = ChatClient.builder(dashscopeChatModel) // 創建基于(某個chatModel)大模型的ChatClient.defaultSystem(SYSTEM_PROMPT) // 設置默認系統提示詞.defaultAdvisors( // 設置默認的Advisornew MessageChatMemoryAdvisor(chatMemory) // 設置基于內存的對話記憶的Advisor
// ,new MyLogAdvisor() // 設置日志Advisor
// ,new ReReadingAdvisor()).build();//構建返回client}/*** Ai 調用MCP*/@Resourceprivate ToolCallbackProvider toolCallbackProvider; //自動注入已經注冊了的工具public String dochatWhithMCP(String message,String id){ChatResponse chatResponse = chatClient.prompt().user(message).advisors(advisorSpec -> advisorSpec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, id) //根據會話id獲取對話歷史.param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))//獲取歷史消息的條數.tools(toolCallbackProvider).call().chatResponse();return chatResponse.getResult().getOutput().getText();}
}
5.測試工具是否生效
進行工具測試,編寫測試類
@SpringBootTest
class LoveAppTest {@ResourceLoveApp loveApp;@Testvoid doChatWithTools() {/*** 網頁搜索工具類(用于互聯網搜索)*/testMessage("柳州市的25年 8 月 24日的天氣怎么樣?");/*** 網頁抓取工具類(用于抓取網頁內容)*/testMessage("最近發現了一個博主寫博客很精彩 https://blog.csdn.net/Dajiaonew?type=blog 能不能靠這個網頁和我說一下他寫了什么文章");}private void testMessage(String message) {String chatId = UUID.randomUUID().toString();String answer = loveApp.dochatWhithTools(message, chatId);Assertions.assertNotNull(answer);}}
日志記錄的結果:
這里是我自定義了 advisor 記錄的日志, 如果你看到結果的話 也可以直接進行sout 輸出