“SELECT 看着没问题,执行也通过了,但数不对。”这是 NL2SQL 项目里最让人头疼的情况。比如业务方问“上月新增付费用户数”,AI 生成的 SQL 语法完全正确,甚至能跑出结果——但它把“付费”理解成了“只要下过单就算付费”,没有排除当月退款用户,也没有剔除内部测试账号。结果页面上的数字和 BI 报表对不上,业务方对整个智能体开始怀疑。
这个问题的本质,是 NL2SQL 落地要跨过两个层次:语法层和语义层。语法层是 SQL 能不能解析、能不能执行;语义层是 SQL 反映的是不是业务方真正想要的口径。大模型对前者已经做得不错,但后者才是决定项目能不能上线的关键。下面按四步拆开:元数据准备、检索配置、SQL 校验、结果验证,每一步都给出可检查的验收标准和失败时的回退方案。
先把问题拆开:语法层和语义层
建一个数据分析智能体,本质上是在“用户问题”和“数据库里的答案”之间架一条链路:用户用自然语言提问,系统把问题转成 SQL,执行后返回结果。这条链路上有四个环节容易出问题。
第一个环节是上下文不够。模型不知道库里有哪些表、哪些字段、字段代表什么业务含义,只能靠猜。第二个环节是检索不准。表太多、字段太多,一次塞不进 prompt,但召回的元数据又没覆盖真正需要的那张表。第三个环节是校验缺失。生成的 SQL 能跑通,但 WHERE 条件、JOIN 关系、聚合粒度全都不对。第四个环节是结果没人对。执行成功就算完事,数字和 BI 报表对不上没人发现。

把这四个环节分别卡住,就是下面的四步。
第一步:元数据准备——别让模型“猜字段”
模型不是数据库管理员,它不会主动知道 user_table 里的 status 字段到底有哪几种取值,也不会知道“付费用户”在你的业务里特指 payment_status = 'paid' 且 refund_flag = 0 的用户。这些信息必须提前准备好,作为上下文喂给模型。
准备的内容分三类。第一类是表结构元数据,包括表名、字段名、字段类型、主键外键关系,以及每个字段的中文注释。第二类是业务语义,包括指标定义、枚举值含义、字段之间的业务关联。比如 order_amount 的单位是“分”还是“元”,created_at 是 UTC 还是北京时间,这些细节模型猜不准。第三类是口径规则,也就是业务方对“活跃”“新增”“付费”这些词的精确定义。
一个实用的做法是把这些信息沉淀成 YAML 或 JSON 文件,作为智能体的固定上下文:
tables:
- name: orders
comment: "订单主表,每行一笔订单"
columns:
- name: user_id
type: bigint
comment: "下单用户ID,关联 users.id"
- name: order_amount
type: decimal(10,2)
comment: "订单金额,单位:元,已扣除优惠"
- name: status
type: varchar(20)
comment: "订单状态:pending/paid/cancelled/refunded"
business_rules:
- name: "付费订单"
definition: "status = 'paid' 且不存在同 order_id 的 refunded 记录"
这一步的验收标准很直接:挑 20 个业务方最常问的问题,逐个人工判断“给模型的元数据是否足以回答这个问题”。如果 20 个里有 3 个以上需要模型额外猜测,就说明元数据不够,回退方案是先把这 3 个问题涉及的字段注释和业务规则补上,再继续往下走。
第二步:检索配置——向量加关键词的混合召回
当库里有上百张表、上千个字段时,不可能把所有元数据全塞进 prompt。这时候需要一个检索环节,从元数据库里挑出和当前问题最相关的表和字段。这里建议用混合检索:向量检索负责语义相似,关键词检索负责精确匹配。
向量检索解决的是“用户说客户,表名叫 customer”这种同义表达问题。先把每张表的描述、每个字段的注释嵌入成向量,再把用户问题也嵌入,计算相似度召回。但向量检索有个毛病:它可能把语义相近但完全无关的表也召回来,还会漏掉精确的枚举值或字段名匹配。
关键词检索补的就是这个短板。用户问“订单状态是 pending 的有多少”,关键词检索可以直接命中 status 字段注释里的 pending。两者结合,才能保证召回既不错也不漏。一个简单的混合策略是把两路召回结果做加权融合,或者先关键词过滤再向量排序。
具体实现上,可以参考 AgentNLQ 这类系统的思路:先从用户问题里提取可能和表、列、取值相关的实体,把这些实体嵌入后走向量检索,同时用原始关键词做精确匹配,最后合并去重。下面是一段简化的混检索示意代码:
def hybrid_retrieve(question: str, metadata_index, top_k: int = 10):
# 关键词精确命中字段名和注释
keyword_hits = metadata_index.keyword_search(question, limit=top_k)
# 向量检索召回语义相近的表和字段
vector_hits = metadata_index.vector_search(question, limit=top_k)
# 合并:关键词命中排前面,向量结果去重补后面
merged = {hit.id: hit for hit in keyword_hits}
for hit in vector_hits:
if hit.id not in merged:
merged[hit.id] = hit
return list(merged.values())[:top_k]
这一步的验收标准是召回命中率。同样拿那 20 个典型问题测试,看目标表和字段是否稳定出现在 top-10 召回结果里。如果命中率低于 80%,回退方案有两个方向:一是加大 top-k 再看是否只是排序问题;二是检查元数据里的注释是否太笼统,导致向量相似度拉不开差距。先把注释写具体,再调检索参数。
第三步:SQL 校验——两道关卡拦下“看着对”的坏查询
模型生成 SQL 之后,不能直接扔给数据库执行。至少要过两道关卡:语法校验和口径校验。
语法校验简单直接。先把 SQL 交给解析器,比如 PostgreSQL 的 EXPLAIN 或者 MySQL 的 EXPLAIN FORMAT=JSON,在只读库上跑一遍。语法错误、表不存在、字段不存在这类问题在这一步就能拦住。注意这里说的是“在只读库上跑”,不是直接在生产库上执行。
口径校验才是重点。语法对的 SQL 不代表口径对。一个常见做法是让模型在生成 SQL 的同时输出一段口径说明,然后用规则或另一个 LLM 调用对拍。校验的内容包括:
- WHERE 条件里的过滤是否符合业务规则定义
- JOIN 是否用对了关联键
- 聚合粒度是否正确,有没有把订单级和用户级混在一起
- 时间范围是否符合问题中的“上月”“近 7 天”等表述
- 有没有把测试账号、内部账号排除掉
下面是一个简化的校验提示词结构:
请检查以下 SQL 是否符合业务规则:
问题:上月新增付费用户数
业务规则:
- "付费用户"定义为存在 status='paid' 且无退款记录的订单
- 时间范围使用 order 创建时间
- 排除 user_type='internal' 的测试账号
生成的 SQL:
SELECT COUNT(DISTINCT user_id) FROM orders
WHERE created_at >= '2025-06-01'
AND created_at < '2025-07-01'
AND status = 'paid'
请逐条判断,并指出不符合规则的地方。
这一步的验收标准是非法 SQL 拦截率和口径偏离检出率。准备 30 个含错误 SQL 和正确 SQL 的混合样本,看校验环节能不能把有问题的都拦下来,同时不误伤太多正确 SQL。如果拦截率上不去,回退方案是缩小校验范围,先只做最容易出错的几类检查:时间范围、枚举值、测试账号过滤、金额单位。把规则写死成代码或严格的 prompt 约束,比让模型自由判断更可靠。

