跳转至

第 6 章 SQL 生成与语义规划

本章学习目标 - 深入理解 NL2SQL 技术谱系(prompt-only → schema-aware → 约束解码 → 语义中间层)及 TTD 的混合路线 - 掌握语义查询规划器的 KMB Steiner 树路由与三大代数重写规则 - 逐层理解五层 SQL 护栏的设计原理与实现 - 理解 SQL 生成 Prompt 工程的细节与自愈循环 - 认清当前 SQL 生成的工程权衡与改进方向

前置知识第 5 章 RAG 检索 关联代码backend/app/semantic/backend/app/agents/sub_agents/sql_generation.pybackend/app/agents/sub_agents/guardrails.pybackend/app/prompt/assembler.py


6.1 背景与动机

6.1.1 NL2SQL 的核心技术挑战

生成 SQL 容易,生成正确的 SQL 难。LLM 生成 SQL 的典型错误:

错误类型 例子 后果 TTD 的防护
表名/列名幻觉 编造不存在的 dim_sales.price 执行失败 Layer 2c AST 列校验 + 白名单
join 路径错误 多事实表直接 join 导致重复计数 结果错误 语义规划器 Steiner 树 + chasm trap 重写
术语消歧失败 "GMV"用错定义 结果错误 术语绑定强路由(第 5 章
性能问题 扫全表、大表 window 函数 拖垮生产库 Layer 3 EXPLAIN 成本估算
安全风险 含 DML、越权访问 数据泄露/破坏 Layer 2a 策略禁词 + 权限过滤

6.1.2 NL2SQL 技术谱系

%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e8f0fe','primaryTextColor':'#202124','primaryBorderColor':'#1a73e8','lineColor':'#5f6368','secondaryColor':'#e0f2f1','tertiaryColor':'#f8f9fa'}}}%%
flowchart LR
    A["Prompt-Only<br/>把 schema 塞进 prompt<br/>SQL 语法对但业务错"] --> B["Schema-Aware RAG<br/>检索相关表/列<br/>缩小生成空间"]
    B --> C["约束解码<br/>Grammar-Constrained<br/>解码时语法约束<br/>与商用 API 不兼容"]
    C --> D["语义中间层<br/>确定性图算法规划 join<br/>+ LLM 填 SQL 细节<br/>TTD 混合路线"]

    classDef bpProcess  fill:#e8f0fe,stroke:#1a73e8,stroke-width:2px,color:#202124
    classDef bpExternal fill:#f8f9fa,stroke:#dadce0,stroke-width:2px,color:#5f6368
    classDef bpInfo     fill:#f3e8fd,stroke:#9334e6,stroke-width:2px,color:#202124

    class A,B,C bpProcess
    class D bpInfo

AI Agent 理论关联:NL2SQL 技术谱系

NL2SQL 技术经历了四代演进:

第一代:Prompt-Only - 方法:把数据库 schema(或部分)塞进 prompt,让 LLM 直接生成 SQL。 - 代表:早期 Spider 基准上的 zero-shot prompt。 - 问题:schema 大时超 token;术语全靠猜;无安全护栏。

第二代:Schema-Aware RAG - 方法:检索相关 schema 片段(表/列/示例)约束生成。 - 代表:DIN-SQL(arxiv.org/abs/2304.11015)、DAIL-SQL。 - TTD 的 第 5 章 四引擎 RAG 服务于此——但 TTD 更进一步,检索的不只是 schema,还有术语绑定、join 规则、业务规则。

第三代:约束解码(Grammar-Constrained Decoding) - 方法:在解码时用 SQL 语法树约束生成,保证语法合法。 - 代表:PICARD(arxiv.org/abs/2103.01331)。 - 优势:语法层面零错误。劣势:需自定义解码器,与商用 LLM API 不兼容(无法干预解码过程)。

第四代:语义中间层(Semantic Layer) - 方法:用确定性算法生成 SQL 骨架(join 路径、聚合结构),LLM 只填细节。 - 代表:dbt 语义层、Cube、TTD 的 Semantic Planner。 - 优势:确定性高、安全、可审计。劣势:灵活性低、覆盖面受语义层定义限制。

TTD 的混合路线:规划器用图算法确定 join 路径与重写建议(第四代语义中间层),LLM 在此约束下生成最终 SQL(第二代 RAG + LLM 生成)。护栏做第三代的语法约束(但用后校验而非解码时约束)。这是"语义中间层 + LLM"的务实混合——详见 ADR-18


6.2 设计原理

6.2.1 SQL 生成 Pipeline

data_retrieval → reranker → semantic_planner → sql_generator → guardrails → sql_executor
                                ↑ 确定性规划          ↑ LLM 生成      ↑ 五层校验
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#e8f0fe','primaryTextColor':'#202124','primaryBorderColor':'#1a73e8','lineColor':'#5f6368','secondaryColor':'#e0f2f1','tertiaryColor':'#f8f9fa'}}}%%
flowchart LR
    RAG["RAG 检索完成<br/>retrieval_results + term_bindings"] --> SP["Semantic Planner<br/>KMB Steiner 树 join 路由<br/>+ 代数重写<br/>确定性规划 · 无 LLM"]
    SP -->|"semantic_guidance<br/>结构化 join 建议"| SG["SQL Generator<br/>LLM 生成 SQL<br/>方言适配 + 反幻觉约束"]
    SG --> GR["Guardrails<br/>五层逐层校验"]
    GR -->|"全部通过"| EX["SQL 执行<br/>注入 LIMIT + timeout"]
    GR -->|"可修复(最多2次)<br/>列错误/术语违规"| CR["Corrective Retrieval<br/>补偿检索 → 重新生成"]
    GR -->|"不可修复<br/>策略违规/禁止操作"| REJ["拒绝执行<br/>返回友好错误提示"]

    classDef bpProcess  fill:#e8f0fe,stroke:#1a73e8,stroke-width:2px,color:#202124
    classDef bpDecision fill:#fef7e0,stroke:#f9ab00,stroke-width:2px,color:#202124
    classDef bpSuccess  fill:#e6f4ea,stroke:#1e8e3e,stroke-width:2px,color:#202124
    classDef bpError    fill:#fce8e6,stroke:#d93025,stroke-width:2px,color:#202124
    classDef bpInfo     fill:#f3e8fd,stroke:#9334e6,stroke-width:2px,color:#202124

    class RAG,SP,SG bpProcess
    class GR bpDecision
    class EX bpSuccess
    class CR bpInfo
    class REJ bpError

    linkStyle 0,1,2 stroke:#1a73e8,stroke-width:2px
    linkStyle 3 stroke:#1e8e3e,stroke-width:2.5px
    linkStyle 4 stroke:#f9ab00,stroke-width:2px
    linkStyle 5 stroke:#d93025,stroke-width:2px

6.2.2 语义查询规划器(Advisory 模式)

规划器位于 backend/app/semantic/,是纯计算节点(无 LLM、无外部调用),产出结构化 guidance 增强 LLM 生成:

组件 职责 为什么需要
Graph Router KMB Steiner 树找最优表路径与 join 关系 join 路径选择是组合优化问题,LLM 容易选错导致 chasm trap
Semantic Algebra 识别 chasm trap、多余 join、存在性语义 这些是确定性的代数变换,不该靠 LLM 猜
LLM SQL Generator 利用上述结构化上下文生成最终 SQL SQL 细节(过滤条件、聚合函数)需 LLM 灵活性

为什么不直接编译 SQL? 纯确定性重写对复杂查询支持有限——用户问题的语义细微差别需要 LLM 理解。Advisory 模式"增强 LLM"而非"替代 LLM",兼顾确定性与灵活性。

安全回退

规划器绝不阻塞管道:整个节点函数体包裹 try/except,任何异常返回空结果 {}。空 guidance = LLM 按原有方式独立生成。详见 ADR-18


6.3 实现详解

6.3.1 KMB 启发式图寻路

系统在内存中维护数据仓库拓扑加权图 \(G(V, E)\)

  • 顶点 (V):数据表(带事实/维度属性、认证状态、领域归属)
  • 边 (E):join 条件(带基数约束 1:1/1:N/M:N 与代价权重)

权重校准

因素 权重增加 理由
基础权重 1.0 默认代价
fanout_risk: high +5.0 高扇出 join 易导致重复计数,应尽量避免
fanout_risk: medium +2.0 中等风险
未认证资产 +1.5 鼓励使用已认证资产
跨领域 join +2.0 跨域 join 需谨慎,增加审批成本

算法

用 NetworkX 的 nx.approximation.steiner_tree() 实现 KMB(Kou-Markowsky-Berman)近似算法:

Steiner 树问题与 KMB 算法

Steiner 树问题:给定图 \(G\) 和一组"终端节点"(必须连通的节点),找到一棵包含所有终端节点的最小权重子树。这棵树可能包含非终端的"Steiner 节点"(桥接节点)。

这是 NP-hard 问题,KMB 是经典的 2-近似算法:

  1. 计算 terminal 节点间的 metric closure(最短路径闭包)
  2. 在 metric closure 上求最小生成树(MST)
  3. 将 MST 的边展开回原图中的最短路径
  4. 对展开后的子图再求一次 MST,去掉冗余边

在 NL2SQL 中的应用:终端节点 = 查询涉及的表(metric 源表 + dimension 表 + filter 表)。Steiner 树 = 连接这些表的最小代价 join 子图。Steiner 节点 = 必要的桥接表(如 fact_A 和 fact_B 通过 dim_C 关联,dim_C 是 Steiner 节点)。

NetworkX 的 steiner_tree(G, terminals, weight="weight") 封装了此算法。

  1. join_rulesgraph_context.join_paths 构建加权图
  2. 调用 steiner_tree(G, terminals, weight="weight")
  3. 提取包含所有终端表及必要桥接表(Steiner Nodes)的最小代价连通子图

输出:一棵最小代价 join 子图——告诉 LLM"只 join 这些表,走这些路径"。

6.3.2 三大语义代数重写规则

拿到路由子图后,应用重写规则产出 RewriteDiagnostic 诊断——这些是关系代数中经典的查询优化变换:

规则 1:无损 Join 消除

  • 触发:叶子节点 \(X\) 无字段被 metrics/dimensions/filters 引用,且为 N:1 外键端
  • 代数依据\(\pi_A(A \bowtie_{N:1} X) \equiv A\)(N:1 外键端的投影可无损去掉)
  • 输出JOIN_ELIMINATED —— 告诉 LLM"这个 join 是多余的,可以去掉"

规则 2:Chasm Trap 消除

  • 触发:路由子图存在 ≥2 个事实表
  • 动作:标记"先聚合、再关联"(Aggregate-before-Join)模式
  • 代数改写建议:每棵以维度表为交汇点的子树先 GROUP BY CTE,再 FULL OUTER JOIN
  • 输出AGGREGATE_BEFORE_JOIN + chasm_trap_warning

Chasm Trap 示例

fact_salesfact_returns 都关联到 dim_product。若直接三表 join,每个 sales 会乘上所有 returns,导致重复计数。正确做法:

WITH sales_agg AS (SELECT product_id, SUM(amount) FROM fact_sales GROUP BY product_id),
     returns_agg AS (SELECT product_id, SUM(amount) FROM fact_returns GROUP BY product_id)
SELECT p.name, s.amount, r.amount
FROM dim_product p
LEFT JOIN sales_agg s ON p.id = s.product_id
LEFT JOIN returns_agg r ON p.id = r.product_id

规则 3:半连接存在性推导

  • 触发:Filter 含 EXISTS/NOT EXISTS 语义
  • 动作:将物理 LEFT JOIN 转为 semi-join(\(\ltimes\))或 anti-join(\(\bar{\ltimes}\)
  • 输出SEMI_JOIN / ANTI_JOIN —— 建议用 EXISTS/NOT EXISTS 替代 join

6.3.3 SQL 生成 Prompt 工程

backend/app/prompt/assembler.py 用 Jinja2 组装 prompt,核心是 assemble_sql_prompt()。模板以内联 jinja2.Template 字符串常量形式定义(无独立模板文件),系统提示分为 18 个 section,每个部分都针对一类幻觉/错误:

Prompt 部分 针对的问题
SQL 方言指导(Redshift / PostgreSQL 差异) 方言不兼容
严格输出规则(仅 SELECT、禁 SELECT *、全限定表名、强制 LIMIT) 安全 + 性能
性能要求(Top-N 用两步 CTE 模式、禁大表 join 上用 window 函数) 性能问题
反幻觉约束(表名/字段名白名单,CTE 内也必须全限定) 表名/列名幻觉
术语绑定约束("价格"→ 必须用 gold_hm.fact_transaction.price 术语消歧
DDL、字段白名单、指标定义、关联规则、图谱 join 拓扑 提供准确上下文
业务规则、反模式、few-shot 示例、用户偏好 业务正确性

Prompt 可被 Admin DB 自定义模板覆盖(custom_template),支持 A/B 测试(ab_group)。

完整 Prompt 示例

以下是 assemble_sql_prompt() 为"上月华东区 GMV 同比趋势"问题组装的真实 system message 片段(已截断 DDL/few-shot 等长 section):

# backend/app/prompt/assembler.py(_DEFAULT_SQL_SYSTEM 渲染后)

你是一个数据分析 SQL 生成助手,请根据以下上下文生成安全的查询 SQL。

## SQL 方言
当前方言:PostgreSQL(pg_mooncake)。注意:使用标准 SQL 语法,避免 Redshift 专有语法(DISTKEY/SORTKEY)。

## 严格输出规则
1. 只生成 SELECT 语句,禁止 INSERT/UPDATE/DELETE/DDL。
2. 禁止 SELECT *,必须显式列出列名。
3. 所有表名必须使用完全限定名(schema.table),且只允许出现于下方白名单。
4. 必须包含 LIMIT 1000。
5. 禁止调用任何函数访问文件系统或发起网络请求。
6. CTE 内的表名同样必须全限定。
7. 输出 JSON: {"sql": "<SQL>", "explanation": "<说明>"}

## 性能要求
- Top-N per group 必须用两步 CTE 模式(先 ROW_NUMBER() OVER,再过滤)。
- 禁止在大表 join 上直接用 window 函数。

## ⚠ 严禁表名/Schema 幻觉
只能使用以下白名单中的完全限定表名,任何不在白名单的表名都是幻觉:
- gold_hm.fact_transaction
- gold_hm.dim_customer
- gold_hm.dim_article

## ⚠ 严禁字段幻觉
只能使用以下白名单中的字段,任何不在白名单的字段名都是幻觉。

## 术语绑定约束
- 术语"GMV"已绑定指标 met_hm_transaction_count,必须基于 gold_hm.fact_transaction 计算。
- 术语"价格"已绑定列 gold_hm.fact_transaction.price,禁止使用其他表的 price 列。

## 可用表结构 (DDL)
CREATE TABLE gold_hm.fact_transaction (
    t_dat DATE, customer_id VARCHAR(64), article_id VARCHAR(16),
    price DECIMAL(18,8), sales_channel_id SMALLINT
);
...(dim_customer / dim_article DDL 略)

## 图谱遍历结果 — Join 拓扑
fact_transaction --[customer_id]--> dim_customer
fact_transaction --[article_id]--> dim_article
(Steiner 树:fact_transaction 为中心,无需桥接表)

## 用户偏好 (来自长期记忆)
- 偏好图表:折线图
- 常用维度:时间、区域

对应的 user message 由 sql_generation.py 拼接而成(问题 + 修复提示 + 语义规划建议):

用户问题:上月华东区 GMV 同比趋势
[语义规划建议] 检测到趋势分析,建议按 t_dat 日期分组聚合;同比需对比去年同期。

用户偏好注入路径

user_preferences 是唯一从记忆系统注入 SQL prompt 的字段——它来自 long_term_context(Profile 偏好),通过 assemble_sql_promptuser_preferences 参数进入系统模板的 ## 用户偏好 section。校正记忆(Correction)当前尚未接入此路径,详见 第 7 章

Before/After 对比:语义规划器的效果

以"上月销量 Top 10 商品"为例,对比无规划器(LLM 直出)与有规划器(Steiner 树 + 术语绑定)的 SQL:

Before(无规划器,LLM 直出)——常见错误:join 了不必要的维度表、列名幻觉、未按销量聚合:

-- ❌ 问题:1) JOIN dim_customer 是多余的 2) "sales" 列不存在(幻觉) 3) 未按 article 聚合
SELECT a.prod_name, SUM(f.sales) AS total
FROM gold_hm.fact_transaction f
JOIN gold_hm.dim_article a ON f.article_id = a.article_id
JOIN gold_hm.dim_customer c ON f.customer_id = c.customer_id
WHERE f.t_dat >= '2020-08-01'
GROUP BY a.prod_name
ORDER BY total DESC
LIMIT 10;

