Working With AI: A Concrete Example

Working With AI: A Concrete Example

与 AI 协作:一个具体的案例

Carson Gross | June 29, 2026 Carson Gross | 2026年6月29日

I am, generally, ambivalent towards AI. There is no doubt it has become a very powerful tool for development in the last year, but it also comes with many dangers, both for us individually (e.g. the slow dulling of our intellects) as well as collectively (e.g. environmental concerns, increasingly expensive personal computing, etc.) 总的来说,我对 AI 持矛盾态度。毫无疑问,在过去的一年里,它已经成为开发领域非常强大的工具,但它也带来了许多危险,无论是对我们个人(例如智力的缓慢迟钝)还是对集体(例如环境问题、日益昂贵的个人计算等)而言。

In “Code is Cheap(er)”, I warn about The Sorcerer’s Apprentice problem, where a developer becomes reliant on AI and is unable to understand and properly address issues that come up in the systems they are building. In this article I want to go through a specific interaction that I had with AI while maintaining hyperscript to show the strengths and weaknesses of AI in general and to demonstrate The Sorcerer’s Apprentice problem (which I narrowly avoided) in particular. 在《代码更廉价了》(Code is Cheap(er))一文中,我曾警告过“魔法师的学徒”问题,即开发者变得依赖 AI,以至于无法理解并妥善解决他们所构建系统中出现的问题。在本文中,我想回顾我在维护 hyperscript 时与 AI 进行的一次具体交互,以展示 AI 的总体优缺点,并特别演示“魔法师的学徒”问题(我险些陷入其中)。

The Hyperscript Parser

Hyperscript 解析器

For some background, hyperscript is an alternative interpreted scripting language for the web. It is, ironically, written entirely in JavaScript. It is a strange piece of software: I intentionally broke many of the rules of parsing when writing it as an experiment to see how things would work out. 作为背景介绍,hyperscript 是一种用于 Web 的替代性解释型脚本语言。讽刺的是,它完全是用 JavaScript 编写的。这是一个奇怪的软件:我在编写它时故意打破了许多解析规则,以此作为实验,看看结果会如何。

Some examples:

  • Parsing logic is colocated on parse elements
  • The parser is pluggable, and the grammar is defined dynamically
  • It supports multiple syntaxes for property access. 一些例子包括:
  • 解析逻辑与解析元素位于同一位置
  • 解析器是可插拔的,语法是动态定义的
  • 它支持多种属性访问语法。

It is not an approach I would recommend for most programming languages, but it has worked out pretty well for this project. Yet another demonstration that there are indeed multiple ways to skin the cat in software. 对于大多数编程语言,我不推荐这种方法,但它在这个项目中效果相当不错。这再次证明了在软件开发中,确实有多种方法可以解决同一个问题。

A Bug Report

一个 Bug 报告

Our story begins when a user reported a regression when upgrading to the 0.9.91 release. The following expression no longer parsed properly: fetch {% url 'trade:get_symbol_data' %}?symbol=${symbol} as JSON 我们的故事始于一位用户报告在升级到 0.9.91 版本时出现了一个回归问题。以下表达式无法再正确解析: fetch {% url 'trade:get_symbol_data' %}?symbol=${symbol} as JSON

In particular, the as JSON was binding too tightly and trying to convert the string literal into JSON before it was handed to fetch instead of doing what the user expected (and what it did previously) namely fetching the given url with the results treated as JSON. This sort of binding conflict is a classic problem in parsing. Because hyperscript is an xTalk style language and inherits many of the ambiguities of English, this problem is all the worse in it. 具体来说,as JSON 的绑定过于紧密,它试图在将字符串字面量传递给 fetch 之前将其转换为 JSON,而不是执行用户预期的操作(也是之前版本的功能),即获取给定的 URL 并将结果视为 JSON。这种绑定冲突是解析中的经典问题。由于 hyperscript 是一种 xTalk 风格的语言,继承了英语的许多歧义性,这个问题在其中尤为严重。

Investigating The Cause

调查原因

The first thing to do was to investigate why this regression occurred. This is an area where I am typically going to lean on AI to help. I use Claude, and it did an admirable job finding the root cause: in 0.9.91 I had been overly aggressive in refactoring the go command to reuse/share logic with the fetch command. I had extracted a common method for both of these commands to use, parseURLOrExpression(), but, in doing so, I accidentally expanded the grammar after the fetch command to include the general expression, er, expression. 首先要做的是调查为什么会出现这种回归。这是我通常会依赖 AI 协助的领域。我使用了 Claude,它在寻找根本原因方面表现出色:在 0.9.91 版本中,我在重构 go 命令以与 fetch 命令重用/共享逻辑时过于激进。我为这两个命令提取了一个通用方法 parseURLOrExpression(),但在这样做时,我不小心扩展了 fetch 命令之后的语法,使其包含了通用的表达式。

The as keyword has a meaning in expressions: it is a conversion expression, allowing you to convert between types: set x to "42" as Int as 关键字在表达式中具有特定含义:它是一个转换表达式,允许你在类型之间进行转换: set x to "42" as Int

