20-ToolCalling授权与幂等执行
Tool Calling 怎样可靠执行?参数、授权与未知结果
模型知道应该调用订单接口,不代表它有权查询任何订单;创建工单超时,也不代表工单没有创建。工具设计从选择开始,最终落在受授权、可恢复的业务执行上。
Spring AI 接入片段使用 Java 17、Spring Boot 3.5.3 与 Spring AI 1.1.8;协议与第三方平台示例另行标明适用范围。
1. 模型提出调用,程序执行规则
参数形状、身份和工具结果有各自来源。
完整链路中谁负责什么?
应用提供工具描述与参数 Schema
→ 模型选择工具并生成参数
→ 应用校验工具名、参数与可信身份
→ 业务服务执行
→ 工具结果返回模型或直接返回调用方
→ 模型可能继续生成或提出下一次调用
提供工具不保证模型一定调用,也不保证只调用一次。某些问题可能无需工具,某些请求可能包含多次调用,循环必须有预算和结束条件。Spring AI 1.1 Tool Calling
工具参数不要包含由模型决定的身份
查询订单可以让模型提供 orderId,用户和租户则来自后端认证上下文。不要让模型生成 userId 后把它当成当前用户。
Spring AI 的 ToolContext 可以传递应用上下文,且该对象不作为工具 Schema 参数直接发给模型。它的可信程度仍取决于来源:如果服务器从客户端任意 JSON 复制 tenantId,使用 ToolContext 也不会自动变可信。
一个有明确归属校验的查询工具
下面代码对应 Spring AI 1.1.8,可放入Java 服务变慢怎样定位?从 CPU、GC 到内存保留链项目。内存订单用于说明授权,不连接数据库,也不是完整订单系统。
package demo;
import java.util.Map;
import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
public final class OrderTools {
public record Scope(long tenantId, long userId) {}
public record Order(long id, long tenantId, long ownerId, String status) {}
public record OrderStatus(long orderId, String status) {}
private final Map<Long, Order> orders = Map.of(
42L, new Order(42, 3, 7, "SHIPPED"),
43L, new Order(43, 3, 8, "PAID"));
@Tool(description = "查询当前用户自己的订单状态,只返回订单编号与状态")
public OrderStatus findOrder(@ToolParam(description = "正整数订单编号") long orderId,
ToolContext context) {
if (orderId <= 0) throw new IllegalArgumentException("invalid order id");
Object value = context.getContext().get("scope");
if (!(value instanceof Scope scope)) {
throw new IllegalStateException("trusted scope required");
}
Order order = orders.get(orderId);
if (order == null || order.tenantId() != scope.tenantId()
|| order.ownerId() != scope.userId()) {
throw new IllegalArgumentException("order unavailable");
}
return new OrderStatus(order.id(), order.status());
}
}
在已经认证的业务调用中注册工具:
String answer = client.prompt()
.user(question)
.tools(new OrderTools())
.toolContext(Map.of("scope", trustedScope))
.call().content();
client、question 和 trustedScope 由业务层提供。trustedScope 应来自认证结果和当前合法租户,不接受模型或客户端自行指定。
针对 scope=(3,7),工具直接调用 42 的预期结果为 SHIPPED,调用 43 应被拒绝。完整模型是否调用它,以及怎样处理工具异常,取决于模型能力和应用异常策略;不能把直接方法结果写成模型实测输出。
2. 只读与写入工具使用不同约束
返回范围、次数限制与副作用恢复不能混用。
只读工具也需要限制
限制分页大小、可访问字段、查询范围和响应体积。一个可以执行任意 SQL 的“查询工具”,并不因为名叫查询就安全;数据库账号和查询入口都要限制。
返回模型的数据应最小化。例如订单状态不需要附带完整地址、电话和支付凭证。原始错误栈也不应直接送给模型或用户。
外部工具内容可能包含恶意指令。工具结果是数据,不应自动获得修改系统规则或授权其他工具的能力。对结果进行格式与大小检查,并保留来源。
写工具需要更完整的执行协议
取消订单、退款和发券需要稳定业务操作号、明确权限、状态校验和执行记录。模型重复提出同一工具调用,不能产生重复副作用。
如果用户需要审批某个高影响操作,确认应对应具体参数和当前状态,避免用户确认“退款”后模型又修改金额。超时以后应查询操作结果,而不是立即生成新操作号重做。
允许模型调用哪些工具,应根据当前任务与身份决定,不必把所有业务方法同时暴露。工具描述帮助模型选择,不是实际权限控制。
失败与循环怎样处理?
区分参数错误、无权限、业务拒绝、外部故障和结果未知。可向模型返回经过整理的错误信息帮助其改正,但不能让它无上限重试。
限制总时间、模型轮次、工具次数和输出大小。Spring AI 不同版本的工具循环位置和记忆可见范围不同,不能直接套用另一个主版本的 Advisor 行为。
审计应关联用户请求、模型轮次、工具名、参数摘要和真实结果。模型回答“操作成功”不等于业务真的成功,要以执行记录为准。
3. 超时后怎样确认是否已经成功
业务操作标识连接同一次意图的多次尝试。
为什么重试不能只由模型决定?
模型一般无法观察服务端事务提交点,也不知道网络故障发生在提交前还是提交后。根据一句“timeout”选择重试,会把未知结果误当成明确失败。
执行器应根据工具语义分类:只读查询、可幂等写入、不可重复写入。只读工具通常更适合受控重试;写工具必须先检查操作记录或使用稳定幂等键。
JSON-RPC 请求编号、模型工具调用编号和业务幂等键用途不同。前两个负责一次通信关联,不能天然跨请求、跨模型轮次识别同一工单创建操作。
幂等键应该由谁生成?
可以在用户明确提交业务意图时,由服务器生成 operationId,绑定当前用户、租户、操作类型和规范化参数摘要。之后所有重试沿用同一标识。
不能要求模型每次自行生成 UUID,因为每次新 UUID 都会被视为新操作。也不能只用参数摘要永久去重:用户可能合法地在不同时间提交相同内容的工单。
幂等键要表达“这次业务意图”,并限定使用范围。相同键搭配不同参数应该拒绝,而不是静默返回另一笔操作的结果。
在同一数据库中实现原子结果
下面是 MySQL 8.0 的表结构示例,未执行。真正落地时根据订单、工单等业务结构补充外键和索引:
CREATE TABLE tool_execution (
tenant_id BIGINT NOT NULL,
operation_id VARCHAR(64) NOT NULL,
actor_id BIGINT NOT NULL,
operation_type VARCHAR(40) NOT NULL,
request_hash CHAR(64) NOT NULL,
status VARCHAR(20) NOT NULL,
result_id BIGINT NULL,
created_at DATETIME(6) NOT NULL,
updated_at DATETIME(6) NOT NULL,
PRIMARY KEY (tenant_id, operation_id)
);
对于本地数据库操作,可以在一个事务内插入执行记录、创建工单、把记录改为完成并保存结果编号。事务提交后,两者同时可见;发生异常回滚,两者都不应成为成功结果。
重复请求触发唯一键竞争后,应在可用的新事务边界读取已有记录,核对操作者、类型和参数摘要,再返回同一结果。不能捕获数据库异常后继续在已经失败或回滚标记的事务里盲目查询。
外部副作用为什么更复杂?
如果工具还调用外部支付、短信或第三方工单服务,本地事务无法把远端结果一起回滚。需要利用远端幂等能力、业务查询接口,或通过本地消息表异步驱动操作。
异步执行可以先提交意图记录,再由 Worker 处理。Worker 崩溃后可能重试,因此外部系统也要能识别操作标识;如果它不支持,需要对账与人工处理流程,不能宣称已经实现严格一次执行。
RUNNING 状态超时不等于业务未发生。租约失效只是允许接管调查,接管者应先查询外部结果。旧 Worker 迟到返回时,还要通过版本或 fencing 标识阻止它覆盖新状态。
助手怎样回复未知结果?
执行记录可以提供查询工具:当前用户查询自己的 operationId,得到处理中、成功、失败或需要人工核查。助手超时后应查询状态,再决定回复或是否允许重试。
预期行为是:成功且响应丢失时,查询返回同一工单编号;明确失败且没有副作用时,按规则重试;外部结果未知时,回复“处理中或需核查”,避免承诺失败。
记录保留时间至少覆盖可能的重试窗口与业务对账需求。删除去重记录之后,旧请求可能被再次执行。因此归档、重放期限和客户端失效策略必须一起设计。
监控重复命中率、未知结果数量和长时间处理中任务。它们反映的是恢复链路问题,不能只看 HTTP 成功率。
4. 设计判断与适用边界
工具尽量表达一个清楚业务能力,参数只包含完成任务需要的业务信息。身份来自服务器,结果使用小而稳定的 DTO。工具描述写明适用条件,但授权仍由代码和数据访问条件执行。
对写操作,先持久化业务意图与结果关系,再允许受控恢复。模型工具调用编号不天然成为幂等键,新的模型轮次也不应自动生成新的业务操作。未知结果通过查询与对账处理,而不是让模型根据异常文本猜成功或失败。
Anthropic 的工具设计文章关注能力表达、返回范围和评估。本文额外将业务授权与未知结果恢复作为执行契约的一部分。 Anthropic:Writing effective tools for agents。
AWS 的幂等 API 文章可用于理解同一调用意图与重复尝试的区别。本文将这一问题落实到自己的数据库事务和执行记录。 AWS:Making retries safe with idempotent APIs。
5. 问答
问:模型会直接连接数据库执行工具吗?
答:通常由应用识别模型提出的调用并执行。应用必须完成参数和权限校验。
问:工具说明写“只能查本人”,是否足够?
答:不够。需要在实际查询条件或服务方法中限制身份和归属。
问:分布式锁能完全解决重复执行吗?
答:锁主要协调并发,持久化执行记录才支持跨重启查询结果;外部副作用还需要额外机制。
问:参数一样就算同一次操作吗?
答:未必。应由业务意图标识区分合法重复提交与故障重试。