上周有个做数据平台的朋友找我吐槽说他们给业务侧做了个用大白话查数据的入口在自家数仓上跑得挺像回事演示会上老板随口问了一句上个月华东区退货率最高的三个品类回车下去SQL 生成得又快又漂亮结果一执行——列不存在。后来才知道模型把refund_rate记成了return_rate因为在训练语料里这两个词几乎总是成对出现。换到另一个从没见过的库问题就更夸张了表名、列名、外键关系全变了模型开始一本正经地编字段。这件事很典型地把ZeroNL2SQL零样本 NL2SQL的处境摊在了台面上。所谓零样本不是不训练而是模型在推理时面对的是一个它从未见过的数据库 schema——没见过的表名、没见过的列名、没见过的业务黑话、没见过的主外键拓扑。它得只靠当前给它的这点上下文把一句人话翻译成能跑通、结果正确的 SQL。这件事在自家库上做微调很容易糊弄过去一旦要求跨库、跨域、跨方言难度是断崖式上升的。我前后在几个数据问答类项目里折腾过这条路线从最早的模板匹配、到基于序列到序列模型的跨域迁移、再到这两年被大模型彻底改写玩法踩过的坑基本都集中在一件事上零样本 NL2SQL 的胜负手八成不在模型会不会写 SQL而在你喂给它的数据库上下文够不够、准不准、干不干净。下面我按自己的实操顺序把这件事从难点拆解、上下文组织、解码保险、子问题拆解、评测归因到上线边界一条条捋清楚。做过 Text-to-SQL 的能直接对号入座没做过的也能照着落地。1. 零样本 NL2SQL 到底卡在哪三处1.1 换个库就失灵模型其实在偷偷依赖熟词很多人第一次做跨库测试时会有强烈的挫败感明明在源库上准确率能到七成换到目标库直接掉到两成出头。这不是模型突然变笨而是它之前那七成里有相当一部分是靠记忆拿到的不是靠推理。举个具体的例子。在电商类的库里orders表几乎一定存在order_items也几乎一定存在status字段的枚举值大概率是paid / shipped / cancelled这类。模型在预训练语料里见过海量的建表语句和查询语句它已经把这些行业惯例背下来了。所以在源库上它其实不需要真的理解 schema linking只要看到 订单 和 状态 就能条件反射出正确字段。零样本场景下这套反射就失效了。目标库可能把订单表叫t_trade_main把状态叫trade_stat枚举值是数字10/20/30甚至这个库根本没有订单表只有一张宽表dwd_trade_detail。这时候模型必须真的去读 schema、真的去做字面对齐任何一步偷懒都会直接表现成生成的列名不存在。我在做归因统计时发现一个规律在源库上表现越好的模型跨库掉得可能越狠因为它对记忆的依赖更重。所以判断一个方案能不能扛零样本不能看同库准确率必须看它在完全陌生 schema 上的一次通过率。1.2 三块硬骨头对齐、词表、执行语义把失败案例拆开看零样本的难点可以收敛成三个层次我习惯按这个顺序排查层次核心问题典型失败表现排查手段schema linking问题里的词能不能落到正确的表和列上选错表、选错列、该 join 没 join人工标注 mention 到列的映射算召回词表与值匹配字面量能不能对上库里的真实值WHERE 条件永远查不到数据、返回空结果抽样看生成 SQL 的 WHERE 值是否真实存在于库中执行语义聚合、去重、排序、时间口径对不对SQL 能跑通但数字对不上对比结果集不对比 SQL 文本第一层是语义对齐。用户说客户库里可能是customer、cust、member、buyer用户说成交额库里可能是pay_amt、gmv、total_fee。零样本下没有标注好的对齐表只能靠列名本身的可读性、注释、以及少量样本值来推断。第二层是值匹配这层最容易被忽略。表名列名对齐了条件值对不上SQL 照样白写。用户问华东区库里存的可能是华东大区、可能是region_id 3。没有任何值检索机制的话模型大概率会生成WHERE region 华东然后返回 0 行——而 0 行结果在自动评测里很多时候会被判成错误但在线上你甚至不知道它是错的用户只会觉得这系统查不出东西。第三层是执行语义。前三层都对SQL 也能跑但结果不对。最典型的是去重问题多表 join 后没加DISTINCT订单数被行数放大或者时间口径问题上个月到底按自然月还是按滚动 30 天。这类错误在文本比对里看不出来只有执行结果对比才暴露。1.3 零样本、少样本、跨库微调边界别搞混这几个概念经常被混着说我在方案评审时一定会先钉死定义否则指标没法比。同库同分布训练和测试用同一个数据库只是问题不同。这是最简单也最没参考价值的设定准确率动辄八九十但对零样本毫无指导意义。跨库cross-database训练见 A 库测试见 B 库但都属于同一个领域schema 风格接近。这是过去几年主流基准的做法考的是泛化。零样本zero-shot目标库完全没参与任何训练或微调连领域都可能换掉比如训练在电商库上测试在教育库、医疗库上。同时不提供任何目标库的示例问答对。少样本few-shot目标库给几条示例问答可以是人工写的也可以是自动合成的。严格说这已经不是零样本了但工程上它往往是零样本方案效果不够时的第一根救命稻草。把这条线划清楚很重要因为它直接决定了你能不能用给几条示例这个后门。我在实际项目里的做法是核心链路按真零样本设计示例作为可选增强层挂在外面这样即使某个库拿不到示例系统也不至于完全不能跑。2. 把陌生的库讲给模型听上下文怎么组织2.1 列名还原把pay_amt变成人能读懂的东西零样本条件下模型对目标库的唯一信息来源就是你塞进上下文的那段 schema 描述。这段描述的质量直接决定了上限。原始的建表语句DDL通常长这样CREATE TABLE t_trade_main ( trade_id BIGINT, mem_id BIGINT, pay_amt DECIMAL(18,2), trade_stat TINYINT, gmt_create DATETIME );直接把这坨东西扔给模型它能猜出个大概但trade_stat的枚举含义、pay_amt是不是含税、mem_id是买家还是会员全靠蒙。我的做法是做一层schema 增强把能拿到的元信息都翻译成人话表 t_trade_main交易主表一行代表一笔交易 trade_id 交易ID主键 mem_id 下单会员ID关联 t_member.member_id pay_amt 实付金额单位元已含税 trade_stat 交易状态10待付款, 20已付款, 30已发货, 40已完成, 50已取消 gmt_create 创建时间关键点在于三件事表的中文语义、列的取值含义、单位与时间口径。这三样补齐之后模型对陌生库的冷启动能明显好转因为它不需要再从缩写下猜语义了。如果库里本来就有注释优先用注释没有注释的话可以用规则模型辅助的方式生成把列名按驼峰和下划线切开做一次中英词典映射再让模型结合同表的其他列名做一次语义补全。这层增强是离线的一次生成、长期复用成本很低收益很高。2.2 连接路径别让模型自己猜外键多表查询是零样本最容易崩的地方。单表查询模型基本不会错一旦涉及两三张表join 条件写错的概率会陡增。原因很简单模型不知道这两张表该用什么字段连。它可能猜到t_trade_main.mem_id t_member.id但库里会员表的主键可能叫member_id甚至可能是一个复合主键。我的处理方式是把外键关系显式写进上下文而不是指望模型从列名相似度里推关联关系 t_trade_main.mem_id - t_member.member_id 多对一 t_trade_main.trade_id - t_trade_item.trade_id 一对多 t_trade_item.sku_id - t_sku.sku_id 多对一 t_sku.cate_id - t_category.cate_id 多对一如果数据库本身有外键约束直接读元数据表就行如果像很多数仓那样根本没有外键约束这是常态就得靠字段名匹配、数据分布比对比如判断 A 列的值域是否被 B 列覆盖来推断。这一步虽然笨但一次做好后面所有查询都受益。更进一步的优化是预计算连接路径。用户问各品类销售额需要t_trade_main - t_trade_item - t_sku - t_category四跳。如果每次都让模型自己找路出错率很高。我会在离线段用图搜索在 schema 图上把常见表对之间的最短路径算出来存成一张路径表检索时优先注入。这招在多表 join 场景里效果很明显。2.3 值检索字面量对不上SQL 必错前面说过条件值匹配是最隐蔽的坑。零样本下没有标注数据告诉你华东在库里到底叫什么只能靠检索。我的做法是给每个可能被当作过滤条件的列建一个取值索引。不是全量建那样太贵而是针对低基数列枚举、地区、品类、状态做。具体三步对候选列跑一次SELECT col, COUNT(*) FROM t GROUP BY col ORDER BY 2 DESC LIMIT 200拿到高频值把这些值和列描述一起做成一个可检索的轻量索引用户提问时先用问题文本去这个索引里做模糊匹配编辑距离 字符 n-gram 向量相似度组合命中的值直接拼进上下文。举个实际效果用户问上海地区的订单库里存的其实是上海市。没有值检索模型生成WHERE city 上海返回空有了值检索上下文里会多出一行提示列 t_member.city 的已知取值示例上海市, 北京市, 杭州市, 深圳市 ...模型看到这个基本就会写对。这一层的投入产出比在所有优化里是最高的我建议凡是做零样本 NL2SQL值检索一定要做。2.4 上下文预算塞多少张表才不撑爆一个中等规模的库动辄两三百张表全塞进上下文既不现实也没必要反而会稀释注意力、拉低准确率。我的策略是两阶段召回第一阶段做表级召回。用问题文本去匹配所有表的描述表名 注释 列名拼接可以用 BM25 加向量检索混合召回 top 8 到 top 12 张候选表。同时把这几张表之间的连接路径一并带出来。第二阶段做列级裁剪。对每张召回的表不是所有列都塞进去而是按相关性排序每张表保留 15 到 25 个最相关的列主键和外键无条件保留。宽表一两百列那种尤其需要裁剪否则光一张表就能把上下文占满。这里有个经验值上下文里表数量控制在 10 张以内、总 token 控制在 3000 到 5000 之间时效果和成本比较平衡。超过这个量准确率提升很有限延迟和成本却线性上涨。当然这是我这边的场景具体还得按模型窗口和自己的库规模调。3. 解码阶段的三道保险约束、执行、投票3.1 语法约束从自由发挥到按文法出牌模型生成 SQL 最烦的问题不是逻辑错是格式错少个括号、多个逗号、用了不存在的函数、引号混用。这些错误在文本层面看毫无价值因为它的 SQL 根本跑不起来。我一般会加一层受限解码。思路不复杂把当前上下文里允许出现的表名、列名、关键字做成一个词表解码时对不在词表里的 token 做惩罚或者直接屏蔽。这样模型就没法凭空编出return_rate这种不存在的列。更严格一点的做法是用 SQL 文法做约束。把目标方言的文法整理成上下文无关文法转成状态机解码时按状态机允许的 token 集合做过滤。工程量不小但收益是生成结果的语法合法率能从七八成提到接近百分之百。如果不想上这么重的手段还有个轻量替代生成后做一次静态校验。用目标方言的解析器大部分语言都有现成的 SQL parser把生成的 SQL 解析一遍解析失败就把错误信息回灌给模型重试一轮。这招简单粗暴但很有效我在好几个项目里都是先上这个。3.2 执行引导让数据库当裁判零样本场景下模型对同一个问题往往能生成好几个看起来都对的 SQL。这时候靠什么选靠执行。具体做法是让模型采样出 N 个候选N 一般 4 到 8逐个执行然后按一套规则重排执行报错的直接淘汰执行超时的降权返回结果为空且问题明显不该为空的降权返回结果非空、行数合理的加权。这里有个坑我必须提醒空结果不等于错。用户真的可能在问一个没有数据的时间段。所以空结果降权只能作为软信号不能作为硬淘汰否则会把正确的 SQL 干掉。我一般的处理是给空结果一个中等惩罚而不是一票否决。另一个细节是执行成本。候选 SQL 直接打到生产库上是危险的一定要走只读从库或者沙箱并且加上超时和LIMIT兜底。我吃过一次亏一个没有 limit 的全表扫描打上去直接把从库拖慢了好几分钟。3.3 自洽投票按结果集聚类而不是按文本自洽性self-consistency这套玩法大家都熟但在 NL2SQL 上有个特殊之处不同的 SQL 文本可能完全等价。SELECT COUNT(*) FROM t WHERE status 1和SELECT COUNT(1) FROM t WHERE status 1是同一个意思按文本聚类会把它们拆成两票白白浪费。所以我在 NL2SQL 上做投票时按执行结果聚类把每个候选 SQL 的执行结果排序后做成一个指纹指纹相同的归为一类票数最多的那一类胜出。这个方法实测比文本投票稳尤其是在模型对写法有分歧、但语义其实一致的场景下。代价是要多执行几次成本会上去。我的折中是只在置信度低的问题上开启多次采样高置信度的直接一次过。判断置信度可以用生成概率、也可以用候选之间的一致性实现上比较灵活。3.4 兜底什么时候该老实说我不会这一点很多人不做但上线之后一定会被问到。零样本系统一定会有它搞不定的问题库结构太怪、问题太模糊、涉及的业务口径上下文里根本没有。我的原则是宁可拒答不要瞎答。判定信号可以组合使用候选 SQL 之间执行结果分歧极大、生成的 SQL 里出现了不在上下文里的列、所有候选都执行失败、或者问题里出现了上下文完全无法对齐的关键词。命中这些信号就返回这个问题我没把握建议换个说法或者联系数据同学同时把原始问题和召回的表结构记进日志。用户对我不知道的容忍度远高于对给了我一个错数字的容忍度。后者一旦发生一次整个系统的信任就没了。4. 把大问题拆成能验证的小问题4.1 骨架先行先定聚合与过滤再填字段一步到位生成完整 SQL对模型的要求其实很高它得同时想清楚用哪几张表、怎么 join、用什么聚合、加什么过滤、怎么排序。任何一步错整体就废。我比较喜欢的一种拆法是先出骨架再填内容。骨架只描述要什么形状的结果几列、有没有聚合、有没有分组、有没有排序和 limit。比如上面的问题骨架大概是这样输出列品类名称, 退货率 聚合按品类分组退货率 退货单数 / 订单数 过滤时间在上个月 排序按退货率降序 限制前 3骨架的好处是可以单独校验。退货率这个指标的口径对不对、时间范围对不对这些在骨架层就能看出来不用等到 SQL 生成完才发现。骨架确认之后再让模型去填具体的表和字段出错面就小了很多。这一步在工程上还有个额外收益骨架是结构化的可以拿来做中间结果缓存和人工审核。有些团队会让数据同学先审核一批骨架模板后续同类问题直接复用稳定性提升明显。4.2 中间表示让模型输出比 SQL 更好校验的东西直接输出 SQL 有个麻烦SQL 是线性的文本你很难在生成过程中判断它是否偏离。用一层中间表示IR会好很多。IR 可以简单到就是一个 JSON{ select: [{table: t_category, column: cate_name, alias: 品类}], aggregations: [{func: divide, args: [退货单数, 订单数], alias: 退货率}], joins: [ {left: t_trade_main, right: t_trade_item, on: [trade_id, trade_id]}, {left: t_trade_item, right: t_sku, on: [sku_id, sku_id]} ], where: [{column: t_trade_main.gmt_create, op: between, value: 上月}], group_by: [t_category.cate_name], order_by: [{column: 退货率, direction: desc}], limit: 3 }JSON 的好处是每一部分都能单独校验表名在不在召回列表里、列名在不在表里、join 的两端是不是真的有外键关系、时间表达式能不能解析。校验通过之后再序列化成 SQL语法错误的概率大幅降低。坏处是多了一步延迟会上去。所以这套我一般用在复杂查询上简单的单表查询还是让模型直接出 SQL 更快。4.3 双向校验问题里的词能不能落到列上这是我自己的一个土办法但很管用生成完 SQL 之后反过来检查一遍。具体是两步。第一步把生成的 SQL 里用到的表和列列出来逐个回查这些列是不是真的存在于上下文里不存在就打回重来。第二步把问题里的实词名词、限定词抽出来看每一个是不是都能在 SQL 里找到对应的落点——要么落到了某个列上要么落到了某个过滤条件上要么落到了聚合上。如果问题里有个词完全找不到落点大概率是漏了条件或者漏了列。比如用户问华东区退货率最高的三个品类SQL 里如果没有出现任何地区相关的过滤那就明显漏了。这个检查不需要模型参与纯规则就能做速度快、成本低能捞回不少低级错误。4.4 自我修正循环把报错信息当成下一轮的输入前面几步都过了SQL 还是可能执行失败。这时候别急着放弃把数据库返回的报错原样回灌给模型让它自己改。报错信息里其实信息量很大Unknown column return_rate in field list直接告诉模型哪一列不存在Every derived table must have its own alias告诉它少了个别名。模型看到这些往往一轮就能改对。我一般设一个最多两轮的修正上限。一轮修不好两轮基本也修不好再往下就是浪费 token 和时间。而且要注意修正的时候上下文里要带上原来失败的 SQL 和报错不能只给报错否则模型不知道自己错在哪。还有一个变种玩法结果校验式修正。SQL 执行成功了但返回结果明显异常比如退货率算出来是 350%这时候也可以触发一轮修正提示模型结果是 350%可能聚合口径有问题请检查。这个信号比较弱容易误触发我一般只在指标类查询上开。5. 怎么判断零样本是真零样本5.1 泄漏排查预训练语料里有没有这个库这是最容易被忽略、也最容易让指标虚高的一件事。如果你的目标库的建表语句、查询日志、数据字典曾经公开过那它很可能已经进了模型的预训练语料。这种情况下你测出来的零样本准确率其实是记忆准确率换一个真正陌生的库立刻打回原形。排查方法不复杂但需要点耐心拿目标库的表名和几个特征列名去搜索引擎和代码托管平台上搜一下看有没有公开的仓库、文档、问答帖。如果有就得把评测集换成真正没公开过的库。我在做内部评测时有个硬规矩评测用的库必须是自己内部私有、从未公开过的。宁可库小一点、领域偏一点也不能用公开库来刷指标。5.2 EX 和 EM 的选择NL2SQL 常用的两个指标含义完全不同别混用。EMExact Set Match比的是 SQL 结构看生成的 SQL 和标准答案在抽象语法树上是否一致。好处是严格坏处是会严重低估——两个语义完全等价的 SQL写法稍微不同就判错。EXExecution Accuracy比的是执行结果结果集一致就算对。这个更贴近真实需求用户关心的是数字对不对不是 SQL 长得像不像。我的建议是对外报指标一律用 EXEM 只在做错误归因时参考。另外 EX 也有陷阱如果测试库里恰好有多条路径能得到相同结果比如某个字段全是空值加不加这个过滤结果都一样EX 会误判为正确。所以评测集最好人工过一遍把这种歧义样本清掉。5.3 错误归因表把失败拆成可修的几类只看一个总准确率没有意义因为它不能告诉你下一步该修什么。我每次做完评测都会把所有错误样本人工或者半自动归到几类里形成一张归因表错误类型占错误总数比例典型原因改进方向schema linking 错约 30%表/列召回没覆盖到正确字段改进召回策略、补列描述值匹配错约 20%字面量对不上库里的真实值补值检索索引join 错约 15%多表路径选错、漏 join注入连接路径聚合/口径错约 15%去重没做、时间范围理解错加口径说明、骨架校验语法/执行错约 10%生成 SQL 跑不起来受限解码、报错回灌问题本身模糊约 10%用户表达不清、多义加澄清追问这张表的价值在于指导投入。如果 30% 的错误来自 schema linking那你花时间优化解码策略就是白费劲应该先去把列描述和召回做好。我在几个项目里都是靠这张表把准确率从看着还行推到敢上线的。5.4 小样本校准不做微调也能对齐方言严格零样本有个现实问题不同数据库的 SQL 方言差异很大。时间函数在一种方言里是DATE_SUB在另一种里是DATEADD字符串拼接有的用||有的用CONCAT。模型在跨方言时经常用错。这个问题不需要微调就能缓解。我的做法是准备十到二十条方言对照示例作为系统提示的一部分常驻在上下文里专门用来对齐写法本库方言说明 - 取上个月WHERE gmt_create DATE_TRUNC(month, CURRENT_DATE - INTERVAL 1 month) AND gmt_create DATE_TRUNC(month, CURRENT_DATE) - 字符串拼接CONCAT(a, b)不支持 || - 空值判断必须用 IS NULL不要用 NULL - 除法必须显式转浮点如 CAST(a AS DOUBLE) / b这些说明是通用知识不涉及具体业务数据所以不违反零样本的前提——目标库本身还是没见过的只是告诉了模型这门方言怎么说话。实测下来方言类的执行错误能减少一大半。6. 真上生产要考虑的事6.1 场景选择哪类问题适合交给零样本零样本不是万能的硬套在所有问题上会很痛苦。我的经验是下面这几类问题收益最高单表或双表的聚合查询如上个月各渠道的订单量schema linking 简单成功率高明细探查如帮我看看这几条异常订单的详情SQL 简单但对字段理解要求高零样本反而够用跨库的临时性查询业务同学偶尔要看一个平时不看的库没人愿意为它单独做一套模板这时候零样本是唯一解。不太适合的是多跳 join 的复杂指标计算、涉及强业务口径的问题比如活跃用户到底怎么定义、需要窗口函数的排名类问题。这些我一般会引导到预置指标 参数化查询的路径上让用户选口径而不是让模型猜。6.2 安全边界只读、白名单、超时与限额这条是红线必须做而且必须在数据库层面做不能只在应用层做。第一连接必须用只读账号只给目标库的 SELECT 权限其他权限一律不给。别指望靠解析 SQL 来拦截写操作总有绕过去的办法。第二语句白名单。生成的 SQL 必须是以 SELECT 或 WITH 开头的单条语句出现分号、出现INSERT/UPDATE/DELETE/DROP/ALTER/TRUNCATE一律拒绝出现注释符号也一律拒绝。第三强制加限制。无论模型生成的 SQL 有没有 limit外层都套一层LIMIT 1000。同时设置查询超时我一般设 30 秒超时直接掐断。第四行级权限注入。如果系统是多租户的生成的 SQL 外层要自动拼上租户过滤条件避免越权。这一步千万别交给模型做它一定会漏。第五全量审计。问题、召回的 schema、生成的 SQL、执行耗时、返回行数全部落日志。出问题的时候这份日志就是唯一的救命稻草。6.3 缓存、降级与人工兜底零样本链路长、成本高线上必须考虑降级。缓存分两层。第一层是文本精确缓存问题完全一致的直接命中这层简单但命中率低。第二层是语义缓存把问题做向量化相似度超过阈值就复用之前的 SQL。第二层要注意相似不等于相同上个月和上上个月向量距离很近但结果是两回事。我的处理是把时间表达式先抽出来单独比对时间不一致就不走缓存。降级分三级。一级是正常走完整链路二级是复杂查询失败时退化成只做单表查询三级是彻底失败时返回召回的表结构和字段列表让用户自己去查。降级路径一定要提前设计好不能等到线上出问题现想。人工兜底也很重要。我在界面上会放一个这个答案不对的反馈按钮点击之后把整条链路的信息打包记录下来进入一个待优化队列。这批数据比任何自动评测集都宝贵因为是真实用户踩出来的。6.4 一份可以直接抄的上线检查清单最后列一份我自己用的检查表上线前逐条过一遍检查项通过标准schema 描述完整性每张表的表语义、列含义、枚举值、单位都有描述外键关系覆盖常见表对的连接路径已预计算并注入值索引低基数列的高频值已建索引命中率可测表召回top-10 召回的覆盖率在测试集上不低于 90%受限解码/静态校验语法合法率不低于 98%结果校验空结果、异常值的处理策略已明确拒答机制低置信度问题能正确返回拒答而非瞎答权限与限流只读账号、超时、LIMIT、审计日志全部到位评测集独立性评测库未公开、未出现在任何训练语料中错误归因最近一批 badcase 已完成分类改进项有优先级我在实际项目里最大的体会是零样本 NL2SQL 的准确率天花板往往不是模型给的而是数据库元数据的质量给的。同样一套模型把列注释补齐、把枚举值写清楚、把外键关系补上准确率能涨十几个点比换模型、调提示词都来得实在。所以如果你正准备做这件事我的建议是先花一周时间把目标库的说明书写好再谈模型选型。另外再分享一个小技巧把线上真实的失败问题定期抽出来人工写成标准 SQL反哺到评测集里这套集子会越用越准比任何公开基准都更贴合你的业务。