跳到正文
原文
Hacker News· davikr·· 2 小时前精选AI 评分63

500B Token 之后:用 AI 智能体反编译一款第一人称射击游戏

500B Tokens Later: Letting AI Agents Decompile a First-Person Shooter

AI 导读

作者用 AI 智能体将一款未具名的第一人称射击游戏反编译为可编译 C++ 源码,重建函数覆盖 99%、其中 83% 与原版字节级一致。

推荐理由

展示了用字节级匹配作为自动化验收标准来约束AI智能体长期任务的经验,对同类需要客观正确性判定的自动化项目有直接借鉴价值。

正文 · AI 翻译

在过去三个月里,我花了一些时间和 token 来反编译一款流行的第一人称射击游戏。 目标不是仅仅达到简单的概念验证状态。 相反,我们真正想要的是对这款游戏进行精确、稳定且功能完整的重制。

我博客的忠实读者可能已经注意到,我之前写过两篇文章,但后来已被删除。 其他读者现在可能在想我到底在说哪款游戏。 对你们我只能说,美国企业界来毁掉了我们的乐趣。

不过,没关系。这篇文章不是关于这款游戏,也不太涉及反编译的过程。 它更多是关于 AI 编排,以及如何优化基础设施、设置和 harness 以获得最佳结果。

这个项目是在 RektInator、Future、st0rm 以及社区其他成员的帮助下完成的。非常感谢他们所有人。

我们的目标是什么?

我们的目标是将游戏精确地反编译为 C++。 除了明显的语义正确性之外,我们还有更多要求: 我们希望生成的 C++ 源代码可读,并且能够编译通过。 鉴于游戏年代久远,我们还希望加入安全性和错误修复,以及可移植性方面的改进。能够在 Linux、macOS、浏览器上运行这款游戏就太好了……

我们后来推迟了现代化和可移植性工作,专注于完全重建原始行为。

显然,总体目标是学习如何在长达数月的时间里有效地编排自主 AI agent。

初始设置

我们最初使用 Claude Max (20x),之后又加入了 Codex Pro,并同时使用这两份订阅。 模型选择变化很大。我们大部分时间在使用 Sonnet 5,但 Opus 5.5、Luna、Sol 和 Terra 也用得很多。 稍后会详细介绍。

Claude agent 运行在 Claude Code CLI 中,Codex agent 运行在 Codex CLI 中。 我们也尝试过其他 agent harness,但选择几乎没什么影响,所以我们坚持使用默认设置。

进度跟踪

使用 GitHub CLI,智能体通过管理 GitHub issue 来跟踪进度。 每个翻译单元(.cpp 文件)对应一个 issue。 此外,还借助标签来对 issue 进行分组和排定优先级。

通信

智能体通过 Discord 进行通信。 它们都可以访问同一个频道,并在其中发布和读取所有消息。

Discord 支持智能体之间的通信,也支持人类与智能体之间的通信。 因此其他参与者可以直接与它们交流,而无需机器访问权限。

GitHub webhook 会将 CI 失败信息推送到共享频道,这样智能体就能在出现故障时收到通知。

反汇编与反编译

智能体几乎全程在使用 Hex-Rays 官方的 ida-mcp。 它非常好用。极其稳定,支持无头运行,并具备本项目所需的全部功能。我强烈推荐它。

第一个月

当时我们有 4 个智能体在运行。

其中 3 个 worker 智能体负责反编译和提交,1 个 reviewer 智能体被动地协调工作并审核提交,以标记出 bug。

智能体们成功反编译了约 80% 的游戏内容,并取得了可见的进展。 游戏可以启动,主菜单可见,我们也能加载地图了。

那 4 周里,我们投入了大量时间来优化工作流程:

我们通过更早地触发压缩来降低了令牌消耗。
默认压缩阈值为上下文填充到 90%。我们将其下调到 42%。 反编译包含大量易变信息:一个函数被反编译之后就已无关紧要,可以从上下文中移除。因此,提前压缩有助于将这些“垃圾”从上下文中清除。

我们还注意到,代理会随着时间推移而逐渐失去专注。即便是在一次压缩周期的范围内,随着上下文持有的数据越来越多,代理也可能发生漂移并丧失专注。

