之前我们通过 RAG 技术让 AI 应用具备了根据外部知识库来获取信息并回答的能力,但是直到目前为止,AI应用还只是个"知识问答助手"。本节我们可以利用 工具调用 特性,实现更多需求如:网页抓取、联网搜索、资源下载、终端操作、文件读写、pdf生成等。
一、工具调用简介
什么是工具调用?
工具调用(Tool Calling/Function Calling)可以理解为让Al大模型借用外部工具 来完成它自己做不到的事情。 跟人类一样,如果只凭手脚完成不了工作,那么就可以利用工具箱来完成。 工具可以是任何东西,比如网页搜索、对外部 API的调用、访问外部数据、或执行特定的代码等。 比如用户提问"帮我查询上海最新的天气",AI 本身并没有这些知识,它就可以调用""查询天气工具",来完成任务。 目前工具调用技术发展的已经比较成熟了,几乎所有主流的、新出的 Al 大模型和 AI 应用开发平台都支持工具调用。
工具调用的原理
并不是让AI服务器自己调用工具,也不是把工具代码给AI让它执行,AI需要向我们的程序提出调用请求,并附带工具名称对应参数,当我们的程序执行完后再把结果告诉AI让他继续工作。 比如:
- 用户提出问题:"编程导航网站有哪些热门文章?
- 程序将问题传递给大模型
- 大模型分析问题,判断需要使用工具(网页抓取工具)来获取信息
- 大模型输出工具名称和参数(网页抓取工具,URL参数为 codefather.cn)
- 程序接收工具调用请求,执行网页抓取操作
- 工具执行抓取并返回文章数据7.程序将抓取结果传回给大模型
- 大模型分析网页内容,生成关于编程导航热门文章的回答
- 程序将大模型的回答返回给用户

