更多智能体,同一个盲点:AI 审查为何没发现真实平台的限制
一个模型实现,另一家厂商的模型审查,仍然让三个 Upstash 运行时限制直到生产迁移时才暴露。我们后来先实测平台,把结果写进测试替身,再在隔离的临时命名空间里演练。
这次迁移为什么终于有了计划,前篇交代了起因:一次“推荐”推送引发的生产事故。
我让一组 AI 智能体执行了一次持续一周的生产数据迁移:一个模型实现,另一家厂商的模型审查,另有一个协调智能体安排并执行各轮迁移操作。审查模型一周给出了约 60 份结论,生产迁移却仍然连续三次失败。每次撞上的,都是没有任何智能体实际探测过的 Upstash Lua 运行时限制。
这次审查的盲点出在验证方式上。实现模型同时写了代码、测试和数据存储的测试替身;审查模型读取这些材料。它们彼此一致,却没有一项检查接触真实平台。增加审查角色,并不会自动增加真实环境的证据。
后来,我们先测量平台,把测量结果写进测试替身,再用真实后端演练完整写入路径。这篇文章说明原因,也给出临时命名空间的防护代码和实测清单。
迁移的内容与分工
我的网站 williamliu.ai 从 Cloudflare R2 提供播客音频,用 Upstash(无服务器 Redis)保存小型媒体索引。这次要把约 3,900 条媒体记录迁移到按内容寻址的格式:文件名包含内容哈希,Lua 脚本负责原子更新索引。
分工如下:
- 协调: 一个 Claude 会话,规划并执行每轮生产迁移操作。
- 实现: OpenAI Codex,先写测试,再实现改动。
- 审查: Claude Opus,为每条发现标明严重程度、文件和行号。CRITICAL 或 HIGH 级别的问题都会阻止改动继续。
单看流程数字,一周约 60 份审查结论,其中约 40% 要求停止推进。可连续三天,生产环境都暴露出了新问题。
三次失败,三个没有测过的限制

第一天:registry overflow。 每批 100 条记录时失败。Upstash 的 Lua table.concat 有一个按元素数量计算的硬上限,与字节数无关。在我们的数据库上,2,561 个元素可以,2,562 个就失败。一批 100 条记录需要约 3,500 个元素。

第二天:一个字段悄悄消失了。 在 Upstash 上,cjson.null 是 nil。将 {"a":null,"b":1} 解码再编码,得到的只有 {"b":1}。真实 Redis 会保留一个表示 null 的特殊值,我们的测试替身也如此。因此,约 3,900 条记录写入时都丢了一个可空字段;最终封存检查正确地拒绝了这批记录。
第三天:HTTP 400。 响应正文是 ERR Error running script: execution timed out。Upstash 限制了单次脚本调用可使用的 CPU 时间。以我们的数据为例,封存 250 条记录耗时 0.15 秒,500 条在 0.27 秒时报超时;因此,这组数据下单次调用可用的实际预算约为四分之一秒。

改变计划的那条指令
第三次出问题时,我对协调智能体说了这句话,保留原来的拼写:
“you do it everyday but nothing is accomplished. wasting time”
这句话指出了反复发生的模式:每天都提交一个经过审查、能够修复昨天故障的正确补丁,第二天又遇到新的生产问题。智能体因此停下局部修补,开始追问为什么整个审查过程总会漏掉这类问题,并由此引出了真实后端演练的规则。
但这条指令也缺少方向。表达不满只能说明现状有问题,没有告诉智能体应该改变什么。更明确的要求应该是:“before the next production session, prove the whole write path on the real backend.” 我本该在第一天就提出这一点。
审查材料为什么会形成闭环
| 材料 | 编写者 | 核对依据 |
|---|---|---|
| 迁移代码 | 实现模型 | 测试 |
| 数据存储的测试替身 | 实现模型 | 同一组测试 |
| 测试 | 实现模型 | 测试替身 |
| 审查结论 | 审查模型 | 代码、测试、测试替身 |
每次检查的依据,都是模型写出的另一份材料。审查模型很善于发现内部矛盾,找出了竞态、缺失的防护和损坏的回滚逻辑,后面第三篇会详述。但这些材料里没有说明 Upstash Lua 与 Redis 的行为差异,它也就无从核对。
智能体审查提高了这套材料的内部一致性;判断代码能否在真实系统里正确运行,还需要真实系统的证据。两部分都要投入。
这种差距对智能体尤其明显。工程师踩过某个平台的坑,通常会把经验带进下一次审查。模型每次审查都可能重新开始,除非我们把那次教训写进测试替身或检查清单。
后来补上的三个步骤
- 实现之前先实测。 我们在真实 Upstash 数据库上运行了几个只做计算的探测脚本,约 20 分钟就测出了拼接元素上限、脚本时间预算和 null 的行为。
- 把测量结果写进测试替身。 现在的替身会丢弃 null 字段,拒绝超过 2,561 个拼接元素,并为脚本操作设定预算。用旧代码运行时,这三个缺陷都会触发单元测试失败。替身描述的是测到的平台限制。
- 在真实后端演练,并限制写入范围。 演练程序连接生产 Upstash,在独立的临时键前缀内运行实际写入路径:开始 → 分批写入 → 封存 → 严格读取 → 创建映射。演练按真实规模运行:2,000 个对象、200 个身份记录。后来正式执行约 2,000 条记录的封存时,按每次调用 64 条分页,总耗时为 2.5 秒。
临时命名空间的防护代码
演练程序需要写入凭据,因此必须让每条命令都经过范围检查:
PREFIX = "m4ctest:"
def guarded(cmd):
verb = cmd[0]
if verb == "EVAL":
keys = cmd[3:3 + cmd[2]] # only declared KEYS
elif verb == "SCAN":
assert cmd[2] == "MATCH" and cmd[3].startswith(PREFIX)
keys = []
elif verb in {"GET", "SET", "DEL", "HGET", "HSCAN", "SMEMBERS", "INCR", "TYPE"}:
keys = cmd[1:] if verb == "DEL" else cmd[1:2]
else:
raise AssertionError(f"unreviewed command {verb}")
assert all(str(k).startswith(PREFIX) for k in keys), (verb, keys[:2])
return ORIGINAL_POST(cmd)
如果这个前缀下已经存在键,演练程序拒绝启动;结束时,它在 finally 块中清理自己创建的全部内容。
首次生产迁移之前的实测清单
- [ ] 逐步增大输入,例如 100 / 250 / 500 条记录,测出脚本或函数的 CPU 时间、墙钟时间限制。
- [ ] 逐步增大规模,找到栈、拼接元素或参数数量开始失败的精确位置。
- [ ] 对路径上的每个序列化器,检查 null、空值、缺失字段经过解码再编码后的结果。
- [ ] 检查请求正文和响应正文的大小限制。
- [ ] 将验证与演练计划的读取量,与每月请求额度比较。我们的额度后来确实成了约束,见第三篇。
- [ ] 把每项实测结果写进替身,补上能让未处理这些限制的代码失败的回归测试。
我接受的代价
在生产 Upstash 上演练,意味着拿着写入凭据,在生产数据旁边运行程序。我接受了这个风险,前提是有前缀防护、启动前的空前缀检查和结束后的清理。此前的做法则让我在专门留出的生产迁移时段,逐个发现这些限制。
下一篇:可以交给智能体执行的发布器。总共启动了 13 次,每次停止分别是谁造成的?