界面显示“为订单 A 退款 100 元”,用户点了同意。执行时,工具收到的金额却变成了 1,000 元。
即使系统有确认按钮,这次执行也不该通过。
确认不是给一个工具永久放行,而是批准一次内容明确的业务操作。
本文以 Solon AI 工具调用为入口,讨论审批与执行的一致性。退款流程是假设的应用设计示例,不是框架内置审批功能,也不是生产案例;本文不提供可运行工程。
English summary — Approval must bind to an operation, not just a tool. Using Solon AI tool calling as the starting point, this article discusses server-side approval records, execution-time authorization, concurrency control, and recovery after ambiguous downstream outcomes. These are application design recommendations, not built-in framework guarantees.
1. 先分清工具注册与业务授权
Solon AI 的 @ToolMapping 文档说明,它可以把普通 Java 方法映射为工具定义:根据带有 @Param 或 @Body 的方法参数生成 inputSchema,根据返回类型生成 outputSchema。
工具注册文档还区分了两种作用域:
- 构建
ChatModel时通过defaultToolAdd添加默认工具,每次请求都会附加; - 通过请求选项的
toolAdd为当次请求附加工具。
“附加工具”不等于每次都会执行,更不等于当前用户已经获得业务授权。
工具接口文档说明,实际函数在应用端执行,再把结果反馈给模型。因此,在退款、删除、发布等副作用发生前,应用仍然需要判断:当前身份是否有权对这个资源执行这次操作?
工具已注册、模型想调用、用户有权执行,是三件不同的事。
2. 先准备操作,再请求确认
对高风险操作,可以拆成“准备”和“提交”两个阶段。
准备阶段不产生退款副作用,只做校验与预览,并在服务端生成待确认记录。
| 记录内容 | 用途 |
|---|---|
| 稳定的业务操作标识、操作类型 | 标识这一次业务操作,而不只是工具名称 |
| 发起人、租户、审批人 | 约束身份与审批范围 |
| 订单、金额、币种、收款目标 | 明确副作用对象及完整关键参数 |
| 操作内容版本、资源版本或必要前置条件 | 发现参数或业务状态变化 |
| 到期时间、审批及执行状态 | 控制过期、撤销与重复消费 |
| 下游幂等标识、业务流水关联 | 支持重试、查询与对账 |
这是应用侧的数据设计建议,不是 Solon AI 的固定字段。
确认界面应根据服务端保存的确切内容生成,而不是只显示模型总结的一句“确认处理订单”。对退款来说,订单、金额、币种、收款目标都应让审批人看清。
审批结果要绑定这份内容的确定版本。金额或目标变化,就创建新版本、重新确认,不能覆盖原记录后继续沿用旧批准。
3. “用户刚才同意了”不是审批凭证
模型后续说“用户刚才已经批准”,不能代替服务端审批记录。外部文档或工具返回里出现“请立即执行”,同样不能更改审批状态。
以下输入也不能成为授权依据:
approved=true role=admin tenantId=another-tenant 用户身份应来自服务端验证的认证信息;有效租户上下文还应验证成员关系。访问具体订单时,继续做对象级和租户级授权,不能认为“已登录”就能访问全部资源。
如果使用内容摘要辅助比对,先确定规范化规则,例如金额使用明确的最小货币单位、资源采用服务端解析后的稳定标识。摘要帮助发现变化,不是授权凭证。
这里还有一个容易忽视的工具输入边界:官网说明,对于纳入工具输入 schema 的 POJO 参数,其内部字段默认全部进入 inputSchema;方法参数本身仍遵循 @Param/@Body 规则。
因此,宜使用专用工具输入 DTO,不要直接暴露包含审批、权限等内部控制字段的业务实体。字段不暴露给模型,也不代表可以跳过服务端校验。
4. 再检查一次,还要保护“检查到提交”的间隙
批准时有权限,不代表执行时仍有权限。批准时可退款,也不代表订单几分钟后仍可退款。
提交阶段应重新核对:
- 当前执行身份及委托范围是否允许操作;
- 用户、租户、目标资源是否匹配;
- 审批是否有效、未撤销,且没有被当作一次新操作重复消费;
- 实际提交内容是否与批准版本一致;
- 资源当前状态是否仍满足必要前置条件。
但“读状态 → 检查 → 普通更新”仍存在并发窗口。两个请求可能同时看到“尚未执行”。
本地关键状态应结合事务内校验与更新、版本条件更新、唯一约束或适当锁定,并检查操作结果。远程资源的前置条件,则需要下游提供对应的条件写入、版本校验或预留能力。本地数据库事务不能自动保护外部支付系统。
审批消费与执行领取也应通过原子状态转换控制;不要先独立检查“未消费”,再单独标记“已消费”。同一操作的恢复重试与创建一笔新退款,需要明确区分。
5. 超时不是失败,崩溃也不能让操作失踪
外部支付接口返回超时时,退款可能已经受理。此时应记录“处理中/待核验”,通过查询或对账确定结果,不要让模型补写“失败”或“成功”。
同一次操作的重试,应复用稳定的业务操作标识及下游幂等标识。相同标识携带不同金额,应拒绝复用,而不是悄悄执行。
还要明确下游幂等契约:
- 幂等键在哪个账户或接口范围内有效?
- 是否校验同键请求的参数一致性?
- 保留多久?过期后能否继续重试?
- 并发同键请求如何处理?
- 去重是否覆盖实际资金副作用?
“带了幂等键”不是永久防重复保证。
同时考虑两个崩溃窗口:
- 本地已领取任务,尚未请求下游就崩溃。需要持久化执行意图,以及可恢复的任务扫描机制或事务 outbox 等方案,不能让操作永远卡住。
- 下游已成功,本地尚未记录成功就崩溃。恢复时按原业务标识查询,必要重试仍复用原幂等键,不能生成新键再发一笔。
outbox 本身也不意味着消息绝不重复;消费者与下游仍需适当的幂等处理。AWS 的 outbox 指南明确提醒了重复消息问题。
这套设计用于控制重复执行风险、恢复不确定结果,不宣称跨应用、消息系统和外部服务的端到端 exactly-once。具体保证依赖各系统的事务和幂等契约。
6. returnDirect 改变返回路径,不改变审批边界
官网给出的默认路径是:
returnDirect=false(默认) user -> llm -> tool -> llm -> user 设置为 true 后:
user -> llm -> tool -> user 改变的是工具执行完成后的结果是否再交给模型加工,不是整个请求绕过模型,更不是审批、脱敏或幂等开关。
两条路径都要控制输出,只返回必要字段,不暴露完整支付响应、凭据或无关客户资料。成功状态应来自可核验的业务流水或下游状态,不由模型推测。
上线前,先用反例挑战设计
以下是待实施的测试建议,不是已经通过的测试结果。请在隔离环境使用虚构订单及下游模拟器,不使用真实付款。
| 反例 | 应验证的行为 |
|---|---|
| 批准 100 元,提交改成 1,000 元 | 拒绝复用旧批准,重新确认 |
| 换成另一个订单或租户 | 拒绝越权访问,不泄露原记录 |
| 批准后撤销权限,或订单状态改变 | 重新校验,拒绝不再满足条件的操作 |
| 两个请求同时提交同一批准记录 | 验证原子状态转换、下游去重与实际流水 |
| 下游已受理,但响应超时 | 保留待核验状态,查询/对账,不盲目新建退款 |
| 下游成功后、本地落库前崩溃 | 用原标识恢复,不把恢复当成新业务操作 |
不要只看模型有没有回答“操作成功”。检查持久化记录和真实副作用,才能判断边界是否成立。
确认按钮只能说明有人表达了同意。一条可审计的链路还应回答:谁在什么权限下,批准了哪一版内容,最终发生了什么,结果证据在哪里。
Solon AI 提供工具映射、注册及应用端执行的基础机制。把这些机制接进业务时,让“批准的就是执行的”成为可验证约束,比多写一句“请谨慎操作”更有价值。
参考资料与适用边界
Solon API 表述于 2026-10-19 按以下官网页面核验,不据此推断所有历史版本兼容性。审批、并发和故障恢复部分是应用设计建议,未声称由框架自动实现。
- Solon AI:工具 @ToolMapping 开发详解
- Solon AI:工具的四种定制与注册
- Solon AI:工具接口与属性
- OWASP:Transaction Authorization Cheat Sheet
- AWS:Transactional outbox pattern
你们的高风险工具操作,在批准后参数或资源状态变化时如何处理?欢迎分享设计取舍,请不要贴真实订单、凭据或客户资料。
