目录

     1. 为什么有了大模型,还需要 Tool Calling?

2. 这次做一个什么项目?

3. 先准备订单业务层

4. 把Java业务能力变成Tool

5. 把Tool交给ChatClient

6. Controller就非常简单了

7. Tool Calling最容易理解错的一点

8. 查询Tool很简单,操作Tool不能这么简单

9. 在调用侧传入可信上下文

10. 权限也不能交给模型判断

11. Tool的返回值也不要随便设计

12. Tool描述其实也是Prompt Engineering的一部分

13. 为什么我不建议把所有Service都暴露给模型?

14. 一个企业级Tool,我认为至少要考虑这几个问题

15. Tool Calling和MCP是什么关系?

16. 从普通聊天到AI应用,真正变化在哪里?


前言

前几篇文章,我们已经逐渐把一个最基础的 AI 聊天接口,扩展成了一个真正能用于项目开发的 AI 应用。

从最开始直接调用模型 API,到 Spring AI,再到 SSE 流式输出、Memory、结构化输出。

但做到这里之后,我逐渐发现一个问题:

无论 Prompt 写得多好,大模型本质上还是在“说话”。

比如用户问:

帮我查一下订单 A1001 现在到哪了?

如果只是普通的 ChatModel,大模型并不知道数据库里有没有这个订单,更不知道它当前是待发货、运输中还是已经签收。

它最多只能回答:

您可以进入订单页面查看物流信息。

这显然不是我们真正想做的 AI 应用。

真正的企业 AI 应用,应该能够:

  • 查询订单
  • 获取物流状态
  • 查询库存
  • 创建工单
  • 查询客户信息
  • 调用内部 API
  • 执行业务操作

也就是说:

让大模型从“生成文本”,进一步变成“能够使用系统能力”。

这就是这篇文章要讲的:

Spring AI Tool Calling。

这篇不准备花太多篇幅介绍 API,而是直接做一个简单的“企业订单助手”。

通过这个项目看看:

  1. Tool Calling 到底解决什么问题;
  2. Spring AI 如何把普通 Java 方法变成 Tool;
  3. 大模型到底是怎么调用 Java 代码的;
  4. Tool 参数和返回值应该怎么设计;
  5. 查询类 Tool 和操作类 Tool 为什么不能一样处理;
  6. 企业项目中如何做权限、确认、审计和安全边界。

1. 为什么有了大模型,还需要 Tool Calling?

假设现在有这样一段代码:

String result = chatClient.prompt()
        .user("帮我查询订单A1001的物流状态")
        .call()
        .content();

从调用方式上看没有任何问题。

但问题在于:

模型没有订单数据。

订单 A1001 可能刚刚在一分钟之前完成发货,这些实时业务数据根本不可能存在于模型参数里。

即使我们把所有订单信息都塞进 Prompt,也不是一个合理方案。

企业系统真正的数据都存在于:

MySQL
Redis
ERP
CRM
订单中心
库存中心
物流系统
内部HTTP接口
第三方API

所以 AI 真正需要的,并不是“知道所有数据”。

而是:

需要数据的时候,知道应该调用哪个系统去获取。

这也是我现在理解 Tool Calling 最重要的一点。

Tool Calling 的价值,不是让模型变得更聪明,而是让模型拥有调用外部能力的入口。

Spring AI 官方对 Tool Calling 的处理流程,本质上也是:

用户问题
   ↓
把可用Tool定义发送给模型
   ↓
模型判断是否需要Tool
   ↓
返回Tool名称 + 参数
   ↓
应用程序执行Java方法
   ↓
Tool执行结果返回给模型
   ↓
模型结合结果生成最终回答

也就是说,并不是大模型真的进入 JVM 调用了我们的 Java 方法。

模型负责的是:

判断调用哪个Tool
+
生成Tool需要的参数

真正执行 Java 代码的,仍然是我们的应用程序。Spring AI 会负责完成 Tool 定义、参数 Schema、调用和结果回传等中间工作。

理解这一点之后,Tool Calling 就没有那么神秘了。


2. 这次做一个什么项目?

这次不做天气查询这种 Demo。

我们做一个更贴近企业开发的场景:

AI订单助手

先实现两个能力:

queryOrder
查询订单状态

cancelOrder
取消订单

用户可以直接和 AI 对话:

用户:
帮我查一下订单 A1001。

AI:
订单 A1001 当前状态为“待发货”,
下单时间为 2026-08-08 14:30,
订单金额为 299 元。

如果用户继续说:

帮我取消这个订单。

AI 就可以调用取消订单 Tool。

不过这里马上会遇到一个非常重要的问题:

查询订单和取消订单,风险完全不同。

查询属于:

Read

取消订单属于:

Write

所以真实项目里,我不会简单地把两个 Java 方法都加个 @Tool 就结束。

我们后面会分别处理。


3. 先准备订单业务层

首先定义一个订单返回对象:

public record OrderInfo(
        String orderNo,
        String status,
        BigDecimal amount,
        LocalDateTime createTime
) {
}

再定义取消订单结果:

public record CancelOrderResult(
        boolean success,
        String code,
        String message
) {
}

为了方便演示,这里暂时不连接真实数据库。

定义一个普通的 OrderService:

@Service
public class OrderService {

    public OrderInfo queryOrder(String orderNo, String userId) {

        // 实际项目:
        // 1. 查询数据库
        // 2. 校验订单是否属于当前用户
        // 3. 返回订单信息

        if (!"A1001".equals(orderNo)) {
            throw new IllegalArgumentException("订单不存在");
        }

        return new OrderInfo(
                orderNo,
                "待发货",
                new BigDecimal("299.00"),
                LocalDateTime.of(2026, 8, 8, 14, 30)
        );
    }

    public CancelOrderResult cancelOrder(
            String orderNo,
            String userId,
            String reason
    ) {

        // 实际项目中这里应该:
        // 1. 查询订单
        // 2. 校验订单归属
        // 3. 校验订单状态
        // 4. 执行取消
        // 5. 记录操作日志

        return new CancelOrderResult(
                true,
                "SUCCESS",
                "订单已取消"
        );
    }
}

到这里为止,它还是一个再普通不过的 Spring Service。

没有任何 AI 代码。

真正有意思的是下一步。


4. 把Java业务能力变成Tool

Spring AI 提供了一个非常直观的方式:

@Tool

我们新建:

@Component
public class OrderTools {

    private final OrderService orderService;

    public OrderTools(OrderService orderService) {
        this.orderService = orderService;
    }

    @Tool(description = """
            查询当前用户的订单信息。
            当用户询问订单状态、订单金额、下单时间等信息时使用。
            """)
    public OrderInfo queryOrder(
            @ToolParam(description = "订单编号,例如A1001")
            String orderNo,
            ToolContext toolContext
    ) {

        String userId = (String) toolContext
                .getContext()
                .get("userId");

        return orderService.queryOrder(orderNo, userId);
    }
}

就这么几行。

一个普通 Java 方法就变成了模型可以使用的 Tool。

Spring AI 会根据方法信息生成 Tool Definition,其中包括:

Tool名称
Tool描述
参数JSON Schema
参数是否必填
返回结果

@ToolParam 则可以帮助我们进一步描述参数格式和含义。Spring AI 默认会将方法参数映射为模型能够理解的 JSON Schema。

例如模型看到的大致信息可以理解为:

{
  "name": "queryOrder",
  "description": "查询当前用户的订单信息",
  "parameters": {
    "orderNo": {
      "type": "string",
      "description": "订单编号,例如A1001"
    }
  }
}

所以为什么 Tool 的 description 很重要?

因为模型选择 Tool,本质上依赖的就是:

用户问题
+
Tool名称
+
Tool描述
+
参数Schema

它并不知道:

queryOrder()

里面究竟写了什么 Java 代码。


5. 把Tool交给ChatClient

Tool 定义好了以后,还需要告诉 ChatClient:

这次对话可以使用哪些 Tool。

@Service
public class OrderAiService {

    private final ChatClient chatClient;

    private final OrderTools orderTools;

