跳到主要内容
多 Agent 并发与事务

第2章:冲突的分类法与失败模式 —— 14 个 ground-truth 任务里 79% 损坏率到底是怎么炸的

把多 Agent 冲突按粒度 × 类型的二维矩阵钉死,逐一拆解 AgentSTM 14 个任务里 11 个损坏案例的失败链路,给一份「看代码就能识别冲突类型」的工程字典

多Agent 冲突分类 失败模式 lost update write skew ground-truth

第 1 章告诉你”多 Agent 并发会炸”——79% 损坏率不是修辞。但**真到了具体代码层面,损坏长什么样?为什么损坏?怎么识别?**这是绝大多数 multi-agent 工程师卡住的地方——读了三篇论文还是看不出自己 AutoGen 代码里哪行有 race。本章不讲”事务怎么解决”(那是第 3 章),只做一件事:把多 Agent 冲突按”粒度 × 类型”的二维矩阵钉死,逐一拆解 AgentSTM benchmark 14 个任务里 11 个失败案例的具体损坏链路,给一份”看代码就能识别冲突类型”的工程字典。读完你会获得一个反射式能力——别人甩一段 multi-agent 代码给你,你 30 秒之内能指出”这里第 N 行第 M 行有 lost update / stale read / write skew”。

📑 目录


1. 不读这章会犯的最大错误

读了第 1 章后,最大的误判是:以为多 Agent 冲突就是”两个写操作打架”——也就是只识别 lost update。

这导致工程师在调试时只关心”两个 Agent 是不是写了同一个 key”,错过:

  • 一个 Agent 改了 record 的字段 A,另一个 Agent 整体写回 record 把 A 覆盖了 → 字段级 lost update
  • 一个 Agent 读了”我有 1000 积分”,并发地另一 Agent 减了 800,第一个 Agent 接着”消费 500”,最后余额变成 -300 → write skew
  • 两个 Agent 都给文件追加内容,但因为读到了同一份基线,最后只追加了一条 → silent file lost update
  • 一个 Agent 的 plan 是”先扣库存再发邮件”,另一个 Agent 的 plan 是”先发邮件再扣库存”,并发执行时邮件发了但库存没扣 → plan-level partial commit

🌟 结论Lost update 只是 4 大类冲突中的一类,且不是最难发现的那类。本章把 4 类全部展开到代码级。

🍎 直觉比喻冲突就像漏水——你看到地上有水(任务跑挂了),但不知道是水管哪里漏的(哪种冲突)。本章给你一张”水管图”。


2. 二维分类法:粒度 × 类型

把多 Agent 冲突切成两个正交维度——粒度(在什么大小的对象上发生)和类型(行为模式是什么)。这两个维度交叉得到一个 5×4 矩阵,覆盖你能在生产里碰到的几乎所有冲突。

2.1 粒度维度

field       record       file        plan         workflow
  ↓           ↓            ↓           ↓             ↓
最细 ────────────────────────────────────────────── 最粗
粒度典型例子检测难度适配场景
fieldbooking 的 bags / cabin 字段难(需要 schema-aware 检测)KV 中嵌套对象、DB 单行多字段
record整个 booking JSON整体 KV、整行 DB
filedoc.md / config.json文件协同编辑
plan一个 Agent 的多步 tool call 序列难(跨工具调用)多步 workflow
workflow多 Agent 联合 plan极难跨 Agent 的 saga / DAG

🧠 关键洞察粒度选错会导致检测又过严又过松

  • 选粗(如 record 级):两个 Agent 改同一 record 的不同字段会被误判为冲突 → 假阳性
  • 选细(如 field 级):复杂依赖(“loyalty 余额改之前必须读 transactions”)的跨字段约束被忽略 → 假阴性

2.2 类型维度

write-write     read-write       causal           plan-level
  ↓                ↓                ↓                  ↓
直接打架     新值依赖旧版本    跨实体不变式破坏    多步部分提交
类型DB 术语行为特征难发现度
write-writeLost Update两个写并发,一个被覆盖
read-writeStale Read读了陈旧版本生成的副作用已经发出⭐⭐⭐
causalWrite Skew跨实体不变式被破坏,单 Agent 看局部都合法⭐⭐⭐⭐
plan-levelSaga 部分提交Plan A 跑完 step 1 失败,但中间副作用已经发生⭐⭐⭐⭐⭐

