臭名昭著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激活

一、💡 一句话理解

第 11 天核心结论

Tool Calling 就是:模型决定“应该调用哪个工具、传什么参数”,Java 应用负责真正执行工具,再把结果交给模型生成最终回答。

本篇对应 0-学习路线 第 11 天。

用户提问
  → 模型选择工具并生成参数
  → Java 应用执行工具
  → 模型根据工具结果回答用户

最重要的边界:模型只提出调用请求,不会直接执行 Java 方法,也不应该直接操作数据库。

二、🧭 理论:它是什么

2.1 为什么需要 Tool Calling

大模型擅长理解和生成文字,但不知道实时业务数据,例如:

  • 订单 A1001 是否发货;
  • 商品还有多少库存;
  • 当前准确时间;
  • 用户是否有退款权限。

如果让模型直接回答,它可能会猜。Tool Calling 让模型向 Java 应用请求真实数据或业务能力。

用大白话说,可以把整个过程想成“用户去服务台查询订单”。下面只是帮助理解的类比,真实系统里仍然是模型、Spring AI 和 Java 代码在交换消息。

角色大白话类比具体负责什么不负责什么
模型服务台接待员听懂用户的问题,选择合适的工具,并从问题中整理出参数,例如 orderId=A1001;拿到工具结果后,再组织成自然语言答案。不直接执行 Java 方法,不直接查询数据库,也不能自己编造订单状态。
Spring AI流程协调员把可用工具说明交给模型;收到 Tool Call 后转给 Java 应用;工具执行完成后,再把结果交回模型,并管理可能出现的多轮工具调用。不提供真实订单数据,也不替业务系统决定权限规则。
Java 应用真正办事的工作人员校验订单号是否合法、用户是否有权限,然后执行指定的 Java 方法;同时处理异常、超时和安全控制。不会无条件相信模型生成的工具名和参数。
业务系统保存真实档案的后台从订单数据库、物流服务或库存系统中返回真实业务数据;它可能位于当前应用中,也可能是其他微服务。不理解用户的自然语言,也不负责选择调用哪个工具。

放进一个真实问题里就是:

用户:“订单 A1001 发货了吗?”
  → 模型:听懂问题,申请调用 queryOrderStatus,参数是 A1001
  → Spring AI:把这次调用申请交给 Java 应用
  → Java 应用:校验订单号和用户权限,再查询订单系统
  → 业务系统:返回真实状态 SHIPPED
  → Spring AI:把 SHIPPED 交回模型
  → 模型:整理成“订单 A1001 已发货”并回答用户

最简单的记法

模型负责“听懂并决定查什么”,Spring AI 负责“把整个调用流程串起来”,Java 应用负责“安全地执行”,业务系统负责“提供真实数据”。

2.2 Function Calling 和 Tool Calling

2.2.1 Function Calling 到底是什么

Function Calling 这个名字很容易让人误以为“模型亲自执行了一个函数”,但真实含义是:

Java 后端先告诉模型:“我这里有一个查询时间的方法可以使用。”模型看完用户问题后回复 Java 后端:“请运行查询时间方法,参数是 America/New_York。”最后由 Java 后端真正运行这个方法。

结合当前代码,.tools(new DateTimeTools()) 会让 Spring AI 读取 @Tool 和 @ToolParam,整理出一份工具说明。发送给模型的内容可以近似理解为:

{
  "用户问题": "纽约现在几点?",
  "可用工具": [
    {
      "name": "getCurrentDateTime",
      "description": "查询指定时区的当前时间",
      "parameters": {
        "zoneId": "时区名称"
      }
    }
  ]
}

注意:这里发给模型的是“方法使用说明”,不是 DateTimeTools 的完整 Java 源码。

模型看完以后,不直接查询时间,而是返回类似:

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

这句话翻译成人话就是:

模型对 Java 后端说:
“这个问题需要查询时间。请你调用 getCurrentDateTime 方法,
并把 America/New_York 作为 zoneId 参数传进去。”

Spring AI 收到这份回复后,才会在 Java 应用中找到对应的 @Tool 方法并真正执行:

getCurrentDateTime("America/New_York");

可以把模型理解成一个只负责填写“函数调用单”的人。调用单中写着:

