返回探索
资讯VibeFix 编辑部更新于 2026年10月12日

DHH Campfire AI 重写翻车压测:送达率 1%,但你还是得动脑

2026 年 10 月 11 日,Piotr Sarnacki 在个人博客 It's all about the bit 发表长文《I'm sorry, but you still have to think》,把 DHH 让 AI 重写(且全程不读代码)的 Campfire Once 三个版本(Rust/Elixir/Go)重新压测了一遍:Rust 版重压下新消息通知送达率仅约 1%,async 任务里塞着阻塞的数据库查询;Elixir 版送达更好看,内存却一路涨到 1.8GB。只改一行 stream_capacity 从 256 到 16384,送达率 14%→90%,pMAX 延迟却从 11 秒恶化到 130 秒以上。这是 cycle37 DHH「不再手写代码」宣言的续集:AI 把「写」变成了便宜货,「想」就成了稀缺品。

DHH 的 AI 重写实验示意图:Rails 原版 Campfire Once 被 AI 重写为 Rust、Elixir、Go 三个版本,全程无人读代码

先说清楚:这是宣言的续集,不是重播

如果你还记得本站 cycle37 那篇「我们不再手写代码」:Rails 之父 DHH 的 Agent 时代宣言——DHH 宣布他已经不再手写代码,改写全交给 AI agent,自己连看都不看——那么今天这篇,就是那场宣言的"售后"。同一主角,同一个实验,新的进展:社区真的去压测了 DHH 让 AI 重写出来的代码,结果不太好看。

事情发生在 10 月 11 日。Piotr Sarnacki 在他的个人博客 It's all about the bit 上发表了一篇题为《I'm sorry, but you still have to think》的拆解长文,把 DHH 那场著名的"AI 重写 Campfire Once"实验从头到尾验了一遍。标题翻译过来就是:抱歉,但你还是得动脑。这篇文章当天就被 Lobsters 打上了 vibecoding 标签,10 月 12 日登上了 Hacker News 首页。评论区里最热的争论不是"Rust 和 Elixir 谁更强"——Sarnacki 恰恰警告不要掉进这个坑——而是"当 AI 替你写了所有代码,工程师的价值到底剩在哪"。

先快速对齐背景,尽量不复述宣言篇:DHH 让 AI(他管它叫 clanker)把 Campfire Once 从 Ruby on Rails 重写成 Rust,自己"受不了读或写 Rust 代码",所以全程不读代码;之后又加了 Elixir 和 Go 的重写版本。Sarnacki 的切入点很毒:这恰好是一个完美的自然实验——一个经验丰富的老程序员,用 AI agent 写代码但完全不读代码,产出到底是什么成色?

答案是:不太妙。而这篇拆解的价值不在于"看,AI 翻车了"这种廉价的幸灾乐祸,而在于它把翻车现场整理成了一份 vibe coder 的失败模式清单。清单上的每一条,都是 AI 不会替你想、但你必须想的东西。

DHH 的 AI 重写实验示意图:Rails 原版 Campfire Once,被 AI 分别重写为 Rust、Elixir、Go 三个版本,全程无人读代码

失败模式一:prompt 没写清楚的,每个架构决策都是抛硬币

Sarnacki 开篇就点破了一个很多人没意识到的真相:如果你的 prompt 不够具体,大量决策其实是抛硬币决定的。因为很多问题根本没有标准答案:要不要保持向后兼容?你更在乎延迟还是吞吐?高负载下系统能用多少内存?崩溃后丢几条新消息通知,能接受吗?

你可能不在乎这些问题,或者只在乎其中几个。但当 LLM 按它理解的"你想要什么"去实现时,这些问题会被悄悄地、默认地回答掉。三个重写版本很快暴露了这一点:Rust 版为了缓存方便,直接丢掉了 CSRF token;为了简单,把 Redis 换成了进程内队列(通知走的就是这个)。Elixir 版则离 Rails 原版近得多。这意味着什么?意味着这场"Rust vs Elixir"的对比从一开始就是无效的——差异跟语言没关系,纯粹是 AI 在没人指定的地方随手抛了硬币。

这对 vibe coder 是第一记耳光:你以为你在比较"哪种语言 AI 写得更好",实际上你在比较"AI 在这次抛硬币里猜中了几次"。prompt 里没写明的约束,不会凭空变成正确的架构决策,只会变成随机的架构决策。下次你让 agent"重写这个服务"时,先问自己:向后兼容、延迟目标、内存预算、失败语义,这四个问题你写进 prompt 了吗?没写的话,出来的东西好不好,全看运气。

失败模式二:不会 yield 的任务,和阻塞的 async

