Spring AI Tool Calling(工具调用)详解——让大模型拥有“动手能力“

Spring AI Tool Calling(工具调用)详解——让大模型拥有“动手能力“

定位:本文是 Spring AI 系列博客之一。我们将从为什么需要工具调用讲起,结合 Spring AI 官方文档实战代码,一步步带你理解 Tool Calling 的原理、用法和进阶技巧。即使你是初学者,也能看懂。
希望对于大家学习Spring AI 有帮助

目录


1. 什么是 Tool Calling?

官方定义

Spring AI 官方文档的描述:

Tool calling (also known as function calling) is a common pattern in AI applications allowing a model to interact with a set of APIs, or tools, augmenting its capabilities.

Spring AI 官方文档 - Tool Calling

翻译:工具调用(也称为函数调用)是 AI 应用中的一种常见模式,它允许模型与一组 API(即工具)进行交互,从而扩展模型的能力

通俗理解

大模型(比如 DeepSeek、GPT)本质上就是一个"文字处理器"——它只能根据已有的训练数据来生成文字。它不能上网、不能查数据库、不能调接口、不能操作你的系统

但是,如果你给它一套"工具",告诉它"需要的时候可以用这些工具",它就能在对话过程中主动请求调用工具,拿到结果后再生成最终回答。

生活比喻

想象你在和一个非常聪明的朋友聊天,但他被关在一个没有窗户的房间里:

你: "今天北京天气怎么样?" 朋友: "我不知道,我看不到外面的天气..." —— 现在你给他一部手机(工具)—— 你: "今天北京天气怎么样?" 朋友: (拿起手机,打开天气 App,查到 "北京:晴,25°C") 朋友: "北京今天天气晴朗,气温 25°C,适合出门!" 

工具调用就是给大模型一部"手机"——让它在需要的时候能"打电话"去获取信息或执行操作。


2. 为什么需要 Tool Calling?

大模型的两大局限

局限说明举例
没有实时信息模型的知识截止于训练数据问"今天几号",它不知道
不能执行操作模型只能生成文字,不能"动手"让它"帮我发个邮件",它做不到

Tool Calling 解决的两类问题

Spring AI 官方文档将工具分为两大类:

1. 信息获取型(Information Retrieval)

工具用于从外部数据源获取信息,弥补模型知识的不足。

  • 查询当前时间、天气
  • 搜索数据库中的记录
  • 调用外部 API 获取实时数据
  • 在 RAG(检索增强生成)场景中检索文档
2. 执行动作型(Taking Action)

工具用于在系统中执行操作,实现任务自动化。

  • 发送邮件、短信
  • 在数据库中创建/修改记录
  • 提交表单、触发工作流
  • 预订机票、设置闹钟

一个关键的安全概念

Spring AI 官方文档特别强调:

Even though we typically refer to tool calling as a model capability, it is actually up to the client application to provide the tool calling logic. The model can only request a tool call and provide the input arguments, whereas the application is responsible for executing the tool call. The model never gets access to any of the APIs provided as tools.

翻译:虽然我们说"模型调用工具",但实际上模型并没有直接访问任何 API 的权限。模型只能"请求"调用工具并提供参数,而真正执行工具的是你的应用程序

这就像:你的朋友(模型)说"帮我查一下北京天气",但实际上是(应用程序)去查的,然后把结果告诉他。模型本身永远不会直接接触你的 API、数据库或任何外部系统——这是一个重要的安全保障


3. Tool Calling 的工作原理

完整的 6 步流程

┌─────────┐ ┌──────────┐ ┌──────────┐ │ 你的 │ │ Spring │ │ AI 模型 │ │ 应用 │ │ AI │ │ (DeepSeek│ │ 程序 │ │ 框架 │ │ /GPT) │ └────┬────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ ① 发送请求 + 工具定义 │ │ │ "今天几号?" │ │ │ 工具: getCurrentDateTime() │ │ │─────────────────────────────>│ 发送给模型 │ │ │──────────────────────────────>│ │ │ │ │ │ ② 模型决定调用工具 │ │ │ "我需要调用 │ │ │ getCurrentDateTime()" │ │ │<──────────────────────────────│ │ │ │ │ ③ 框架执行工具 │ │ │ 调用 getCurrentDateTime() │ │ │<─────────────────────────────│ │ │ │ │ │ ④ 返回工具结果 │ │ │ "2025-02-17T15:30:00" │ │ │─────────────────────────────>│ │ │ │ │ │ │ ⑤ 把结果发给模型 │ │ │──────────────────────────────>│ │ │ │ │ │ ⑥ 模型生成最终回答 │ │ │ "今天是 2025 年 2 月 17 日" │ │ │<──────────────────────────────│ │ │ │ │ 返回最终答案 │ │ │<─────────────────────────────│ │ │ │ │ 

用文字总结这 6 步

步骤谁在做做什么
你的应用 → Spring AI发送用户问题 + 工具定义(工具名、描述、参数格式)
AI 模型分析问题,决定需要调用哪个工具,返回工具名 + 参数
Spring AI 框架根据工具名找到对应的工具实现,执行它
你的工具方法返回执行结果(比如当前时间)
Spring AI 框架 → AI 模型把工具结果发回给模型
AI 模型利用工具结果生成最终的自然语言回答

