作者提出「假设账本」法:用 AI 解释代码而非生成代码,错误成本极低(编译器/调试器即可纠正),安全修改了 6 万行无人敢动的遗留 C++。
三个月前,一位同事离职了,我接手了他的项目:大约 6 万行 C++17 代码,没有设计文档,注释大多写的是 // TODO: clean this up,构建系统只有他一个人懂。老板的要求很简单:"到本季度末,能够安全地进行修改。"
我没有一个季度的时间逐行阅读。我也不信任任何 LLM,放心让它改代码。所以我把它用在了唯一一种错误答案代价很低的工作上:阅读。这篇文章要讲的就是这个工作流程——我现在称之为"假设台账"——以及它失效的那些地方。
所有关于 AI 生成代码的恐怖故事都有一个共同模式:看起来合理的输出,未经审核就合并,后期爆炸。阅读则把经济学逻辑反转了。当模型给我解释一个函数但说错了,爆炸半径只是我自己的理解错误——而编译器、调试器,或者十分钟的 grep,就能在它让任何人付出代价之前纠正过来。
这种不对称就是整个策略的核心:把模型用在它的错误可被检测且代价低廉的地方,让它远离任何未经审核就进入代码库的东西。这不是一个采用 AI 编写代码的故事。这是一个把 AI 当作一个非常快、但经常出错的城市导游的故事。
核心规则:模型所说的关于代码库的一切都不算知识。在被无法被糊弄的工具验证之前,这些都只是假设。我在探索代码时保持一个纯 Markdown 文件打开:
| # | Hypothesis (from model) | Source | Verification method | Status |
|---|------------------------|--------|--------------------|--------|
| 1 | `RingBuffer::push` overwrites oldest entry when full | LLM reading buffer.cpp | Write probe main.cpp, push 6 items into cap-5 buffer | CONFIRMED |
| 2 | `OrderBook` is only mutated from the IO thread | LLM summarizing call graph | `grep -rn "order_book\." src/ | grep -v io_thread` + tsan build | REJECTED — also mutated in metrics.cpp:214 |
| 3 | Retry backoff is exponential, base 100ms | LLM reading retry.h | Read source myself | PARTIAL — capped at 2s, model missed the clamp |
三列做的是真正重要的工作。验证方法一栏迫使我在相信任何东西之前先决定如何去检查。状态栏保留了一个永久记录——记录模型错了多少——这校准了下一次对话我该给它多少发挥空间。三周后,我的台账显示大约每四个实质性主张中就有一个是错误或不完整的——有用,但远谈不上可信。恰好是导航的正确工具;恰好不是权威的正确工具。
模糊的问题会得到模糊、自信的无稽之谈。有效的是一套固定协议:
粘贴窄切片,不要粘贴整个文件。一个类、一个函数、一个头文件。大段粘贴会明显降低回答质量,而且我没兴趣去试探上下文窗口的极限。
问结构,不要问观点。"列出这个构造函数修改的每个字段,以及每个字段后来在哪里被读取"比"解释这个类"效果好。结构性主张成本低,用 grep 验证即可。
问调用图,然后检查它。"什么调用了 flush(),它又调用了什么?"然后用 ctags -R . 和 grep 验证,或者用 -Wunused 编译,看看链接器实际保留了哪些。
要求模型引用行号。有行号的主张是可证伪的主张。没有行号的主张是睡前故事。
对于验证,我依赖的是比这一切都古老的工具:gdb 观察点来测试"这个字段只在关闭期间改变",一个链接可疑对象文件的临时 main.cpp 来测试行为主张,以及任何涉及线程的东西都用 ThreadSanitizer 构建。模型在总结并发代码时从未一次提到过 TSan。工具也不在乎那个总结。
这个工作流程很啰嗦。仅仅一个下午的代码探索就轻易达到 40–60 次交换:粘贴一个切片,得到一个结构性主张,验证,follow up。如果是一个按量计费的 API,我会精打细算地提问,工作质量会更差——每花一块钱,接受第一个看似合理答案的诱惑就增加一分。
我通过 MonkeyCode 运行这些对话,它目前提供免费模型访问和免费服务器选项,所以再问一个澄清问题的成本是零。披露:本文是作为 MonkeyCode 产品推广的一部分准备的。两个诚实的警告:我对其提供哪些模型、其限制或免费套餐能持续多久不作任何保证——在你配置当天去核实,而不是从本文获取。而且台账方法是有意设计为与提供商无关的;它就是一个 Markdown 文件和一个习惯,所以换后端不会改变工作流程的任何部分。
保持台账意味着我也积累了一份它如何失效的目录,事实证明这是本季度最有价值的产物:
自信的时间性主张。"这个缓存是在启动时填充的"——实际上是懒加载,在第一次请求时才填充。模型从代码布局推断顺序,而不是从执行。
遗漏第二个写入者。反复找到一个修改点就断言是独占的。grep 三次都不同意。
把丑陋的部分抹平。当代码做了奇怪的事情时(一个有意的内存泄漏,解释为什么的注释在另一个文件里),模型描述的是代码应该做什么的样子。遗留代码的奇怪部分通常是"承重"的,而这些恰恰是它 normalization 掉的部分。
它头脑中的版本漂移。它描述的是项目没有使用的标准里的 std::shared_mutex 语义。根据构建标志验证语言层面的主张,而不是模型的记忆。
注意这份列表里缺少什么:语法错误、破坏的代码、任何编译器能捕捉到的东西。阅读侧的失败是语义层面和社会层面的——讲得很流利但错误的错误故事。
台账是纪律,而纪律不会永远规模化。到第六周,我花了真实的时间维护它,而在五人的团队中,每人一份的台账需要变成一份带约定的共享文档。它还假设你可以验证——这之所以有效是因为项目可以在本地构建和运行。对于你无法执行的代码(硬件相关的固件、私有依赖项),验证这一栏会变薄,整个方法也会弱化。最后,这些都不能转移到写作上:一旦模型的输出进入代码库,你就回到了这篇文章明确避免的风险模式,你需要我在这里有意没有讨论的审查门控。
如果代码库小到可以一个周末读完,那就用一个周末读完——台账是开销。如果你已经有原来的作者可以请教,请他们吃顿饭就行;和一个人类谈三十分钟胜过一周的假设验证。而且如果你的雇主政策禁止将专有代码粘贴到外部服务,那个约束在工作流程开始之前就解决了问题。
到本季度末,我可以安全地修改这个项目了,模型大概值得三分之一的功劳——剩下的归功于 gdb、TSan,以及那个拒绝相信它的台账。这个比例感觉是 AI 辅助遗留工作的诚实版本:模型加速了提问,而其他你本来就拥有的工具完成了真正的认知。如果你现在正面对一个继承来的代码库,在粘贴任何一行代码之前先建一个 Markdown 表格——那栏你记录它有多错的记录,比它的任何答案都教得更快。