接下来是代码层面的硬伤,Sarnacki 读代码读出来的——是的,讽刺的是,这篇题为"你还是得动脑"的文章,恰恰是通过"读代码"发现问题的。

Elixir 版被社区诟病的一点是:它用单个进程串行处理所有 SQL 查询,连那些本可以并发跑的读查询也不例外。DHH 对此的反应是觉得"这让 Elixir 挺没面子的"——AI 写不出高性能代码,怪语言咯?Sarnacki 不太同意,因为他看了 Rust 版:那边的毛病可能更严重。

Rust 版里有些数据库操作根本不是异步的——它们是阻塞操作,却跑在 async task 里。在 Rust 的 async runtime 里,调度是协作式而非抢占式的:一个 task 不主动 yield,同 worker 线程上的其他 task 就一个都跑不了。想象一个查库 100ms 的 task 在那儿干等,所有排队的 task 陪着它一起等。正确做法是用会 yield 的异步 I/O。锁也一样:拿一个同步锁 10ms,同线程所有任务陪你罚站,而那个拿着锁的 task 自己还没在干活。最稳妥的是用 tokio::sync::Mutex 这类异步锁。

这些不是"AI 写错了个变量名"级别的问题,这是并发模型理解层面的问题。AI 可以背下 tokio 的 API,但它不会替你想"这个查询会不会阻塞调度器"。Sarnacki 的判词很克制:不,编程大概率还没有被"solved",你还是得知道自己在干什么。

对 vibe coder 来说,这里藏着一个更扎心的现实:这类 bug 在 happy path 下是完全隐形的。功能测试全绿、demo 跑得飞快,只有流量上来、worker 线程被一个个 100ms 的阻塞查询钉死,系统才突然塌掉。而 AI 在写这段代码时,没有任何"心智负担"——它不知道自己在用协作式调度,它只是在拼 API 调用。你不读代码,就永远不知道这里埋了个定时炸弹。等线上炸了,你连从哪开始查都不知道,因为"代码是 AI 写的"约等于"没有人真正理解这段代码"。一个没人理解的系统,出了问题就是玄学问题。

Rust 异步运行时协作式调度示意图:一个阻塞 100ms 的数据库任务拖住整个 worker 线程,其他任务全部等待

失败模式三:背压和失败策略,AI 一个都没替你想

这是全文最精彩的一段,一个关于"一行代码改命运"的故事。

Rust 版用 tokio::sync::broadcast channel 给所有在线客户端广播新消息事件。这个 channel 是有界的:容量一满,跟不上的接收者就会收到 Lagged 错误,消息直接被丢掉。重写版里这个容量被设成了 256。这就是为什么 Zach Daniels 测出来 Rust 版在重压下新消息通知送达率只有约 1%——不是网络问题,不是逻辑 bug,是背压策略在无人指定的情况下,默认成了"悄悄丢消息"。

然后 Sarnacki 干了一件漂亮的事:他只改了一行,把 stream_capacity 从 256 改成 16384。在每秒 100 个请求的压测下,送达率从约 14% 直接飙到约 90%。故事到这里如果是营销号,就该结尾"一行代码性能提升 6 倍"了。但真正的工程师会接着看第二个指标:pMAX 事件送达延迟从 11 秒恶化到了 130 秒以上。消息是不丢了,但用户要在 2 分钟后才收到"有人发了新消息"的通知。

哪个更好?Sarnacki 的判断很明确:还不如 fail fast,让跟不上的客户端断开重连。浏览器重连后会直接拉取最新消息,压根不需要那些迟到的事件。相比之下,Elixir 版的 GenServer mailbox 默认无界——消息不丢,但代价是压测中内存一路涨到 1.8GB。于是终极问题来了:你是愿意丢一部分消息,还是愿意被 OOM killer 干掉?

注意这个推理链条里真正值钱的部分:它不是"调大一个数字",而是先回答"用户体验到底是什么"。对聊天应用来说,"断线重连后看到最新消息"是一种完全可以接受、甚至用户无感知的体验;而"通知 2 分钟后才到"是对产品核心价值的直接破坏。AI 在写 broadcast 逻辑时,不可能替你做这个产品判断——它甚至不知道这是一个聊天应用的通知通道,而不是一个可以慢慢消费的后台队列。同样的 256 容量,放在日志收集场景可能完全没问题,放在实时通知场景就是灾难。脱离业务语义谈"正确配置",本身就是伪命题。

背压多大算合适、跟不上时该丢弃还是排队、慢和丢哪个更不可接受——这三个问题,AI 在写代码时一个都没替你想,因为 prompt 里一个都没写。这是 vibe coder 最该抄走的一课:凡是涉及"系统过载时怎么办"的决策,默认配置几乎永远是错的,而 AI 只会给你默认配置。

