为了助于大家学习理解,本文会通过旅游规划应用案例演示工具调用的实际应用开发:

工具调用(入门)

什么是工具调用呢?工具调用是Tool Calling也叫Function Calling,让AI大模型借用外部工具来完成它自己做不到的事情

工具调用的工作原理:整个过程是由我们应用程序来控制的,AI拿到用户的问题,发现回答不了,这时候它需要使用工具来回答用户的问题,后端程序判断出AI需要哪个工具,然后我们让AI使用这个工具。其实就是说AI只是决定什么时候用工具,需要传递什么参数,真正执行工具的是我们的程序。

那为什么不直接把工具丢给AI,而要通过后端来执行判断把工具给AI?关键是安全性,如果就把工具丢给AI让AI自己决定,那肯定是不行的,要是AI想用就用,想干嘛就干嘛,那还得了,而且这样也给AI服务器本身增加压力。所以呢所有操作必须由我们程序控制执行,不让AI接触到你的API和系统资源。

工具调用的流程:

1、工具定义:我们程序告诉AI你可以使用哪些工具,并描述工具的功能和所需参数

2、工具选择:AI根据用户问题判断要使用哪个工具,并准备好相应参数

3、返回意图:AI给程序返回“我想要使用这个工具...,参数是...”

4、工具执行:程序接收到请求,执行相应的工具操作

5、结果返回:程序将工具执行的结果返回给AI

6、继续对话:AI根据根据返回的结果,生成最终回答给用户

要实现这些我们当然可以自主开发,不过还是更推荐使用Spring AI、LangChain等开发框架

Spring AI工具开发:

1、定义工具:在Spring AI中主要有两种模式:1、(推荐)基于Methods方法 2、Functions函数式编程。第二种了解即可咱只要用第一种基于Methods方法来定义工具就可以,因为它更容易编写、理解、支持的参数和返回类型更多。

Methods模式:通过@Tool注解定义工具,通过tools方法绑定工具,使用@ToolParam注解提供额外的描述信息

Functions模式:通过@Bean注解定义工具,通过functions方法绑定工具

2、使用工具:Spring AI提供了多种灵活的方式将工具提供给ChatClient,让AI能在需要时调用工具。1、按需使用 2、全局使用 3、更底层使用方式 4、动态解析

主流工具开发:如果在github社区没有找到能满足我们需求的工具,就要自己开发一个工具,那就让我们来开始开发吧!

文件操作工具

---首先在dogaiagent根目录下新建一个tools包,在tmp目录下新建一个file目录。那为什么要在tmp下建一个file呢?这是因为文件操作提供了两大功能-->往我们电脑保存文件和读取文件,那就会涉及到一定风险,如果AI脑抽了不断往我们电脑保存垃圾文件,那就完蛋了,所以我们要规定跟AI操作文件有关的限制在tmp临时目录下的file中。

那我们继续,在dogaiagent下再新建一个constant包,再在包下新建一个FileConstant接口,来放文件操作要用的一些常量,代码如下:

public interface FileConstant { 
    /**
    * 文件保存目录 
    */ 
    String FILE_SAVE_DIR = System.getProperty("user.dir") + "/tmp";
}

在tools包下新建一个FileOperationTool类,来写文件操作工具(读写功能),代码如下:

/**
* 文件操作工具类,提供文件读写功能
*/
public class FileOperationTool {
    private final String FILE_DIR = FileConstant.FILE_SAVE_DIR + "/file";
    @Tool(description = "Read content from a file") 
    public String readFile(@ToolParam(description = "Name of the file to read") String fileName) { 
        String filePath = FILE_DIR + "/" + fileName; 
        try {
            return FileUtil.readUtf8String(filePath);
        } catch (Exception e) {
            return "Error reading file: " + e.getMessage(); 
        } 
    }  
    @Tool(description = "Write content to a file")
    public String writeFile(
        @ToolParam(description = "Name of the file to write") String fileName,
        @ToolParam(description = "Content to write to the file") String content) { 
        String filePath = FILE_DIR + "/" + fileName;
        try { 
            // 创建目录
            FileUtil.mkdir(FILE_DIR); 
            FileUtil.writeUtf8String(content, filePath); 
            return "File written successfully to: " + filePath;
        } catch (Exception e) {
            return "Error writing to file: " + e.getMessage();  
        }  
    }
}

