回到手写代码
Published: 2026-05-11
一位开发者将他花了7个月 234个commit 完全靠Claude vibe-coding构建的项目存档了 决定回到手写代码
这个帖子在Hacker News首页迅速引发了热议 52分/56分钟前发布的帖子讨论热度正在上升。作者在 blog.k10s.dev 上坦诚地分享了他使用AI编码工具7个月后的反思 以及为什么最终决定放弃AI驱动的开发模式
这不是一篇"AI很烂"的吐槽文 而是一次对AI编码工具隐藏成本的结构化分析 对任何认真使用Claude Code或Cursor等工具的开发者都有参考价值
故事: k10s 和那个吞掉自己的上帝对象
作者构建了 k10s 一个面向NVIDIA集群运维人员的GPU感知Kubernetes TUI仪表盘 全程使用Claude vibe-coding完成。开始的几周像魔法一样 每个功能都又快又干净。基础版的k9s克隆——资源视图 命令面板 实时更新 vim快捷键——大约花了3个周末
然后到了GPU集群视图——k10s存在的核心原因。Claude一次性生成了它 FleetView结构体 tab筛选 分配条 颜色编码的GPU状态 看起来很美
直到作者输入 :rs pods 切回pods视图 什么也没渲染出来 数据是过时的 状态已经损坏了
上帝对象把自己吞掉了
当作者坐下来读 model.go 的时候——整整1690行——他被吓到了。一个结构体里什么都有: UI组件 K8s客户端 每个视图的状态 导航历史 缓存 鼠标处理。Update() 是一个500行的函数 有110个switch/case分支。快捷键有冲突含义: 在logs视图里 s 键是"自动滚动" 在pods视图里是"shell" 在containers视图里是"进入容器shell"——全在一个扁平的switch语句里
作者数了一下 9个手动 = nil 清理行 散落在整个文件中 少写一个 上一个视图的鬼数据就会渗漏到当前视图
从废墟中提炼的五条原则
作者将7个月的痛苦总结为五条教训 对任何认真使用AI编码工具的人都有参考价值
1. AI写功能 不写架构
每次prompt都交付了一个能用的功能。问题是每个功能都是在"让这个模块现在就能跑"的上下文里实现的 完全不知道有49个其他功能和它共享同一个状态。AI优化的是"现在就跑起来" 而不是长期可维护性
解决办法: 在写任何代码之前 先自己设计好架构。把你的接口定义 消息类型 所有权规则写进 CLAUDE.md 或 agent.md。AI会遵守你写下来的规则 但它不会自己发明这些规则
2. 上帝对象是AI的默认产物
AI倾向于把所有东西塞进一个结构体 因为这是满足当前prompt的最小路径。每个新功能加一个新字段 一个新if分支 一个新的特例。用字符串比较来做类型区分(m.currentGVR.Resource == "nodes") 而不是用正确的多态
解决办法: 每个视图应该是独立的struct 实现View接口。每个视图声明自己的快捷键。增加一个视图意味着新建一个文件 而不是修改现有文件
3. 速度幻觉会让你无节制地扩大范围
这是心理上的 也是最危险的陷阱。当每个功能都感觉是"免费"的(AI一个session就搞定了) 你会对一切说yes。作者从一个GPU专用小工具开始 最后搞成了一个通用k9s克隆。架构撑不住这么大规模的增长
解决办法: 写一个愿景文档 明确说你不为谁构建。在CLAUDE.md里写上范围边界。Vibe-coding给你无限的代码行预算 但你的复杂度预算仍然是有限的
4. 位置数据是一颗定时炸弹
AI喜欢把结构化数据展平为 []string 因为这样能立刻喂给任何表格组件。列的身份变成了纯粹的位置:ra[3] 表示"Alloc" 因为注释是这么写的。加一列? 每个排序 每个条件渲染 每个展示都静悄悄地错了。编译器帮不了你 因为全都是 []string
解决办法: 永远不要展平结构化数据。所有数据以类型化struct的形式流动 直到渲染函数。列的身份来自struct字段名 而不是数组索引
5. AI不管理状态变迁
文章展示了一个教科书级的数据竞争: Update() 里生成了一个闭包 在一个goroutine里修改Model字段 而 View() 在主goroutine里读同样的字段。没有锁 没有消息传递。99%的时间里它正常工作 但1%的时间里它会莫名其妙地显示错乱
解决办法: 所有对渲染可见状态的修改都在主循环中发生。后台worker生产数据 通过消息发送回来。主循环收到消息 原子地应用它
什么时候用AI编码 什么时候手写
这篇文章不是要你完全放弃AI工具 而是告诉你什么时候该用 什么时候不该用
| AI编码好用的时候 | 应该手写的时候 |
|---|---|
| 单文件脚本和原型 | 多模块架构设计 |
| 样板代码生成(CRUD 测试 类型定义) | 状态管理和所有权边界设计 |
| 探索性代码——"这个API长什么样?" | 并发和数据流设计 |
| 已知代码库中的熟悉模式 | 新抽象层和接口决策 |
| 一次性工具函数 | 边缘情况下的正确性很重要的代码 |
真正的成本: 认知债务
这个问题和 认知债务 的概念紧密相关——代码承诺的架构和实际结构之间的认知开销的积累。AI生成的代码特别容易产生这种债务 因为它优化的是局部正确性 而不是全局一致性
作者决定 用Rust重写k10s 不是因为Rust本质上更好 而是因为这是他能"掌舵"的语言——他对什么感觉不对的本能反应更敏锐。用他的话来说: "AI交给你看起来合理的代码 你需要有嗅出它是不是垃圾的直觉"
这和最近的 AI生成的PR涌入开源项目 讨论很相似。当开发者提交他们不完全理解的AI生成代码时 审查负担就转嫁给了维护者。个人项目也是同样的模式:"审查"阶段变成了凌晨两点你一个人调试幽灵数据
你的CLAUDE.md里应该写什么
作者防止AI架构崩溃的建议:
- 架构不变量: 每个视图实现View接口。视图不访问其他视图的状态。增加视图不应修改现有视图
- 状态所有权规则: 永远不要把视图特定状态加到App/Model结构体里。每个视图是独立的struct
- 范围边界: 明确说明你不为谁构建。Vibe-coding让一切感觉"便宜"——提前说no
- 数据表示: 永远不要展平结构化数据。所有数据以类型化struct流动直到渲染
- 并发规则: 所有对渲染可见状态的修改在主循环中发生。后台worker发送消息
想了解更多AI编码工作流的结构化方法 可以看看我们的 生产环境编码Agent指南 和 Addy Osmani的Agent Skills 解读
不是反AI 只是诚实面对权衡
这个故事最重要的启示是:它不是反AI的。作者显然是Claude Code和AI编码工具的重度用户 他亲身体验了10倍速度的快感。但他也撞上了每个认真使用AI的开发者最终都会撞上的那堵墙: AI交付功能 你拥有架构
决定回到手写代码不是对AI的否定 而是一种重新校准。用AI擅长的地方(功能 样板代码 探索) 自己掌握架构。当你发现因为每个功能感觉"免费"而对一切说yes的时候 停下来问一句: 架构撑得住吗?
作者正在从零重写k10s——架构手写 实现细节用AI。这是务实的中间地带。正如他所说: "直觉是vibe-coding无法替代的东西"