摘要
大模型只能生成文本,无法天然知道订单状态、库存数量、用户权限或企业内部系统数据。要让 AI 应用完成真实业务任务,需要让模型提出结构化的工具调用请求,由服务端完成参数校验、权限判断和业务执行,再把工具结果交回模型生成最终回答。
Spring AI 提供了 Tool Calling 抽象,可以将 Java 方法、函数或 ToolCallback 暴露为模型可调用的工具。本文围绕“订单查询与售后客服”场景,介绍如何设计工具契约、注册工具、执行调用、处理多轮工具链,并重点讨论:
- Tool Calling 与普通 Prompt 的区别;
@Tool、MethodToolCallback和ToolCallback的使用方式;- 工具参数的结构化描述和校验;
- 用户权限、租户隔离和高风险操作审批;
- 模型连续调用多个工具时的状态管理;
- 流式输出、超时、重试、幂等和审计;
- 如何避免让模型直接获得数据库或 Shell 权限。
一、背景与问题
1. 仅靠 Prompt 无法访问业务系统
下面的 Prompt 只能让模型根据用户输入生成文字:
用户:查询订单 O1001 当前状态。
模型:我无法访问你的订单系统。
如果把订单数据直接写进 Prompt,又会出现数据过期、权限绕过和上下文泄露问题。
更合理的流程是:
用户问题
↓
模型判断需要查询订单
↓
模型生成结构化工具调用
↓
服务端校验并执行订单查询
↓
工具结果返回模型
↓
模型生成用户可读答案
2. Tool Calling 的基本结构
工具调用包含四个角色:
| 角色 | 作用 |
|---|---|
| 工具名称 | 标识要调用的业务能力 |
| 参数 Schema | 描述参数名称、类型和约束 |
| 服务端执行器 | 校验权限并执行真实业务逻辑 |
| 工具结果 | 将执行结果返回给模型或前端 |
模型只负责提出调用意图,不能直接绕过服务端执行器访问数据库。
3. 为什么不能把所有方法都暴露成工具
暴露工具意味着模型可能在满足条件时请求调用。以下能力需要特别谨慎:
- 修改订单;
- 关闭工单;
- 发送邮件和短信;
- 删除文件;
- 修改权限;
- 执行退款;
- 访问生产数据库;
- 执行 Shell 命令。
建议按风险分级:
只读查询
↓
草稿和预览
↓
用户确认
↓
执行写操作
二、核心概念
1. Function Calling 与 Tool Calling
Function Calling 侧重模型返回一个结构化函数名和参数;Tool Calling 是更宽泛的能力,工具可以是 Java 方法、HTTP API、数据库查询、搜索服务或人工审批流程。
应用层可以统一成:
public interface BusinessTool {
String name();
ToolResult execute(ToolContext context, JsonNode arguments);
}
2. Spring AI 的工具抽象
Spring AI 支持通过 @Tool 注解、ToolCallback、ToolCallbackProvider 或 Function 等方式提供工具。工具定义最终会转换为模型能够理解的名称、描述和参数 Schema。
业务服务通常使用:
ChatClient
↓
ToolCallback
↓
业务方法
↓
业务结果
3. 工具描述比方法名更重要
工具描述会参与模型决策。下面的描述过于模糊:
@Tool
public Object query(String id) {
...
}
更好的描述应说明:
- 什么时候使用;
- 参数代表什么;
- 返回结果包含什么;
- 不应该用于什么;
- 是否只读;
- 是否需要用户确认。
4. 工具调用不是权限模型
即使模型决定调用 queryOrder,服务端仍然要校验:
当前用户
↓
是否属于当前租户
↓
是否拥有订单访问权限
↓
订单是否属于当前用户
↓
是否允许当前 Agent 使用该工具
↓
执行查询
模型输出的参数、用户输入的订单号和客户端传入的租户 ID 都不能直接作为最终授权依据。
三、工作原理
1. 一次工具调用的完整流程
1. 服务端向模型发送工具定义
2. 模型返回 tool_call
3. 应用解析工具名称和 JSON 参数
4. 校验工具是否在白名单
5. 校验参数格式和业务权限
6. 执行 Java 业务方法
7. 保存工具调用记录
8. 将工具结果加入对话消息
9. 再次调用模型
10. 返回最终答案
2. 多工具调用
一个问题可能需要多个工具:
用户:我的订单什么时候到?如果超过承诺日期,能否申请补偿?
↓
查询订单
↓
查询物流
↓
查询售后政策
↓
模型整合结果
每个工具调用都应该有独立的 toolCallId、状态、参数摘要和结果摘要。
3. 工具调用状态
REQUESTED
↓
VALIDATING
├─ REJECTED
└─ APPROVED
↓
RUNNING
├─ COMPLETED
├─ FAILED
├─ TIMEOUT
└─ CANCELLED
4. 工具结果不等于最终答案
工具结果可能是内部结构化数据:
{
"orderId": "O1001",
"status": "SHIPPED",
"internalRiskScore": 0.81
}
最终返回给用户时,不应该自动暴露 internalRiskScore。工具执行结果需要经过输出策略或 DTO 转换。
四、实战示例
1. 定义只读订单工具
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;
@Component
public class OrderTools {
private final OrderQueryService orderQueryService;
public OrderTools(OrderQueryService orderQueryService) {
this.orderQueryService = orderQueryService;
}
@Tool(description = """
查询当前用户有权访问的订单状态。
只用于订单查询,不执行修改、取消或退款。
当用户没有提供明确订单号时,不要猜测订单号。
""")
public OrderSummary queryOrder(
OrderToolContext context,
String orderId) {
return orderQueryService.queryOwnedOrder(
context.tenantId(),
context.userId(),
orderId
);
}
}
OrderToolContext 不应该由模型填写,应该由服务端从认证上下文生成:
public record OrderToolContext(
UUID tenantId,
UUID userId,
String requestId) {
}
2. 使用 ToolCallback
import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;
ToolCallback[] callbacks = ToolCallbacks.from(
orderTools
);
String answer = chatClient.prompt()
.user(question)
.tools(callbacks)
.call()
.content();
不同 Spring AI 版本中工具 API 的包名和方法名可能变化,项目应以锁定版本的官方 Tool Calling 文档为准。
3. 使用 ChatClient 注册工具
@Service
public class CustomerAssistant {
private final ChatClient chatClient;
private final ToolCallback[] orderToolCallbacks;
public CustomerAssistant(
ChatClient.Builder builder,
OrderTools orderTools) {
this.chatClient = builder.build();
this.orderToolCallbacks = ToolCallbacks.from(orderTools);
}
public String answer(String question) {
return chatClient.prompt()
.system("""
你是企业客服助手。
查询订单时只能使用工具返回的事实。
工具调用失败时说明暂时无法查询,
不要编造订单状态。
""")
.user(question)
.tools(orderToolCallbacks)
.call()
.content();
}
}
4. 工具参数校验
模型返回的 JSON 仍然是不可信输入:
public record QueryOrderRequest(
@NotBlank
@Pattern(regexp = "^[A-Z0-9-]{4,32}$")
String orderId) {
}
在执行前还需要:
if (!orderIdBelongsToUser(
context.tenantId(),
context.userId(),
request.orderId())) {
throw new AccessDeniedException("order is not accessible");
}
5. 高风险工具增加确认
退款工具不应该和查询工具一样自动执行:
public record ToolApproval(
String approvalId,
String userId,
String toolName,
String argumentsHash,
Instant expiresAt) {
}
流程:
模型提出退款请求
↓
服务端校验订单和退款金额
↓
返回 approval_required
↓
用户确认具体订单和金额
↓
服务端校验 approvalId
↓
执行退款
确认必须绑定具体参数,不能只确认“允许退款”这一抽象动作。
6. 工具调用审计
CREATE TABLE ai_tool_call (
id BIGSERIAL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
conversation_id BIGINT NOT NULL,
message_id BIGINT NOT NULL,
tool_call_id VARCHAR(128) NOT NULL,
tool_name VARCHAR(128) NOT NULL,
arguments_json JSONB,
arguments_hash VARCHAR(128),
status VARCHAR(32) NOT NULL,
result_summary TEXT,
error_code VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
completed_at TIMESTAMPTZ
);
日志中不要保存完整密码、Token、银行卡号和未脱敏工具结果。
7. 工具调用超时和取消
return Mono.fromCallable(() -> tool.execute(context, arguments))
.subscribeOn(Schedulers.boundedElastic())
.timeout(Duration.ofSeconds(5))
.onErrorMap(
TimeoutException.class,
error -> new ToolTimeoutException(tool.name())
);
阻塞式数据库或 HTTP 客户端不能直接占用 WebFlux 事件线程。工具超时后要更新调用状态,并阻止迟到结果覆盖已经失败或取消的任务。
8. 限制工具调用次数
public record ToolPolicy(
int maxCallsPerTurn,
Set<String> allowedTools,
boolean requireApprovalForWrites) {
}
调用策略应限制:
- 单轮最大工具次数;
- 单个工具最大重试次数;
- 工具参数大小;
- 工具结果大小;
- 工具链最长深度;
- 单次任务总耗时。
9. 流式 Tool Calling
流式模型可能先返回工具调用片段,再返回工具参数。不要在收到第一个片段时立即执行:
接收工具名称和参数片段
↓
持续拼接并验证 JSON
↓
确认参数完整
↓
执行权限检查
↓
执行工具
↓
发送 tool_call 状态事件
高风险工具还要暂停流,等待用户确认。
五、常见问题与实践建议
1. 模型为什么不调用工具
可能原因:
- 工具描述不清楚;
- 当前模型不支持工具调用;
- 工具没有注册到本次请求;
- 用户问题不需要工具;
- 参数 Schema 不完整;
- 模型被系统 Prompt 要求直接回答;
- Provider 兼容层丢失了工具字段。
排查时记录工具列表、模型能力、请求模式和模型原始 tool call,但注意脱敏。
2. 模型调用了错误工具
可以通过:
- 缩小每次请求的工具集合;
- 改善工具名称和描述;
- 将只读和写入工具分开;
- 在系统 Prompt 中明确决策边界;
- 增加服务端意图和权限校验;
- 对高风险工具强制人工确认。
不要只依赖 Prompt 让模型“永远不要调用某工具”。
3. 工具结果太长
工具结果过长会消耗上下文。应返回摘要或分页结果:
{
"items": [
{
"id": "O1001",
"status": "SHIPPED"
}
],
"nextCursor": "..."
}
模型需要详细数据时,再通过下一次工具调用获取指定内容。
4. 工具失败后是否重试
查询类工具可以有限重试;写操作必须使用幂等键,避免重复执行:
tool_name + tenant_id + business_request_id
写入业务系统前,服务端先检查请求是否已经成功执行。
5. 工具调用和事务怎么配合
不要把长时间模型调用放在数据库事务中。推荐:
创建任务和消息
↓ 提交事务
执行工具和模型
↓
保存结果
↓
短事务更新状态
6. 是否可以让模型直接执行 SQL
不建议让模型直接连接生产数据库。更安全的方式是:
- 暴露固定业务查询工具;
- 使用只读数据库账号;
- 限制表和字段;
- 限制查询耗时和返回行数;
- 做 SQL 审计;
- 对高敏感字段脱敏。
六、进阶思考
1. 工具目录和动态注册
当工具数量增加后,可以设计工具目录:
Tool Catalog
├─ tool metadata
├─ required scopes
├─ risk level
├─ timeout
├─ rate limit
└─ approval policy
Agent 根据当前用户和任务只获得一部分工具,避免把全量工具描述发送给模型。
2. Tool Calling 与 Agent 状态机
多步任务可以用状态机管理:
UNDERSTAND
↓
PLAN
↓
CALL_TOOL
↓
CHECK_RESULT
├─ NEED_MORE_TOOL
├─ NEED_APPROVAL
└─ ANSWER
比起让模型无限循环调用工具,状态机更容易设置预算、超时和人工接管。
3. 工具结果可信度
工具返回的内容也可能来自外部系统或用户可编辑字段。模型不能把所有工具结果都当成系统规则:
业务工具结果 = 事实数据
用户备注和网页文本 = 不可信内容
系统授权和策略 = 服务端规则
4. 评估 Tool Calling
测试集应包括:
- 应该调用哪个工具;
- 参数是否正确;
- 无权限时是否拒绝;
- 工具失败时是否正确处理;
- 需要确认的动作是否暂停;
- 是否出现重复调用;
- 是否在预算和次数限制内完成。
指标包括工具选择准确率、参数准确率、成功率、平均调用次数和越权拒绝率。
结论
Spring AI Tool Calling 的核心不是给模型增加几个 Java 方法,而是建立一条受控的业务执行链路:
- 模型提出结构化调用意图;
- 服务端验证参数和权限;
- 业务工具执行真实操作;
- 工具结果经过脱敏和摘要;
- 模型根据结果生成最终回答;
- 每次调用都可追踪、可取消、可审计。
查询工具可以先从只读、低风险场景开始;涉及修改、发送、删除和资金操作时,必须增加幂等、审批和人工确认。模型能力可以帮助系统理解用户意图,但最终业务权限必须由服务端掌握。