Reed's News
← 返回精选

与AI协作:一个具体案例分析

AI 61 comma_at 2026/6/29 1833 字 原文 ↗

我对AI的态度总体上是矛盾的。毫无疑问,过去一年里它已成为开发领域极具威力的工具,但也带来诸多隐患——对个人而言,它会慢慢钝化我们的思维;对整个社会来说,则引发环境担忧、个人计算成本攀升等问题。

在《代码(更)廉价》https://htmx.org/essays/code-is-cheap/一文中,我曾警示过"魔法师学徒"问题:开发者过度依赖AI,最终无法理解并妥善解决自己搭建的系统中出现的问题。

本文中,我将结合自己维护hyperscripthttps://hyperscript.org时与AI的一次具体互动,剖析AI的优劣,尤其要展现我险些陷入的"魔法师学徒"困境。

先做个背景介绍:hyperscript是一款面向网页的另类解释型脚本语言,颇具讽刺意味的是,它完全用JavaScripthttps://github.com/bigskysoftware/_hyperscript/blob/master/src/_hyperscript.js编写。

这款软件的设计十分出格:我在开发时刻意打破诸多解析规则,想以此做个实验,看看结果会如何。

举几个例子:

我不建议大多数编程语言采用这种思路,但对这个项目而言,效果意外地不错。这再次证明,软件开发领域确实条条大路通罗马。

故事始于一位用户反馈:升级到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的表现就逊色不少。我承认当时自己犯懒,直接让AI给出解决方案,现在吐槽这些方案似乎有点五十步笑百步,但整个过程颇具参考价值,不妨详细说说。

AI给出的第一个方案是:优先解析"类字符串"叶子节点,解析失败再回退到完整表达式:

return this.parseElement("stringLike") || this.requireElement("expression");

这个方案能解决用户反馈的具体问题,但局限性很强,无法覆盖通用场景——比如用变量作为fetch目标的情况:

fetch $url as JSON

因此我否决了这个方案:太像临时补丁,通用性不足。 (顺带一提,hyperscript解析器本身就有不少"原生"补丁,这么说可能有点自相矛盾。)

第二个方案更有意思:给解析器加一个noConversions标记,在解析URL时启用该标记,让AsExpression.parse在标记生效时直接返回:

// AsExpression.parse()
if (parser.noConversions) return;

很多解析器工程师看到这个方案可能会崩溃,因为它会让hyperscript解析器变成上下文敏感的类型。

挺好的。毕竟hyperscript解析器本来就是上下文敏感的。

仔细思考这个方案后,我意识到我们其实已经有一套现成的上下文敏感机制,完全不用新增标记,但Claude没注意到。

hyperscript解析器中有"follows"https://github.com/bigskysoftware/_hyperscript/blob/ea9a6534d24cf5c7257adcaad75ee75b0c612d8e/src/core/tokenizer.js#L11的概念:即被"上层"解析元素"占用"的标记词。作为一款(有点奇怪的)递归下降解析器,它允许解析元素(通常是命令)"占用"某个关键字,这样表达式解析时就不会匹配该关键字。

比如when特性中,or就被用作分隔符,而非逻辑连接词:

<div _="when $x or $y changes put it into me"></div>

(我仿佛听到不少解析器工程师气得关掉了页面,挺好的。)

这套机制刚好能满足我们的需求:不用给解析器加新标记,只需在解析表达式前将as加入follows列表,解析完成后再移除,就能阻止AsExpression解析,同时不影响变量等常规表达式的正常工作。

我把这个思路告诉Claude,它兴奋地回复我"完全正确!",随即用这个方法编写了修复代码。Claude在parseURLOrExpression()中添加了正确的代码,既通用地解决了问题,又没有新增解析器机制。

看起来没问题了。但我在审核代码时发现,这个修复的范围太广:fetchgo命令共用parseURLOrExpression()方法,但只有fetchas作为修饰符。现有方案会同时阻止go命令中完全合法的as转换表达式。

于是我自己在FetchCommand#parse()中实现了最终修复:

parser.pushFollow("as");
try {
var url = parser.parseURLOrExpression();
} finally {
parser.popFollow();
}

if (parser.matchToken("as")) {
...

这样就把特殊处理的范围限定在了fetch命令,不会影响go命令的解析。这就是最终的修复方案。

修复过程中,我还让Claude生成了多组测试用例。hyperscript已有完善的测试套件,Claude生成的测试用例小巧且针对性强,既能复现问题,也能验证修复效果。这又是AI擅长的领域。

这个看似平淡的bug修复故事,有什么值得关注的地方?

我认为它清晰展现了AI的优势——定位问题、生成测试,也暴露了它的短板——难以给出简洁优雅的解决方案。如果我不熟悉hyperscript解析器的底层机制,很可能会引入技术债务:比如新增一个解析补丁,或是给解析器加个新状态等等。

我(毫无根据地)断言,技术债务会呈指数级增长,因此在项目中必须尽可能减少。这个案例说明,让熟悉底层架构的人类与AI协作,比让AI独自处理更能有效控制复杂度。

有人看到hyperscript的代码库,可能会嗤之以鼻,觉得它从一开始就没考虑过复杂度控制。我理解这种看法,但在这个案例中,我们能具体看到,一位熟悉技术的开发者与AI协作,至少在修复这个略显尴尬的bug时,成功遏制了复杂度的增长。

我没有做"魔法师学徒"——盲目接受AI给出的方案,而是尝试做"魔法师"(这么说可能有点自大),要求AI给出更贴合现有代码架构的正确方案。我理解问题本质,知道正确的解决方向,借助AI实现方案,再用AI生成的测试验证效果。

这与当前流行的"氛围驱动开发"形成鲜明对比:有些开发者以不懂底层原理为荣。

回顾这次经历,我还意识到另一点:我今年50岁了,属于资深开发者。随着年龄增长,开发者多少会有些"力不从心",对我而言具体体现在两方面:

  • 记忆力不如从前
  • 无法像年轻时那样长时间工作

而AI恰好能弥补这两个短板: 在记忆力方面,虽然我记不住所有细节,但通过恰当的提示,能借助AI快速重新理解问题。这让我在开源项目和工作项目间切换时,效率比之前高得多。 在工作时长方面,AI能持续高强度工作,哪怕是年轻时的我,也很难跟上它的节奏。比如,现在我的项目能拥有比之前更完善的测试套件——Claude这次生成的测试用例,就比我自己能攒出来的更全面。

AI弥补了我作为资深开发者的两大相对劣势。但另一方面,我也非常担心,它会加速我整体思维能力的退化——毕竟衰老本身就会导致认知衰退,而依赖AI可能会让这个过程更快。回顾这次修复,我有点惭愧:我依赖Claude太久,才终于下定决心自己动手做正确的事。这是我至今仍在摸索平衡的问题。

我写下这段与AI的互动经历,是因为它展现了AI辅助编码的利弊:既体现了懂行的开发者与AI协作的价值,也暴露了盲目接受AI方案的风险。希望能帮助你形成自己对AI工具的思考和使用策略。

——

1 这个结论是我在梦里得到的。