Text2SQL 幻觉抑制全景实战:从元数据 Schema 注入到生成校验的四层护栏
在将大语言模型(LLM)落地到企业级数仓与取数系统的过程中,几乎所有团队都会被同一个幽灵般的现象所折磨——大模型的“知识幻觉(Hallucination)”。
在通用对话场景下,大模型如果稍微有一点“自由发挥”,用户可能觉得幽默风趣;
但在面向数仓的 Text2SQL 场景中,哪怕只有一个字段产生幻觉,整条 SQL 就会直接报错失败,或者更可怕地返回完全错误的假业务数据!
看一看大模型在 Text2SQL 场景下的三大典型幻觉套路:
- 凭空捏造字段(Hallucinated Columns):业务提问“查一下 VIP 会员”,大模型在没有该字段的情况下,信誓旦旦地写出了
WHERE is_vip = 1(而真实数仓里该状态存放在user_level >= 5); - 凭空发明表关联路径(Invalid Join Path):试图直接关联两个在物理上根本不存在主外键关系的独立业务域大表;
- 捏造不存在的函数(Fake Functions):在 MySQL 语法中随手生成 Oracle 独有的
NVL()或 ClickHouse 独有的toDate()。
如何构建一套包含静态元数据注入、语义负向约束、AST 编译期硬拦截以及受限执行闭环的“四层防幻觉确定性护栏(Four-Layer Guardrails)”?
[Text2SQL 工业级四层防幻觉确定性防御护栏] [业务自然语言提问] │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 第一层:元数据白名单与物理 Schema 拓扑注入 (Schema Grounding)│ │ - 仅注入精准召回的 4~6 张表,携带物理类型与枚举值样例 │ │ - 强制声明: "严禁使用以下上下文未提及的任何列或表名!" │ └────────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 第二层:少样本负向约束与反模式注入 (Negative Few-shots) │ │ - 注入高频幻觉负面案例 (例如: 严禁捏造 is_vip, 必须用 level)│ │ - 强制约束: 遇到未知字段必须输出 <AMBIGUOUS> 触发主动反问 │ └────────────────────────┬────────────────────────────────────┘ │ ▼ (LLM 生成候选 SQL) ┌─────────────────────────────────────────────────────────────┐ │ 第三层:AST 编译器静态元数据存在性硬校验 (AST Compiler Check)│ │ - 遍历 AST 提取所有 Table/Column 引用 │ │ - 对齐数仓物理字典: 一旦发现不存在列 ──▶ 立即拦截并自动修复 │ └────────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 第四层:只读沙箱试运行与 EXPLAIN 验证 (Dry-Run Verification) │ │ - 在克隆只读影子库中执行 EXPLAIN 确保语法 100% 通过编译 │ └────────────────────────┬────────────────────────────────────┘ │ ▼ [安全输出可信 SQL]第一层:元数据物理白名单(Schema Grounding)
大模型之所以会编造字段,往往是因为 Context 中给出的元数据过于粗糙,或者直接把上百张表扔给大模型导致注意力稀释。
黄金工程规范:
- 通过向量检索 + 表共现图谱,将单次 Prompt 中的表数量严格限制在4 到 6 张;
- 每个字段必须携带明确的物理注释与取值样例(例如:
status TINYINT COMMENT '支付状态: 1=待支付, 2=已支付, 3=已关闭'); - 在 System Prompt 末尾加入绝对硬指令:
“【铁律】:你只能使用上述【物理 Schema】中明确存在的表名与列名!严禁根据英文常识推测或发明任何未在 Schema 中声明的字段!”
第二层:负向案例注入与主动反问(Negative Constraint)
对于业务中极易发生幻觉的“历史深坑”:
- 在 Prompt 中显式注入负面少样本(Negative Few-shots);
- 明确告知模型:“当用户提到‘退货’时,不要在
t_order表中寻找is_return字段,必须关联t_order_refund表”; - 允许模型在字段确实不存在时输出特定的标记
<UNCERTAIN_COLUMN: xxx>,从而触发前台交互界面向业务人员主动反问澄清,而不是强行胡编乱造。
第三层:AST 编译器物理存在性硬拦截(AST Compiler Guard)
生成的 SQL 必须经过本地 Python AST 解析器(基于sqlglot)的严格审查:
import sqlglot from sqlglot import exp class SchemaASTValidator: """AST 编译器级物理列与表存在性硬校验护栏""" def __init__(self, physical_schema_dict: dict): self.valid_schema = physical_schema_dict # 格式: {"t_order": {"id", "pay_amount", "status"}} def validate_sql_against_schema(self, generated_sql: str) -> dict: try: tree = sqlglot.parse_one(generated_sql, read="mysql") except Exception as e: return {"valid": False, "error": f"语法错误: {str(e)}"} # 提取 SQL 中引用的所有表与列 tables = [t.name.lower() for t in tree.find_all(exp.Table)] for col_node in tree.find_all(exp.Column): col_name = col_node.name.lower() # 校验该字段是否存在于声明的物理表字典中 if not self._is_column_exist_in_allowed_tables(col_name, tables): return { "valid": False, "error_type": "HALLUCINATED_COLUMN", "column": col_name, "message": f"字段 [{col_name}] 为模型凭空捏造的幻觉字段,物理表中不存在!" } return {"valid": True}一旦 AST 验证器捕获到幻觉字段,系统自动将报错信息组装为反馈 Prompt(Error Feedback),在本地触发一次微调模型的秒级自我修正(Self-Correction),将幻觉字段替换为合法物理列。
第四层:只读沙箱试运行(Dry-Run Verification)
最后一道防线:将生成的 SQL 发送给连接受限只读库的代理执行EXPLAIN。
只有在数据库内核真正完成词法、语法解析与执行计划编译且确认无错后,才正式将数据返回给前台用户。
总结
对抗大模型幻觉,不能寄希望于模型的自觉。
通过四层环环相扣的物理约束与编译器护栏,我们成功将生产环境中 Text2SQL 的字段幻觉率从原本的18.4% 彻底压制在 0.1% 以下,为自动化取数系统的工业化可用性奠定了最坚实的可信底盘。