臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

第 12 天核心结论

在 Spring AI 中,给 Java 方法加上 @Tool,再通过 .tools(...) 把工具对象注册给 ChatClient,模型就能根据方法说明生成参数,由 Spring AI 调用这个 Java 方法。

本篇对应 0-学习路线 第 12 天,前置知识是 11.Function Calling、Tool Calling 原理。

今天要掌握的是“怎样定义工具”,不是继续重复工具调用原理:

普通 Java 方法
  + @Tool:说明这个方法能做什么
  + @ToolParam:说明每个参数表示什么
  + .tools(...):把工具提供给本次模型请求
  = 模型可以申请调用的 Tool

最重要的边界仍然是:模型生成工具名和参数,真正执行 Java 方法的是应用。

二、🧭 理论:Java 方法怎样变成 Tool

2.1 一个 Tool 由哪几部分组成

Spring AI 官方把模型能看到的工具说明称为 ToolDefinition。它主要包含三部分:

组成大白话解释当前示例
name工具叫什么,模型调用时用这个名字getCurrentDateTime
description工具能做什么、什么时候应该使用查询指定时区的当前时间
inputSchema参数有哪些、是什么类型、是否必填zoneId 是可选字符串

日常开发不用手写 ToolDefinition。通常使用 @Tool 和 @ToolParam,让 Spring AI 根据 Java 方法自动生成上面三个组成部分:

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
 
public class DateTimeTools {
 
    @Tool(
            // name:对应 ToolDefinition.name。
            // 模型发起 Tool Call 时使用这个名称查找工具。
            // 如果不写 name,默认使用 Java 方法名 getCurrentDateTime。
            name = "getCurrentDateTime",
 
            // description:对应 ToolDefinition.description。
            // 模型根据这段说明判断工具能做什么、什么时候应该调用。
            description = "查询指定时区的当前日期和时间;用户询问当前时间时使用"
    )
    public String getCurrentDateTime(
            // 方法参数的类型、名称和 @ToolParam 共同生成 inputSchema。
            @ToolParam(
                    // 这段说明会成为 inputSchema 中 zoneId 的 description。
                    description = "IANA 时区名称,例如 Asia/Shanghai",
                    // required = false 表示模型可以不传这个参数。
                    required = false
            )
            String zoneId) {
 
        // 方法体是工具真正执行的业务逻辑,不属于 ToolDefinition。
        return "当前时间查询结果";
    }
}

代码与三个组成部分的对应关系是:

ToolDefinition 组成对应代码
name@Tool(name = "getCurrentDateTime")
description@Tool(description = "...")
inputSchemaSpring AI 根据方法参数、参数类型和 @ToolParam 自动生成

Spring AI 读取方法和注解后,会生成一份近似下面这样的说明发送给模型:

{
  "name": "getCurrentDateTime",
  "description": "查询指定时区的当前日期和时间;当用户询问现在几点、今天日期或当前时间时使用",
  "inputSchema": {
    "type": "object",
    "properties": {
      "zoneId": {
        "type": "string",
        "description": "IANA 时区名称,例如 Asia/Shanghai;用户未说明时区时可以不传"
      }
    }
  }
}

inputSchema 就是参数填写说明书

它不是模型真正传入的参数,只是告诉模型“有哪些参数、参数是什么类型、应该填写什么内容”。

这段 Schema 可以简单理解为:

内容大白话解释
最外层的 type: object所有工具参数放在一个 JSON 对象中
properties这个工具允许填写哪些参数
zoneId参数名,对应 Java 方法中的 String zoneId
zoneId 里面的 type: string这个参数必须是字符串
description给模型看的填写提示

模型看完 inputSchema 后,才会生成真正的调用参数:

{
  "zoneId": "Asia/Shanghai"
}

Spring AI 再把它转换成 Java 方法调用:

getCurrentDateTime("Asia/Shanghai");

可以把两者记成:

inputSchema = 空白表单,规定应该怎么填
arguments   = 模型填写后的实际内容

如果只想直观看清 ToolDefinition 三个字段,也可以手动构建一份工具说明:

import org.springframework.ai.tool.definition.ToolDefinition;
 
ToolDefinition toolDefinition = ToolDefinition.builder()
        // 工具唯一名称。
        .name("getCurrentDateTime")
        // 工具用途和使用时机。
        .description("查询指定时区的当前日期和时间;用户询问当前时间时使用")
        // 模型调用工具时必须遵守的参数结构。
        .inputSchema("""
                {
                  "type": "object",
                  "properties": {
                    "zoneId": {
                      "type": "string",
                      "description": "IANA 时区名称,例如 Asia/Shanghai"
                    }
                  },
                  "required": []
                }
                """)
        .build();