失败模式四:benchmarks are hard,单指标是个骗子

Sarnacki 花了很大篇幅讲基准测试的方法论,因为这场风波里最热闹的数字恰恰最会骗人。

先看最初的那个"1% vs 100%":Rust 版送达 1%,Elixir 版送达 100%。Elixir 社区很多人看完直接开香槟:Elixir 就是并发之王!但等等——Rust 版处理了 6000-7000 个事件,Elixir 版只处理了约 1700 个。分母都不一样,这个对比说明的其实只有一件事:Elixir 版背压处理得更好看,但前提是请求量还没把它压垮。

更深的问题在测试方法本身。DHH 和 Zach 用的都是闭环(closed-loop)压测:N 个客户端,每个发完一个请求、收到响应立刻发下一个,测的是"单位时间内系统能吞多少"。但真实世界的流量高峰往往是大量用户同时动手、谁也不等谁——这时候你该用恒定到达率(constant arrival rate)来压。Sarnacki 用恒定 100 POST/s 重跑了一遍,故事完全变了:Rust 版送达率约 14%,Elixir 版约 60%,Elixir 还是赢——先别急,Elixir 有约 23% 的 HTTP POST 请求直接超时了,最坏的事件送达延迟接近 180 秒。

于是问题从"谁送达率高"变成了"你想要哪种用户体验":客户端在后台静默重连、顺手拉到最新消息,和"新消息通知 3 分钟后才到",你选哪个?单指标给出的"1% vs 100%"是一个简单、错误、煽动性的答案;多指标拼出来的是一张 trade-off 的地图。Sarnacki 对那些急着宣布"Elixir 吊打 Rust"的人泼了盆冷水:不看根因、只看一个指标就宣布语言优越,是会误导人的。

这段对 vibe coder 的警示是双重的:第一,别信 AI 给你跑出来的 benchmark 截图,"吞吐量翻倍"可能只是它把失败策略改成了"悄悄丢弃";第二,你自己给 AI 下"优化性能"的指令时,如果不指定"在什么约束下优化、保住哪些指标",它一定会拿你最在乎的那个指标去换你没提的那个。

还有第三层,更隐蔽:Sarnacki 指出,最初的闭环压测里,Rust 版处理了 4 倍于 Elixir 版的请求——光是"处理了更多请求"这件事本身,就让它的通知投递任务更重。也就是说,连"谁的架构更好"这个问题,测试工具都在悄悄偏袒一方。方法论上的不对称,会系统性地生产出错误的结论,而错误结论一旦配上漂亮的数字,传播速度比真相快十倍。Elixir 社区那波"我们赢了"的欢呼,就是这么来的。这也是为什么 Sarnacki 要亲手用恒定到达率重跑一遍:他不信任别人的 benchmark,他只信任自己理解方法论之后跑出来的数字。

给 vibe coder 的三件事:AI 写得越快,你越得会的三件事

把这篇拆解压缩成行动清单,其实就是三件事。巧的是,这三件全是"AI 写代码"叙事里默认你不需要的东西。

第一,把 trade-off 写进 prompt。向后兼容要不要保?延迟和吞吐先保哪个?内存上限多少?崩溃丢消息能不能接受?这些没有标准答案的问题,你不写,AI 就替你抛硬币。Sarnacki 的原话值得刻在墙上:prompt 不够具体,大量决策就是抛硬币。下次开新项目,先写半页"非功能性需求",比调 prompt 技巧管用十倍。

具体怎么写?不用写论文,四个填空题就够:"这个服务必须兼容 ___(旧客户端/旧 API/都不用);延迟目标是 ___(p99 多少毫秒),吞吐目标是 ___;内存预算 ___,超过就 ___(拒绝/降级/告警);失败时优先保 ___(数据不丢/服务可用/延迟稳定)。"填完这四个空,AI 抛硬币的空间就被压缩到了最小。很多人抱怨"AI 写的架构总差点意思",差的那点意思,十有八九是这四个空没填。

第二,想清楚失败模式,再让 AI 动手。系统过载时,是丢消息、排队、还是直接拒绝?跟不上的客户端,是让它慢慢等,还是断开让它重连?Rust 版那个 256 的 broadcast 容量,就是"没人想过失败模式"的纪念碑。AI 给你的永远是"happy path 跑得通"的代码,failure mode 是你作为工程师必须亲手指定的部分——这部分外包不出去。

第三,学会做 benchmark,而不是看 benchmark。闭环还是恒定到达率?分母对齐了吗?除了吞吐,看延迟了吗?看了 p99/pMAX 吗?看了内存吗?看了错误率吗?"benchmarks are hard"不是一句感慨,是方法论警告:单指标一定会骗你,区别只是它这次想骗你相信什么。

