有些设计决策,不是在白板上想清楚的,是被系统跑崩了才搞清楚的。
🚀 快速导读(给不想细看代码的管理者/产品/决策者)
这篇文章讲了什么:我在做一个让 AI 自动写代码、自动测试、自动修 bug 的系统(ASR)。Loop Engineering 这套方法论听起来很对,但真的落地到工程里,三个月时间我经历了几十次设计推翻重来。这篇文章挑了 6 个典型,说明方向正确 ≠ 实现简单。
你可能关心什么:
如果你只有 2 分钟,看第 4、5 两个转变就够了。
不谈「我做得早」,谈「我改了多少次」
上篇文章说了一件巧合:我做的 ASR 和 Loop Engineering 的方法论高度吻合。
但那篇文章说的是「结果态」。
现在说说「过程态」——ASR 从第一版到现在,有几十次关键设计调整,其中每一次都是因为系统告诉我「这个方向走不通」。
这里挑出 6 个最有代表性的——不是全部,但每个都代表了某一类必须撞了墙才能发现的工程问题。
Loop Engineering 是框架,框架不会告诉你实现路上有哪些坑。
第一个转变:Spec 从「必须」变成「可选」
最初的设计
Supervise-Agent(ASR 的前身)最初的设计里,Spec 是整个系统的核心输入:
Spec(需求) → Builder → Tester → Analyzer → 收敛这个设计逻辑上没有问题——你想让 AI 做什么,你得先说清楚,最好是结构化的 YAML。
文档里甚至专门有一节叫「为什么必须结构化 Spec」:
「无结构需求无法验证——这是 AI Coding 最大问题之一。」
所以早期版本里,--spec spec.yaml 是必选参数,没有 spec 就跑不起来。
为什么改掉了
实际使用中,写一份合格的 spec.yaml 要花不少时间。
更关键的是:大多数开发者已经有了一份设计文档(DESIGN.md),他们不想再把它翻译成 YAML。
如果为了跑 ASR 必须先花两小时写 spec,这个工具的门槛就太高了。
怎么改的
引入了一个简单的优先级规则:先找 spec.yaml,找不到就自动读取项目里的 DESIGN.md。--spec 变成了可选参数,CLI 帮助文档从 --spec spec.yaml(必选) 改成了 --spec /path/to/spec.yaml(可选,省略则读取 DESIGN.md)。
这个改动代码量很小——但它改变了 ASR 的「入场门槛」。用户只需要一份 Markdown 设计文档,就可以启动收敛循环。
这是 Loop Engineering 里「降低人工介入成本」的一次具体实践。
第二个转变:Tester 从「Agent 生成测试」到「纯 pytest」再回到「Agent + pytest」
第一阶段:Agent 生成测试
最初的 Tester 设计是:用 OpenCode 读取 DESIGN.md,自动生成测试文件,然后跑 pytest。
这个设计的出发点是:「Builder 写了什么代码,Tester 应该基于设计意图来写测试,而不是基于已有代码。」
听起来很理想——独立的 Tester,不受 Builder 生成代码的影响,从设计意图出发验证。
但实际遇到了什么
Agent 生成的测试质量非常不稳定:
更严重的问题:Tester 每次跑都重新生成测试,上一轮的测试文件被覆盖。Builder 刚修好一个测试,Tester 下轮生成了新的测试,又全部归零。
收敛 streak 永远无法累积。
第二阶段:纯 pytest(跑已有测试)
尝试过一个极简方案:Tester 不再生成任何测试,只负责跑 tests/ 目录里已有的测试文件。
这解决了测试不稳定的问题,但带来了新问题:
Builder 新增了功能,没有对应的测试。循环跑了 10 轮,测试通过率一直是 100%——因为测试根本没有覆盖新代码。
系统以为自己收敛了,实际上是测试在骗它。
第三阶段:回到 Agent + 完成标记机制
最终的方案是回到 Agent,但加了一个关键机制:完成标记文件。
Tester 写完测试后,必须创建一个名为 TESTER_COMPLETE.md 的文件来明确表达「我做完了」。如果这个文件不存在,系统认为测试生成不完整,直接返回错误。
这是一个「确定性信号」:Agent 的任务完成情况不再靠推断,而是用一个文件来明确表达。
这是 Loop Engineering「确定性 Feedback」原则的一次实践——不靠 Agent 记得报告,用外部状态来确认。
第三个转变:Tester 沙箱从「每轮重建」到「增量同步」
这个问题没有任何先验原因让你怀疑它——直到你分析日志发现循环在原地打转。
原来的逻辑
每轮测试前,Tester 先清空旧的沙箱文件夹,从项目目录完整复制一份新的。出发点是好的:每轮用干净的环境跑测试,避免上一轮的修改污染结果。
实际发生了什么
Builder 在第 1 轮修复了 ImportError。修复在沙箱里,测试通过。
第 2 轮:Tester 删掉沙箱,从项目目录新建。项目目录里的 ImportError 从来没被修复过(Builder 只改了沙箱)。
ImportError 重新出现。Builder 又去修。第 3 轮又删。
这是修复状态的「传递链断裂」:Builder 改的不是项目目录,是沙箱。沙箱删了,修复就消失了。
修复:增量同步,双向回写
改为不删重建,只同步有变化的文件。同时,沙箱内的所有修改——不只是 tests/ 目录,包括源码——全部回写到项目目录。
早期版本里只回写了 tests/ 目录,Builder 改的源码无法同步回来,等于白修。
第四个转变:三个 Agent 的「记忆权」不对等,而且这是故意的
最初的设计
ASR 最开始就是三个 Agent:Builder、Tester、Analyzer。
但从第一行代码起,它们就不是「平等的」——Builder 有记忆,Tester 和 Analyzer 没有。
具体来说:Builder 调用 OpenCode 时传递 --continue --session <id>,保持会话延续,上一轮做了什么改了什么,下一轮都记得。Tester 和 Analyzer 每次调用都是全新会话,前一轮发生了什么它们完全不知道。
为什么这么设计
这不是疏忽,是故意的架构选择:
Agent角色为什么需要/不需要记忆Builder执行者需要记住之前修了什么、改了哪些文件、为什么改。如果每轮失忆,会反复犯同样的错Tester裁判不应该记住上一轮的测试结果。如果记住了,可能会对某些文件「放水」Analyzer审计不应该记住上一轮的分析结论。如果记住了,可能会对同一个问题反复盯着看,忽略新出现的问题
核心思想:执行者需要累积上下文才能改进,但裁判和审计应该每轮「从零开始看代码」,避免历史判断的惯性。
这个设计实际导致了什么
第一条线:Builder 有记忆 → 好处和代价
好处很明显:Builder 在多轮迭代中能累积对代码库的理解。第 1 轮修 ImportError,第 5 轮修 LogicError,不需要每轮重新理解整个项目。
但代价也出现了:当 Builder 的会话上下文累积到一定程度,AI 的「记忆窗口」接近上限,Builder 开始输出空响应——看起来在工作,实际上什么都没做。我们内部叫它「假死」。
代码里后来加了一个检测机制:如果 Builder 连续两次输出完全为空(即 prompt 和 completion 的 token 数都是 0),自动重置 Builder 的会话,相当于在记忆太长时主动让他失忆——有记忆但不超过临界点。
第二条线:Tester/Analyzer 无记忆 → 好处和代价
好处是保证了独立性——Analyzer 不会因为上一轮说「ALL CLEAR」就放弃本轮发现新问题。
但代价出现在 GLM-4.7-FP8 这种弱模型上:
每次全新会话,OpenCode 从零开始,需要:
GLM-4.7-FP8 在全新会话里经常只输出一个意图声明就结束了:
「我来分析一下这个项目的代码...」
然后什么都没输出。5-29 日志记录的实测数据:10 轮中只有 3 轮 Analyzer 产出了有意义的分析,其余 7 轮只有意图声明。
本质矛盾:无记忆保证了独立性,但在弱模型上,没有上下文预热会降低输出质量。
后来改了什么
没有改成「所有 Agent 都有记忆」,而是在无记忆的前提下加了结构化约束:
完成标记文件机制(Tester/Analyzer 都用了):Agent 必须写一个标志文件(TESTER_COMPLETE.md / ANALYSIS_REPORT.md)来明确表达「我做完了」。没写出来 = 没做完。不靠 Agent 自己说「完成了」。
Prompt 强制输出格式:在 prompt 里明确规定:
「不要在你的对话文本中输出分析结论。只写入 ANALYSIS_REPORT.md。」
这迫使 Agent 在没有上下文记忆的情况下,也必须把结果落到文件里,而不是依赖对话历史的延续。
Builder 的 session 重置策略:Builder 的「假死检测」+ 自动重置,相当于在记忆太长时主动失忆——有记忆但不超过临界点。
一个反直觉的设计选择
大多数系统会对称地对待所有 Agent——要么都有记忆,要么都没有。
ASR 选择了不对称:执行者有记忆、裁判和审计没有记忆。
这个选择来自于一个问题:「你希望你的代码审查者记得上轮审查的结果吗?」
答案通常是「不希望」——因为他应该每轮都重新看代码,而不是基于上轮的印象做判断。
这是 Loop Engineering 里一个隐蔽的工程决策:Cadence 不只是「跑多快」,还包含「每个角色在循环里带着什么状态进入新一轮」。
第五个转变:发现了一个藏了三个月的「阉割变量」
这是整个迭代过程中最戏剧性的一次发现。但要讲清楚为什么它重要,得先说一个工程路线的根本选择。
先交代一个关键背景:我为什么要站在 OMO 的肩膀上
写 ASR 之前,我的约束是这样的:
在这个约束下,如果我让 ASR 的三个 Agent(Builder/Tester/Analyzer)直接调用裸的 GLM-4.7-FP8 去干活,结果一定很差——弱的模型单独干活就是弱的。
所以我做了一件事:让 ASR 的每个 Agent 内部调用 OpenCode + oh-my-openagent(OMO)。
OMO 本身是一套多智能体编排框架,它已经帮你把单个代码生成任务拆成了:
IntentGate → Prometheus(规划) → Metis(架构设计)→ Momus(代码生成) → Atlas(审查) → Sisyphus + Ralph(修复循环)换句话说,OMO 自己就是一个微小的 Loop。
而 ASR 做的事情是:在 OMO 这个「微循环」外面,再套一层更粗粒度的收敛循环(Builder → Tester → Analyzer)。
这样做的逻辑是:
GLM-4.7-FP8 单个 Agent 干活 → 输出质量不够OMO 多 Agent 编排 → 把弱模型的质量拉上来(但只拉一次)ASR 收敛循环 → 多次迭代,把 OMO 的输出再往上拉ASR 不是在替代 OpenCode 或 OMO,是在它们的基础上搭了第二层。
这个背景非常重要,因为它解释了为什么「CI=true 的事」不仅仅是「找到了一个 bug」——它意味着我用了一个被阉割的基础层,搭了三层楼,然后奇怪为什么三楼不稳。
「CI=true」对我的真正影响:不止是被阉割,是反复的自我怀疑
CI=true 把 OMO 的多智能体编排完全关闭。
OpenCode 退化成一个没有 Planning(规划)、没有 Review(审查)、没有 Ralph(修复循环)的单步执行器。
但这不是最伤的。
最伤的是:我不知道。
我不知道 Builder 调的不是完整的 OMO,我只看到结果不好。然后我开始怀疑:
每次怀疑完,重新审视一遍设计逻辑,又觉得理论上应该是对的——于是继续推。
然后推不动了,再怀疑一遍。循环往复。
那三个月我在做什么:改提示词,而不是改 CI
因为不知道根因在 CI,我以为问题出在「提示词不够好」。
那段时间 Builder 的提示词改了不知道多少轮——调整反馈注入的格式、调 Test Summary 的量、调整收敛条件——每一次改完之后跑一轮,效果有好有坏,但永远不稳定。
最难受的是测试反馈周期太长。
DESIGN.md 是一个长任务,不是 5 分钟能跑完的。一次完整的 iter=10 要跑很久。很多时候改完一个参数,等一轮跑完,发现没用——但又不知道是参数没用,还是 CI 在背后抵消了效果。
有时候只跑 1-2 个 iter 就出问题,但问题出的位置每次不一样,无法归因。
这就是在错误的方向上做精细优化——所有 prompt 调优都在对一个被阉割的引擎生效,调三个月只在原地画圈。
后来怎么发现的
asr_10 跑完,480/530 测试通过(90.6%)。
但同样的任务,用 OpenCode CLI 手动跑,代码质量明显更好。
这不合理——ASR 在 OMO 基础上多了收敛循环,理论上应该更好。
于是回到代码比对 ASR 调 OpenCode 和 CLI 调 OpenCode 的区别,最终发现了它:
env["CI"] = "true"删掉之后
asr_10+ 结果:
一行环境变量,让我在错误的方向上调了三个月的提示词和参数,反复怀疑自己的设计,反复说服自己继续推。
删掉之后,一切阻塞瞬间消失。
Loop Engineering 说「节奏(Cadence)是循环什么时候跑、跑什么」——前提是你要确保跑的东西没有被静默阉割。
还有一个更深的教训:做工程的人,搞清楚你的「肩膀」长什么样,和设计你的「增量」一样重要。
第六个转变:收敛条件从「一次通过」到「连续 3 轮 + 双层裁决」
最初的收敛条件
# 早期版本if test_passed and spec_aligned: return CONVERGED简洁,但有致命问题:
从实测数据学到的教训
asr_20 实测:测试全部通过(289/289),Analyzer 说 spec_aligned,系统触发收敛。
但仔细检查代码发现:这只是一个「局部最优」。Analyzer 在那一轮没有发现问题,但下一轮换个视角又找到了问题。
最终的收敛条件
现在改成三层条件,缺一不可:
为什么是 3 轮而不是 1 轮?
因为 AI 的判断本身有随机性。Analyzer 这一轮说「没问题」,可能只是当轮视角的局限。连续 3 轮都认为没问题,才有统计意义上的可信度。
不是六次,是几十次
上面挑了 6 个最有代表性的转变来说。
实际上整个迭代过程中,类似的关键设计调整有几十次——有些是方向层面的(像 CI=true),有些是参数层面的(像 timeout=120→600),有些是策略层面的(像收敛 streak 从 1 改成 3)。
但规律是一样的:都是被跑崩了才改的。
把这六个典型放在一起看:
转变原始设计实际问题新方向Spec必须 YAML门槛太高可选,fallback 到 DESIGN.mdTesterAgent 生成测试生成不稳定,测试互相覆盖Agent + 完成标记文件沙箱每轮重建修复状态无法传递增量同步 + 双向回写记忆权Builder 有记忆,Tester/Analyzer 无假死 + 弱模型输出不稳非对称记忆 + 标记文件约束OpenCode默认 CI=true → 关闭了 OMO 编排ASR 在残废的肩膀上搭了三层删除 CI=true,恢复 OMO + 收敛循环双层结构收敛条件单次通过即收敛假收敛、Analyzer 随机性连续 3 轮双层裁决
这些问题没有一个是理论上能预见的——它们来自真实运行数据的分析。
Loop Engineering 给了正确的方向:设计循环、设计闸门、设计反馈。
但方向正确不等于实现简单。每一个环节都有具体的工程陷阱,每一个陷阱都要用真实的崩溃来发现。
为什么要开源这些「历史遗留问题」
ASR 的代码库里,这些转变都留下了痕迹——注释、兼容逻辑、版本说明。
这些不是屎山,是「这里曾经有人踩过坑」的路标。
如果你正在做类似的事——构建一个能自主运行的 AI 软件工程循环——你迟早会遇到其中的某几个问题。
现在你知道前面有人走过了。
给管理者 / 决策者的几句话
如果你不是每天都在写代码,但从这篇文章里读到了一些东西,下面是你可以带走的:
1. Loop Engineering 不等于「买个 AI 工具跑循环」
这六个转变说明一件事:方向对和落地对之间,隔着大量的工程细节。不是买个工具配个 pipeline 就能搞定的。沙箱不小心删错了、AI 偷偷被你阉割了、收敛条件设得太松——每个坑都能让你在错误的方向上耗几个月。
2. 团队的工程文化比工具选择重要
CI=true 这个问题,如果团队里没有「为什么 ASR 比手动还差?」这个追问,它可能永远不被发现。推动 Loop Engineering 落地,不是找个架构师画个图就完了——你需要一个愿意追问「为什么不对」的团队。
3. 弱模型 + 好结构 > 强模型 + 烂结构
ASR 从头到尾只用 GLM-4.7-FP8,没有 Claude,没有 GPT-4。但最终能稳定产出 96.5% 测试通过率的代码。这说明对大多数工程场景,把有限的 AI 能力用好,比等一个更强的 AI 更实际。OMO 的多智能体编排 + ASR 的收敛循环,就是用结构弥补能力的案例。
4. 警惕那些「看起来在工作」的系统
Builder 假死、Analyzer 只输出意图声明、CI=true 静默阉割——这些问题的共同点是:系统没有报错,它只是在低效地运转。如果你在引入 AI 自动化流程,确保你的监控不是在测「系统有没有崩」,而是在测「系统有没有真的产出质量」。
如果你在做 Loop Engineering 相关的项目,欢迎聊聊你遇到的设计转变。