跳到主要内容
Spring AI Tool 机制详解:让大模型调用外部工具 | 极客日志
Java AI java
Spring AI Tool 机制详解:让大模型调用外部工具 基于 Spring AI 1.1.0,深入讲解 Tool Calling(工具调用)机制。内容包括工具定义 (@Tool)、参数描述方式对比 (@JsonPropertyDescription vs @ToolParam)、自动执行与手动控制两种模式的实现与选择、异常处理策略以及完整代码示例。旨在帮助开发者掌握如何让大模型感知并调用外部工具,构建智能 AI Agent。
橘子海 发布于 2026/3/24 更新于 2026/5/12 5.6K 浏览一、引言:为什么需要 Tool Calling?
在与大模型交互的过程中,我们经常面临这样的场景:
用户问: '帮我查一下现在几点了?'
传统方案的问题: 大模型没有能力直接查询系统时间,只能根据训练数据回答,极容易出错(尤其是关于实时数据、当前时间、最新新闻等)。
Tool Calling 的优雅解决方案: 大模型识别到这个问题需要调用'获取当前时间'工具,主动告诉应用程序'我需要调用这个工具',应用程序执行工具,将结果返回给大模型,大模型基于实际数据进行回答。
这正是 Tool Calling(工具调用) 机制的核心价值:让大模型能够感知外部工具的存在,并在需要时自主选择调用,就像人类一样根据实际情况使用工具来完成任务。
Tool Calling 是构建真正智能 AI Agent 的基础。一个没有工具的大模型,就像一个被禁闭在房间里的人——再聪明也做不了实际的事情。而掌握 Tool Calling,就打开了让 AI 与外部世界交互的大门。
二、Tool Calling 的核心概念
2.1 Tool Calling 的执行流程
Tool Calling 遵循一个明确的循环流程:
┌─────────────────────────────────────────────────────────────┐
│ 1. 用户输入问题 │
│ "帮我设置一个明天上午 10 点的闹钟" │
└─────────────────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 2. 大模型分析问题 + 可用工具列表 │
│ 发现需要调用 setAlarm() 工具 │
└─────────────────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 3. 大模型输出 ToolCall │
│ 工具名称:setAlarm │
│ 参数:{time :"2025 年 3 月 31 日" , address:"卧室" } │
└─────────────────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 4. 应用程序执行工具 │
│ 调用 setAlarm(AlarmRequest) 方法 │
└─────────────────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 5. 工具返回结果 │
│ "闹钟已设置,明天上午 10 点在卧室提醒" │
└─────────────────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 6. 结果反馈给大模型 │
│ 大模型合成最终回答:"已为您设置好了..." │
└─────────────────────────────────┬───────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ 7. 用户收到最终答案 │
└─────────────────────────────────────────────────────────────┘
这个循环可能执行多次。如果大模型在看到工具结果后,认为需要调用另一个工具来进一步优化答案,就会继续发起新的 Tool Call,形成一个完整的 Agent 执行流程。
2.2 Tool Calling 的三大关键元素 一个完整的 Tool Calling 系统包含三个关键要素:
要素 作用 体现形式 工具定义 告诉大模型有哪些工具可用,每个工具做什么 @Tool 注解标记的方法 参数描述 告诉大模型每个工具需要什么参数,参数的含义和约束 @JsonPropertyDescription、@ToolParam 执行与反馈 当大模型决定调用工具时,执行真实的业务逻辑,将结果返回 ToolCallingManager、异常处理
其中,参数描述的质量直接决定了大模型能否正确调用工具 。如果参数描述不清楚,大模型可能会传递错误的参数值,导致工具执行失败。
三、工具定义:@Tool 注解的使用
3.1 基础工具定义 在 Spring AI 中,使用 @Tool 注解来标记一个可被大模型调用的方法:
@Component
public class ArtisanTools {
@Tool(description = "获取当前时间")
LocalDateTime getCurrentDateTime () {
System.out.println("获取当前时间" );
return LocalDateTime.now();
}
@Tool(description = "用指定时间设置闹钟")
void setAlarm (AlarmRequest alarmRequest) {
System.out.println("地址:" + alarmRequest.getAddress());
System.out.println("闹钟时间为:" + alarmRequest.getTime());
}
}
@Component 注解: 必须将工具类注册为 Spring Bean,这样 Spring AI 才能发现和管理它。
@Tool 注解: 标记哪些方法是可被大模型调用的工具。
description 参数: 这是非常关键的!它用自然语言描述工具的功能。大模型会根据这个描述来决定是否调用该工具。
3.2 Description 的重要性 让我们通过对比来理解 description 为什么关键:
@Tool(description = "工具")
@Tool(description = "调用方法")
@Tool(description = "获取当前系统时间,返回格式为 LocalDateTime")
@Tool(description = "根据指定的日期和地点设置闹钟,闹钟会在指定时间触发提醒")
当你编写 description 时,想象你在向一个助手说话:
说清楚 做什么 :获取时间、设置闹钟、查询数据库
说清楚 返回什么 :时间对象、设置成功、查询结果
如果有特殊说明,加上去:格式要求、范围限制、副作用
@Tool(description = "获取指定日期的天气预报。返回温度、风力、降雨概率等信息。" +
"如果日期超过 7 天,只能返回预测数据,准确度降低。")
WeatherInfo getWeather (String date) { }
四、参数描述:两种方式的区别与选择 工具方法可能需要参数,问题是:如何清楚地告诉大模型这些参数是什么含义?
Spring AI 提供了两种参数描述的方式,它们各有特点:
4.1 方式一:@JsonPropertyDescription(推荐) public class AlarmRequest {
@JsonPropertyDescription("用中文中的年月日的格式,比如 2025 年 3 月 31 日")
private String time;
@JsonPropertyDescription("闹钟要设置的位置,比如卧室、客厅")
private String address;
}
Jackson 标准注解: @JsonPropertyDescription 是 Jackson 库提供的标准注解,用于 JSON Schema 描述。
更灵活的描述方式: 可以包含格式示例、取值范围、特殊说明等详细信息。
直观易读: 描述直接关联到字段定义。
最佳实践选择: 在生产环境中广泛使用。
复杂的参数类,有多个字段需要描述
需要提供详细的格式示例
字段有特殊的输入格式要求
4.2 方式二:@ToolParam(备选方案) public class AlarmRequest {
private String time;
@ToolParam(description = "地址", required = false)
private String address;
}
Spring AI 原生注解: 专为 Spring AI Tool Calling 而设计。
支持 required 属性: 可以标记字段是否必需。
针对性强: 完全针对 Tool Calling 场景。
public class AlarmRequest {
@JsonPropertyDescription("用中文中的年月日的格式,比如 2025 年 3 月 31 日")
private String time;
@ToolParam(description = "地址", required = false)
private String address;
}
4.3 两种方式的对比与最佳实践 对比维度 @JsonPropertyDescription @ToolParam 字段格式示例 ✅ 支持,在描述中体现 ❌ 不支持 必需/可选 ❌ 无法标记 ✅ 通过 required 属性 复杂对象嵌套 ✅ 支持 ⚠️ 有限支持 代码可维护性 ✅ 高(与 JSON 序列化一致) ✅ 高(明确的 Tool 语义) 可读性 ✅ 优秀 ✅ 优秀
public class AlarmRequest {
@JsonPropertyDescription("用中文中的年月日的格式,比如 2025 年 3 月 31 日。例如:2025 年 3 月 31 日")
private String time;
@JsonPropertyDescription("闹钟要设置的位置,比如卧室、客厅、办公室等")
@ToolParam(required = false)
private String address;
public String getTime () { return time; }
public void setTime (String time) { this .time = time; }
public String getAddress () { return address; }
public void setAddress (String address) { this .address = address; }
}
"时间" // 太简洁,大模型不知道格式
"address" // 中英混用,可读性差
"参数 1、参数 2、参数 3" // 没有实际信息
"设置闹钟的时间,格式为中文年月日,例如:2025 年 3 月 31 日"
"闹钟要设置的位置,可选值包括:卧室、客厅、办公室、车内等"
"查询的日期范围,格式为 YYYY-MM-DD,最多查询未来 30 天的数据"
五、自动执行模式:让大模型自主调用工具 Spring AI 提供了最简洁的工具调用方式:自动执行模式 。在这个模式下,框架自动处理整个 Tool Calling 流程。
5.1 自动执行模式的实现 @RestController
public class ToolController {
@Autowired
private ChatClient chatClient;
@Autowired
private ArtisanTools artisanTools;
@GetMapping("/tool")
public String tool (String question) {
return chatClient
.prompt()
.user(question)
.tools(ArtisanTools)
.call()
.content();
}
}
chatClient.prompt()
.user(question)
.tools(ArtisanTools)
.call()
.content()
5.2 自动执行模式的工作原理 用户输入: "帮我设置一个明天上午 10 点的闹钟"
┌─ 第 1 轮 ─────────────────────────────────────────┐
│ ChatClient 构造 Prompt: │
│ - System : (系统提示词) │
│ - User : "帮我设置一个明天上午 10 点的闹钟" │
│ - Tools: [getCurrentDateTime, setAlarm] │
│ │
│ 发送给大模型,大模型返回: │
│ {
│ "toolCalls": [{ │
│ "toolName": "setAlarm", │
│ "arguments": { │
│ "time": "2025 年 3 月 31 日", │
│ "address": null │
│ } │
│ }] │
│ }
│ ✓ 有 Tool Call ,继续执行 │
└────────────────────────────────────────────────┘
┌─ 第 2 轮:框架自动执行工具 ──────────────────────┐
│ 框架识别到 Tool Call ,执行: │
│ ArtisanTools.setAlarm( │
│ AlarmRequest{time : "2025 年 3 月 31 日", ...} │
│ ) │
│ │
│ 工具执行成功,返回结果 │
│ 框架将结果加入 Prompt,再次调用大模型 │
│ │
│ 大模型返回: │
│ {
│ "text": "已为您设置好了明天上午 10 点...", │
│ "toolCalls": [] │
│ }
│ ✓ 没有更多 Tool Call ,停止循环 │
└────────────────────────────────────────────────┘
┌─ 最终结果 ──────────────────────────────────────┐
│ "已为您设置好了明天上午 10 点的闹钟" │
└────────────────────────────────────────────────┘
5.3 自动执行模式的优势与局限
✅ 代码最简洁: 只需一行 .tools(ArtisanTools) 就完成了整个工具调用流程
✅ 框架自动处理循环: 无需手动管理 Tool Call 的循环执行
✅ 适合大多数场景: 绝大部分的工具调用都可以用这种方式完成
❌ 无法精细控制: 如果你需要在工具执行后做特殊处理(比如持久化、审核、日志),就无法实现
❌ 工具执行同步: 所有工具执行都是同步的,不支持异步并发执行
❌ 无法中断循环: 如果大模型陷入了无限的 Tool Call 循环,框架会继续执行直到超时
@GetMapping("/simple-tool-call")
public String simpleToolCall (String question) {
return chatClient.prompt().user(question).tools(ArtisanTools).call().content();
}
@GetMapping("/complex-tool-call")
public String complexToolCall (String question) {
}
六、手动控制模式:精细化控制 Tool Calling 当需要对工具调用流程进行精细控制时,就应该使用手动控制模式 。这个模式给予开发者对 Tool Calling 流程的完全掌控权。
6.1 手动控制模式的实现 @RestController
public class ToolController {
@Autowired
private ChatClient chatClient;
@GetMapping("/userControlledTool")
public String userControlledTool (String question) {
ToolCallback[] toolCallbacks = ToolCallbacks.from(new ArtisanTools ());
ToolCallingChatOptions toolCallingChatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(toolCallbacks)
.internalToolExecutionEnabled(false )
.build();
Prompt prompt = Prompt.builder()
.chatOptions(toolCallingChatOptions)
.content(question)
.build();
ChatResponse chatResponse = chatClient.prompt(prompt).call().chatResponse();
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
while (chatResponse.hasToolCalls()) {
ToolExecutionResult toolExecutionResult = toolCallingManager
.executeToolCalls(prompt, chatResponse);
chatResponse = chatClient.prompt(new Prompt (
toolExecutionResult.conversationHistory(),
toolCallingChatOptions
)).call().chatResponse();
}
return chatResponse.getResult().getOutput().getText();
}
}
6.2 手动控制模式的核心概念
ToolCallingChatOptions toolCallingChatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(toolCallbacks)
.internalToolExecutionEnabled(false )
.build();
参数值 含义 用途 false 框架不自动执行工具,只是识别 Tool Call 信息 需要手动控制和定制工具执行逻辑 true 框架自动执行工具,对开发者透明 简单场景,框架全自动处理
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, chatResponse);
初始 Prompt :
[User :"设置闹钟" ]
↓ 第 1 轮调用大模型后
对话历史:
[User :"设置闹钟" , Assistant :ToolCall-> {toolName: "setAlarm" , arguments: {...}}]
↓ ToolCallingManager .executeToolCalls() 后
对话历史更新为:
[User :"设置闹钟" , Assistant :ToolCall-> {toolName: "setAlarm" , arguments: {...}}, ToolResult :"闹钟已设置" ,]
↓ 将更新后的对话历史再次发送给大模型
大模型基于完整的对话历史生成最终答案
6.3 手动控制模式的完整执行流程 ┌─────────────────────────────────────────────────────────┐
│ 用户输入:question ="帮我设置一个明天的闹钟" │
└────────────────┬────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 步骤 1 :转换工具为 ToolCallback[] │
│ ToolCallbacks.from (new ArtisanTools()) │
└────────────────┬────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 步骤 2 :创建工具调用选项 │
│ ToolCallingChatOptions │
│ .internalToolExecutionEnabled (false) │
│
└────────────────┬────────────────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────┐
│ 步骤 3 :首次调用大模型 │
│ chatClient.prompt (prompt).call ().chatResponse () │
│ │
│ 响应包含:ToolCall 信息 │
│ hasToolCalls ()=true │
└────────────────┬────────────────────────────────────────┘
▼
┌──────────┴──────────┐
│ while (chatResponse │
│ .hasToolCalls()) │
│ ▼ ▼
│ ┌─────────────┐ ┌────────────────┐
│ │ ToolCall │ │ NoToolCalls │
│ │ 存在 │ │ 循环结束 │
│ └──┬──────────┘ └────────┬───────┘
│ │ │
│ ▼ ▼
│ ┌──────────────────────────────────────────────────────────┐
│ │ 步骤 4 :执行工具(手动) │
│ │ toolCallingManager.executeToolCalls (prompt, chatResponse)│
│ │ │
│ │ 获得 ToolExecutionResult,包含: │
│ │ - 更新的对话历史 │
│ │ - 工具执行结果 │
│ └──────────────┬───────────────────────────────────────────┘
│ ▼
│ ┌──────────────────────────────────────────────────────────┐
│ │ 步骤 5 :将结果反馈给大模型 │
│ │ chatClient.prompt ( │
│ │ newPrompt( │
│ │ toolExecutionResult.conversationHistory(), │
│ │ toolCallingChatOptions │
│ │ ) │
│ │ ).call ().chatResponse () │
│ │ │
│ │ 大模型基于工具结果生成新的响应 │
│ │ 可能包含: │
│ │ - 直接回答(没有更多工具需要调用) │
│ │ - 新的 ToolCall(需要调用另一个工具) │
│ └──────────────┬───────────────────────────────────────────┘
│ ▼
│ 判断是否还有 ToolCall? (返回循环判断条件)
6.4 手动控制模式的优势与使用场景
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, chatResponse);
saveToolCallLog(chatResponse.getToolCalls());
validateToolResults(result);
recordToolCallMetrics(toolName, executionTime);
int maxToolCallAttempts = 5 ;
int attempts = 0 ;
while (chatResponse.hasToolCalls() && attempts < maxToolCallAttempts) {
attempts++;
}
if (attempts >= maxToolCallAttempts) {
return "工具调用已达到最大次数,无法完成任务" ;
}
while (chatResponse.hasToolCalls()) {
List<ToolCall> toolCalls = chatResponse.getToolCalls();
List<ToolCall> filteredToolCalls = toolCalls.stream()
.filter(tc -> !isSensitiveTool(tc.getToolName()))
.collect(Collectors.toList());
if (filteredToolCalls.isEmpty()) {
return "无法执行此操作:工具调用涉及敏感操作" ;
}
ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, chatResponse);
}
场景 是否适合手动控制 原因 简单的信息查询 ❌ 自动模式就足够 无需特殊处理 需要持久化工具调用日志 ✅ 需要手动控制 在执行后保存记录 需要限制工具调用次数 ✅ 需要手动控制 防止无限循环 需要审核或权限验证 ✅ 需要手动控制 在执行前进行审核 需要实时监控工具执行 ✅ 需要手动控制 收集执行指标 需要条件性执行某些工具 ✅ 需要手动控制 基于业务规则过滤 涉及安全敏感操作(删除、修改等) ✅ 需要手动控制 强制人工审核
七、异常处理策略:DefaultToolExecutionExceptionProcessor 在实际的 Tool Calling 过程中,工具执行经常会失败。关键问题是:当工具执行失败时,应该如何处理?
Spring AI 提供了 ToolExecutionExceptionProcessor 接口来处理这种情况。
7.1 问题场景 @Tool(description = "获取当前时间")
LocalDateTime getCurrentDateTime () {
System.out.println("获取当前时间" );
throw new RuntimeException ("获取当前时间异常" );
}
当这个工具被调用并抛出异常时,系统有两种处理策略:
Tool Call -> 执行工具 -> 抛出异常 -> 应用程序崩溃 ❌
Tool Call -> 执行工具 -> 捕获异常 -> 返回错误信息给大模型 -> 大模型尝试其他方案 ✅
显然,策略 2 更符合 AI Agent 的设计理念 :让大模型知道工具执行失败了,并决定下一步该做什么。
7.2 异常处理器的配置 @SpringBootApplication
public class SpringAIApplication {
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor () {
return new DefaultToolExecutionExceptionProcessor (false );
}
public static void main (String[] args) {
SpringApplication.run(SpringAIApplication.class, args);
}
}
7.3 参数详解:true vs false DefaultToolExecutionExceptionProcessor 的构造函数接受一个布尔参数:
new DefaultToolExecutionExceptionProcessor (false )
new DefaultToolExecutionExceptionProcessor (true )
参数值 行为 大模型的感知 适用场景 false 捕获异常,返回错误信息给大模型 '工具执行失败:xxxx' ✅ 推荐:让大模型处理失败 true 抛出异常,中断流程 应用崩溃 ❌ 不推荐:丧失容错能力
7.4 执行流程对比 用户:"帮我查一下现在几点"
↓ 大模型:"我需要调用 getCurrentDateTime 工具"
↓ 框架执行工具 -> 异常!RuntimeException ("获取当前时间异常" )
↓ 异常处理器捕获 -> 不抛出异常
↓ 异常信息被转换为工具结果:"工具执行失败:获取当前时间异常"
↓ 返回给大模型:"工具返回:执行失败,异常信息为..."
↓ 大模型分析结果:"抱歉,我无法获取当前时间。您可以告诉我您所在的时区,我可以帮您计算时间。"
↓ 用户收到:一个有理有据的错误说明,而不是应用崩溃 ✅ 用户体验好,系统继续运行
用户:"帮我查一下现在几点"
↓ 大模型:"我需要调用 getCurrentDateTime 工具"
↓ 框架执行工具 -> 异常!RuntimeException ("获取当前时间异常" )
↓ 异常处理器直接抛出异常
↓ 应用程序崩溃
↓ 用户看到:500In ternalServerError ❌ 用户体验差,系统出错
7.5 异常处理的最佳实践 第 1 步:始终配置异常处理器(参数为 false)
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor () {
return new DefaultToolExecutionExceptionProcessor (false );
}
@Tool(description = "获取当前时间")
LocalDateTime getCurrentDateTime () {
try {
return LocalDateTime.now();
} catch (Exception e) {
logger.error("获取时间失败" , e);
throw new RuntimeException ("系统时间服务暂时不可用,请稍后重试" , e);
}
}
@Component
public class ArtisanTools {
private static final Logger logger = LoggerFactory.getLogger(ArtisanTools.class);
@Tool(description = "获取当前时间")
LocalDateTime getCurrentDateTime () {
try {
return LocalDateTime.now();
} catch (Exception e) {
logger.error("Tool execution failed: getCurrentDateTime" , e);
alertService.sendAlert("工具异常" , "getCurrentDateTime 执行失败" );
throw new RuntimeException ("获取时间异常:" + e.getMessage());
}
}
@Tool(description = "设置闹钟")
void setAlarm (AlarmRequest alarmRequest) {
try {
if (alarmRequest.getTime() == null || alarmRequest.getTime().isEmpty()) {
throw new IllegalArgumentException ("时间不能为空" );
}
System.out.println("设置闹钟:" + alarmRequest.getTime());
} catch (IllegalArgumentException e) {
throw new RuntimeException ("参数错误:" + e.getMessage());
} catch (Exception e) {
logger.error("Tool execution failed: setAlarm" , e);
alertService.sendAlert("工具异常" , "setAlarm 执行失败" );
throw new RuntimeException ("设置闹钟异常:" + e.getMessage());
}
}
}
如果工具经常因为参数不对而失败,说明参数描述不清楚:
@JsonPropertyDescription("时间")
private String time;
@JsonPropertyDescription("闹钟时间,必须使用中文年月日格式,例如:2025 年 3 月 31 日")
private String time;
八、完整的工具调用示例 为了更清楚地展示整个过程,让我们看一个完整的、可运行的例子。
8.1 工具定义 - ArtisanTools.java package com.Artisan;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class ArtisanTools {
@Tool(description = "获取当前系统时间,返回格式为 LocalDateTime 对象")
LocalDateTime getCurrentDateTime () {
System.out.println("获取当前时间" );
throw new RuntimeException ("获取当前时间异常" );
}
@Tool(description = "用指定时间和位置设置闹钟,闹钟会在指定时间提醒")
void setAlarm (AlarmRequest alarmRequest) {
System.out.println("地址:" + alarmRequest.getAddress());
System.out.println("闹钟时间为:" + alarmRequest.getTime());
}
}
8.2 参数模型 - AlarmRequest.java package com.Artisan;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import org.springframework.ai.tool.annotation.ToolParam;
public class AlarmRequest {
@JsonPropertyDescription("闹钟的时间,必须使用中文年月日格式。" +
"例如:2025 年 3 月 31 日、2025 年 12 月 25 日。" +
"不接受其他格式如 2025-03-31 或 31/03/2025")
private String time;
@JsonPropertyDescription("闹钟要设置的位置,用于标识闹钟提醒的场景。" +
"常见值:卧室、客厅、办公室、车内、客房等")
@ToolParam(required = false)
private String address;
public String getTime () { return time; }
public void setTime (String time) { this .time = time; }
public String getAddress () { return address; }
public void setAddress (String address) { this .address = address; }
}
8.3 控制器 - ToolController.java package com.Artisan;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.tool.ToolCallingChatOptions;
import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.tool.ToolExecutionResult;
import org.springframework.ai.support.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ToolController {
@Autowired
private ChatClient chatClient;
@Autowired
private ArtisanTools artisanTools;
@GetMapping("/tool")
public String tool (String question) {
return chatClient
.prompt()
.user(question)
.tools(ArtisanTools)
.call()
.content();
}
@GetMapping("/userControlledTool")
public String userControlledTool (String question) {
ToolCallback[] toolCallbacks = ToolCallbacks.from(new ArtisanTools ());
ToolCallingChatOptions toolCallingChatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(toolCallbacks)
.internalToolExecutionEnabled(false )
.build();
Prompt prompt = Prompt.builder()
.chatOptions(toolCallingChatOptions)
.content(question)
.build();
ChatResponse chatResponse = chatClient.prompt(prompt).call().chatResponse();
ToolCallingManager toolCallingManager = ToolCallingManager.builder().build();
while (chatResponse.hasToolCalls()) {
ToolExecutionResult toolExecutionResult = toolCallingManager
.executeToolCalls(prompt, chatResponse);
chatResponse = chatClient.prompt(new Prompt (
toolExecutionResult.conversationHistory(),
toolCallingChatOptions
)).call().chatResponse();
}
return chatResponse.getResult().getOutput().getText();
}
}
8.4 应用启动类 - SpringAIApplication.java package com.Artisan;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemoryRepository;
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
import org.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;
import org.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;
import org.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class SpringAIApplication {
@Bean
public ChatClient chatClient (ChatClient.Builder builder) {
return builder.build();
}
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor () {
return new DefaultToolExecutionExceptionProcessor (false );
}
public static void main (String[] args) {
SpringApplication.run(SpringAIApplication.class, args);
}
}
九、常见问题与最佳实践
9.1 大模型没有调用我定义的工具
@Tool(description = "工具")
@Tool(description = "根据日期查询该天的天气预报,包括温度、风力、降雨概率等信息")
public class MyTools {
@Tool(description = "...")
}
@Component
public class MyTools {
@Tool(description = "...")
}
@JsonPropertyDescription("日期")
private String date;
@JsonPropertyDescription("要查询的日期,格式为 YYYY-MM-DD,如 2025-03-31。" +
"只能查询过去 30 天和未来 7 天的数据")
private String date;
9.2 工具执行失败,导致应用崩溃 @Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor () {
return new DefaultToolExecutionExceptionProcessor (false );
}
9.3 大模型陷入无限循环,不断调用工具
参数描述歧义 - 大模型理解错了参数含义
工具描述过于宽泛 - 大模型认为所有问题都需要这个工具
工具返回结果不清楚 - 大模型不知道工具是否执行成功
int maxToolCallAttempts = 5 ;
int attempts = 0 ;
while (chatResponse.hasToolCalls() && attempts < maxToolCallAttempts) {
attempts++;
}
if (attempts >= maxToolCallAttempts) {
return "执行失败:工具调用已达到最大次数" ;
}
9.4 工具有多个参数,大模型总是遗漏某些参数 使用 @ToolParam 明确标记参数的可选性:
public class Request {
@JsonPropertyDescription("用户的 ID 号,例如:12345")
private String userId;
@JsonPropertyDescription("查询的开始日期,格式为 YYYY-MM-DD,可选。如果不指定,默认查询最近 30 天")
@ToolParam(required = false)
private String startDate;
}
9.5 同一个工具类有很多方法,应该都标记为工具吗? 只标记那些真正需要被大模型调用的方法。不要把所有方法都标记为工具,这会增加大模型的决策复杂度。
@Component
public class UserService {
@Tool(description = "根据用户 ID 查询用户信息")
public User queryUser (String userId) { ... }
@Tool(description = "根据用户名模糊搜索用户")
public List<User> searchUsers (String keyword) { ... }
private void validateUserId (String userId) { ... }
@Tool
private void queryUserInternal (String userId) { ... }
}
十、总结与展望
10.1 Tool Calling 的核心价值 Tool Calling 机制打破了大模型与外部世界的隔阂,它让大模型:
能够感知工具的存在 - 通过 @Tool 注解和 description
能够正确选择工具 - 通过清晰的参数描述和工具职责
能够自主调用工具 - 框架自动处理复杂的 Tool Calling 流程
能够基于工具结果进行推理 - 完整的对话历史维护
能够应对工具执行失败 - 异常处理和容错机制
10.2 自动执行 vs 手动控制的选择 维度 自动执行 手动控制 代码复杂度 极低 较高 控制精度 低(不可控) 高(完全控制) 适用场景 简单、可信任的工具 复杂、需要监控的工具 推荐使用 开发初期、快速原型 生产环境、关键操作
默认使用自动执行模式,代码简洁
如果需要特殊处理(审核、持久化、限流等),升级到手动控制模式
不要为了可控性而过度设计,简单就是最好的设计
10.3 参数描述的黄金法则
清晰 - 用自然语言清楚地说明字段的含义
具体 - 提供具体的格式示例和取值范围
完整 - 说明所有的约束条件和特殊情况
@JsonPropertyDescription("查询的日期范围,格式为 YYYY-MM-DD。" +
"示例:2025-03-31。" +
"约束:不能查询过去 90 天之前的数据,不能查询未来数据。" +
"如果不指定,默认查询今天的数据")
@ToolParam(required = false)
private String queryDate;
10.4 走向 AI Agent 的下一步 掌握了 Tool Calling,你已经拥有了构建真正 AI Agent 的基础。下一步可以探索:
多工具协作 - 设计工具系统,让多个工具协同工作
工具组合 - 复杂任务分解为多个工具的组合调用
工具优先级 - 不同情况下选择不同的工具
工具链路优化 - 监控和优化工具的执行效率
Agent 框架 - 集成 Spring AI 的 Agent 框架,实现完整的自主代理
十一、参考资源 相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online