Java Tool Calling

Java Tool Calling 的核心是把 Java 方法注册成模型可调用的工具,用 mock 数据先跑通“模型选择工具、后端执行工具、模型生成最终回答”的链路。

Java Tool Calling 是什么

Java Tool Calling,中文可以理解为“Java 工具调用”。

它指的是在 Java 项目里把方法暴露为大模型可选择的工具。

模型不会直接执行 Java 方法。

流程是:

Java 定义工具方法
  -> Spring AI 生成工具定义
  -> 模型选择工具并生成参数
  -> Spring AI 调用 Java 方法
  -> 方法结果回填给模型
  -> 模型生成最终回答

结论:

Java Tool Calling 本质上是把业务方法包装成模型可理解、后端可执行的工具。

适合做工具的方法

适合:

查询订单
查询库存
查询用户信息
创建工单
发送通知
计算价格

不适合直接暴露:

删除数据
批量修改
高风险支付
无权限校验的内部接口
不可回滚操作

学习阶段先做 mock。

Mock,中文一般翻译为“模拟数据”或“模拟对象”。

先不用真实数据库,避免把重点搞复杂。

注册 Java 方法为 Tool

Spring AI 可以用 @Tool 注解把方法声明为工具。

示例:

import org.springframework.ai.tool.annotation.Tool;

public class OrderTools {

    @Tool(description = "根据订单号查询订单状态")
    public String queryOrder(String orderId) {
        return "订单 " + orderId + " 当前状态:已发货";
    }
}

重点是 description

它会影响模型什么时候选择这个工具。

描述要写清楚:

工具用途
输入参数
适用场景
不适用场景

订单查询 mock

先定义订单结果:

public record OrderInfo(
        String orderId,
        String status,
        String trackingNo
) {
}

工具类:

import org.springframework.ai.tool.annotation.Tool;

import java.util.Map;

public class OrderTools {

    private static final Map<String, OrderInfo> ORDERS = Map.of(
            "A10001", new OrderInfo("A10001", "已发货", "SF123456"),
            "A10002", new OrderInfo("A10002", "待发货", "")
    );

    @Tool(description = "根据订单号查询订单状态。只用于查询订单发货、物流和当前处理状态。")
    public OrderInfo queryOrder(String orderId) {
        OrderInfo orderInfo = ORDERS.get(orderId);
        if (orderInfo == null) {
            return new OrderInfo(orderId, "未查询到订单", "");
        }
        return orderInfo;
    }
}

用户问题:

帮我查一下订单 A10001 发货了吗?

期望模型选择 queryOrder

库存查询 mock

库存结果:

public record StockInfo(
        String skuId,
        String productName,
        int availableStock
) {
}

工具类:

import org.springframework.ai.tool.annotation.Tool;

import java.util.Map;

public class StockTools {

    private static final Map<String, StockInfo> STOCKS = Map.of(
            "SKU-001", new StockInfo("SKU-001", "机械键盘", 12),
            "SKU-002", new StockInfo("SKU-002", "无线鼠标", 0)
    );

    @Tool(description = "根据商品 SKU 查询可售库存。只用于库存数量查询。")
    public StockInfo queryStock(String skuId) {
        StockInfo stockInfo = STOCKS.get(skuId);
        if (stockInfo == null) {
            return new StockInfo(skuId, "未知商品", 0);
        }
        return stockInfo;
    }
}

用户问题:

SKU-001 还有库存吗?

期望模型选择 queryStock

在 ChatClient 中使用工具

Service 示例:

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.stereotype.Service;

@Service
public class AiToolService {

    private final ChatClient chatClient;

    public AiToolService(ChatClient.Builder builder) {
        this.chatClient = builder
                .defaultSystem("""
                        你是一个售后客服助手。
                        如果用户询问订单状态,优先使用订单查询工具。
                        如果用户询问库存,优先使用库存查询工具。
                        不要编造订单或库存结果。
                        """)
                .build();
    }

    public String chat(String message) {
        return chatClient.prompt()
                .user(message)
                .tools(new OrderTools(), new StockTools())
                .call()
                .content();
    }
}

这里每次请求都把工具传给模型。

如果是全局默认工具,也可以在构建 ChatClient 时配置默认工具。

学习阶段先用 .tools(...) 更直观。

Controller 示例

import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/tool-chat")
public class ToolChatController {