2.3 二维矩阵全图

                  write-write    read-write     causal         plan-level
              ┌──────────────┬─────────────┬──────────────┬─────────────┐
field         │ 字段级 LU     │ 字段陈旧读   │ 字段约束破坏  │ —           │
              │ (booking.bags)│ (profile.v)  │ (a+b≥0)      │             │
              ├──────────────┼─────────────┼──────────────┼─────────────┤
record        │ Record LU    │ 整 record    │ 跨字段约束    │ —           │
              │ (整 booking) │ 陈旧读       │              │             │
              ├──────────────┼─────────────┼──────────────┼─────────────┤
file          │ 文件 LU      │ 基于旧版本    │ —            │ —           │
              │ (整文件覆盖) │ 修改         │              │             │
              ├──────────────┼─────────────┼──────────────┼─────────────┤
plan          │ —            │ —           │ —            │ Plan 部分    │
              │              │              │              │ 提交         │
              ├──────────────┼─────────────┼──────────────┼─────────────┤
workflow      │ —            │ —           │ —            │ Saga 级联    │
              │              │              │              │ 失败         │
              └──────────────┴─────────────┴──────────────┴─────────────┘

🌟 结论生产里 80% 的损坏发生在 record × write-write 和 file × write-write 这两个格子——但最难调试的、损失最大的那 20% 都在 causal 和 plan-level 这两列。


3. KV 域损坏案例:7 个任务里的 5 个炸点

AgentSTM benchmark 的 7 个 KV 任务,无保护并发下 5 个会损坏。逐一拆解。

3.1 concurrent_increment(计数器递增)

任务:3 个 Agent 各给计数器加 5,期望最终值 = 15。

def increment(ctx, amount=1):
    val = ctx.read("counter")          # ← (1) 读
    time.sleep(random.uniform(0.001, 0.01))  # 模拟 LLM 推理
    ctx.write("counter", {"value": val["value"] + amount})  # ← (2) 写

冲突类型record × write-write(Lost Update)

损坏链路

Agent A: read counter → {value: 0}, v=0
Agent B: read counter → {value: 0}, v=0  ← 也读到 0
Agent A: write {value: 5}              ← 提交成功
Agent B: write {value: 5}              ← 也写 5(应是 10)
Agent C: read counter → {value: 5}     ← 读到 A 的提交,但 B 的写已丢
Agent C: write {value: 10}             ← 最终是 10 不是 15

为什么是教科书 lost update:3 个 increment 都是”读-改-写”模式,并发下读到同一基线后写回。损失等于”读基线时丢失的并发增量”。

3.2 concurrent_transfer(账户转账)

任务:3 个 Agent 各从账户 A 转 100 元到 B 或 C。期望转账后 A+B+C = 1000(守恒),且 A ≥ 0。

def transfer(ctx, from_acc="A", to_acc="B", amount=100):
    accs = ctx.read("accounts")
    time.sleep(...)
    new_accs = {**accs}
    new_accs[from_acc] = accs[from_acc] - amount
    new_accs[to_acc] = accs[to_acc] + amount
    ctx.write("accounts", new_accs)

冲突类型record × causal(Write Skew)——这是本 benchmark 最有教学价值的一个

损坏链路

A=1000, B=0, C=0
Agent X: read → A=1000, B=0, C=0;决定 A→B 100;本地算 A=900, B=100, C=0
Agent Y: read → A=1000, B=0, C=0;决定 A→C 100;本地算 A=900, B=0, C=100
Agent Z: read → A=1000, B=0, C=0;决定 A→B 100;本地算 A=900, B=100, C=0

Agent X commit → A=900, B=100, C=0   ← v=1
Agent Y commit → A=900, B=0, C=100   ← 整体覆盖,X 的 B=100 丢了
Agent Z commit → A=900, B=100, C=0   ← 整体覆盖,Y 的 C=100 丢了

最终:A=900, B=100, C=0
total = 1000 ✓ (但少扣了 200)
A = 900 > 0 ✓ (但应该是 700)