要调用哪个函数?
要给这个函数传什么参数?

模型只负责填单,不负责真正办事。

2.2.2 用查询时间举例

Java 应用先告诉模型存在一个函数:

函数名:getCurrentDateTime
作用:查询指定时区的当前时间
参数:zoneId,例如 Asia/Shanghai

用户提问:

纽约现在几点?

普通聊天模型可能直接猜时间;支持 Function Calling 的模型可以不回答正文,而是返回一份结构化调用请求:

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

这一步就是 Function Calling。它表示模型判断应该使用 getCurrentDateTime,并从问题中整理出了参数,但此时 Java 方法还没有执行。

接下来由 Java 应用完成真正的调用:

getCurrentDateTime("America/New_York");

Java 方法返回真实时间后,应用再把结果交给模型,模型最后回答:

纽约当前时间是……

完整过程可以记成:

模型生成:函数名 + 参数
  → Java 应用执行函数
  → 函数返回真实结果
  → 模型整理最终答案

2.2.3 Function Calling 和 Tool Calling 的关系

Function Calling 是把一个“函数”作为模型可申请使用的能力。Tool Calling 是更宽泛的总称,工具除了函数,还可能是平台内置搜索、代码执行或 MCP 服务器提供的远程能力。

名称大白话理解当前项目中的例子
Function Calling模型生成函数名和参数,由应用执行函数getCurrentDateTime({"zoneId":"America/New_York"})
Tool Calling模型申请使用外部能力的整体机制Spring AI 把 Java 方法包装成 Tool 并管理完整调用循环

在当前学习路线中,Spring AI 使用 @Tool 把 Java 方法提供给模型,因此统一称为 Tool Calling;底层仍然包含“模型生成方法名和参数”这个 Function Calling 思路。

2.3 四个核心概念

概念含义示例
Tool Definition给模型看的工具说明书名称、用途、参数 JSON Schema
Tool Call模型生成的调用请求queryOrderStatus({"orderId":"A1001"})
Tool ResultJava 应用执行后的真实结果{"status":"SHIPPED"}
Final Response模型整理后的最终回答“订单 A1001 已发货。”

模型看到的是工具说明,不是 Java 方法源码、数据库连接或 API Key。

2.4 和结构化输出有什么区别

6.结构化输出、JSON Schema 与 Tool Calling 都使用结构化数据,但用途不同。

对比结构化输出Tool Calling
目标让最终回答符合固定格式调用外部能力获取数据或执行动作
是否执行 Java 方法通常不执行会执行
是否再次请求模型通常不需要通常需要

结构化输出可以判断用户想查订单,但只有 Tool Calling 才能调用订单系统取得真实状态。

三、⚙️ 理论:它是怎么工作的

3.1 完整调用流程

以“订单 A1001 发货了吗”为例:

  1. Java 应用把用户问题和可用工具说明发给模型。
  2. 模型返回工具名称和 JSON 参数。
  3. Java 应用校验参数并执行真实方法。
  4. Java 应用把执行结果发回模型。
  5. 模型生成最终回答;如果还需要其他工具,则继续循环。

一次 Tool Calling 通常至少产生两次模型请求:第一次决定工具,第二次根据工具结果组织答案。

3.2 什么时候会调用,什么时候不会调用

先记住:.tools(...) 只是把工具放进本次请求的“候选工具列表”,不是命令模型必须调用。默认情况下,模型会根据用户问题和工具描述自行判断。

模型的判断不是 Java 中写死的 if/else,而是一次语言模型预测。应用请求模型时,会同时发送用户问题和每个工具的说明;模型结合下面这些信息,预测本轮应该直接输出文字,还是输出一个结构化 Tool Call。

判断依据模型会看什么示例
用户问题用户真正想知道或完成什么“订单 A1001 发货了吗?”说明需要真实订单状态。
工具名称工具名称是否和问题语义匹配queryOrderStatus 比 queryData 更容易被正确选择。
工具描述工具能做什么、什么时候使用、有什么限制“根据订单编号查询当前订单状态;只读,不修改订单。”
参数 Schema调用工具需要哪些参数以及参数类型模型从问题中提取 orderId=A1001。
System Prompt应用对模型设置的规则“实时业务数据必须通过工具查询,不允许猜测。”
对话上下文前几轮已经提供了什么信息用户上一轮已经给出订单号,本轮只说“它发货了吗”。
工具调用策略允许模型自由选择,还是由应用强制或禁止默认通常是自动选择;部分模型接口也支持强制调用或禁止调用。