    public OrderAiService(
            ChatClient.Builder builder,
            OrderTools orderTools
    ) {

        this.chatClient = builder
                .defaultSystem("""
                        你是企业订单助手。

                        订单相关的实时信息必须通过系统提供的工具查询,
                        不允许根据已有知识猜测订单状态。

                        如果工具返回不存在、无权限或操作失败,
                        应明确告诉用户,不得自行编造结果。
                        """)
                .build();

        this.orderTools = orderTools;
    }

    public String chat(
            String message,
            String userId
    ) {

        return chatClient.prompt()
                .user(message)
                .tools(orderTools)
                .toolContext(Map.of(
                        "userId", userId
                ))
                .call()
                .content();
    }
}

Spring AI 的 ChatClient 支持通过 tools() 为当前请求注册 Tool,也可以通过 defaultTools() 给整个 ChatClient 注册默认 Tool。

这里我更喜欢:

.tools(orderTools)

原因很简单。

企业系统中的 Tool 一多,我一般不会把所有 Tool 永久暴露给模型。

例如:

订单助手
→ OrderTools

客服助手
→ CustomerTools
→ TicketTools

HR助手
→ EmployeeTools
→ PolicyTools

报表助手
→ ReportTools

不同业务场景,只提供真正需要的 Tool。

这比把几十个 Tool 一股脑全部交给模型更合理。


6. Controller就非常简单了

定义请求对象:

public record AiChatRequest(String message) {
}

Controller:

@RestController
@RequestMapping("/api/ai/order")
public class OrderAiController {

    private final OrderAiService orderAiService;

    public OrderAiController(
            OrderAiService orderAiService
    ) {
        this.orderAiService = orderAiService;
    }

    @PostMapping("/chat")
    public String chat(
            @RequestBody AiChatRequest request
    ) {

        // Demo写死
        // 真实项目应该从SecurityContext获取
        String userId = "USER_001";

        return orderAiService.chat(
                request.message(),
                userId
        );
    }
}

调用:

{
  "message": "帮我查询订单A1001现在是什么状态"
}

整个调用链大致就是:

Controller
    ↓
OrderAiService
    ↓
ChatClient
    ↓
模型判断需要查询订单
    ↓
queryOrder
    ↓
OrderService
    ↓
数据库 / 订单系统
    ↓
OrderInfo
    ↓
返回模型
    ↓
生成自然语言结果

这时候 AI 已经不再只是回答问题。

它开始真正进入我们的业务系统。


7. Tool Calling最容易理解错的一点

刚开始接触 Function Calling / Tool Calling 时,我自己也容易产生一种理解:

模型是不是直接调用了我的Java方法?

实际上并不是。

假设用户输入:

帮我查订单A1001

模型第一次返回的可能并不是最终答案,而是类似:

{
  "tool": "queryOrder",
  "arguments": {
    "orderNo": "A1001"
  }
}

Spring AI 收到以后,再执行:

queryOrder("A1001", toolContext);

得到:

{
  "orderNo": "A1001",
  "status": "待发货",
  "amount": 299.00
}

然后把这个结果再次交给模型。

模型最后生成:

订单A1001目前处于待发货状态,
订单金额299元。

所以整个过程至少可以理解成两次推理阶段:

第一次:
决定是否调用Tool

第二次:
结合Tool结果生成回答

Spring AI 使用 ChatClient 时,会帮我们管理这套 Tool Calling 生命周期。

这也是为什么 Tool Calling 的一次请求,可能比普通聊天接口:

耗时更长
Token更多
链路更复杂

后面做性能治理的时候,这些都需要考虑。


8. 查询Tool很简单,操作Tool不能这么简单

现在增加:

@Tool
public void cancelOrder(String orderNo) {
    orderService.cancelOrder(orderNo);
}

代码当然能跑。

但我认为这在真实项目里是不够安全的。

因为模型可能:

参数识别错误
意图判断错误
上下文理解错误
Tool选择错误

而:

查错一次订单

和:

误取消一次订单

完全不是一个风险等级。

所以对于有副作用的 Tool,我通常至少会增加一层:

用户明确确认。

例如:

@Tool(description = """
        取消当前用户尚未发货的订单。
        该工具只能在用户已经明确确认取消订单之后执行。
        """)