为什么是 Write Skew 而不是 Lost Update:单看每个 Agent,它的本地操作都”自洽”——读到 A=1000,扣 100,A=900 合法。但多 Agent 合起来违反”应该总共扣 300”的不变式

🧠 关键洞察Write Skew 的恐怖之处在于”事务都成功了”——但语义上整体不对。比 lost update 更难发现,因为不会触发任何 abort。

3.3 concurrent_inventory(库存扣减)

任务:3 个 Agent 同时下单,每单扣 1 件库存,初始库存 5,期望剩 2。

def order(ctx, item="A"):
    inv = ctx.read("inventory")
    if inv[item] <= 0:
        return  # 缺货
    new_inv = {**inv, item: inv[item] - 1}
    ctx.write("inventory", new_inv)

冲突类型record × write-write(Lost Update with conditional check)

损坏链路:经典 read-modify-write 循环。3 个 Agent 都读到 5,都判 5>0 通过检查,都各自写入 4 → 最终库存 4 而不是 2,超卖了。

变种:如果初始库存是 1,3 个 Agent 都能通过 “inv[item] > 0” 检查,最终库存变成 0 但实际卖出了 3 单 → 超卖。这是电商里最经典的 race condition。

3.4 concurrent_upsert(用户档案更新)

任务:3 个 Agent 同时给用户表 upsert 不同字段(如 name / email / age)。期望最终 record 有所有 3 个字段。

def upsert(ctx, user_id, field, value):
    users = ctx.read("users")
    user = users.get(user_id, {})
    user[field] = value           # 改局部
    new_users = {**users, user_id: user}
    ctx.write("users", new_users)  # ← 整体写回 users

冲突类型record × write-write,但根因是粒度选错

损坏链路

Agent A: users={}; user={name:"A"}; write users={u42:{name:"A"}}
Agent B: users={}; user={email:"b@x"}; write users={u42:{email:"b@x"}}  ← 没 name
Agent C: users={}; user={age:30}; write users={u42:{age:30}}            ← 没 name 没 email

最终 users={u42:{age:30}}  ← A 和 B 全丢

为什么粒度选错:tool 接口让 Agent 整体写回 users实际操作意图是 field-level upsert。在 record 粒度下任何并发都是 lost update;如果粒度做到 field(直接 path-set),就不会冲突。

🌟 结论很多”lost update”其实是 tool 接口设计的锅,不是 Agent 的锅。第 8 章实战会专门讲怎么设计 tool 接口避免这种”接口诱导冲突”。

3.5 concurrent_max_update(最高分更新)

任务:3 个 Agent 上报新分数,期望最终 record = 3 人中最大值。

def report_score(ctx, score):
    rec = ctx.read("max_score")
    if score > rec["value"]:
        ctx.write("max_score", {"value": score})

冲突类型record × read-write(Conditional Stale Read)

损坏链路

当前 max=5
Agent A: 报 8。read → 5;判 8 > 5 → 写 8
Agent B: 报 10。read → 5;判 10 > 5 → 准备写 10  ← 此时 A 已经写了 8
Agent C: 报 7。read → 5;判 7 > 5 → 准备写 7

A commit → max=8
B commit → max=10
C commit → max=7  ← 把 10 覆盖成 7

为什么是 read-write 而非 write-write:C 的写决策依赖于读到的 max=5(陈旧)——如果读到 10,C 不会写。这是条件 stale read。


🌟 KV 域 5 个炸点小结

任务类型矩阵格子难发现度
concurrent_incrementLost Updaterecord × write-write
concurrent_transferWrite Skewrecord × causal⭐⭐⭐⭐
concurrent_inventoryLost Update with checkrecord × write-write⭐⭐
concurrent_upsert粒度错位 LUrecord × write-write⭐⭐
concurrent_max_updateConditional Stale Readrecord × read-write⭐⭐⭐

剩 2 个(price_update / queue_push)变体类似,留作读者识别练习——大概率是 record × write-write 和 record × read-write 的组合。


4. 文件域损坏案例:4 个任务里的 4 个炸点

文件域比 KV 更脆弱——文件天生是粗粒度对象,任何并发写都会撞。

4.1 add_functions(多 Agent 给一个 Python 文件加函数)

