大模型开发 - Spring AI 之 Tool 机制应用:赋予大模型调用外部工具的能力

大模型开发 - Spring AI 之 Tool 机制应用:赋予大模型调用外部工具的能力

文章目录

在这里插入图片描述
本文基于 Spring AI 1.1.0,深入讲解 Tool Calling(工具调用)机制的完整实现。通过详细的代码示例、参数描述方式对比、自动执行与手动控制两种模式的分析,以及异常处理策略的设计,帮助开发者理解和掌握如何让大模型像人类一样,根据需要自主选择和调用外部工具来解决问题。

一、引言:为什么需要 Tool Calling?

在与大模型交互的过程中,我们经常面临这样的场景:

  1. 用户问: “帮我查一下现在几点了?”
  2. 传统方案的问题: 大模型没有能力直接查询系统时间,只能根据训练数据回答,极容易出错(尤其是关于实时数据、当前时间、最新新闻等)。
  3. 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());}}

关键要点:

  1. @Component 注解: 必须将工具类注册为 Spring Bean,这样 Spring AI 才能发现和管理它。
  2. @Tool 注解: 标记哪些方法是可被大模型调用的工具。
  3. 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...}

特点与优势:

  1. Jackson 标准注解:@JsonPropertyDescription 是 Jackson 库提供的标准注解,用于 JSON Schema 描述。
  2. 更灵活的描述方式: 可以包含格式示例、取值范围、特殊说明等详细信息。
  3. 直观易读: 描述直接关联到字段定义。
  4. 最佳实践选择: 在生产环境中广泛使用。

适用场景:

  • 复杂的参数类,有多个字段需要描述
  • 需要提供详细的格式示例
  • 字段有特殊的输入格式要求

4.2 方式二:@ToolParam(备选方案)

