Agent 应用开发会员账号
知识目录选择核心方向与细分内容
知识单元 11高级实现约 15 分钟

理解 → 实现 → 排错 → 取舍

结果未知下的幂等与补偿

用稳定操作标识、参数摘要与结果查询处理外部副作用的不确定结果。

幂等副作用重试不确定结果

知识内容核对 2026-10-03 · 原题来源核对 2026-10-02

这个知识点,你想学到哪一步?

按当前基础选择起点,也可以依次深入。遇到不熟悉的概念,先回到核心原理;完成后用知识练习检查理解。

先理解

刚接触这个知识点

补齐先备概念,读原理与反例,再用自己的话解释为什么。

从核心原理开始 →

再实现

准备把原理写进代码

理解实现步骤与边界,完成小任务,对照验收要求检查结果。

查看代码示例 →

会排错

需要处理故障与条件变化

沿连续追问定位失效前提,再比较迁移案例,说明方案应如何调整。

沿问题继续深入 →

能取舍

需要设计或评审方案

结合工程推演与资深自评标准,解释方案的适用条件、代价和替代选择。

分析工程场景 →
知识单元目录

LEARN · PRACTICE · REFLECT

知识学习与个人记录

我的笔记与复习 ↗

先沿着原理、问答和迁移案例阅读。需要检查理解时,再切换巩固练习或展开个人记录。

作答与个人记录

每次修改后的提交会保留为独立历史。掌握程度由你对照标准自评。

核心知识 · 结果未知下的幂等与补偿

先理解核心原理

先备概念:网络超时、唯一约束、业务操作标识

调用方没拿到响应无法区分未执行与已执行。安全重试依赖同一业务意图在执行端被识别并去重;本地检查点不能跨越远端副作用与本地记录之间的原子性缺口。

先画三种结果

请求可能没到服务端、被拒绝、已执行但响应丢失,调用方都可能只看到异常。前两种有时适合重新提交,第三种可能双发或双付。所以写请求需要“结果未知”状态,不能用统一 failed 直接驱动重试。

幂等键的关键是稳定

UUID 可以作为键,问题是一次业务动作的全部尝试是否复用它。服务端比较相同键的参数与执行结果,同键不同意图拒绝。并发去重要靠原子约束,先查询不存在再执行存在竞态。

保留期限与跨系统限制

Stripe 的具体接口展示参数比较和键保留策略,但其他远端未必一样。任务暂停超过远端保留期,复用同键也可能被当成新请求,必须核对业务状态。outbox 可以防本地任务丢失,不能凭空使不支持幂等的邮件服务恰好发送一次。补偿是新业务动作,也可能失败。

用一个问题检查理解

工具超时后重试,如何防止重复发信、重复下单或重复发布?

超时只说明调用方没拿到结果,不说明服务端没有执行。写工具要用稳定业务操作 ID 作为幂等键,服务端保存参数摘要与执行结果;同键同参数返回旧结果,同键不同参数拒绝。若远端支持幂等就透传同一业务键;不支持时先查询状态,再人工判断或执行补偿。工作流检查点与工具去重解决不同问题,不能用一次状态落库来宣称外部动作 exactly-once。

实现与取舍

为什么普通重试会出错

客户端发出发布请求,服务器已完成发布,但响应在网络中丢失。此时客户端看到超时,重新执行就可能产生第二篇文章。把超时统一映射成 failed 会掩盖事实。运行记录应允许 outcome_unknown,保存操作 ID 和远端请求标识,并走查询确认流程。只读查询通常更容易重试;写请求是否可重试取决于其幂等设计,不取决于模型是否认为值得重试。

幂等键要表达业务意图

键可以由任务 ID、动作阶段和目标版本构成,关键是同一逻辑动作在恢复和重试时保持不变。每次尝试都新建随机键会失去去重效果。服务端以租户和操作键建唯一约束,并存储规范化参数摘要、状态和结果。同键同参数重放旧结果,同键不同参数报冲突。并发执行要在数据库事务或远端原子操作中争夺同一键,不能先查是否存在、退出事务后再写。日志记录 attempt,业务记录 operation,两者不要混成一个 ID。

跨系统边界怎么处理

下面的 SQLite 代码将本地业务写入与结果存储放进同一事务,展示本地去重。对于发信或支付,数据库提交和远端执行无法靠这个事务合并。优先使用远端幂等 API,并将相同业务键传过去;否则采用持久化待发送记录和可重试投递,仍需承认远端重复风险。若远端可按业务编号查询,先核验后重试;既不能去重也不能查询的高影响动作进入人工核验,补偿动作必须单独记录,不能冒充回滚成功。

如何用故障注入验证

分别在执行前、业务写入后、提交后响应前制造失败,再发两次相同请求。断言本地只出现一条业务记录、重放结果一致、修改参数得到冲突。并发场景另行验证唯一约束,跨进程场景用持久化数据库。LangGraph 中断恢复会重跑所在节点,更说明工具网关必须独立保护副作用。验收关注业务结果而非“函数被调用一次”;生产代码还需过期策略、审计、权限重验和远端对账。

代码示例

SQLite 事务内的业务操作去重

Python 3 标准库演示,使用内存 SQLite;真实恢复需要文件或服务端数据库,外部动作需要远端幂等能力。

import hashlib
import json
import sqlite3

