Hacker News 中文摘要

RSS订阅

I'm sorry, but you still have to think

DHH(David Heinemeier Hansson)是 Ruby on Rails 框架创造者,决定将 Campfire Once 由 Ruby on Rails 重写为 Rust;因受不了读写 Rust 代码,他让 AI 智能体(clanker)代劳,后又用 Elixir、Go 等语言重写。这展示有丰富编程经验者不读代码就用 AI 智能体的结果;若想很快不读代码甚至不思考,结果并不乐观。重写问题多,先看非功能性差异:提示不够具体时很多决定就像抛硬币,因为大量问题没有唯一正确答案,比如是否保持向后兼容、更看重延迟还是吞吐量。

正文

Ruby on Rails 框架的创造者 DHH 决定把 Campfire Once 从 Ruby on Rails 重写为 Rust。更确切地说,是让 AI(他称之为 clanker)替他完成,因为他自己根本受不了读写 Rust 代码。之后他又加入了其他语言的重写版本,比如 Elixir 和 Go。

这让我很感兴趣,因为它很好地展示了一个人使用 AI 智能体、却完全不读代码会得到什么结果。而在这个例子中,这个人还有丰富的编程经验。结果嘛……如果你指望自己很快就能不再读代码,甚至不再思考,那可不太乐观。这些重写版本有很多问题,不过我们先从非功能性差异说起,因为它们清楚地揭示了一个很多人似乎没意识到的事实:如果你的提示不够具体,很多决定就只是在抛硬币。这是因为大量问题并没有唯一正确的答案。我们要保持向后兼容吗?我们更在意延迟还是吞吐量?系统在负载下最多能用多少内存?崩溃后丢失新内容通知可以接受吗?你可能不在乎这些问题,或者至少不在乎其中一部分,但当 LLM 去实现你以为自己想要的东西时,它们会被隐式地回答。

如果再仔细看看这些重写版本,很快就会发现它们对向后兼容和其他约束的处理并不一样。Rust 版并没有保持 100% 向后兼容,比如它去掉了 CSRF token,好让缓存更容易;它还用进程内队列取代了 Redis,例如通知就走了进程内队列。Elixir 版则更贴近 Rails 原版。这已经让比较变得几乎没意义了,因为这些差异与语言无关。并不是 AI(clanker)听到“用 Elixir 重写”,就因为所用语言而选择保持 100% 向后兼容。但更精彩的还在后面!

如果你去看代码——当然,我知道,现在我们都不该再读代码了——你会很快发现很多地方写得并不好。比如有人抱怨 Elixir 版用一个进程串行处理所有 SQL 查询,即便这些查询是可以并发执行的读操作。这个抱怨是合理的,但 DHH 似乎认为,AI 写不出高性能代码,对 Elixir 的形象不太好看。可当我去看 Rust 版时,就不太确定自己是否同意了。因为在 Rust 版里,有些数据库操作并不是 async 的,在某些情况下可能更糟。要知道,在 Rust 里使用 async 运行时,调度不是抢占式的,而是协作式的。如果一个任务不主动让出(yield),同一个工作线程上其他任务都无法运行。这意味着实践中的任务占用时间应该尽可能短。例如,当你执行一个耗时 100 毫秒的 SQL 查询时,你肯定不希望其他任务都干等着,因为发出查询的任务本身也在等待。因此,理想情况下应该使用异步 I/O 操作,在等待数据库响应时把执行权交还给运行时。在 Rust 重写版里,有些查询跑在工作线程上,有些却在 async 任务里以阻塞操作的方式运行。锁也是一样。使用 async 运行时时,最稳妥的做法是使用异步锁,比如 tokio::sync::Mutex。如果你确定锁只会被持有极短时间,用非异步版本也没问题;但如果你持有一个同步锁 10 毫秒,同一线程上的所有任务都会等它,而那个阻塞任务本身其实并没在干活。所以我很遗憾地通知你:不,编码大概率并没有被“解决”,你仍然得清楚自己在做什么。

