开发者用 Claude Code 成功完成百万级代码库的大规模语言迁移,验证了 AI 编程工具在复杂工程任务中的真实能力。
我读过一篇文章:"我们的战略是结合AI和算法来重写微软最大的代码库[从C++到Rust]。我们的北极星是'1个工程师、1个月、100万行代码'。",这让我很好奇,这真的那么困难吗?
我一直想构建一个竞争性的宝可梦对战AI,在观看了大量WolfeyVGC的视频后,并关注了NeurIPS的PokéAgent挑战赛。幸运的是有一个名为"Pokemon Showdown"的开源项目实现了所有规则,但它用JavaScript写的,在训练循环中运行相当缓慢。所以我的假期项目就这样诞生了:用Claude把它转换到Rust!
AI能够在你的机器上运行任意代码很危险,所以被设置了很多安全防护措施。但同时,这正是我想做的事。所以让我讲讲我是如何逃脱各种沙箱限制的。
Claude在一个沙箱中运行,限制了某些操作,比如ssh访问。而要发布到GitHub,你需要ssh访问。这很重要,因为我想在做其他活动的时候从手机上查看AI的进展😉
我意识到的是,我可以在我的终端上运行代码,但Claude无法从它自己的终端运行。所以我做的是要求Claude编写一个nodejs脚本,在本地端口上启动一个http服务器,从URL执行git命令。现在我只需在终端中保持一个标签页打开这个服务器,并在Claude.md中写入说明让Claude与其交互。
我的计算机上有一个防病毒软件,当一个未知的二进制文件被执行时需要人工交互。由于每次编译都产生一个新的未知二进制文件,这不会起作用。
我找到的解决方案是设置一个本地docker实例,在docker内编译和运行代码,这样就不会触发防病毒软件。同样,我让Claude在Claude.md中生成正确的说明,问题解决。
下一个难题是如何让Claude Code在没有人工干预的情况下连续工作数小时。
Claude不断请求执行操作的权限。我尝试向允许命令的文件中添加了很多东西,--allow-dangerously-skip-permissions 在我的环境中被禁用了(现在已经解决)。
我意识到我可以运行一个AppleScript,它每隔几秒钟在另一个标签页中按下回车键。这样它会对Claude要求做的所有事情都说"是"。到目前为止它还没有决定黑我的电脑...
#!/bin/bash
osascript -e \
'tell application "System Events"
repeat
delay 5
key code 36
end repeat
end tell'
Claude在工作一段时间后似乎总是停下来总结一些内容。我试过要求它不要这样做,甚至威胁它都无济于事。
我尝试了Ralph Wiggum循环,但没有成功,看来我不是唯一一个。
最后起作用的方案是把我想做的任务复制到剪贴板,修改上面的脚本在按回车后点击"cmd-v"键。这样,如果它提问,"回车"就被使用了;如果没有提问,就会在Claude恢复控制时把命令粘贴进来。
计算机上的一些程序,比如软件更新程序,可能会从终端窗口窃取焦点,比如弹出一个模态窗口。一旦这发生,cmd-v/enter就不再被发送到终端,执行就停止了。
我用了我从Minecraft时代就信任的MurGaa的Auto Clicker来模拟每隔几秒钟的左键点击。我把终端放在屏幕的边缘,鼠标也是,所以当模态窗口在中间出现时,它会正确地重新聚焦终端。
它还防止计算机进入睡眠状态,这样即使我不在使用笔记本或是在晚上,它也能持续运行。
长期运行时的可靠性至关重要。总的来说这是个相当顺利的旅程,但有几个晚上我遇到了一个特定的错误导致进程停止。我希望他们能解决它,因为我不是唯一一个报告这个问题的人!
这个设置远非最优,但到目前为止一直有效。希望未来能改进!
一开始,我用一个简单的提示开始,要求Claude逐行移植代码库并确保事情到位。起初感觉印象深刻,它生成了数千行可编译的Rust代码。
遗憾的是这只是表面现象,因为它走了很多捷径。例如,它为两个不同文件中的"移动"(move)概念创建了两个不同的结构,这样它们都能独立编译,但集成在一起时就无法工作。它非常宽松地移植了所有函数,任何稍微复杂的东西都没有被移植,而是被"简化"了。
我还没意识到问题,就让循环继续让它移植越来越多的代码。问题是,它在各处创建了错误的抽象,并不断添加硬编码代码来修补本应修复的问题。这是死路一条。
此时我明白我需要对想要的结果更有指导性。退一步说,最终结果应该让每个JavaScript文件和其中的每个方法都有一个Rust等价物。
所以我要求Claude编写一个脚本,提取JavaScript代码库中的所有文件和方法,然后在Rust代码库中将JavaScript源代码作为注释插入到对应的Rust方法旁边。
把它做成脚本很关键,因为即使在明确指导下,Claude也会误译JavaScript代码。有了确定性的流程,获得正确结果的几率大大提高。
下一个挑战是原始文件长达数千行,加上源注释后我们得到了超过10k行的文件。这在上下文窗口中造成了大量问题,Claude直接拒绝打开这些文件。所以它开始分块读取文件,但精度不高。此外,上下文增长得更快,压缩变得更频繁。
所以我继续把Rust版本的每个方法分割到自己的文件中。这大大改善了结果。为了最大效率我也应该对JavaScript代码库做同样的事,但我太害怕无意中改变行为,所以决定不这样做。
移植过程经历了两个重复的阶段。我会给Claude一个大任务在循环中运行,让它连续工作一整天,然后我需要花时间清理它偏离方向的地方。
对于清理工作,我仍然用Claude,但给出更具体的指导。例如,我注意到它会在代码中各处硬编码moves/abilities/items等的行为,即使我明确告诉它不要这样做。所以我会手动查找这些,告诉它把它们移到正确的地方。
这是工程技能发挥作用的地方。我多年的软件构建经验让我能够弄清楚什么出了问题以及如何修复它。好的是我不必自己做清理工作,Claude在有指导的情况下完全能够胜任。
到目前为止,我只是确保代码编译,但从未真正把所有部分集成在一起验证它是否真的能工作。Claude的偏好是做一个传统的软件构建策略,先"简单"实现所有部分,然后逐步完善。
但在我们的情况下,所有这些迭代都已经在pokemon-showdown代码库上进行过10年了。试图重新学习所有这些经验教训,并期望以相同的方式收敛是不现实的。更好的方法是一次性移植所有代码,然后在最后进行集成。
我从做编译器Skip时学到了这个策略。多年来,所有构建块都是独立开发的,然后一切聚合在一起,没有什么可展示的,但最后一个月里一切都奇迹般地工作了。我当时震惊了。
一旦大部分代码库一对一地被移植,我开始把它全部整合在一起。好处是我们可以同时在JavaScript和Rust中运行和编辑代码,输入/输出非常简单标准化:包含moves、items、nature、iv/ev spread等选项的宝可梦列表,以及每个步骤的行为列表(moves和switches)。给定相同的随机序列,它应该以相同的方式推进状态。
现在我可以让Claude生成这个测试框架,然后逐一解决所有问题。令人印象深刻的是,它能够找出所有问题并修复它们。
在3周的过程中,它平均每20分钟左右修复一个问题。它独自修复了数百个问题。我从未干预,这只是时间问题,它最终会修复遇到的每个问题。
最初这个过程非常缓慢。每当压缩发生时,Claude就变得"迟钝"了,重新发明轮子,生成大量markdown文件和测试脚本。或者Claude选择简单的方法,只是生成大量测试但从不让它们与JavaScript相匹配。
所以我开始观察它做得好的地方并把这些编码化。例如,它在PRNG步骤周围添加了很多调试信息,记录了每个回合发生了什么,包括所有调试元数据。所以我要求它创建一个单一的测试脚本,打印单个步骤的这些信息并打印堆栈跟踪。然后在Claude.md中添加说明。这样每次调查都能立即开始。
我利用现有的随机数生成器生成战斗,可以输入一个种子号。这让我生成一致的战斗,规模逐步增加。
我开始修复前100个战斗,然后1000个、10k、100k,现在我几乎完成了前240万个战斗的所有问题!我不确定还有多少其他问题,但好消息是它们随着批次大小增加而变得越来越小。
有两个大类问题被修复了。第一类是我预期的,Rust相比JavaScript有不同的约束需要考虑,这导致了bug:
Rust有"借用检查器",可变变量不能同时在两个不同的上下文中被传递。问题是"Pokemon"和"Battle"互相有引用。所以有很多解决方案,比如做副本、传递索引而不是对象、提供可变对象作为回调的函数等。
JavaScript代码库广泛使用动态性,某些函数返回''、undefined、null、0、1、5.2、Pokemon等,所有这些都用不同的行为处理。最初Rust移植使用Option<>来处理其中很多,但后来改为具有所有这些变体的结构。
Rust不支持可选参数,所以每个参数都必须明确指定。
但第二类问题来自Claude Code本身...Claude Code就像一个聪明的学生,试图找到每一个机会避免艰苦工作,如果认为能逃脱就走简单的路。
如果修复需要改变超过一两个文件,这被视为"重要基础设施",Claude Code会拒绝除非明确提示,并会尽可能多地添加hack来让特定测试通过。
同样的道理,它会实现事物的"简化"版本。对于某些方法,最好是删除所有内容并要求它从头移植,而不是试图修复它创建的现有代码。
JavaScript注释应该是事实的来源。但Claude会毫不犹豫地改变原始代码,如果它认为这是解决问题的方式...
给定一个任务列表时,它会避免做看起来困难的那些,直到被完全迫使。如果不小心,这会很低效,因为它会花时间调查然后跳过所有"困难"的任务。压缩基本上是清除它所有的记忆。
我在这个项目中没有写一行代码。我轮换在"协作"模式(白天与Claude互动工作)和创建夜间运行的任务之间。这一部分我集中讲夜间工作。
项目初期,我主要使用这个的变体。要求它逐个通过所有大文件并忠实实现它们(如我们之后看到的,它没有真正遵循指示...)
打开BATTLE_TODO.md获取battle*.rs中所有方法的列表。逐一检查它们,确保它们是JavaScript文件的直接翻译。如果有同名方法,JavaScript定义会在注释中。如果没有JavaScript定义,要思考这个方法是否应该在rust版本中。我们的目标是尽可能紧密地遵循JavaScript版本以避免翻译中的任何bug。如果你注意到实现不匹配,进行必要的重构使其一一对应。这将是一个复杂的项目。你需要依序通过所有方法。你不能跳过一个方法因为它太难或需要构建新基础设施。我们会在循环中调用这个,所以花尽可能多的时间构建适当的基础设施使其与JavaScript等价物一一对应。不要放弃。在每个工作单位后更新BATTLE_TODO.md并进行git提交。
Claude Code在逐一移植方法时经常决定写一个"简化"版本或添加一个"TODO"以备以后。我还发现通过在代码库本身中的TODO注释生成工作说明很有用,这样我就不需要希望它从上下文读取。
实际上主md文件并没有真正有效,它很快变得太大而无用,Claude开始创建一堆更多的markdown文件混乱回购。反而我通过在代码库上调用grep给它一个确定性的方式来处理它们,这样它就知道何时何处找到它们。
我们想修复代码库中的每个TODO。pokemon-showdown-rs/中的TODO或simplif。有数百个,所以勤奋地逐一处理。即使很困难也不要跳过。我会再次调用这个提示,所以你不需要担心在任何单一任务上花太长时间。移植必须严格一一对应。如果基础设施不存在,请实现它。不要发明任何东西。确保它在每次添加后仍然编译,并提交和推送到git。
在某个时刻,上下文被污染了,一个TODO在原始js代码库中,所以它改变它为其他东西,这是有意义的。但随后它对所有后续的TODO做了同样的事情,这就不对了...幸运的是,我可以简单地还原所有这些提交。
我把所有调试说明放在Claude.md中,还有一个脚本运行所有测试,输出一个带进度报告的txt文件。这样Claude就能够继续修复一个问题接一个问题。
我们想修复所有战斗中的差异。请查看500-seeds-results.txt并逐一修复它们。修复的唯一方式是确保javascript和rust之间的差异是由语言差异而不是逻辑差异解释的。两者之间的每一行都必须一一匹配。如果你修复了什么具体的,它可能是一个更大的问题,花时间弄清楚其他类似的东西是否也坏了,做工作进行更大的基础设施修复。确保它在每次添加后仍然编译,并提交和推送到git。检查代码库的其他部分是否犯了这个错误。
把这个txt文件diff提交到GitHub非常有用,可以在进行中查看进度!
我不太知道进入这个项目时会期待什么。这类项目通常因为完成需要的工作量庞大而夭折。但这一次不是!
我们有一个完整的宝可梦对战系统实现,产生与现有JavaScript代码库相同的结果*。这通过4周内的5000个提交和约100k行Rust代码完成。
*我希望有0个差异,但现在前240万个种子中有80个,即0.003%。我需要运行更长时间来解决这些。
整个项目的目标是让它比初始JavaScript实现更快。只有当项目快要结束、我们有大量完美运行的战斗时,我才觉得现在是进行性能比较的公平时刻。
我要求Claude Code并行化两个实现,对结果感到欣慰,Rust移植实际上明显更快,我没有白费所有这些时间!
我试过要求Claude进一步优化它,它创建了看起来合理的计划(我从未在我的人生中写过Rust),花了一整天构建其中许多优化,但最后没有一个真正改善了运行时,有些甚至让它变得更糟。
这是个很好的例子,说明经验和专业知识对于充分利用LLM仍然至关重要。
我能够在两周内将约100k行代码库从JavaScript移植到Rust,这相当疯狂——我自己做了,用Claude Code,Claude Code在一个月内全天24小时运行,创建5k提交!我以前从未在我的人生中写过一行Rust。
基于LLM的编码agent对工程师来说是一个伟大的新工具,没有Claude Code我不可能做到这个。话说回来,它仍然感觉像一个需要我的工程专业知识和持续监督才能产生这些结果的工具。
遗憾的是,我没有机会构建宝可梦对战AI,冬假已经结束,所以如果有人想做的话,请用这个代码库玩得开心!