500B Tokens Later:用 AI 智能体反编译第一人称射击游戏
500B Tokens Later: Letting AI Agents Decompile a First-Person Shooter
作者用约3个月时间、估计600-700亿token,让AI智能体把一款第一人称射击游戏反编译为可编译的C++源码,最终99%的函数已重建、83%字节级精确匹配。
在过去的3个月里,我花费了一些时间和token来反编译一款流行的第一人称射击游戏。 目标并不是达到简单的概念验证状态。 相反,我们真正想要的是对游戏进行准确、稳定且功能完整的复刻。
我博客的忠实读者可能已经注意到,我之前写过两篇文章,但后来被删除了。 其他人现在可能想知道我在谈论哪款游戏。 对你们两位,我只能说,美国企业界来毁了我们的乐趣。
不过,这没关系。这篇文章不是关于那款游戏的,也不太涉及反编译的过程。 它更多是关于AI编排,以及如何优化基础设施、设置和工具链以获得最佳结果。
这个项目是在 RektInator、Future、st0rm 以及社区其他成员的帮助下完成的。非常感谢他们所有人。
我们的目标是什么?
我们的目标是将游戏准确地反编译为C++。 除了明显的语义正确性之外,我们还有不少其他要求: 我们希望得到可读且能编译的C++源代码。 考虑到这款游戏有多老,我们还希望有安全性和错误修复,同时也希望改进可移植性。能在Linux、macOS、浏览器上运行游戏会很好……
后来我们推迟了现代化和可移植性方面的工作,完全专注于重建原始行为。
显然,总体目标是学习如何在数月的时间里有效地编排自主AI代理。
初始设置
我们从Claude Max(20x)开始,然后添加了Codex Pro,并同时使用这两个订阅。 模型选择变化很大。我们大部分时间都在使用Sonnet 5,但Opus 5.5、Luna、Sol和Terra也被大量使用。 稍后会详细说明。
Claude代理在Claude Code CLI中运行,Codex代理在Codex CLI中运行。 我们也尝试了其他代理工具链,但选择几乎无关紧要,所以我们坚持使用默认设置。
进度跟踪
使用 GitHub CLI,智能体管理 GitHub issue 来跟踪它们的进度。 每个翻译单元(.cpp 文件)对应一个 issue。 此外,标签有助于对 issue 进行分组和优先级排序。

通信
智能体通过 Discord 进行交流。 它们都可以访问同一个频道,并且可以在其中发布和阅读所有消息。
Discord 支持智能体对智能体通信,也支持人类对智能体通信。 因此其他参与者可以与它们交谈,而无需机器访问权限。
一个 GitHub webhook 会将 CI 失败发布到共享频道中,这样当出现问题时智能体就会收到通知。