重点:整个过程中,步骤 ③ 和 ④ 是你需要写的代码(定义工具方法),其余步骤 Spring AI 框架自动帮你完成。


4. 快速入门:第一个工具

4.1 信息获取型工具——获取当前时间

AI 模型不知道"现在是几点",但我们可以写一个工具告诉它:

importjava.time.LocalDateTime;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.context.i18n.LocaleContextHolder;classDateTimeTools{@Tool(description ="Get the current date and time in the user's timezone")StringgetCurrentDateTime(){returnLocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}}

代码解读

  • @Tool 注解:告诉 Spring AI “这个方法是一个工具”
  • description:告诉 AI 模型"这个工具是干什么的"——这个描述非常重要,模型根据它来判断什么时候该调用这个工具
  • 方法体:实际的工具逻辑——返回当前时间

然后在调用时把工具传给模型:

ChatModel chatModel =...String response =ChatClient.create(chatModel).prompt("What day is tomorrow?").tools(newDateTimeTools())// ← 把工具传给模型.call().content();System.out.println(response);// 输出:Tomorrow is 2025-02-18.

如果不提供工具,同样的问题模型会回答:

I am an AI and do not have access to real-time information. Please provide the current date so I can accurately determine what day tomorrow will be. 

4.2 执行动作型工具——设置闹钟

除了获取信息,工具还可以执行操作:

importjava.time.LocalDateTime;importjava.time.format.DateTimeFormatter;importorg.springframework.ai.tool.annotation.Tool;importorg.springframework.ai.tool.annotation.ToolParam;classDateTimeTools{@Tool(description ="Get the current date and time in the user's timezone")StringgetCurrentDateTime(){returnLocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}@Tool(description ="Set a user alarm for the given time, provided in ISO-8601 format")voidsetAlarm(@ToolParam(description ="Time in ISO-8601 format")String time){LocalDateTime alarmTime =LocalDateTime.parse(time,DateTimeFormatter.ISO_DATE_TIME);System.out.println("Alarm set for "+ alarmTime);}}

调用:

String response =ChatClient.create(chatModel).prompt("Can you set an alarm 10 minutes from now?").tools(newDateTimeTools()).call().content();

这个请求会触发模型连续调用两个工具

  1. 先调用 getCurrentDateTime() → 知道现在是几点
  2. 再调用 setAlarm("2025-02-17T15:40:00") → 设置 10 分钟后的闹钟

模型自己规划了工具调用顺序——它知道要先获取当前时间,才能计算 10 分钟后是几点。


5. 定义工具的三种方式

Spring AI 提供了三种方式来定义工具,适用于不同场景:

5.1 声明式:@Tool 注解(最常用,推荐)

在方法上加 @Tool 注解,最简单直观:

classDateTimeTools{@Tool(description ="Get the current date and time in the user's timezone")StringgetCurrentDateTime(){returnLocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();}}

@Tool 注解的属性

属性说明默认值
name工具名称,模型用这个名字来调用工具方法名
description工具描述,非常重要,模型根据描述判断何时使用方法名(建议手动写)
returnDirect是否把结果直接返回给用户(而不是回传给模型)false
resultConverter自定义结果转换器默认 JSON 序列化

关于方法的要求

  • 可以是 publicprotectedpackage-privateprivate
  • 可以是静态方法或实例方法
  • 参数类型支持:基本类型、POJO、枚举、List、数组、Map 等
  • 返回类型支持:大部分类型,包括 void
  • 不支持OptionalCompletableFutureMonoFlux 等异步/响应式类型

5.2 编程式:MethodToolCallback(灵活,适合第三方类)

当你无法修改工具类的源码(比如第三方库),或者需要更精细地控制工具定义时,可以使用编程式:

// 工具类(注意:没有 @Tool 注解)classWeatherTools{StringgetCurrentWeatherByCityName(String cityName){// 查询天气的逻辑return"晴天,25°C";}}// 手动构建 ToolCallbackMethod method =ReflectionUtils.findMethod(WeatherTools.class,"getCurrentWeatherByCityName",String.class);ToolCallback toolCallback =MethodToolCallback.builder().toolDefinition(ToolDefinitions.builder(method).description("根据给定的城市名称, 获取城市当前的天气").build()).toolMethod(method).toolObject(newWeatherTools()).build();

MethodToolCallback.Builder 的关键属性

