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 {


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

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