大模型开发 - 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

微信小程序虚拟支付整合thinkphp核心实现 你的小程序如有开通会员等则为虚拟类型 要使用虚拟支付了 要不然判定为违规

微信小程序虚拟支付整合thinkphp核心实现 你的小程序如有开通会员等则为虚拟类型 要使用虚拟支付了 要不然判定为违规

小程序虚拟支付业务管理规范更新公告2026-02-27 各位开发者: 微信小程序现已全面支持iOS端虚拟支付服务,为虚拟支付业务相关的开发者提供更广阔的用户覆盖。目前iOS端虚拟支付享受15%优惠费率,极大降低开发者的运营成本。 为保障用户权益,提高交易安全,开发者在小程序内提供的虚拟商品、购买和支付现均需接入小程序虚拟支付。 若你的小程序内涉及虚拟支付业务,请在4月1日前全终端(包括iOS端、安卓端、Windows与鸿蒙端)接入虚拟支付,到期未接入将被判定为违规,根据违规程度将对该小程序采取风险提醒、限制功能直至暂停或终止提供服务等措施,请广大开发者及时对照以下接入指引、运营规范等文件业务,确保合规经营。 什么是虚拟支付业务:虚拟支付业务是指购买非实物商品,比如:VIP会员、充值代币、录制课程、录制音频视频等虚拟产品。 接入指引:小程序虚拟支付接入指引 运营规范:小程序虚拟支付行为运营规范 v 基于微信虚拟支付文档,你需要实现以下关键服务器API。所有接口请求方式均为POST,Content-Type: application/json,且需在URL中携带access_toke

By Ne0inhk
构建基于Go语言的高性能命令行AI对话客户端:从环境部署到核心实现

构建基于Go语言的高性能命令行AI对话客户端:从环境部署到核心实现

前言 在现代软件开发领域,Go语言凭借其卓越的并发处理能力、静态类型安全以及高效的编译速度,已成为构建命令行工具(CLI)的首选语言之一。本文将详细阐述如何在Ubuntu Linux环境下部署Go开发环境,并结合蓝耘(Lanyun)提供的DeepSeek大模型API,手写一个支持多轮对话、上下文记忆的智能终端聊天工具。 一、 基础运行环境的准备与构建 任何上层应用的稳健运行都离不开坚实的底层系统支持。本次部署的目标环境为Ubuntu LTS系列(20.04/22.04/24.04),这些长期支持版本保证了系统库的稳定性与安全性。硬件层面,建议配置至少1GB的内存与5GB的磁盘空间,以满足编译器运行及依赖包缓存的需求。 1. 系统包索引更新与系统升级 在进行任何开发工具安装之前,首要任务是确保操作系统的软件包索引与现有软件处于最新状态。这不仅能修复已知的安全漏洞,还能避免因依赖库版本过旧导致的编译错误。 执行系统更新操作: sudoapt update &&sudoapt upgrade -y 该指令分为两部分:apt update 用于从软件源服务器获取最新的软件包列

By Ne0inhk

PostgreSQL(pgSQL)常用操作

目录 一、数据库管理(创建、连接、删除等) 1. 创建数据库 2. 连接数据库 (1)命令行方式(psql 工具) (2)SQL 内部切换数据库 3. 删除数据库 二、表操作(创建、修改、删除、索引等) 1. 创建表 2. 修改表(ALTER TABLE) 3. 删除表 4. 查看表信息 5. 创建索引(优化查询速度) 三、数据操作(增、删、改、查) 1. 插入数据(INSERT) 2. 更新数据(UPDATE)

By Ne0inhk
【强化学习】深度解析 GRPO:从原理到实践的全攻略

【强化学习】深度解析 GRPO:从原理到实践的全攻略

文章目录 * 一、提出背景 * 二、核心思想 * 2.1 组内相对奖励 * 2.2 去价值网络设计 * 2.3 稳定优化机制 * 2.4 PPO vs GRPO * 三、算法原理 * 3.1 生成响应(Generating completions ) * 3.2 计算优势值(Computing the advantage) * 3.3 估计KL散度(Estimating the KL divergence) * 3.4 计算损失(Computing the loss) * 3.5 重要性采样*(Importance Sampling)

By Ne0inhk