为何要这么设计?这样不是会导致程序请求AI多次造成token浪费吗?为何不让AI服务直接调用工具程序? 是出于安全性 考虑,这样设计其实AI的所有调用工具行为都可以通过我们的程序来控制,我们能掌握AI的行为,如果将工具调用能力直接赋予AI就会导致一些不确定的安全性问题,AI最大的弊端就是不确定性。
工具调用流程
- 工具定义:程序告诉 A!"你可以使用这些工具",并描述每个工具的功能和所需参数
- 工具选择:AI 在对话中判断需要使用某个工具,并准备好相应的参数
- 返回意图:A| 返回"我想用 XX 工具,参数是 XXX"的信息
- 工具执行:我们的程序接收请求,执行相应的工具操作
- 结果返回:程序将工具执行的结果发回给 AI
- 继续对话:AI 根据工具返回的结果,生成最终回答给用户
通过上述流程,我们会发现,程序需要和 AI 多次进行交互、还要能够执行对应的工具,怎么实现这些呢?我们当然可以自主开发,不过还是更推荐使用 Spring Al、La1gChain 等开发框架。此外,有些 A1大模型服务商也提供了对应的 SDK,都能够简化代码编写。 不是所有大模型都支持工具调用,可在Spring AI 官方文档查看不同模型支持情况
二、Spring AI工具开发
工具的定义
在 Spring AI 中,定义工具主要有两种模式:基于 Methods 方法或者 Functions 函数式编程。
记结论就行了,我们只用学习 基于 Methods 方法 来定义工具,另外一种了解即可。原因是 Methods 方式更容易编写、更容易理解、支持的参数和返回类型更多。
Spring AI 提供了两种定义工具的方法 ------ 注解式 和 编程式。
1)注解式:只需使用 @Tool
注解标记普通 Java 方法,就可以定义工具了,简单直观。 每个工具最好都添加详细清晰的描述,帮助 AI 理解何时应该调用这个工具。对于工具方法的参数,可以使用@ToolParam
注解提供额外的描述信息和是否必填。
java
class WeatherTools {
@Tool(description = "获取指定城市的当前天气情况")
String getWeather(@ToolParam(description = "城市名称") String city) {
// 获取天气的实现逻辑
return "北京今天晴朗,气温25°C";
}
}
2)编程式:如果想在运行时动态创建工具,可以选择编程式来定义工具,更灵活。 先定义工具类
java
class WeatherTools {
String getWeather(String city) {
// 获取天气的实现逻辑
return "北京今天晴朗,气温25°C";
}
}
然后将工具类转换为 ToolCallback 工具定义类,之后就可以把这个类绑定给 ChatClient,从而让 AI 使用工具了。
java
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();
使用工具
1)按需使用:这是最简单的方式,直接在构建 ChatClient 请求时通过 tools()
方法附加工具。这种方式适合只在特定对话中使用某些工具的场景。
java
String response = ChatClient.create(chatModel)
.prompt("北京今天天气怎么样?")
.tools(new WeatherTools()) // 在这次对话中提供天气工具
.call()
.content();
2)全局使用:如果某些工具需要在所有对话中都可用,可以在构建 ChatClient 时注册默认工具。这样,这些工具将对从同一个 ChatClient 发起的所有对话可用。
java
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new WeatherTools(), new TimeTools()) // 注册默认工具
.build();
3)更底层的使用方式:除了给 ChatClient 绑定工具外,也可以给更底层的 ChatModel 绑定工具(毕竟工具调用是 AI 大模型支持的能力),适合需要更精细控制的场景。
java
// 先得到工具对象
ToolCallback[] weatherTools = ToolCallbacks.from(new WeatherTools());
// 绑定工具到对话
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(weatherTools)
.build();
// 构造 Prompt 时指定对话选项
Prompt prompt = new Prompt("北京今天天气怎么样?", chatOptions);
chatModel.call(prompt);
工具生态
首先,工具的本质就是一种插件。能不自己写的插件,就尽量不要自己写。我们可以直接在网上找一些优秀的工具实现,比如 Spring AI Alibaba 官方文档 中提到了社区插件。在 GitHub 社区找到官方提供的更多 工具源码,包含大量有用的工具!比如翻译工具、网页搜索工具、爬虫工具、地图工具等。
三、主流工具开发
文件操作
文件操作工具主要提供 2 大功能:保存文件、读取文件。
由于会影响系统资源,所以我们需要将文件统一存放到一个隔离的目录进行存储,在 constant
包下新建文件常量类,约定文件保存目录为项目根目录下的 /tmp
目录中。
java
public interface FileConstant {
/**
* 文件保存目录
*/
String FILE_SAVE_DIR = System.getProperty("user.dir") + "/tmp";
}
建议同时将这个目录添加到 .gitignore
文件中,避免提交隐私信息。
编写文件操作工具类,通过注解式定义工具,代码如下:
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 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();
}
}
}
编写单元测试验证工具功能:
java
@SpringBootTest
public class FileOperationToolTest {
@Test
public void testReadFile() {
FileOperationTool tool = new FileOperationTool();
String fileName = "编程导航.txt";
String result = tool.readFile(fileName);
assertNotNull(result);
}
@Test
public void testWriteFile() {
FileOperationTool tool = new FileOperationTool();
String fileName = "编程导航.txt";
String content = "https://www.codefather.cn 程序员编程学习交流社区";
String result = tool.writeFile(fileName, content);
assertNotNull(result);
}
}
联网搜索
联网搜索工具的作用是根据关键词搜索网页列表。
我们可以使用专业的网页搜索 API,如 Search API 来实现从多个网站搜索内容,这类服务通常按量计费。当然也可以直接使用 Google 或 Bing 的搜索 API(甚至是通过爬虫和网页解析从某个搜索引擎获取内容)。
1)阅读 Search API 的 官方文档,重点关注 API 的请求参数和返回结果。从 API 返回的结果中,我们只需要提取关键部分:
json
{
"organic_results": [
...
{
"position": 1,
"title": "编程导航 - 程序员一站式编程学习交流社区,做您编程学习路...",
"link": "https://codefather.cn/",
"displayed_link": "codefather.cn/",
"snippet": "学编程,就来编程导航,程序员免费编程学习交流社区。Java,Python,前端,web网站开发,C语言,C++,Go,后端,SQL,数据库,PHP入门学习、技能提升、求职面试法宝。提升编程效率、优质IT技术文章、海...",
"snippet_highlighted_words": [
"编程",
"编程导航",
"程序员"
],
"thumbnail": "https://t8.baidu.com/it/u=661528516,2886240705&fm=217&app=126&size=f242,150&n=0&f=JPEG&fmt=auto?s=73B489634AD237E3660C19280200A063&sec=1744477200&t=b5d8762a6f5728d5f2fbc6bcf1774b20"
},
...
]
}
2)可以把接口文档喂给 AI,让它帮我们生成工具代码,网页搜索工具代码如下:
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();
}
}
}
3)我们需要获取 API Key 来调用网页搜索,注意不要泄露哦~

4)在配置文件中添加 API Key:
yaml
# searchApi
search-api:
api-key: 你的 API Key
5)编写单元测试代码,读取配置文件中的密钥来创建网页搜索工具:
java
@SpringBootTest
public class WebSearchToolTest {
@Value("${search-api.api-key}")
private String searchApiKey;
@Test
public void testSearchWeb() {
WebSearchTool tool = new WebSearchTool(searchApiKey);
String query = "程序员鱼皮编程导航 codefather.cn";
String result = tool.searchWeb(query);
assertNotNull(result);
}
}
运行效果如图,成功搜索到了网页:

