Spring Ai (Function Calling / Tool Calling) 工具调用
·
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 {
@Test
public 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; }
@Override
public 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; }
@Override
public 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; }
@Override
public 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;
@Test
void 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;
@Bean
public 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( // 设置默认的Advisor
new MessageChatMemoryAdvisor(chatMemory) // 设置基于内存的对话记忆的Advisor
// ,new MyLogAdvisor() // 设置日志Advisor
// ,new ReReadingAdvisor()
)
.build();//构建返回client
}
/**
* Ai 调用MCP
*/
@Resource
private 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 {
@Resource
LoveApp loveApp;
@Test
void 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 输出


更多推荐



所有评论(0)