207 lines
9.1 KiB
Markdown
207 lines
9.1 KiB
Markdown
---
|
||
name: system-design-lessons
|
||
description: >-
|
||
系统设计实战教训:Counter/锁生命周期、进程管理、续杯机制、
|
||
广播路径一致性、JSON 解析验证。涉及并发控制、进程调度、
|
||
外部命令集成时触发。
|
||
---
|
||
|
||
# 系统设计教训
|
||
|
||
> 来源:从三国团队(庞统/司马懿/诸葛亮)~2GB 对话历史中蒸馏
|
||
> 卡片数:7(批次2卡片4/5/6/7/8/9/14,部分与批次1卡片7有交叉)
|
||
|
||
## 适用场景
|
||
|
||
- 设计并发控制、锁、counter 机制时
|
||
- 实现进程管理(spawn/retry/exit)时
|
||
- 集成外部命令(stdout JSON 解析)时
|
||
- 实现广播/单播路径时
|
||
- 需要三层一致性审查(PRD→设计→代码)时
|
||
|
||
## 经验清单
|
||
|
||
### 1. Counter/锁的生命周期必须贯穿完整调用链(severity: high)
|
||
|
||
**场景**:Agent 并发控制需要 counter/锁机制。acquire-release 不成对会导致永久占满或双重释放。
|
||
|
||
**正确做法**:
|
||
1. **Counter 贯穿 retry 链**:acquire 在 spawn 前,release 在最终退出(done/failed)后
|
||
2. Retry 不释放 counter,而是复用同一个 counter slot
|
||
3. Cooldown 机制:release 后短期冷却,防止立即 re-acquire 导致抖动
|
||
4. 三层控制:cooldown → global limit → per-agent → per-session
|
||
|
||
**⚠️ 常见错误**:
|
||
- `_handle_exit` retry 路径提前调 `on_complete("retry_release")` 释放 counter,导致同一 Agent 被并发 spawn 多次 [simayi/mail]
|
||
- `_do_retry` 中 counter 仍被占用但 `on_complete=None`,续杯永远无法 acquire
|
||
|
||
**关键细节**:
|
||
- 所有 `release` 调用都要带 session_id(counter v2.1 的教训)
|
||
- on_complete 回调的时序:进程退出 ≠ 回调完成
|
||
- 广播 spawn 路径也要 catch AgentBusyError
|
||
|
||
**反面教训**:counter 提前释放导致 zhipu API 429(rate limit 被打爆),Gateway 假死。修复后 counter 泄漏导致续杯死循环。
|
||
|
||
> 来源:批次2卡片6(counter生命周期,freq=3)
|
||
|
||
---
|
||
|
||
### 2. 进程退出 ≠ 资源释放(severity: high)
|
||
|
||
**场景**:异步系统中,子进程退出和回调执行不是原子操作。假设进程退出时资源已释放会出现竞态。
|
||
|
||
**正确做法**:
|
||
1. 在 `_do_retry` 中手动释放 counter,并将 `on_complete` 设为 None 防止 double release
|
||
2. 资源释放应有明确触发点,不能依赖"进程退出自然释放"
|
||
3. 每条退出路径都要有对应的资源释放
|
||
|
||
**⚠️ 常见错误**:
|
||
- 进程退出 → on_complete 未调用 → counter 仍占用 → spawn_full_agent 的 can_acquire 返回 False → AgentBusyError → 续杯永远不成功
|
||
|
||
**关键细节**:
|
||
- `on_complete=None` 后续杯 spawn 完成无幻觉门控,靠 ticker 超时兜底——影响可接受但需记录
|
||
- 安装目录同步是必须的检查项(司马懿每次评审都验证)
|
||
|
||
**反面教训**:未处理回调时序导致续杯死循环,任务永远无法完成。
|
||
|
||
> 来源:批次2卡片7(进程管理,freq=2)
|
||
|
||
---
|
||
|
||
### 3. 续杯(retry)机制是最大假死来源(severity: high)
|
||
|
||
**场景**:任务执行超时后需要 retry。retry 机制本身有 Bug 会导致无限重试或永久卡住。
|
||
|
||
**正确做法**:
|
||
1. 续杯模板必须专用(MAIL_RETRY_PROMPT),明确禁止状态转换命令
|
||
2. retry_count 必须在每次续杯时递增,max_retries 兜底
|
||
3. 续杯应复用 session 保持上下文连续性
|
||
4. use_main_session 在三处(主 dispatch、legacy broadcast、retry)必须统一
|
||
|
||
**⚠️ 常见错误**:
|
||
- retry_count 在续杯中永远不变(retry_count=1/3),续杯永不停
|
||
- Mail 续杯用 RETRY_PROMPT(含 review 标记指令),Agent 标了 review 但 _mail_auto_complete 只认 done
|
||
- 续杯创建新 session 而非复用 main session,每次重试是全新上下文,丢失之前工作
|
||
|
||
**关键细节**:
|
||
- 续杯模板的"不要执行状态转换"指令是关键——Agent 容易自作主张标 done
|
||
- 司马懿在 Bug-8 评审中确认 use_main_session 必须统一
|
||
|
||
**反面教训**:续杯死循环阻塞 tick,导致整个调度停滞。PM2 restart 后旧 session 仍在续杯,新 session 又被创建,资源爆炸。
|
||
|
||
> 来源:批次2卡片8(续杯机制,freq=5+)
|
||
|
||
---
|
||
|
||
### 4. 广播路径与单播路径行为必须一致(severity: medium)
|
||
|
||
**场景**:任务调度有指定 Agent 的单播和广播认领两条路径。路径行为不一致会导致系统性问题。
|
||
|
||
**正确做法**:
|
||
1. 所有 spawn 路径(单播、广播、retry)必须传递完整上下文参数
|
||
2. 评审时重点检查:新功能是否覆盖了所有 spawn 路径
|
||
3. 广播 spawn 路径也要 catch AgentBusyError
|
||
|
||
**⚠️ 常见错误**:
|
||
- 广播路径 `ticker._broadcast_claim` 调 `spawn_full_agent` 时没传 `task_id` 和 `db_path`
|
||
- 导致续杯时无法查询任务状态,retry_count 永远不变 [simayi/mail]
|
||
- Mail 变广播导致所有 Agent 假死——路径不一致的典型案例
|
||
|
||
**关键细节**:
|
||
- 司马懿在 v2.7.2 Pipeline 重构评审中建议统一 TickResult 返回值 + 方法注册
|
||
- 每次新增参数时,必须检查所有 spawn 调用点
|
||
|
||
**反面教训**:广播路径缺失参数导致续杯死循环,影响所有通过广播认领的任务。
|
||
|
||
> 来源:批次2卡片9(广播路径,freq=3)
|
||
|
||
---
|
||
|
||
### 5. stdout JSON 解析路径必须实测验证(severity: medium)
|
||
|
||
**场景**:`openclaw agent --json` 的输出结构可能随版本变化。解析路径是猜的而非实测的会导致静默失败。
|
||
|
||
**正确做法**:
|
||
1. 解析外部命令输出前,先用实际命令验证输出格式
|
||
2. 评审时要求看到实测输出样例
|
||
3. JSON 解析失败时应该报错,不应静默走 fallback
|
||
|
||
**⚠️ 常见错误**:
|
||
- spawner 代码取 `data.response.meta.transport`,但实际输出格式不同
|
||
> "P0:_parse_stdout_json 解析路径错误(根因)" [pangtong/mail]
|
||
- 解析不到数据时走了 fallback,掩盖了问题——静默失败比报错更危险
|
||
|
||
**关键细节**:
|
||
- 这个 Bug 从第一天就存在("从第一天就存在的根因 bug")
|
||
- 司马懿在 Spawner v3.0 评审中指出新路径 `data.result.meta.executionTrace` 需确认
|
||
- 所有依赖 stdout 判定的功能(如幻觉门控)都会受影响
|
||
|
||
**反面教训**:解析路径错误导致 stdout 信息丢失,幻觉门控等关键功能失效。
|
||
|
||
> 来源:批次2卡片14(JSON解析,freq=2)
|
||
|
||
---
|
||
|
||
### 6. 设计文档-代码三层一致性审查(severity: high)
|
||
|
||
**场景**:迭代开发中设计文档更新不及时,或代码实现偏离设计。需要 PRD→设计→代码的三层审查。
|
||
|
||
**正确做法**:
|
||
1. 每次代码评审前,先对设计文档逐条检查实现覆盖度
|
||
2. 设计文档用明确编号(§B1/B2/B3)便于逐条追踪
|
||
3. 安排"背靠背一致性审查":PRD→设计→代码全覆盖
|
||
4. 司马懿的核心职责就是做这种三层对照
|
||
|
||
**⚠️ 常见错误**:
|
||
- 设计文档 §B2 在代码中完全遗漏,评审者初评也未发现 [pangtong/mail]
|
||
> "代码只实现了 B1/B3/B4,B2 遗漏了"
|
||
|
||
**关键细节**:
|
||
- 庞统专门发 #311 邮件请求三层一致性审查
|
||
- 司马懿在 counter 评审中发现设计与代码不一致
|
||
- 三层审查顺序:先 PRD→设计(需求覆盖),再设计→代码(实现忠实)
|
||
|
||
**反面教训**:设计-代码不一致导致假死检测失效,compact 后无法区分"还在跑"和"卡死了"。
|
||
|
||
> 来源:批次2卡片4(设计-代码一致性,freq=4+)
|
||
|
||
---
|
||
|
||
### 7. E2E 测试必须用真实环境(severity: medium)
|
||
|
||
**场景**:单元测试只能验证单个模块逻辑。编排系统的 Bug 往往出在模块交互,只有真实环境 E2E 能暴露。
|
||
|
||
**正确做法**:
|
||
1. E2E 测试走真实 Ticker + 真实 Agent spawn,不手动推动状态
|
||
2. 测试用例覆盖完整生命周期:创建→调度→执行→续杯→完成/失败
|
||
3. 司马懿独立跑 E2E 并做根因分析,与庞统修复做交叉验证
|
||
4. 每次修复后全量重跑 E2E(不是只跑失败的用例)
|
||
|
||
**⚠️ 常见错误**:
|
||
- 只跑单元测试就部署,dashboard 上大量任务停在中间状态 [user/simayi-correction]
|
||
- 手动改 DB 推进任务(污染数据、掩盖真正 bug)
|
||
|
||
**关键细节**:
|
||
- 司马懿 E2E 报告格式:通过/失败计数 + 每个失败用例的根因分析
|
||
- 从 6/10 → 8/10 → 9/10 → 10/10 的渐进修复证明 E2E 驱动开发有效
|
||
- MEMORY.md 中 L1-L7 经验教训也是 E2E 驱动复盘的产出
|
||
|
||
**反面教训**:未做 E2E 导致假死问题在生产环境中才暴露,排查成本远高于测试阶段。
|
||
|
||
> 来源:批次2卡片10(E2E测试,freq=3)+ MEMORY.md L1-L7
|
||
|
||
---
|
||
|
||
## 检查清单(快速参考)
|
||
|
||
- [ ] Counter 的 acquire-release 是否成对?retry 链中是否贯穿?
|
||
- [ ] 进程退出后的 on_complete 回调是否处理?有无资源泄漏?
|
||
- [ ] 续杯模板是否专用?是否禁止状态转换指令?
|
||
- [ ] retry_count 是否在每次续杯时递增?
|
||
- [ ] 续杯是否复用 session(不创建新 session)?
|
||
- [ ] 新增参数时,所有 spawn 路径(单播/广播/retry)是否都传了?
|
||
- [ ] 外部命令输出格式是否实测验证过?
|
||
- [ ] JSON 解析失败是报错还是静默 fallback?
|
||
- [ ] 设计文档是否逐条检查了实现覆盖度?
|
||
- [ ] E2E 测试是否走真实环境(非手动推进状态)?
|