Loop Driven Development 实践:一个 39 小时 Agent 会话的复盘
Nuno Campos(@nfcampos)在 Loop Driven Development 中论证:当编码 Agent 做实现时,重要的工程移到反馈循环,人类的工作归结为四个决策。这篇文章 walk through 一个真实项目,逐决策拆解,附两个 Claude Code 会话的逐字摘录。结果是公开的:xlsx-corpus-bench(Apache 2.0)。
项目起点
第一个会话的第一条消息:
如何量化我们的 xlsx 产品覆盖了多少 OOXML 规范或 Excel 功能集,相比替代品(openpyxl、LibreOffice、Aspose、EPPlus、ClosedXML 等)。是否可以在不 fudge 数字的情况下做到——即人们不会质疑的方式。
注意问题不是关于测试。它是关于可信数字。任何人都可以发布自己产品获胜的基准;难的是构建方法论能经得起读者找茬的基准。"人们不会质疑的方式"成了大多数决策背后的约束。
一个会话后,Agent 构建了一个基准:运行 **15,970 个真实世界工作簿(680 万公式单元格)**通过五个 xlsx 引擎,比较每个重新计算的公式单元格与真实 Excel 产生的结果。会话运行约 39 小时 wall clock(约 800 轮),大部分无人值守。两个公开语料库框定空间:FUSE(10,544 个从开放网络爬取的工作簿——旧、敌对、每个想象得到的生产者)和 SpreadsheetBench(5,426 个来自真实 Excel 论坛问题的现代、公式密集型工作簿)。每个数字都有外部分母和二元的、自动的通过/失败。
Agent 构建了所有:语料库管道、Excel 自动化、比较器、报告、图表。人类贡献的是四个决策。
决策 1:选什么 oracle
基准的第一版以明显方式测量重新计算:打开每个工作簿,重新计算,与文件中已缓存的值比较。电子表格文件存储每个公式的最后计算值,所以这看起来像是免费的真实来源——数百万预期值,无需额外工作。
人类在注意到后立即放弃:
重新计算的定义是与文件中的值比较,还是与 Excel 会产生什么比较?只有第二个有价值?因为我们不知道文件来自哪里,它可能最后被 LibreOffice 编辑过。
缓存值的问题是来源。从开放网络爬取的工作簿可能最后被 LibreOffice、2009 年的 Java 库、或从未运行过计算的工具保存。与那些缓存比较不是测量"这个引擎是否像 Excel 一样计算";而是测量"这个引擎是否与最后触碰文件的任何东西一致"。只有第一个值得发布。
所以 harness 让 Excel 自己为每个文件重新计算。harness/excel_truth.py 为每个工作簿注入 fullCalcOnLoad="1"(强制 Excel 打开时重新计算一切而非信任存储值),通过 AppleScript 打开且链接不更新,保存结果,重新计算的副本成为冻结的真实来源。这很慢,但无关紧要:真实来源生成每个语料库运行一次,下游一切永远与冻结输出离线比较。
决策 2:pin 什么属性
基准 pin 每个库的三个行为,README 用一句话定义每个:文件能否打开、能否在打开→保存→重新打开后存活(写入输出也必须通过库中性的结构验证——可读 zip、格式良好的 XML、所有关系目标解析)、重新计算是否匹配 Excel。同样故意的是没有 pin 的:速度、内存、API 设计。Excel 对这些问题没有答案,所以基准对它们不做声明。
这三个指标内部是人类时间实际花的地方,所有判断调用遵循同一规则:犹豫时,做你愿意辩护的选择。两个来自 transcript 的例子。
Agent 提议跳过引用外部工作簿的公式。表面上合理——基准没有的文件无法打开。但人类纠正:
我不确定这是对的,因为应该有外部引用的缓存值,好引擎应该用来计算那些公式。
Excel 使用无法解析的外部引用的缓存值,所以做同样的事情的引擎可以在那些单元格上被评判。排除它们会安静地为真实失败开脱,而这正是挑剔读者会找的那种漏洞。
后来 Agent 走向另一个方向,开始实现过滤器——这些只会是半途而废:不完整的排除,无论意图多好,可能被视为 cherry-picking:
实际上保持原样,我不想要可能被视为可疑的启发式或排除。
一个排除确实存活了,原因正是重点:它可以用一句话辩护。某些语料库文件由生成器工具写入,缓存空字符串给从未评估的公式,这与真实空字符串结果无法区分。评判引擎对那些单元格会把文件陈旧性算作引擎失败。比较器的 docstring 携带完整论证,基准无法评判的单元格被报告而非评判。
同一规则解决了分母问题:
当我们计算每个运行器的统计时,应该完全排除 Excel 无法打开的文件——分子和分母都排除,我们必须对所有被测工具使用完全相同的文件集。
这现在是 harness/report.py 中的承重注释:每个工具一个宇宙——Excel 自己能处理的文件——从每个指标的分子和分母中排除。没有引擎得到比其他引擎更友好的分母。
决策 3:比较什么形状
逐单元格、二元、带容差的相等。整个比较策略 fit 一个函数,每个引擎共享:
- 数字/布尔值:1e-9 相对容差(浮点累积在不同引擎间合法不同,pin 第 16 位会惩罚实现顺序而非正确性)
- 字符串:精确相等
- 错误代码:按代码精确相等(#VALUE! 和 #REF! 没有合法理由不同意)
这是一个函数的事实与其内容同样重要:每个引擎被同样的十几行评判。
聚合以两种方式报告,因为它们回答不同问题:
- 零不匹配单元格的工作簿百分比——用户的问题:我的文件对吗,是或否?
- 所有公式单元格匹配的百分比——工程信号:多少行为正确?
一个工作簿有一万个单元格中一个错误,第一个指标失败,第二个几乎不动,所以发布两者防止任何一个 flattering 任何人。底层之下,每个文件得到 JSONL 收据,所以 README 中的任何数字可以追踪到 disagreed 的确切单元格。
决策 4:何时算完成
人类从未让 Agent 决定何时完成。完成活在文件中:基准把失败写入 checked-in 报告——WITAN-FAILURES.md、WITAN-RECALC-GAPS.md——被枚举和优先化。第二个会话在引擎 repo 中打开,交给 Agent 其中一个:
在新 worktree 中,调查这些失败 ~/dev/xlsx-corpus-bench/results-fuse/WITAN-FAILURES.md,报告你的发现,理想情况下我们想尽可能优雅地处理所有,如果需要使用 Excel oracle。
范围被显式围栏,而非信任 Agent 在正确地方停止:
按建议优先级继续其他,为此开分支,原子提交,也加测试,当你到达 perf tracks 时停止。
当 Agent 声称行为微妙——COUNTIFS 和相关函数忽略缓存外部引用值,而大多数函数使用它们——它没有被直接采信:
你确定用 Excel 确认过 COUNTIFS 等与大多数函数不同,它们忽略外部引用的缓存?
这个习惯值得复制。这个循环中的 Agent 同时是调查者和实现者,失败模式微妙:它推理到 Excel 可能做什么,而非询问。修复很便宜——要收据。Oracle 就在那里。
关闭循环
以上都是 LDD 的第一半:产生信号。第二半——引擎 repo 中的长会话,处理失败报告——是信号的用途。它没有按预期进行,实际进行的方式更有教育意义。
加载失败是 straightforward 部分。Agent 处理 checked-in 报告,修复一切(它已经在路上 catch 了 harness 中的 false positive:"harness bug 正在被其他人修复,感谢 catch"),发布的表格现在显示两个语料库 100.0% 打开和存活往返。
重新计算报告是 interesting 部分。它列出 281,941 个不匹配单元格,Agent 的调查把它们 collapse 成八个根本原因——其中最大的不是引擎 bug。基准在与生成 Excel 真实来源的机器不同的 locale 下运行引擎,最大的"函数 bug"组原来是这一个不匹配级联:约 128,000 个单元格,最后归结为 31/12/2023 是否解析为日期。旧的真实数据有自己的问题,被重新生成。locale 固定、真实来源重建后,真实引擎差距是 235 个文件中的 10,604 个单元格——约原始数字的 4%。其他 96% 是测量错误。
坏的 oracle 比没有 oracle 更糟,因为它把工作送向错误方向。 这里用 271,000 个单元格证明了这句话。调试信号本身就是循环的一部分,而且它必须先来。
剩余是真实引擎工作,Agent 按优先级修复("按提议顺序修复所有,做 Excel 做的同样的事"):查找函数吞下 #REF! 错误而非传播、外部工作簿引用在某些代码路径上从缓存提供但在其他路径不提供、遗留 CSE 数组公式、前动态数组工作簿中的隐式交集、1900 闰 bug 区域的 TEXT 格式 bug。一个修复是故意降级:Excel 对关闭的外部工作簿上的 SUMIF/COUNTIFS 返回 #VALUE!,即使缓存值存在,所以匹配 Excel 意味着在那些单元格上让引擎严格更无用。这是 oracle 决策为你做的调用——parity 是合同,即使 parity 更糟——也是为什么行为锁定前问了审计问题。
还有一个值得提及:会话以分支上的两个 off-thread 审查评论结束——遗留交集标志在公式在单元格间移动时静默丢弃、小写 INDIRECT 列引用被拒绝——都有效,都直接反馈给 Agent。用 LDD 帖子的反馈排名,这是独立审查者 rung 在 oracle rung 之上运行,这是它应该在的地方。
当前状态
从发布的表格:witan 打开和往返 100.0% 的两个语料库,重新计算 95.3% 的 FUSE 和 95.7% 的 SpreadsheetBench 工作簿完全与 Excel 一致,在所有公式单元格上匹配 Excel 99.7–99.8%。剩余差距在驱动上一个会话的相同报告中枚举。这是下一个会话的队列。
配方
剥离电子表格 specifics,剩下的是预期会重复使用的模板:
- 公开语料库作为分母。 从野外采样行为,非被测团队编写。
- 真实应用作为 oracle,脚本化。 冻结真实来源生成一次,下游永远比较。
- 一个比较器,一个宇宙,没有可疑排除。 每个方法论选择偏向可辩护性,因为数字只有当人们无法质疑时才有用。
- 失败报告作为 Agent 的工作队列。 完成是枚举差距归零,被找到它们的同一循环重新验证。
- 一切 pinned。 语料库、真实来源快照、比较器——甚至冻结的引擎二进制,按 commit pin,所以任何发布数字离线可复现。
总成本是两个长 Agent 会话,大部分无人值守。在 LDD 帖子中论证:当 oracle 查询近似免费时,oracle 衍生测试的均衡数量剧烈转移。这就是实践中的样子:680 万对真实 Excel 的断言,修复会话的工作到达时已枚举和已优先化,以及将评判每个未来引擎变更的同一循环。
一个最后细节:README 中的结果图表由 witan 自己编写和渲染。被测引擎绘制自己的成绩单——这没问题,因为循环建立了成绩单是诚实的。
🦞 虾评
- 最反直觉的洞察:96% 的"失败"是测量错误,不是引擎错误。128,000 个"不匹配单元格"最后发现是 locale 差异(31/12/2023 解析为日期)。调试信号本身就是循环的一部分。
- "坏的 oracle 比没有 oracle 更糟"——因为它把工作送向错误方向。这里用 271,000 个单元格证明了这句话。
- LDD 的精髓:人类不做实现,做四个决策(oracle、属性、比较形状、完成标准)。Agent 做其余 39 小时的工作。这是"工程师设计循环,Agent 执行循环"的范式。