手动创建的 ToolDefinition 只是工具说明书

它还没有绑定真正执行查询的 Java 方法。当前项目优先使用 @Tool 和 @ToolParam,再通过 .tools(new DateTimeTools()) 同时提供工具说明与执行逻辑。

上面的 JSON 只是帮助理解的近似结构。模型不会收到完整 Java 源码,也看不到方法内部怎样读取时间。

2.2 @Tool:声明方法是工具

@Tool 标在方法上,作用是告诉 Spring AI:这个方法可以被包装成工具。

@Tool(description = "查询指定时区的当前日期和时间;当用户询问现在几点、今天日期或当前时间时使用")
public String getCurrentDateTime(String zoneId) {
    // 真正的业务逻辑
}

常用属性如下:

属性作用当前阶段怎么用
name自定义工具名;不写时默认使用 Java 方法名入门阶段优先使用清晰的方法名
description告诉模型工具用途和使用时机强烈建议始终填写
returnDirect是否把工具结果直接返回调用方,不再交给模型整理当前保持默认 false
resultConverter自定义工具返回值如何转换成字符串当前使用默认转换器

description 不是代码

它只帮助模型做选择,不会执行权限校验、参数校验或设置默认值。真正的业务规则必须写在 Java 代码里。

一个好的工具描述至少要回答两个问题:

  1. 这个工具能做什么;
  2. 什么情况下应该使用它。

对比下面两个描述:

// 太模糊:模型不知道查询的是什么,也不知道何时调用。
@Tool(description = "查询数据")
 
// 更清楚:能力、输入对象和使用时机都明确。
@Tool(description = "根据订单编号查询当前订单状态;用户询问订单是否付款、发货或完成时使用;只读,不修改订单")

2.3 @ToolParam:说明参数含义

@ToolParam 标在方法参数上,帮助 Spring AI 生成参数 JSON Schema。

public String getCurrentDateTime(
        @ToolParam(
                description = "IANA 时区名称,例如 Asia/Shanghai;用户未说明时区时可以不传",
                required = false)
        String zoneId) {
    // ...
}
配置含义
description告诉模型参数表示什么、采用什么格式
required = true参数必填;这也是默认值
required = false参数可以不传,Java 代码必须正确处理 null 或空值

本例把 zoneId 设为可选,是因为用户可能只问“现在几点”,没有明确说时区。此时工具方法用 Asia/Shanghai 作为业务默认值。

不要把模型逼到“编一个值”

如果参数被标成必填,但用户问题中没有这个信息,模型为了满足 Schema 可能猜一个值。确实允许缺省的参数,应标成可选并由后端设置安全默认值;不能缺省的参数,应让应用要求用户补充。

2.4 Tool 不是加完注解就自动生效

只写 @Tool 并不会让所有模型请求都自动获得这个工具。还需要把工具对象注册给 ChatClient。

本次请求注册:

chatClient.prompt()
        .user(question)
        .tools(new DateTimeTools())
        .call()
        .content();

默认注册:

ChatClient client = builder
        .defaultTools(new DateTimeTools())
        .build();

两种方式的区别:

注册方式生效范围适合场景
.tools(...)只对当前这一次请求生效按接口、用户权限或业务场景精确开放工具
.defaultTools(...)这个 ChatClient 的每次请求都可使用每次请求都需要、风险较低的通用工具

本篇使用 .tools(...)。这样最容易看清楚“哪个接口开放了哪个工具”,也能减少模型误选工具和越权调用的风险。

Spring AI 2.0.1 的行为

.tools(...) 会追加本次请求的工具,不会替换 ChatClient 上已有的默认工具。模型最终看到的是默认工具和本次工具的合集,因此工具名称必须唯一。

三、⚙️ 理论:从注解到方法执行发生了什么

3.1 官方推荐的声明式定义顺序

Spring AI 2.0.1 官方教程把“用方法定义工具”的入门流程组织为:

  1. 在 Java 方法上添加 @Tool;
  2. 使用 @ToolParam 描述参数;
  3. 创建包含这些方法的对象;
  4. 通过 .tools(...) 或 .defaultTools(...) 注册;
  5. 调用 ChatClient,由工具调用循环完成模型请求和方法执行。