模型最后会在两类输出中选择一种:

不需要工具 → 输出普通文字回答
 
需要工具   → 输出结构化 Tool Call
             name = queryOrderStatus
             arguments = {"orderId":"A1001"}

这仍然是概率判断,不保证每次都完全正确。工具名和描述越明确、候选工具越少、参数 Schema 越清晰,模型越容易选对。Java 应用仍要校验最终的工具名和参数。

用户的问题是否通常调用 Tool原因
“订单 A1001 发货了吗?”会需要查询业务系统中的实时订单状态。
“上海现在几点?”会需要取得当前时间,不能只依赖模型已有知识。
“帮我创建一个售后工单”会提出调用请求需要执行系统动作;Java 应用仍应先进行权限校验和人工确认。
“什么是订单状态?”不会这是通用概念,模型可以直接解释。
“请帮我润色这段文字”不会只需要模型自身的语言能力。
“1 + 1 等于多少?”通常不会问题简单,不需要外部数据或动作。
“查询订单 A1001”,但本次请求没有注册订单工具不会模型看不到这个工具,只能说明无法查询,或者产生不可靠的猜测。
已注册时间工具,但用户询问 Java 集合不会工具虽然可用,但和当前问题无关。

通常满足下面三个条件,才会发生工具调用:

当前请求注册了合适的工具
  + 模型本身支持 Tool Calling
  + 模型判断回答需要外部数据或系统动作
  = 模型返回 Tool Call,Spring AI 执行工具

不会调用的常见情况有:

  1. 问题靠模型自身知识就能回答。
  2. 注册的工具与问题无关。
  3. 当前请求根本没有注册所需工具。
  4. 工具名称或描述太模糊,模型没有正确识别。
  5. 当前模型或供应商接口不支持 Tool Calling。
  6. 应用明确禁止工具调用,或者在执行前因参数、权限、人工审批没有通过而拦截。

“没有调用”和“没有执行”不完全一样

模型没有返回 Tool Call,叫“没有调用”;模型已经申请调用,但被 Java 应用的权限或审批逻辑拦住,叫“提出了调用请求,但没有真正执行”。

3.3 Spring AI 2.0.1 做了什么

Spring AI 2.0.1 使用 ToolCallingAdvisor 管理调用循环,使用 ToolCallingManager 查找并执行 ToolCallback。

业务代码通常只需要写一次 .call():

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

内部过程是:

第一次请求模型
  → 收到 Tool Call
  → 执行 Java 方法
  → 把 Tool Result 加入对话
  → 再次请求模型
  → 返回最终回答

如果模型认为问题不需要工具,它也可以直接回答。

3.4 必须记住的安全边界

模型生成的工具名和参数都是不可信输入,Java 应用必须掌握最终执行权。

风险必要控制
调错工具只注册当前请求允许使用的工具
参数错误类型、格式、范围和业务校验
越权访问从登录态读取身份并在后端鉴权
重复写入幂等键、事务和审计日志
高风险操作执行前人工确认
无限循环超时、最大步数和重复调用检测

写操作不能直接自动执行

查询时间等只读工具可以自动执行;退款、删除、创建工单等写操作必须增加权限、幂等、审计和人工确认。

四、🚀 实践:跑通第一个工具

4.1 前置准备

沿用学习路线已有环境:

  • Java 21;
  • Spring Boot 4.1.1;
  • Spring AI 2.0.1;
  • Maven 3.9.16;
  • 一个支持 Tool Calling 的聊天模型。

pom.xml 已包含:

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

application.properties 沿用已有模型配置:

# 密钥只从环境变量读取,不写入代码或 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 可以拿来干什么

Tool Calling 主要有两类用途:

  1. 查询真实数据:时间、天气、订单、物流和库存。
  2. 执行系统动作:创建工单、发送消息和触发审批。