任务:3 个 Agent 各给 module.py 添加一个新函数,期望最终文件有 3 个函数。

def add_function(ctx, name, body):
    content = ctx.read("module.py")
    new_content = content + f"\n\ndef {name}():\n    {body}\n"
    ctx.write("module.py", new_content)

冲突类型file × write-write(File Lost Update)

损坏链路

初始 module.py 有 1 个函数 foo()
Agent A: read content(含 foo);append def funcA(); write
Agent B: read content(含 foo,没看到 funcA,因为读发生在 A 的 commit 之前);append def funcB(); write  ← 把 funcA 覆盖
Agent C: read content(可能含 funcB 没 funcA,或反之);append def funcC(); write

最终:module.py 可能只有 foo + funcC,丢失 A 和 B

🍎 直觉比喻像三个人同时编辑同一份 Word 文档——后保存的覆盖前保存的。Google Docs 用 OT/CRDT 解决;纯文件操作没解决方案。

🧠 关键洞察这是函数级粒度的强需求场景。AgentSTM 论文 Appendix A 提到 function-level resource wrapper 实现 100% function preservation——做法是把文件按 AST 切成 function 列表,每个 function 是独立 ATVar。粒度从 file 降到 function 级,并发并写不再冲突。

4.2 concurrent_config(多 Agent 改配置文件)

任务:3 个 Agent 各改 config.json 的不同字段(log_level / max_retries / timeout)。

冲突类型file × write-write,本质是 record × write-write 但落到文件层

损坏链路:和 KV 的 concurrent_upsert 一样——读到同一基线 JSON,本地改一个字段,整体写回,互相覆盖。

修复路径:把 JSON 解析后按 field 粒度处理(即不要”读 → 整体改 → 写”,而是用 path-set 接口)。

4.3 concurrent_json_merge(合并多 Agent 的 JSON 输出)

任务:3 个 Agent 各产出一份 partial JSON(如不同 section 的 schema),期望最终文件包含所有 sections。

冲突类型file × write-write,混合 plan-level

特殊性:Agent 的产出互相不冲突(每个产 section A / B / C 互不重叠),但写文件这一动作冲突——因为大家都”读 → merge → 写”,merge 步骤丢失对方。

🧠 关键洞察JSON merge 的语义冲突 = file-level 操作冲突。如果直接用 jq mergedict.update 做”两次 merge”,结果取决于顺序。CRDT 在这场景下是合适方案(CodeCRDT 论文 §4 专门讨论),AgentSTM 路线则是把 JSON 按 path 切成 ATVar。

4.4 concurrent_log_append(并发追加日志)

任务:3 个 Agent 各追加 5 条 log,期望最终文件有 15 条。

def append_log(ctx, msg):
    content = ctx.read("app.log")
    ctx.write("app.log", content + "\n" + msg)

冲突类型file × write-write(Append Lost Update)

特殊性:和 add_functions 完全同构——append 操作天然是 read-modify-write。

修复路径:用 OS 的 O_APPEND 原子追加(write 系统调用保证整条追加原子)。这是文件层 race 唯一不需要事务的解法——但限制是必须用文件 descriptor 操作,Agent 工具调用层一般做不到。


🌟 文件域 4 个炸点小结:4 个全部炸——这是文件域的本质:整体读、整体写的并发模式必然丢失。

唯一解法只有两条:

  1. 降粒度:file → function / section / path-set,把粗对象切成细对象(AgentSTM 路线)
  2. 应用 CRDT:让 merge 算子无冲突(CodeCRDT 路线)

OS 级的 O_APPEND 是个例外,但只对单一 append 场景起作用,且 Agent 工具调用层接触不到。


5. 预订域损坏案例:3 个任务里的 2 个炸点

5.1 concurrent_booking_update(多 Agent 改预订不同字段)

任务:3 个 Agent 各改 booking 的不同字段(bags / cabin / loyalty_points)。和 concurrent_upsert 同构——但这次的语义是”航班订票”。

def modify_booking(ctx, field, value):
    booking = ctx.read("booking")
    new_booking = {**booking, field: value}
    ctx.write("booking", new_booking)

冲突类型record × write-write,粒度选错

损坏链路:和 concurrent_upsert 完全相同——3 个 Agent 都读到同一基线 booking,各自改一个字段写回,互相覆盖。

