1. 工具调用是什么
1.1 一句话定义
Tool Calling(工具调用) 是大语言模型(LLM)借用外部工具来完成它自己做不到的事情------LLM 在生成回复的过程中,自主决定调用你注册的工具/函数,获取外部数据或执行操作,然后将结果纳入上下文继续生成。 比如用户提问"帮我查询北京最新的天气",AI 本身并没有这些知识,它就可以调用"查询天气工具"来完成任务。
1.2 为什么需要它
| 局限 | 解决方案 |
|---|---|
| 训练数据截止于某个时间点 | 工具可以查询实时数据(天气、股价、数据库) |
| 无法访问内部系统 | 工具可以查订单、查库存、发邮件 |
| 无法执行精确计算 | 工具可以调用数学库、业务规则引擎 |
| 没有"行动"能力 | 工具可以创建工单、下单、发送通知 |
1.3 与 Function Calling 的关系
业界统称 Function Calling (OpenAI 术语),Spring AI 将其抽象为 Tool Calling。两者本质相同,Spring AI 的命名更通用------因为它不只支持 OpenAI,还支持 Claude(工具调用)、Gemini(函数声明)等各种模型。
2. 核心原理:LLM ↔ 你的代码
2.1 完整调用序列
scss
┌─────────────────────────────────────────────────────────────────┐
│ Application │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ ChatClient │────▶│ Adapter │────▶│ LLM Provider │ │
│ │ (M6 引入) │ │ (Model Spec) │ │ (OpenAI/etc.) │ │
│ └──────┬───────┘ └──────────────┘ └────────┬─────────┘ │
│ │ │ │
│ │ ① 注册工具描述 ←─┤ │
│ │ │ │
│ │ ② 用户提问 "北京的天气怎么样?" │ │
│ │─────────────────────────────────────────────▶ │
│ │ │ │
│ │ ③ 返回: tool_call (getWeather) │ │
│ │◀───────────────────────────────────────────── │
│ │ │ │
│ │ ④ 解析 ToolExecutionRequest │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ⑤ 执行 getWeather(城市="北京") │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ⑥ 返回结果: {温度: 28, 天气: 晴} │ │
│ │ │ │
│ │ ⑦ 将工具结果 + 原始对话发回 LLM │ │
│ │─────────────────────────────────────────────▶ │
│ │ │ │
│ │ ⑧ 最终回复: "北京今天 28°C,天气晴朗" │ │
│ │◀───────────────────────────────────────────── │
└─────────────────────────────────────────────────────────────────┘
2.2 分步详解
| 步骤 | 发生了什么 | M6 中的关键对象 |
|---|---|---|
| ① | 应用注册工具。Spring AI 将 @Tool 方法或 ToolCallback 解析为 JSON Schema(函数名、描述、参数结构) |
ToolCallback, ToolCallbackProvider |
| ② | 用户提问,连同工具 Schema 发给 LLM。模型"看到"可用工具 | ChatClient, Prompt |
| ③ | LLM 判断需要调用工具,返回一个工具调用请求(工具名称 + JSON 参数) | ToolExecutionRequest |
| ④ | Spring AI 解析响应,匹配 ToolCallback 准备执行 |
ChatClient, ToolCallback |
| ⑤ | 执行真正的业务逻辑 | 你的 @Tool 方法 |
| ⑥ | 工具结果封装 | ToolExecutionResult |
| ⑦ | 结果注入对话上下文,再次发 LLM | ChatClient |
| ⑧ | LLM 生成最终回复 | 无 |
2.3 大白话理解
- 工具定义:程序告诉 AI 大模型"你可以使用这些工具",并描述每个工具的功能和所需参数
- 工具选择:AI 大模型在对话中判断需要使用某个工具,并准备好相应的参数
- 返回意图:AI 大模型返回"我想用 XX 工具,参数是 XXX"的信息
- 工具执行:我们的程序接收请求,执行相应的工具操作
- 结果返回:程序将工具执行的结果发回给 AI 大模型
- 继续对话:AI 大模型根据工具返回的结果,生成最终回答给用户
2.4 关键洞察
工具调用是一个循环,不是一次往返。 LLM 可能在一次交互中多次调用工具------甚至一个工具的输出作为另一个工具的输入(工具链)。Spring AI 从 M6 开始通过
ChatClient内置的自动循环替你管理这个过程。
虽然看起来是 AI 在调用工具,但实际上整个过程是由我们的应用程序控制的 。AI 只负责决定什么时候需要用工具,以及需要传递什么参数,真正执行工具的是我们的程序。安全性是这种设计的核心考量------AI 模型永远无法直接接触你的 API 或系统资源,所有操作都必须通过你的程序来执行。
3.Spring AI 工具开发
在 Spring AI 中,定义工具主要有两种模式:基于 Methods 方法或者 Functions 函数式编程。
二者的详细对比:
| 特性 | Methods 方式 | Functions 方式 |
|---|---|---|
| 定义方式 | 使用 @Tool和 @ToolParam注解标记类方法 | 使用函数式接口并通过 Spring Bean 定义 |
| 语法复杂度 | 简单,直观 | 较复杂,需要定义请求/响应对象 |
| 支持的参数类型 | 大多数 Java 类型,包括基本类型、POJO、集合等 | 不支持基本类型、Optional、集合类型 |
| 支持的返回类型 | 几乎所有可序列化类型,包括 void | 不支持基本类型、Optional、集合类型等 |
| 使用场景 | 适合大多数新项目开发 | 适合与现有函数式API集成 |
| 注册方式 | 支持按需注册和全局注册 | 通常在配置类中预先定义 |
| 类型转换 | 自动处理 | 需要更多手动配置 |
| 文档支持 | 通过注解提供描述 | 通过Bean描述和JSON属性注解 |
举个例子来对比这两种定义模式:
1)Methods 模式:通过 @Tool 注解定义工具,通过 tools 方法绑定工具
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();
2)Functions 模式:通过 @Bean 注解定义工具,通过 functions 方法绑定工具
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();
3.1工具定义
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();
其实你会发现,编程式就是把注解式的那些参数,改成通过调用方法来设置了而已。
@Tool 注解核心属性:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
name |
String | 方法名(蛇形) | 工具名称,LLM 通过此名称调用 |
description |
String | 空 | 工具描述,LLM 据此判断何时调用 |
returnDirect |
boolean | false | 若为 true,工具结果直接作为最终回复返回给用户,不再经过 LLM 加工 |
@ToolParam 注解核心属性
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
required |
boolean | false | 是否必填(方法参数默认必填) |
description |
String | "" | 参数描述 |
deprecated |
boolean | false | 标记为已弃用 |
3.2使用工具
定义好工具后,Spring AI 提供了多种灵活的方式将工具提供给 ChatClient,让 AI 能够在需要时调用这些工具。
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);
4)动态解析:一般情况下,使用前面 3 种方式即可。对于更复杂的应用,Spring AI 还支持通过 ToolCallbackResolver 在运行时动态解析工具。这种方式特别适合工具需要根据上下文动态确定的场景,比如从数据库中根据工具名搜索要调用的工具。在本节的工具进阶知识中会讲到,先了解到有这种方式即可。
总结一下,在使用工具时,Spring AI 会自动处理工具调用的全过程:从 AI 模型决定调用工具 => 到执行工具方法 => 再到将结果返回给模型 => 最后模型基于工具结果生成最终回答。这整个过程对开发者来说是透明的,我们只需专注于 实现工具 的业务逻辑即可。
3.3工具生态
首先,工具的本质就是一种插件。能不自己写的插件,就尽量不要自己写。我们可以直接在网上找一些优秀的工具实现,比如 Spring AI Alibaba 官方文档 中提到了社区插件。
虽然文档里只提到了屈指可数的插件数,但我们可以顺藤摸瓜,在 GitHub 社区找到官方提供的更多 工具源码,包含大量有用的工具!比如翻译工具、网页搜索工具、爬虫工具、地图工具等。
4. 核心接口深度剖析
4.1 ToolCallback------工具定义的核心契约
java
// Spring AI 1.0.x
public interface ToolCallback {
/** 工具名称,LLM 通过此名称引用工具 */
String getName();
/** 工具描述,LLM 理解工具用途的关键文本(越清晰越好) */
String getDescription();
/** 工具的输入 JSON Schema(OpenAPI 格式) */
JsonSchema getInputSchema();
/** 执行工具,参数为 JSON 字符串,返回 JSON 字符串 */
String call(String functionInput);
// --- 1.0+ 新增 ---
/** 获取工具的执行结果转换器(可选) */
default ToolResponseConverter getResponseConverter() { return null; }
/** 工具元数据(名称、描述、Schema 的封装) */
default ToolMetadata getMetadata() {
return new ToolMetadata(getName(), getDescription(), getInputSchema());
}
}
4.2 ToolExecutionRequest------LLM 的调用指令
java
public class ToolExecutionRequest {
/** LLM 想要调用的工具名称(必须匹配 ToolCallback.getName()) */
private final String name;
/** 工具参数,JSON 格式的字符串 */
private final String arguments;
/** 工具调用 ID(用于多轮对话追踪,由模型提供) */
private final String id;
// --- 1.0+ 增强 ---
/** 内部调用追踪 ID */
private final String toolCallId;
/** 父工具调用 ID(支持嵌套调用链追踪) */
private final String parentToolCallId;
}
4.3 ToolExecutionResult------工具执行结果
java
public class ToolExecutionResult {
/** 原始请求 */
private final ToolExecutionRequest request;
/** 执行输出(JSON 字符串) */
private final String output;
/** 执行状态 */
private final ToolExecutionStatus status;
/** 执行耗时(毫秒) */
private final long duration;
/** 异常信息(执行失败时) */
private final Throwable error;
/** ------ 1.0+ 新增 ------ */
/** 是否直接返回(对应 @Tool(returnDirect=true)) */
private final boolean isDirect;
/** 元数据(可附加审计信息) */
private final Map<String, Object> metadata;
}
public enum ToolExecutionStatus {
SUCCESS,
FAILURE,
RETRY_PENDING
}
4.4 ToolCallbackProvider------工具注册中心
java
@FunctionalInterface
public interface ToolCallbackProvider {
/** 返回当前可用的所有工具回调 */
Collection<ToolCallback> getToolCallbacks();
/** ------ 1.0+ 新增 ------ */
/** 根据工具名称获取特定回调 */
default ToolCallback getToolCallback(String name) {
return getToolCallbacks().stream()
.filter(tc -> tc.getName().equals(name))
.findFirst()
.orElse(null);
}
/** 动态更新可用工具(运行时增减) */
default void refresh() { }
}
4.5 ToolContext------工具执行上下文(1.0+)
java
public class ToolContext {
/** 用户认证信息 */
private final Authentication authentication;
/** 会话 ID */
private final String sessionId;
/** 请求 ID */
private final String requestId;
/** 自定义属性 */
private final Map<String, Object> attributes;
}
4.6 ChatClient 中的工具相关 API
java
// ===== 构建阶段(ChatClient.Builder) =====
// 注册默认工具(对所有对话生效)
ChatClient.Builder.defaultTools(Object... toolObjects);
// 注册带上下文的默认工具
ChatClient.Builder.defaultTools(ToolCallback... toolCallbacks);
// 设置工具执行回调(审计钩子)
ChatClient.Builder.defaultToolExecutionCallback(ToolExecutionCallback callback);
// ===== 请求阶段(ChatClient.RequestSpec) =====
// 为本次请求指定工具(覆盖默认)
RequestSpec.tools(Object... toolObjects);
// 为本次请求指定工具(ToolCallback 方式)
RequestSpec.tools(ToolCallback... toolCallbacks);
// 设置本次请求的工具执行回调
RequestSpec.toolExecutionCallback(ToolExecutionCallback callback);
// 设置工具调用时的行为参数
RequestSpec.toolParameters(ToolParameters parameters);
4.7 ToolExecutionCallback------审计钩子
java
public interface ToolExecutionCallback {
/** 工具执行前触发 */
default void onToolStart(ToolExecutionRequest request, ToolContext context) {
}
/** 工具执行成功时触发 */
default void onToolSuccess(ToolExecutionRequest request,
ToolExecutionResult result,
ToolContext context) {
}
/** 工具执行失败时触发 */
default void onToolError(ToolExecutionRequest request,
Throwable error,
ToolContext context) {
}
/** 工具执行完成(无论成功/失败,最终都会调用) */
default void onToolCompletion(ToolExecutionRequest request,
ToolExecutionResult result,
ToolContext context) {
}
}
4.8 架构关系图
sql
┌──────────────────────────────────────────────────────────────────────┐
│ ToolCallbackProvider │
│ (注册中心) │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ │
│ │ @Tool Bean │ │ ToolCallback Impl │ │ MethodToolCallback│ │
│ │ (声明式) │ │ (编程式) │ │ (手动构建) │ │
│ └────────┬─────────┘ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │ │
│ └──────────┬───────────┴───────────┬──────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌──────────────────────────────────────────┐ │
│ │ ToolCallback │ │
│ │ getName() / getDescription() / call() │ │
│ └──────────────────┬───────────────────────┘ │
│ │ │
└─────────────────────────────────┼─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────┐
│ ChatClient │
│ │
│ ① 构建 Prompt(含 Tool Schema) │
│ ② 向 LLM 发送请求 │
│ ③ 解析响应 → ToolExecutionRequest[] │
│ ④ 匹配 ToolCallback → call() │
│ ⑤ 触发 ToolExecutionCallback(审计) │
│ ⑥ 将结果注入 Conversation │
│ ⑦ 循环直到 LLM 返回纯文本或达到限制 │
└──────────────────────────────────────────────────────────────────────┘