在实际应用中,我们可以进一步过滤结果,只保留title、link 和 snippet 等关键信息就够了。
网页抓取
网页抓取工具的作用是根据网址解析到网页的内容。
1)可以使用 jsoup 库实现网页内容抓取和解析,首先给项目添加依赖:
xml
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.19.1</version>
</dependency>
2)编写网页抓取工具类,几行代码就搞定了:
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();
}
}
}
3)编写单元测试代码:
java
@SpringBootTest
public class WebScrapingToolTest {
@Test
public void testScrapeWebPage() {
WebScrapingTool tool = new WebScrapingTool();
String url = "https://www.codefather.cn";
String result = tool.scrapeWebPage(url);
assertNotNull(result);
}
}
执行效果如图,成功抓取到了网页内容:

终端操作
终端操作工具的作用是在终端执行命令,比如执行 python 命令来运行脚本。
1)可以通过 Java 的 Process API 实现终端命令执行,注意 Windows 和其他操作系统下的实现略有区别)。工具类代码如下:
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 {
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 操作系统,要使用下面这段代码,否则命令执行会报错:
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();
}
}
2)编写单元测试代码:
java
@SpringBootTest
public class TerminalOperationToolTest {
@Test
public void testExecuteTerminalCommand() {
TerminalOperationTool tool = new TerminalOperationTool();
String command = "ls -l";
String result = tool.executeTerminalCommand(command);
assertNotNull(result);
}
}
运行效果如图,成功执行了 ls
打印文件列表命令并获取到了输出结果:

资源下载
资源下载工具的作用是通过链接下载文件到本地。
1)使用 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();
}
}
}
2)编写单元测试代码:
java
@SpringBootTest
public class ResourceDownloadToolTest {
@Test
public void testDownloadResource() {
ResourceDownloadTool tool = new ResourceDownloadTool();
String url = "https://www.codefather.cn/logo.png";
String fileName = "logo.png";
String result = tool.downloadResource(url, fileName);
assertNotNull(result);
}
}
执行测试,可以在指定目录下看到下载的图片:

PDF 生成
PDF 生成工具的作用是根据文件名和内容生成 PDF 文档并保存。
可以使用 itext 库 实现 PDF 生成。需要注意的是,itext 对中文字体的支持需要额外配置,不同操作系统提供的字体也不同,如果真要做生产级应用,建议自行下载所需字体。
不过对于学习来说,不建议在这里浪费太多时间,可以使用内置中文字体(不引入 font-asian 字体依赖也可以使用):
java
PdfFont font = PdfFontFactory.createFont("STSongStd-Light", "UniGB-UCS2-H");
document.setFont(font);
1)给项目添加依赖:
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>
2)编写工具类实现代码:
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();
}
}
}
上述代码中,为了实现方便,我们是直接保存 PDF 到本地文件系统。此外,你还可以将生成的文件上传到对象存储服务,然后返回可访问的 URL 给 AI 去输出;或者将本地文件临时返回给前端,让用户直接访问。
3)编写单元测试代码:
java
@SpringBootTest
public class PDFGenerationToolTest {
@Test
public void testGeneratePDF() {
PDFGenerationTool tool = new PDFGenerationTool();
String fileName = "编程导航原创项目.pdf";
String content = "编程导航原创项目 https://www.codefather.cn";
String result = tool.generatePDF(fileName, content);
assertNotNull(result);
}
}
集中注册
开发好了这么多工具类后,结合我们自己的需求,可以给 AI 一次性提供所有工具,让它自己决定何时调用。所以我们可以创建 工具注册类,方便统一管理和绑定所有工具。
代码如下:
java
@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
);
}
}
💡 可别小瞧这段代码,其实它暗含了好几种设计模式:
- 工厂模式:allTools() 方法作为一个工厂方法,负责创建和配置多个工具实例,然后将它们包装成统一的数组返回。这符合工厂模式的核心思想 - 集中创建对象并隐藏创建细节。
- 依赖注入模式:通过
@Value
注解注入配置值,以及将创建好的工具通过 Spring 容器注入到需要它们的组件中。 - 注册模式:该类作为一个中央注册点,集中管理和注册所有可用的工具,使它们能够被系统其他部分统一访问。
- 适配器模式的应用:ToolCallbacks.from 方法可以看作是一种适配器,它将各种不同的工具类转换为统一的 ToolCallback 数组,使系统能够以一致的方式处理它们。
有了这个注册类,如果需要添加或移除工具,只需修改这一个类即可,更利于维护。
使用工具
在构造ChatClient时指定tools参数即可
java
@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;
}