为什么这个例子特别值得重复:航班预订是 τ-bench 经典 workload。真实生产里这种”3 Agent 改 3 个不同字段”的并发模式天天发生——客服、行李、舱位变更。如果 booking 是一个 record,三个并发改字段 100% 损坏。

5.2 concurrent_seat_assignment(并发分配座位)

任务:3 个 Agent 各给一名乘客分配座位(1A / 1B / 1C),从 3 张可用座位中扣减。

def assign_seat(ctx, seat, passenger):
    seats = ctx.read("seats")
    if seats[seat] is not None:
        return  # 已被占
    new_seats = {**seats, seat: passenger, "available": seats["available"] - 1}
    ctx.write("seats", new_seats)

冲突类型record × causal(Write Skew with Counter)

损坏链路

seats={1A: None, 1B: None, 1C: None, available: 3}
Agent A: read → available=3;分 1A 给 Alice;本地 {1A:Alice, 1B:None, 1C:None, available:2}
Agent B: read → available=3;分 1B 给 Bob;本地 {1A:None, 1B:Bob, 1C:None, available:2}
Agent C: read → available=3;分 1C 给 Carol;本地 {1A:None, 1B:None, 1C:Carol, available:2}

A commit → {1A:Alice, available:2}
B commit → {1B:Bob, available:2}     ← 但 1A 应该还是 Alice
C commit → {1C:Carol, available:2}   ← 1B 应该还是 Bob

最终:{1A:None, 1B:None, 1C:Carol, available:2}  ← 实际只分配了 1 个,但 available 也减了 1

为什么 available 字段是 write skew 经典:每个 Agent 看局部都”对”——读到 available=3,分一个减为 2 合法。但没有 Agent 看到其他 Agent 也在做同样的事,约束 “已分配数 + available = 3” 被并发整体破坏。

🌟 这是航空公司超卖座位的真实场景——多个客服同时改同一份座位图,低并发时偶发,高并发(黑五开抢、突发取消改签)时频发。Sabre / Amadeus 等 GDS 系统用悲观锁解决,代价是并发吞吐被夹死。

5.3 concurrent_loyalty_spend(并发消费会员积分)

任务:3 个 Agent 各帮用户消费 1000 积分,期望从 10000 减到 7000,且 transactions 里有 3 条记录。

def spend_points(ctx, amount=1000, desc="upgrade"):
    loyalty = ctx.read("loyalty")
    new_loyalty = {
        "points": loyalty["points"] - amount,
        "transactions": loyalty["transactions"] + [{"amount": amount, "desc": desc}],
    }
    ctx.write("loyalty", new_loyalty)

冲突类型record × causal + record × write-write(混合)

损坏链路(最常见的一种):

loyalty={points: 10000, transactions: []}
3 个 Agent 都 read → points=10000, txs=[]
3 个 Agent 都本地算 points=9000,txs=[一条新记录]
3 个 Agent 都 write → 最后一个赢

最终:points=9000,txs=[1 条]  ← 应该 points=7000, txs=[3 条]

为什么是混合冲突

  • points 字段:lost update(应该 -3000,实际 -1000)
  • transactions 数组:lost append(应该 3 条,实际 1 条)

🧠 关键洞察业务上的”扣余额 + 写流水”就是经典的两件事必须原子绑定——也是数据库领域 ACID transaction 的诞生场景。在 multi-agent 场景下,这两件事如果不原子绑定,要么对账平不上、要么流水对不上扣款。


🌟 预订域 2 个炸点小结

任务类型矩阵格子难发现度
concurrent_booking_update粒度错位 LUrecord × write-write⭐⭐
concurrent_seat_assignmentWrite Skew with Counterrecord × causal⭐⭐⭐⭐
concurrent_loyalty_spendLU + 流水丢失record × causal + write-write⭐⭐⭐⭐

剩 1 个(其实总共 3 个 booking 任务,只有 2 个炸——seat_assignment 在某些初始条件下不炸,看具体并发)——和金钱、库存相关的混合冲突在生产里永远是最高优先级 bug


6. 失败模式按”检测难度”排序

把前 5 节看到的所有损坏案例按”工程上发现这个 bug 有多难”排序:

最容易            ←──────────────────────────→             最难
┌────────────┬────────────┬────────────┬────────────┬────────────┐
│Lost Update │Stale Read  │条件 Stale  │Write Skew  │Plan Partial│
│            │            │Read        │            │Commit      │
├────────────┼────────────┼────────────┼────────────┼────────────┤
│2 个写撞,  │读了旧值生成 │读了旧值,   │多 Agent 各 │Plan 跑一半 │
│明显的丢失  │副作用已发出 │用旧值判分支 │自合法,整体│崩,副作用  │
│            │            │            │违反不变式  │已经发了    │
├────────────┼────────────┼────────────┼────────────┼────────────┤
│检测:        │检测:       │检测:       │检测:       │检测:       │
│diff 期望和  │看 commit  │看 commit  │看跨实体不  │看 plan     │
│实际值      │版本与读版  │版本 +      │变式被破坏  │trace +     │
│            │本之差      │分支决策    │            │副作用 log  │
└────────────┴────────────┴────────────┴────────────┴────────────┘

关键观察

  • Lost Update 最容易发现:跑 ground-truth checker 立刻发现”实际值 ≠ 期望”
  • Stale Read 难发现:副作用(发邮件、发推送)已经出去了,事后只能从用户投诉发现
  • Conditional Stale Read 更难:分支决策依赖陈旧值,但事后看代码每个分支判断都”对”
  • Write Skew 最难发现:每个事务都成功 commit,没有任何 abort 或异常——只能靠跨实体不变式校验
  • Plan Partial Commit 灾难性:Plan 跑了一半挂了,前半步骤的副作用(发短信、扣款)已经生效,后半补救做不到

🧠 关键洞察Lost update 不是最危险的,Write Skew 才是——前者噪声大、后者隐蔽。生产事故里大部分”为什么这个用户多扣了一笔钱”、“为什么这个座位卖了 4 次”都是 Write Skew,不是 Lost Update

🌟 结论不要只盯着 lost update 防御。如果你的 multi-agent 系统涉及金钱、库存、座位、配额——优先做 Write Skew 防御(即跨实体不变式校验)。


7. 不可能三角:原子性 / 隔离性 / 性能

数据库领域的 CAP 定理(Consistency / Availability / Partition tolerance 三选二)在 Agent 事务层有个降维的对应——原子性 / 隔离性 / 性能三选二:

                        原子性
                       (Atomicity)

                         ╱ ╲
                        ╱   ╲
                       ╱     ╲
                      ╱       ╲
                     ╱   ✗    ╲
                    ╱  三选二  ╲
                   ╱             ╲
                  ▼               ▼
              隔离性 ───────── 性能
            (Isolation)      (Performance)
取舍组合代表方案牺牲什么
原子 + 隔离,弃性能悲观锁 / 全串行吞吐被压到 1
原子 + 性能,弃隔离CRDT 最终一致5-10% 残余语义冲突
隔离 + 性能,弃原子无保护并发79% 任务损坏
三角折中:乐观执行 + 验证 + 重规划AgentSTM复杂度上升、LLM cost 上升

🧠 关键洞察AgentSTM 的”乐观+重规划”路线本质是承认这是个不可能三角,然后用 LLM 推理代价换三角内部的折中。不是不付代价,是付LLM cost 这种相对可控的代价,换原子+隔离+(高并发性能)三个目标的同时近似达成。

🍎 直觉比喻像人类社会的法律系统——犯罪不被允许(原子+隔离),但社会还是要运转(性能)。代价是法庭、警察、律师(LLM cost)这一整套基础设施。

7.1 不同派系的三角位置

派系取舍三角定位
MetaGPT (SOP 串行)原子 + 隔离弃性能
AutoGen (无保护)隔离 + 性能(伪)弃原子(结果连隔离都没了)
SagaLLM (Saga)原子 + 隔离弃性能
CodeCRDT原子 + 性能弃严格隔离(5-10% 残余)
AgentSTM三角折中用 LLM cost 换三选二的 trade-off

🌟 结论没有”完美”方案。读完任何 multi-agent 论文,第一件事是把它放到这个三角的哪个角或边上——这能避免被论文里”我们更好”的描述误导。