有意思的是,这三件事一件比一件"不性感":写约束文档、画失败流程图、搭压测脚手架。没有一条能让你在 demo 里惊艳全场,但少了任何一条,你的"AI 写的系统"都是在裸奔。这大概就是 Sarnacki 起这个标题的原因——不是"AI 不行",而是"动脑的部分,AI 替不了你"。

我们的判断:宣言和拆解,是同一枚硬币的两面

把这篇和本站 cycle37 的DHH 宣言篇放在一起看,会发现一个漂亮的对称。宣言篇里,DHH 说"我不再手写代码了"——这是关于"写"的宣言;拆解篇里,Sarnacki 说"但你还是得动脑"——这是关于"想"的提醒。一个讲生产,一个讲验收,合起来才是一套完整的 AI 编程观。

最值得玩味的一个细节是:DHH 不是新手。他是有几十年经验的 Rails 之父,连他"不读代码"交出去的产出,都有阻塞 async runtime、背压默认丢消息这种毛病。这说明问题不在"使用者水平不够",而在"不读代码"这个动作本身——经验可以帮你写出更好的 prompt,但省不掉对产出的批判性审查。AI 把"写"变成了便宜货,于是"想"变成了稀缺品,而稀缺的东西只会更贵。

所以这不是一篇"反 AI"文章,Sarnacki 自己也说他喜欢 Elixir、也在生产环境用过,它不是来站队的。它真正想说的是那句结尾:批判性思考、别被单指标忽悠、知道系统什么时候该 fail、以及 benchmarks are hard。对 vibe coder 来说,翻译一下就是:你可以让 AI 写下第一版,但"这版能不能上生产"的判断题,答案只能你自己写。

宣言说"AI 写就行",拆解说"写完还得你动脑"。两篇都读完,你才算拿到了这个 saga 的完整拼图。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...

原始来源

浏览项目广场发布你的项目

相关文章

独立开发者在手术台般的工作台上为机器人更换芯片的插画,象征 AI 项目的模型换芯流程
指南
模型退役潮生存手册:一人项目的"换芯"SOP

10 月 Copilot 两天退役 10 个模型、GPT-5.5 从 Codex 退役、OpenAI API 关停 17 个模型标识——换模型已从技术升级变成运营常态。这篇实战指南给出完整换芯 SOP:三层抽象设计、30 分钟最小 eval 基线、影子到全量的灰度四步、每周 10 分钟公告监控、换芯真实成本公式、三种翻车模式,以及一页纸可复制 checklist。

开发工作流模型动态独立开发
新 TRAE 双模式界面:Agent 模式多任务卡片并行推进,IDE 模式保留完整代码编辑,右上角一键切换
资讯
TRAE 双端融合:字节把“工作”和“代码”装进同一个 Agent 工作流

10 月 9 日,字节宣布 TraeWork 与 TraeCode 双端融合,统一升级为新 TRAE:一个入口承接连续任务,右上角一键切换 Agent 模式与 IDE 模式。Agent 模式是全局工作台——多个 Agent 分工做代码、测试、修复;IDE 模式保留 SOLO 与完整 IDE 编辑,专抠细节。代码变更、文档、图片、视频、测试报告统一沉淀进「项目产物」,不再埋在聊天记录里。这次升级真正值得讨论的不是"又一个 AI IDE",而是字节把"工作"和"代码"装进同一个 Agent 工作流——以及它给一人团队的分工方法论启示:写代码、测试、修复三 Agent 接力,还得有个统筹者角色。

开发工作流工具技巧AI 编程实践
Stack Overflow 论"Agent = 模型 + 脚手架":LangChain 同模型只改脚手架,在 Terminal-Bench 2.0 上提升 13.7 个百分点
资讯
模型只占一成:Stack Overflow 论"Agent = 模型 + 脚手架",LangChain 只改配置提 13.7 个点

Stack Overflow 工程博客 10 月 7 日刊发编码 Agent 操作系统系列第六篇,核心论点:Agent = 模型 + 脚手架,模型也许只占一成。证据:LangChain 的 coding agent 在 Terminal-Bench 2.0 上从 52.8% 涨到 66.5%(+13.7 个百分点),从 30 名开外冲进前 5,模型完全没换,只改了脚手架。文章开出六件套处方:宪法文件、自主边界、分层 CONTEXT.md、持久记忆、多视角评审团、PostToolUse 钩子;多 agent 协作则靠共享书面状态与 append-only 决策日志。本站判断:模型正在商品化,差异化迁往脚手架,而那九成是你今天就能开始造的东西。

行业趋势开发工作流工具技巧