当前代码对应关系如下:

DateTimeTools#getCurrentDateTime
        ↓ @Tool、@ToolParam
Spring AI 生成 ToolDefinition 和参数 Schema
        ↓ .tools(new DateTimeTools())
工具说明随用户问题一起发送给模型
        ↓
模型返回 getCurrentDateTime 的 Tool Call
        ↓
Spring AI 把 JSON 参数转换为 Java 参数并执行方法
        ↓
方法结果交回模型,模型生成最终回答

3.2 @Tool、.tools(...) 和 .call() 各自负责什么

这三个位置最容易混淆:

代码真正职责不负责什么
@Tool声明方法可作为工具,并提供工具说明不注册工具,不执行方法
.tools(new DateTimeTools())把该对象中的工具提供给本次请求不保证模型一定调用
.call()发起同步模型调用,并由 Spring AI 驱动工具调用循环不替代方法内部的校验和权限控制

一句话记忆:

@Tool 负责“定义”
.tools(...) 负责“上架”
.call() 负责“启动本轮流程”

3.3 方法参数怎样传进来

假设用户问:

纽约现在几点?

模型根据工具说明生成类似下面的调用请求:

{
  "name": "getCurrentDateTime",
  "arguments": {
    "zoneId": "America/New_York"
  }
}

Spring AI 找到同名工具后,把 JSON 中的 zoneId 转成 Java 的 String 参数,然后执行:

getCurrentDateTime("America/New_York");

所以参数并不是 Controller 手动传入的,也不是 @ToolParam 自动生成的;它是模型根据用户问题和参数 Schema 生成,再由 Spring AI 绑定到 Java 方法上的。

3.4 方法返回值去了哪里

当前方法返回一个 String:

return now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME);

默认情况下,Spring AI 会把这个结果作为 Tool Result 再交给模型。模型看到真实时间后,才生成更自然的中文回答。

Java 方法返回:2026-09-24T15:30:00+08:00[Asia/Shanghai]
        ↓
模型最终回答:上海当前时间是 2026 年 9 月 24 日 15:30。

工具也可以返回 Java 对象。Spring AI 默认会使用 Jackson 把结果序列化为模型可以读取的字符串。入门阶段优先返回结构清楚、字段稳定的 record 或简单对象,不要把实体对象、异常堆栈或敏感字段直接暴露给模型。

3.5 方法定义的限制

Spring AI 2.0.1 的方法工具不支持把下面这些类型直接作为方法参数或返回值:

  • Optional;
  • CompletableFuture、Future;
  • Mono、Flux 等响应式类型;
  • Function、Supplier、Consumer 等函数类型。

可选参数使用 @ToolParam(required = false) 或可空标记表达,不要使用 Optional。需要异步、响应式或复杂执行控制时,应把这些细节留在业务服务内部,对工具暴露普通、稳定、可序列化的输入输出。

四、🚀 实践:定义并调用时间工具

4.1 前置准备

4.1.1 当前项目环境

本篇直接沿用当前项目:

  • Java 21;
  • Spring Boot 4.1.1;
  • Spring AI 2.0.1;
  • Maven 3.9.16;
  • OpenAI 兼容模型配置;
  • 模型本身支持 Tool Calling。

pom.xml 已有依赖:

<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>

不需要为了 @Tool 再添加一个独立 Starter。

4.1.2 模型配置

src/main/resources/application.properties 沿用已有配置:

# API Key 从环境变量读取,不写进配置文件或 Git。
spring.ai.openai.api-key=${DEEPSEEK_API_KEY}
spring.ai.openai.base-url=https://api.deepseek.com
spring.ai.openai.chat.model=deepseek-v4-flash

模型名称和能力可能变化。如果普通聊天能成功,但工具始终没有执行,应先确认当前模型及其兼容接口是否支持 Tool Calling。

4.2 可以拿来干什么

4.2.1 把现有只读查询包装成 Tool

用途:把订单查询、物流查询、库存查询等现有 Java Service 方法提供给模型,让用户可以用自然语言查询真实数据。

前置:查询方法必须已有明确输入输出,当前用户也必须有权读取这份数据。工具层不能因为模型传来了订单号就跳过业务鉴权。

@Tool(description = "根据订单编号查询当前订单状态;用户询问订单是否付款、发货或完成时使用;只读,不修改订单")
public OrderStatusResult queryOrderStatus(
        @ToolParam(description = "订单编号,例如 A1001")
        String orderId) {
    // orderService 来自现有业务系统;工具只负责提供受控入口。
    return orderService.queryStatus(orderId);
}