8. 检测粒度的工程取舍

第 2 节说粒度选错会假阳/假阴,这里展开”工程上具体怎么选”。

8.1 检测粒度的层级与代价

粗 ←─────────────────────────────────────────→ 细
workflow    plan    file    record    field    sub-field
(Saga)    (Atomix) (file)   (KV)    (schema) (path-set)
   ↓        ↓        ↓        ↓        ↓         ↓
检测开销   小        小       中       中       大        极大
假阳率     高        中高     高       中       低        极低
假阴率     低        中       高(粗)   中       低        极低

8.2 不同粒度下相同案例的检测结果

concurrent_booking_update(3 Agent 改不同字段)为例:

检测粒度是否报冲突是否实际有冲突判定
workflow否(不同字段)假阳性,所有改 booking 的 task 都被串行化
plan假阳性
record否(不同字段)假阳性,AgentSTM 默认行为
field✓ 正确,3 个并发不冲突

但同样的粒度,对 concurrent_loyalty_spend

检测粒度是否报冲突是否实际有冲突判定
workflow正确
plan正确
record是(points 字段都被改)正确
field是!(points 字段没切到)假阴性,points lost update 漏了

🧠 关键洞察没有一刀切的”对的粒度”——它依赖语义。

  • 写不同字段的并发:粒度越细越好(field 最优,避免假阳)
  • 写同一字段的并发:粒度细 = 检测保留正确(field 仍报)
  • 读了某字段、写另一字段且依赖被读字段:field 粒度漏掉跨字段依赖——这是为什么需要 read-set 的原因(不只是写集,读集也要纳入冲突检测)

8.3 AgentSTM 的默认选择与补救

AgentSTM 默认 record 粒度(KV / 文件整体)。这能覆盖大部分 lost update,但:

  • 假阳:3 Agent 改 booking 不同字段被串行化(性能损失)
  • 假阴:跨字段依赖(如 spend_points 里依赖 points 写 transactions)需要在 task_fn 里显式 read-then-write 触发 read-set

补救

  • 降粒度:FileATVar 提供 function-level 切分(前面 add_functions 任务的解法)
  • 升语义:把 task_fn 的 read-then-write 写齐,让 ConflictDetector 的 read-set 检测发挥作用
  • 接 AST/JSONPath:用 path-set 接口让粒度自适应

🌟 结论默认 record 粒度,配合 read-set 补检测,是当前最稳的工程默认。需要更细粒度时按需降到 field/path。


9. 工程现实:损坏 ≠ 论文里的冲突类型

读完前 8 节,理论上你能识别冲突类型。但生产里还有 4 个”理论之外的现实”:

9.1 LLM tool call 不是确定性的——同样的 plan,结果可能不一样

数据库事务有”确定输入 → 确定输出”的假设。Agent 的 tool call 调 LLM,同样输入下两次执行可能产出不同结果

Agent A: tool_call(generate_email, user="Bob") → "Dear Bob, ..."
Agent A: 重试 → "Hi Bob, ..."  ← 同一 prompt,不同结果

对冲突分类的影响:lost update 的判定难度上升——你看到两份不同邮件,无法判断”是被覆盖了”还是”LLM 自己产出了不同结果”。

9.2 副作用是不可逆的——发出去的邮件追不回

数据库事务的回滚是写日志反向应用。Agent 的 tool call 副作用(发邮件、扣信用卡、调外部 webhook)大多没有回滚原语。

对冲突分类的影响:plan-partial-commit 是真实存在且无救援的——一旦发了一半邮件,剩下一半不能补也不能撤,业务上必须人工介入。

9.3 部分 LLM 工具是 stateless,部分是 stateful——分类难

stateless tool:  read_db(key) → value           ← 多次调相同结果
stateful tool:   send_email(to, body) → ok      ← 副作用,重发会重复

对冲突分类的影响:同一段代码,stateless tool 的并发是”最多浪费 LLM cost”,stateful tool 的并发是”邮件发了 3 次”。冲突分类必须按 tool 性质分类,不能纯靠代码语法。

9.4 LLM 的 reasoning 本身可能”判断错了有没有冲突”

