与 AI 协作编程:一个具体案例
Carson Gross · 2026年6月29日
总体而言,我对 AI 持矛盾态度。毫无疑问,在过去一年里它已经成为开发领域一个非常强大的工具,但它也伴随着许多危险,既针对我们个人(比如智力的缓慢钝化),也针对集体(比如环境问题、日益昂贵的个人计算等)。
在《代码变便宜了》(“Code is Cheap(er)“)一文中,我警告过「魔法师学徒」问题——开发者过度依赖 AI,无法理解并妥善解决他们所构建系统中出现的问题。
在这篇文章中,我想通过一个在维护 hyperscript 时与 AI 交互的具体案例,来展示 AI 的优势和劣势,并特别演示「魔法师学徒」问题(我险些中招)。
Hyperscript 解析器
先说点背景知识。hyperscript 是一种面向 Web 的替代性解释型脚本语言。讽刺的是,它完全用 JavaScript 编写。
它是一个相当奇怪的软件:在编写它时,我故意打破了许多解析规则,把它作为一个实验,看看结果会如何。
举几个例子:
- 解析逻辑与解析元素放在一起
- 解析器是可插拔的,语法是动态定义的
- 支持多种属性访问语法
对于大多数编程语言来说,我不会推荐这种方案,但这个项目用起来效果相当不错。这再一次证明了软件开发中确实有多种解决问题的方法。
Bug 报告
故事始于一个用户反馈升级到 0.9.91 版本后出现回归。以下表达式不再能正确解析:
fetch `{% url 'trade:get_symbol_data' %}?symbol=${symbol}` as JSON
具体来说,as JSON 绑定得过紧,试图在将字符串字面量传给 fetch 之前就将其转换为 JSON,而不是像用户期望的那样(也是之前的正确行为)——获取指定 URL 并将结果按 JSON 处理。
这种绑定冲突是解析中的经典问题。由于 hyperscript 是一种 xTalk 风格的语言,继承了英语的许多歧义性,这个问题在它身上尤为严重。
调查原因
首先需要调查为什么会发生这个回归。在这方面我通常会倾向于靠 AI 帮忙。
我使用 Claude,它在找到根本原因方面做得相当不错:在 0.9.91 版本中,我过于激进地重构了 go 命令来复用/共享 fetch 命令的逻辑。我为这两个命令提取了一个公共方法 parseURLOrExpression(),但这样做时,我不小心将 fetch 命令之后的语法扩展为包含了通用表达式的解析。
as 关键字在表达式中有其含义:它是一个转换表达式,允许你在类型之间转换:
set x to "42" as Int
但 as 关键字同时也是 fetch 命令的一个修饰符,告诉它如何转换响应:
fetch https://hyperscript.org as Text
(或许这个事实让你有点反胃。很好。)
问题的关键在于,重构时我无意中让解析器在 fetch 关键字之后解析表达式,现在表达式把 as 关键字当作表达式的一部分消费掉了,而不是让它作为 fetch 的修饰符。
在 Claude 的帮助下,我几分钟内就搞清楚了这一点,比我自己琢磨要快得多。
修复问题
AI 在找到问题原因方面非常有用。然而在修复问题时,它的表现就差多了。
我得承认,我在这里是偷懒了,直接让 AI 给出解决方案,所以抱怨那些方案质量不高感觉有点……懒,但我仍然认为这一系列事件很有启发性,所以让我们来看看具体发生了什么。
修复方案 1:一个 hack
第一个建议是先解析所谓的「类字符串」叶子节点,然后回退到完整表达式:
return this.parseElement("stringLike") || this.requireElement("expression")这个修复可以解决用户报告的当前问题。但是它过于针对这个具体 bug,无法解决一般情况,比如有人用变量作为 fetch 的目标:
fetch $url as JSON
我拒绝了这个方案,因为:太 hacky,不够通用。
(注意,hyperscript 解析器本身就包含了很多有机长出来的 hack,所以这可能是五十步笑百步。)
修复方案 2:更好,但不必要的复杂性
第二个建议更有趣:在解析器上添加一个 noConversions 标志,在 URL 解析时设上它,让 AsExpression.parse 在标志设置时直接退出:
// AsExpression.parse()
if (parser.noConversions) return这会让很多解析器工程师感到惊恐,因为它使 hyperscript 解析器变成上下文敏感的。很好。hyperscript 解析器本来就是上下文敏感的。
在看到这个修复方案后,我想了一秒钟就意识到,我们其实已经有现成的上下文敏感基础设施,不需要给解析器引入新标志——但 Claude 漏掉了这一点。
Hyperscript 解析器中的 “Follows”
在 hyperscript 解析器中,我们有一个「follows」概念,即被「上层」解析元素声明占用的 token。
hyperscript 解析器是一个(有些奇怪的)递归下降解析器,这让一个解析元素(通常是命令)可以「声明占用」一个关键字,表达式在解析时就不会匹配到它们。
举个例子,when 特性使用 or 作为分隔符,而不是逻辑连接符:
<div _="when $x or $y changes put it into me"></div>(我能听到很多解析器工程师愤怒地关掉这个页面的声音。很好。)
事实证明,这个特性可以用来实现我们想要的效果:与其给解析器添加新标志,我们可以将 as 压入 follows,然后解析表达式,最后将其弹出 follows。这会阻止 AsExpression 的解析,同时仍然允许大多数通用表达式(如变量)正常工作。
修复方案 3:接近,但差一点
我把这一点告诉了 Claude,它带着一阵兴奋告诉我「完全正确!」,然后开始使用这个技术修复 bug。
Claude 在 parseURLOrExpression() 中添加了正确的代码,无需引入任何额外的解析器基础设施就解决了一般的 case。看起来没问题了。
最终的半有机修复
然而,在审查这个改动时,我意识到新的修复过于宽泛:fetch 和 go 都共享这个方法,但只有 fetch 使用 as 作为修饰符信号。现有的修复同时也阻止了 go 命令中完全合法的 as 转换表达式的使用。
所以我亲自实现了最终修复,在 FetchCommand#parse() 中:
parser.pushFollow("as");
try {
var url = parser.parseURLOrExpression();
} finally {
parser.popFollow();
}
if (parser.matchToken("as")) {
...
}这里我把特殊情况限制在了 fetch 命令上,让 go 的解析不受影响。这最终成为了我对此 bug 的最终答案。
测试
在这个过程中,我让 Claude 为各种情况生成了一些测试。hyperscript 有一个不错的现有测试套件,Claude 做得很好,创建了小巧、聚焦的测试来展示问题并验证修复是否正常工作。这似乎是 AI 表现良好的另一个领域。
这个故事的寓意
好吧,这个相当平淡无奇的 bug 修复故事有什么有趣之处呢?
我认为有趣的地方在于,看到 AI 在哪些方面做得好——即调查和测试创建——并将其与它做得不好的方面进行对比:想出一个干净的解决方案。
如果我不熟悉 hyperscript 解析器及其基础设施,这个修复很可能导致项目中技术债务的积累:又一个 hacky 的解析边界情况、又一点解析器上的状态位,等等。
我无需证据地断言1,技术债务是指数级增长的,因此在项目中最小化它至关重要。
这个故事展示了——让一个人类参与其中,与智能体协作并深刻理解底层基础设施——在控制复杂性方面,比让智能体自行其是要有效得多。
有些人会看着 hyperscript 的代码库,嘲笑说控制复杂性从来就不是它考虑的问题。我对此表示理解。然而,在这个例子中,我们可以看到在修复一个承认尴尬的 bug 时,一个懂行的人类与 AI 智能体协作,是如何至少在一定程度上抑制了复杂性的增长。
在这种情况下,我不是作为魔法师学徒盲目接受 AI 提出的方案,而是作为魔法师(我希望这样说不会太傲慢!)要求一个更符合现有代码库架构的正确方案。我理解了问题,看清了正确的解决方案,能够与 AI 协作实现它,然后在 AI 生成测试的帮助下验证了方案。
我希望这与当前一些被推动的「氛围编程」(vibe coding)形成良好对比——后者中开发者(或随便称为什么)似乎以不理解实际发生了什么为荣。
附记:AI 与年长开发者
在回顾这段经历时,我还想到了另一件事。
我是一位年长开发者,今年 50 岁了。随着开发者年龄增长,现实是我们往往会「失去快速球」,至少在某种程度上。
对我来说,这在实践中意味着两件事:
- 我无法像以前那样记住那么多东西
- 我无法像以前那样长时间工作
事实证明,AI 直接解决了这两个问题。
关于记忆力,虽然我不能记住所有以前能记住的东西,但我可以通过恰当的提示(prompting)很快地重新理解事物。AI 在这方面非常擅长帮助我,它让我在开源项目和工作项目之间切换的效率比没有它时高得多。
关于长时间工作,AI 能够以一种——即便是年轻开发者时的我也很难跟上——的方式进行「苦力型」工作。这意味着,例如,我可以为我的项目拥有比原本更广泛的测试套件。
看看 Claude 在这个案例中生成的测试,比我凭自己精力所能做到的更全面。
所以,AI 解决了我作为年长开发者产生的两个根本性(相对)弱点。
另一方面,我非常担心它也使我整体智力的衰退变得更加容易。这本身是随着年龄增长自然发生的,但 AI 依赖可能加速这个过程。不得不说,回顾这个故事,我对自己在 Claude 上依赖了那么久才自己动手做正确的事感到有点羞愧。这是我仍在试图把握的一个方面。
结论
我写下这一系列交互的故事,是因为我认为它捕捉到了 AI 辅助编码中一些好的方面和一些不好的方面。它展示了让一个有一定能力的人类参与其中与 AI 智能体协作的价值,同时也展示了盲目接受 AI 智能体为解决某个问题给出的第一个(或第二个)方案的危险。
希望这对你自己形成关于 AI 智能体的思考和策略有所帮助。
1这源于我在一场梦境中得到的启示。