Skip to content

工具调用

字数: 0 字 时长: 0 分钟

工具调用有什么用?

之前我们通过 RAG 技术让 AI 大模型具备了更专业、更准确的回答问题的能力,但也只是个问答助手,就好像是被捆住手脚的大脑一样,只能思考回答,并不能做事。

而工具调用技术(Tool CallingFunction Calling)就是让 AI 拥有手脚,不过工具调用并不是 AI 服务器自己调用这些工具,而是 AI 提出要求(“我需要执行 XX 工具完成 XX 任务”),真正执行工具的权限还是由我们的应用程序控制的,执行后再把结果告诉 AI ,让它继续工作

Spring AI 工具调用

SpringAI 工具调用实现原理.webp

  1. 工具定义与注册:Spring AI 通过注解自动生成工具定义和 JSON Schema,让 Java 方法转变为 AI 可调用的工具
  2. 工具调用请求:Spring AI 识别 AI 大模型工具调用请求并找到对应工具方法
  3. 工具执行:Spring AI 解析参数完成工具方法调用
  4. 处理工具结果:Spring AI 内置结果转换器,将工具处理结果转换为 Java 对象返回
  5. 返回结果给模型:将工具调用结果正确传递给大模型或用户
  6. 生成最终响应:自动整合工具调用结果到对话上下文

工具定义

Spring AI 支持基于 Methods 方法或 Functions 函数来定义工具,我们主要使用 Methods 方式来定义工具,更容易编写、理解、支持的参数和返回值类型更多。

java
class WeatherTools {
    @Tool(description = "Get current weather for a location")
    public String getWeather(@ToolParam(description = "The city name") String city) {
        return "Current weather in " + city + ": Sunny, 25°C";
    }
}

// 使用方式
ChatClient.create(chatModel)
    .prompt("What's the weather in Beijing?")
    .tools(new WeatherTools())
    .call();
java
@Configuration
public class ToolConfig {
    @Bean
    @Description("Get current weather for a location")
    public Function<WeatherRequest, WeatherResponse> weatherFunction() {
        return request -> new WeatherResponse("Weather in " + request.getCity() + ": Sunny, 25°C");
    }
}

// 使用方式
ChatClient.create(chatModel)
    .prompt("What's the weather in Beijing?")
    .functions("weatherFunction")
    .call();

工具使用

Spring AI 提供了多种灵活的方式将工具提供给 ChatClient,让 AI 能在需要时调用这些工具。

  1. 按需使用:直接在构建 ChatClient 请求时指定 tools() 工具
java
String response = ChatClient.create(chatModel)
    .prompt("北京今天天气怎么样?")
    .tools(new WeatherTools())  // 在这次对话中提供天气工具
    .call()
    .content();
  1. 全局使用:使用 defaultTools() 指定同一一个 ChatClient 全局对话都能使用的工具
java
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultTools(new WeatherTools(), new TimeTools())  // 注册默认工具
    .build();
  1. 直接给更底层的 ChatModel 绑定工具
java
// 先得到工具对象
ToolCallback[] weatherTools = ToolCallbacks.from(new WeatherTools());
// 绑定工具到对话
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(weatherTools)
    .build();
// 构造 Prompt 时指定对话选项
Prompt prompt = new Prompt("北京今天天气怎么样?", chatOptions);
chatModel.call(prompt);
  1. 动态解析:通过 ToolCallbackResolver 在运行时动态解析工具

工具实现

工具本质是一种插件,一种轮子,尽量不要自己写,可以在网上找到优秀的经过大众验证过的工具实现。以下列出一些常见常见的工具类:

文件操作工具类

java
/**
 *  文件操作工具类 (提供文件读写功能)
 */
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 a 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 a file to write") String fileName,
                            @ToolParam(description = "Content to write 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 (IORuntimeException e) {
            return "Error writing file: " + e.getMessage();
        }
    }

}

联网搜索工具类

可以使用专业的网页搜索 API,如 Search API 来实现联网搜索,需要注册账号,创建 API key

java
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();
        }
    }
}

PDF 生成工具类

可以使用 itext 库实现 PDF 生成,需要引入依赖

xml
<!-- https://mvnrepository.com/artifact/com.itextpdf/itext-core -->
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-core</artifactId>
    <version>9.1.0</version>
    <type>pom</type>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/font-asian -->
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>font-asian</artifactId>
    <version>9.1.0</version>
    <scope>test</scope>
</dependency>
java
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();
        }
    }
}

资源下载工具类

可以使用 Hutool 的 HttpUtil.downloadFile 实现

java
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();
        }
    }

}

终端命令操作工具类

java
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();
    }
}

网页抓取工具类

可以使用 jsoup 库实现网页内容抓取和解析,需要添加依赖

xml
<dependency>
    <groupId>org.jsoup</groupId>
    <artifactId>jsoup</artifactId>
    <version>1.19.1</version>
</dependency>
java
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();
        }
    }
}

天气预告工具类

天气预告需要调用第三方 API ,我选择的是阿里云第三方接口市场,需要申请 API key

java
public class WebSearchTool {

    // SearchAPI 的搜索接口地址
    private static final String SEARCH_API_URL = "https://www.searchapi.io/api/v1/search";

    @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();
        }
    }
}

工具注册类

编写好各种工具类之后,可以编写一个统一的注册类,将所有工具注册到 ToolCallback

java
@Configuration
public class ToolRegistration {

   @Bean
   public ToolCallback[] allTools() {
       FileOperationTool fileOperationTool = new FileOperationTool();
       PDFGenerationTool pdfGenerationTool = new PDFGenerationTool();
       ResourceDownloadTool resourceDownloadTool = new ResourceDownloadTool();
       TerminalOperationTool terminalOperationTool = new TerminalOperationTool();
       WeatherTools weatherTools = new WeatherTools();
       WebScrapingTool webScrapingTool = new WebScrapingTool();
       WebSearchTool webSearchTool = new WebSearchTool();
       return ToolCallbacks.from(fileOperationTool,
               pdfGenerationTool,
               resourceDownloadTool,
               terminalOperationTool,
               weatherTools,
               webScrapingTool,
               webSearchTool);
   }

}

测试

我提问“周末想和女朋友在重庆约会”,AI 大模型就成功调用天气工具和联网搜索工具为我推荐了适合约会的地点:

工具调用测试.webp