一、为什么需要工具调用?
传统的AI应用就像一个“知识问答助手”,只能基于已有知识回答问题。而通过工具调用,我们可以让AI应用具备更强大的能力:
- 🌐 联网搜索
- 📄 网页抓取
- ⬇️ 资源下载
- 💻 终端操作
- 📁 文件操作
- 📑 PDF生成
二、什么是工具调用?
工具调用是指AI应用通过调用外部工具或服务来完成特定任务的能力。这些工具可以是第三方API、系统命令、数据库查询等。
核心原理
工具调用不是AI服务器自己调用工具,也不是把工具代码发给AI服务器执行。AI服务器只是提出要求(“我需要执行XX工具完成任务”),真正执行工具的是我们自己的应用程序,执行后再把结果告诉AI。
用户 → 应用程序 → AI模型(判断需要工具)
↓
AI返回工具调用请求
↓
应用程序执行工具
↓
结果返回给AI模型
↓
AI生成最终回答 → 用户
为什么不让AI自己调用工具?
安全性是首要考虑。所有操作都必须通过你的程序执行,你可以完全控制AI能做什么、不能做什么。
💡 术语说明:工具调用(Tool Calling)和功能调用(Function Calling)本质上是同一个概念,只是叫法不同。
三、Spring AI工具开发实战
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框架,我们可以:
- ✅ 使用
@Tool注解轻松定义工具 - ✅ 统一注册和管理多个工具
- ✅ 让AI自动判断何时调用哪个工具
- ✅ 扩展无限可能的功能边界
掌握工具调用,你的AI应用将不再局限于对话,而是成为一个真正的智能执行者