public CancelOrderResult cancelOrder(
        @ToolParam(description = "需要取消的订单编号")
        String orderNo,

        @ToolParam(
                description = "取消订单的原因",
                required = false
        )
        String reason,

        ToolContext toolContext
) {

    String userId = (String) toolContext
            .getContext()
            .get("userId");

    Boolean confirmed = (Boolean) toolContext
            .getContext()
            .get("confirmed");

    if (!Boolean.TRUE.equals(confirmed)) {

        return new CancelOrderResult(
                false,
                "NEED_CONFIRMATION",
                "取消订单属于重要操作,需要用户明确确认"
        );
    }

    return orderService.cancelOrder(
            orderNo,
            userId,
            reason
    );
}

这里我没有把:

confirmed

设计成普通 Tool 参数。

这是一个非常重要的细节。

如果这样设计:

cancelOrder(
    String orderNo,
    boolean confirmed
)

那么 confirmed 是谁提供的?

模型。

模型完全可能自己生成:

{
  "confirmed": true
}

这实际上根本不能证明用户真的确认过。

所以我更倾向于把这种可信上下文放进:

ToolContext

例如:

userId
tenantId
role
confirmed
traceId

这些信息由我们的应用程序提供,而不是让模型生成。

Spring AI 本身就支持通过 ToolContext 向 Tool 传递额外的应用上下文。


9. 在调用侧传入可信上下文

修改 Service:

public String chat(
        String message,
        String userId,
        boolean confirmed
) {

    Map<String, Object> context = new HashMap<>();

    context.put("userId", userId);
    context.put("confirmed", confirmed);
    context.put(
            "traceId",
            UUID.randomUUID().toString()
    );

    return chatClient.prompt()
            .user(message)
            .tools(orderTools)
            .toolContext(context)
            .call()
            .content();
}

第一次用户说:

帮我取消订单A1001

系统传:

confirmed = false;

Tool 返回:

NEED_CONFIRMATION

AI 最终可以回答:

订单A1001可以申请取消。

取消订单属于重要操作,
请确认是否继续取消。

用户再次明确:

确认取消订单A1001。

业务系统确认这次请求满足确认条件之后,再传入:

confirmed = true;

这时 Tool 才真正执行取消操作。

这比单纯依赖 Prompt:

“重要操作一定要先询问用户”

可靠得多。

因为 Prompt 是模型约束。

而:

if (!confirmed) {
    return ...
}

才是代码约束。

在企业系统里,这两个完全不是一个级别。


10. 权限也不能交给模型判断

类似的问题还有权限。

千万不要在 Prompt 里面写:

普通用户只能查询自己的订单,
管理员可以查询全部订单。

然后希望模型自己遵守。

真正的权限应该仍然存在 Java 业务层。

例如:

public OrderInfo queryOrder(
        String orderNo,
        String userId
) {

    Order order = orderRepository
            .findByOrderNo(orderNo)
            .orElseThrow();

    if (!order.getUserId().equals(userId)) {
        throw new AccessDeniedException(
                "无权访问该订单"
        );
    }

    return convert(order);
}

Tool Calling 并没有改变企业应用原有的安全模型。

原来应该有的:

认证
授权
参数校验
数据权限
幂等
事务
审计
限流
异常处理

接入 AI 以后一个都不能少。

甚至因为中间多了一层不确定性模型,这些约束应该更加严格。


11. Tool的返回值也不要随便设计

例如查询订单,我们当然可以直接:

return order.toString();

但我一般不推荐。

更好的方式还是返回:

OrderInfo

这样的结构化对象:

public record OrderInfo(
        String orderNo,
        String status,
        BigDecimal amount,
        LocalDateTime createTime
) {
}

这样有几个好处:

第一,字段稳定。

第二,模型更容易理解数据。

第三,后面增加日志和测试更容易。

第四,不会把数据库 Entity 中不应该暴露的字段全部传给模型。

例如订单 Entity 可能还有:

内部成本
渠道编号
数据库ID
用户内部ID
逻辑删除字段
风控状态
内部备注

这些字段根本没有必要进入模型上下文。

所以我更倾向于:

Entity
   ↓
Service
   ↓
Tool DTO
   ↓
LLM

而不是:

Entity
   ↓
LLM