    private final AiToolService aiToolService;

    public ToolChatController(AiToolService aiToolService) {
        this.aiToolService = aiToolService;
    }

    @PostMapping
    public ChatResponse chat(@RequestBody ChatRequest request) {
        String answer = aiToolService.chat(request.message());
        return new ChatResponse(answer);
    }
}

请求:

curl -X POST http://localhost:8080/api/tool-chat \
  -H "Content-Type: application/json" \
  -d '{"message":"帮我查一下订单 A10001 发货了吗?"}'

期望结果:

订单 A10001 已发货,物流单号是 SF123456。

工具参数校验

模型生成的参数不能直接信任。

订单号可以先做简单校验:

private void validateOrderId(String orderId) {
    if (orderId == null || orderId.isBlank()) {
        throw new IllegalArgumentException("订单号不能为空");
    }
    if (!orderId.matches("[A-Z][0-9]{5}")) {
        throw new IllegalArgumentException("订单号格式不正确");
    }
}

在工具里使用:

@Tool(description = "根据订单号查询订单状态。订单号格式示例:A10001。")
public OrderInfo queryOrder(String orderId) {
    validateOrderId(orderId);
    return ORDERS.getOrDefault(orderId, new OrderInfo(orderId, "未查询到订单", ""));
}

后面接真实业务系统时,还要加:

用户身份
订单归属校验
接口限流
审计日志
异常处理

调试工具调用日志

调试时要看清楚几件事:

模型有没有选择工具
选择的是哪个工具
参数是什么
工具是否执行成功
工具返回了什么
最终回答是否引用了工具结果

工具方法里可以先加日志:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class OrderTools {

    private static final Logger log = LoggerFactory.getLogger(OrderTools.class);

    @Tool(description = "根据订单号查询订单状态")
    public OrderInfo queryOrder(String orderId) {
        log.info("queryOrder called, orderId={}", orderId);
        return new OrderInfo(orderId, "已发货", "SF123456");
    }
}

日志不要打印敏感信息。

真实项目里要避免打印:

手机号
身份证
详细地址
API Key
完整用户隐私数据

工具调用失败怎么处理

常见失败:

参数缺失
参数格式错误
业务接口超时
订单不存在
库存服务不可用
模型选错工具

处理方式:

参数错误:返回明确错误
订单不存在:返回未查询到
接口超时:返回稍后重试
模型选错:优化工具描述和 Prompt

工具异常不要直接把堆栈返回给模型或用户。

可以返回业务可理解的信息:

return new OrderInfo(orderId, "查询失败,请稍后重试", "");

常见问题

为什么模型没有调用工具

先检查:

模型是否支持 Tool Calling
调用时是否传入 tools
工具 description 是否清楚
用户问题是否真的需要工具
system prompt 是否说明了工具使用规则

为什么调用错工具

可能是工具描述太模糊。

比如:

订单工具
库存工具

不如写成:

根据订单号查询订单状态。只用于订单发货、物流、售后进度查询。
根据 SKU 查询商品可售库存。只用于库存数量查询。

mock 有什么意义

mock 可以先验证 Tool Calling 链路。

不需要一开始就连数据库、权限和真实接口。

先把链路跑通,再替换为真实业务服务。

练习清单

完成几件事情:

能用 @Tool 注册 Java 方法
能写订单查询 mock 工具
能写库存查询 mock 工具
能把工具传给 ChatClient
能用 curl 触发工具调用
能在日志里看到工具调用参数
知道工具参数要校验
知道工具异常要兜底

建议目录:

ai-agent-study
├── java
│   └── tool-calling-demo
│       ├── ToolChatController.java
│       ├── AiToolService.java
│       ├── tools
│       │   ├── OrderTools.java
│       │   └── StockTools.java
│       └── dto
│           ├── OrderInfo.java
│           └── StockInfo.java
└── docs
    └── java-tool-calling.md

小结

本节的结论:

Java Tool Calling 先用 @Tool 和 mock 数据跑通,再逐步接真实业务接口。

最小链路:

用户问题
  -> ChatClient
  -> 模型选择工具
  -> Java 方法执行
  -> 工具结果回填
  -> 模型生成回答

工具调用进入真实业务前,必须补权限、校验、日志和异常兜底。

参考资料


这个家伙很懒,啥也没有留下😋