做 AI 订单客服,最先遇到的问题往往很实际。
用户发来一条消息:
帮我查一下订单 A1024 到哪了。
订单数据存在业务系统里,模型手里没有这条记录。要给它提供一个可以调用的 Java 方法:
queryOrder("A1024")
这就是 Tool Calling。
我把本次查询的每一轮消息都打印出来了。结果就是:1 次用户请求,内部调用 2 次模型,执行 1 次 Java 工具。
这篇就用这个订单案例跑通 Spring AI 2.0 Tool Calling,看清楚两次模型调用之间发生了什么。
先看运行结果
我在 ToolCallingAdvisor 循环内外各放了一个日志 Advisor,运行之后得到了下面这条轨迹:
[outside-loop] before #1 messages=[user]
[inside-loop] before #1 messages=[user]
[model] call #1
[inside-loop] after #1 hasToolCalls=true
[tool] queryOrder(A1024), tenantId=shop-001
[inside-loop] before #2 messages=[user, assistant, tool]
[model] call #2
[model] received tool result: {"orderId":"A1024","status":"SHIPPED","carrier":"顺丰","trackingNo":"SF1234567890"}
[inside-loop] after #2 hasToolCalls=false
[outside-loop] after #1 hasToolCalls=false
[answer] 查到了:订单 A1024 已发货,承运商是顺丰,运单号 SF1234567890。
案例使用 Java 21、Spring Boot 4.0.8 和 Spring AI 2.0.1。为了固定两轮响应,测试使用 ScriptedOrderChatModel 驱动 Spring AI 调用链。本文结论覆盖框架执行过程;真实模型选择工具的质量还需要另外测试。
第一步:创建订单工具
项目引入 spring-boot-starter-webmvc、spring-ai-starter-model-openai 和 Spring AI 2.0.1 BOM 后,先创建一个普通的 Spring Bean:
@Component
public class OrderTools {
@Tool(
name = "queryOrder",
description = "根据订单号查询物流状态、承运商和运单号"
)
public OrderSnapshot queryOrder(
@ToolParam(description = "订单号,例如 A1024")
String orderId,
ToolContext toolContext) {
String tenantId = (String) toolContext
.getContext()
.get("tenantId");
System.out.printf(
"[tool] queryOrder(%s), tenantId=%s%n",
orderId,
tenantId);
return orderService.query(tenantId, orderId);
}
}
这里有三个重要的地方。
@Tool 让模型知道这个方法能做什么
Spring AI 会读取工具名称、描述和方法签名,然后生成包含 JSON Schema 的 Tool Definition。工具描述会影响模型的选择,所以要写清适用场景和返回数据。
@ToolParam 说明参数应该怎么填
模型会把用户的提问转成 JSON 参数。订单号、日期和金额格式,都可以写进参数说明里。
ToolContext 传递本地上下文
tenantId 来自当前登录会话,由应用确定。官方文档提到,ToolContext 数据不会发给模型。用户 ID、租户 ID、权限和 Trace ID 都可以放在里面。
第二步:把工具交给 ChatClient
然后创建一个订单客服接口:
@RestController
public class OrderController {
private final ChatClient chatClient;
OrderController(
ChatClient.Builder builder,
OrderTools orderTools) {
this.chatClient = builder
.defaultTools(orderTools)
.build();
}
@GetMapping("/orders/ask")
String ask(@RequestParam String message) {
return chatClient.prompt()
.system("""
你是订单客服。
涉及订单状态时必须调用查询工具,
根据工具返回结果回答。
""")
.user(message)
.toolContext(Map.of("tenantId", "shop-001"))
.call()
.content();
}
}
.defaultTools(orderTools) 会把订单工具注册到这个 ChatClient 的每次请求中。某次请求才需要工具时,也可以在 prompt() 后调用 .tools(orderTools)。
Spring AI 2.0 的 ChatClient 会自动注册 ToolCallingAdvisor,常规业务代码不需要手动创建它。
到这里,最小的 Tool Calling 已经接上了。
两次模型调用做了什么
第一次模型请求会包含:
SystemMessage + UserMessage + ToolDefinition
模型读到“订单 A1024”后,就会发出一个 tool call:
{
"name": "queryOrder",
"arguments": {
"orderId": "A1024"
}
}
此时还没有查询订单表。这个 JSON 表示模型建议应用执行 queryOrder。
ToolCallingAdvisor 检查到 hasToolCalls=true 后,就会把响应交给 ToolCallingManager。Manager 根据名称找到工具,把 JSON 转成 Java 参数,然后执行方法。
工具返回:
{
"orderId": "A1024",
"status": "SHIPPED",
"carrier": "顺丰",
"trackingNo": "SF1234567890"
}
Spring AI 同时把这两条消息追加到当前调用的历史中:
AssistantMessage(tool_call: queryOrder)
ToolResponseMessage(order status JSON)
所以,第二轮请求的消息类型是:
messages=[user, assistant, tool]
第二次调用会读取工具结果,把 JSON 整理成用户可以理解的客服回答。如果这一轮仍然产生 tool call,ToolCallingAdvisor 还会进入下一轮。模型返回不包含 tool call 的回复后,循环就结束了。
整条链路可以简化为下面六步:
用户问题
↓
第一次调用模型:选择工具,生成参数
↓
ToolCallingManager:执行本地 Java 方法
↓
ToolResponseMessage:追加工具结果
↓
第二次调用模型:组织最终回答
↓
没有新 tool call,结束
四个组件,记住它们各自的作用
Spring AI 2.0 把工具执行循环提到 Advisor 链上,日志、Memory、重试和观测代码都可以根据顺序进入循环内外。
ToolCallingAdvisor 的默认顺序是 Ordered.HIGHEST_PRECEDENCE + 300。本次测试中,它前面的日志执行了 1 次,后面的日志跟着模型请求执行了 2 次。
请求审计应该放在循环外。要检查每轮模型延迟、工具参数和失败原因时,日志就要进入循环,同时对订单号和工具结果做好脱敏。
一个有用的对照:returnDirect
有些工具已经返回了前端可以直接使用的 JSON,可以设置为:
@Tool(
name = "queryOrderDirect",
description = "返回订单原始状态",
returnDirect = true
)
String queryOrderDirect(String orderId) {
return """
{"orderId":"%s","status":"SHIPPED"}
""".formatted(orderId);
}
对照测试的结果是:
[return-direct] {"orderId":"A1024","status":"SHIPPED"}
这条路径只调用 1 次模型、执行 1 次工具,原始结果直接返回客户端。对话式客服还需要第二轮模型来组织自然语言回答。
回到开头
用户只问了这么一句,Spring AI 内部调用了两次模型。
现在可以给出确切的答案:
- 第一次调用负责选择工具并生成参数。
- ToolCallingManager 在应用中调用 Java 方法。
- ToolResponseMessage 将查询结果放入消息历史。
- 第二次调用读取工具结果,再整理好答案。
把这四步记下来,之后再看多工具 Agent、Tool Search、Chat Memory 和 MCP,就会有一条明了的主线。
下一篇文章会解决一个实际问题:当 Agent 里有几十个工具时,怎么用 ToolSearchToolCallingAdvisor 只把当前需要的工具发给模型。
本文由 AI图鉴自动抓取并翻译,版权归原作者所有;如需查看原文请点下方链接。
The first problems encountered in making AI orders for customer service are often practical.
Other Organiser
Find out where the order A1024 is.
The order data is in the business system and is not recorded in the model. To give it a Java method to call:
queryOrder("A1024")
That's Tool Calling.
I printed out every round of information on this query. As a result: 1 user request, 2 internal calls to implement 1 Java tool.
This article runs through Spring AI 2.0 Tool Calling to see what happened between the two calls.
Look at the results first.
I put a log inside and outside the ToolCallingAdvisor cycle, and then I got the following track:
[outside-loop] before #1 messages=[user]
[inside-loop] before #1 messages=[user]
[model] call #1
[inside-loop] after #1 hasToolCalls=true
[tool] queryOrder(A1024), tenantId=shop-001
[inside-loop] before #2 messages=[user, assistant, tool]
[model] call #2
[Model] received tool result: {"orderId": "A1024", "status": "SHIPPED", "carrier": "Sun Fong", "tracking No": "SF1234567890"}
[inside-loop] after #2 hasToolCalls=false
[outside-loop] after #1 hasToolCalls=false
[answer] Found: Order A1024 has been shipped and the carrier is Sunfung, shipping order SF1234567890.
The cases used Java 21, Spring Boot 4.0.8 and Spring AI 2.0.1. To fix two rounds of response, the test uses the Scripted OrderChatModel driver Spring AI call chain. The conclusions of this paper cover the framework implementation process; the quality of the real model selection tool needs to be tested separately.
Step 1: Create an order tool
Create a normal Spring Bean:
@Component
public class OrderTools {
@Tool(
name = "queryOrder",
Decryption = "Query Logistics Status, Carrier and Shipping Line Number by Order"
)
public OrderSnapshot queryOrder(
@ToolParam
String orderId,
ToolContext toolContext) {
String tenantId = (String) toolContext
.getContext()
.get("tenantId");
System.out.printf(
"[tool] queryOrder(%s), tenantId=%s%n",
orderId,
tenantId);
return orderService.query(tenantId, orderId);
}
}
There are three important places here.
@Tool Let the model know what it can do
Spring AI will read the tool name, description and method signature, and then generate a Tool Defense that contains JSON Schema. Tool descriptions affect the selection of models, so the applicable scene and return data are written.
@ToolParam explains how parameters should be filled
The model converts user questions to JSON parameters. The order number, date and amount format can be included in the statement of parameters.
ToolContext Pass Local context
TenantId is from the current login session, as determined by the application. The official document mentions that the ToolContext data will not be sent to the model. User ID, tenant ID, permission and Trace ID can all be placed in it.
Step 2: Hand over the tools to ChatClint
Then create an order service interface:
@RestController
public class OrderController {
private final ChatClient chatClient;
OrderController(
ChatClient.Builder builder,
OrderTools orderTools) {
this.chatClient = builder
.defaultTools(orderTools)
.build();
}
@GetMapping("/orders/ask")
String ask(@RequestParam String message) {
return chatClient.prompt()
.system("""
You're the order customer.
When it comes to the order state, you must call the query tool.
Returns the result according to the tool.
""")
.user(message)
.toolContext(Map.of("tenantId", "shop-001"))
.call()
.content();
}
}
.defaultTools (orderTools) will register the order tools in each of the ChatClit requests. You can also call.tools (orderTools) after a request requires tools.
ChatClient for Spring AI 2.0 will automatically register ToolCallingAdvisor, and the normal business code does not need to be created manually.
Here, the smallest Tool Calling has been picked up.
What did the two models call for?
The first model request will include:
SystemMessage + UserMessage + ToolDefinition
When the model reads "order A1024", it sends out a tool call:
{
"name": "queryOrder",
"arguments": {
"orderId": "A1024"
}
}
The order form has not yet been checked. This JSON indicates that the model recommends the application of queryOrder.
ToolCallingAdvisor checkes to have ToolCalls=true and then gives the response to ToolCallingManager. Manager finds a tool by name to convert JSON to Java parameters, and then executes the method.
Tool returns:
{
"orderId": "A1024",
"status": "SHIPPED",
"Carrier": "Sun Fung",
"trackingNo": "SF1234567890"
}
Spring AI adds both messages to the current call history:
AssistantMessage(tool_call: queryOrder)
ToolResponseMessage(order status JSON)
So the type of information requested in the second round is:
messages=[user, assistant, tool]
The second call will read the results of the tool and make JSON an understandable client response. If this round still produces a Tool call, ToolCallingAdvisor will enter the next round. The loop ends when the model returns a response that does not contain a tool call.
The whole link can be simplified into the following six steps:
User problems
↓
First call model: select tool, generate parameters
↓
ToolCallingManager: Execute local Java method
↓
ToolResponseMessage: Add Tool Results
↓
Second call model: final organizational response
↓
No new tool call, over
Four components. Remember their respective roles.
Spring AI 2.0 refers the tool execution cycle to the Advisor chain, and logs, memory, retry and observation codes can enter the cycle and out in order.
The default order for ToolCallingAdvisor is Ordered.HIGHEST PRECEDENCE+300. For this test, one of its previous logs was performed and two of the latter logs followed the model request.
Requests for audits should be placed outside the cycle. To check the delay, tool parameters and cause of failure of each round of models, logs go into a cycle, while being unsensitized with order numbers and tool results.
A useful contrast: returnDirect
Some tools have returned to JSON, which can be used directly at the front end, and can be set to:
@Tool(
name = "queryOrderDirect",
Description = "Return to original order"
returnDirect = true
)
String queryOrderDirect(String orderId) {
return """
{"orderId":"%s","status":"SHIPPED"}
""".formatted(orderId);
}
The results of the control tests were:
[return-direct] {"orderId":"A1024","status":"SHIPPED"}
This path calls only one model, executes one tool, and the original result returns directly to the client. The second round of the dialogue service also requires a model to organize the natural language responses.
Back to the beginning.
The user asked just one sentence, and Spring AI used the model twice internally.
Now you can give the exact answer:
- First call to select tools and generate parameters.
- ToolCallingManager calls Java in application.
- ToolResponseMessage places the query results in message history.
- Second call to read the results of the tool, then sort out the answers.
Write down these four steps, and then look at the multiple tools Agent, Tool Search, Chat Memory and MCP, there's a clear line.
The next article will solve a practical problem: how to use ToolSearchToolCallAdvisor to send only the tools that are needed to model them when there are dozens of tools in Agent.
Auto-fetched and translated by AI Atlas. All rights belong to the original author.