这其实还是传统 Java 企业开发的 DTO 思想。

到了 AI 项目里依然适用。


12. Tool描述其实也是Prompt Engineering的一部分

例如:

@Tool(description = "查询订单")

当然可以。

但模型真正面对多个 Tool 时:

queryOrder
queryLogistics
queryRefund
createTicket
cancelOrder

过于模糊的描述就很容易出现选择错误。

我更建议写清楚:

@Tool(description = """
        查询当前登录用户的订单基础信息,
        包括订单状态、金额和创建时间。

        当用户询问订单是否存在、
        订单当前状态、订单金额、
        下单时间时使用。

        本工具不负责物流查询和退款查询。
        """)

这里其实已经不是传统 Java 注解的思路了。

它本质上是在告诉模型:

这个工具是什么
什么时候使用
能做什么
不能做什么

所以 Tool Description,本身也是 Prompt Engineering 的一部分。

Spring AI 官方也特别强调,Tool 的描述会帮助模型判断何时以及如何使用工具,描述不清楚可能导致模型不调用或者错误调用 Tool。


13. 为什么我不建议把所有Service都暴露给模型?

做到这里很容易产生一个想法:

那我把所有 Service 方法都加上 @Tool,不就拥有一个超级 AI 助手了吗?

技术上当然可以。

工程上我不推荐。

例如一个订单系统可能有:

OrderService
InventoryService
PaymentService
RefundService
CustomerService
LogisticsService
CouponService
InvoiceService

每个 Service 又有几十个方法。

如果全部暴露给模型,会出现几个问题。

13.1 Tool选择越来越困难

模型每次都需要从大量 Tool 中选择。

工具越多,语义越接近,就越容易选错。


13.2 Tool定义本身也消耗上下文

Tool 不是免费的。

Tool 的:

名称
描述
参数Schema

都需要提供给模型。

工具越多,额外 Token 成本就越高。


13.3 安全边界会越来越难控制

不是所有业务接口都应该让模型拥有调用资格。

特别是:

删除
付款
退款
审批
修改权限
批量操作

应该非常谨慎。


13.4 AI能力层和业务Service不应该完全耦合

我更喜欢增加一层:

Controller
    ↓
AI Service
    ↓
Tools
    ↓
Domain Service
    ↓
Repository / RPC

也就是:

OrderTools
CustomerTools
TicketTools

专门负责 AI 与业务系统之间的能力映射。

这样后续才能在这一层统一做:

参数转换
权限校验
结果裁剪
操作确认
Tool审计
异常转换

代码结构也会清晰很多。


14. 一个企业级Tool,我认为至少要考虑这几个问题

真正把 Tool Calling 用到项目里以后,我现在会重点检查下面几个问题。

14.1 Tool是否足够单一?

不要设计:

processOrder

这种什么都能干的 Tool。

应该拆成:

queryOrder
queryLogistics
cancelOrder
createRefundRequest

14.2 参数是否明确?

不要:

String data

而应该:

String orderNo
String reason

必要的时候使用 DTO。


14.3 Tool Description是否明确?

让模型知道:

什么时候用
什么时候不用
参数代表什么
能力边界是什么

14.4 返回结果是否结构化?

尽量:

DTO / record

而不是随意拼接 String。


14.5 权限是否来自可信上下文?

类似:

userId
tenantId
role

不要让模型自己提供。


14.6 有副作用的操作是否二次确认?

特别是:

删除
取消
审批
付款
退款
修改

不要只依赖 Prompt。


14.7 是否有审计?

至少能够知道:

谁
什么时候
哪次对话
调用了哪个Tool
传了什么业务参数
执行是否成功
耗时多少

14.8 Tool失败以后怎么办?

真实系统里 Tool 一定会失败。

例如:

数据库超时
RPC异常
订单不存在
权限不足
参数错误
第三方服务不可用

这些错误不能直接变成:

500

更不能让模型自己猜一个结果。

应该转换成模型能够理解的明确业务状态。


15. Tool Calling和MCP是什么关系?

学到这里之后,下一个非常容易遇到的问题就是:

Tool Calling 和 MCP 到底有什么区别?

我现在的理解很简单。

