大模型开发 - Spring AI 之 Tool 机制应用:赋予大模型调用外部工具的能力
文章目录
- 一、引言:为什么需要 Tool Calling?
- 二、Tool Calling 的核心概念
- 三、工具定义:@Tool 注解的使用
- 四、参数描述:两种方式的区别与选择
- 五、自动执行模式:让大模型自主调用工具
- 六、手动控制模式:精细化控制 Tool Calling
- 七、异常处理策略:DefaultToolExecutionExceptionProcessor
- 八、完整的工具调用示例
- 九、常见问题与最佳实践
- 十、总结与展望
- 十一、参考资源

本文基于 Spring AI 1.1.0,深入讲解 Tool Calling(工具调用)机制的完整实现。通过详细的代码示例、参数描述方式对比、自动执行与手动控制两种模式的分析,以及异常处理策略的设计,帮助开发者理解和掌握如何让大模型像人类一样,根据需要自主选择和调用外部工具来解决问题。
一、引言:为什么需要 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 注解来标记一个可被大模型调用的方法:
@ComponentpublicclassArtisanTools{@Tool(description ="获取当前时间")LocalDateTimegetCurrentDateTime(){System.out.println("获取当前时间");returnLocalDateTime.now();}@Tool(description ="用指定时间设置闹钟")voidsetAlarm(AlarmRequest alarmRequest){System.out.println("地址:"+ alarmRequest.getAddress());System.out.println("闹钟时间为:"+ alarmRequest.getTime());}}关键要点:
- @Component 注解: 必须将工具类注册为 Spring Bean,这样 Spring AI 才能发现和管理它。
- @Tool 注解: 标记哪些方法是可被大模型调用的工具。
- description 参数: 这是非常关键的!它用自然语言描述工具的功能。大模型会根据这个描述来决定是否调用该工具。
3.2 Description 的重要性
让我们通过对比来理解 description 为什么关键:
不好的 description 示例:
@Tool(description ="工具")// ❌ 太模糊,大模型不知道这是什么工具@Tool(description ="调用方法")// ❌ 没有说明具体功能好的 description 示例:
@Tool(description ="获取当前系统时间,返回格式为 LocalDateTime")// ✅ 清晰、具体@Tool(description ="根据指定的日期和地点设置闹钟,闹钟会在指定时间触发提醒")// ✅ 详细说明功能和效果实战技巧:
当你编写 description 时,想象你在向一个助手说话:
- 说清楚 做什么:获取时间、设置闹钟、查询数据库
- 说清楚 返回什么:时间对象、设置成功、查询结果
- 如果有特殊说明,加上去:格式要求、范围限制、副作用
@Tool(description ="获取指定日期的天气预报。返回温度、风力、降雨概率等信息。"+"如果日期超过7天,只能返回预测数据,准确度降低。")WeatherInfogetWeather(String date){// ...}四、参数描述:两种方式的区别与选择
工具方法可能需要参数,问题是:如何清楚地告诉大模型这些参数是什么含义?
Spring AI 提供了两种参数描述的方式,它们各有特点:
4.1 方式一:@JsonPropertyDescription(推荐)
publicclassAlarmRequest{@JsonPropertyDescription("用中文中的年月日的格式,比如2025年3月31日")privateString time;@JsonPropertyDescription("闹钟要设置的位置,比如卧室、客厅")privateString address;// getter/setter...}特点与优势:
- Jackson 标准注解:
@JsonPropertyDescription是 Jackson 库提供的标准注解,用于 JSON Schema 描述。 - 更灵活的描述方式: 可以包含格式示例、取值范围、特殊说明等详细信息。
- 直观易读: 描述直接关联到字段定义。
- 最佳实践选择: 在生产环境中广泛使用。
适用场景:
- 复杂的参数类,有多个字段需要描述
- 需要提供详细的格式示例
- 字段有特殊的输入格式要求
4.2 方式二:@ToolParam(备选方案)
publicclassAlarmRequest{// @ToolParam(description = "时间") // ❌ 这样写不推荐privateString time;@ToolParam(description ="地址", required =false)privateString address;// 标记为可选字段// getter/setter...}特点:
- Spring AI 原生注解: 专为 Spring AI Tool Calling 而设计。
- 支持 required 属性: 可以标记字段是否必需。
- 针对性强: 完全针对 Tool Calling 场景。
使用建议:
publicclassAlarmRequest{@JsonPropertyDescription("用中文中的年月日的格式,比如2025年3月31日")privateString time;// 必需字段,用 @JsonPropertyDescription@ToolParam(description ="地址", required =false)privateString address;// 可选字段,用 @ToolParam 标记 required=false}4.3 两种方式的对比与最佳实践
| 对比维度 | @JsonPropertyDescription | @ToolParam |
|---|---|---|
| 字段格式示例 | ✅ 支持,在描述中体现 | ❌ 不支持 |
| 必需/可选 | ❌ 无法标记 | ✅ 通过 required 属性 |
| 复杂对象嵌套 | ✅ 支持 | ⚠️ 有限支持 |
| 代码可维护性 | ✅ 高(与 JSON 序列化一致) | ✅ 高(明确的 Tool 语义) |
| 可读性 | ✅ 优秀 | ✅ 优秀 |
最佳实践方案:
publicclassAlarmRequest{/** * 优先使用 @JsonPropertyDescription 进行主要描述 * 可以提供详细的格式示例和说明 */@JsonPropertyDescription("用中文中的年月日的格式,比如2025年3月31日。例如:2025年3月31日")privateString time;/** * 对于可选字段,配合 @ToolParam(required = false) * 两者结合,既有详细描述,又有必需性标记 */@JsonPropertyDescription("闹钟要设置的位置,比如卧室、客厅、办公室等")@ToolParam(required =false)privateString address;publicStringgetTime(){return time;}publicvoidsetTime(String time){this.time = time;}publicStringgetAddress(){return address;}publicvoidsetAddress(String address){this.address = address;}}参数描述写作指南:
❌ 不好的例子:
"时间" // 太简洁,大模型不知道格式 "address" // 中英混用,可读性差 "参数1、参数2、参数3" // 没有实际信息 ✅ 好的例子:
"设置闹钟的时间,格式为中文年月日,例如:2025年3月31日" "闹钟要设置的位置,可选值包括:卧室、客厅、办公室、车内等" "查询的日期范围,格式为 YYYY-MM-DD,最多查询未来 30 天的数据" 五、自动执行模式:让大模型自主调用工具
Spring AI 提供了最简洁的工具调用方式:自动执行模式。在这个模式下,框架自动处理整个 Tool Calling 流程。
5.1 自动执行模式的实现
@RestControllerpublicclassToolController{@AutowiredprivateChatClient chatClient;@AutowiredprivateArtisanToolsArtisanTools;/** * 自动执行模式:最简洁的方式 * 用户提问 -> 大模型决定调用工具 -> 框架自动执行工具 -> 大模型生成回答 -> 返回结果 */@GetMapping("/tool")publicStringtool(String question){return chatClient .prompt().user(question).tools(ArtisanTools)// ← 关键:传入工具对象.call().content();}}执行流程解析:
chatClient.prompt() // 1. 创建 Prompt 构建器 .user(question) // 2. 设置用户问题 .tools(ArtisanTools) // 3. 注册可用工具 .call() // 4. 发起调用,框架自动处理 Tool Calling 循环 .content() // 5. 提取最终文本结果 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")publicStringsimpleToolCall(String question){// 简单的、无需特殊处理的工具调用return chatClient.prompt().user(question).tools(ArtisanTools).call().content();}// ❌ 不适合自动执行模式(需要手动控制模式)@GetMapping("/complex-tool-call")publicStringcomplexToolCall(String question){// 需要在工具执行后进行特殊处理:// 1. 持久化工具调用记录// 2. 对工具结果进行验证// 3. 控制 Tool Call 的最大次数// 4. 在特定条件下中断 Tool Call 循环// -> 这些场景需要使用"手动控制模式"}六、手动控制模式:精细化控制 Tool Calling
当需要对工具调用流程进行精细控制时,就应该使用手动控制模式。这个模式给予开发者对 Tool Calling 流程的完全掌控权。
6.1 手动控制模式的实现
@RestControllerpublicclassToolController{@AutowiredprivateChatClient chatClient;/** * 手动控制模式:对工具调用过程进行细粒度控制 */@GetMapping("/userControlledTool")publicStringuserControlledTool(String question){// 步骤1:将工具转换为 ToolCallback 数组ToolCallback[] toolCallbacks =ToolCallbacks.from(newArtisanTools());// 步骤2:创建工具调用选项,禁用框架自动执行ToolCallingChatOptions toolCallingChatOptions =ToolCallingChatOptions.builder().toolCallbacks(toolCallbacks).internalToolExecutionEnabled(false)// ← 关键:禁用自动执行.build();// 步骤3:创建 Prompt,设置工具选项Prompt prompt =Prompt.builder().chatOptions(toolCallingChatOptions).content(question).build();// 步骤4:首次调用大模型,获取 Tool Call 信息ChatResponse chatResponse = chatClient.prompt(prompt).call().chatResponse();// 步骤5:创建 ToolCallingManager,手动执行工具ToolCallingManager toolCallingManager =ToolCallingManager.builder().build();// 步骤6:循环执行 Tool Callingwhile(chatResponse.hasToolCalls()){// 步骤6.1:执行工具,获取结果ToolExecutionResult toolExecutionResult = toolCallingManager .executeToolCalls(prompt, chatResponse);// 步骤6.2:将工具结果反馈给大模型,获取新的响应 chatResponse = chatClient.prompt(newPrompt( toolExecutionResult.conversationHistory(),// 更新对话历史 toolCallingChatOptions )).call().chatResponse();}// 步骤7:返回最终的文本结果return chatResponse.getResult().getOutput().getText();}}6.2 手动控制模式的核心概念
关键配置详解:
// internalToolExecutionEnabled = false 的含义ToolCallingChatOptions toolCallingChatOptions =ToolCallingChatOptions.builder().toolCallbacks(toolCallbacks).internalToolExecutionEnabled(false)// false: 框架不自动执行工具// true: 框架自动执行工具(默认).build();
| 参数值 | 含义 | 用途 |
|---|---|---|
| false | 框架不自动执行工具,只是识别 Tool Call 信息 | 需要手动控制和定制工具执行逻辑 |
| true | 框架自动执行工具,对开发者透明 | 简单场景,框架全自动处理 |
ToolCallingManager 的职责:
ToolCallingManager toolCallingManager =ToolCallingManager.builder().build();// 执行工具,返回结果ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, chatResponse);// ToolExecutionResult 包含:// 1. conversationHistory: 更新后的对话历史(工具调用 + 执行结果)// 2. 工具执行的所有中间结果对话历史的作用:
初始 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(newArtisanTools()) │ └────────────────┬────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 步骤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){// 有 Tool Call 且未超过最大尝试次数,继续执行 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 ="获取当前时间")LocalDateTimegetCurrentDateTime(){System.out.println("获取当前时间");thrownewRuntimeException("获取当前时间异常");// ← 工具执行失败!// return LocalDateTime.now();}当这个工具被调用并抛出异常时,系统有两种处理策略:
策略1:抛出异常,中断整个流程
Tool Call -> 执行工具 -> 抛出异常 -> 应用程序崩溃 ❌ 策略2:捕获异常,将错误信息返回给大模型
Tool Call -> 执行工具 -> 捕获异常 -> 返回错误信息给大模型 -> 大模型尝试其他方案 ✅ 显然,策略2 更符合 AI Agent 的设计理念:让大模型知道工具执行失败了,并决定下一步该做什么。
7.2 异常处理器的配置
@SpringBootApplicationpublicclassSpringAIApplication{/** * 配置异常处理器 * 参数 false 表示:不抛出异常,而是将异常信息返回给大模型 */@BeanToolExecutionExceptionProcessortoolExecutionExceptionProcessor(){returnnewDefaultToolExecutionExceptionProcessor(false);}publicstaticvoidmain(String[] args){SpringApplication.run(SpringAIApplication.class, args);}}7.3 参数详解:true vs false
DefaultToolExecutionExceptionProcessor 的构造函数接受一个布尔参数:
// 参数值为 falsenewDefaultToolExecutionExceptionProcessor(false)// 参数值为 truenewDefaultToolExecutionExceptionProcessor(true)
| 参数值 | 行为 | 大模型的感知 | 适用场景 |
|---|---|---|---|
| false | 捕获异常,返回错误信息给大模型 | “工具执行失败:xxxx” | ✅ 推荐:让大模型处理失败 |
| true | 抛出异常,中断流程 | 应用崩溃 | ❌ 不推荐:丧失容错能力 |
7.4 执行流程对比
参数为 false 的执行流程:
用户:"帮我查一下现在几点" ↓ 大模型:"我需要调用 getCurrentDateTime 工具" ↓ 框架执行工具 -> 异常!RuntimeException("获取当前时间异常") ↓ 异常处理器捕获 -> 不抛出异常 ↓ 异常信息被转换为工具结果:"工具执行失败:获取当前时间异常" ↓ 返回给大模型:"工具返回:执行失败,异常信息为..." ↓ 大模型分析结果:"抱歉,我无法获取当前时间。您可以告诉我您所在的时区,我可以帮您计算时间。" ↓ 用户收到:一个有理有据的错误说明,而不是应用崩溃 ✅ 用户体验好,系统继续运行 参数为 true 的执行流程:
用户:"帮我查一下现在几点" ↓ 大模型:"我需要调用 getCurrentDateTime 工具" ↓ 框架执行工具 -> 异常!RuntimeException("获取当前时间异常") ↓ 异常处理器直接抛出异常 ↓ 应用程序崩溃 ↓ 用户看到:500InternalServerError ❌ 用户体验差,系统出错 7.5 异常处理的最佳实践
第1步:始终配置异常处理器(参数为 false)
@BeanToolExecutionExceptionProcessortoolExecutionExceptionProcessor(){// 参数为 false:不抛出异常,允许大模型处理失败returnnewDefaultToolExecutionExceptionProcessor(false);}第2步:在工具方法中添加防御性编程
@Tool(description ="获取当前时间")LocalDateTimegetCurrentDateTime(){try{// 业务逻辑returnLocalDateTime.now();}catch(Exception e){// 记录详细的错误信息,便于调试 logger.error("获取时间失败", e);// 抛出更有信息的异常thrownewRuntimeException("系统时间服务暂时不可用,请稍后重试", e);}}第3步:监控和告警工具执行异常
@ComponentpublicclassArtisanTools{privatestaticfinalLogger logger =LoggerFactory.getLogger(ArtisanTools.class);@Tool(description ="获取当前时间")LocalDateTimegetCurrentDateTime(){try{returnLocalDateTime.now();}catch(Exception e){// 记录错误日志 logger.error("Tool execution failed: getCurrentDateTime", e);// 发送告警 alertService.sendAlert("工具异常","getCurrentDateTime 执行失败");// 抛出异常给框架处理thrownewRuntimeException("获取时间异常:"+ e.getMessage());}}@Tool(description ="设置闹钟")voidsetAlarm(AlarmRequest alarmRequest){try{// 验证参数if(alarmRequest.getTime()==null|| alarmRequest.getTime().isEmpty()){thrownewIllegalArgumentException("时间不能为空");}// 业务逻辑System.out.println("设置闹钟:"+ alarmRequest.getTime());}catch(IllegalArgumentException e){// 参数错误,抛出异常给大模型thrownewRuntimeException("参数错误:"+ e.getMessage());}catch(Exception e){ logger.error("Tool execution failed: setAlarm", e); alertService.sendAlert("工具异常","setAlarm 执行失败");thrownewRuntimeException("设置闹钟异常:"+ e.getMessage());}}}第4步:根据异常信息优化工具描述
如果工具经常因为参数不对而失败,说明参数描述不清楚:
// ❌ 描述不清,导致大模型经常传错参数@JsonPropertyDescription("时间")privateString time;// ✅ 清晰的描述,大模型知道该传什么格式@JsonPropertyDescription("闹钟时间,必须使用中文年月日格式,例如:2025年3月31日")privateString time;八、完整的工具调用示例
为了更清楚地展示整个过程,让我们看一个完整的、可运行的例子。
8.1 工具定义 - ArtisanTools.java
packagecom.Artisan;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.stereotype.Component;importjava.time.LocalDateTime;@ComponentpublicclassArtisanTools{/** * 获取当前时间 * 清晰的 description 帮助大模型理解工具的用途 */@Tool(description ="获取当前系统时间,返回格式为 LocalDateTime 对象")LocalDateTimegetCurrentDateTime(){System.out.println("获取当前时间");thrownewRuntimeException("获取当前时间异常");// return LocalDateTime.now();}/** * 设置闹钟 * 接受一个复杂的参数对象 AlarmRequest */@Tool(description ="用指定时间和位置设置闹钟,闹钟会在指定时间提醒")voidsetAlarm(AlarmRequest alarmRequest){System.out.println("地址:"+ alarmRequest.getAddress());System.out.println("闹钟时间为:"+ alarmRequest.getTime());}}8.2 参数模型 - AlarmRequest.java
packagecom.Artisan;importcom.fasterxml.jackson.annotation.JsonPropertyDescription;importorg.springframework.ai.tool.annotation.ToolParam;/** * 闹钟请求参数 * 展示了参数描述的最佳实践 */publicclassAlarmRequest{/** * time 字段:必需 * 使用 @JsonPropertyDescription 提供详细的格式说明和示例 */@JsonPropertyDescription("闹钟的时间,必须使用中文年月日格式。"+"例如:2025年3月31日、2025年12月25日。"+"不接受其他格式如 2025-03-31 或 31/03/2025")privateString time;/** * address 字段:可选 * 使用 @ToolParam(required = false) 标记为可选 */@JsonPropertyDescription("闹钟要设置的位置,用于标识闹钟提醒的场景。"+"常见值:卧室、客厅、办公室、车内、客房等")@ToolParam(required =false)privateString address;publicStringgetTime(){return time;}publicvoidsetTime(String time){this.time = time;}publicStringgetAddress(){return address;}publicvoidsetAddress(String address){this.address = address;}}8.3 控制器 - ToolController.java
packagecom.Artisan;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.model.tool.ToolCallingChatOptions;importorg.springframework.ai.model.tool.ToolCallingManager;importorg.springframework.ai.model.tool.ToolExecutionResult;importorg.springframework.ai.support.ToolCallbacks;importorg.springframework.ai.tool.ToolCallback;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;/** * 工具调用演示控制器 * 展示自动执行和手动控制两种模式 */@RestControllerpublicclassToolController{@AutowiredprivateChatClient chatClient;@AutowiredprivateArtisanToolsArtisanTools;/** * 自动执行模式:最简洁的方式 * * 请求示例: * GET /tool?question=帮我设置一个明天上午10点的闹钟 */@GetMapping("/tool")publicStringtool(String question){return chatClient .prompt().user(question).tools(ArtisanTools)// 注册工具,框架自动处理 Tool Calling.call().content();}/** * 手动控制模式:对工具调用过程进行精细控制 * * 请求示例: * GET /userControlledTool?question=帮我设置一个明天上午10点的闹钟 */@GetMapping("/userControlledTool")publicStringuserControlledTool(String question){// 步骤1:将工具对象转换为 ToolCallback 数组ToolCallback[] toolCallbacks =ToolCallbacks.from(newArtisanTools());// 步骤2:创建工具调用选项,禁用框架自动执行ToolCallingChatOptions toolCallingChatOptions =ToolCallingChatOptions.builder().toolCallbacks(toolCallbacks).internalToolExecutionEnabled(false)// 关键:禁用自动执行.build();// 步骤3:创建 PromptPrompt prompt =Prompt.builder().chatOptions(toolCallingChatOptions).content(question).build();// 步骤4:首次调用大模型ChatResponse chatResponse = chatClient.prompt(prompt).call().chatResponse();// 步骤5:创建 ToolCallingManager,准备手动执行工具ToolCallingManager toolCallingManager =ToolCallingManager.builder().build();// 步骤6:循环执行 Tool Calling,直到大模型不再需要调用工具while(chatResponse.hasToolCalls()){// 执行大模型指定的所有工具ToolExecutionResult toolExecutionResult = toolCallingManager .executeToolCalls(prompt, chatResponse);// 将工具执行结果反馈给大模型,获取新的响应 chatResponse = chatClient.prompt(newPrompt( toolExecutionResult.conversationHistory(),// 包含工具调用和结果的完整对话历史 toolCallingChatOptions )).call().chatResponse();}// 步骤7:返回最终文本结果return chatResponse.getResult().getOutput().getText();}}8.4 应用启动类 - SpringAIApplication.java
packagecom.Artisan;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.ai.chat.memory.ChatMemory;importorg.springframework.ai.chat.memory.InMemoryChatMemoryRepository;importorg.springframework.ai.chat.memory.MessageWindowChatMemory;importorg.springframework.ai.chat.memory.repository.jdbc.JdbcChatMemoryRepository;importorg.springframework.ai.tool.execution.DefaultToolExecutionExceptionProcessor;importorg.springframework.ai.tool.execution.ToolExecutionExceptionProcessor;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.context.annotation.Bean;@SpringBootApplicationpublicclassSpringAIApplication{@BeanpublicChatClientchatClient(ChatClient.Builder builder){return builder.build();}/** * 配置异常处理器 * 参数 false:不抛出异常,而是将异常信息返回给大模型 * 这样大模型可以尝试其他方案,而不是导致应用崩溃 */@BeanToolExecutionExceptionProcessortoolExecutionExceptionProcessor(){returnnewDefaultToolExecutionExceptionProcessor(false);}publicstaticvoidmain(String[] args){SpringApplication.run(SpringAIApplication.class, args);}}九、常见问题与最佳实践
9.1 大模型没有调用我定义的工具
可能的原因:
- 工具 description 不清楚
// ❌ 不好的做法@Tool(description ="工具")// ✅ 好的做法@Tool(description ="根据日期查询该天的天气预报,包括温度、风力、降雨概率等信息")- 工具没有被注册为 Bean
// ❌ 缺少 @ComponentpublicclassMyTools{@Tool(description ="...")}// ✅ 正确做法@ComponentpublicclassMyTools{@Tool(description ="...")}- 参数描述不清楚,大模型不知道该传什么
// ❌ 参数描述不足@JsonPropertyDescription("日期")privateString date;// ✅ 详细的参数描述@JsonPropertyDescription("要查询的日期,格式为 YYYY-MM-DD,如 2025-03-31。"+"只能查询过去 30 天和未来 7 天的数据")privateString date;9.2 工具执行失败,导致应用崩溃
解决方案:
确保配置了异常处理器:
@BeanToolExecutionExceptionProcessortoolExecutionExceptionProcessor(){returnnewDefaultToolExecutionExceptionProcessor(false);// false 表示不抛出异常}9.3 大模型陷入无限循环,不断调用工具
可能的原因:
- 参数描述歧义 - 大模型理解错了参数含义
- 工具描述过于宽泛 - 大模型认为所有问题都需要这个工具
- 工具返回结果不清楚 - 大模型不知道工具是否执行成功
解决方案:
使用手动控制模式,添加最大尝试次数的限制:
int maxToolCallAttempts =5;int attempts =0;while(chatResponse.hasToolCalls()&& attempts < maxToolCallAttempts){ attempts++;// 执行工具...}if(attempts >= maxToolCallAttempts){return"执行失败:工具调用已达到最大次数";}9.4 工具有多个参数,大模型总是遗漏某些参数
解决方案:
使用 @ToolParam 明确标记参数的可选性:
publicclassRequest{// 必需参数:什么都不加@JsonPropertyDescription("用户的 ID 号,例如:12345")privateString userId;// 可选参数:加上 required = false@JsonPropertyDescription("查询的开始日期,格式为 YYYY-MM-DD,可选。如果不指定,默认查询最近 30 天")@ToolParam(required =false)privateString startDate;}9.5 同一个工具类有很多方法,应该都标记为工具吗?
建议:
只标记那些真正需要被大模型调用的方法。不要把所有方法都标记为工具,这会增加大模型的决策复杂度。
@ComponentpublicclassUserService{// ✅ 可以标记为工具:用户关心的、需要大模型调用的@Tool(description ="根据用户ID查询用户信息")publicUserqueryUser(String userId){...}// ✅ 可以标记为工具:用户可能需要的常见操作@Tool(description ="根据用户名模糊搜索用户")publicList<User>searchUsers(String keyword){...}// ❌ 不要标记为工具:内部方法,用户不需要大模型调用privatevoidvalidateUserId(String userId){...}// ❌ 不要标记为工具:工具方法的重复,增加混乱@ToolprivatevoidqueryUserInternal(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)privateString queryDate;10.4 走向 AI Agent 的下一步
掌握了 Tool Calling,你已经拥有了构建真正 AI Agent 的基础。下一步可以探索:
- 多工具协作 - 设计工具系统,让多个工具协同工作
- 工具组合 - 复杂任务分解为多个工具的组合调用
- 工具优先级 - 不同情况下选择不同的工具
- 工具链路优化 - 监控和优化工具的执行效率
- Agent 框架 - 集成 Spring AI 的 Agent 框架,实现完整的自主代理
十一、参考资源
- Spring AI 官方文档:https://docs.spring.io/spring-ai/
- Tool Calling 概念:https://en.wikipedia.org/wiki/Tool_use_(artificial_intelligence)