输入是模型从用户问题中提取的订单编号,输出是结构化订单状态。适合只读、可审计、结果明确的查询能力。

4.2.2 只给某个接口开放需要的工具

用途:不同接口只开放自己需要的工具,避免把所有系统能力一次性暴露给模型。

前置:已经创建工具对象;如果工具依赖 Service,应由 Spring 注入,而不是在每次请求中手动创建复杂依赖。

return chatClient.prompt()
        .user(question)
        // tools:本次请求只能看到时间工具,其他 Tool 不会自动出现。
        .tools(new DateTimeTools())
        .call()
        .content();

输入是自然语言问题;Spring AI 会把当前工具说明和问题一起交给模型。适合按场景控制工具集合,也是后续做权限隔离的基础。

4.3 完整实践:查询指定时区的当前时间

4.3.1 定义 DateTimeTools

文件位置:

src/main/java/com/afyke/ai/tool/DateTimeTools.java

当前项目的完整代码如下:

package com.afyke.ai.tool;
 
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
 
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
 
public class DateTimeTools {
 
    // @Tool:把下面的 Java 方法声明为一个可供模型选择的工具。
    // 只有当 DateTimeTools 对象通过 .tools(...) 或 .defaultTools(...) 注册后,
    // Spring AI 才会读取这个注解,并把工具说明发送给模型。
    //
    // description:告诉模型这个工具能做什么、什么时候应该使用。
    // 它会影响模型是否选择该工具,但不会执行 Java 代码,也不负责参数校验。
    @Tool(description = "查询指定时区的当前日期和时间;当用户询问现在几点、今天日期或当前时间时使用")
    public String getCurrentDateTime(
            // @ToolParam:描述 zoneId 参数的含义和格式。
            // Spring AI 会用它生成工具参数的 JSON Schema,帮助模型正确传参。
            //
            // required = false:告诉模型这个参数可以不传。
            // 注意:它只把参数标记为可选,不会自动把参数设置成 Asia/Shanghai。
            @ToolParam(
                    description = "IANA 时区名称,例如 Asia/Shanghai;用户未说明时区时可以不传",
                    required = false)
            String zoneId) {
 
        // description 只是给模型看的说明,不会自动生成 Java 默认值。
        // 因此参数为空时,仍要由业务代码真正设置默认时区。
        String actualZoneId = (zoneId == null || zoneId.isBlank())
                ? "Asia/Shanghai"
                : zoneId;
 
        // ZoneId.of:把最终时区字符串转换为 Java 时区对象。
        // 如果模型传入不存在的时区,这里会抛出 DateTimeException;第 13 天再完善参数校验。
        ZoneId zone = ZoneId.of(actualZoneId);
 
        // ZonedDateTime.now:由 Java 应用读取指定时区的真实时间,不让模型猜测。
        ZonedDateTime now = ZonedDateTime.now(zone);
 
        // 临时学习日志:通过控制台确认 Java 方法确实执行了。
        System.out.println("执行 getCurrentDateTime,zoneId=" + zoneId);
 
        // format:生成稳定的 ISO 时间字符串,再交给模型组织最终回答。
        return now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
    }
}

这段代码的输入、过程和输出分别是:

阶段内容
输入模型生成的 zoneId,例如 America/New_York
处理设置默认时区、解析时区、读取真实当前时间
输出ISO 格式时间字符串,交回模型整理

4.3.2 在 Controller 中注册工具

文件位置:

src/main/java/com/afyke/ai/controller/toolController/ToolCallingController.java

当前项目的完整代码如下:

package com.afyke.ai.controller.toolController;
 
import com.afyke.ai.tool.DateTimeTools;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
 
@RestController
@RequestMapping("/api/tool")
public class ToolCallingController {
 
    private final ChatClient chatClient;
 
    public ToolCallingController(ChatClient.Builder builder) {
        // build:使用 Spring Boot 已经配置好的 ChatModel 创建 ChatClient。
        // API Key、模型名和服务地址仍由 application.properties 提供。
        this.chatClient = builder.build();
    }
 
