工具调用:让AI应用从“问答助手”进化为“全能执行者”

0 阅读5分钟

一、为什么需要工具调用?

传统的AI应用就像一个“知识问答助手”,只能基于已有知识回答问题。而通过工具调用,我们可以让AI应用具备更强大的能力:

  • 🌐 联网搜索
  • 📄 网页抓取
  • ⬇️ 资源下载
  • 💻 终端操作
  • 📁 文件操作
  • 📑 PDF生成

二、什么是工具调用?

工具调用是指AI应用通过调用外部工具或服务来完成特定任务的能力。这些工具可以是第三方API、系统命令、数据库查询等。

核心原理

工具调用不是AI服务器自己调用工具,也不是把工具代码发给AI服务器执行。AI服务器只是提出要求(“我需要执行XX工具完成任务”),真正执行工具的是我们自己的应用程序,执行后再把结果告诉AI。

用户 → 应用程序 → AI模型(判断需要工具)
                ↓
          AI返回工具调用请求
                ↓
          应用程序执行工具
                ↓
          结果返回给AI模型
                ↓
          AI生成最终回答 → 用户

image.png

为什么不让AI自己调用工具?

安全性是首要考虑。所有操作都必须通过你的程序执行,你可以完全控制AI能做什么、不能做什么。

💡 术语说明:工具调用(Tool Calling)和功能调用(Function Calling)本质上是同一个概念,只是叫法不同。

三、Spring AI工具开发实战

image.png

3.1 定义工具的方式

Spring AI提供两种定义工具的方式:

方式一:注解式(推荐)

使用@Tool注解标记普通Java方法,简单直观:

class WeatherTools {
    @Tool(description = "获取指定城市的当前天气情况")
    String getWeather(@ToolParam(description = "城市名称") String city) {
        return "北京今天晴朗,气温25°C";
    }
}

方式二:编程式

适合运行时动态创建工具,更灵活:

Method method = ReflectionUtils.findMethod(WeatherTools.class, "getWeather", String.class);
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinition.builder(method)
            .description("获取指定城市的当前天气情况")
            .build())
    .toolMethod(method)
    .toolObject(new WeatherTools())
    .build();

3.2 主流工具开发示例

📁 文件操作工具

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 (IORuntimeException e) {
            return "Error reading file: " + e.getMessage();
        }
    }

    @Tool(description = "Write content to a file")
    public String writeFile(@ToolParam(description = "Content to write to the file") String content,
                            @ToolParam(description = "Name of the file to write") String fileName) {
        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();
        }
    }
}

🔍 联网搜索工具

使用Search API实现百度搜索:

public class WebSearchTool {
    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);
            JSONObject jsonObject = JSONUtil.parseObj(response);
            JSONArray organicResults = jsonObject.getJSONArray("organic_results");
            int endIndex = Math.min(5, organicResults.size());
            List<Object> objects = organicResults.subList(0, endIndex);
            return objects.stream().map(obj -> ((JSONObject) obj).toString()).collect(Collectors.joining(","));
        } catch (Exception e) {
            return "Error searching Baidu: " + e.getMessage();
        }
    }
}

🕸️ 网页抓取工具

使用jsoup库解析网页内容:

// 添加依赖
// <dependency>
//     <groupId>org.jsoup</groupId>
//     <artifactId>jsoup</artifactId>
//     <version>1.22.1</version>
// </dependency>

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

💻 终端操作工具

执行系统终端命令:

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

⬇️ 资源下载工具

通过URL下载文件到本地:

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") String fileName) {
        String fileDir = FileConstant.FILE_SAVE_DIR + "/download";
        String filePath = fileDir + "/" + fileName;
        try {
            FileUtil.mkdir(fileDir);
            HttpUtil.downloadFile(url, new File(filePath));
            return "Resource downloaded successfully to: " + filePath;
        } catch (Exception e) {
            return "Error downloading resource: " + e.getMessage();
        }
    }
}

📄 PDF生成工具

使用itext库生成PDF文档:

public class PDFGenerationTool {
    @Tool(description = "Generate a PDF file with given content")
    public String generatePDF(@ToolParam(description = "File name") String fileName,
                              @ToolParam(description = "Content to include") String content) {
        String fileDir = FileConstant.FILE_SAVE_DIR + "/pdf";
        String filePath = fileDir + "/" + fileName;
        try {
            FileUtil.mkdir(fileDir);
            try (PdfWriter writer = new PdfWriter(filePath);
                 PdfDocument pdf = new PdfDocument(writer);
                 Document document = new Document(pdf)) {
                PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H");
                document.setFont(font);
                document.add(new Paragraph(content));
            }
            return "PDF generated successfully to: " + filePath;
        } catch (IOException e) {
            return "Error generating PDF: " + e.getMessage();
        }
    }
}

3.3 统一注册所有工具

@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注入配置值
  • 注册模式:集中管理和注册所有可用工具
  • 适配器模式:将不同工具转换为统一的ToolCallback数组

3.4 在ChatClient中使用工具

@Service
public class LoveApp {
    @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))
                .tools(allTools)  // 绑定所有工具
                .call()
                .chatResponse();
        return response.getResult().getOutput().getText();
    }
}

3.5 测试工具调用

@SpringBootTest
class ToolCallingTest {
    @Test
    void doChatWithTools() {
        // 测试联网搜索
        testMessage("周末想带女朋友去上海约会,推荐几个适合情侣的小众打卡地?");
        
        // 测试网页抓取
        testMessage("最近和对象吵架了,看看编程导航网站(codefather.cn)的其他情侣是怎么解决矛盾的?");
        
        // 测试资源下载
        testMessage("直接下载一张适合做手机壁纸的星空情侣图片为文件");
        
        // 测试终端操作
        testMessage("执行 Python3 脚本来生成数据分析报告");
        
        // 测试文件操作
        testMessage("保存我的恋爱档案为文件");
        
        // 测试PDF生成
        testMessage("生成一份'七夕约会计划'PDF,包含餐厅预订、活动流程和礼物清单");
    }
    
    private void testMessage(String message) {
        String chatId = UUID.randomUUID().toString();
        String answer = loveApp.doChatWithTools(message, chatId);
        Assertions.assertNotNull(answer);
    }
}

四、总结

工具调用让AI应用从“只会说”进化为“既能说又能做”。通过Spring AI框架,我们可以:

  1. ✅ 使用@Tool注解轻松定义工具
  2. ✅ 统一注册和管理多个工具
  3. ✅ 让AI自动判断何时调用哪个工具
  4. ✅ 扩展无限可能的功能边界

掌握工具调用,你的AI应用将不再局限于对话,而是成为一个真正的智能执行者