当前位置: 首页 > news >正文

Spring Ai (Function Calling / Tool Calling) 工具调用

1.工具调用介绍

工具调用是现代大语言模型(LLM)的一项重要能力,允许模型在生成回复时“决定”是否需要调用某个外部函数来获取信息或执行操作。例如:

  • 联网搜索 (实现查询到大模型未学习和RAG知识库中不存在的数据)
  • 网页抓取(给大模型一个链接地址 让大模型进行分析网页)

模型会返回一个结构化的调用请求(如函数名和参数),由框架负责执行该函数并将结果返回给模型,最终生成自然语言回复。

工具调用 是一个能解决大模型非常多能力的方法,使得原先只有对话能力的大模型依靠工具能够生成 PDF 或者 根据你当前的位置信息进行附近公园 或者 游玩景点进行推荐,有或者联网搜索

2.工具调用的流程

流程解读:

  1. 用户提问 → 应用发送:将用户问题和可用工具列表一起发送给LLM

  2. LLM分析 → 判断是否需要调用工具

  3. 需要工具 → 返回调用 → 执行工具 → 返回结果

  4. LLM生成最终自然语言回答

  5. 返回用户最终结果

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 输出

http://www.dtcms.com/a/352448.html

相关文章:

  • 78-dify案例分享-零基础上手 Dify TTS 插件!从开发到部署免费文本转语音,测试 + 打包教程全有
  • 使用【阿里云百炼】搭建自己的大模型
  • Linux网络设备分析
  • 构建绿色园区新方案:能源监测+用电安全的综合能源管理系统
  • LeetCode - 227. 基本计算器 II
  • C++ `std::map` 解析:`find`, `end`, `insert` 和 `operator[]`
  • redis 在 nodejs 中如何应用?
  • 常用 Kubernetes (K8s) 命令指南
  • DevSecOps 集成 CI/CD Pipeline:实用指南
  • 【RAGFlow代码详解-30】构建系统和 CI/CD
  • 【智能化解决方案】大模型智能推荐选型系统方案设计
  • 简明 | ResNet特点、残差模块、残差映射理解摘要
  • VGVLP思路探索和讨论
  • C++ 并发编程中的锁:总结与实践
  • 绝命毒师模拟器2|单机+联机+绝命毒师模拟器1 全DLC(Drug Dealer Simulator 2+1)免安装中文版
  • 事件驱动架构详解
  • AI Agent安全的“阿喀琉斯之踵”:深度解析MCP核心风险与纵深防御架构
  • Python爬虫: 分布式爬虫架构讲解及实现
  • mysql是怎样运行的(梳理)
  • Java基础第二课:hello word
  • 传统联邦 VS 联邦+大模型
  • freeModbus TCP收发数据一段时间后,出现掉线情况(time out问题)
  • 依托边缘计算方案,移动云全面化解算力、效率、安全平衡难题
  • Wireshark捕获数据的四种层次
  • 【Python数据分析】商品数据可视化大屏项目
  • YggJS RButton 按钮组件 v1.0.0 使用教程
  • 亚马逊运营效能提升:广告策略优化与自配送售后管理的协同路径
  • Makefile构建优化:提升编译效率的关键
  • 打卡day49
  • RocketMq程序动态创建Topic