Blog·Studio
文章系列日历归档关于搜索
Blog·Studio

一个记录思考、笔记与作品的技术博客。

Connect

© 2026 · Blog Studio

鄂ICP备19019526号

crafted with care

stay curious ✦

  1. 文章
  2. ›Agent 副作用的可撤销执行

Index

  • 一、问题不是如何调用工具,而是如何收回已经发生的结果
  • 二、先定义副作用:哪些动作必须进入事务状态机
  • 三、五阶段执行协议:让每一步都留下可验证的证据
  • 四、幂等与重试:把“超时”从二选一变成可查询状态
  • 五、并行工具与部分成功:不要让模型承担分布式事务
  • 六、权限与人机协作:审批不是对话功能,而是策略执行点
  • 七、可观测性:让一次任务能够回答“谁在何时基于什么执行了什么”
  • 八、评测与回归:验证 Agent 的不是答案,而是状态转移
  • 九、落地架构:把运行时边界放在模型之外
  • 十、从工具调用协议走向可恢复的 Agent 计算
  • 一句话摘要
  • 参考文献

Agent 副作用的可撤销执行

把 Agent 的工具调用当作可授权、可追踪、可补偿的资源操作,而不是一次性函数请求,才能在模型不确定、外部系统半成功和权限变化并存的生产环境中安全地执行与恢复。

2026年8月28日·约 33 分钟阅读·9,664 字·2 次阅读·博主
#Agent 技术
Agent 副作用的可撤销执行

Index

  • 一、问题不是如何调用工具,而是如何收回已经发生的结果
  • 二、先定义副作用:哪些动作必须进入事务状态机
  • 三、五阶段执行协议:让每一步都留下可验证的证据
  • 四、幂等与重试:把“超时”从二选一变成可查询状态
  • 五、并行工具与部分成功:不要让模型承担分布式事务
  • 六、权限与人机协作:审批不是对话功能,而是策略执行点
  • 七、可观测性:让一次任务能够回答“谁在何时基于什么执行了什么”
  • 八、评测与回归:验证 Agent 的不是答案,而是状态转移
  • 九、落地架构:把运行时边界放在模型之外
  • 十、从工具调用协议走向可恢复的 Agent 计算
  • 一句话摘要
  • 参考文献

Agent 副作用的可撤销执行

一、问题不是如何调用工具,而是如何收回已经发生的结果

在单轮对话里,工具调用看起来像一个普通的请求:模型输出参数,服务端执行函数,再把返回值放进上下文。这种接口足够简单,却掩盖了 Agent 系统最危险的工程事实:一次调用可能跨越多个外部系统,修改数据库、发送邮件、创建工单、扣减额度,甚至触发一次不可逆的物理操作。模型在第一步决定要做什么,并不意味着系统在后续步骤永远应该继续执行。

可以把 Agent 的一次任务表示为一个有副作用的状态机。每一步都可能产生不可忽略的结果,因此整个系统的正确性不能只由模型是否选对工具决定,还要由权限边界、事务语义、补偿能力、审计证据和恢复策略共同决定。所谓可撤销执行,不是给模型增加一个“撤销”按钮,而是把业务世界里的副作用纳入 Agent 运行时,使每一步都具备身份、边界、状态和补偿路径。

生产事故往往发生在状态转换的缝隙里。模型先创建了工单,随后发现用户没有权限;工具先写入数据库,随后重试机制再次执行;一个并行分支成功后,另一个分支失败,最终留下无法解释的部分状态;模型收到超时响应,实际上服务端已经成功,客户端却把任务判定为失败并再次提交。仅靠提示词要求模型“不要重复调用”无法解决这些问题,因为模型看到的是自然语言和有限上下文,而不是一个可以保证原子性的事务协调器。

本文讨论一种面向 Agent 的工程方案:把工具调用拆成“预备、授权、执行、确认、补偿”五个阶段,并让运行时维护每次调用的执行凭证、幂等键、结果摘要和补偿动作。方案不依赖某个特定框架,核心思想可以落地到 LangGraph、CrewAI、AutoGen 或自研编排器中。我们重点放在落地时容易忽略的边界,尤其是半成功状态、不可逆操作、并发分支、权限变化和审计复盘。

