Hacker News Best

I'm sorry, but you still have to think

8.5内容质量

TL;DR · AI 摘要

AI代理在代码重写中仍需人类深度参与,技术决策不可替代。DHH用AI重写Campfire应用时,不同语言实现暴露了设计决策的复杂性。

核心要点

  • Rust版本移除CSRF令牌以优化缓存,牺牲了向后兼容性
  • Elixir实现存在SQL查询串行化性能问题
  • Rust异步调度需主动让出线程,否则影响并发性能

结构提纲

按章节快速跳转。

  1. DHH委托AI代理用Rust重写Campfire应用,揭示AI辅助开发的局限性

  2. 向后兼容性、性能指标等隐性决策影响系统设计

  3. 移除CSRF令牌、使用进程内队列导致兼容性损失

  4. SQL查询串行化暴露AI生成代码的性能缺陷

  5. Rust非抢占式调度要求任务主动让出线程

  6. AI代理无法替代人类在关键设计决策中的判断

思维导图

用一张图看清主题之间的关系。

查看大纲文本(无障碍 / 无 JS 友好)
  • AI代理在代码重写中的局限性
    • 技术挑战
      • 非功能性决策隐含性
      • 异步编程复杂性
    • 语言差异
      • Rust兼容性取舍
      • Elixir性能缺陷
    • 人类决策必要性
      • 设计权衡判断
      • 性能优化决策

金句 / Highlights

值得收藏与分享的关键句。

#AI代理#代码重写#Rust#Elixir#技术决策
打开原文

抱歉,但你仍然必须思考 | 一切都关乎比特

撰写人

Piotr Sarnacki

于

2026年10月11日

抱歉,但你仍然必须思考

Ruby on Rails框架的创造者DHH决定将Campfire从Ruby on Rails重写为Rust。或者说更准确地说,他让AI代理代劳,因为他自己根本无法忍受阅读或编写Rust代码。此后他甚至又要求用其他语言如Elixir和Go进行重写。

这让我非常感兴趣,因为这为我们提供了一个绝佳的视角,观察使用AI代理完成任务时的结果,而无需阅读代码。在这种情况下,这位使用者还拥有丰富的编程经验。但结果却并不令人鼓舞,特别是如果你期待着未来可以停止阅读代码,甚至彻底停止思考的话。这些重写版本存在诸多问题,但让我们先从非功能性差异开始讨论,因为这些差异很好地揭示了一个很多人似乎没有意识到的事实:如果提示指令不够具体,很多决策将变得随机。这是因为很多问题本身就没有唯一正确的答案。我们是否要保持向后兼容性?我们更关注延迟还是吞吐量?系统在负载下可以使用多少内存?崩溃后丢失新内容通知是否可以接受?你可能并不关心这些问题,或者至少不关心其中某些问题,但当LLM实现你认为自己想要的功能时,这些问题都会被隐式地回答。

如果你仔细观察这些重写版本,会很快发现它们对向后兼容性和其他约束条件的处理方式存在显著差异。例如,Rust版本没有保持100%的向后兼容性,它移除了CSRF令牌以简化缓存。它还用进程内队列替代了Redis,例如用于通知功能。Elixir的重写版本则与原始的Rails版本更为接近。这些差异本身已经让比较变得几乎毫无意义,因为它们与语言无关。这并不是说AI代理听到"用Elixir重写"后,会因为选择了Elixir这种语言而决定保持100%的向后兼容性。但事情还有更令人惊讶的地方!

如果你看了代码,虽然我知道我们不应该再继续阅读代码了,但你会发现很多地方并不理想。例如,我曾看到有人抱怨Elixir的版本采用了一个进程顺序处理所有SQL查询,即使这些查询是能够并发执行的读操作。这确实是一个合理的批评,但DHH似乎认为,如果Elixir无法写出高性能代码,这会显得不够体面。我不确定是否同意这个观点,当我看到Rust版本时,发现其中一些数据库操作并未使用异步方式,这在某些情况下可能更糟糕。在Rust中,当使用异步运行时,调度是协作式的而非抢占式的。如果一个任务没有主动让出执行权,其他任务就无法在同一个工作线程上运行。这意味着在实际应用中,任务执行时间应尽可能短。例如,当你运行一个耗时100ms的SQL查询时,不希望其他任务因为等待这个查询而被阻塞。因此,理想情况下应使用异步I/O操作,在等待数据库响应时将控制权交还给运行时。在Rust的重写版本中,有些查询在工作线程上运行,而有些则以阻塞操作的形式在异步任务中执行。锁的使用也是如此。在使用异步运行时时,最安全的选择是使用异步锁,例如tokio::sync::Mutex。如果你确定锁的持有时间非常短,使用非异步版本也是可以接受的。但如果同步锁持有时间达到10ms,那么同一线程上的所有任务都会等待锁释放,而执行阻塞任务的线程本身却没有任何实际工作。因此,我必须遗憾地告诉你,编程问题远未解决,你仍然需要了解自己在做什么。

