拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

与 AI 协作修复解析器 Bug:一次 hyperscript 上下文敏感解析问题的完整复盘

与 AI 协作修复解析器 Bug:一次 hyperscript 上下文敏感解析问题的完整复盘
  • 前端

【免费下载链接】htmx

htmx - high power tools for HTML

项目地址:https://gitcode.com/GitHub_Trending/ht/htmx
点击查看免费下载

本文是 htmx 项目维护者 Carson Gross 撰写的实战复盘:以 hyperscript 解析器在一次版本升级后出现的as关键字绑定回归为例,完整还原了"AI 定位根因 → AI 给出三版修复方案 → 人类工程师最终收敛出正确方案 → AI 生成测试"的全过程。读者可以从中掌握 hyperscript 解析器"follows 机制"的底层原理,理解上下文敏感解析的取舍,并形成一套"人类在环(human-in-the-loop)+ AI 代理"的可控协作方法论。

引言:为什么值得复盘一次"平淡无奇的 Bug 修复"

作者在文中坦言,自己对 AI 的态度是"普遍矛盾"的:一方面,过去一年 AI 无疑已成为非常强大的开发工具;另一方面,它也带来诸多风险——对个人而言是智力的缓慢钝化,对集体而言是环境成本、越来越昂贵个人计算设备等。在此前发表的 Code is Cheap(er) 一文中,作者已警告过"魔法师的学徒(The Sorcerer's Apprentice)"问题:开发者过度依赖 AI,以至于无法理解和处理自己所构建系统中出现的问题。

本文的价值在于:它把一次真实的、与 AI 的交互过程完整摊开,展示 AI 的强项(调查定位、测试生成)与弱项(给出干净、贴合现有架构的解决方案),并具体演示了作者如何险些落入"魔法师的学徒"陷阱、最终又凭借对底层基础设施的熟悉而避开它。

hyperscript 解析器:一个"故意打破解析规则"的实验品

要理解这个 Bug,必须先了解 hyperscript 解析器的设计取向。

hyperscript 是一种面向 Web 的、替代性的解释型脚本语言。具有讽刺意味的是,它完全用 JavaScript 编写。在本仓库中即可找到它的完整源码:test/lib/_hyperscript.js(该文件约 6000 行,被 htmx 的测试环境直接引入,作为测试依赖运行)。

它是一个相当"奇怪"的软件:作者在编写它时故意打破了许多解析规则,把它当作一场实验,想看看结果会怎样。典型例子包括:

  • 解析逻辑与语法元素(parse elements)就近放置(colocated);
  • 解析器是可插拔的,语法由运行时动态定义;
  • 同一个语义(如属性访问 property access)支持多种语法写法。

作者并不推荐大多数编程语言采用这种思路,但对 hyperscript 这个项目而言它运转得相当不错——这再次证明,软件世界里"杀猫不止一种方法"(there are indeed multiple ways to skin the cat)。

Bug 报告:一次版本升级引入的回归

故事的起点,是一位用户报告在升级到0.9.91版本后出现的回归:下面的表达式不再能正确解析:

fetch `{% url 'trade:get_symbol_data' %}?symbol=${symbol}` as JSON

具体症状是:as JSON绑定得过紧,在字符串字面量交给fetch之前,就被当作"类型转换表达式"试图把字符串转成 JSON;而用户期望的行为(也是旧版本的行为)是:先 fetch 给定的 URL,再把响应结果当作 JSON 处理。

这种"绑定冲突(binding conflict)"是解析领域的经典问题。而由于 hyperscript 是一种 xTalk 风格的语言,继承了英语的许多歧义性,这个问题在它身上被放得更大——as这个词同时具有两种含义,是整件事的核心矛盾。

调查根因:AI 的强项

定位"为什么会出现这个回归",是作者通常会求助 AI的领域。作者使用的工具是 Claude,而 Claude 在查找根因上表现令人满意:

根因:在 0.9.91 中,作者重构go命令时过于激进,试图让go与fetch命令复用/共享逻辑。为此提取了一个公共方法parseURLOrExpression(),但在这个过程中意外地把fetch命令之后的语法从"URL 字面量"扩展成了通用的expression(表达式)。

于是冲突产生了:

  • 在表达式中,as是转换表达式(conversion expression)关键字,允许做类型转换,例如:
set x to "42" as Int
  • 在fetch命令中,as又是一个修饰符,告诉命令如何转换响应,例如:
fetch https://hyperscript.org as Text

(作者调侃道:"也许这个事实会让你恶心一下。很好。")

问题症结在于:重构之后,解析器在fetch关键字后开始解析一个通用表达式,而这个表达式先吞掉了as关键字,使其被当作表达式的一部分,而不再有机会成为fetch的修饰符。

借助 Claude,作者几分钟内就弄清了这一点——比独自排查快得多。

修复阶段:AI 的三版提案与逐版否决

与"定位问题"时的出色表现相反,AI 在修复问题上要弱得多。作者坦言当时自己有点偷懒,直接向 AI 索要解决方案——但接下来的"连环提案"仍然非常有信息量。

提案 1:一个 Hack

AI 的第一个建议是:先解析所谓的 "string-like"(类字符串)叶子节点,失败后再回退到完整表达式:

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 解析器变成了上下文敏感(context-sensitive)的。但作者的反应是:"好。"因为hyperscript 解析器本来就已经是上下文敏感的了。

不过,在审视这个方案时,作者意识到:项目里已经有所需的"hacky 上下文敏感基础设施",根本不需要给解析器新增标志——而 Claude 没有发现这一点。

提案 3(转折):hyperscript 解析器中的 "follows" 机制

hyperscript 解析器中有一种"follows"概念:即被"更上层"的语法元素声明认领(claim)为 follow 令牌的 token。解析器是一个(有些奇怪的)递归下降解析器,这个机制允许某个语法元素(通常是命令)"认领"一个关键字,使表达式在解析期间不会去匹配它。

该机制在源码中的实现非常直白。在 test/lib/_hyperscript.js 中,follows 就是一个简单的栈:

var follows = []; function pushFollow(str) { follows.push(str); } function popFollow() { follows.pop(); }

而它之所以能拦截匹配,关键在于matchToken的第一步检查(见 test/lib/_hyperscript.js):