截至 2026 年,主流工具调用协议通常能够表达工具名和参数,但“调用成功”具体意味着什么,仍需要应用自己定义。一个严谨的 Agent 平台应该把工具返回值分成状态、结果和证据三个部分,而不能只返回一段自然语言。对于副作用工具,返回值至少要说明资源标识、业务版本、执行时间、可重试性和补偿入口。把这些信息显式化,模型才有机会在后续步骤中做出可靠的判断。

二、先定义副作用:哪些动作必须进入事务状态机

工程上可以把副作用定义为“会影响外部世界可观察状态,并且该状态不能仅由当前进程恢复”的操作。读取公开网页通常不是强副作用;发送邮件、修改客户资料、支付订单、删除文件则是强副作用。边界并不绝对:读取用户的私有邮件可能产生审计记录,修改草稿也可能在外部版本系统中留下历史。因此不能只靠工具名称上的 create、update、delete 分类,而要为每个工具声明副作用等级和回滚能力。

一个实用的副作用元数据可以包含以下字段:

{
  "tool": "billing.capture",
  "sideEffectLevel": "irreversible",
  "idempotent": false,
  "requiresApproval": true,
  "compensation": "billing.refund",
  "resourceSelector": "invoice_id",
  "auditFields": ["actor", "policy", "request_hash", "result_version"]
}

副作用等级可以分为只读、可补偿和不可逆三类。只读工具通常可以重试,因为重复读取不会改变业务状态;可补偿工具已经改变资源,但可以通过反向动作恢复,例如创建后删除、扣款后退款、提交后撤回;不可逆工具可能没有业务层面的撤销动作,例如发送真实短信、向外部机构提交合规材料。此时系统必须把风险控制前移,在执行前要求审批、额度限制、目标确认或人工接管。

工具声明还应该区分“声明幂等”和“实际幂等”。接口返回 idempotent true,不等于下游服务在所有故障模式下都真正实现了幂等。数据库唯一约束、消息消费位点、去重表、支付系统的幂等键和外部 API 的重复请求机制都可能不同。Agent 运行时不能把声明字段当作证明,而应将幂等键、调用指纹和资源版本作为事实来源。

建议每个副作用工具都暴露一个 prepare 方法。它不执行业务变更,只检查参数、资源、权限、额度、依赖和预计风险,并返回 plan_id、预计变化、可撤销性、风险等级和审批人。模型可以先看到计划,用户可以在计划上修改目标,运行时再根据冻结后的计划执行。这样做的价值是把“讨论”从“执行”中隔离开来,避免模型在一次回复中连续生成多个动作,导致用户无法审核中间结果。

三、五阶段执行协议:让每一步都留下可验证的证据

第一阶段是预备。运行时根据模型请求生成候选调用,并冻结参数、租户、用户、策略版本和工具版本。参数不能直接使用自然语言改写后的自由文本;需要在服务端重新校验类型、范围、资源归属和依赖。对于金额、收件人、目标仓库、生产环境等高风险字段,必须由业务校验器生成机器可读的确认对象。

第二阶段是授权。授权不是简单地检查用户是否有权限,还要检查任务是否仍然允许继续。用户的权限可能在预备与执行之间变化,额度可能在等待审批期间耗尽,外部资源可能被其他任务锁定。因此执行瞬间要重新读取当前策略,并将当时的策略摘要写入执行记录。若授权失败,工具不应该偷偷执行;系统应返回结构化错误,说明失败阶段、缺失权限、是否需要重新申请和是否可以安全重试。

第三阶段是执行。运行时创建 execution_id,并把它作为下游请求的幂等键。建议请求头同时携带 trace_id、parent_execution_id、attempt、policy_version 和 tool_version。工具端在处理重复请求时,应返回第一次执行的结果摘要,而不是再次修改资源。对于无法保证幂等的下游,至少要提供查询接口,让运行时可以通过执行编号确认服务端实际状态。