进一步来看,事实证明,slop基准测试本身也存在缺陷,因为它们仅测量吞吐量,而忽略了系统的其他特性。例如,Zach Daniels在负载压力下测试了新文章通知的投递率,发现Rust版本在高负载下仅有1%的成功投递率。这显然不是一个令人满意的测试结果。基准测试本身非常困难。即使改进后的基准测试可能并未真正测试你想要验证的内容,这取决于系统的特性。你可能会发现,当测试系统时,你可能希望在负载下强调不同的特性。DHH和Zach的基准测试都是闭环测试,因此它们测试的是“系统在特定时间内能处理多少请求”。测试使用了N个客户端,每个客户端在收到前一个请求的响应后立即发送新消息。然而,在许多实际场景中,系统负载的增加可能来自于大量用户同时执行某个操作,而这些用户不会等待其他用户完成操作。在这种情况下,你更希望使用恒定到达率,即以恒定速率发送请求,而不是让请求速率依赖于系统响应速度。如果你想验证事件投递的可靠性,理想情况下应比较相同数量的事件。从结果来看,Rust版本在6000-7000条通知中仅成功投递了1%,而Elixir版本在约1700条通知中实现了100%的投递率。这表明Elixir版本在处理背压时表现更好,但这一优势仅在请求量未超出系统处理能力时成立。

但让我们暂时放下基准测试,谈谈权衡取舍,因为编程本质上就是关于权衡取舍的。当然,确实存在某些工具或解决方案明显优于其他方案且没有缺点的情况,但这种情况非常罕见,尤其是在需要构建可靠复杂系统时。我见过许多Elixir社区的成员在看到Rust版本1%的投递率后得出各种结论,却并未真正尝试理解背后的原因。普遍的共识是:Elixir在并发方面表现更好!Rust的调度器是协作式的,你怎么能忍受这种生活?我喜欢Elixir,并且成功地在生产环境中使用过它,但它并不是万能的银弹。确实,Elixir(或其他基于BEAM的语言)在并发处理方面表现优异,而且公认地,用Elixir编写并发代码通常比Rust更容易,但这也意味着失去了底层控制能力、更高的内存占用以及经常性的性能损失。这就是rustler存在的原因。仅凭单一指标宣称某种语言的优越性,而不了解根本原因,可能会产生误导。

还记得我之前提到闭环压力测试只展示了系统的一个属性吗?当Rust处理了约4倍于Elixir的请求数量时,需要通过WebSocket向客户端发送更多事件,这导致了测试结果的差异。我用DHH的Rust版本和Zach修复后的Elixir版本重新运行了测试,这次使用恒定投递率。在100 POST/s的流量下,Rust客户端接收了约14%的事件,而Elixir接收了约60%。看起来Elixir仍然更好,对吗?别急着下结论!在这样的流量水平下,Rust没有出现任何HTTP错误,而Elixir有约23%的HTTP POST请求超时。那延迟呢?最差的事件投递延迟接近180秒。当Rust的投递出现延迟时,客户端会被断开连接。重新连接后,浏览器客户端会获取最新消息,这在很大程度上消除了对丢失事件的需求。你认为哪种用户体验更好:客户端在后台静默重连并获取新更新,还是需要等待3分钟才能收到新消息的通知?这再次证明,单一指标无法讲述完整的故事。

回到权衡取舍的话题。你知道为什么Rust版本在高负载下会丢失这么多消息吗?它使用了tokio::sync::broadcast通道向连接的客户端广播事件。当有新聊天消息需要发送给多个连接时,这种设计是合理的。广播通道的一个特性是其容量是有限的。如果接收方无法及时处理消息,接收方会收到RecvError::Lagged错误(更多关于延迟的细节请参考文档)。在Rust的重写版本中,广播容量被设置为256。而在Elixir中,GenServer进程负责处理事件投递,而GenServer的邮箱默认没有容量限制。只需在Rust中修改一行配置:

code
- stream_capacity: 256
+ stream_capacity: 16384

在100 req/s的请求速率下,投递率从约14%提升至约90%。现在比Elixir更好,不是吗?其实不然。我认为这个版本实际上更差,因为在这种情况下,最好快速失败并强制客户端重新连接,而不是极其缓慢地处理。你知道提高容量后还发生了什么变化吗?pMAX事件投递延迟从11秒上升至超过130秒,与Elixir的行为类似,我认为这比断开滞后客户端的表现更差。如果问我,即使11秒也太长了,如果接收方无法在这么短时间内传递事件,还不如直接丢弃事件。这说明了在系统中设置合理约束的重要性,也表明Elixir并不能自动解决所有并发问题。此外,如果你不自行设置限制,很可能会遇到外部限制。在100 req/s的压力测试中,Elixir版本的内存使用达到了1.8GB。另一个需要思考的问题:是丢弃部分消息更好,还是被OOM终止更好?权衡取舍无处不在。Elixir很强大,但它不会自动解决你所有的问题。无论使用哪种语言,你都必须考虑失败模式和权衡取舍。

那么今天我们学到了什么?你仍然需要批判性思考。了解自己在做什么很重要。不要仅根据单一指标就仓促做出假设。如果想要构建可靠系统,你需要知道何时应该失败。此外,基准测试非常困难。

如果你喜欢这篇文章,请考虑在Twitter上关注我。

Top