![]()
如果说这几年 AI 编程令人沉迷的地方,是“几乎不费力就能把东西做出来”,那么真正让人开始警觉的瞬间,往往发生在系统第一次“看起来还在工作,但已经开始悄悄失控”的时候。
这篇来自 k10s 作者的长文,就是从那个临界点开始往回看的。他记录了一次几乎完全依赖 vibe-coding 的开发实验:从一个 GPU 感知的 Kubernetes TUI,到一个功能不断膨胀、最终被自身复杂度吞噬的系统。过程中有过极高的开发速度,也有过 AI 一次性生成完整功能的“爽感时刻”,但同样也埋下了架构失控、状态混乱和隐性 bug 的伏笔。
更重要的是,这不是一篇单纯的“翻车复盘”,而是一份带着具体代码细节、设计选择和反思路径的拆解记录。它试图回答的并不是“AI 能不能写代码”,而是另一个更现实的问题:当 AI 真的参与到软件构建的每一层时,人类到底还需要在哪些地方重新夺回控制权。
原文:https://blog.k10s.dev/im-going-back-to-writing-code-by-hand/
作者 | shvbsle 责编 | 苏宓
出品 | CSDN(ID:CSDNnews)
这件事最开始其实更像一次实验,或者说一个问题——“如果我尽可能不亲自参与开发流程,只靠 AI 来写软件,我到底能走多远?”
话不多说,这篇开发日志最终得出的核心结论是:如果真想做出点有意义的东西,人类依然必须参与其中。
几点感受:
就像 “em-dash(长破折号)” 已经快成 AI 写作的标志一样,“god-object(上帝对象)” 也快成 AI 写代码的典型特征了。
“氛围编程” 会让一切看起来都很廉价,你最后很可能会失去重点,做出越来越臃肿的东西。
架构一定要由人(也就是你)来设计,而不是不停地让 AI 往里加功能。
另外,我还整理了一些 AGENTS.md / CLAUDE.md 的指令配置,它们确实能让我稍微少亲自下场一点。
截至 2026 年 5 月 10 日,人类干预依然是必需的。
这项实验到底验证了哪些东西,以下是完整的过程。
![]()
234 次提交、30 个周末,用氛围编码开始一个项目
这是 k10s GitHub 仓库:https://github.com/shvbsle/k10s/tree/archive/go-v0.4.0
234 次提交,差不多花了 30 个周末。整个项目几乎完全是在“氛围编程”状态下完成的——只要 Anthropic 的 Claude token 额度还没耗尽,我就继续往前做、继续发功能。
现在,我决定把这个 TUI 工具归档,然后从头开始重写。
k10s 最初的定位,是一个“支持 GPU 感知”的 Kubernetes Dashboard,也是我第一次认真尝试用 AI 去开发一个真正复杂的软件。
你可以把它理解成一个面向 NVIDIA GPU 集群的 k9s,服务的是那些真正关心 GPU 利用率、DCGM 指标,以及哪些节点正在空转烧钱(每小时 32 美元)的人。
我用 Go 和 Bubble Tea 写了它,而且它一开始确实能跑。
至少……有一段时间是这样。
过去 7 个月里我学到的东西,比我现在准备删掉的那 1690 行 model.go 更有价值。我觉得,任何认真尝试“vibe-coding”的人,可能都会从这些经验里得到点东西,因为这部分内容其实很少有人讨论——它通常都会被那些炫酷 Demo 和“开发速度暴涨”的故事掩盖掉。
一句话总结:AI 会帮你写功能,但不会帮你设计架构。如果你长时间不加约束地让它“自己运行”,最后只会留下越来越严重的事故现场。开发速度会让你误以为自己一路领先,直到某个瞬间,整个系统开始同时崩塌。
加入 AMD AI 开发者计划,免费领 50 小时云算力券
进群月月抽显卡、AIPC,好运不停!
![]()
“氛围编程”上头的时候
我是在 2025 年 9 月底开始做 k10s 的。
最开始那几周,简直像魔法一样。
我对 Claude 说一句:“加一个支持实时更新的 Pods 视图”,然后“砰”的一下,功能就出来了。
资源列表视图、命名空间筛选、日志流、描述面板、键盘导航……一个个功能都顺利落地。原因也很简单:那时候项目还足够小,AI 还能把整个工程都“装进上下文”里。
最基础的那个 “k9s 克隆版”,我大概只花了 3 个周末。它包含了:
Pods、Nodes、Deployments、Services 的资源视图;
命令面板;
基于 Watch 的实时更新;
Vim 风格快捷键……
全部都能跑,而且几乎全是在单次 session 里“vibe-coded”出来的。我当时的开发速度,可能是平时的 10 倍。那种感觉真的很爽。
然后,我开始做这个项目真正的核心卖点。
k10s 存在的真正原因,其实是 GPU 集群视图(GPU Fleet View)。
我想做一个专门的界面,让你能一眼看到每个节点的 GPU 分配情况、DCGM 利用率、温度、功耗、显存占用。
不是把信息埋在 kubectl describe node 那种输出里,而是做成一个专门设计的表格界面,并且带颜色状态提示:空闲节点显示黄色、繁忙节点显示绿色、GPU 打满显示红色。
![]()
结果 Claude 一次就生成成功了。
我只写了个 prompt,它就直接生成了 FleetView 结构体、GPU / CPU / All 的标签过滤、自定义渲染逻辑、GPU 分配进度条,而且界面看起来还特别漂亮。
那时候我整个人都沉浸在“AI 开发效率爆炸”的快感里。
直到后来,我输入了一句:
:rs pods想切回 Pods 视图。
结果什么都没出来。表格是空的。实时更新停了。
我切换到 Nodes 视图时,它显示的还是 Fleet View 的旧过滤数据。
再切回 Fleet View,标签统计数字又全错了。
那个“上帝对象(god object)”,终于把自己吞噬掉了。
这也是这篇博客标题的来源。也是我第一次真正“人工介入”的时刻。
整整 7 个月里,我一直都在“prompt → 生成 → 发布”,却从来没有真正坐下来认真读过 Claude 写的代码。
我通常只是看一下 diff、确认能编译、再测一下 happy path、然后继续往前做。
但这次不一样了。
这已经不是再写个 prompt 就能解决的问题。系统的基础结构已经坏掉了。
于是,我第一次真正坐下来,开始读 model.go。整整 1690 行代码。我当场头皮发麻。
代码大概是这样的:一个结构体统治一切。
}UI 组件、K8s Client、日志状态、Describe 状态、Fleet 状态、导航历史、缓存、鼠标事件处理……全部塞进了同一个 struct。
而 Update() 方法更夸张:一个 500 行的大函数,里面根据 msg.(type) 分发逻辑,堆了 110 个 switch/case 分支。
也就是从这一刻开始——我不再只是凭氛围进行编码,而是开始真正思考软件工程了。
![]()
![]()
从“事故现场”里总结出的五条原则
这是我花了 7 个月时间,看着 AI 一点点生成、并最终“反噬自身”的代码库后,总结出来的东西。
下面每一条,都是我亲自踩过的坑:
我哪里做错了?
为什么 AI 辅助编程特别容易掉进这个坑?
以及,你到底应该在 CLAUDE.md 或 agents.md 里写些什么,才能提前避免它?
原则一:AI 会写功能,但不会设计架构
每次我让 Claude 加一个功能,它都能做出来,而且完成得相当漂亮。
Fleet View 一次成功,日志流能跑,鼠标支持功能也能运行。
问题在于:每个功能,都是站在“先把当前需求做出来”的角度实现的。它根本不会意识到,系统里还有另外 49 个共享同一份状态的功能。
举个例子,下面这个 resourcesLoadedMsg handler,是每次切换视图时都会执行的代码。
}你看到这个「if msg.gvr.Resource == k8s.ResourceNodes && m.fleetView != nil」条件语句了吗?
这代表 Fleet View 被硬编码进了通用资源加载流程。之后,每新增一个需要“特殊行为”的视图,这里就会再多一个 if branch。
而且每个 branch 还得手动清理对应字段,否则前一个视图的数据就会“泄漏”到下一个视图里。
我后来专门数了一下:这个文件里到底有多少个 = nil 的手动清理逻辑?
m.logLines = nil答案是:9 个。9 个散落在 1690 行文件里的“手动状态回收”。漏掉任何一个,你就会看到前一个视图留下的“幽灵数据”。
这就是没有“视图隔离”时必然发生的事情。而 AI 根本看不到这个架构正在慢慢腐烂。
因为每一次 prompt,它只会关注当前那一条代码路径。
正确做法的是,在写任何代码之前,先自己把架构设计好。
不是那种空泛的设计文档。而是要编写一套有明确的接口、消息类型、状态所有权规则。然后把这些规则写进 CLAUDE.md:
- The App struct is a thin router. It owns navigation and message dispatch. Nothing else.这样 AI 每次收到 prompt 时,都会先看到这些约束。
原则二:“上帝对象”是 AI 默认生成的产物
AI 天生喜欢“一个 struct 管一切”。因为这是满足 prompt 最省事的方法。
但问题会越来越严重。由于没有视图隔离,键盘事件处理最终会变成灾难。
比如下面这个 s 键的实际逻辑:
return m, nil同一个快捷键,在不同视图里有三种完全不同的含义:
在 Logs 里表示“自动滚动”
在 Pods 里表示“进入 Shell”
在 Containers 里表示“进入容器 Shell”
所有逻辑,全塞在同一个巨型 switch 里。
为什么会变成这样?因为我对 AI 说:“给 Pods 加 shell 支持。”
于是它就找到最近的键盘处理逻辑,直接把代码塞进去。
再看 Enter 键。整个 drill-down handler 也是同样的问题。
// ... 25 more lines of generic drill-down ...所有视图都被塞进一个“平铺式 dispatch”。整个文件里,有 20 多处:m.currentGVR.Resource == ...这样的字符串判断。
这里说的不是类型系统、不是抽象接口,只是字符串比较。
这意味着每新增一个视图,你都得改所有 handler。
正确的做法是,你应该在 CLAUDE.md 里明确写下规则:
- Adding a view means adding a file. If your change requires modifying existing views, stop and ask.AI 永远会走“最短路径”——也就是“再加一个 if 分支”。你的工作,是让“最短路径”同时也是“正确路径”。而方法,就是把约束提前写进 AI 每次都会读取的规则文件里。
原则三:开发速度的幻觉,会不断扩大你的项目范围
这一条不是技术问题,是心理问题,也是我觉得最危险的一条。
我一开始做 k10s 时,目标其实很简单,就是设计一个面向 GPU 集群的小众工具,给那些跑训练集群的人用,也就是我自己这种人。
但 “vibe-coding” 会让一切看起来都太便宜了。“哦?Pods 视图一晚上就做完了?”
那顺手再加 Deployments、Services、完整命令面板、鼠标支持、Context 管理、Namespace 管理..
![]()
最后我突然发现:我已经在重新造一个 k9s 了。
一个面向所有人的通用 Kubernetes TUI,因为 AI 让每个功能都显得“像不要钱一样”。
但它其实不是免费的。每个新功能,都是那个“上帝对象”里新增的一条分支。
比如这个 keybinding struct:
}所有视图共用一个扁平化的 keymap。
注释里甚至还得写:“这个快捷键属于哪个视图”;s 同时代表 Autoscroll、Shell;之所以“还能工作”,只是因为 dispatch 里会先检查 m.currentGVR.Resource。
但代价是你已经无法局部理解任何一个快捷键了。
你必须一路追踪整个 500 行的 Update(),才能知道一个按键最终会干什么。
复杂度正在悄悄累积,而 AI 给你的“速度反馈”却一直在告诉你:“你开发得真快。”
正确的做法是:提前写一份 Vision Doc。明确写清楚“哪些用户不是你的目标用户”,然后把项目边界写进 CLAUDE.md。
If a feature request doesn't serve someone running GPU training jobs, reject it.“Vibe-coding” 会让你误以为自己拥有无限开发预算。其实你拥有的,只是“无限代码行预算”。AI 可以无限生成代码,但你的“复杂度预算”仍然是有限的。无论代码写得多快,架构能承受的功能数量始终有限。超过之后,它一定会塌。
CLAUDE.md 里的 scope section,本质上就是:在“速度快感”让你什么都想加之前,提前替自己说“不”。
原则四:位置型数据结构,就是定时炸弹
在 k10s 里,所有资源数据从 Kubernetes API 拉下来之后,都会立刻被“拍平”:
type OrderedResourceFields []string列信息完全依赖“位置”。比如 Fleet View 的排序逻辑:
}ra[3] 表示 Alloc。ra[2] 表示 Compute。ra[0] 表示 Name。全是“魔法数字”。
index 3 为什么代表 Alloc?
唯一的依据,只是一条注释和 resource.views.json 里的列顺序。
}在 Instance 和 Compute 之间加一列?那所有排序逻辑、所有条件渲染、所有写着 ra[2] 或 ra[3] 的地方,都会在不知不觉中失效。编译器根本帮不上忙,因为这些数据全都是 []string。
更糟的是,JSON 配置根本没法表达排序行为、条件渲染或自定义 drill-down 跳转目标,所以这些逻辑最后只能写进 Go 代码里,而代码又硬编码地依赖 JSON 中字段的位置。
AI 会生成这种模式,因为这是从“获取数据”到“渲染表格”的最短路径。一个 []string 可以立刻塞进任何表格组件里,而强类型结构体(typed struct)前期需要更多样板代码(ceremony)。于是 AI 总会选择那条最快的路。结果六个月后,你开始排查为什么排序后,“Name” 列的数据会出现在 “Alloc” 列里。
正确做法是什么?把这条规则写进你的 CLAUDE.md:
- The ONLY place strings are created for display is inside render()/view() functions.这样一来,你定义的强类型结构体(typed struct)就能让“不可能出现的状态”真正变得不可能出现 [2]:
}当列对应的是具名字段时,你就不可能排错列。你也不可能误把 Alloc 字段的字符串当成名字去比较。编译器会替你强制保证这些约束。
而 AI 永远会倾向于选择 Vec ,因为它能更快满足提示词要求。你在 CLAUDE.md 里写下的规则,本质上就是在把“强类型”这条路,变成阻力最小、最容易被 AI 采用的默认路径。
原则五:AI 不应该掌控状态变更。
Bubble Tea 的架构里有一个很漂亮的理念:Update() 是唯一允许状态发生变化的地方,而且所有状态变更都由消息驱动。但 k10s 违背了这一点。
updateTableMsg 处理器启动了一个闭包,在 goroutine 内部修改 Model 字段:
}这个返回函数(tea.Cmd)会被 Bubble Tea 放到独立 goroutine 执行。它里面调用了 m.updateColumns(m.viewWidth) 和 m.updateTableData() 函数。这些函数会同时读写 m.resources、m.table、m.viewWidth。
与此同时,主线程里的 View() 也在读取同一批字段。没有锁。没有 mutex。
<-m.updateTableChan 虽然会阻塞 goroutine,直到收到更新信号,但它根本无法阻止 View() 在“状态只更新到一半”的时候进行读取。
这是教科书级别的数据竞争(data race)。99% 的时间它看起来都正常。剩下 1% 的时间,它会以一种极其诡异的方式损坏 UI。严重到让我一度怀疑自己精神出了问题。
AI 为什么会生成这种代码?是因为“直接在 closure 里修改状态”是最快能跑起来的方案。
而正确的消息传递架构是 worker 发消息、Update() 收消息、主循环统一修改状态,需要更多类型、更多 plumbing。
AI 不会为了并发正确性去做这些。它只会为了“当前 prompt 能跑”去优化。
正确的做法是,所有对“影响渲染的状态”的修改,都必须发生在主循环里,仅此一条原则,没有例外。后台 worker 只负责产生数据,然后把数据以 message 的形式发送出去。主循环接收 message,再统一应用状态变更。在并发 UI 代码里,这是唯一不能被打破的规则。
}不要共享可变状态。不要数据竞争。不要“99% 情况下能跑”。
把这些规则写进 CLAUDE.md。
- If you need to update state from async work, define a new AppMsg variant.如果 AI 默认不会这样写代码。那规则文件就必须强制它只能这样写。
![]()
我现在准备怎么做
我正在用 Rust 重写 k10s。不是因为 Rust “更强”。而是因为——这是我真正“能掌控”的语言。我已经写了足够多的 Rust,以至于很多时候,代码哪里不对劲,我甚至在还没说清原因之前,就已经能本能地察觉到。而这种“直觉”,恰恰是 vibe-coding 永远替代不了的东西。AI 会给你一份“看起来很合理”的代码。但你必须自己具备一种嗅觉:知道它什么时候其实是一坨垃圾。
另一个变化则更简单:这一次,我会亲手完成设计工作。而且是在写任何代码之前。不是那种空泛的大纲文档。而是拥有明确的接口、消息类型、所有权规则。
之前那些总被 AI 做错的架构决策,现在我会在第一条 prompt 发出去之前,先白纸黑字地写清楚。
至于这样做,最终能不能避免这次重构再次“被自己的复杂度压垮”……拭目以待!
从“拥抱 AI”到“AI 原生”,我们正站在生产力变革的奇点。
由 CSDN 与奇点智能研究院联合举办的「2026 全球产品经理大会」将于7 月 17-18 日在北京正式召开。本次大会精心设计了十二个深度专题,旨在通过最前沿的实战案例,拆解 AI 原生时代的进化密码。
目前大会正式开启演讲议题与优质分享嘉宾招募。
你的每一次真实分享,都在为 AI 原生时代的产品实战手册添砖加瓦。
我们在北京,期待听见你的声音。
议题 & 嘉宾推荐/自荐方式:hemiao@csdn.net
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.