db = sqlite3.connect(":memory:")
db.executescript("""
CREATE TABLE publications (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tenant TEXT NOT NULL,
    title TEXT NOT NULL
);
CREATE TABLE operations (
    tenant TEXT NOT NULL,
    op_key TEXT NOT NULL,
    payload_hash TEXT NOT NULL,
    result_id INTEGER NOT NULL,
    PRIMARY KEY (tenant, op_key)
);
""")

def publish(tenant, op_key, title):
    payload = json.dumps({"title": title}, sort_keys=True).encode()
    digest = hashlib.sha256(payload).hexdigest()
    db.execute("BEGIN IMMEDIATE")
    try:
        old = db.execute(
            "SELECT payload_hash, result_id FROM operations WHERE tenant=? AND op_key=?",
            (tenant, op_key),
        ).fetchone()
        if old:
            if old[0] != digest:
                raise ValueError("IDEMPOTENCY_CONFLICT")
            db.commit()
            return old[1]
        cur = db.execute(
            "INSERT INTO publications(tenant,title) VALUES (?,?)", (tenant, title)
        )
        result_id = cur.lastrowid
        db.execute("INSERT INTO operations VALUES (?,?,?,?)",
                   (tenant, op_key, digest, result_id))
        db.commit()
        return result_id
    except Exception:
        db.rollback()
        raise

print(publish("t1", "task-7:publish:v3", "Agent recovery"))
print(publish("t1", "task-7:publish:v3", "Agent recovery"))
print("rows", db.execute("SELECT COUNT(*) FROM publications").fetchone()[0])
try:
    publish("t1", "task-7:publish:v3", "Changed title")
except ValueError as exc:
    print(str(exc))
db.close()

预期输出

1
1
rows 1
IDEMPOTENCY_CONFLICT

工程推演

场景
假设工程场景:文章发布 API 超时,Agent 恢复后重新请求发布。
设计决策
操作键绑定任务与草稿版本;同键重试返回原发布结果;参数变化要求新审批与新操作。
验证目标
演示结果:两次相同调用返回同一 ID,数据库只有一条发布记录,不同参数被拒绝。
适用边界
代码只证明单数据库事务内去重;不证明外部网络动作 exactly-once,也未实现分布式投递。

连续追问与解答

沿着问题的前提和约束继续向下读。先理解参考解答,再尝试收起答案,用自己的话解释因果和取舍。

举一反三:条件变了,怎样推导?

先找出改变的条件,再判断原方案中哪些前提仍成立。下面的案例是教学推演,便于将原理迁移到新问题。

只读查询超时

改变的条件:有副作用写入变为无副作用查询

延伸问题:是否还要业务幂等键?

推导与参考解答

通常不必为查询结果防重,但仍用截止时间、退避和调用预算,避免放大负载。查询可能读到不同版本,涉及核对付款时必须按同一业务编号判断,而不是把新查询结果当成原请求失败证明。只读易重试,不代表一致性不重要。

保持不变的原理:重试是否安全取决于操作语义,超时本身不说明业务结果。

撤销已发布内容

改变的条件:创建动作变为补偿动作

延伸问题:删除后是否就等同从未发布?

推导与参考解答

不等同,用户可能已读、外部缓存仍在。把撤销作为新操作,核对权限与目标版本,保存发布和撤销两份回执;对外说明能恢复的范围。撤销超时也进入未知处理,不能用补偿请求发出替代补偿成功。

保持不变的原理:副作用不能从账本抹除,补偿的结果也需独立验证。

易错点

  • 把本地 timeout 当作远端未执行
  • 先查后写但没有事务和唯一约束
  • 认为保存检查点即可保证外部副作用只发生一次

参考资料

依据公开技术资料设计;参考资料支持技术机制,场景与评分标准为本站设计,不代表某公司面试原题。 新增问答与迁移案例用于原理讲解,来源核查与案例运行验证分别记录。

检查自己理解到哪一步

读完后可以对照这些标准解释原理、边界和取舍。掌握程度由你自评;需要进一步验证时,再完成下方小任务。

基础达标
知道超时可能发生在外部提交之后。
中高级信号
使用稳定业务幂等键、意图账本和回执核对。
资深信号
明确不支持幂等时的未知终态、人工处理与故障注入。

查看独立示例的校验记录

继续做进阶研究实验

进程真的退出后,还能怎样继续?

从两份数据库的幂等反例,继续验证租约、检查点和独立服务方回执。解压可靠性实验 v3,在独立目录执行。

阅读全文与故障分析 → · 下载可靠性实验 v3 ↓

python3 cli.py submit --db crash.sqlite
python3 cli.py run --db crash.sqlite --lease-seconds 2 --fault after_collect
python3 cli.py inspect --db crash.sqlite
# 首次运行预期退出码 75;等待至少 2 秒后分别执行
python3 cli.py run --db crash.sqlite
python3 evaluate.py --db crash.sqlite

保留证据,逐条核对

  • 首次 inspect 显示 running,已保存 collect 检查点。
  • 租约过期后 succeeded,generation=2,collect 只提交一次。
  • 按 README 再跑 after_effect,核对独立 publisher 数据库只有一个 receipt。

固定 collect → draft → verify → publish 流程;验证本地持久协议与真实进程退出,不证明任意远程服务恰好一次执行。

动手验证 按需完成 · 建议 15 分钟

演示发布成功但响应丢失时的一次恢复流程。

展开验收要求与检查点
  • 不盲目重复发布
  • 业务键跨重试稳定
  • 无法核对时不宣称成功或未执行

重点检查

  • 区分未执行、已执行和结果未知
  • 同键重复请求与同键不同参数有明确处理
  • 能说明本地事务无法保证远端副作用恰好一次