第四步:结果验证——抽样核对,别只信执行成功
SQL 执行成功只代表语法没问题,不代表答案对。结果验证要做的,是把智能体返回的数字和已知正确的参照值做对比。
一个低成本的做法是准备一组“黄金问题集”:这些问题在业务方那里有已经确认的标准答案,比如“上月总订单数”“本月活跃用户数”。每次上线或调整 prompt 后,跑一遍黄金问题集,看智能体的输出和标准答案的偏差。偏差超过阈值,比如 1%,就说明检索、生成或校验的某个环节出了问题。
另一个做法是抽样核对。对于线上真实用户的问题,定期抽几条,人工看生成的 SQL 和返回结果是否合理。不需要抽很多,每天抽 5 到 10 条,积累一周就能发现不少问题。
这一步的验收标准是黄金问题集通过率。如果 20 个黄金问题里 18 个以上和标准答案一致,才算基本可用。达不到的话,回退方案不是继续调 prompt,而是从上三步往回排查:先看结果不对的问题,是 SQL 校验没拦住,还是检索没召回正确的表,还是元数据本身就没定义清楚。定位到哪一步,就回退到哪一步修。
常见问题排查
落地过程中有几个问题会反复出现,提前知道排查方向能省不少时间。
字段幻觉:模型生成的 SQL 里出现了不存在的字段或表。原因是检索环节没召回目标表结构,或者元数据里字段注释太模糊,模型自己“补”了一个。排查方向是检查召回日志,看目标表有没有出现在上下文里;如果没出现,问题在检索;如果出现了但模型还是编造,问题在元数据质量和校验环节。
口径不一致:同一个问题,不同时间问答案不一样。原因是 prompt 里的业务规则不够明确,模型每次理解有偏差。排查方向是检查口径校验环节有没有生效,以及业务规则描述里有没有歧义词,比如“活跃”指的是登录还是下单。
权限过大:智能体连的是生产库,且使用了一个有写权限的账号。这是最危险的问题。解决方式是只读沙箱约束:为智能体单独创建只读账号,限制在必要的表范围,禁止 INSERT、UPDATE、DELETE、DROP 等写操作;同时在执行层做白名单校验,拦截不在允许列表里的 SQL 类型。即使模型生成了一条 DELETE FROM orders,沙箱账号也没有权限执行。
| 问题现象 | 常见原因 | 排查方向 |
|---|---|---|
| SQL 里出现不存在的字段 | 检索未召回目标表,模型幻觉补全 | 检查召回日志,确认元数据是否进入上下文 |
| 数字和 BI 报表对不上 | 业务口径定义不明确,模型理解偏差 | 检查口径规则描述是否有歧义,校验环节是否生效 |
| 同一问题答案不稳定 | prompt 温度过高或规则不完整 | 降低温度参数,补充明确的过滤规则 |
| 执行报错 | 表名或字段名写错、SQL 方言不匹配 | 检查元数据中的表名是否与实际库一致,确认方言设置 |
落地建议:从最小可行配置开始
不建议一上来就追求完美的语义层和复杂的校验链路。先做最小可行版本:挑一个业务域,准备 15 到 20 张核心表的元数据,配置好混合检索,加一道语法校验和两三行最核心的口径规则,用只读账号连接,然后跑黄金问题集。跑通了再扩表、加规则、上更细的验证。
NL2SQL 能不能在生产环境用起来,关键不在于模型写得有多快,而在于你相不相信它给出的数字。四步走下来,每一步都有一个明确的验收点:元数据够不够、检索准不准、校验拦不拦得住、结果对不对得上。把前三个环节做扎实,第四个环节的数字才会慢慢变得可信。

评论列表 (1条):
加载更多评论 Loading...