【SpringAI】第五弹:基于 Spring AI ToolCallback Function 实现文件操作、联网搜索、网页抓取、终端操作、资源下载、PDF生成等程序的创建与调用、核心特性解析



本节重点
以 Spring AI 框架为例,学习 AI 应用开发的核心特性 —— 工具调用,大幅增强 AI 的能力,并实战主流工具的开发,熟悉工具的原理和高级特性。
具体内容包括:
- 工具调用介绍
- Spring AI 工具开发
- 主流工具开发
- 文件操作
- 联网搜索
- 网页抓取
- 终端操作
- 资源下载
- PDF 生成
- 工具进阶知识(原理和高级特性)
重点理解
- Tool Calling 的工作原理

- Spring AI 实现工具调用的流程;

- 使用
@Tool和@ToolParam注解标记类方法
一、需求分析
之前我们通过 RAG 技术让 AI 应用具备了根据外部知识库来获取信息并回答的能力,但是直到目前为止,AI 应用还只是个 “知识问答助手”。本节我们可以利用 工具调用 特性,实现更多需求。
1. 联网搜索
比如智能推荐约会地点,示例用户提问:
- 周末想带女朋友去上海约会,推荐几个适合情侣的小众打卡地?
- 女朋友生气了,有哪些温柔的哄人技巧?
2. 网页抓取
比如分析恋爱案例,示例用户提问:
- 最近和对象吵架了,看看恋爱帖子上的其他情侣是怎么解决矛盾的?
3. 资源下载
比如恋爱相关的图片 / 音视频下载,示例用户提问:
- 下载一张适合做手机壁纸的星空情侣图片
- 推荐并下载几首适合约会时播放的钢琴曲
4. 终端操作
比如执行代码来生成恋爱报告,示例用户提问:
- 执行 Python 脚本来生成数据分析报告
5. 文件操作
比如保存用户恋爱档案,示例用户提问:
- 帮我保存我的恋爱档案为文件
6. PDF 生成
比如恋爱计划、情感分析报告 PDF 生成,示例用户提问:
- 生成一份《七夕约会计划》PDF,包含餐厅预订、活动流程和礼物清单
- 分析我和对象近一个月的聊天记录,生成情感报告

而且这些需求还可以进行组合,比如用户先让 AI 联网搜索约会地点、再下载约会地点的图片、最后将获取到的内容组合生成 PDF、并保存到本地,一条龙服务。
如果 AI 能够完成上述需求,就不再只是一个有知识的 “大脑”,而是有手有脚,会利用工具完成任务的 “智能体” 了。
下面我们就来学习下实现上述需求的关键 —— 工具调用 技术。
二、工具调用介绍
什么是工具调用?
工具调用(Tool Calling)可以理解为让 AI 大模型 借用外部工具 来完成它自己做不到的事情。
跟人类一样,如果只凭手脚完成不了工作,那么就可以利用工具箱来完成。
工具可以是任何东西,比如网页搜索、对外部 API 的调用、访问外部数据、或执行特定的代码等。
比如用户提问 “帮我查询上海最新的天气”,AI 本身并没有这些知识,它就可以调用 “查询天气工具”,来完成任务。
目前工具调用技术发展的已经比较成熟了,几乎所有主流的、新出的 AI 大模型和 AI 应用开发平台都支持工具调用。
工具调用的工作原理
其实,工具调用的工作原理非常简单,并不是 AI 服务器自己调用这些工具、也不是把工具的代码发送给 AI 服务器让它执行,它只能提出要求,表示 “我需要执行 XX 工具完成任务”。而真正执行工具的是我们自己的应用程序,执行后再把结果告诉 AI,让它继续工作。
举个例子,假如用户提问 “编程导航网站有哪些热门文章?”,就需要经历下列流程:
- 用户提出问题:“编程导航网站有哪些热门文章?”
- 程序将问题传递给大模型
- 大模型分析问题,判断需要使用工具(网页抓取工具)来获取信息
- 大模型输出工具名称和参数(网页抓取工具,URL参数为 codefather.cn)
- 程序接收工具调用请求,执行网页抓取操作
- 工具执行抓取并返回文章数据
- 程序将抓取结果传回给大模型
- 大模型分析网页内容,生成关于编程导航热门文章的回答
- 程序将大模型的回答返回给用户
虽然看起来是 AI 在调用工具,但实际上整个过程是 由我们的应用程序控制的。AI 只负责决定什么时候需要用工具,以及需要传递什么参数,真正执行工具的是我们的程序。
你可能会好奇,为啥要这么设计呢?这样不是要让程序请求 AI 多次么?为啥不让 AI 服务器直接调用工具程序?
有这个想法很正常,但如果让你自己设计一个 AI 大模型服务,你就能理解了。很关键的一点是 安全性,AI 模型永远无法直接接触你的 API 或系统资源,所有操作都必须通过你的程序来执行,这样你可以完全控制 AI 能做什么、不能做什么。
举个例子,你有一个爆破工具,用户像 AI 提了需求 ”我要拆这栋房子“,虽然 AI 表示可以用爆破工具,但是需要经过你的同意,才能执行爆破。反之,如果把爆破工具植入给 AI,AI 觉得自己能炸了,就炸了,不需要再问你的意见。而且这样也给 AI 服务器本身增加了压力。
工具调用和功能调用
大家可能看到过 Function Calling(功能调用)这个概念,别担心,其实它和 Tool Calling(工具调用)完全是同一概念!只是不同平台或每个人习惯的叫法不同而已。
Spring AI 工具调用文档 的开头就说明了这一点:

煮波更喜欢 “工具调用” 这个说法,因为 Function 这个词更像是计算机行业的术语,不如工具更形象易懂、更具普适性。
工具调用的技术选型
我们先来梳理一下工具调用的流程:
- 工具定义:程序告诉 AI “你可以使用这些工具”,并描述每个工具的功能和所需参数
- 工具选择:AI 在对话中判断需要使用某个工具,并准备好相应的参数
- 返回意图:AI 返回 “我想用 XX 工具,参数是 XXX” 的信息
- 工具执行:我们的程序接收请求,执行相应的工具操作
- 结果返回:程序将工具执行的结果发回给 AI
- 继续对话:AI 根据工具返回的结果,生成最终回答给用户
通过上述流程,我们会发现,程序需要和 AI 多次进行交互、还要能够执行对应的工具,怎么实现这些呢?我们当然可以自主开发,不过还是更推荐使用 Spring AI、LangChain 等开发框架。此外,有些 AI 大模型服务商也提供了对应的 SDK,都能够简化代码编写。
本教程后续部分将以 Spring AI 为例,带大家实战工具调用开发。
需要注意的是,不是所有大模型都支持工具调用。有些基础模型或早期版本可能不支持这个能力。可以在 Spring AI 官方文档 中查看各模型支持情况。

三、Spring AI 工具开发
首先我们通过 Spring AI 官方 提供的图片来理解 Spring AI 在实现工具调用时都帮我们做了哪些事情?

- 工具定义与注册:Spring AI 可以通过简洁的注解
自动生成工具定义和 JSON Schema,让 Java 方法轻松转变为 AI 可调用的工具。 - 工具调用请求:Spring AI 自动处理
与 AI 模型的通信并解析工具调用请求,并且支持多个工具链式调用。 - 工具执行:Spring AI 提供
统一的工具管理接口,自动根据 AI 返回的工具调用请求,找到对应的工具并解析参数进行调用,让开发者专注于业务逻辑实现。 - 处理工具结果:Spring AI 内置
结果转换和异常处理机制,支持各种复杂 Java 对象作为返回值并优雅处理错误情况。 - 返回结果给模型:Spring AI
封装响应结果并管理上下文,确保工具执行结果正确传递给模型或直接返回给用户。 - 生成最终响应:Spring AI
自动整合工具调用结果到对话上下文,支持多轮复杂交互,确保 AI 回复的连贯性和准确性。
下面是一个较早版本的流程图,也能帮助我们理解这个过程:

定义工具
工具定义模式
在 Spring AI 中,定义工具主要有两种模式:基于 Methods 方法或者 Functions 函数式编程。
记结论就行了,我们只用学习 基于 Methods 方法 来定义工具,另外一种了解即可。原因是 Methods 方式更容易编写、更容易理解、支持的参数和返回类型更多。
二者的详细对比:
| 特性 | Methods 方式 | Functions 方式 |
|---|---|---|
| 定义方式 | 使用 @Tool和 @ToolParam注解标记类方法 | 使用函数式接口并通过 Spring Bean 定义 |
| 语法复杂度 | 简单,直观 | 较复杂,需要定义请求/响应对象 |
| 支持的参数类型 | 大多数 Java 类型,包括基本类型、POJO、集合等 | 不支持基本类型、Optional、集合类型 |
| 支持的返回类型 | 几乎所有可序列化类型,包括 void | 不支持基本类型、Optional、集合类型等 |
| 使用场景 | 适合大多数新项目开发 | 适合与现有函数式API集成 |
| 注册方式 | 支持按需注册和全局注册 | 通常在配置类中预先定义 |
| 类型转换 | 自动处理 | 需要更多手动配置 |
| 文档支持 | 通过注解提供描述 | 通过Bean描述和JSON属性注解 |
举个例子来对比这两种定义模式:
1. Methods 模式:通过 @Tool 注解定义工具,通过 tools 方法绑定工具
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();
2. Functions 模式:通过 @Bean 注解定义工具,通过 functions 方法绑定工具
@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();
显然 Methods 模式的开发量更少(我估计很多同学都没写过 Function 函数式编程),更推荐这种方式,所以下面重点讲解这种方式。
定义工具
Spring AI 提供了两种定义工具的方法 —— 注解式 和 编程式。
1. 注解式
只需使用 @Tool 注解标记普通 Java 方法,就可以定义工具了,简单直观。
每个工具最好都添加详细清晰的描述,帮助 AI 理解何时应该调用这个工具。对于工具方法的参数,可以使用 @ToolParam 注解提供额外的描述信息和是否必填。
示例代码:
class WeatherTools {
@Tool(description = "获取指定城市的当前天气情况")
String getWeather(@ToolParam(description = "城市名称") String city) {
// 获取天气的实现逻辑
return "北京今天晴朗,气温25°C";
}
}
2. 编程式
如果想在运行时动态创建工具,可以选择编程式来定义工具,更灵活。
先定义工具类:
class WeatherTools {
String getWeather(String city) {
// 获取天气的实现逻辑
return "北京今天晴朗,气温25°C";
}
}
然后将工具类转换为 ToolCallback 工具定义类,之后就可以把这个类绑定给 ChatClient,从而让 AI 使用工具了。
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();
其实你会发现,编程式就是把注解式的那些参数,改成通过调用方法来设置了而已。
在定义工具时,需要注意方法参数和返回值类型的选择。Spring AI 支持大多数常见的 Java 类型作为参数和返回值,包括基本类型、复杂对象、集合等。而且返回值需要是可序列化的,因为它将被发送给 AI 大模型。
以下类型目前不支持作为工具方法的参数或返回类型:
- Optional
- 异步类型(如 CompletableFuture, Future)
- 响应式类型(如 Flow, Mono, Flux)
- 函数式类型(如 Function, Supplier, Consumer)
使用工具
定义好工具后,Spring AI 提供了多种灵活的方式将工具提供给 ChatClient,让 AI 能够在需要时调用这些工具。
1. 按需使用:这是最简单的方式,直接在构建 ChatClient 请求时通过 tools() 方法附加工具。这种方式适合只在特定对话中使用某些工具的场景。
String response = ChatClient.create(chatModel)
.prompt("北京今天天气怎么样?")
.tools(new WeatherTools()) // 在这次对话中提供天气工具
.call()
.content();
2. 全局使用:如果某些工具需要在所有对话中都可用,可以在构建 ChatClient 时注册默认工具。这样,这些工具将对从同一个 ChatClient 发起的所有对话可用。
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new WeatherTools(), new TimeTools()) // 注册默认工具
.build();
3. 更底层的使用方式:除了给 ChatClient 绑定工具外,也可以给更底层的 ChatModel 绑定工具(毕竟工具调用是 AI 大模型支持的能力),适合需要更精细控制的场景。
// 先得到工具对象
ToolCallback[] weatherTools = ToolCallbacks.from(new WeatherTools());
// 绑定工具到对话
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(weatherTools)
.build();
// 构造 Prompt 时指定对话选项
Prompt prompt = new Prompt("北京今天天气怎么样?", chatOptions);
chatModel.call(prompt);
4. 动态解析:一般情况下,使用前面 3 种方式即可。对于更复杂的应用,Spring AI 还支持通过 ToolCallbackResolver 在运行时动态解析工具。这种方式特别适合工具需要根据上下文动态确定的场景,比如从数据库中根据工具名搜索要调用的工具。在本节的工具进阶知识中会讲到,先了解到有这种方式即可。
总结一下,在使用工具时,Spring AI 会自动处理工具调用的全过程:
- 从 AI 模型决定调用工具 =>
- 到执行工具方法 =>
- 再到将结果返回给模型 =>
- 最后模型基于工具结果生成最终回答。
这整个过程对开发者来说是透明的,我们只需专注于 实现工具 的业务逻辑即可。
那么,怎么实现工具呢?
工具生态
首先,工具的本质就是一种插件。能不自己写的插件,就尽量不要自己写。我们可以直接在网上找一些优秀的工具实现,比如 Spring AI Alibaba 官方文档 中提到了社区插件。
虽然文档里只提到了屈指可数的插件数,但我们可以顺藤摸瓜,在 GitHub 社区找到官方提供的更多 工具源码,包含大量有用的工具!比如翻译工具、网页搜索工具、爬虫工具、地图工具等:

这种搜集资源的能力,希望大家也能够掌握,尤其是学新技术的时候,即使官方文档写的不够清晰完善,我们也可以从开源社区中获取到一手信息。
四、主流工具开发
如果社区中没找到合适的工具,我们就要自主开发。需要注意的是,AI 自身能够实现的功能通常没必要定义为额外的工具,因为这会增加一次额外的交互,我们应该将工具用于 AI 无法直接完成的任务。
下面我们依次来实现需求分析中提到的 6 大工具,开发过程中我们要 格外注意工具描述的定义,因为它会影响 AI 决定是否使用工具。
先在项目根包下新建 tools 包,将所有工具类放在该包下;并且工具的返回值尽量使用 String 类型,让结果的含义更加明确。

文件操作
文件操作工具主要提供 2 大功能:保存文件、读取文件。
由于会影响系统资源,所以我们需要将文件统一存放到一个隔离的目录进行存储:

在 constant 包下新建文件常量类,约定文件保存目录为项目根目录下的 /tmp 目录中。
/**
* 文件常量
*/
public interface FileConstant {
/**
* 文件保存目录
*/
String FILE_SAVE_DIR = System.getProperty("user.dir") + "/tmp";
}
建议同时将这个目录添加到 .gitignore 文件中,避免提交隐私信息。

编写文件操作工具类,通过注解式定义工具,代码如下:
- 可以将文件读、写操作的具体实现直接交给 AI 生成:我们只需把 Spring AI 官方文档中的示例代码以及拟调用的 API 接口告诉 AI 即可。
- 为了简化框架处理并提升性能,建议将这两个方法的返回值都声明为 String。因为一旦被 Spring AI 注册为“工具”,其执行结果会作为上下文回传给大模型;String 类型无需额外序列化转换,既减少了失败风险,也让我们能够精确控制回传给模型的内容。

/**
* 文件操作工具类 (提供文件读写功能)
*/
public <

浙公网安备 33010602011771号