    @GetMapping("/time")
    public String queryCurrentTime(
            @RequestParam(defaultValue = "上海现在几点?请先使用工具查询,不要猜测。")
            String question) {
 
        return chatClient
                // prompt:开始组织本次模型请求。
                .prompt()
                // user:放入用户的自然语言问题。
                .user(question)
                // tools:只把这个工具对象暴露给本次请求。
                // Spring AI 会读取对象中带 @Tool 的方法并生成 ToolDefinition。
                .tools(new DateTimeTools())
                // call:发起同步调用。
                // 如果模型返回 Tool Call,Spring AI 会执行工具并再次请求模型。
                .call()
                // content:取出整个工具调用循环结束后的最终文本回答。
                .content();
    }
}

4.3.3 启动项目

先在 IntelliJ IDEA 的 AiApplication 运行配置中设置环境变量:

DEEPSEEK_API_KEY=你的真实密钥

然后使用 IDEA 的绿色运行按钮启动 AiApplication。也可以在项目根目录执行:

/Volumes/data/develop/apache-maven-3.9.16/bin/mvn spring-boot:run

启动成功后,应用监听 8080 端口。

4.3.4 发送验证请求

使用浏览器或 HTTP 客户端访问:

GET http://localhost:8080/api/tool/time?question=纽约现在几点?请使用工具查询

命令行验证:

curl --get 'http://localhost:8080/api/tool/time' \
  --data-urlencode 'question=纽约现在几点?请使用工具查询'

预期发生的过程:

模型选择 getCurrentDateTime
  → 生成 zoneId=America/New_York
  → Spring AI 执行 Java 方法
  → 控制台打印“执行 getCurrentDateTime...”
  → 模型根据真实时间生成中文回答

最终回答的措辞不固定,可能类似:

纽约当前时间是 2026 年 9 月 24 日 03:52,时区为 America/New_York。

时间数值必须以实际请求时的工具结果为准,不能照抄示例。

4.3.5 再验证一次“不需要工具”的问题

发送:

GET http://localhost:8080/api/tool/time?question=请解释什么是Java集合

模型通常会直接回答,不执行 getCurrentDateTime。控制台不应出现工具日志。

这说明 .tools(...) 只是提供候选工具,不是强制每个问题都调用工具。

4.4 关键报错排查

4.4.1 普通聊天成功,但工具没有执行

按顺序检查:

  1. 当前模型和服务接口是否支持 Tool Calling;
  2. 请求链中是否真的调用了 .tools(new DateTimeTools());
  3. 方法上是否有 org.springframework.ai.tool.annotation.Tool;
  4. @Tool 描述是否明确说明了用途和调用时机;
  5. 用户问题是否确实需要实时数据;
  6. 查看控制台是否出现工具执行日志。

4.4.2 出现 Unknown time-zone ID

原因:模型生成了 Java 不认识的时区名称,ZoneId.of(...) 解析失败。

当前章节先用它说明“模型参数是不可信输入”。下一篇“工具参数设计和校验”再增加白名单、异常转换和让用户补充信息的处理。

4.4.3 出现工具名称冲突

同一次模型请求中,每个工具名称必须唯一。如果两个方法都通过 @Tool(name = "queryData") 暴露,就需要修改名称,使它们能被明确区分。

4.4.4 为什么工具类没有加 @Component

当前代码使用:

.tools(new DateTimeTools())

因此由业务代码直接创建工具对象,不需要 Spring 管理它。

如果工具需要注入 OrderService、InventoryService 等业务依赖,更合适的做法是把工具类交给 Spring 管理,再通过构造方法注入到 Controller。不要在 Controller 中手动 new 一个依赖复杂的工具。

另外,Spring AI 官方说明:构建 GraalVM Native Image 时,包含 @Tool 方法的类应成为 Spring Bean,或者显式注册反射信息。当前普通 JVM 学习项目暂时不涉及这个限制。

五、📌 总结

  • @Tool 把 Java 方法声明为模型可申请调用的工具。
  • @ToolParam 描述参数的含义、格式和是否必填。
  • .tools(...) 只为当前请求注册工具;.defaultTools(...) 为同一个 ChatClient 的所有请求注册默认工具。
  • 注解说明只影响模型选择,不能替代 Java 代码中的默认值、校验、鉴权和异常处理。
  • 工具参数由模型生成,再由 Spring AI 转成 Java 参数,所以必须按不可信输入处理。
  • 默认情况下,Java 方法结果会先交回模型,再由模型整理成最终回答。

记忆句

@Tool 负责定义,.tools(...) 负责上架,.call() 启动流程,Java 方法负责真正办事。

下一步:学习“工具参数设计和校验”,解决必填与可选参数、格式约束、错误提示和模型乱传值的问题。

六、📚 官方资料