然后编写对应单元测试,代码如下:

@SpringBootTest
class FileOperationToolTest { 
    @Test 
    void readFile() {
        FileOperationTool fileOperationTool = new FileOperationTool(); 
        String fileName = "旅游规划.txt"; 
        String result = fileOperationTool.readFile(fileName);
        Assertions.assertNotNull(result); 
    }
    @Test 
    void writeFile() {  
        FileOperationTool fileOperationTool = new FileOperationTool();
        String fileName = "旅游规划.txt";   
        String content = "TheChosenOne---欢迎来到GitHub";  
        String result = fileOperationTool.writeFile(fileName, content); 
        Assertions.assertNotNull(result); 
    }
}

注意:测试时要先写再读,也就是先运行writeFile()再运行readFile()。

联网搜索工具

---使用专业的网页搜索API,如Search API,实现从多个网站搜索内容,不过要计费,但是呢新用户免费送100次请求,那让我们来开发一个联网搜索工具吧。

在tools包下新建一个WebSearchTool类,然后呢很简单,找到Search API的baidu将这里面文档复制给AI让AI给我们生成Java代码,然后将代码复制粘贴到WebSearchTool类中,代码如下:

/**
* Web 搜索工具类,提供百度搜索功能
*/
public class WebSearchTool { 
    // SearchAPI 的搜索接口地址
    private static final String SEARCH_API_URL = "https://www.searchapi.io/api/v1/search";
    private final String apiKey; 
    public WebSearchTool(String apiKey) { 
        this.apiKey = apiKey; 
    } 
    @Tool(description = "Search for information from Baidu Search Engine") 
    public String searchWeb( 
        @ToolParam(description = "Search query keyword") String query) { 
        Map<String, Object> paramMap = new HashMap<>();   
        paramMap.put("q", query);   
        paramMap.put("api_key", apiKey); 
        paramMap.put("engine", "baidu");   
        try {        
            String response = HttpUtil.get(SEARCH_API_URL, paramMap);   
            // 取出返回结果的前 5 条 
            JSONObject jsonObject = JSONUtil.parseObj(response); 
            // 提取 organic_results 部分  
            JSONArray organicResults = jsonObject.getJSONArray("organic_results");  
            List<Object> objects = organicResults.subList(0, 5);  
            // 拼接搜索结果为字符串  
            String result = objects.stream().map(obj -> {   
                JSONObject tmpJSONObject = (JSONObject) obj;  
                return tmpJSONObject.toString(); 
            }).collect(Collectors.joining(",")); 
            return result;  
        } catch (Exception e) { 
            return "Error searching Baidu: " + e.getMessage();  
        }  
    }
}

然后在application.yml和application-local.yml中配置API Key:

# searchApi
search-api:
api-key: 你的 API Key

然后编写对应的单元测试,代码如下:

@SpringBootTest
class WebSearchToolTest {
    @Value("${search-api.api-key}") 
    private String searchApiKey;
    @Test 
    void searchWeb() {  
        WebSearchTool webSearchTool = new WebSearchTool(searchApiKey);
        String query = "如何学习Java+AI";  
        String result = webSearchTool.searchWeb(query);  
        Assertions.assertNotNull(result);  
    }
}

注意:如果测试出来的result为error,不用担心,因为AI是有可能失败的,多Debug测试几次会成功的。

网页抓取工具

---网页抓取工具的作用是根据网址解析到网页的内容,使用jsoup库来实现网页内容抓取和解析,让我们来开发吧!

首先引入jsoup的依赖:

<dependency>
  <groupId>org.jsoup</groupId>
  <artifactId>jsoup</artifactId>
  <version>1.21.2</version>