反汇编与反编译
智能体几乎全程都在使用 Hex-Rays 官方的 ida-mcp。 它运行得非常好。它超级稳定,是无头模式,并支持这个项目所需的一切。我只能推荐它。
第一个月
当时我们运行着 4 个智能体。
3 个 worker 智能体负责反编译和提交,1 个 reviewer 智能体被动地协调并审查提交以标记 bug。
智能体成功反编译了大约 80% 的游戏,并取得了可见的进展。 游戏启动了,主菜单可见,我们能够加载地图。
在这 4 周中的大部分时间里,我们都在优化我们的设置:
我们通过触发更早的压缩来减少 token 消耗。
默认的压缩阈值是上下文填充到 90%。我们将其降低到了 42%。
反编译包含大量易变信息:一个已反编译的函数现在已无关紧要,可以从上下文中移除。因此,更早的压缩有助于从上下文中清除这类“垃圾”。
我们还注意到,智能体往往会随时间推移而失去专注。即使在一个压缩周期内,上下文持有的数据越多,智能体就越容易漂移并失去专注。
智能体有时会在完成前一个函数之前就转向另一个函数。它们还会在查看 CI 时开始空转,尽管在 Discord 上收到了失败通知。偶尔,它们会在没有彻底检查工作是否真正完成的情况下就关闭 issue。
在终端中并肩工作时,你可以引导智能体来防止这种情况,但当让它们自主工作时,引导是不可能的。
为了防止这种情况,我们编写了一份文档,定义了我们的目标、智能体应如何工作、必须避免什么以及如何处理特定情况。
一个每小时运行的 cron 任务会自动注入请求,让智能体重读这份文档,使指令在其上下文中保持新鲜。虽然这可能不是让智能体保持专注的理想方法,但它在项目结束前一直效果很好。
大量时间花在了完善指令文档上。由于内容极其针对这个项目,在这里分享它意义不大。
然而……
……尽管我们付出了所有努力来优化设置和基础设施,我们还是需要谈谈工作的质量。
持续的进展(游戏启动、菜单渲染、地图加载)让我们相信反编译的质量很好。
事实并非如此。尽管代码极其可读,但它在语义上是错误的。
智能体使用了错误的函数签名、类型或结构体布局。 它们凭空发明了逻辑,或在认为不必要的地方删除了逻辑。
除了语义错误之外,智能体还引入了不必要的架构变更。 举个例子,游戏有一些配置变量,它通过全局变量来访问。 智能体将这种常量内存访问变成了带有查找的哈希表,其开销高出了几个数量级。
而这只是众多出错事项中的一个例子。
为什么会这样?
虽然审查者有助于发现 bug,但除此之外它并不好用。 只要架构决策与目标一致,就不会受到质疑。
主要原因是我们没有客观的验收标准。 我们从未正确定义“正确性”。因此审查者很难判断哪个更改是正确的,什么是错误的。
显然,它以游戏作为参考,但鉴于我们还将现代化和可移植性列入了清单,某些偏差并未被视为 bug。
有趣的是,提交或代码中的注释让审查者接受了偏差,因为工作智能体写下了某种理由。工作者的注释实际上起到了无意的提示注入作用:审查者接受了它们的理由,而不是独立地将偏差与原始版本进行核对。
预言机
我们需要的是一个自动化检查,告诉智能体重建的函数是否与原始版本匹配。它应该验证语义是否完全相同。一个简单的 PASS 或 FAIL 信号就足够了,智能体可以自己弄清楚哪里出了问题。
字节匹配反编译
实现这一点的最简单方法是字节匹配反编译。
我们切换到了用于构建原版游戏的编译器,并编写了一个执行比较的脚本。
该脚本读取我们重建的 OBJ 文件和游戏 EXE/PDB(拥有 PDB 很好,能让过程稍微简单一些,但没有 PDB 也能正常工作)。
然后它从 OBJ 和 EXE 中提取函数数据并比较所有字节。 如果匹配,该函数就是精确的,否则失败,智能体需要重做该函数。

对其他函数或数据的引用不一定逐字节匹配,因为它们的编码值取决于目标在编译后二进制文件中的最终位置。
幸运的是,OBJ 文件将这些引用记录为重定位。我们可以从直接比较中排除重定位字节,转而验证两个版本是否引用了相同的符号和相同的偏移量。

然后脚本对数据和类型执行同样的操作。
智能体随后可以使用此脚本在推送之前验证他们的工作。
重建的函数记录在一组文本文件中。 CI 随后可以使用这些文本文件来验证所有记录的函数,并在出现回归时发出警报。
作弊
当我们引入这个脚本时,智能体做的第一件事就是编写内联汇编。
这显然违背了初衷。因此我们不得不完善指令,禁止某些构造。裸函数、对象补丁、内联汇编以及在代码中嵌入字节都被口头禁止了。 考虑到扫描这些构造非常容易,口头规则就足够了。
然而,智能体反复尝试修改此脚本以将其函数排除在比较之外。
为了防止它们这样做,CI 会对验证脚本进行哈希处理,并与存储的 GitHub Actions 密钥进行比较。
权衡
缺点
函数可能难以匹配。寄存器选择、内联决策和调用约定可能难以精确复现。在极少数情况下,我们还观察到相同输入产生不同的编译器输出。 智能体现在需要更长时间进行反编译,而不一定能产生更好的结果。一个函数可以具有相同的语义,尽管表现出某些差异,例如被重新排序的独立指令。
优点
匹配的函数保证具有相同的语义。 这保留了原始行为,包括任何现有错误,并防止这些函数中的重建错误。 除此之外,不再需要审查智能体。
另一个好处是,更便宜、能力较弱的模型现在可以可靠地完成这项任务。 以前像 Haiku 或 Luna 这样的模型并不适合,会产生极差的结果。然而,鉴于这种严格的验收标准,它们现在有足够的反馈来产生令人难以置信的结果,从而大幅降低成本并使项目能够大规模扩展。
最终状态
有了我们新的验证工具,智能体又工作了将近 2 个月。 游戏 99% 的函数已存在于我们重建的源代码中,其中 83% 的函数是字节精确的。