After(有规划器,Steiner 树 + 术语绑定 + 白名单约束)——规划器消除多余 join、术语绑定确保正确列引用:

-- ✅ 规划器消除 dim_customer join(Steiner 树判定无字段引用)
-- ✅ 术语"销量"绑定 COUNT(*),"商品"绑定 dim_article
SELECT a.prod_name, COUNT(*) AS sales_count
FROM gold_hm.fact_transaction f
JOIN gold_hm.dim_article a ON f.article_id = a.article_id
WHERE f.t_dat >= '2020-08-01' AND f.t_dat < '2020-09-01'
GROUP BY a.prod_name
ORDER BY sales_count DESC
LIMIT 10;

规划器的价值在于:① 多余 join 被无损消除规则去掉;② 术语绑定确保"销量"映射到 COUNT(*) 而非不存在的 sales 列;③ 白名单约束在生成阶段就阻止了列名幻觉。

6.3.4 五层 SQL 护栏

护栏是"纵深防御"原则在 SQL 管道的体现——五层独立校验,每层针对不同风险:

检查项 拒绝条件 自愈策略 延迟
Layer 1 语法 SQL 解析、多语句检测 非 SELECT / 多条语句 直接拒绝 < 1ms
Layer 2a 策略 禁止关键词、LIMIT 检查 含 DML 关键词 直接拒绝 < 1ms
Layer 2c 列校验 AST 列引用静态校验(sql_column_validator.py 引用不存在的列 有唯一候选时自动替换;否则触发 corrective_retrieval < 5ms
Layer 2d 术语绑定 术语绑定语义校验 绑定列被引用到错误的表 携带正确表列信息触发 corrective_retrieval < 2ms
Layer 3 执行计划 EXPLAIN 分析 预估行数 > 10M / cost > 100K 降级重写或拒绝 10-50ms

Layer 2b 去哪了?

编号中 Layer 2b 的空缺是历史遗留,并非遗漏。早期设计中 Layer 2b 为"表名白名单校验",后在重构中与 Layer 2a(策略校验)合并——表名白名单检查(禁止引用非 retrieval_results 中的表)现在由 Layer 2a 的禁止关键词逻辑承担,并在 Prompt 的 ## ⚠ 严禁表名/Schema 幻觉 section 中前置约束。保留 2b 空缺是为了避免改动既有日志、审计记录与告警规则中已固化的层编号;新代码不应再依赖此编号语义。

Layer 2c 列校验细节

  • sqlparse 纯内存 AST 解析,延迟 < 5ms
  • 构建白名单:从 retrieval_results 提取所有可见列(含 alias_terms)
  • 边界处理:CTE 虚拟列、SELECT alias、* 通配、函数参数、LATERAL JOIN、WITH RECURSIVE、最大递归深度 15
  • 自动列名修正:alias_terms 精确匹配 > difflib 0.8+ 唯一候选 > 不替换(保守策略)
  • 每次自动修正写入 structlog 审计日志

Layer 2d 术语绑定校验细节

  • 仅在 retrieval_results_term_bindings 时激活
  • 对每个绑定列,检查 SQL 是否出现 wrong_table.column 模式
  • 例:术语"价格"绑定 fact_transaction.price,但 SQL 写了 dim_article.price → 拦截
  • 返回 Term binding violation 错误,含正确表列名,供 corrective_retrieval 精准修复

6.3.5 自愈循环

Guardrails 失败后的修复策略(与 第 4 章 呼应):

  • 列引用错误 → corrective_retrieval 拉正确列 → sql_generator 重试(最多 2 次)
  • 术语绑定违规 → corrective_retrieval 拉正确表 → 重试
  • 性能问题(EXPLAIN 超阈值)→ sql_generator 性能优化重试(最多 1 次)

重试次数的依据

上述重试上限为经验阈值,依据是成本-收益权衡:

  • 列错误/术语违反最多 2 次:这类错误多由检索不全或 LLM 一次幻觉导致,第 1 次补充检索后通常可修复;第 2 次作为兜底。超过 2 次仍失败通常意味着问题更深(如术语资产缺失),继续重试边际收益低且增加延迟与 token 成本。
  • 性能问题最多 1 次:EXPLAIN 超阈值的 SQL 通常是结构性问题(如缺 LIMIT、join 顺序错),1 次性能优化提示后若仍不达标,应拒绝并提示用户细化问题,而非反复消耗计算资源。

这些阈值为初始经验值,尚未基于实际触发数据校准。改进方向:统计各层实际触发率与重试成功率,用数据驱动调整(第 15 章第 16 章)。

6.3.6 SQL 方言适配

settings.sql_dialect 根据 data_plane_backend 自动返回方言:

  • redshift → Redshift SQL(支持 DISTKEY/SORTKEY/APPROXIMATE COUNT)
  • pg_mooncake → PostgreSQL SQL

规划器用 sqlglot 做方言渲染与校验(backend/app/semantic/sql_compiler.py),支持 redshift/postgres/duckdb/snowflake/mysql/trino。


6.4 方案选型对比

6.4.1 SQL 生成路线对比

路线 代表 优势 劣势
纯 LLM 生成 早期 Text2SQL 简单、灵活 幻觉、不可控
语义层编译 dbt 语义层、Cube 确定性、安全 灵活性低、覆盖面窄
约束解码 PICARD 精度高 需自定义解码器,不兼容商用 API
混合(TTD) 语义规划器 + LLM 确定性规划 + LLM 灵活性 Advisory 可能被忽略

6.4.2 SQL 解析工具对比

TTD 同时使用 sqlparsesqlglot,但二者职责不同、非替代关系

工具 代码位置 职责 是否在热路径 特点
sqlparse backend/app/skills/sql_column_validator.py Layer 2c 列引用提取与白名单校验 (生成后立即校验) 轻量、纯内存、< 5ms;token 树遍历,方言感知弱
sqlglot backend/app/semantic/sql_compiler.py 方言转译、语法校验、规划器编译 (模块自述"NOT in the primary query path") 完整 AST、支持 redshift/postgres/duckdb/snowflake/mysql/trino 方言互转
ANTLR 通用语法分析器 未用 sqlglot 已满足需求

为何用两个工具

sqlparse 用于生成后热路径的快速列引用校验——它足够快(< 5ms)且只需 token 级遍历。sqlglot 用于离线/非主路径的方言转译与完整 AST 校验(如规划器编译、guardrails 语法校验、方言转换),其模块 docstring 明确声明不在主查询路径。两者互补:sqlparse 求"快",sqlglot 求"全"。6.3.4 Layer 2c 的"sqlparse"与 6.3.6 的"sqlglot"各司其职,不存在混用。

6.4.3 护栏策略对比

策略 说明 TTD 状态
静态校验 解析 SQL 检查(语法/列/关键词) Layer 1-2
EXPLAIN 成本估算 执行计划预估 Layer 3
执行沙箱 沙箱内试执行 未实现
结果校验对齐 校验结果是否符合预期 未实现

性能与质量指标(设计目标)

以下为 SQL 生成与护栏的设计目标值,TTD 尚未进行系统化实测

指标 设计目标 说明
SQL 生成延迟(LLM 调用) 1-3s deepseek-v4-pro 单次生成
Layer 1 语法校验 < 1ms 多语句检测
Layer 2a 策略校验 < 1ms 禁词/LIMIT 检查
Layer 2c 列校验 < 5ms sqlparse AST 遍历
Layer 2d 术语绑定 < 2ms 正则匹配
Layer 3 EXPLAIN 10-50ms 执行计划预估
语义规划器延迟 < 50ms 纯计算(Steiner 树 + 代数重写)
规划器建议采纳率 ≥ 0.8 LLM 遵循规划器 guidance 的比例(未实测)
自愈成功率 ≥ 0.7 首次失败后经重试修复成功的比例(未实测)

评估基础设施见 第 15 章,改进路线见 第 16 章

当前实现可改进之处

  1. 语义规划器 Advisory 太弱:规划器产出的是"建议",LLM 可能忽略,削弱了规划器价值。当前无"建议采纳率"指标。改进:短期增加采纳率指标量化效果;长期在 Phase 4 让规划器成为默认路径,LLM SQL 仅 fallback(与 Rollout 路线图一致)。
  2. 护栏部分用正则,脆弱:Layer 2d 术语绑定校验用正则匹配 wrong_table.column,无法处理复杂 SQL 结构(子查询嵌套、CTE 别名)。改进:升级为基于 sqlglot AST 的结构化校验,在语法树上做列引用与术语绑定检查。
  3. 缺执行结果校验对齐:护栏校验 SQL 语法与成本,但不校验"执行结果是否真的回答了用户问题"。LLM 可能生成语法正确但语义错误(答非所问)的 SQL。改进:引入结果校验——用 LLM 对比"用户问题"与"SQL 结果摘要",低分触发重试。
  4. 无执行沙箱:高危查询(如大范围扫描)只靠 EXPLAIN 预估拦截,没有"先在沙箱试执行"的机制。改进:对超阈值的查询支持沙箱试执行(限定行数/超时),确认无害后再正式执行。

改进方向

SQL 生成的演进方向是让确定性规划更强、让 LLM 的自由度更受控。短期:升级护栏为 AST 结构化校验、增加结果校验对齐。长期:规划器从 Advisory 升级为默认路径,把 LLM 限制在"填空"而非"自由生成"。这与业界语义层(dbt/Cube)的趋势一致——确定性越高,可信度越高。


6.5 小结与延伸阅读

本章要点

  1. SQL 生成 pipeline:语义规划器(Steiner 树 + 代数重写)→ LLM 生成 → 五层护栏 → 自愈循环。
  2. 语义规划器用 KMB 算法找最小代价 join 子图,用三条代数规则(join 消除、chasm trap、semi-join)提供建议。
  3. 五层护栏:语法 → 策略 → AST 列校验 → 术语绑定 → EXPLAIN 成本,每层针对不同风险。
  4. 自愈循环在 guardrails 失败后补充检索重试,基于 Reflexion 范式。
  5. TTD 选择"语义中间层 + LLM"混合路线,兼顾确定性与灵活性。

延伸阅读

下一章第 7 章 记忆与会话系统——四层记忆、SQL 语义缓存、动态 few-shot 自学习如何让系统越用越聪明。