第四阶段是确认。成功不是“函数没有抛异常”,而是要满足一组可验证条件。例如,创建工单后返回 ticket_id 和版本号;发送邮件后返回 message_id;支付捕获后返回交易状态和金额。运行时将工具响应转换为 Result 事件,并记录真实执行结果。模型只能在确认事件之后引用该结果,未确认的动作必须标记为 unknown,而不是 failed。

第五阶段是补偿。补偿是预先约定的反向动作或业务处理流程,不是让模型自由生成一个“相反工具”。例如,删除工单可以补偿创建工单,但已经发送的通知可能需要单独撤回;退款可以撤销扣款,但渠道手续费未必可退。补偿动作自身也可能失败,因此要记录 attempt、原因、当前状态和人工处理建议。系统不能把补偿失败伪装成成功,也不能无限重试造成二次副作用。

伪代码如下:

plan = prepare(tool, args, context)
authorize(plan)
if not allowed: return blocked
attempt = begin(plan, idempotency_key)
result = execute_once(attempt)
confirmed = confirm(attempt, result)
if not confirmed: return unknown
record(attempt, confirmed)
if later_step_fails and plan.compensatable:
    compensation = execute(plan.compensation, confirmed.resource)
    if not confirm(compensation): return needs_manual_review
return completed

关键点是 execute_once 只能在一个明确的协调边界内发生。对于跨多个工具的长任务,不能把整条链路视为一次调用,而应建立可重复的状态机和补偿图。每个节点都应有 success、failed、unknown、compensating、compensated 五种状态。模型只看到适合决策的状态,运维人员则可以看到完整的状态转移日志。

四、幂等与重试:把“超时”从二选一变成可查询状态

网络超时是 Agent 工具系统中最容易被误判的错误。客户端没有收到响应,并不能说明服务端没有执行。如果每次重试都重新发送,就可能创建两笔订单、两张工单或两封邮件。正确策略是使用稳定的幂等键,并让服务端在写入发生前和发生后都能回答“这个键是否已经处理”。

幂等键最好由高层的任务意图和工具身份共同生成,而不是每次重试随机生成。过于随机的键无法把同一业务意图关联起来,过于固定则可能让不同参数错误地共享结果。一个简单的构造是 tenant_id + operation_id + tool_name + normalized_resource,operation_id 来自用户确认后的执行计划。只要计划内容发生变化,就应生成新的计划版本,而不能复用旧幂等键。

重试策略要同时考虑错误类型。只读工具和已知幂等工具可以采用指数退避;参数错误、权限错误和资源不存在不应重试;unknown 状态先查询执行编号;不可逆动作在授权后发生超时时,优先进入人工确认,而不是自动重复。工程上可把错误分为 validation、policy、transport、resource、confirmation、system 六类,每类绑定默认动作。

重试还会受到 token 成本和上下文质量影响。Agent 很容易在同一个循环里反复调用同一个工具,因为模型没有看到“请求已经发出”的证据。把 execution_id 和当前状态写入每次工具结果,并在结果中加上 human-readable explanation,可以减少重复尝试。更进一步,运行时可以在工具层启用单飞机制:同一 plan_id、同一资源、同一签名在确认前只允许一个活动执行,其余请求返回 pending 或 reuse。

对于写操作,服务端应该提供幂等查询或结果回收接口。若下游是第三方支付、短信或物流平台,外部协议未必统一,可以在 Agent 与下游之间增加一个本地 effect ledger。ledger 以 execution_id 为主键,记录 request_hash、状态、下游业务编号和响应摘要。外部 API 每次都可能被重试,但本地记录可以先决定是否需要发送新的请求。effect ledger 不替代下游幂等实现,而是把不确定结果转化为可查询对象。

五、并行工具与部分成功:不要让模型承担分布式事务

复杂 Agent 经常并行调用多个工具:同时读取订单和客户信息,同时生成多个文件,或让多个子任务共同处理一个目标。并行能降低墙钟时间,却把失败模式从顺序执行扩大为部分成功。一个分支成功、另一个分支超时是常态;某个工具重试时,其他分支可能已经产生不可逆副作用。

编排层应为每个并行分支设置独立的 execution_id,并把分支状态汇总到 join 节点。join 不应使用简单的“全部成功才继续”策略,而要根据副作用等级选择策略。读取操作可以要求全部成功;可补偿写操作可以接受部分成功后执行补偿;不可逆操作则默认串行或要求人工审批。对于会产生共同业务结果的工具组,还要设置 commit 协议:所有准备结果满足条件后,只执行一次 commit。

