💡 Claude Code 团队工程师 Adam Wolff 分享了一个案例研究,展示了团队如何使用 Claude Code 本身来构建 Claude Code。他讲述了开发过程中的三个故事:重建输入光标类、重新设计用于并行命令执行的 Shell,以及一个使用 SQLite 实现对话持久化的失败实验。核心论点是,AI 已将软件开发的瓶颈从“实现”转移到了“发现真实需求”。
🚀 序言
我是 Adam Wolff,Claude Code 团队的一名工程师。今天我想聊的不是如何使用 Claude Code,而是我们如何使用 Claude Code 来构建 Claude Code。这是一个关于当你把 Claude Code 交给一个团队,并围绕它重新组织项目时会发生什么的案例研究。
当我和同事 Faye 讨论这个话题时,我原本想说 Claude Code 有多简单,以及这种简单如何让我们跑得飞快。但在深入挖掘我们的代码仓库后,我发现这些故事一点也不简单。相反,它们非常复杂。
关键不在于事情是简单还是复杂,而在于 AI 如何改变了软件开发生命周期(SDLC)中的瓶颈。 过去,“实现”是最大的环节,你需要花大量时间预先弄清楚要构建什么以及如何构建。而现在,更好的做法通常是直接冲锋,让用户和开发过程告诉你该构建什么。
在接下来的三个故事中,请关注以下几点:
为什么我们必须通过发布来发现真实需求? 哪些架构选择在最后真正起到了作用? 我们如何知道何时该硬着头皮顶住痛苦,何时该果断回头?
🎭 第一集:重建输入(Rebuilding Input)
首先讲讲 Claude Code 中的光标(Cursor)类。我选了一张计算尺的照片,因为 Cursor 这个词最初就来源于计算尺上移动的部分。
遇到的挑战
我们希望在用户输入特殊字符时实现各种特殊行为,比如输入斜杠命令(/)弹出菜单,或者使用 @ 符号提及文件名。我们需要拦截每一个按键。
传统的观念是:千万不要尝试重新发明输入(Input)。如果你用过 React 的受控组件处理文本输入,你就会知道其中的坑。用户期望的行为太多了(如 Ctrl-A 回到开头,Ctrl-E 到末尾,Ctrl-K 删除等),而这些在基础库里并不总是内置的。
我们的做法
我们决定构建一个虚拟光标类。
我们实现了自己的自动换行(Word Wrapping)算法(这其实很难,需要回溯)。 这个类大约 300 行代码,最重要的是它是完全可测试的。 它是**不可变(Immutable)**的,采用了流式接口(Fluent Interface)。
两个月后,当我们准备外部发布时,我的同事在一次 PR 中就实现了 Vim 模式。这在传统的 Readline 库中几乎是不可能轻松实现的。正是因为我们有了全面的测试覆盖,AI 才能在运行测试的同时不断调整实现,开发速度极快。架构选择在这里得到了巨大的回报。
Unicode 的“诅咒”
然而,随着国际用户的加入,我们终于明白了为什么大家说不要重建输入。答案只有一个词:Unicode。
在 Unicode 中,代码点、字符和列宽之间并没有简单的对应关系。有些字符是双倍宽度的,有些是组合字符。
- 字形聚簇(Grapheme Clustering)
:我们必须引入它来正确识别单词边界。 - 规范化(Normalization)
:我们必须将所有内容规范化为 NFC 格式,以处理像重音符号这样的组合字符。
AI 在这种大规模重构中表现得像个超级明星。虽然这又是几百行代码和无数测试,但在 AI 的辅助下,这一切都进行得非常顺利。
性能优化
随着 Claude Code 后台任务增多,输入开始变慢。因为我们每按一次键都在运行一堆 JavaScript。我的那位实现了 Vim 模式的朋友再次出马,进行了一次“宅男式”深度优化(Nerd Sniped)。
他通过延迟计算(Lazy Computation),只在渲染时才进行必要的布局计算。少做工是唯一的真实优化。
结论: 这是一个巨大的胜利。我们做了一件“不该做”的事,虽然发布了很多 bug,但我们修复得很快,并获得了对输入的完全控制。
🐚 第二集:重新设计 Shell(Reimagining Shell)
Claude 使用的是 Bash 工具。最初,我实现了一个极其简单的类,叫 Persistent Shell(持久化 Shell)。
串行的瓶颈
起初我认为,用户在终端输入,Claude 也应该一样。所以它一次运行一个命令,等待输出,再运行下一个。 但当我们开始构建 Batch Tool(批量工具)以实现并行执行时,持久化 Shell 成为了瓶颈。并行是提升智能体(Agent)性能的关键。
转向瞬时 Shell(Transient Shell)
我们删除了辛辛苦苦写的持久化 Shell,换成了瞬时 Shell。 但在 Node.js 中实现这一点很痛苦。Node 模拟了许多系统调用,导致我们无法像在 C 或 Python 中那样灵活地处理管道(Pipes)和文件描述符。
最糟糕的是,我们丢失了用户环境。 没有了 .bash_profile、.zshrc,也没有了别名(Aliases)和环境变量的持久性。我开启这个功能仅一天,用户就炸锅了:“你不能这么干!”用户的 Shell 配置是不可协商的。
环境快照(Snapshot)
我们必须在瞬时 Shell 中恢复用户环境。我们提出了**快照(Snapshot)**的概念:
启动一次用户 Shell,捕获所有别名、函数和环境变量。 每次启动新命令时,重新播放这个脚本。
Bash 和 Zsh 的各种古怪特性(比如 Heredoc、复杂的别名嵌套)让这个过程极其艰辛。我们花了三周时间才打磨好这个方案。
结论: 最终,快照方案虽然复杂,但它是可组合的(Composable)。这种模块化的复杂度远好于纠缠不清的“乱麻”复杂度。这让我们后来能轻松集成**沙箱(Sandboxing)**功能。
📉 第三集:回滚 SQLite(Reversing SQLite)
这是一个失败的实验。我们曾尝试用 SQLite 代替 JSONL 文件来存储对话记录。
为什么选择 SQLite?
- 数据库才是存数据的地方
:受传统观念影响,觉得文件存储不专业。 - SQLite 的声誉
:它测试严密,无处不在。 - Drizzle ORM
:提供了优雅的模式定义和迁移(Migration)工具。
惨痛的 15 天
- 第一天
:刚合并就导致开发环境崩溃,因为依赖问题。 - 一周后
:GitHub 上全是报错。SQLite 是 原生依赖(Native Dependency)。在 npm 生态中分发包含原生组件的应用是个噩梦。在某些 OS 上,它甚至尝试用 node-gyp现场编译。 - 压死骆驼的稻草
:当我们需要支持多进程时,发现 SQLite 的锁定机制(Locking)极其复杂。它锁定的是整个数据库,而不是单行。在开发者工具这种场景下,可用性(Availability)远比一致性(Consistency)重要。
迁移的噩梦
在 SQLite 中,你不能直接给表添加约束(如 Foreign Key)。你必须关闭约束、重命名旧表、创建新表、迁移数据。在用户机器上进行这种操作,且没有监控,风险极大。
结论: 我们回滚到了 JSONL。它虽然不完美,但它在任何地方都能工作,且不会让程序启动失败。如果 AI 实验不包含失败,那说明实验得还不够多。
🎯 总结与反思
在 AI 时代,工程实践发生了哪些变化?
- 瓶颈转移
:实现成本正在趋向于零。过去我们花大量时间写设计文档和需求,是因为实现很贵。现在,发现真实需求成了新的瓶颈。 - 发布即学习
:最核心的竞争优势是你学习的速度,而不是代码产出的速度。 - 架构决定成败
: 好的架构(如 Cursor 的自包含、Shell 的可组合)能让 AI 发挥最大威力。 错误的决策(如在分发工具中使用原生依赖)会抵消 AI 带来的所有收益。
笔者的建议
- 优化交付节奏
:争取实现持续部署(CD)。缩短反馈循环是最强有力的杠杆。 - 拥抱可逆性
:使用功能开关(Feature Flags)和模块化架构,让回滚变得简单。 - 先发布,后编辑
:不要在路线图上纠结太久。直接把功能推出去,然后根据反馈进行修剪。
💬 Q&A 精选
Q:你们在发布前没发现 SQLite 的问题吗?Adam: 我们的测试环境有限。在 Anthropic 内部很难模拟所有用户的 Windows/Linux 环境。原生依赖的坑只有在大规模分发时才会真正显现。
Q:你和 AI 的协作关系是怎样的?Adam: 技巧在不断进化。我发现,如果你在给需求的同时给出约束和原因,输出质量会好得多。此外,如果发现对话陷入“这行不通”的死循环,最好的办法是结束对话,换个新思路重来。
🖋️ 笔者锐评
读完 Adam Wolff 的分享,最受启发的一点是:AI 时代,软件开发的“重资产”已经从“代码实现能力”转变为“架构设计与需求定义能力”。
在过去,一个复杂的 Unicode 处理逻辑或 Shell 环境捕获方案可能需要资深工程师闭关数周;而现在,AI 可以在几分钟内给出原型。这导致了一个有趣的现象:我们可以通过“快速犯错”来探索正确答案。
反观国内的开发环境,很多团队仍深陷于“事无巨细的 PRD”和“漫长的评审会”中。如果你的团队已经引入了类似 Claude Code 或 GitHub Copilot 的工具,却依然保持着旧时代的敏捷流程,那其实是在用法拉利拉板车。
正如文中所说,如果你的实验从来没有失败过,那说明你实验得还不够多。 在 AI 的加持下,我们应该更有勇气去挑战那些“不建议做”的事情。
求点赞 👍 求关注 ❤️ 求收藏 ⭐️你的支持是我更新的最大动力!
夜雨聆风