Spring AI Tool Calling实战,让大模型真正调用你的业务系统|Java转AI第7课
目录
12. Tool描述其实也是Prompt Engineering的一部分
前言
前几篇文章,我们已经逐渐把一个最基础的 AI 聊天接口,扩展成了一个真正能用于项目开发的 AI 应用。
从最开始直接调用模型 API,到 Spring AI,再到 SSE 流式输出、Memory、结构化输出。
但做到这里之后,我逐渐发现一个问题:
无论 Prompt 写得多好,大模型本质上还是在“说话”。
比如用户问:
帮我查一下订单 A1001 现在到哪了?
如果只是普通的 ChatModel,大模型并不知道数据库里有没有这个订单,更不知道它当前是待发货、运输中还是已经签收。
它最多只能回答:
您可以进入订单页面查看物流信息。
这显然不是我们真正想做的 AI 应用。
真正的企业 AI 应用,应该能够:
- 查询订单
- 获取物流状态
- 查询库存
- 创建工单
- 查询客户信息
- 调用内部 API
- 执行业务操作
也就是说:
让大模型从“生成文本”,进一步变成“能够使用系统能力”。
这就是这篇文章要讲的:
Spring AI Tool Calling。
这篇不准备花太多篇幅介绍 API,而是直接做一个简单的“企业订单助手”。
通过这个项目看看:
- Tool Calling 到底解决什么问题;
- Spring AI 如何把普通 Java 方法变成 Tool;
- 大模型到底是怎么调用 Java 代码的;
- Tool 参数和返回值应该怎么设计;
- 查询类 Tool 和操作类 Tool 为什么不能一样处理;
- 企业项目中如何做权限、确认、审计和安全边界。
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项目实战、架构设计与职业成长。
更多推荐


所有评论(0)