Tool Calling 解决的是:

模型如何决定并调用一个工具。

而 MCP 更进一步解决:

不同AI应用如何以统一协议发现和使用外部工具。

今天我们写:

@Tool
public OrderInfo queryOrder(...)

Tool 和当前 Spring Boot 项目运行在一起。

后面如果订单能力来自:

独立订单服务
远程工具服务
其他团队维护的系统

就可能继续涉及:

MCP Client
MCP Server
Tool注册
Tool发现
远程调用

但是无论后面是本地 Tool 还是 MCP Tool,模型最终使用的核心思想其实没有变化:

理解用户意图
    ↓
选择工具
    ↓
准备参数
    ↓
执行工具
    ↓
获取结果
    ↓
继续推理

所以我认为 Tool Calling 是后面理解 Agent 和 MCP 非常关键的一块基础。


16. 从普通聊天到AI应用,真正变化在哪里?

回头看这个系列最开始的代码:

restTemplate.postForObject(
        url,
        request,
        String.class
);

它解决的是:

Java怎么调用大模型?

后来换成 Spring AI:

chatClient.prompt()
        .user(question)
        .call()
        .content();

解决的是:

怎么更工程化地使用大模型?

现在有了 Tool Calling:

chatClient.prompt()
        .user(question)
        .tools(orderTools)
        .call()
        .content();

看起来可能只是增加了一行:

.tools(orderTools)

但它背后的意义完全不同。

因为从这里开始:

AI终于可以真正进入业务系统。

以前:

用户
 ↓
模型
 ↓
回答

现在:

用户
 ↓
模型
 ↓
业务系统
 ↓
实时数据 / 真实操作
 ↓
模型
 ↓
回答

这也是我认为:

Tool Calling 是从“AI聊天功能”走向“AI应用开发”的一个明显分界点。


17. 总结

这篇我们没有做很复杂的 Agent。

只实现了两个非常简单的 Tool:

queryOrder
cancelOrder

但是通过这个例子,实际上已经把 Tool Calling 最核心的工程逻辑走了一遍。

Spring AI 中,可以通过:

@Tool

把普通 Java 方法变成模型可使用的工具。

通过:

@ToolParam

描述工具参数。

通过:

.tools(...)

把 Tool 提供给当前 ChatClient。

通过:

ToolContext

传递:

用户身份
租户
权限
确认状态
Trace信息

这些不应该由模型生成的可信上下文。

更重要的是:

不要因为接入了大模型,就推翻原来的企业开发原则。

权限还是权限。

事务还是事务。

参数校验还是参数校验。

审计还是审计。

幂等还是幂等。

AI 负责的是理解自然语言和选择能力。

真正能不能执行、允许执行到什么程度,最终还是应该由我们的业务代码决定。

我现在越来越觉得,Java 开发转 AI 真正需要补的,并不是把过去十年的经验全部扔掉重新学习。

恰恰相反。

当 AI 开始真正进入企业业务以后:

Spring
权限
事务
微服务
缓存
数据库
接口治理
稳定性
安全

这些传统后端能力反而重新变得非常重要。

而 Tool Calling,就是把这两部分真正连接起来的其中一座桥。


📚 推荐专栏

🏗️ 企业AI架构与实战

从企业真实落地视角,持续分享RAG、Agent、Workflow、AI Gateway、工程治理与系统架构设计。

👉 https://blog.csdn.net/qupengkun/category_13192323.html

🤖 Java转AI技术专栏

从0到1学习AI应用开发,持续分享Spring AI、RAG、Agent与企业级AI项目实战。

👉 https://blog.csdn.net/qupengkun/category_13184360.html

🧩 AI开源项目实战

持续分享Java AI业务项目、AI Coding效率工具和工程治理实践。

👉 https://blog.csdn.net/qupengkun/category_13189373.html

🚀 AI转型日记

记录一名10年Java开发者从传统后端转向AI应用开发的全过程。

👉 https://blog.csdn.net/qupengkun/category_13183497.html


👨‍💻 关于作者

QCoding

专注AI应用开发与Java技术实践。

持续分享Spring AI、RAG、Agent、企业级AI项目实战、架构设计与职业成长。

更多推荐