第 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.py、backend/app/agents/sub_agents/guardrails.py、backend/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-近似算法:
- 计算 terminal 节点间的 metric closure(最短路径闭包)
- 在 metric closure 上求最小生成树(MST)
- 将 MST 的边展开回原图中的最短路径
- 对展开后的子图再求一次 MST,去掉冗余边
在 NL2SQL 中的应用:终端节点 = 查询涉及的表(metric 源表 + dimension 表 + filter 表)。Steiner 树 = 连接这些表的最小代价 join 子图。Steiner 节点 = 必要的桥接表(如 fact_A 和 fact_B 通过 dim_C 关联,dim_C 是 Steiner 节点)。
NetworkX 的 steiner_tree(G, terminals, weight="weight") 封装了此算法。
- 从
join_rules和graph_context.join_paths构建加权图 - 调用
steiner_tree(G, terminals, weight="weight") - 提取包含所有终端表及必要桥接表(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 BYCTE,再FULL OUTER JOIN - 输出:
AGGREGATE_BEFORE_JOIN+chasm_trap_warning
Chasm Trap 示例
fact_sales 和 fact_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 拼接而成(问题 + 修复提示 + 语义规划建议):
用户偏好注入路径
user_preferences 是唯一从记忆系统注入 SQL prompt 的字段——它来自 long_term_context(Profile 偏好),通过 assemble_sql_prompt 的 user_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 同时使用 sqlparse 与 sqlglot,但二者职责不同、非替代关系:
| 工具 | 代码位置 | 职责 | 是否在热路径 | 特点 |
|---|---|---|---|---|
| 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 | 首次失败后经重试修复成功的比例(未实测) |
当前实现可改进之处
- 语义规划器 Advisory 太弱:规划器产出的是"建议",LLM 可能忽略,削弱了规划器价值。当前无"建议采纳率"指标。改进:短期增加采纳率指标量化效果;长期在 Phase 4 让规划器成为默认路径,LLM SQL 仅 fallback(与 Rollout 路线图一致)。
- 护栏部分用正则,脆弱:Layer 2d 术语绑定校验用正则匹配
wrong_table.column,无法处理复杂 SQL 结构(子查询嵌套、CTE 别名)。改进:升级为基于 sqlglot AST 的结构化校验,在语法树上做列引用与术语绑定检查。 - 缺执行结果校验对齐:护栏校验 SQL 语法与成本,但不校验"执行结果是否真的回答了用户问题"。LLM 可能生成语法正确但语义错误(答非所问)的 SQL。改进:引入结果校验——用 LLM 对比"用户问题"与"SQL 结果摘要",低分触发重试。
- 无执行沙箱:高危查询(如大范围扫描)只靠 EXPLAIN 预估拦截,没有"先在沙箱试执行"的机制。改进:对超阈值的查询支持沙箱试执行(限定行数/超时),确认无害后再正式执行。
改进方向
SQL 生成的演进方向是让确定性规划更强、让 LLM 的自由度更受控。短期:升级护栏为 AST 结构化校验、增加结果校验对齐。长期:规划器从 Advisory 升级为默认路径,把 LLM 限制在"填空"而非"自由生成"。这与业界语义层(dbt/Cube)的趋势一致——确定性越高,可信度越高。
6.5 小结与延伸阅读¶
本章要点:
- SQL 生成 pipeline:语义规划器(Steiner 树 + 代数重写)→ LLM 生成 → 五层护栏 → 自愈循环。
- 语义规划器用 KMB 算法找最小代价 join 子图,用三条代数规则(join 消除、chasm trap、semi-join)提供建议。
- 五层护栏:语法 → 策略 → AST 列校验 → 术语绑定 → EXPLAIN 成本,每层针对不同风险。
- 自愈循环在 guardrails 失败后补充检索重试,基于 Reflexion 范式。
- TTD 选择"语义中间层 + LLM"混合路线,兼顾确定性与灵活性。
延伸阅读:
- PICARD: Parser-Constraint Decoding — arxiv.org/abs/2103.01331
- DIN-SQL: Decomposed In-Context Learning — arxiv.org/abs/2304.11015
- dbt 语义层 — docs.getdbt.com/docs/use-dbt-semantic-layer
- sqlglot — github.com/tobymao/sqlglot
下一章:第 7 章 记忆与会话系统——四层记忆、SQL 语义缓存、动态 few-shot 自学习如何让系统越用越聪明。