代理有时在完成一个函数之前就转去处理另一个函数。它们在盯看 CI 时还会开始闲置,尽管在 Discord 上收到了失败通知。偶尔,它们还会在没有仔细核实工作是否真正完成的情况下关闭工单。

在终端中与代理并肩工作时,你可以通过引导来防止这种情况;但让它们自主工作时,就无法进行引导了。

为防止这种情况,我们编写了一份文档,定义了我们的目标、代理应如何工作、必须避免的事项,以及如何处理特定情况。

一个每小时运行的 cron 任务会自动注入一条请求,让代理重新阅读该文档,使其上下文中的指令保持新鲜。虽然这可能不是让代理保持专注的理想方法,但在项目结束前效果一直很好。

我们花了大量时间来打磨这份指令文档。不过,由于其内容极其针对本项目,在这里分享意义并不大。

然而……

……尽管我们在优化设置和基础设施方面付出了种种努力,但仍需要谈谈工作的质量问题。

持续的进展(游戏启动、菜单渲染、地图加载)让我们相信反编译质量很高。

事实并非如此。尽管代码极其易读,但在语义上是错误的。

代理使用了错误的函数签名、类型或结构体布局。它们凭空发明了逻辑,或者在认为不必要时将其移除。

除了语义错误之外,代理还引入了不必要的架构变更。举例来说,游戏通过全局变量访问某些配置变量。代理却将这种常量内存访问改写为代价高出数个数量级的哈希表查找。

而这只是众多出错案例中的一个。

为什么会这样?

虽然评审员有助于捕获 Bug,但在超出此范围的事情上效果不佳。只要架构决策与目标保持一致,就不会受到质疑。

其主要原因是我们缺乏客观的验收标准。我们从未正确定义过“正确性”。因此评审员很难判断哪些变更是正确的、哪些是错误的。

显然,它以游戏作为参考,但鉴于我们的清单上还列出了现代化和可移植性,某些偏差并未被视为 Bug。
有意思的是,提交记录或代码中的注释让评审员接受了这些偏差,原因仅是工作代理所写下的任何辩解。工作代理的注释实际上起到了无意的提示注入作用:评审员接受了它们的辩解,而非独立地将偏差与原始版本进行核对。

Oracle(预言机)

我们所需要的是一种自动化检查,能告诉代理重建的函数是否与原始版本匹配。它应当验证语义是否完全一致。一个简单的 PASS 或 FAIL 信号就已足够,代理可以自行弄清问题所在。

字节匹配反编译

实现这一目标最简单的方法是字节匹配反编译。

我们切换到了用于构建原版游戏的编译器,并编写了一个执行比较的脚本。

脚本读取我们重建的 OBJ 文件和游戏 EXE/PDB(拥有 PDB 非常好,可以让流程稍微简单一些,但即使没有 PDB,这个过程同样可以正常工作)。

然后它从 OBJ 和 EXE 中提取函数数据并比较所有字节。 如果匹配,则函数完全一致,否则失败,代理需要重新处理该函数。

对其他函数或数据的引用不一定能逐字节匹配,因为它们的编码值取决于目标在编译后的二进制文件中的最终位置。

幸运的是,OBJ 文件将这些引用记录为重定位信息。我们可以从直接比较中排除重定位字节,而是验证两个版本是否引用了带有相同偏移量的同一符号。

然后脚本对数据和类型执行相同的操作。

然后代理可以在推送之前使用此脚本来验证其工作成果。

重建的函数被记录在一组文本文件中。 然后 CI 可以使用这些文本文件来验证所有已记录的函数,并在出现回归时发出警报。

作弊

我们引入此脚本后,代理做的第一件事就是编写内联汇编。

这显然违背了初衷。因此我们不得不细化我们的指令以禁止某些构造。裸函数、对象修补、内联汇编以及在代码中嵌入字节都被口头禁止。 鉴于扫描这些构造非常容易,口头规则已经足够了。

然而,代理屡次试图修改此脚本,以将其函数排除在比较之外。

为了防止他们这样做,CI 对验证脚本进行哈希运算,并与存储的 GitHub Actions 密钥进行比较。

权衡取舍

缺点

函数可能很难匹配。寄存器选择、内联决策和调用约定可能难以精确重现。在极少数情况下,我们还观察到相同输入产生了不同的编译器输出。 代理现在需要更长时间进行反编译,却不一定能产生更好的结果。一个函数可以具有相同的语义,尽管会表现出某些差异,例如独立指令被打乱顺序。