</dependency>

在tools包下新建一个WebScrapingTool类,实现网页抓取工具代码如下:

/**
* Web 抓取工具类,提供网页抓取功能
*/
public class WebScrapingTool {
    @Tool(description = "Scrape the content of a web page")
    public String scrapeWebPage(@ToolParam(description = "URL of the web page to scrape") 
                                String url) {
        try { 
            Document doc = Jsoup.connect(url).get(); 
            return doc.html(); 
        } catch (IOException e) {   
            return "Error scraping web page: " + e.getMessage();  
        } 
    }
}

然后生成对应单元测试,代码如下:

class WebScrapingToolTest {
    @Test 
    void scrapeWebPage() { 
        WebScrapingTool webScrapingTool = new WebScrapingTool(); 
        String url = "https://www.baidu.com/"; 
        String result = webScrapingTool.scrapeWebPage(url);
        Assertions.assertNotNull(result);
    }
}

注意:Debug测试的result可能抓取不了网页,这是因为网页是异步的,没有加载时间所以看不到抓取的信息,可以使用Sublime Text粘贴进去看。

终端操作工具

---指在终端执行命令,比如执行python命令来运行脚本

在tools包下新建一个TerminalOperationTool类,AI生成就好了,不用自己写,实现终端操作工具代码如下(注意:这个的代码Windows和其他系统不一样),其他系统代码如下:

public class TerminalOperationTool { 
    @Tool(description = "Execute a command in the terminal") 
    public String executeTerminalCommand(@ToolParam(description = "Command to execute in the terminal") String command) {
        StringBuilder output = new StringBuilder();
        try {
            Process process = Runtime.getRuntime().exec(command);
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line;
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n");
                }
            } 
            int exitCode = process.waitFor();
            if (exitCode != 0) {
                output.append("Command execution failed with exit code: ").append(exitCode); 
            }
        } catch (IOException | InterruptedException e) { 
            output.append("Error executing command: ").append(e.getMessage()); 
        }
        return output.toString();
    }
}

Windows的代码如下:

public class TerminalOperationTool {
    @Tool(description = "Execute a command in the terminal")
    public String executeTerminalCommand(@ToolParam(description = "Command to execute in the terminal") String command) {
        StringBuilder output = new StringBuilder();
        try {  
            ProcessBuilder builder = new ProcessBuilder("cmd.exe", "/c", command);
            //            Process process = Runtime.getRuntime().exec(command); 
            Process process = builder.start(); 
            try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
                String line; 
                while ((line = reader.readLine()) != null) {
                    output.append(line).append("\n"); 
                }   
            }    
            int exitCode = process.waitFor();
            if (exitCode != 0) { 
                output.append("Command execution failed with exit code: ").append(exitCode);            }
        } catch (IOException | InterruptedException e) { 
            output.append("Error executing command: ").append(e.getMessage()); 
        }  
        return output.toString();
    }
}

然后生成对应单元测试,代码如下:

@SpringBootTest
class TerminalOperationToolTest { 
    @Test  
    void executeTerminalCommand() {  
        TerminalOperationTool tool = new TerminalOperationTool();  
        String command = "dir"; 
        String result = tool.executeTerminalCommand(command);  
        Assertions.assertNotNull(result);   
    }
}

资源下载工具

---使用Hutool的HttpUtil.downloadFile方法实现资源下载

在tools包下新建一个ResourceDownloadTool类,实现代码如下:

public class ResourceDownloadTool {
    @Tool(description = "Download a resource from a given URL") 
    public String downloadResource(@ToolParam(description = "URL of the resource to download") String url, @ToolParam(description = "Name of the file to save the downloaded resource") String fileName) { 
        String fileDir = FileConstant.FILE_SAVE_DIR + "/download";  
        String filePath = fileDir + "/" + fileName; 
        try {    
            // 创建目录    
            FileUtil.mkdir(fileDir);
            // 使用 Hutool 的 downloadFile 方法下载资源    
            HttpUtil.downloadFile(url, new File(filePath)); 
            return "Resource downloaded successfully to: " + filePath; 
        } catch (Exception e) { 
            return "Error downloading resource: " + e.getMessage();
        }  
    }
}