function matchToken(value, type) { if (follows.indexOf(value) !== -1) { return; // disallowed token here } ... }

也就是说:只要某个关键字位于 follows 栈中,任何语法元素用matchToken去匹配它都会直接失败。

一个现成的例子是when特性:它把or当作分隔符使用,而不是逻辑连接词:

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

(作者调侃:已经能听到很多解析工程师愤怒地关掉窗口了。"好。")

把这个机制用于本次修复:与其给解析器新增一个标志,不如pushas为 follow → 解析表达式 → pop follow。这既能阻止AsExpression解析,又仍然允许变量等大多数通用表达式正常工作。

作者把这个思路指给 Claude 后,Claude 在一阵兴奋中表示作者"绝对正确",并开始用这一技术修复 Bug——Claude 在parseURLOrExpression()中加入了正确代码,在未新增任何解析器基础设施的前提下一般性地解决了问题。看起来可以收工了。

最终修复:人类的最后一公里

然而,在审阅变更时,作者发现这个"新修复"仍然过宽:fetch和go共享同一个方法parseURLOrExpression(),但只有fetch用as表示修饰符;现有修复把go命令中完全合法的as转换表达式也一并禁用了。

于是作者亲自实现了最终修复,把特殊处理收窄到FetchCommand#parse()内部:

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

这里的关键是try/finally结构:无论解析是否抛错,follow 都会被弹栈,避免污染后续解析状态。通过把pushFollow("as")限定在 fetch 命令的 URL 解析区间内,go的解析完全不受影响。这正是作者对 Bug 的最终答案。

值得一提的对照:在本仓库 vendored 的 hyperscript 源码 test/lib/_hyperscript.js 中,fetch命令的语法元素注册依然保留了as修饰符的解析分支——json、response、html、text以及自定义转换路径dotOrColonPath——可见as作为 fetch 修饰符是该语言的长期既定语义,也印证了本次修复必须保住这条路径的正确性。

测试:AI 的另一个出色领域

在修复过程中,作者让 Claude 为各种情况生成了一些测试。hyperscript 已有相当好的测试套件,而 Claude 出色地创建了小而聚焦的测试,既展示出问题的存在,又证明修复生效。

这构成了"AI 表现良好"的第二个领域:调查与测试创建。

故事的教训:人类在环的价值

那么,这段平淡无奇的 Bug 修复故事,有什么值得玩味的?

作者认为值得注意的对照是:AI 在"调查"和"测试创建"上表现出色,但在"给出干净的解决方案"上表现不佳。

如果作者不熟悉 hyperscript 解析器及其基础设施,这次修复很容易给项目积累技术债:多一个 hacky 的解析特例,或在解析器上多一块状态。而作者断言(虽未提供证据,注释里调侃"这是在梦里被启示的"):技术债呈指数级增长,因此控制技术债极其重要。

这个故事展示:一个熟悉底层基础设施的人类,与 AI 代理协作,在控制复杂度方面远胜于放任 AI 自行其是。

具体到本案例,作者扮演的不是"盲目接受 AI 方案的魔法师的学徒",而是"魔法师"本人——他理解问题、看清了正确解法、能指挥 AI 达成目标,并借助 AI 生成的测试验证方案。这与当下某些"vibe coding"(氛围编程)形成鲜明对比——后者中,开发者(或任何人)似乎以"不理解实际发生了什么"为荣。

番外:AI 与年长开发者

作者在回顾这次经历时还有另一层感触。他是一位年长开发者(当年 50 岁)。随着年龄增长,开发者往往会在一定程度上"失去快球"(lose our fastball),具体到作者身上表现为两点:

  • 记忆力不如从前;
  • 无法像以前那样长时间工作。

而 AI 恰好直接缓解了这两个问题:

  • 关于记忆:虽然无法记住以前能记住的一切,但借助恰当的提示,作者能很快重新理解事物。AI 很擅长帮他做到这一点,也让他能在开源项目与工作项目之间切换得更高效。
  • 关于长时间工作:AI 能持续"苦干",这是即使年轻时的作者也难以跟上的。这意味着作者能为项目维护远比从前更庞大的测试套件——例如本次案例中 Claude 生成的测试,其覆盖面就超过了作者自己愿意投入精力去写的程度。

因此,AI 直击了作者作为年长开发者形成的两个相对短板。

但硬币的另一面:作者非常担心 AI 同时也在加速自己整体智力的衰退——这本来就会随年龄自然发生,而 AI 依赖可能加速这一过程。回顾这段经历,作者甚至对自己"在亲手做正确的事之前,依赖 Claude 那么久"感到有些惭愧。这是他仍在摸索的领域。

结论

作者写下这一系列交互,是因为它同时捕捉了 AI 辅助编程的好与坏:它证明了"一个还算称职的开发者 + AI 代理"的价值,也展示了盲目接受 AI 给出的第一个(或第二个)方案的危险性。

这个故事的可迁移结论可以总结为三条实操原则:

  1. 让 AI 干它擅长的活:问题定位/根因调查、测试用例生成,是当前 AI 的高性价比区域——本次案例中,Claude 把"找到parseURLOrExpression重构引入回归"这一根因分析做得很出色。
  2. 修复方案必须由熟悉系统架构的人把关:AI 的三版提案要么过窄(仅解决单个用例的 hack)、要么过重(给解析器新增全局状态)、要么过宽(误伤go命令)。最终修复之所以收敛,靠的是作者对 follows 机制、pushFollow/popFollow栈以及fetch/go共享方法的透彻理解。
  3. 用测试锚定修复:AI 生成的小而聚焦的测试,既复现了回归,也验证了修复,是"人类在环"协作中成本最低的验证手段。

希望这段真实记录,能帮助读者形成自己的 AI 代理使用策略——尤其是:在让代理动手之前,先确保自己对系统有足够理解,并始终把复杂度控制握在自己手里。

延伸阅读

  • 原始文章:working-with-ai.md
  • 作者对"代码便宜"的论述:Code is Cheap(er)
  • 本次讨论的解析器实现:test/lib/_hyperscript.js(follows 栈见 第 463-471 行,matchToken拦截见 第 340-348 行,asExpression注册见 第 2704-2719 行,fetch命令注册见 第 4838-4869 行)
  • 前端

【免费下载链接】htmx

htmx - high power tools for HTML

项目地址:https://gitcode.com/GitHub_Trending/ht/htmx
点击查看免费下载
上一篇:告别对账噩梦:ERPNext如何自动搞定关联方交易凭证处理
下一篇:OpenCore Legacy Patcher 手把手教程:老款 Mac 升级最新 macOS 的完整流程

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

返回列表