可以把并行执行抽象为两阶段提交:prepare 阶段只做检查和锁定,commit 阶段才产生效果。对于无法真正锁定资源的系统,至少要使用 reservation token,并在执行前再次确认资源版本。准备阶段的返回结果包含 resource_version;执行请求携带该版本,服务端发现版本变化就拒绝并要求重新规划。这样可以避免 Agent 基于旧状态做出重复或冲突动作。

一个常见反例是“先并行创建三个资源,再让模型根据返回值决定最终保留哪个”。即使三个创建动作都可删除,这种设计也会制造不必要的外部变更,并且模型可能在第四步才发现目标错误。更好的做法是让模型在 prepare 阶段选择候选方案,运行时只执行被选中的方案;候选探索应优先使用草稿、预估或沙箱数据,而不是真实世界写入。

部分成功的结果不能只输出一句“有两个步骤失败”。每个失败都要带 stage、execution_id、resource、retryable、compensatable、safe_action 和 manual_action。模型根据这些字段生成下一步,运行时根据 safe_action 执行受控动作。结构化状态比自然语言错误更能避免模型误把 pending 当成 failed,也能帮助评测系统把错误类型单独统计。

六、权限与人机协作:审批不是对话功能,而是策略执行点

Agent 工具的权限不能只由模型决定,也不能只在工具内部用角色判断。一个安全的授权链至少包含用户身份、任务上下文、资源归属、动作风险和策略版本。模型可以说“我要退款”,但只有策略系统可以决定是否允许退款以及退款上限。授权结果要绑定具体的 plan_id,模型不能拿一次低风险检查的通过结果去执行高风险动作。

建议把工具分成三个访问级别。第一级是自动只读,允许模型在上下文中检索和分析;第二级是可补偿写操作,满足条件后自动执行,但必须写入 effect ledger;第三级是不可逆或高价值动作,必须由用户或指定审批人确认。审批界面应展示冻结后的计划、预计影响、涉及资源、工具版本和补偿策略,而不是展示一大段模型解释。

审批人也可能误解任务。系统不能把用户的“同意”当作无限授权。审批应绑定资源、金额、有效期和动作集合;用户批准发送一封邮件,不应被解释为批准发送同一收件人的全部后续邮件。有效期过期后要重新授权,策略版本变化后要重新计算。对于批量操作,可以要求逐步确认,或者提供明确的批量上限。

人机协作还需要考虑拒绝和撤回。用户可能在工具执行后改变主意,但系统不能因此自动执行模型提出的任意补偿动作。撤回请求应经过策略检查,确认补偿动作仍然有效且没有造成新的影响。例如用户撤回已发送的营销邮件,系统可以尝试撤回,但若邮件已送达,应返回“已通知撤回但无法保证未读”。准确性比礼貌更重要。

七、可观测性:让一次任务能够回答“谁在何时基于什么执行了什么”

Agent 调试最困难的地方是,模型输出只是决定链的一段,真实状态却分散在模型网关、编排器、工具服务、数据库和外部平台。普通日志只能证明某个函数被调用,无法证明资源最终处于什么状态。工具平台应提供统一的 trace,把 trace_id、plan_id、execution_id、tool_name、tool_version、actor、policy_version 和 effect_id 串起来。

每个事件至少包含时间、状态、输入摘要、输出摘要、错误分类和资源引用。敏感参数不能直接写入日志,应先脱敏再记录稳定哈希。输入摘要要保留足够信息用于复现,例如参数归一化后的结构化值和字段长度;不能只保存“用户要求执行某操作”这种无法验证的文本。

观测指标也必须围绕状态机设计。除了 token 数量、延迟和错误率,还要统计 unknown 比例、幂等命中率、补偿成功率、人工升级率、重复副作用拦截次数和策略拒绝率。一次工具调用延迟很高但没有副作用,可能仍比一次低延迟但产生错误业务结果更健康。把这些指标放到同一张执行图里,团队才能发现系统究竟在哪个边界反复消耗成本。