然后生成对应单元测试,测试成功会在download目录下生成logo.png,测试代码如下:

@SpringBootTest
class ResourceDownloadToolTest { 
    @Test
    void downloadResource() { 
        ResourceDownloadTool tool = new ResourceDownloadTool(); 
        String url = "https://www.baidu.com/img/bd_logo1.png"; 
        String fileName = "logo.png"; 
        String result = tool.downloadResource(url, fileName); 
        assertNotNull(result);  
    }
}

PDF生成工具

---作用是根据文件名和内容生成PDF文档并且保存,使用itext库实现PDF生成工具,不过itext对中文字体不太友好,需要我们额外配置中文字体,要做好建议自行下载需要的字体使用,我们这边直接就使用itext内置的中文字体

首先引入依赖:

<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>itext-core</artifactId>
  <version>9.1.0</version>
  <type>pom</type>
</dependency>
<dependency>
  <groupId>com.itextpdf</groupId>
  <artifactId>font-asian</artifactId>
  <version>9.1.0</version>
  <scope>test</scope>
</dependency>

在tools包下新建一个PDFGenerationTool类,实现以下代码:

public class PDFGenerationTool { 
    @Tool(description = "Generate a PDF file with given content")
    public String generatePDF(  
        @ToolParam(description = "Name of the file to save the generated PDF") String fileName,            @ToolParam(description = "Content to be included in the PDF") String content) { 
        String fileDir = FileConstant.FILE_SAVE_DIR + "/pdf"; 
        String filePath = fileDir + "/" + fileName;  
        try {      
            // 创建目录  
            FileUtil.mkdir(fileDir);  
            // 创建 PdfWriter 和 PdfDocument 对象 
            try (PdfWriter writer = new PdfWriter(filePath);   
                 PdfDocument pdf = new PdfDocument(writer);  
                 Document document = new Document(pdf)) {   
                // 自定义字体(需要人工下载字体文件到特定目录)
                //                String fontPath = Paths.get("src/main/resources/static/fonts/simsun.ttf")
                //                        .toAbsolutePath().toString();
                //                PdfFont font = PdfFontFactory.createFont(fontPath,
                //                        PdfFontFactory.EmbeddingStrategy.PREFER_EMBEDDED);                // 使用内置中文字体 
                PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H");                document.setFont(font); 
                // 创建段落 
                Paragraph paragraph = new Paragraph(content);
                // 添加段落并关闭文档  
                document.add(paragraph);
            } 
            return "PDF generated successfully to: " + filePath; 
        } catch (IOException e) {  
            return "Error generating PDF: " + e.getMessage(); 
        } 
    }
}

然后生成对应单元测试进行测试一下,代码如下:

@SpringBootTest
class PDFGenerationToolTest {
    @Test 
    void generatePDF() { 
        PDFGenerationTool tool = new PDFGenerationTool(); 
        String fileName = "百度.pdf"; 
        String content = "百度网址 https://www.baidu.com/"; 
        String result = tool.generatePDF(fileName, content); 
        assertNotNull(result);  
    }
}

开发了这么多工具,为了方便我们统一管理和绑定所有工具,可以给AI一次性提供所有工具,让AI决定什么时候使用,我们需要创建一个集中工具注册类

在tools包下新建一个ToolRegistration类,编写以下代码:

/**
* 集中的工具注册
*/
@Configuration
public class ToolRegistration {
    @Value("${search-api.api-key}") 
    private String searchApiKey; 
    @Bean 
    public ToolCallback[] allTools() { 
        FileOperationTool fileOperationTool = new FileOperationTool();  
        WebSearchTool webSearchTool = new WebSearchTool(searchApiKey); 
        WebScrapingTool webScrapingTool = new WebScrapingTool(); 
        ResourceDownloadTool resourceDownloadTool = new ResourceDownloadTool();   
        TerminalOperationTool terminalOperationTool = new TerminalOperationTool();  
        PDFGenerationTool pdfGenerationTool = new PDFGenerationTool(); 
        return ToolCallbacks.from(  
            fileOperationTool,  
            webSearchTool,   
            webScrapingTool,  
            resourceDownloadTool,   
            terminalOperationTool,   
            pdfGenerationTool 
        );  
    }
}

