跳转到内容

Day 0 · 为什么做这件事

写于 2026 年 3 月。这不是一篇成功学文章,这是一份真实的开发记录。

我在数据行业工作了很多年。LLM 的到来让 Text-to-SQL 重新热闹起来——演示视频很漂亮,但真实的数据团队开始用这些工具后,问题浮出水面。

一个典型场景:AI 生成的 SQL 里,应该用 LEFT JOIN 的地方写成了 INNER JOIN,直接把没有订单记录的用户过滤掉。那份报告里的用户留存率少了二十个百分点。

更糟糕的是,这类错误不会报错。SQL 执行成功,数据也出来了,只是数字是错的——安安静静地错着。

这就是做 Forge 的起点:一种对于”静默错误”的愤怒。


我花了两周整理所有见过的 SQL 生成失败案例,把错误分成三类。这个分类在整个开发过程中作为北极星一直没有变过:

错误类型定义例子Forge 能解决
生成错误推理对了,翻译成 SQL 时出岔子LEFT 写成 INNER、NOT IN 的 NULL 陷阱、WHERE vs HAVING 混淆是
业务逻辑错误无法知道你们公司的指标定义”复购率”的分母是全部用户还是下过单的用户是(Registry)
算法逻辑错误需要了解具体实现方法递归 CTE 生成日期序列否

关键推论:对于前两类错误,可以系统性消灭;对于第三类,应该诚实地承认边界。


LLM 的错误率与输出空间大小正相关。

如果我们能让 LLM 只从 Registry 里存在的表名和列名中选择——幻觉列名就物理上不可能出现。如果强制 JOIN 类型从枚举值里选——裸 JOIN 就物理上不可能出现。如果用 anti join 代替 NOT IN——NULL 陷阱就物理上不可能出现。

不是让 LLM 写出更好的 SQL,而是设计一个中间语言,让某些错误在物理上不可能发生。

这个思路后来找到了学术先例:IRNet(ACL 2019)提出的 SemQL 用同样的”中间语义表示 + 确定性编译”路径,在 Spider benchmark 上提升了 19.5pp。Forge DSL 是同一理念的工程化实现。


最初设计了一种类 GraphQL 的查询语言。结果:LLM 评分 7.63,编译失败率 3.8%。而对照组(直接生成 SQL)得分 8.38——DSL 反而不如没有它。

问题根源:文本 DSL 的约束是软性的。即使 prompt 写了”JOIN 必须声明 type”,模型也可以无视。

关键转折:用 JSON 格式 + Structured Output(token 级别约束)。

  • JSON Schema 定义 fn 是枚举值 → 幻觉值物理上不可能出现
  • JOIN 的 type 是必填枚举 → 无类型 JOIN 物理上不可能出现

结果:LLM 评分 8.46(+0.83),编译失败率降到 1.2%。

这是整个项目中单次最大的跳跃。从”希望 LLM 遵守规则”到”物理上不可能违反规则”。


在整理错误案例的过程中,我意识到很多”生成错误”根植于 SQL 本身的历史包袱:

  • 书写顺序 != 执行顺序:SQL 把 SELECT 放最前面,但实际最后执行。Forge DSL 强制按执行顺序组织
  • NULL 三值逻辑:NOT IN 的经典陷阱。Forge 用 anti join 代替
  • GROUP BY 跨方言不一致:_coerce 自动补齐非聚合列

Forge DSL 其实是在修补 SQL 五十年来留下的坑——不是全部的坑,但至少是 LLM 生成场景下最频繁被踩的坑。


本页为精简版,提取自完整开发故事的前三章。完整内容参见 docs/devlog/forge-dev-story.md。