属性说明是否必须
toolDefinition工具定义(名称 + 描述 + 参数 Schema)✅ 必须
toolMethod工具方法的 Method 反射对象✅ 必须
toolObject工具方法所在对象的实例(静态方法可省略)实例方法必须
toolMetadata额外配置(如 returnDirect可选
toolCallResultConverter自定义结果转换器可选

5.3 动态式:@Bean + Function(Spring Bean 方式)

把工具定义为 Spring Bean,Spring AI 在运行时自动解析:

// 定义 Function Bean@Configuration(proxyBeanMethods =false)classWeatherToolConfig{@Bean@Description("Get the weather in location")Function<WeatherRequest,WeatherResponse>currentWeather(){return request ->newWeatherResponse(30.0,Unit.C);}}// 入参和出参必须是 POJOpublicrecordWeatherRequest(@ToolParam(description ="The name of a city or a country")String location,Unit unit ){}publicrecordWeatherResponse(double temp,Unit unit){}publicenumUnit{C,F}

使用时通过工具名称(Bean 名称)来引用:

ChatClient.create(chatModel).prompt("What's the weather like in Copenhagen?").toolNames("currentWeather")// ← 传工具名称,而不是工具对象.call().content();

注意事项

  • Bean 名称就是工具名称
  • @Description 注解提供工具描述
  • 入参和出参必须是 public 的 POJO,不支持基本类型和集合类型
  • 这种方式类型安全性较弱(运行时解析)

三种方式对比

声明式 @Tool编程式 MethodToolCallback动态式 @Bean
简单程度⭐⭐⭐ 最简单⭐ 最复杂⭐⭐ 中等
适用场景自己写的工具类第三方库的方法需要 Spring Bean 注入的场景
参数类型支持丰富支持丰富仅支持 POJO
类型安全✅ 编译时✅ 编译时❌ 运行时
需要修改源码需要加注解不需要不需要
推荐程度✅ 首选特定场景使用特定场景使用

6. 工具规范(Tool Specification)

在前面的章节中,我们学习了如何通过 @ToolMethodToolCallback@Bean 三种方式来定义工具。这一节深入讲解 Spring AI 工具系统的底层设计——即这些工具在框架内部是如何被描述、组织和转换的。

官方文档的说法:In Spring AI, tools are modeled via the ToolCallback interface.

翻译:在 Spring AI 中,工具通过 ToolCallback 接口进行建模。

6.1 ToolCallback——工具的核心接口

所有工具最终都是一个 ToolCallback。不管你用 @Tool 注解还是手动构建 MethodToolCallback,最终都会变成一个实现了 ToolCallback 接口的对象。

publicinterfaceToolCallback{/** 工具定义——告诉模型这个工具叫什么、干什么、参数是什么格式 */ToolDefinitiongetToolDefinition();/** 工具元数据——额外配置信息(如 returnDirect) */ToolMetadatagetToolMetadata();/** 执行工具(无上下文) */Stringcall(String toolInput);/** 执行工具(带上下文) */Stringcall(String toolInput,ToolContext toolContext);}

通俗理解ToolCallback 就是一个"工具包装盒",它包含两部分信息:

  1. 工具说明书ToolDefinition)——名字、描述、参数格式,给模型看的
  2. 工具本体call 方法)——实际执行逻辑,给应用程序调用的

Spring AI 提供了两个内置实现:

实现类对应的工具定义方式
MethodToolCallback方法作为工具(@Tool 或编程式)
FunctionToolCallback函数作为工具(@Bean + Function

6.2 ToolDefinition——工具的"说明书"

ToolDefinition 是工具的定义信息,它决定了模型看到的工具长什么样

publicinterfaceToolDefinition{/** 工具名称——在同一次请求中必须唯一 */Stringname();/** 工具描述——模型根据这个描述来判断什么时候该用这个工具 */Stringdescription();/** 参数的 JSON Schema——告诉模型参数的格式、类型、约束 */StringinputSchema();}
自动生成 vs 手动构建

自动生成:当你使用 @Tool 注解时,Spring AI 会自动从方法签名中提取这三项信息:

@Tool(description ="获取当前时间")// → descriptionStringgetCurrentDateTime(){// → name = "getCurrentDateTime"...// → inputSchema = {} (无参数)}

手动构建:你也可以用 ToolDefinition.Builder 手动指定:

ToolDefinition toolDefinition =ToolDefinition.builder().name("currentWeather").description("Get the weather in location").inputSchema(""" { "type": "object", "properties": { "location": { "type": "string" }, "unit": { "type": "string", "enum": ["C", "F"] } }, "required": ["location", "unit"] } """).build();
从方法自动生成 ToolDefinition
// 方式一:完全自动(使用方法名和 @Tool 注解的信息)Method method =ReflectionUtils.findMethod(DateTimeTools.class,"getCurrentDateTime");ToolDefinition toolDefinition =ToolDefinitions.from(method);// 方式二:部分自定义(覆盖名称和描述,但参数 Schema 仍然自动生成)ToolDefinition toolDefinition =ToolDefinitions.builder(method).name("currentDateTime").description("Get the current date and time in the user's timezone").inputSchema(JsonSchemaGenerator.generateForMethodInput(method)).build();

6.3 JSON Schema——模型理解参数的关键

当你给模型提供一个工具时,模型需要知道参数长什么样才能正确调用。Spring AI 通过 JsonSchemaGenerator 类自动为工具参数生成 JSON Schema

什么是 JSON Schema?

JSON Schema 就是一份"数据格式说明"。比如下面这个工具方法:

@Tool(description ="设置闹钟")voidsetAlarm(@ToolParam(description ="ISO-8601 格式的时间")String time){...}

Spring AI 会自动生成这样的 JSON Schema:

{"type":"object","properties":{"time":{"type":"string","description":"ISO-8601 格式的时间"}},"required":["time"]}

模型看到这个 Schema 后就知道:调用 setAlarm 需要传一个 time 参数,类型是字符串,格式应该是 ISO-8601。

JSON Schema 的生成规则
你写的代码Schema 中的效果
String time"type": "string"
int count"type": "integer"
boolean flag"type": "boolean"
List<String> tags"type": "array", "items": {"type": "string"}
enum Unit { C, F }"type": "string", "enum": ["C", "F"]
@ToolParam(description = "...")"description": "..."
@ToolParam(required = false)不在 "required" 数组中
描述参数的四种注解

你可以用以下注解来为参数添加描述(优先级从高到低):

注解来源适用场景
@ToolParam(description = "...")Spring AI推荐,方法参数描述
@JsonClassDescription("...")JacksonPOJO 类级别描述
@JsonPropertyDescription("...")JacksonPOJO 字段级别描述
@Schema(description = "...")Swagger项目已引入 Swagger 时
标记可选参数的四种注解

默认所有参数都是必填(required),用以下注解可标记为可选(优先级从高到低):

注解来源
@ToolParam(required = false)Spring AI
@JsonProperty(required = false)Jackson
@Schema(required = false)Swagger
@NullableSpring Framework
这些注解支持递归——如果你的参数是一个嵌套的 POJO,内层字段也可以用这些注解来描述。

6.4 Result Conversion——工具结果的转换

工具执行完毕后,结果需要转换成字符串才能发回给模型。Spring AI 通过 ToolCallResultConverter 接口来处理这个转换。

@FunctionalInterfacepublicinterfaceToolCallResultConverter{/** 将工具返回的对象转换为字符串 */Stringconvert(@NullableObject result,@NullableType returnType);}
默认行为

默认使用 DefaultToolCallResultConverter,它会把结果序列化为 JSON 字符串(通过 Jackson)。

// 你的工具方法返回一个对象@Tool(description ="查询客户信息")CustomergetCustomerInfo(Long id){returnnewCustomer(42L,"张三","[email protected]");}// 默认转换结果(JSON 字符串):// {"id": 42, "name": "张三", "email": "[email protected]"}// 这个 JSON 字符串会被发回给模型
自定义结果转换器

如果默认的 JSON 转换不满足需求(比如你想返回 XML 格式、或者只返回部分字段),可以自定义:

// 自定义转换器publicclassSimpleCustomerConverterimplementsToolCallResultConverter{@OverridepublicStringconvert(Object result,Type returnType){if(result instanceofCustomer c){return"客户姓名:"+ c.getName()+",邮箱:"+ c.getEmail();}return result.toString();}}// 在 @Tool 注解中指定@Tool(description ="查询客户信息", resultConverter =SimpleCustomerConverter.class)CustomergetCustomerInfo(Long id){return customerRepository.findById(id);}
什么时候需要自定义?
场景是否需要自定义
返回简单对象(String、数字、POJO)❌ 默认 JSON 就够了
返回的对象太大,想只取部分字段✅ 自定义,减少 Token 消耗
想返回特定格式(如 Markdown 表格)✅ 自定义
返回敏感信息需要脱敏✅ 自定义

6.5 工具规范的整体结构图

 ToolCallback(工具的完整表示) ┌──────────────────────────────┐ │ │ ┌───────────────┤ getToolDefinition() │ │ │ → 工具的"说明书" │ │ │ │ │ ┌────────────┤ getToolMetadata() │ │ │ │ → 额外配置(returnDirect 等) │ │ │ │ │ │ │ │ call(input) │ │ │ │ call(input, context) │ │ │ │ → 实际执行工具 │ │ │ └──────────────────────────────┘ │ │ │ │ ToolMetadata │ │ ┌──────────────────┐ │ └───────────→│ returnDirect │ 是否直接返回结果给用户 │ └──────────────────┘ │ │ ToolDefinition(给模型看的) │ ┌──────────────────┐ └──────────────→│ name() │ 工具名称 │ description() │ 工具描述 │ inputSchema() │ 参数的 JSON Schema └──────────────────┘ │ JsonSchemaGenerator 自动从方法签名生成 工具执行后 ──→ ToolCallResultConverter ──→ 字符串结果 ──→ 发回模型 

7. 使用工具的两种 API

定义好工具后,你可以通过 ChatClientChatModel 来使用它们。

7.1 通过 ChatClient(推荐,更高层抽象)

声明式工具(@Tool)
// 单次请求中使用ChatClient.create(chatModel).prompt("What day is tomorrow?").tools(newDateTimeTools())// ← 传工具类实例.call().content();
编程式工具(MethodToolCallback)
ToolCallback toolCallback =...;// 前面构建的ChatClient.create(chatModel).prompt("What day is tomorrow?").toolCallbacks(toolCallback)// ← 传 ToolCallback 对象.call().content();
动态式工具(@Bean)
ChatClient.create(chatModel).prompt("What's the weather like in Copenhagen?").toolNames("currentWeather")// ← 传工具名称(Bean 名称).call().content();
设置默认工具

如果每次请求都需要同一组工具,可以设置为默认:

ChatClient chatClient =ChatClient.builder(chatModel).defaultTools(newDateTimeTools())// 声明式默认工具// .defaultToolCallbacks(toolCallback) // 编程式默认工具// .defaultToolNames("currentWeather") // 动态式默认工具.build();// 之后每次请求自动带上工具,不用再手动传 chatClient.prompt("What day is tomorrow?").call().content();
注意:如果同时设置了默认工具和请求级工具,请求级工具会完全覆盖默认工具(不是合并)。

7.2 通过 ChatModel(更底层)

// 声明式ToolCallback[] dateTimeTools =ToolCallbacks.from(newDateTimeTools());ChatOptions chatOptions =ToolCallingChatOptions.builder().toolCallbacks(dateTimeTools).build();Prompt prompt =newPrompt("What day is tomorrow?", chatOptions); chatModel.call(prompt);// 动态式ChatOptions chatOptions =ToolCallingChatOptions.builder().toolNames("currentWeather").build();Prompt prompt =newPrompt("What's the weather?", chatOptions); chatModel.call(prompt);

ChatClient vs ChatModel 使用工具时的区别

ChatClientChatModel
抽象层次高层,更简洁底层,更灵活
传声明式工具.tools(new XxxTools())ToolCallbacks.from(...) + ToolCallingChatOptions
传编程式工具.toolCallbacks(callback)ToolCallingChatOptions.builder().toolCallbacks(...)
传动态式工具.toolNames("name")ToolCallingChatOptions.builder().toolNames(...)
推荐✅ 日常开发首选需要精细控制时使用

8. @ToolParam——告诉模型参数怎么填

当工具方法有参数时,你需要告诉模型每个参数的含义。@ToolParam 注解就是干这个的。

基本用法

classDateTimeTools{@Tool(description ="Set a user alarm for the given time")voidsetAlarm(@ToolParam(description ="Time in ISO-8601 format")String time){LocalDateTime alarmTime =LocalDateTime.parse(time,DateTimeFormatter.ISO_DATE_TIME);System.out.println("Alarm set for "+ alarmTime);}}

@ToolParam 的属性:

属性说明默认值
description参数描述,告诉模型这个参数的格式、含义、允许值等
required参数是否必填true

可选参数

默认情况下,所有参数都是必填的。如果某个参数是可选的:

classCustomerTools{@Tool(description ="Update customer information")voidupdateCustomerInfo(Long id,String name,@ToolParam(required =false)String email // ← 可选参数){System.out.println("Updated info for customer with id: "+ id);}}

你也可以用 @Nullable 标注来表示可选:

voidupdateCustomerInfo(Long id,String name,@NullableString email){...}

参数描述的替代注解

除了 @ToolParam,还可以使用:

注解来源用法
@ToolParam(description = "...")Spring AI推荐
@Schema(description = "...")Swagger如果项目已引入 Swagger
@JsonPropertyDescription("...")Jackson函数式工具的 POJO 字段

为什么参数描述很重要?

模型在决定如何调用工具时,完全依赖你提供的工具描述和参数描述。如果描述不清楚,模型可能:

  • 不该调用工具时却调用了
  • 该调用工具时没调用
  • 传了错误格式的参数
建议:工具描述和参数描述要写得尽量详细和准确。不要写"时间",要写"Time in ISO-8601 format, e.g. 2025-02-17T15:30:00"。

9. ToolContext——给工具传递额外上下文

什么场景需要?

有时候,工具需要一些模型不知道的信息——比如当前登录用户的 ID、租户标识等。这些信息不应该让模型来提供(模型也提供不了),而是由你的应用程序传递。

用法

工具方法中声明 ToolContext 参数

classCustomerTools{@Tool(description ="Retrieve customer information")CustomergetCustomerInfo(Long id,ToolContext toolContext){// 从 ToolContext 中获取租户 ID(这个信息是应用程序传的,不是模型传的)String tenantId = toolContext.getContext().get("tenantId");return customerRepository.findById(id, tenantId);}}

调用时通过 .toolContext() 传入上下文数据

// 通过 ChatClientString response =ChatClient.create(chatModel).prompt("Tell me more about the customer with ID 42").tools(newCustomerTools()).toolContext(Map.of("tenantId","acme"))// ← 传入上下文.call().content();// 通过 ChatModelChatOptions chatOptions =ToolCallingChatOptions.builder().toolCallbacks(ToolCallbacks.from(newCustomerTools())).toolContext(Map.of("tenantId","acme"))// ← 传入上下文.build();Prompt prompt =newPrompt("Tell me about customer 42", chatOptions); chatModel.call(prompt);

重要注意事项

⚠️ 如果你的工具方法声明了 ToolContext 参数,调用时就必须通过 .toolContext() 提供上下文数据,否则会报错

java.lang.IllegalArgumentException: ToolContext is required by the method as an argument 

这正是你之前遇到的那个错误!如果你的工具方法不需要额外上下文,就不要声明 ToolContext 参数。


10. returnDirect——工具结果直接返回给用户

默认行为

默认情况下,工具的执行结果会先发回给模型,让模型基于结果生成最终回答:

用户 → 模型 → 调用工具 → 工具结果 → 回传给模型 → 模型生成回答 → 用户 

什么时候需要 returnDirect?

有些场景你不需要模型再"加工"工具结果,想直接把结果返回给用户:

  • RAG 场景:工具检索到的文档直接返回,不需要模型再处理
  • 终止推理循环:工具执行后就结束对话,不需要模型继续思考

用法

classCustomerTools{@Tool(description ="Retrieve customer information", returnDirect =true)// ← 直接返回CustomergetCustomerInfo(Long id){return customerRepository.findById(id);}}

设置 returnDirect = true 后,工具结果跳过模型,直接返回给调用方。


11. 工具执行的两种模式

11.1 框架控制模式(默认)

这是默认行为——Spring AI 框架自动处理一切:

① 你发请求 + 工具定义 ② 模型返回工具调用请求 ③ 框架自动执行工具 ← 自动的 ④ 框架把结果发回模型 ← 自动的 ⑤ 模型生成最终回答 

你只需要写工具方法,其他的 Spring AI 全部帮你搞定。大多数场景用这个就够了。

11.2 用户控制模式

如果你需要在工具执行的过程中插入自定义逻辑(比如审批、日志、人工确认),可以关闭框架自动执行:

ChatOptions chatOptions =ToolCallingChatOptions.builder().toolCallbacks(newCustomerTools()).internalToolExecutionEnabled(false)// ← 关闭自动执行.build();Prompt prompt =newPrompt("Tell me about customer 42", chatOptions);ChatResponse chatResponse = chatModel.call(prompt);// 手动控制工具执行循环ToolCallingManager toolCallingManager =ToolCallingManager.builder().build();while(chatResponse.hasToolCalls()){// 你可以在这里加日志、审批、过滤等自定义逻辑System.out.println("模型请求调用工具,正在执行...");ToolExecutionResult toolExecutionResult = toolCallingManager.executeToolCalls(prompt, chatResponse); prompt =newPrompt(toolExecutionResult.conversationHistory(), chatOptions); chatResponse = chatModel.call(prompt);}System.out.println(chatResponse.getResult().getOutput().getText());
框架控制(默认)用户控制
设置默认就是internalToolExecutionEnabled(false)
工具执行框架自动执行你手动调用 ToolCallingManager
适用场景大多数场景需要人工审批、自定义日志、复杂流程
代码量

12. 异常处理

工具执行失败怎么办?

当工具方法抛出异常时,Spring AI 通过 ToolExecutionExceptionProcessor 来处理。默认行为:

  • RuntimeException:将错误信息发送回模型,让模型基于错误信息继续对话
  • 检查异常和 Error(如 IOExceptionOutOfMemoryError):直接抛出,由调用方处理

配置异常处理行为

通过配置文件:

# application.ymlspring:ai:tools:throw-exception-on-error:true# true: 所有异常都抛出; false(默认): RuntimeException 发回模型

或者自定义 Bean:

@BeanToolExecutionExceptionProcessortoolExecutionExceptionProcessor(){returnnewDefaultToolExecutionExceptionProcessor(true);// true = 始终抛出异常}

什么时候选哪种?

策略适用场景
默认(发回模型)模型可以"重试"或"换一种方式"完成任务
始终抛出不希望模型继续处理错误,而是让应用程序统一处理

13. 工具解析(Tool Resolution)

前面我们学的都是手动传工具——通过 .tools(new XxxTools()).toolCallbacks(callback) 把工具传给 ChatClient。但 Spring AI 还支持另一种方式:通过工具名称动态解析

13.1 什么是工具解析?

工具解析就是:你只告诉 Spring AI 工具的名字,框架会自动根据名字找到对应的 ToolCallback

// 不传工具对象,只传工具名称 chatClient.prompt("今天天气如何?").toolNames("currentWeather")// ← 只传名称.call().content();

Spring AI 会在运行时通过 ToolCallbackResolver 接口来解析这个名称,找到对应的工具。

13.2 ToolCallbackResolver 接口

publicinterfaceToolCallbackResolver{/** 根据工具名称解析出对应的 ToolCallback */@NullableToolCallbackresolve(String toolName);}

13.3 默认的解析策略

Spring AI 默认使用 DelegatingToolCallbackResolver,它委托给两个子解析器:

DelegatingToolCallbackResolver ├── SpringBeanToolCallbackResolver │ → 从 Spring 容器中查找 Function/Supplier/Consumer/BiFunction 类型的 Bean │ → 对应 @Bean + Function 动态式工具 │ └── StaticToolCallbackResolver → 从静态的 ToolCallback 列表中查找 → 自动收集应用上下文中所有 ToolCallback 类型的 Bean 

13.4 使用场景

场景用哪种方式
工具类就在你的代码里.tools(new XxxTools()) 手动传
工具是 Spring Bean(@Bean + Function.toolNames("beanName") 按名称解析
工具注册为 ToolCallback Bean.toolNames("toolName") 按名称解析
需要在运行时动态决定用哪些工具自定义 ToolCallbackResolver

13.5 自定义工具解析器

如果默认的解析策略不满足需求,你可以自定义:

@BeanToolCallbackResolvertoolCallbackResolver(List<FunctionCallback> toolCallbacks){StaticToolCallbackResolver staticResolver =newStaticToolCallbackResolver(toolCallbacks);returnnewDelegatingToolCallbackResolver(List.of(staticResolver));}
总结:工具解析是 toolNames() 方式的底层支撑。大多数场景下你不需要关心它——直接 .tools(new XxxTools()) 手动传就够了。当你使用 @Bean 动态式工具或需要运行时动态注册工具时,才会用到工具解析机制。

14. 实战案例:天气查询工具

以你的项目代码为例,完整演示从工具定义到调用的全过程。

场景

用户问"北京今天天气怎么样?",我们提供一个天气查询工具让模型调用。

方式一:声明式(@Tool)

定义工具

publicclassWeatherTools{@Tool(description ="根据给定的城市名称,获取城市当前的天气")StringgetCurrentWeatherByCityName(@ToolParam(description ="城市名称,例如:北京、上海、广州")String cityName ){switch(cityName){case"北京":return"北京今天天气: 晴空万里";case"上海":return"上海今天天气: 电闪雷鸣";case"广州":return"广州今天天气: 细雨蒙蒙";default:return"没有该城市的天气信息";}}}

在 ChatClient 中使用

@RequestMapping("/chat")@RestControllerpublicclassChatController{privateChatClient chatClient;publicChatController(DashScopeChatModel chatModel){this.chatClient =ChatClient.builder(chatModel).build();}@RequestMapping("/call")publicStringcall(String message){return chatClient.prompt().user(message).tools(newWeatherTools())// ← 声明式,传工具类实例.call().content();}}

方式二:编程式(MethodToolCallback)

WeatherTools 类上没有 @Tool 注解时,用编程式构建:

@RequestMapping("/call2")publicStringcall2(String message){// 1. 通过反射获取方法Method method =ReflectionUtils.findMethod(WeatherTools.class,"getCurrentWeatherByCityName",String.class);// 2. 手动构建 ToolCallbackToolCallback toolCallback =MethodToolCallback.builder().toolDefinition(ToolDefinitions.builder(method).description("根据给定的城市名称, 获取城市当前的天气").build()).toolMethod(method).toolObject(newWeatherTools()).build();// 3. 传给 ChatClientreturn chatClient.prompt().user(message).toolCallbacks(toolCallback)// ← 编程式,传 ToolCallback 对象.call().content();}

方式三:通过 ChatModel + ToolCallingChatOptions

@RequestMapping("/callByTool")publicStringcallByTool(String message){// 1. 从工具类生成 ToolCallback 数组ToolCallback[] weatherTools =ToolCallbacks.from(newWeatherTools());// 2. 构建 ChatOptionsChatOptions chatOptions =ToolCallingChatOptions.builder().toolCallbacks(weatherTools).build();// 3. 构建 Prompt 并调用Prompt prompt =newPrompt(message, chatOptions);return chatModel.call(prompt).getResult().getOutput().getText();}

15. 常见问题与排坑

问题一:ToolContext is required by the method as an argument

报错信息

java.lang.IllegalArgumentException: ToolContext is required by the method as an argument 

原因:工具方法声明了 ToolContext 参数,但调用时没有通过 .toolContext() 提供上下文数据。

解决方案

// 方案A:如果不需要 ToolContext,从方法参数中移除它@Tool(description ="Get current time")StringgetCurrentDateTime(){// ← 没有 ToolContext 参数returnLocalDateTime.now().toString();}// 方案B:如果需要 ToolContext,调用时提供它 chatClient.prompt().user(message).tools(newMyTools()).toolContext(Map.of("key","value"))// ← 提供 ToolContext.call().content();

问题二:模型不调用工具

可能原因

  1. 工具描述不够清楚 — 模型不知道什么时候该用这个工具
  2. 用户的问题和工具描述不匹配 — 模型判断不需要这个工具
  3. 模型不支持工具调用 — 有些小模型不支持 function calling

解决方案

  • 优化 @Tool(description = "...") 的描述,写得更具体
  • 确认你使用的模型支持工具调用(如 DeepSeek、GPT-4、通义千问等)

问题三:同名工具冲突

规则:同一次请求中,工具名称必须唯一。如果你传了两个同名的工具,会报错。

// ❌ 错误:两个类中都有 getCurrentDateTime 方法 chatClient.prompt().tools(newDateTimeTools(),newAnotherDateTimeTools())// 可能同名冲突.call().content();

解决方案:通过 @Tool(name = "customName") 给工具取不同的名字。

问题四:默认工具 vs 请求级工具

重要规则:如果同时设置了默认工具和请求级工具,请求级工具会完全覆盖默认工具,不是合并!

ChatClient chatClient =ChatClient.builder(chatModel).defaultTools(newDateTimeTools())// 默认工具.build();// 这次请求只有 WeatherTools,DateTimeTools 被覆盖了! chatClient.prompt().tools(newWeatherTools())// 请求级工具覆盖默认工具.call().content();

如果你两个都需要,就在请求级工具中一起传。

问题五:方法的参数类型不支持

Method as Tool 不支持的类型

  • Optional
  • CompletableFutureFuture
  • MonoFlux(响应式类型)
  • FunctionSupplierConsumer(函数式类型)

Function as Tool(@Bean 方式)不支持的类型

  • 基本类型(intString 等,必须包装成 POJO)
  • 集合类型(ListMapSet 等)

16. 总结

核心概念速查表

概念说明
Tool Calling让 AI 模型在对话中调用外部工具(API/方法)来扩展能力
@Tool声明式定义工具的注解,最简单常用
MethodToolCallback编程式定义工具,适合第三方类
@Bean + Function动态式定义工具,通过 Spring Bean 管理
ToolCallback工具的核心接口,包含工具定义 + 执行逻辑
ToolDefinition工具的"说明书"——名称、描述、参数 JSON Schema
JSON Schema工具参数的格式描述,由 JsonSchemaGenerator 自动生成
ToolCallResultConverter工具结果转换器,默认将结果序列化为 JSON 字符串
@ToolParam描述工具参数的注解,帮助模型理解怎么传参
ToolContext给工具传递应用层上下文(如用户ID、租户ID)
returnDirect工具结果跳过模型,直接返回给用户
ToolCallingManager管理工具执行生命周期的核心接口
ToolCallbackResolver工具解析器,根据名称动态查找 ToolCallback

选择工具定义方式的决策树

你能修改工具类的源码吗? ├── 能 → 用 @Tool 注解(声明式)✅ 推荐 └── 不能 → 工具方法的参数是 POJO 吗? ├── 是 → 用 @Bean + Function(动态式) └── 不是 → 用 MethodToolCallback(编程式) 

一句话总结

Tool Calling = 给大模型装上"手和脚"。你定义工具(写 Java 方法),模型决定何时调用(基于工具描述),Spring AI 负责串联整个流程(自动执行工具、传递结果)。三者分工明确,各司其职。

17. 参考资料

官方文档

相关博客

Read more

Java 连接 Elasticsearch 8.x 安全模式实战:证书校验与 ApiKey 认证全解析

Java 连接 Elasticsearch 8.x 安全模式实战:证书校验与 ApiKey 认证全解析

引言 Elasticsearch 8.x 版本迎来了一个重大的安全变革:默认开启安全特性(Security Features)。这意味着,当你安装好 ES 8.x 后,不再像以往那样可以直接通过 http://localhost:9200 匿名访问。集群默认强制启用 HTTPS (TLS/SSL) 加密传输,并要求进行身份认证。 对于 Java 开发者而言,这带来了一个直接的挑战:如何在代码中正确处理自签名证书(CA Certificate)并完成认证?如果处理不当,你会频繁遇到 SSLHandshakeException: PKIX path validation failed 或 unable to find valid certification path to requested target

By Ne0inhk
【JAVA 进阶】SpringBoot自动配置机制:从原理到实践的深度解析

【JAVA 进阶】SpringBoot自动配置机制:从原理到实践的深度解析

文章目录 * 前言 * 第一章 初识SpringBoot自动配置 * 1.1 自动配置的定义 * 1.2 自动配置的核心价值 * 1.2.1 降低开发门槛 * 1.2.2 提高开发效率 * 1.2.3 保证配置一致性 * 1.3 自动配置与传统Spring配置的对比 * 1.3.1 传统Spring Web配置(Spring 4.x及之前) * 1.3.2 SpringBoot自动配置实现 * 第二章 深入原理:SpringBoot自动配置是如何实现的 * 2.1 核心注解:@SpringBootApplication的“三位一体” * 2.1.1 @SpringBootConfiguration:标识配置类

By Ne0inhk
基于java 员工理系统设计与实现

基于java 员工理系统设计与实现

博主介绍:翰文编程 专注于Java(springboot ssm 等开发框架) vue  .net  php phython node.js    uniapp 微信小程序 等诸多技术领域和课设项目实战、企业信息化系统建设,从业十八余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆☆☆不然下次找不到哟 我的博客空间发布了2000+题目解决方法案例  方便大家学习使用 感兴趣的可以先收藏起来,还有大家在毕设选题,项目以及论文编写等相关问题都可以给我留言咨询,希望帮助更多的人 文末下方有源码获取地址 通过分析员工管理系统相似系统功能要求,总结本系统的主要功能 本系统模块实现功能如下: (1)员工管理:对员工信息进行添加、删除、修改和查看 (2)员工评语管理:对员工评语信息进行添加、删除、修改和查看 (3)奖金管理:对奖金信息进行添加、删除、修改和查看 (4)社保记录管理:对社保记录信息进行添加、删除、修改和查看

By Ne0inhk