注意:@Value引的是import org.springframework.beans.factory.annotation.Value;

补充:它暗含了好几种设计模式:

      1. 工厂模式:allTools() 方法作为一个工厂方法,负责创建和配置多个工具实例,然后将它们包装成统一的数组返回。这符合工厂模式的核心思想 - 集中创建对象并隐藏创建细节。
      2. 依赖注入模式:通过 @Value 注解注入配置值,以及将创建好的工具通过 Spring 容器注入到需要它们的组件中。
      3. 注册模式:该类作为一个中央注册点,集中管理和注册所有可用的工具,使它们能够被系统其他部分统一访问。
      4. 适配器模式的应用:ToolCallbacks.from 方法可以看作是一种适配器,它将各种不同的工具类转换为统一的 ToolCallback 数组,使系统能够以一致的方式处理它们。

有了这个注⁡册类,如果需要添加或移除工具,只需修改这一个类即可,更利于维护。

然后我们来给我们的项目使用这些工具,在TravelApp中,编写以下代码:

//旅游规划大师调用工具能力
@Resource
private ToolCallback[] allTools;
public String doChatWithTools(String message, String chatId) { 
    ChatResponse response = chatClient 
    .prompt()         
    .user(message) 
    .advisors(spec -> spec.param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId)
              .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 10))  
    // 开启日志,便于观察效果    
    .advisors(new MyLoggerAdvisor())   
    .tools(allTools)   
    .call()       
    .chatResponse();  
    String content = response.getResult().getOutput().getText(); 
    log.info("content: {}", content);  
    return content;
}

生成对应的单元测试,代码如下:

@Test
void doChatWithTools() {   
    // 测试联网搜索问题的答案 
    testMessage("国庆打算和女朋友在福建约会,推荐几个治愈的小众打卡地?"); 
    // 测试网页抓取:旅游规划案例分析
    testMessage("最近学习压力太大了,怎么通过旅游来放松"); 
    // 测试资源下载:图片下载    
    testMessage("下载一张厦门鼓浪屿的图片");   
    // 测试终端操作:执行代码   
    testMessage("执行 Python3 脚本来生成数据分析报告"); 
    // 测试文件操作:保存用户档案     
    testMessage("保存我的旅游规划档案为文件");    
    // 测试 PDF 生成   
    testMessage("生成一份‘国庆旅游计划清单’PDF,包含餐厅预订、活动流程和景点打卡"); 
}  
private void testMessage(String message) { 
String chatId = UUID.randomUUID().toString(); 
String answer = travelApp.doChatWithTools(message, chatId); 
Assertions.assertNotNull(answer);
}

给工具⁡类的代码打断点,在 Debug 模式下观察工具的调 用过程和结果。不过,这边报错了,是因为itext的字体原因无法处理,那我们把注释的自定义字体给它开启,把内置的字体关掉,就可以了。

工具调用(进阶知识,了解即可)

  1. 工具底层数据结构:Spring AI工具调用的核心在ToolCallback接口,它是所有工具实现的基础。
  2. 工具上下文:实际开发应用中,工具执行可能需要额外的上下文信息,Spring AI通过ToolContext提供这一功能。
  3. 立即返回:工具执行的结果不需要再经过AI模型处理,而是希望直接返回给用户,Spring AI通过returnDirect属性支持这一功能。
  4. 工具底层执行原理:Spring AI提供了两种工具执行模式1、框架控制的工具执行 2、用户控制的工具执行,它们都与ToolCallingManager这个管理AI调用全过程的核心组件有关。
  5. 工具解析
  6. 可观测性
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