建议在 trace 中记录可重放的事件而不是只记录最终文本。事件包括 plan_created、authorized、execution_started、result_confirmed、compensation_started、compensation_failed 和 manual_review_required。重放时可以用相同 fixture 重新运行工具模型,但外部副作用必须使用 dry-run、fake provider 或已确认的测试资源。审计复盘与线上回放必须严格分离,避免为了重现问题而再次改变业务世界。

八、评测与回归:验证 Agent 的不是答案,而是状态转移

传统问答评测只关心最终答案是否正确,Agent 工具评测则至少要同时评估决策、授权、执行、确认和补偿。模型可能第一次选错工具,但在发现错误后及时停止,这种行为不应被简单判为失败;模型也可能第一次选对,却在失败恢复阶段重复调用,最终造成严重副作用。因此,评测集要记录完整轨迹,并对每个状态转换设置断言。

最小工具测试可以包括以下项目:相同参数重复调用是否只产生一个效果;参数缺失是否在执行前被拦截;权限撤销后是否拒绝动作;下游超时后是否进入 unknown;部分并行失败是否按策略补偿;人工审批后是否严格绑定 plan_id;下游结果不可确认时是否升级人工处理;补偿失败是否保持可见而不是被吞掉。

回归测试还要覆盖模型版本和工具版本的变化。模型升级可能更频繁地产生同一工具调用,工具升级可能改变参数语义或副作用等级。每个版本都应保存兼容性矩阵,说明哪些参数被删除、哪些字段变成必填、哪些动作从可补偿变为不可逆、哪些旧 execution_id 仍然可查询。没有版本矩阵,所谓灰度和回滚就只是部署按钮。

在 A/B 测试中,实验指标不能只看任务完成率。对于副作用 Agent,完成率提高可能伴随重复写入率、退款率和人工升级率上升。应把风险指标作为护栏:零重复外部效果、零未授权高风险动作、补偿失败为零,或在允许范围内有明确人工处理。工具平台的策略应允许在风险护栏触发时自动停止实验,即使表面完成率因此下降。

九、落地架构:把运行时边界放在模型之外

一个可落地的实现可以分为六层。第一层是模型适配层,只负责把模型输出转换为结构化 tool intent,不直接访问数据库或网络。第二层是计划与策略层,负责参数校验、权限、资源版本、风险等级和审批。第三层是 effect ledger,保存 execution_id、幂等键、状态和结果摘要。第四层是执行器,负责单飞、重试、超时、确认和补偿。第五层是工具适配层,把每个外部服务映射为统一协议。第六层是观测与审计层,把事件写入 trace、指标和长期存储。

模型只能提出意图,不能直接指定“这个动作已经成功”。例如模型可以请求 capture_payment,但系统只有在下游返回 transaction_id、金额、状态和确认时间后,才把事件标记为 confirmed。模型可以请求 refund,但系统会重新检查原支付是否已捕获、是否超过可退额度以及当前用户是否仍有权限。这样的职责分离能显著降低 prompt injection 和模型越权的影响。

工具适配器应提供统一的 read、prepare、execute、confirm、compensate 接口。execute 不是所有工具都必须实现,例如纯查询工具可以没有补偿,但必须有结果确认。接口返回值使用固定 schema,包含 status、resource、version、idempotency、retryable、compensation、evidence 和 message。对于不支持幂等的外部服务,适配器必须显式返回 unsupported,而不是通过重试制造风险。

部署时要为每个工具设置并发限制、速率限制、最大批量、有效期和资源配额。模型一次生成大量工具调用并不意味着运行时应该同时执行。执行器需要根据租户、工具风险和下游能力进行排队,并在超过预算时生成可解释的 pending 状态。对于不可逆操作,宁可让用户多等一步,也不要用自动化换取不可控性。

最后要设计人工接管通道。人工接管不是把错误日志截图发给工程师,而是携带已经冻结的计划、已确认的执行、待处理资源、可能的补偿和完整 trace。工程师可以直接批准、回滚、重新规划或标记为不可恢复。接管界面应显示幂等键和资源版本,避免人工在不知情的情况下再次执行。

