Day 0 · 为什么做这件事
写于 2026 年 3 月。这不是一篇成功学文章,这是一份真实的开发记录。
起点:对静默错误的愤怒
Section titled “起点:对静默错误的愤怒”我在数据行业工作了很多年。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 生成日期序列 | 否 |
关键推论:对于前两类错误,可以系统性消灭;对于第三类,应该诚实地承认边界。
核心洞见:约束论
Section titled “核心洞见:约束论”LLM 的错误率与输出空间大小正相关。
如果我们能让 LLM 只从 Registry 里存在的表名和列名中选择——幻觉列名就物理上不可能出现。如果强制 JOIN 类型从枚举值里选——裸 JOIN 就物理上不可能出现。如果用 anti join 代替 NOT IN——NULL 陷阱就物理上不可能出现。
不是让 LLM 写出更好的 SQL,而是设计一个中间语言,让某些错误在物理上不可能发生。
这个思路后来找到了学术先例:IRNet(ACL 2019)提出的 SemQL 用同样的”中间语义表示 + 确定性编译”路径,在 Spider benchmark 上提升了 19.5pp。Forge DSL 是同一理念的工程化实现。
从 Version A 到 Version D
Section titled “从 Version A 到 Version D”Version A:SQL 风格 DSL
Section titled “Version A:SQL 风格 DSL”最初设计了一种类 GraphQL 的查询语言。结果:LLM 评分 7.63,编译失败率 3.8%。而对照组(直接生成 SQL)得分 8.38——DSL 反而不如没有它。
问题根源:文本 DSL 的约束是软性的。即使 prompt 写了”JOIN 必须声明 type”,模型也可以无视。
Version D:JSON DSL + Structured Output
Section titled “Version D:JSON DSL + Structured Output”关键转折:用 JSON 格式 + Structured Output(token 级别约束)。
- JSON Schema 定义
fn是枚举值 → 幻觉值物理上不可能出现 - JOIN 的
type是必填枚举 → 无类型 JOIN 物理上不可能出现
结果:LLM 评分 8.46(+0.83),编译失败率降到 1.2%。
这是整个项目中单次最大的跳跃。从”希望 LLM 遵守规则”到”物理上不可能违反规则”。
SQL 的设计缺陷作为机会
Section titled “SQL 的设计缺陷作为机会”在整理错误案例的过程中,我意识到很多”生成错误”根植于 SQL 本身的历史包袱:
- 书写顺序 != 执行顺序:SQL 把 SELECT 放最前面,但实际最后执行。Forge DSL 强制按执行顺序组织
- NULL 三值逻辑:
NOT IN的经典陷阱。Forge 用antijoin 代替 - GROUP BY 跨方言不一致:
_coerce自动补齐非聚合列
Forge DSL 其实是在修补 SQL 五十年来留下的坑——不是全部的坑,但至少是 LLM 生成场景下最频繁被踩的坑。
本页为精简版,提取自完整开发故事的前三章。完整内容参见 docs/devlog/forge-dev-story.md。