
> 本文编译自外网
把 SQL parser 重写一遍这种重活儿,听起来不该让 Agent 去干。
因为Parser 是典型的"一个字符都不能错"的东西。PostHog 里尤其如此:用户直接写 SQL 查数据,产品分析、session replay、error tracking 这些内部功能也走同一道门。Parser 搞错的后果非常严重,会影响后面的权限控制、查询优化、ClickHouse等等。
但是我通过设计了一套双层Loop 的 Harness系统让AI完成了这个工作,我同时跑了几个 Claude Code session,让它们并行把原来的 C++ ANTLR parser 重写成了 Rust 版本的 hand-rolled parser。最终产出:16K 行 parser 代码,5K 行工具代码,外加几千行测试。本地 benchmark 快了约 70 倍,线上平均快了 454 倍。
Agent开发过程中有意思的是,整个过程中我几乎没手写一行 parser 代码。让我敢把这种东西上生产的,也不是"Claude 写的代码看着还行"。是一套我专门设计的 Agent harness——它不写 parser,它只做一件事:制造反例。
给Agent定义好问题,并告诉它到底需要做什么
PostHog 的 SQL 查询不是直连 ClickHouse。中间有一层转译:把用户看到的逻辑数据模型,变成 ClickHouse 能直访问的物理 SQL。好处很明显——数据库换物理结构,用户查询不受影响;转译阶段顺便加权限控制和性能优化。
Parser 错,后面就会全错。这就是问题定义:你需要一个又快又好的 agentic 代码。不是 demo,是跑在生产上的核心组件。
旧实现:ANTLR 通用引擎的重负
原来的 parser 是 ANTLR 自动生成的,C++ 版本。
在 vibe coding 成熟之前,这几乎是唯一合理的选择。手写 SQL parser 完全是脏活,几个月起步,语法边角无穷无尽,维护成本高到不值得。ANTLR 解决了边际效应的问题。
但 ANTLR 的通用性要付代价。它把 grammar 编译成一张运行时状态图,然后用一个通用解释器去遍历图。每处理一个 token,都是一次图遍历。它还支持动态 lookahead——如果当前位置有多条分支都合法,就同时模拟这些分支,一路跑到只剩一条成立。
这听起来就很通用,但通用的东西一般都不会拥有很好的性能。手写 recursive-descent 可以把 99% 的热路径变成一串直连调用,只有真正需要的地方才做 lookahead 或回溯。ANTLR 照顾的是任意 grammar,不能只为你的业务逻辑优化。
旧实现已经是 C++ 了。这次提速跟语言关系不大。真正改的是执行模型:扔掉通用解释引擎,回到最直接的代码路径。
问题是——在 AI coding 出现之前,这条路走不通。因为"最直接的代码路径"意味着你要自己去写和维护十万行级别的 parser 代码。
AI coding 让手写变得可能。让开发速度极具变快,但"可能"和"敢上线"中间,隔着一整套 harness。
Agent 不是下指令就完事
我的初版尝试很朴素:让 Opus 用 Rust 写一个新 parser,嘱咐它别出错。
这种口述的prompt根本是不起作用的,它当然出了,而且不只出 bug。写到一半它还会开始怀疑你做的这件事的正确性——"这个语法规则太复杂了""也许我们不该完全重写"——每跑完一轮修正,它就倾向于收工。"差不多了吧。"
这是Agent的通病,为了继续快速迭代我开了两个 parallel agent session,走两条不同的实现路线。
第一条压性能:recursive descent 核心 + Pratt expression loop,只在极少数位置放宽到 lookahead 和回溯。这是我脑子里最快的 parser 形态。
第二条压成功率:行为上尽量贴近 ANTLR 的语义,但把运行时图遍历换成显式手写代码,不依赖通用引擎。
两条到后来都跑通了,效果差不多。但在跑了好几天之前,我不知道它们能不能活下来——Agent 也不知道。能做成,不是靠 prompt 写得妙。是靠着 oracle。
旧的 C++ ANTLR parser 就是 oracle。它不是用来比速度的。它是用来告诉你:在新 parser 上跑同一条 SQL,吐出来的 AST 对不对。
有了 oracle,Agent 的工作就变成了:找一条输出不同的 SQL,修,加入回归测试,找下一条。Agent 不需要"对"的判断力——它只需要修复差异,harness 负责判断对不对。
这个 harness 的设计,围绕着同一个句式展开:只要新 parser 和 oracle 输出不同,就记下来,缩小,加入回归测试。Agent 只管修到消失为止。
Harness 的核心:制造分歧
起步阶段,分歧很好找。PostHog 自己就积累了不少回归测试。
把这些跑完之后,有意思的问题来了:怎么持续生成新反例,让 Agent 不断暴露它还不懂的东西?
答案:property-based testing。
Property 是一行定义:新 parser 的输出永远跟 oracle 一致。输入是一条 SQL。PBT 框架要做的就只有一件事——拼命搜索能打破这条规则的反例。
我用的是 Hypothesis。但我不能直接让它生成一堆字节当作 SQL 扔进去——那样大部分时间都浪费在非 SQL 字符串上。我需要告诉它"什么是合理的 SQL"。
于是我和 Claude 又写了一个工具:让它读 ANTLR 的 .g4 grammar 文件,自动 codegen 出 SQL generator 的代码。写 parser 写到一半,又给 grammar 文件写了个 parser——挺好笑,但这一步是 harness 的分水岭。
有了 grammar-based generator,harness 知道了"这个语法长什么样"。它可以生成合法但不常见的 SQL,可以故意在合法边界上试探,可以反复打 parser 的冷门分支。
后来我在这台 fuzzer 身上又加了几层:
基于 grammar 的变体生成:随机交换 token 位置、加括号嵌套、打乱顺序仍然合法的子句。 生产日志采样:从匿名化的真实查询日志里抽。现实世界用户的 SQL 经常写得比 AI 想的更怪。 "认真想边界条件"Agent:这是我最意外的做法——我放了一个后台 Claude agent,不写代码,只负责坐在那里想"什么 SQL 会让 parser 难受"。听起来不像工程,但它找出来的反例质量异常高。
光生成不够。反例往往很大——一条两 KB 的 SQL 把 parser 炸了,Agent 没法对着它 debug。Hypothesis 自带 shrinking,能把大样本压缩成最小复现。生产日志里出来的 SQL 不走这套,我又给 harness 加了 ShrinkRay。
再往后,加了 coverage-guided generation。生成器追踪自己覆盖了哪些 grammar 分支、哪些还没动过,然后有意识地往缺口填。这一步不是必须的——要在生产查询上做到零分歧,前面的措施已经够了。但它挖出了一小撮非常隐蔽的边界问题,属于"只有专门写坏模型的人才想得到"的那种。
这时候回头看:我写的 parser 代码量是零。我写的 harness 代码量,大概五千行。
Agent 会忘事,Harness 要提醒它
vibe coding的时候总是会有段时间特别容易让人暴躁:那就是Agent 修 bug 修得特别脆弱的时候。
它经常会这次发现差一个 token lookahead,它加上。下一轮发现不够,改成两个。再下一轮,发现这里不该补 lookahead,应该按 grammar 的另一条分支处理——之前加的代码全白费,还搅乱了别的逻辑。
根因分析很简单:context window 打满后做了 compaction,Agent 忘了完整的 grammar 长什么样,也忘了旧 C++ parser 在相关位置到底用了什么策略。
人类工程师忘了可以用 git blame。Agent 忘了,就只能靠 context 里还剩下的碎片往前推。
我的解决方法一点都不神秘。
每次开始修一个具体分歧之前,我让 haraness 强制把两样东西再喂进 context:grammar 文件,和旧 C++ 实现在相关位置的源码。不是一直放在 context 里占窗口——是在动手前那一刻,重新塞进去。
这不是 prompt 玄学。是 context 管理。Agent 的记忆不可靠,但文件是可靠的。它需要被提醒,harness 的职责之一就是在它动手之前替它翻开这两页。
这个道理我花了比预想中长得多的时间才学会。但一旦学会了,Agent 的修复质量直接跳了一个台阶——不再像在补丁堆上打架,更像一个有完整信息的工程师在做局部重构。
两个 loop 都跑满
到了冲刺阶段,我只想做一件事:CPU 永远在 fuzz,Agent 永远在修。
我给 harness 加了一个后台常驻进程:PBT 和线上采样不停跑,新发现的失败反例不断往文件里写。几个 parallel Agent session 共享同一个失败队列——手头没活了就取下一个,修完归队。
两条 parser 路线的 Agent 共享同一套回归测试。A 路线发现的 bug,B 路线的 Agent 也能立刻受益——不需要我手动同步,队列天然就是共享状态。
每轮修完后,Agent 不会直接跑下一轮。它会先输出一段简短说明给人类 operator(也就是我):这次修了什么,用什么思路。我的工作从看代码变成了读说明、看通过率曲线、判断修法是在逼近通用解还是在打补丁。
Agent 修代码。Harness 负责找茬、缩小反例、维护回归测试、喂上下文、判断修得对不对。两边各跑各的,互相不挡。
Harness 够好,你才敢上线
新 parser 太快了。快到我不需要对线上流量抽样验证——直接把新 parser 放到生产环境旁边跑 shadow mode。
同一个请求过来,旧 parser 和新 parser 各跑一遍,暗中对比输出。几百万次 parse,我就等在另一边看分歧计数。
零分歧。
我本来打算让它跑几天。几个小时之后的结果已经足够硬,直接把生产流量切到了新 parser,只留 0.1% 的 reverse shadow 继续盯。
线上平均快 454 倍。标题说 70 倍,是 Mac 本地 benchmark。线上 SQL 平均更长,而且很多请求不命中 parser cache,差距拉得更开。
这是是一个过去大概要领域专家花几个月才能做完的东西,我用几天时间做到了——不是凭靠 Agent 的智力,是凭靠 harness。
整体的流程其实不能再称为vibe coding
如果流程是:跟 Claude 说"写一个 Rust SQL parser,要快"——然后拿回来,测试跑了一遍没报错,就上线了——那确实很吓人。
我没有依赖 Agent 的判断力。我依赖的是一台专门设计的找茬机。它的每一环都是确定性的:property 是确定的、oracle 是确定的、回归测试是确定的、shrink 是确定的。Agent 只需要在确定问题面前执行确定任务——找到分歧就修,修完就跑测试。
这件事让我对"Agentic coding"有了一个很具体的看法:不是 prompt 写得好叫 Agentic。是把 Agent 放进一个自己会自检的 harness 里,让 Agent 的每一步产出都被外部系统验证,才叫 Agentic。
最后得到的 parser 可以精确地描述成:以预测式 recursive descent 为主体的手写 parser,中间嵌一个 Pratt expression 核心;默认 LL(2) cursor,少数关键处分叉用有界 lookahead probe;只有极个别决策点才亮出局部 speculative backtracking。Rust 实现,2026 年 5 月由 Claude Opus 4.7 完成。
我也开始重新想 ANTLR 这类 toolchain 的未来。
Parser generator 大概不会消失。但它的角色会变。过去它负责产最终的 parser。以后它可能更适合做某种 oracle——提供一份可信参考实现。LLM 照着这份 oracle 手写出高性能版本,PBT 和 fuzzing 做护栏。generator 变 referee,Agent 变成场上的选手。
这件事上我学到最核心的一条经验很简单:Agent 能做多少,不取决于它多聪明。取决于你给它的 harness 能在多短时间内、用多高的确定性,告诉它"你错了"。
Agentic coding 的流程,不是靠你相信模型,而给它当成许愿机。是靠不停地写test,制造反例而不停的修复代码
-- 完 --
机智流推荐阅读:
1.
2.
3.
4.
cc | 大模型技术交流群 hf | HuggingFace 高赞论文分享群 lc|LangChain 技术交流群 code | AI Coding 交流群 具身 | 具身智能交流群 硬件 | AI 硬件交流群 推理 | AI 推理框架交流群 Agent | Agent 技术交流群