AgentSTM 让 LLM 做 replan,但LLM 自己也可能误判

  • 真的没冲突,LLM 觉得有 → 不必要的重规划
  • 真的有冲突,LLM 觉得没有 → 业务级 inconsistency

对冲突分类的影响:分类逻辑要鲁棒,不能假设 LLM 永远 reason 正确。AgentSTM 论文 Limitations 提到”replan quality depends on LLM reasoning capability”——弱模型下假阳/假阴都更高。


🧠 工程现实小结:理论分类(粒度 × 类型)是地图,工程现实是泥地。实际项目落地需要在地图基础上加 4 层补丁

  1. 区分 LLM 决定性 vs 非决定性
  2. 区分 tool 副作用可逆 vs 不可逆
  3. 按 tool 性质(stateless / stateful)分类
  4. 不假设 LLM reason 永远正确

✅ 自我检验清单

  • 二维矩阵:能默写”粒度(field/record/file/plan/workflow)× 类型(write-write/read-write/causal/plan-level)” 5×4 矩阵,并各举一个 Agent 场景
  • 5 类损坏排序:能用一句话排出”Lost Update / Stale Read / Conditional Stale Read / Write Skew / Plan Partial Commit”由易到难的发现难度
  • counter increment:能 30 秒内识别 read → modify → write 是 lost update,并讲清损坏链路
  • transfer write skew:能解释为什么 3 个并发转账”事务都成功”但总额对不上,区分这与 lost update 的根本不同
  • 粒度错位:能解释”concurrent_upsert 表面是 lost update 实际是粒度选错”,并指出修复路径
  • 文件域 4 个全炸:能解释为什么文件域的 4 个任务在无保护下 100% 损坏,列出 2 条修复路径
  • Write Skew 最危险:能解释”为什么 Write Skew 比 Lost Update 在生产里更糟”
  • 不可能三角:能画出”原子性 / 隔离性 / 性能”三选二图,并把 MetaGPT / AutoGen / SagaLLM / CodeCRDT / AgentSTM 5 个方案放到对应位置
  • 粒度选择:能用”假阳/假阴”框架解释为什么 record 粒度对不同字段并发是假阳、对同字段写是正确
  • 工程现实补丁:能列出 4 个理论分类无法覆盖的工程现实(LLM 非决定性 / 副作用不可逆 / stateless vs stateful tool / LLM 误判)

📚 参考资料

概念入门

  • Lost Update / Write Skew 的标准定义:Berenson et al. 1995, “A Critique of ANSI SQL Isolation Levels” —— SIGMOD 1995。隔离级别经典文献,第 4 章给出 8 种 phenomenon 的定义
  • ANSI SQL 隔离级别Isolation (database systems) —— 维基百科
  • CAP 定理CAP theorem —— 维基百科。原子-隔离-性能三角的灵感来源

关键论文

  • AgentSTM(Anonymous, ARR/EMNLP 2026) ⭐:本章 14-task ground-truth benchmark 的来源;§3 RQ1 给出 79% 损坏率的具体损坏案例
  • τ-bench(Yao et al., 2024)arXiv 2406.12045 —— concurrent_booking_update / concurrent_seat_assignment 的灵感来自 τ-bench airline domain
  • CodeCRDT(Pugachev, 2025)arXiv 2510.18893 —— 文件域 §4.3 concurrent_json_merge 的 CRDT 视角
  • Sagas(García-Molina & Salem, 1987):ACM SIGMOD Record 1987 —— Plan Partial Commit / Plan-level 冲突的形式化前身

行业讨论

  • 航空业的座位超卖:Sabre / Amadeus GDS 系统设计相关公开博客 —— concurrent_seat_assignment 的真实场景背景
  • 电商超卖故障:各大电商技术博客(如阿里、京东)“双 11 库存超卖修复”系列 —— concurrent_inventory 的真实场景背景
  • 分布式系统 Write Skew 经典案例:Martin Kleppmann “Designing Data-Intensive Applications” 第 7 章 —— Write Skew 在工业界标准教材

框架文档

  • AgentSTM benchmark 源码:本章引用的 14 个任务在论文 prototype 的 experiments/task_success_benchmark.py
  • AutoGen 多 Agent 并发示例microsoft.github.io/autogen —— GroupChat 文档展示了无保护并发的标准代码模式