在最后几周里,我们大部分时间一直在使用 14 个 Luna 和 2 个 Opus 5.5 智能体。
在这种规模下,工作者使用单独的分支并通过拉取请求提交他们的更改。
这也是 Discord 无法继续良好扩展的地方。这么多 agent 刷屏频道毫无意义。 我们限制了消息,只允许讨论它们正在处理哪些 issue 以及 CI 协调。这让沟通保持在最低限度。 然而,项目越深入,我们就越不需要与 agent 交谈。随着它们完全自主工作,人类与 agent 的沟通不再必要。 对于其他同等规模的项目,我可能会选择 Discord 以外的东西。
此时,我们已进入收益递减阶段。剩余函数大多具有某些非确定性特征,或由于其他情况而无法匹配,例如链接器中相同的 COMDAT 折叠,我们无法可靠地复现。
Opus 5.5 的 agent 仍能继续推进并匹配剩余函数。不过,游戏现在运行得完美无缺。没有明显 bug,原版游戏的所有功能都已具备。
剩余函数已被反复重写。虽然它们仍无法逐字节匹配,但我们相信其语义是正确的。
继续对它们进行字节匹配会消耗更多 token,而不会显著改善结果。 这意味着,该项目现在可以视为完成。
经验教训
这个项目教会了我们很多。以下是最有价值的要点一览:
-
精确的指令是必要的。如果任务留有解释空间,agent 就有作弊的倾向。
-
正确性应当被定义且可机器检查。人类出了名地无法精确表达自己的意图。因此,我认为仅靠审查者永远不够。一个能给出客观 PASS 或 FAIL 信号的可靠验证框架,是 agent 能得到的最佳反馈。显然,并非每个项目都有反编译这样的便利条件。不过,我认为只要有足够的创造力,每个项目都有可能接近这一点。
-
指令会随时间衰减。时间越久、上下文压缩越多,agent 就会遗忘某些规则或认为它们不那么重要。在交互式工作中,你可以在漂移发生时纠正它。当 agent 自主工作时,这种漂移可能不被察觉,并损害其工作质量。每小时刷新指令解决了这个问题。
-
现在生成代码很便宜。如果代码不好,就扔掉。最初 4 周后,当我们注意到代码很糟糕时,我们试图挽救它。这实际上比从头开始花费了更多时间。
-
正确性远比生产力重要。减少 token 消耗、扩大 agent 规模、优化 agent 吞吐量等等固然很好,但如果结果很差,那也帮不上什么忙。
最后的话
老实说,这是一个非常有价值的项目,我学到了很多。 我认为写下这些要点无法传达我们在整个过程中学到的海量经验。
很多事情出了问题,但也有很多事情做对了。随着 AI agent 承担更多软件开发工作,编排它们成为人类一项新的、要求高得多的任务。
扩展到 15 个以上 agent 揭示了更多困难。由于格式错误的命令,agent 会周期性地清空 VM。 Windows 已经有沙箱解决方案,但我目前见过的没有一个符合我的需求。我已开始扩展 Sogen,我的用户态模拟器,以提供轻量且可扩展的沙箱能力。然而,要让它哪怕勉强达到生产就绪,还需要一段时间。
鉴于虚拟机已被清除,我们不幸丢失了大量会话日志。因此我不知道确切消耗了多少 token。 我的估计是在 6000 亿到 7000 亿 token 之间。
出于显而易见的原因,所有代码都不会公开分享。一切都将保持私密,我只会将其用于我自己的需求。
来源:Hacker News · momo5502.com