publicclassAlarmRequest{// @ToolParam(description = "时间") // ❌ 这样写不推荐privateString time;@ToolParam(description ="地址", required =false)privateString address;// 标记为可选字段// getter/setter...}

特点:

  1. Spring AI 原生注解: 专为 Spring AI Tool Calling 而设计。
  2. 支持 required 属性: 可以标记字段是否必需。
  3. 针对性强: 完全针对 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 大模型没有调用我定义的工具

可能的原因:

  1. 工具 description 不清楚
// ❌ 不好的做法@Tool(description ="工具")// ✅ 好的做法@Tool(description ="根据日期查询该天的天气预报,包括温度、风力、降雨概率等信息")
  1. 工具没有被注册为 Bean
// ❌ 缺少 @ComponentpublicclassMyTools{@Tool(description ="...")}// ✅ 正确做法@ComponentpublicclassMyTools{@Tool(description ="...")}
  1. 参数描述不清楚,大模型不知道该传什么
// ❌ 参数描述不足@JsonPropertyDescription("日期")privateString date;// ✅ 详细的参数描述@JsonPropertyDescription("要查询的日期,格式为 YYYY-MM-DD,如 2025-03-31。"+"只能查询过去 30 天和未来 7 天的数据")privateString date;

9.2 工具执行失败,导致应用崩溃

解决方案:

确保配置了异常处理器:

@BeanToolExecutionExceptionProcessortoolExecutionExceptionProcessor(){returnnewDefaultToolExecutionExceptionProcessor(false);// false 表示不抛出异常}

9.3 大模型陷入无限循环,不断调用工具

可能的原因:

  1. 参数描述歧义 - 大模型理解错了参数含义
  2. 工具描述过于宽泛 - 大模型认为所有问题都需要这个工具
  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 机制打破了大模型与外部世界的隔阂,它让大模型:

  1. 能够感知工具的存在 - 通过 @Tool 注解和 description
  2. 能够正确选择工具 - 通过清晰的参数描述和工具职责
  3. 能够自主调用工具 - 框架自动处理复杂的 Tool Calling 流程
  4. 能够基于工具结果进行推理 - 完整的对话历史维护
  5. 能够应对工具执行失败 - 异常处理和容错机制

10.2 自动执行 vs 手动控制的选择

维度自动执行手动控制
代码复杂度极低较高
控制精度低(不可控)高(完全控制)
适用场景简单、可信任的工具复杂、需要监控的工具
推荐使用开发初期、快速原型生产环境、关键操作

选择建议:

  • 默认使用自动执行模式,代码简洁
  • 如果需要特殊处理(审核、持久化、限流等),升级到手动控制模式
  • 不要为了可控性而过度设计,简单就是最好的设计

10.3 参数描述的黄金法则

好的参数描述有三个特征:

  1. 清晰 - 用自然语言清楚地说明字段的含义
  2. 具体 - 提供具体的格式示例和取值范围
  3. 完整 - 说明所有的约束条件和特殊情况
// ✅ 优秀的参数描述@JsonPropertyDescription("查询的日期范围,格式为 YYYY-MM-DD。"+"示例:2025-03-31。"+"约束:不能查询过去 90 天之前的数据,不能查询未来数据。"+"如果不指定,默认查询今天的数据")@ToolParam(required =false)privateString queryDate;

10.4 走向 AI Agent 的下一步

掌握了 Tool Calling,你已经拥有了构建真正 AI Agent 的基础。下一步可以探索:

  1. 多工具协作 - 设计工具系统,让多个工具协同工作
  2. 工具组合 - 复杂任务分解为多个工具的组合调用
  3. 工具优先级 - 不同情况下选择不同的工具
  4. 工具链路优化 - 监控和优化工具的执行效率
  5. Agent 框架 - 集成 Spring AI 的 Agent 框架,实现完整的自主代理

十一、参考资源

在这里插入图片描述

Read more

2. Linux下FFmpeg C++音视频解码+推流开发

2. Linux下FFmpeg C++音视频解码+推流开发

前言 已经掌握FFmpeg命令行基础,现在想深入Linux下C++开发音视频解码、视频推流,这份教程完全贴合你的需求: ✅ 全程基于Linux + C++ 环境,所有代码可直接在Ubuntu/CentOS等发行版编译运行; ✅ 从「FFmpeg开发环境搭建(源码编译,带完整开发库)」→「核心结构体拆解」→「解码完整流程」→「推流完整流程」→「解码+推流一体化实战」,层层深入,无跳步; ✅ 所有代码都是工业级可运行版本,逐行注释核心逻辑,重点讲解「资源管理、错误处理、内存泄漏规避」(C++开发核心痛点); ✅ 详细拆解FFmpeg核心概念(时间基、AVPacket/AVFrame、编码器上下文),不仅教“怎么写”,还教“为什么这么写”; ✅ 覆盖RTMP推流(最常用)、RTSP推流,以及「硬解码/软解码」「推流卡顿优化」等进阶点,满足实际开发需求。 ✅ 一、前置准备:

By Ne0inhk
【C++】多态到底难在哪?虚函数表 + 动态绑定,一篇吃透底层逻辑!

【C++】多态到底难在哪?虚函数表 + 动态绑定,一篇吃透底层逻辑!

【C++】多态到底难在哪?虚函数表 + 动态绑定,一篇吃透底层逻辑! * 摘要 * 目录 * 一、多态的概念 * 二、多态的定义和实现 * 1. 多态的构成必要条件 * 2. 虚函数(virtual) * 2.1 虚函数的重写 / 覆盖 * 2.2 重写 / 覆盖 的例外(协变) * 2.3 重写析构函数的重要性 * 2.4 析构函数重写成虚函数的原理 * 2.5 C++11 的 override 和 final * 3. 重载 / 重写 / 隐藏的对比 * 三、抽象类 * 1. 抽象类 * 1.1

By Ne0inhk
【C++初阶】:C++入门相关知识(3):引用 & inline内联函数 & nullptr相关概念

【C++初阶】:C++入门相关知识(3):引用 & inline内联函数 & nullptr相关概念

🎈主页传送门:良木生香 🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》《鼠鼠的C++学习之路》 🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离 前言:在上一篇文章中,我们学习了C++的输入输出,缺省参数以及函数重载,这些都是C++入门必备的基础知识,那么在这篇文章中,我们就要来学习剩下C++其他的基础知识,那就是引用、inline、以及nullptr这些知识。 一、引用 1.1、引用的概念和定义 引用不是定义一个新变量,而是给已经存在的变量起一个别名,那么编译器就不会为别名重新开辟空间,它和引用变量共同使用同一块空间。就好比我们把土豆称为马铃薯,番茄称为西红柿一样,都是取了一个新的别名,但是东西是同一个东西,所以引用的语法如下: 类型& 别名 = 变量 使用方法如下: int a = 10; int&

By Ne0inhk