再往下看,那些粗制滥造的基准测试也同样粗制滥造,因为它们只测量吞吐量,而不管系统的其他属性。例如,Zach Daniels 测量了负载下新帖子通知的投递率,发现 Rust 版在高负载下的成功投递率只有 1%。这可不太好看。基准测试本来就不容易。但即便是改进后的基准测试,也可能并没有真正测到你想测的东西,这取决于你的系统特征。你要知道,测试系统时,你可能希望在负载下强调不同的属性。DHH 和 Zach 的基准测试都是闭环(closed-loop)基准测试,测的是“系统在特定时间内能处理多少请求?”。测试使用 N 个客户端,每个客户端一收到上一条消息的响应,就立刻发送新消息。但在很多情况下,系统负载的增加可能来自大量用户同时执行某个操作,而这些用户并不会等别人操作完。这种情况下,你更应该用恒定到达率,也就是以恒定速率发送请求,而不是让速率取决于系统响应有多快。如果你想检查事件投递有多可靠,理想情况下应该比较相同数量的事件。从结果来看:Rust 版在 6–7k 条通知里只投递了 1%,而 Elixir 版在约 1.7k 条通知里投递了 100%。这说明 Elixir 版更好地处理了背压(backpressure),但这只在请求数量没有把系统压垮的前提下成立。

不过我们暂时放下基准测试,来聊聊权衡,因为编程说到底就是权衡。确实有些情况下,某个工具或方案明显更好且没有任何缺点,但这相当罕见,尤其是当你面对必须可靠的复杂系统时。我看到很多 Elixir 社区的人在看到 Rust 版 1% 的投递率后,得出了各种结论,却没真正去理解为什么会发生这种情况。大家的普遍共识是什么?Elixir 就是更擅长并发!Rust 的调度器是协作式的,这日子还怎么过?我喜欢 Elixir,也在生产环境里成功用过它,但它并不是银弹。没错,Elixir(以及其他基于 BEAM 的语言)非常擅长并发,而且说实话,用 Elixir 写并发代码通常也比用 Rust 更容易;但这是有代价的:你失去了底层控制能力,内存占用更高,而且往往速度也更慢。rustler 的存在是有原因的。在不了解根本原因的情况下,仅凭单一指标就宣称某门语言更优越,可能会产生误导。

还记得我说过闭环压力测试只展现了系统的一个属性吗?Rust 版处理了约 4 倍的请求,因此也要通过 WebSocket 向客户端发送更多事件。我用 DHH 的 Rust 版和 Zach 的 Elixir 版(都带了一些修复)以恒定投递率重跑了测试。在每秒 100 个 POST 请求时,Rust 的客户端只收到约 14% 的事件,Elixir 则是约 60%。还是 Elixir 更好,对吧?先别急!在这个流量水平下,Rust 没有任何 HTTP 错误,而 Elixir 有约 23% 的 HTTP POST 请求超时。那延迟呢?最糟的事件投递延迟接近 180 秒。在 Rust 里,当投递滞后时,客户端会被断开连接;重连后,浏览器客户端会去拉取最新消息,这基本上就抵消了那些错过的事件的意义。你觉得哪种用户体验更好:客户端在后台静默重连并拉取新更新,还是为了等一条新消息的通知苦等 3 分钟?这正说明,单一指标依旧无法呈现全貌。

回到权衡。你知道 Rust 版为什么在高负载下丢掉这么多消息吗?它使用 tokio::sync::broadcast 通道把事件广播给已连接的客户端。一条新聊天消息的事件可能需要发给多个连接,所以用广播通道完全合理。广播通道的一个特性是它有固定容量上限。如果某个接收者处理消息不够快,就会收到 RecvError::Lagged 错误(更多关于lagging的内容可查看文档)。Rust 重写版里的广播容量被设成了 256。而在 Elixir 里,负责事件投递的是 GenServer 进程,且默认情况下 GenServer 的邮箱没有限制。Rust 里只要改一行:把 stream_capacity 从 256 改成 16384,就能把每秒 100 个请求时的投递率从约 14% 提升到约 90%。现在比 Elixir 好了,是吧?其实不是。我觉得这个版本实际上更糟,因为在这种情况下,更好的做法应该是快速失败、强制客户端重连,而不是以极慢的速度硬撑。把容量调高之后还发生了什么变化?pMAX 事件投递延迟从 11 秒涨到了 130 秒以上,表现和 Elixir 类似,而我认为这比直接断开滞后的客户端更糟糕。如果你问我,连 11 秒都太长了;如果接收者不能更快地把事件传出去,那还不如直接丢弃事件。这正说明在系统中设置合理约束有多重要,也说明 Elixir 并不会自动帮你解决所有并发问题。而且,如果你自己不设边界,很可能就会撞上外部限制。在每秒 100 个请求的压力测试中,Elixir 版的内存占用达到了 1.8GB。还有一件事值得琢磨:丢掉一些消息好,还是被 OOM 杀掉好?权衡无处不在。Elixir 很棒,但它不会神奇地解决你所有的问题。无论你用哪门语言,都必须思考失效模式和权衡。