本篇只实现无副作用的“查询当前时间”,用最少代码观察完整调用链。

4.3 完整代码

下面这张图对应后文 DateTimeTools 和 ToolCallingController 的完整调用过程:

图中的两次“大模型”交互都发生在一次 .call() 内部:第一次让模型选择工具并生成参数,第二次让模型根据工具结果整理最终答案;中间的工具执行由 Spring AI 自动管理。

4.3.1 定义工具

文件: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 时区对象。
        ZoneId zone = ZoneId.of(actualZoneId);
 
        // ZonedDateTime.now:由 Java 应用读取真实时间,不让模型猜测。
        ZonedDateTime now = ZonedDateTime.now(zone);
 
        // format:生成稳定的工具结果,再交给模型组织最终回答。
        return now.format(DateTimeFormatter.ISO_ZONED_DATE_TIME);
    }
}

这两个注解都会参与生成模型看到的工具说明,但职责不同:

注解标注位置大白话作用
@ToolJava 方法上告诉 Spring AI:“这个方法可以作为工具提供给模型”,并通过 description 告诉模型什么时候应该选择它。
@ToolParam方法参数上告诉模型这个参数表示什么、应该传什么格式以及是否必填,帮助模型生成正确的 Tool Call 参数。

例如用户问“上海现在几点”,模型可能根据这两个说明生成:

{
  "name": "getCurrentDateTime",
  "arguments": {
    "zoneId": "Asia/Shanghai"
  }
}

description 只会影响模型的选择和参数生成,不会自动执行校验,也不会自动给 Java 参数设置默认值。真正的默认值、格式校验和异常处理仍要写在 Java 方法中。

4.3.2 创建接口

文件:src/main/java/com/afyke/ai/controller/ToolCallingController.java

package com.afyke.ai.controller;
 
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:使用已有模型配置创建 ChatClient。
        this.chatClient = builder.build();
    }
 
    @GetMapping("/time")
    public String queryCurrentTime(
            @RequestParam(defaultValue = "上海现在几点?请使用工具查询。")
            String question) {
 
        return chatClient
                // prompt:开始组织本次模型请求。
                .prompt()
                // user:放入用户的自然语言问题。
                .user(question)
                // tools:只把时间工具提供给本次请求。
                .tools(new DateTimeTools())
                // call:发起调用;Spring AI 自动完成工具循环。
                .call()
                // content:取出循环结束后的最终文字回答。
                .content();
    }
}

4.4 启动和验证

编译并启动:

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

发送请求:

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

或使用命令行:

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

返回内容的措辞不固定,结果可能类似:

上海当前时间是 2026 年 9 月 23 日 15:00,时区为 Asia/Shanghai。

可以临时在工具方法中加入下面的日志,确认 Java 方法真的执行了:

System.out.println("执行 getCurrentDateTime,zoneId=" + zoneId);

4.5 常见问题

4.5.1 模型直接回答,没有调用工具

依次检查:

  1. 模型是否支持 Tool Calling;
  2. 请求中是否包含 .tools(new DateTimeTools());
  3. @Tool 描述是否明确;
  4. 问题是否确实需要实时数据。

4.5.2 出现 Unknown time-zone ID

模型生成了 Java 不支持的时区名称。说明 Tool Call 参数仍然需要校验;第 13 天会专门学习参数设计和校验。

4.5.3 为什么比普通聊天慢

普通聊天通常只请求一次模型。Tool Calling 至少要经过“模型选择工具”和“模型整理工具结果”两次调用,因此延迟和 Token 消耗通常更高。

五、📌 总结

  • Tool Calling 是“模型提出调用请求,Java 应用执行真实代码”。
  • Tool Definition 包含工具名、用途和参数 JSON Schema。
  • Spring AI 2.0.1 自动管理“请求模型 → 执行工具 → 再请求模型”的循环。
  • 模型生成的参数不可信,后端必须校验参数和权限。
  • 查询工具可以自动执行,高风险写操作必须人工确认并保证幂等。

记忆句

模型负责“想调用什么”,应用负责“能不能调、怎么调、结果是什么”。

下一步:学习如何用 Java 方法、@Tool 和 @ToolParam 定义业务工具。

六、📚 官方资料