优点

匹配的函数保证具有相同的语义。 这保留了原始行为,包括任何现有的 bug,并防止这些函数出现重建错误。 最重要的是,不再需要审阅代理。

另一个好处是,更便宜、功能较弱的模型现在可以可靠地完成此任务。 以前,像 Haiku 或 Luna 这样的模型并不适合,会产生极差的结果。然而,鉴于这一严格的接受标准,它们现在有了足够的反馈来产生令人难以置信的结果,大幅降低了成本,并使项目能够大规模扩展。

最终状态

借助我们的新验证框架,代理已经又工作了将近 2 个月。 我们重建的源代码中已包含 99% 的游戏函数,其中 83% 的所有函数实现了字节级完全匹配。

在最后几周里,我们大部分时间使用了 14 个 Luna 和 2 个 Opus 5.5 代理。

在那个规模下,工作人员使用单独的分支,并通过拉取请求提交他们的更改。

Discord 也正是从这里开始无法良好扩展。大量智能体在频道里刷屏毫无意义。我们限制消息只能针对它们正在处理的问题和 CI 协调。这将沟通保持在最低限度。然而,随着项目推进,我们与智能体沟通的需求越来越少。由于它们能够完全自主工作,人与智能体之间的沟通已不再必要。对于同等规模的其他项目,我可能会选择 Discord 以外的工具。

此时我们已遇到收益递减。剩余的函数大多具有某些非确定性特征,或因其他原因无法匹配,例如链接器中相同的 COMDAT 折叠,我们无法可靠地复现。

Opus 5.5 智能体仍然能够继续推进并匹配剩余的函数。然而游戏现在运行得毫无瑕疵。没有明显的 bug,原版游戏的所有功能都得到了保留。

剩余的函数经过反复重写。虽然它们仍然无法逐字节匹配,但我们相信它们的语义是正确的。

继续对它们进行字节匹配只会消耗更多 token,却无法实质性地改进结果。也就是说,这个项目现在可以视为完成了。

经验教训

这个项目让我们学到了很多。以下是最有价值的经验总结:

  • 精准的指令是必要的。如果任务留有解释空间,智能体就会有作弊的冲动。

  • 正确性应当是可定义且机器可验证的。众所周知,人类无法精确表达自己的意图。因此,我认为光靠评审永远不够。一个能够输出客观 PASS 或 FAIL 信号的可靠验证框架是智能体能获得的最佳反馈。显然不是每个项目都拥有反编译那样的便利条件。但我认为,只要足够有创意,每个项目都有可能接近这一点。

  • 指令会随时间衰减。随着时间推移和上下文不断被压缩,智能体会遗忘某些规则或将它们视为不那么重要。在交互式工作时,你可以随时纠正这种偏移。当智能体自主工作时,这种偏移可能不被察觉,从而损害它们工作的质量。按小时刷新指令解决了这个问题。

  • 现在生成代码的成本很低。如果代码不好,就扔掉重来。在最初 4 周之后,当我们发现代码质量不佳时,我们曾试图挽救它。事实上,这反而比从头开始花了我们更多时间。

  • 正确性远比生产力重要。减少 token 消耗、扩大智能体规模、优化智能体吞吐等等固然都好,但如果结果糟糕,那也帮不了多少忙。

结语

说实话,这是一个非常有价值的项目,我学到了太多东西。我认为把这些心得写下来也无法传达我们在整个过程中学到的海量经验。

很多事情出了错,但也有很多做对了。随着 AI 智能体承担更多软件开发工作,编排它们成为人类一项新的、要求高得多的任务。

将规模扩大到 15+ 个智能体后,更多的困难浮现出来。由于命令格式错误,智能体会周期性地把虚拟机搞坏。Windows 上虽然已有沙箱方案,但我目前见到的都不符合我的需求。我已开始扩展我的用户态模拟器 Sogen,以提供轻量且可扩展的沙箱能力。但要达到哪怕勉强可用的程度,仍需时日。

由于虚拟机被清空,我们不幸丢失了大量会话日志。因此我不知道具体花费了多少 tokens。我的估计介于 6000 亿到 7000 亿 tokens 之间。

出于显而易见的原因,所有代码都不会公开。一切都将保持私有,我只会将其用于自己的需求。

来源:Hacker News · momo5502.com