那么我们今天学到了什么?你仍然需要批判性地思考。知道自己到底在做什么是有好处的。不要基于单一指标匆忙下结论。如果你想要一个可靠的系统,就应该知道什么时候该失败。另外,基准测试也很难。

评论

讨论围绕“LLM 完成代码移植或重写后,工程师是否还需阅读并理解代码、继续思考”展开,多数人强调理解系统与工程判断仍不可缺席,少数人则认为 AI 已能承担调查、基准测试和修复,单靠“你仍要思考”不足以抵御被替代。另有评论把争议延伸到 DHH 的 Elixir 示例,质疑其移植方式不公正,并怀疑他在借争议为 Omarchy 引流。

认为理解代码与工程判断仍不可缺席

  • 多位评论者认为,LLM 再强也只是工具,不能成为程序员停止工程的理由;要避免 p95 延迟飙升、OOM 和可靠性事故,就必须理解系统现状与未来状态,阅读或至少略读代码、在权衡下执行决策仍是工作核心。虽然在成熟系统里偶尔可以不读代码,现阶段仍不应把判断权交给 AI。(较多人) > 你需要对系统有足够理解,才能对其现状和未来状态做出明智决策;现阶段我宁愿读(或略读)代码,也不把判断权交给 AI。(@Ozzie_osman)

认为“仍要思考”挡不住被 AI 替代

  • 有评论者根据经验指出,发现高负载不稳定等问题后,只要把现象告诉 Claude,模型就能搭建高负载基准、定位并修复广播通道问题;虽然思考确实能提升结果,但把“你仍然必须要思考”当作安全区,并不能真正保护程序员免于失业。(少数人) > 思考确实能改善结果,但我不认为“你仍然必须要思考”是能让我们免于失业的安全空间。(@IshKebab)

认为移植成功只因原有思考已完成,不能推广

  • 有评论提醒,本次上下文是让 LLM 翻译既有系统,原开发者早已完成约九成思考,这对 LLM 是理想用例;因此案例并不能说明面对更复杂或全新系统时可以省掉人类思考。(少数人) > 这里的系统是让 LLM 翻译既有系统,对它是理想用例,约九成思考早已由原系统开发者完成。(@discreteevent)

质疑 DHH 的 Elixir 示例不公正且意在炒作

  • 部分评论认为 DHH 在 Elixir 版本中故意“做脏”它:Elixir 本可在不依赖 Redis 等组件的情况下高效运行,但他严格照搬 Rails 架构,甚至忽视 José Valim 等人的提醒。还有人怀疑他是在故意制造争议,为 Omarchy 引流;如果他真相信“从不读代码”,就应该让设计师基于高层描述直接迭代,而不是只说不练。(少数人) > Elixir 本可以在没有 Redis 之类的东西下飞快且极其强大,却被故意做成受束缚的样子。(@sergiotapia)

有价值的补充

  • 若要用移植案例证明效率提升,应在动手前预先约定基准测试来量化真实收益;但即便如此,完全脱手、无人工监督的复杂重写仍未被认为已具备可直接交付的成熟度。
  • 移植之后仍有维护与迭代的未解问题:新功能到底该在旧代码库实现后再移植,还是在陌生新代码库里开发,或是直接开 Jira 工单让 AI 代理随意发挥?这涉及后续代码所有权与流程。
  • AI 生成的低质量产出极难应对:它生产几乎不费力气,而要揭穿、修正或证伪却需要付出巨大精力。