与 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。看起来没问题了。

最终的半有机修复

然而,在审查这个改动时,我意识到新的修复过于宽泛:fetchgo 都共享这个方法,但只有 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 岁了。随着开发者年龄增长,现实是我们往往会「失去快速球」,至少在某种程度上。

对我来说,这在实践中意味着两件事:

  1. 我无法像以前那样记住那么多东西
  2. 我无法像以前那样长时间工作

事实证明,AI 直接解决了这两个问题。

关于记忆力,虽然我不能记住所有以前能记住的东西,但我可以通过恰当的提示(prompting)很快地重新理解事物。AI 在这方面非常擅长帮助我,它让我在开源项目和工作项目之间切换的效率比没有它时高得多。

关于长时间工作,AI 能够以一种——即便是年轻开发者时的我也很难跟上——的方式进行「苦力型」工作。这意味着,例如,我可以为我的项目拥有比原本更广泛的测试套件。

看看 Claude 在这个案例中生成的测试,比我凭自己精力所能做到的更全面。

所以,AI 解决了我作为年长开发者产生的两个根本性(相对)弱点。

另一方面,我非常担心它也使我整体智力的衰退变得更加容易。这本身是随着年龄增长自然发生的,但 AI 依赖可能加速这个过程。不得不说,回顾这个故事,我对自己在 Claude 上依赖了那么久才自己动手做正确的事感到有点羞愧。这是我仍在试图把握的一个方面。

结论

我写下这一系列交互的故事,是因为我认为它捕捉到了 AI 辅助编码中一些好的方面和一些不好的方面。它展示了让一个有一定能力的人类参与其中与 AI 智能体协作的价值,同时也展示了盲目接受 AI 智能体为解决某个问题给出的第一个(或第二个)方案的危险。

希望这对你自己形成关于 AI 智能体的思考和策略有所帮助。


1这源于我在一场梦境中得到的启示。