第2章:冲突的分类法与失败模式 —— 14 个 ground-truth 任务里 79% 损坏率到底是怎么炸的
把多 Agent 冲突按粒度 × 类型的二维矩阵钉死,逐一拆解 AgentSTM 14 个任务里 11 个损坏案例的失败链路,给一份「看代码就能识别冲突类型」的工程字典
第 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. 不读这章会犯的最大错误
- 2. 二维分类法:粒度 × 类型
- 3. KV 域损坏案例:7 个任务里的 5 个炸点
- 4. 文件域损坏案例:4 个任务里的 4 个炸点
- 5. 预订域损坏案例:3 个任务里的 2 个炸点
- 6. 失败模式按”检测难度”排序
- 7. 不可能三角:原子性 / 隔离性 / 性能
- 8. 检测粒度的工程取舍
- 9. 工程现实:损坏 ≠ 论文里的冲突类型
- 自我检验清单
- 参考资料
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
↓ ↓ ↓ ↓ ↓
最细 ────────────────────────────────────────────── 最粗
| 粒度 | 典型例子 | 检测难度 | 适配场景 |
|---|---|---|---|
| field | booking 的 bags / cabin 字段 | 难(需要 schema-aware 检测) | KV 中嵌套对象、DB 单行多字段 |
| record | 整个 booking JSON | 中 | 整体 KV、整行 DB |
| file | doc.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-write | Lost Update | 两个写并发,一个被覆盖 | ⭐ |
| read-write | Stale Read | 读了陈旧版本生成的副作用已经发出 | ⭐⭐⭐ |
| causal | Write Skew | 跨实体不变式被破坏,单 Agent 看局部都合法 | ⭐⭐⭐⭐ |
| plan-level | Saga 部分提交 | 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_increment | Lost Update | record × write-write | ⭐ |
| concurrent_transfer | Write Skew | record × causal | ⭐⭐⭐⭐ |
| concurrent_inventory | Lost Update with check | record × write-write | ⭐⭐ |
| concurrent_upsert | 粒度错位 LU | record × write-write | ⭐⭐ |
| concurrent_max_update | Conditional Stale Read | record × 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 merge 或 dict.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 个全部炸——这是文件域的本质:整体读、整体写的并发模式必然丢失。
唯一解法只有两条:
- 降粒度:file → function / section / path-set,把粗对象切成细对象(AgentSTM 路线)
- 应用 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 | 粒度错位 LU | record × write-write | ⭐⭐ |
| concurrent_seat_assignment | Write Skew with Counter | record × causal | ⭐⭐⭐⭐ |
| concurrent_loyalty_spend | LU + 流水丢失 | 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 层补丁:
- 区分 LLM 决定性 vs 非决定性
- 区分 tool 副作用可逆 vs 不可逆
- 按 tool 性质(stateless / stateful)分类
- 不假设 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 文档展示了无保护并发的标准代码模式