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

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

希望对于大家学习Spring AI 有帮助

目录

  • [1. 什么是 Tool Calling?](#1. 什么是 Tool Calling?)
  • [2. 为什么需要 Tool Calling?](#2. 为什么需要 Tool Calling?)
  • [3. Tool Calling 的工作原理](#3. Tool Calling 的工作原理)
  • [4. 快速入门:第一个工具](#4. 快速入门:第一个工具)
  • [5. 定义工具的三种方式](#5. 定义工具的三种方式)
  • [6. 工具规范(Tool Specification)](#6. 工具规范(Tool Specification))
  • [7. 使用工具的两种 API](#7. 使用工具的两种 API)
  • [8. @ToolParam------告诉模型参数怎么填](#8. @ToolParam——告诉模型参数怎么填)
  • [9. ToolContext------给工具传递额外上下文](#9. ToolContext——给工具传递额外上下文)
  • [10. returnDirect------工具结果直接返回给用户](#10. returnDirect——工具结果直接返回给用户)
  • [11. 工具执行的两种模式](#11. 工具执行的两种模式)
  • [12. 异常处理](#12. 异常处理)
  • [13. 工具解析(Tool Resolution)](#13. 工具解析(Tool Resolution))
  • [14. 实战案例:天气查询工具](#14. 实战案例:天气查询工具)
  • [15. 常见问题与排坑](#15. 常见问题与排坑)
  • [16. 总结](#16. 总结)
  • [17. 参考资料](#17. 参考资料)

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 模型不知道"现在是几点",但我们可以写一个工具告诉它:

java 复制代码
import java.time.LocalDateTime;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.context.i18n.LocaleContextHolder;

class DateTimeTools {

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

代码解读

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

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

java 复制代码
ChatModel chatModel = ...
String response = ChatClient.create(chatModel)
        .prompt("What day is tomorrow?")
        .tools(new DateTimeTools())    // ← 把工具传给模型
        .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 执行动作型工具------设置闹钟

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

java 复制代码
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;

class DateTimeTools {

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

    @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
    void setAlarm(@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);
    }
}

调用:

java 复制代码
String response = ChatClient.create(chatModel)
        .prompt("Can you set an alarm 10 minutes from now?")
        .tools(new DateTimeTools())
        .call()
        .content();

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

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

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


5. 定义工具的三种方式

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

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

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

java 复制代码
class DateTimeTools {

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

@Tool 注解的属性

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

关于方法的要求

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

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

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

java 复制代码
// 工具类(注意:没有 @Tool 注解)
class WeatherTools {
    String getCurrentWeatherByCityName(String cityName) {
        // 查询天气的逻辑
        return "晴天,25°C";
    }
}

// 手动构建 ToolCallback
Method method = ReflectionUtils.findMethod(
        WeatherTools.class, 
        "getCurrentWeatherByCityName", 
        String.class
);

ToolCallback toolCallback = MethodToolCallback.builder()
        .toolDefinition(ToolDefinitions.builder(method)
                .description("根据给定的城市名称, 获取城市当前的天气")
                .build())
        .toolMethod(method)
        .toolObject(new WeatherTools())
        .build();

MethodToolCallback.Builder 的关键属性

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

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

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

java 复制代码
// 定义 Function Bean
@Configuration(proxyBeanMethods = false)
class WeatherToolConfig {

    @Bean
    @Description("Get the weather in location")
    Function<WeatherRequest, WeatherResponse> currentWeather() {
        return request -> new WeatherResponse(30.0, Unit.C);
    }
}

// 入参和出参必须是 POJO
public record WeatherRequest(
        @ToolParam(description = "The name of a city or a country") String location,
        Unit unit
) {}

public record WeatherResponse(double temp, Unit unit) {}

public enum Unit { C, F }

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

java 复制代码
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 接口的对象。

java 复制代码
public interface ToolCallback {

    /** 工具定义------告诉模型这个工具叫什么、干什么、参数是什么格式 */
    ToolDefinition getToolDefinition();

    /** 工具元数据------额外配置信息(如 returnDirect) */
    ToolMetadata getToolMetadata();

    /** 执行工具(无上下文) */
    String call(String toolInput);

    /** 执行工具(带上下文) */
    String call(String toolInput, ToolContext toolContext);
}

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

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

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

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

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

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

java 复制代码
public interface ToolDefinition {

    /** 工具名称------在同一次请求中必须唯一 */
    String name();

    /** 工具描述------模型根据这个描述来判断什么时候该用这个工具 */
    String description();

    /** 参数的 JSON Schema------告诉模型参数的格式、类型、约束 */
    String inputSchema();
}
自动生成 vs 手动构建

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

java 复制代码
@Tool(description = "获取当前时间")   // → description
String getCurrentDateTime() {          // → name = "getCurrentDateTime"
    ...                                // → inputSchema = {} (无参数)
}

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

java 复制代码
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
java 复制代码
// 方式一:完全自动(使用方法名和 @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 就是一份"数据格式说明"。比如下面这个工具方法:

java 复制代码
@Tool(description = "设置闹钟")
void setAlarm(@ToolParam(description = "ISO-8601 格式的时间") String time) { ... }

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

json 复制代码
{
    "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("...") Jackson POJO 类级别描述
@JsonPropertyDescription("...") Jackson POJO 字段级别描述
@Schema(description = "...") Swagger 项目已引入 Swagger 时
标记可选参数的四种注解

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

注解 来源
@ToolParam(required = false) Spring AI
@JsonProperty(required = false) Jackson
@Schema(required = false) Swagger
@Nullable Spring Framework

这些注解支持递归------如果你的参数是一个嵌套的 POJO,内层字段也可以用这些注解来描述。

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

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

java 复制代码
@FunctionalInterface
public interface ToolCallResultConverter {
    /** 将工具返回的对象转换为字符串 */
    String convert(@Nullable Object result, @Nullable Type returnType);
}
默认行为

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

java 复制代码
// 你的工具方法返回一个对象
@Tool(description = "查询客户信息")
Customer getCustomerInfo(Long id) {
    return new Customer(42L, "张三", "zhangsan@example.com");
}

// 默认转换结果(JSON 字符串):
// {"id": 42, "name": "张三", "email": "zhangsan@example.com"}
// 这个 JSON 字符串会被发回给模型
自定义结果转换器

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

java 复制代码
// 自定义转换器
public class SimpleCustomerConverter implements ToolCallResultConverter {
    @Override
    public String convert(Object result, Type returnType) {
        if (result instanceof Customer c) {
            return "客户姓名:" + c.getName() + ",邮箱:" + c.getEmail();
        }
        return result.toString();
    }
}

// 在 @Tool 注解中指定
@Tool(description = "查询客户信息", resultConverter = SimpleCustomerConverter.class)
Customer getCustomerInfo(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)
java 复制代码
// 单次请求中使用
ChatClient.create(chatModel)
        .prompt("What day is tomorrow?")
        .tools(new DateTimeTools())         // ← 传工具类实例
        .call()
        .content();
编程式工具(MethodToolCallback)
java 复制代码
ToolCallback toolCallback = ...;  // 前面构建的

ChatClient.create(chatModel)
        .prompt("What day is tomorrow?")
        .toolCallbacks(toolCallback)        // ← 传 ToolCallback 对象
        .call()
        .content();
动态式工具(@Bean)
java 复制代码
ChatClient.create(chatModel)
        .prompt("What's the weather like in Copenhagen?")
        .toolNames("currentWeather")        // ← 传工具名称(Bean 名称)
        .call()
        .content();
设置默认工具

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

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultTools(new DateTimeTools())              // 声明式默认工具
        // .defaultToolCallbacks(toolCallback)           // 编程式默认工具
        // .defaultToolNames("currentWeather")           // 动态式默认工具
        .build();

// 之后每次请求自动带上工具,不用再手动传
chatClient.prompt("What day is tomorrow?")
        .call()
        .content();

注意 :如果同时设置了默认工具和请求级工具,请求级工具会完全覆盖默认工具(不是合并)。

7.2 通过 ChatModel(更底层)

java 复制代码
// 声明式
ToolCallback[] dateTimeTools = ToolCallbacks.from(new DateTimeTools());
ChatOptions chatOptions = ToolCallingChatOptions.builder()
        .toolCallbacks(dateTimeTools)
        .build();
Prompt prompt = new Prompt("What day is tomorrow?", chatOptions);
chatModel.call(prompt);

// 动态式
ChatOptions chatOptions = ToolCallingChatOptions.builder()
        .toolNames("currentWeather")
        .build();
Prompt prompt = new Prompt("What's the weather?", chatOptions);
chatModel.call(prompt);

ChatClient vs ChatModel 使用工具时的区别

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

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

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

基本用法

java 复制代码
class DateTimeTools {

    @Tool(description = "Set a user alarm for the given time")
    void setAlarm(@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

可选参数

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

java 复制代码
class CustomerTools {

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

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

java 复制代码
void updateCustomerInfo(Long id, String name, @Nullable String 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 参数

java 复制代码
class CustomerTools {

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

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

java 复制代码
// 通过 ChatClient
String response = ChatClient.create(chatModel)
        .prompt("Tell me more about the customer with ID 42")
        .tools(new CustomerTools())
        .toolContext(Map.of("tenantId", "acme"))    // ← 传入上下文
        .call()
        .content();

// 通过 ChatModel
ChatOptions chatOptions = ToolCallingChatOptions.builder()
        .toolCallbacks(ToolCallbacks.from(new CustomerTools()))
        .toolContext(Map.of("tenantId", "acme"))    // ← 传入上下文
        .build();
Prompt prompt = new Prompt("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 场景:工具检索到的文档直接返回,不需要模型再处理
  • 终止推理循环:工具执行后就结束对话,不需要模型继续思考

用法

java 复制代码
class CustomerTools {

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

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


11. 工具执行的两种模式

11.1 框架控制模式(默认)

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

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

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

11.2 用户控制模式

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

java 复制代码
ChatOptions chatOptions = ToolCallingChatOptions.builder()
        .toolCallbacks(new CustomerTools())
        .internalToolExecutionEnabled(false)     // ← 关闭自动执行
        .build();

Prompt prompt = new Prompt("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 = new Prompt(toolExecutionResult.conversationHistory(), chatOptions);
    chatResponse = chatModel.call(prompt);
}

System.out.println(chatResponse.getResult().getOutput().getText());
框架控制(默认) 用户控制
设置 默认就是 internalToolExecutionEnabled(false)
工具执行 框架自动执行 你手动调用 ToolCallingManager
适用场景 大多数场景 需要人工审批、自定义日志、复杂流程
代码量

12. 异常处理

工具执行失败怎么办?

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

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

配置异常处理行为

通过配置文件:

yaml 复制代码
# application.yml
spring:
  ai:
    tools:
      throw-exception-on-error: true   # true: 所有异常都抛出; false(默认): RuntimeException 发回模型

或者自定义 Bean:

java 复制代码
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
    return new DefaultToolExecutionExceptionProcessor(true);  // true = 始终抛出异常
}

什么时候选哪种?

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

13. 工具解析(Tool Resolution)

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

13.1 什么是工具解析?

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

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

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

13.2 ToolCallbackResolver 接口

java 复制代码
public interface ToolCallbackResolver {
    /** 根据工具名称解析出对应的 ToolCallback */
    @Nullable
    ToolCallback resolve(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 自定义工具解析器

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

java 复制代码
@Bean
ToolCallbackResolver toolCallbackResolver(List<FunctionCallback> toolCallbacks) {
    StaticToolCallbackResolver staticResolver = new StaticToolCallbackResolver(toolCallbacks);
    return new DelegatingToolCallbackResolver(List.of(staticResolver));
}

总结 :工具解析是 toolNames() 方式的底层支撑。大多数场景下你不需要关心它------直接 .tools(new XxxTools()) 手动传就够了。当你使用 @Bean 动态式工具或需要运行时动态注册工具时,才会用到工具解析机制。


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

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

场景

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

方式一:声明式(@Tool)

定义工具

java 复制代码
public class WeatherTools {

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

在 ChatClient 中使用

java 复制代码
@RequestMapping("/chat")
@RestController
public class ChatController {

    private ChatClient chatClient;

    public ChatController(DashScopeChatModel chatModel) {
        this.chatClient = ChatClient.builder(chatModel).build();
    }

    @RequestMapping("/call")
    public String call(String message) {
        return chatClient.prompt()
                .user(message)
                .tools(new WeatherTools())       // ← 声明式,传工具类实例
                .call()
                .content();
    }
}

方式二:编程式(MethodToolCallback)

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

java 复制代码
@RequestMapping("/call2")
public String call2(String message) {
    // 1. 通过反射获取方法
    Method method = ReflectionUtils.findMethod(
            WeatherTools.class,
            "getCurrentWeatherByCityName",
            String.class
    );
    
    // 2. 手动构建 ToolCallback
    ToolCallback toolCallback = MethodToolCallback.builder()
            .toolDefinition(ToolDefinitions.builder(method)
                    .description("根据给定的城市名称, 获取城市当前的天气")
                    .build())
            .toolMethod(method)
            .toolObject(new WeatherTools())
            .build();

    // 3. 传给 ChatClient
    return chatClient.prompt()
            .user(message)
            .toolCallbacks(toolCallback)      // ← 编程式,传 ToolCallback 对象
            .call()
            .content();
}

方式三:通过 ChatModel + ToolCallingChatOptions

java 复制代码
@RequestMapping("/callByTool")
public String callByTool(String message) {
    // 1. 从工具类生成 ToolCallback 数组
    ToolCallback[] weatherTools = ToolCallbacks.from(new WeatherTools());

    // 2. 构建 ChatOptions
    ChatOptions chatOptions = ToolCallingChatOptions.builder()
            .toolCallbacks(weatherTools)
            .build();

    // 3. 构建 Prompt 并调用
    Prompt prompt = new Prompt(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() 提供上下文数据。

解决方案

java 复制代码
// 方案A:如果不需要 ToolContext,从方法参数中移除它
@Tool(description = "Get current time")
String getCurrentDateTime() {                    // ← 没有 ToolContext 参数
    return LocalDateTime.now().toString();
}

// 方案B:如果需要 ToolContext,调用时提供它
chatClient.prompt()
        .user(message)
        .tools(new MyTools())
        .toolContext(Map.of("key", "value"))     // ← 提供 ToolContext
        .call()
        .content();

问题二:模型不调用工具

可能原因

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

解决方案

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

问题三:同名工具冲突

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

java 复制代码
// ❌ 错误:两个类中都有 getCurrentDateTime 方法
chatClient.prompt()
        .tools(new DateTimeTools(), new AnotherDateTimeTools())  // 可能同名冲突
        .call()
        .content();

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

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

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

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
        .defaultTools(new DateTimeTools())       // 默认工具
        .build();

// 这次请求只有 WeatherTools,DateTimeTools 被覆盖了!
chatClient.prompt()
        .tools(new WeatherTools())               // 请求级工具覆盖默认工具
        .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. 参考资料

官方文档

相关博客

相关推荐
qq_454245031 小时前
计算机与AI领域中的“上下文”:多维度解析
数据结构·人工智能·分类
琅琊榜首20201 小时前
AI赋能内容创作:小说改编短剧全流程实操指南
人工智能
minhuan1 小时前
大模型应用:最优路径规划实践:A*算法找最优解,大模型做自然语言解释.91
人工智能·astar算法·混元大模型·最优路径规划
南部余额1 小时前
SpringBoot文件上传全攻略
java·spring boot·后端·文件上传·multipartfile
fpcc1 小时前
AI和大模型之一介绍
人工智能·cuda
小雨中_1 小时前
2.9 TRPO 与 PPO:从“信赖域约束”到“近端裁剪”的稳定策略优化
人工智能·python·深度学习·机器学习·自然语言处理
艾醒(AiXing-w)1 小时前
打破信息差——2026年2月19日AI热点新闻速览
人工智能
小雨中_1 小时前
2.5 动态规划方法
人工智能·python·深度学习·算法·动态规划
癫狂的兔子1 小时前
【Python】【机器学习】DBSCAN算法
人工智能·机器学习