十、从工具调用协议走向可恢复的 Agent 计算

Agent 系统的核心挑战正在从“模型能不能生成正确参数”转向“系统能否安全地承认不确定性”。模型擅长提出下一步,但不应承担分布式系统的事务责任。工具调用必须拥有身份,副作用必须拥有边界,执行结果必须可确认,失败必须可追踪,补偿必须经过策略控制。

在工程团队中,可以先从最小闭环开始:选择一个有业务价值、同时可以补偿的写操作;为它加入 prepare、execute、confirm、compensate 和 effect ledger;在执行前后保存 resource_version;把超时统一映射为 unknown;为重复调用和补偿失败建立告警;最后把同一套协议扩展到并行工具与高风险审批。这个过程不需要一次性改造所有 Agent,只要让最常见的副作用工具先具备可恢复语义,就能暴露大量此前被自然语言掩盖的事故。

从更长远的视角看,工具接口会逐渐从“函数调用”演变为“可授权的资源操作”。每次工具请求都应携带操作意图、租户、策略、资源、幂等信息和确认证据;每次工具响应都应携带资源版本、副作用等级、补偿能力和不可逆性。模型负责在这些约束中选择计划,运行时负责保证计划被按约执行,平台负责在不确定状态出现时停止、确认和升级。

因此,可撤销执行不是一项孤立的可靠性技巧,而是 Agent 工程化的分水岭。它把 prompt 之外的权限、事务、可观测性和恢复重新放回系统设计中心。只有当一个 Agent 在最坏情况下也知道哪些动作已经发生、哪些仍未确认、哪些可以安全补偿、哪些必须交给人处理时,它才真正具备从 demo 走向生产的条件。

一句话摘要

把 Agent 的工具调用当作可授权、可追踪、可补偿的资源操作,而不是一次性函数请求,才能在模型不确定、外部系统半成功和权限变化并存的生产环境中安全地执行与恢复。

参考文献

  1. OpenAI, Function calling and tools, 官方 API 文档,https://platform.openai.com/docs/guides/function-calling
  2. Anthropic, Tool use with Claude,官方工程文档,https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview
  3. LangGraph Documentation, Durable execution and persistence,https://langchain-ai.github.io/langgraph/concepts/durable_execution/
  4. Temporal Technologies, Durable execution concepts,https://docs.temporal.io/
  5. Martin Kleppmann, A Critique of the CAP Theorem,https://martin.kleppmann.com/2015/05/27/CRDT-alg-for-realtime.html
  6. Pat Helland, Life Beyond Distributed Transactions,https://queue.acm.org/detail.cfm?id=3029970
  7. Chris Richardson, Microservices Patterns: Idempotent Consumer,https://microservices.io/patterns/communication-style/idempotent-consumer.html
  8. Amazon Web Services, Prescriptive Guidance for Retries and Backoff with Jitter,https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/
  9. OpenTelemetry, Distributed Traces specification,https://opentelemetry.io/docs/concepts/signals/traces/
  10. Leslie Lamport, Time, Clocks, and the Ordering of Events in a Distributed System,https://lamport.azurewebsites.net/pubs/time-clocks.pdf
  11. Peter Deutsch, The Eight Fallacies of Distributed Computing,https://resources.sei.cmu.edu/library/entry/the-eight-fallacies-of-distributed-computing
  12. Michael J. Fischer, Nancy A. Lynch, Michael S. Paterson, Impossibility of Distributed Consensus with One Faulty Process,https://doi.org/10.1145/5.5858.5860
  13. Kubernetes, Jobs and CronJobs,https://kubernetes.io/docs/concepts/workloads/controllers/job/
  14. OWASP, Agentic Applications Security Initiative,https://owasp.org/www-project-top-10-for-large-language-model-applications/
←返回文章列表

Related

可能也会喜欢

  • Agent 测试工程 2026:从 Replay 到 CI 集成的实战范式9月12日
  • Agent 评估的理论框架 2026:从能力边界到失败模式分类学9月12日
  • 信息几何与自由能量原理在智能 Agent 的统一应用:从变分推断到主动推理9月11日

Conversation

0 条

留下你的想法

加载评论中…

New comment