1 小时 vs 30 个周末,写出来是同一种代码
今天下午 4 点 57 分,我让 Claude 一口气写了个叫 md-stat 的命令行工具。
14 个子命令:add / list / stats / words / headings / tags / search / export / history……全是 Markdown 文件统计相关的功能。
约束很简单:单文件、不抽象、按功能顺序加、写完能跑就行。
一小时后我拿到 429 行 Python。每个命令都能跑,所有 case 都对,连边界都处理了。
然后我把这 429 行往下翻了一遍。
if target.isdigit() else os.path.abspath 这一行——把用户输入解析成文件路径或者列表索引——出现了 9 次。
「索引越界检查 + 报错 + return」9 次。
「path not found, did you forget to add it?」这个 print 9 次。
scan_history.append({"op": ..., "ts": ...}) 散落在 12 个 handler 里,每个 handler 自己拼字典自己 append。
save_state()——也就是把全局状态写盘——调用了 16 处,没有任何一处是统一入口。
文件顶部 8 个 module-level 全局可变变量:tracked_files、scan_history、current_file_cache、current_file_path、last_search_results、last_export_path、last_op_time、verbose。
函数里 global X 声明 7 次。
main() 里 15 个 if/elif 分发分支,加一个命令就在这里再续一个 elif。
这是教科书 god object——状态归属混乱、特殊情况堆积、单文件吞下所有职责。
但没有任何一个单独的决策是错的。
第一次写 if target.isdigit() 的时候,它是最直观的 idx 解析;第二次写也合理;第九次……没人会停下来说"等等我应该抽个 helper"。
第一次 scan_history.append,副作用就埋在这个 handler 里,干净利落。第十二次,整个项目已经没有"状态写入的中心"了,但每一处单看都合理。
<figure><img src=“images/01-speed-vs-architecture.png” alt=“01-speed-vs-architecture”></figure>
之所以今天下午跑这个实验,是因为 HN 头条挂着一篇 886 分的文章。
k10s 这位老哥用 Go 写了 7 个月 Kubernetes 可观测 TUI,234 个 commit,约 30 个周末,全程 AI 辅助。
上周他第一次完整读了一遍自己的 model.go——1690 行。Update 函数里 110 个 switch/case。20+ 处 if m.currentGVR.Resource ==。9 处 nil 清理是手写的。后台 goroutine 直接改 UI 没有 mutex。
他自己的话是"震惊"。然后他写了一句被疯转的:
AI writes features, not architecture.
那篇文章下面 1100 条评论吵成一团。有人说这是 LLM 偷懒,有人说是作者不 review,有人说"你不 prompt 它当然写不好"。
我那 429 行就是为了回答这件事。
没人催我"快写",我也没让 Claude 草率。约束就是"单文件、按功能顺序加"——一个普通人在没经验时会自然写出的方式。
一小时后拿到的代码,是 k10s 那 1690 行的同构微缩版:9 次重复 ≈ k10s 的 20 次重复,15 个 elif ≈ k10s 的 110 个 case,8 个全局变量 ≈ k10s 的 god struct。
形态完全相同,只是规模差了 30 倍。
那句"AI writes features, not architecture" 在我看来描述的是这样一件事:当你只给 LLM 一个特性的局部上下文时,它每次都给你局部最优解;N 步局部最优叠加起来,就是全局烂。
这跟你 prompt 写得多用心没关系。这跟模型有多强也没关系。
这是"按功能顺序快速实现"这个模式本身的终点。
god object 不是 234 个 commit 才出现的。我那 429 行测下来,第 5 到第 10 个功能时它就开始成形了。
等你察觉的时候,重构成本已经高于重写。
那能怎么办?老办法。
写 5 个功能就停一次,让 LLM 自己 review 自己——“你刚才写的这些 handler 里有什么重复模式?应该抽出来吗?module-level globals 应该收进一个 State class 吗?”
它能看出来。它一开始没主动做,是因为没人让它做。
或者更直接:把架构约束写进 CLAUDE.md / AGENTS.md。
比如"全局状态必须收口到 State 类,handler 不允许直接读写 module-level 变量;任何 dispatch 超过 5 个分支必须改 registry pattern"。
一行硬规则胜过 100 次复盘。
<figure><img src=“images/02-claude-md-template.png” alt=“02-claude-md-template”></figure>
我那 14 个命令的 md-stat 跑得很好。
但下周再加 5 个命令,main() 就是第二个 god 函数。再加并发,8 个全局变量就是 8 个 race。
k10s 老哥 30 个周末才意识到这件事。我 1 小时就看到了——只是因为我专门去数了。
代码出问题,不是从某一行错的开始的。是从没人停下来数一遍开始的。