But the as keyword is also a modifier of the fetch command, telling it how to convert the response: fetch https://hyperscript.org as Text (Perhaps this fact makes you throw up a little bit in your mouth. Good.) 但 as 关键字同时也是 fetch 命令的修饰符,告诉它如何转换响应: fetch https://hyperscript.org as Text (也许这个事实让你感到有点反胃。很好。)

The crux of the issue was that, inadvertently in the refactor, I had made the parser parse an expression after a fetch keyword which was now consuming the as keyword as an expression, rather than allowing it to be a modifier for fetch. With the help of Claude I was able to figure this out in a few minutes, much faster than if I had had to figure it out on my own. 问题的症结在于,在重构过程中,我无意中让解析器在 fetch 关键字之后解析了一个表达式,该表达式现在将 as 关键字作为表达式的一部分消耗掉了,而不是让它作为 fetch 的修饰符。在 Claude 的帮助下,我在几分钟内就弄清了这一点,这比我自己摸索要快得多。

Fixing The Issue

修复问题

AI was very helpful in finding the cause of the problem. In fixing the problem, however, it was much weaker. I will admit here I was being lazy and asked AI for a solution, so complaining about those solutions feels a bit, well, lazy, but I still think the string of events is informative, so let’s go through exactly what happened. AI 在寻找问题原因方面非常有帮助。然而,在修复问题时,它的表现要弱得多。我承认我当时很懒,直接向 AI 索要解决方案,所以抱怨这些方案显得有点……嗯,懒惰,但我仍然认为这一系列事件很有参考价值,所以让我们看看究竟发生了什么。

Proposed Fix 1: A Hack

建议修复方案 1:一种 Hack 手段

The first suggestion that was given was to parse what is called a “string-like” leaf first, then fall back to a full expression: return this.parseElement("stringLike") || this.requireElement("expression"); 第一个建议是先解析所谓的“类字符串”(string-like)叶子节点,然后回退到完整表达式: return this.parseElement("stringLike") || this.requireElement("expression");

This fix would have solved the immediate problem presented by the user. However, it was very specific to the reported bug and wouldn’t have fixed the general case, such as if someone uses a variable as the target of a fetch: fetch $url as JSON 这个修复方案可以解决用户提出的燃眉之急。然而,它非常针对所报告的 Bug,无法解决通用情况,例如如果有人使用变量作为 fetch 的目标: fetch $url as JSON

I rejected this proposal because of this: too hacky and not general enough. (Note that the hyperscript parser has plenty of organically supplied hacks in it, so this may have been the pot calling the kettle black.) 我拒绝了这个提议,因为它太像“打补丁”了,而且不够通用。(请注意,hyperscript 解析器本身就包含大量自然形成的 Hack,所以这可能有点“五十步笑百步”了。)

Proposed Fix 2: Better But Unnecessary Complexity

建议修复方案 2:更好但没必要的复杂性

The second proposal was more interesting: add a noConversions flag on the parser, set it around the URL parse, and have AsExpression.parse bail when it is set: // AsExpression.parse() if (parser.noConversions) return; 第二个提议更有趣:在解析器上添加一个 noConversions 标志,在解析 URL 时设置它,并让 AsExpression.parse 在该标志被设置时退出: // AsExpression.parse() if (parser.noConversions) return;

This will horrify many parser engineers because it makes the hyperscript parser context-sensitive. Good. The hyperscript parser was already context-sensitive. In looking at this fix and thinking for a second, I realized that we already had the hacky context-sensitive infrastructure we needed without introducing a new flag on the parser, but Claude had missed it. 这会让许多解析器工程师感到恐惧,因为它使 hyperscript 解析器变得上下文相关。很好。hyperscript 解析器本来就是上下文相关的。在审视这个修复方案并思考片刻后,我意识到我们已经有了所需的上下文相关基础设施,无需在解析器上引入新标志,但 Claude 忽略了这一点。

“Follows” In The Hyperscript Parser

Hyperscript 解析器中的“Follows”机制

In the hyperscript parser we have a notion of “follows”, that is, tokens that are claimed by a “higher up” parse element as a follow token. The hyperscript parser is (a somewhat strange) recursive descent parser, and this allows a parse element (usually a command) to “claim” a keyword, and expressions won’t match against them during parsing. 在 hyperscript 解析器中,我们有一个“follows”的概念,即被“上层”解析元素声明为后续标记的 token。hyperscript 解析器是一个(有点奇怪的)递归下降解析器,这允许解析元素(通常是命令)“声明”一个关键字,这样表达式在解析过程中就不会匹配到它们。

As an example, the when feature uses or as a separator rather than as a logical connective in its declaration: <div _="when $x or $y changes put it into me"></div> (I can hear many parser engineers closing this window in anger. Good.) 例如,when 功能在其声明中使用 or 作为分隔符,而不是作为逻辑连接词: <div _="when $x or $y changes put it into me"></div> (我仿佛能听到许多解析器工程师愤怒地关闭了这个窗口。很好。)

It turns out that this feature could be used to achieve what we wanted: rather than adding a new flag to the parser we could push as as a follow, then parse the expression, the… 事实证明,这个功能可以用来实现我们想要的效果:与其向解析器添加新标志,不如将 as 推入 follows 列表,然后解析表达式,……