零样本NL2SQL实战:Schema Linking与执行反馈流水线 零样本 NL2SQL 这件事我前后折腾了大半年从最开始“拿个大模型直接问一句能不能写 SQL”到后来搭出一套能稳定跑在几十个陌生库上的流水线中间踩的坑基本都集中在同一个地方模型不是不会写 SQL而是不知道你的库长什么样。一旦表的命名稍微飘一点、字段带个前缀、或者问题里出现了库里没有的行业黑话生成的语句就开始“看起来很像、跑起来全错”。这套方案我在内部顺手起了个名字叫 ZeroNL2SQL顾名思义就是把“零样本”当成第一约束——没有任何目标库的标注问答对没有针对性的微调全靠 schema 理解加执行反馈把结果压上去。它适合两类人一类是手上有大量长尾数据库、根本没人力给每个库标注训练数据的工程团队另一类是想搞清楚 NL2SQL 到底卡在哪一环、想自己动手复现一套评估流水线的技术同学。下面我按“问题拆解、方案设计、核心实现、参数调优、问题排查、效果评估”这个顺序把整条链路和盘托出。1. 先把问题拆开零样本 NL2SQL 到底难在哪1.1 三种“没见过”要分开算账很多人把“零样本”当成一个笼统的概念实测下来必须拆成三层因为每一层的应对手段完全不同。第一层是库没见过模型在训练阶段根本没有接触过目标数据库的表结构。这是最表层的零样本也是最容易缓解的因为你可以把 schema 完整地塞进上下文里。第二层是领域没见过库本身可能见过一些但业务领域的术语、口径、隐含约定是陌生的。比如“月度活跃”到底按登录算还是按下单算“新客”是首单还是首访问这类信息不在 schema 里只存在于业务方的脑子里。这一层是零样本 NL2SQL 真正的天花板。第三层是问法没见过同一个意图用户可能用“上个月卖了多少”“近三十天成交额”“最近一个月营收情况”三种说法。模型需要把口语化的表达映射到确定的列和时间窗口上这个过程业内叫schema linking我个人认为它比 SQL 语法生成本身更重要。我做方案设计时的第一判断是第一层靠上下文工程解决第二层靠业务词典和澄清机制兜底第三层靠检索加规则约束。三条线分开投入才不会出现“模型换了个更大的还是不行”这种无效迭代。1.2 从“翻译任务”到“推理任务”的认知转变早期我把 NL2SQL 当成机器翻译来做自然语言进SQL 出。这个思路在小库、单表、简单条件上表现还行一旦上多表 JOIN 就崩。原因在于翻译是逐词对齐的而 SQL 生成需要先做结构决策——先确定主表是谁再确定关联路径再确定聚合粒度最后才是条件和排序。我举个具体的例子。“统计每个城市下单超过三笔的用户数”——这句话里至少藏着四步推理主表是订单表需要按用户聚合计数过滤条件是计数大于三输出维度是城市而城市大概率不在订单表里需要 JOIN 用户表。模型如果跳过前三步直接开始写 SELECT写出来的东西十有八九是错的。所以我在设计提示词的时候刻意把“先想清楚再写”这个动作显式化让模型先输出一段结构化的思考摘要再输出 SQL。这个改动听起来很小但在多表场景下的提升非常明显。让模型把推理过程外化本质上是在给它自己创造中间变量这和人类写复杂 SQL 前先在纸上画关系图是一个道理。1.3 为什么值得死磕零样本有人会问既然少样本效果更好为什么不干脆给每个库标几十条数据答案很现实长尾库太多了标注成本根本摊不平。我做过一个粗略估算一个中等复杂度的库要让少样本方案达到可用水平至少需要 30 到 50 条高质量问答对而且得覆盖各种查询模式。标一条平均要 10 到 15 分钟因为标注的人得先理解业务口径还得自己写 SQL 验证。一个库就是 5 到 12 个小时。如果有两百个库呢这就是一个人几个月的工作量而且业务一变标注就过期。零样本路线的价值在于边际成本接近于零新库接进来只要能连上、能读到 schema当天就能出第一版效果。哪怕准确率只有六成也远比“等标注排期”要有价值。我的实际做法是零样本打底跑一段时间把高频失败样本捞出来再针对性地补少量示例形成“零样本为主、少样本补丁”的混合形态。这个思路后面第 6 章会展开。1.4 评估口径必须先定死这一条是我踩过最贵的坑。刚开始我用文本相似度去比对生成的 SQL 和标准答案结果发现模型写了一堆语义完全正确但写法不同的语句全被判错——比如WHERE a 3 AND b 5和WHERE b 5 AND a 3再比如用 JOIN 和用子查询实现同一个逻辑。后来我全部改成执行结果比对把生成的 SQL 和标准 SQL 分别放到只读副本上跑比对结果集。比对时要注意两点一是排序不稳定得先归一化除非原句明确要求排序二是浮点数要设容差我一般用相对误差 1e-6。这个口径定下来之后我才算真正看清了模型的水平之前那些“文本相似度高但跑不对”的假象全部暴露出来。2. 整体方案设计一套可复现的零样本流水线2.1 流水线总览与各环节职责整套 ZeroNL2SQL 我拆成了六个环节每个环节职责单一方便单独定位问题环节输入输出主要手段库结构抽取数据库连接结构化 schema读元数据表Schema 裁剪问题 全量 schema候选表列集合关键词召回 向量召回值匹配问题中的实体词命中值列表列值索引 模糊匹配提示组装上述全部完整 prompt模板 规则SQL 生成prompt候选 SQL 若干条多路采样执行校验与修复候选 SQL最终 SQL试跑 错误回灌我特意把“裁剪”和“值匹配”单独拎出来是因为这两个环节决定了上下文里放什么。大模型的注意力是有限资源塞进去的无关表越多选错表的概率越高。这一点在后面 4.2 节还会细说。2.2 把数据库“讲”给模型听schema 表示的三次迭代最早的版本我直接SHOW CREATE TABLE把建表语句原样塞进去。跑下来问题一堆注释缺失、字段名是拼音缩写、外键约束经常没建。模型看着cst_id、ord_amt这种字段只能瞎猜。第二次迭代我改成结构化描述每张表按“表名 中文注释 字段列表字段名、类型、注释、示例值”的格式组织。这一版明显好转因为示例值给了模型极强的语义锚点。比如status字段光看名字不知道有哪些取值给出“1待付款, 2已付款, 3已取消”之后模型判断过滤条件的准确率立马上来了。第三次迭代加了两个东西一是主外键关系显式声明因为很多库的外键没建我从命名规律和值分布上推断出来再写进去二是表的行数量级帮助模型判断该表适合做主表还是维度表。最终的 schema 片段大概长这样表: order_info (订单主表, 约 2400 万行) 字段: - id: bigint, 主键 - user_id: bigint, 用户ID, 关联 user_info.id - amount: decimal(12,2), 订单金额, 单位元 - status: tinyint, 订单状态, 取值 {1:待付款, 2:已付款, 3:已取消, 4:已退款} - created_at: datetime, 下单时间 索引: user_id, created_at我把这种格式称为**“带注释的 DDL 摘要”**。实测下来它比纯 DDL 好也比纯 JSON 好——JSON 结构清晰但太啰嗦token 消耗翻倍纯 DDL 又太干瘪。这种半结构化文本在信息密度和可读性之间找到了平衡点。2.3 提示词骨架四个区块的固定结构提示词我固定成四块顺序不乱角色与任务声明一句话说清“你是 SQL 生成器只输出 SQL”避免模型开始闲聊。规则约束方言我用 MySQL 语法为准、必须加 LIMIT、禁止 DML、时间函数用哪种写法、NULL 如何处理。这些规则要短、要硬。Schema 上下文裁剪后的相关表结构。问题与输出格式用户原话加上“先给一句思路再给 SQL用固定分隔符隔开”。规则这块我建议宁可写死也不要留模糊。早期我写过“尽量使用索引友好的写法”结果模型开始自作主张改写逻辑。后来我把所有软性建议全部删掉只保留能被程序校验的硬规则。提示词里的每一条模糊表述都会变成一次不可预测的输出。2.4 为什么选提示工程而不是微调这是方案选型上的关键决策我把当时的对比列出来维度零样本提示通用微调目标库微调新库接入成本分钟级天级周级需标注对 schema 变化的适应性强改了就能生效中弱需重训复杂 JOIN 能力依赖模型本身可提升最强运维复杂度低中高单位查询成本较高长上下文低低我当时的判断是先上零样本用成本换时间等业务量起来、确认哪些库是高频库之后再考虑对高频库做蒸馏或者轻量微调。事实证明这个顺序是对的因为很多库接入之后发现查询量极低根本不值得投入训练资源。先跑起来再优化永远比先优化再跑起来划算。3. 核心环节实现Schema Linking 与 SQL 生成3.1 表列召回先把搜索空间砍到十分之一一个中型业务库动辄上百张表全塞进上下文既贵又容易干扰。我的做法是双路召回再合并第一路是字面召回。把问题分词后跟表名、字段名、字段注释做匹配。中文场景要注意分词粒度“本月新增用户”切出来是“本月/新增/用户”其中“用户”能命中user_info。这一路召回率高但噪声大。第二路是向量召回。把每张表的“表名 注释 字段注释拼接文本”做向量化问题也向量化算余弦相似度取 top-k。这一路能处理同义表达比如“成交”能召回“订单”相关的表。两路结果合并去重后我一般控制在 8 到 12 张表。这个数量不是拍脑袋定的我做过一轮实验召回表数从 5 加到 20准确率先升后降拐点大概在 10 到 12 之间。表太少会漏掉关联表太多则会分散注意力。如果你的库普遍是宽表、单表查询为主这个数值可以再往下压到 5 到 6。另外有个小技巧外键关联表要强制加入。主表召回之后把它所有一层邻居表无条件带上因为 JOIN 路径经常是模型自己想不到的。3.2 值匹配把口语里的实体对到库里的真实取值这一步最容易被忽略但对准确率的贡献可能超过 20%。用户说“华东区的订单”库里存的是region_code E01。模型光看 schema 是不知道这个映射的。我的做法是给高频低基数列建一份值索引扫描该列的去重值如果基数小于 5000 就全存超过就取样。然后把问题里可能的实体词跟这些值做模糊匹配命中的值以“列名 - 候选值”的形式附在 schema 后面。值提示: region_info.region_name 可能匹配: [华东, 华东大区] order_info.status 可能匹配: [已付款 - 2]要注意的是值匹配必须给置信度和多个候选不能替模型做决定。我早期直接替换成唯一值结果遇到“东北”同时匹配到“东北”“东北大区”“东北三省”三个值的时候强行选一个反而制造了错误。后来我改成列出候选让模型自己判断准确率反而更稳。3.3 输出约束让模型只能吐 SQL模型输出格式失控是零样本方案的头号敌人。我的处理分三层第一层是提示层约束明确要求“先输出一行思路然后用---SQL---分隔后面只跟一条 SQL不要解释不要 Markdown 代码块”。第二层是程序层解析。拿到原始输出后按分隔符切取最后一段去掉可能的代码块标记再用 SQL 解析器做语法校验。这一步能拦掉大概 5% 到 8% 的畸形输出。第三层是语义层校验。解析出来的 AST 我至少检查四件事是不是只读语句只允许 SELECT碰到 INSERT/UPDATE/DELETE/DROP 直接拒、引用的表和列是不是都在给定的 schema 里、有没有明显的笛卡尔积FROM 里有多个表但没有关联条件、LIMIT 是否存在。这三层做完基本上不会出现“模型把库删了”这种事故。别觉得这是杞人忧天我在测试环境里真见过模型生成 DROP 语句就是因为有一次提示词改版把“禁止写操作”那条规则漏掉了。3.4 执行反馈让错误自己教模型改这是我认为零样本方案里性价比最高的一环。流程很简单生成的 SQL 拿去只读副本试跑报错了就把错误信息原样回灌给模型让它修一版。我设的修复轮数上限是 2 轮超过就放弃。回灌的提示大概是这样上一条 SQL 执行报错 Error 1054: Unknown column order_info.user_name in field list 请修正后重新输出仍然只输出 SQL。实测下来第一轮修复能救回大约 60% 的语法类错误和字段名错误。典型的可修复错误包括字段名拼错、表别名未定义、聚合函数位置不对、GROUP BY 漏了非聚合列。而不可修复的错误大多是逻辑性错误比如选错了表——这种错误 SQL 能跑通只是结果不对执行反馈就无能为力了。所以我特别强调执行反馈只能解决“跑不通”解决不了“跑得通但答非所问”。这两类问题要分开治理后者得靠 schema linking 和业务词典。3.5 多路采样与结果投票单条 SQL 的可靠性有限我的做法是同一个问题采样 N 条N 一般取 5 到 8然后做投票。投票不是比文本而是比执行结果。具体做法把 N 条 SQL 全部试跑结果集做归一化列排序、行排序、浮点容差然后按结果集分组取最大的一组作为答案。如果最大组的大小不到总数的一半说明模型自己都不确定这时候可以降级返回“需要澄清”。这个手段在我这里平均能带来 5 到 8 个点的准确率提升代价是推理成本涨了 N 倍。所以我通常做成分级策略先用 1 路跑如果执行报错或者置信度低比如生成的 SQL 里引用了 schema 外的表再升级到多路采样。这样一个库里真正需要多路的问题可能只占两成成本可控。4. 参数与工程细节把“能用”调到“好用”4.1 采样参数怎么定这块我给一组实际在用的数值供参考参数单路确定性生成多路采样投票temperature0 或 0.10.7 到 0.9top_p1.00.95最大输出长度512 token512 token路数 N15 到 8温度选 0.7 到 0.9 是有讲究的。太低比如 0.3采样出来的 SQL 几乎一样投票没意义太高比如 1.2会出现大量语法离谱的输出浪费执行资源。0.8 附近是我试出来多样性最好、同时语法合规率还能维持在九成以上的点。还有一点多路采样时不要让模型看到彼此的结果。我一开始偷懒把前几轮生成的 SQL 拼进上下文让它“参考改进”结果输出高度趋同投票退化成单路。多样性的前提是独立采样。4.2 上下文长度与 schema 裁剪的取舍上下文长度不是越长越好这一点我做过对比实验。同一个问题把全库 80 张表塞进去和只塞 10 张相关表执行准确率差了将近 15 个百分点。原因有两个一是无关表提供了错误的 JOIN 诱惑模型会去关联一些看起来似乎相关的表二是长上下文里关键信息被稀释模型对具体字段的注意力下降。我的经验阈值是schema 上下文的 token 数控制在模型窗口的三分之一以内。如果超了优先砍两类东西——行数极少的配置表、以及跟问题毫无字面交集的表。宁可让召回少一张表导致 JOIN 失败这种错误至少能被执行反馈抓住也不要塞一堆噪声让模型选错主表这种错误执行能过但结果全错。4.3 成本与延迟控制零样本方案的成本主要来自 token 消耗和试跑次数。我做的几件事一是缓存。schema 摘要、向量索引、值索引都是按库缓存的库结构不变就不重算。schema 变更我用元数据表的更新时间戳做失效判断简单可靠。二是并发。多路采样的 N 条请求并发发出而不是串行。串行的话延迟直接乘以 N用户体验没法看。并发之后整体延迟基本等于最慢的那一条。三是分级降级。我设了三档单路直出、单路加一轮修复、多路投票。前两档覆盖了大部分查询只有疑难问题才走第三档。这样算下来平均每个问题的成本大概是全量多路方案的三分之一。4.4 安全护栏必须做在数据库层前面说的程序层校验是应用层的但我不建议把安全完全托付给应用层。我的做法是连接数据库的账号只给 SELECT 权限并且只连只读副本。另外加上语句超时我设 30 秒和结果集行数上限。为什么要双保险因为应用层的 SQL 解析总会有绕过的方式比如注释混淆、多语句拼接。数据库权限是最后一道墙它不会因为解析器有 bug 就失效。能靠权限解决的不要靠代码解决。5. 常见问题与排查技巧实录5.1 高频错误速查表我把过去半年积累的错误样本做了归类大致是这么分布的错误类型典型表现占比主要对策选错表用了用户表而不是订单表约 30%强化表注释、加表用途说明字段名错列名拼写或前缀错约 20%值索引 执行反馈修复聚合粒度错GROUP BY 维度漏了或多了约 18%提示里显式要求先定粒度JOIN 缺失多表查询没有关联条件约 12%强制带上外键关系时间条件错时间窗口理解偏差约 10%业务词典 当前日期注入值映射错中文描述没映射到编码约 6%值匹配环节语法/函数错方言不匹配约 4%方言声明 执行修复这张表我建议每个做这条线的人都自己统计一份因为错误分布直接决定了你该往哪儿投人力。比如选错表占了三分之一那你的优化重点就应该是 schema 描述的质量而不是去调采样温度。5.2 几个真实排查过程有一个案例我印象很深。某个库上的准确率一直卡在五成上不去把错误样本拉出来一看大量查询把“客户”映射到了customer表而业务上真正应该用的是account表——这两张表在库里都存在customer是早期废弃的存量表字段结构几乎一样。这个问题靠执行反馈完全抓不住因为错的 SQL 也能跑通。最后的解决办法是在 schema 描述里给customer表加一行“历史归档表已停用请勿使用”准确率立刻上去了。很多模型错误其实是数据治理问题的外化模型没错是库本身就乱。另一个案例是时间理解。用户说“上个月”模型有时候按自然月算有时候按 30 天算。更麻烦的是模型的“当前时间”是训练时的截止时间不是真实时间。我的处理是每次都在提示里显式注入当天日期和本周起止日并且规定“上个月”统一解释为上一个自然月。这类模糊表述必须在规则层统一不能指望模型自己猜。还有一个比较隐蔽的坑中文字段注释里的括号和特殊符号导致解析失败。我在组装 schema 文本时用了自定义分隔符结果某个表的注释里恰好包含了同样的符号导致整个 prompt 结构错乱。后来改成用不易冲突的多字符分隔符并且在拼装前做一次转义清洗。5.3 我踩过的几条硬教训第一条不要把标准答案写进提示词的示例里。我早期为了提升效果把几个典型问答对放进提示词结果里面恰好有一条跟测试问题很像的模型直接照抄在离线评测上分数虚高上线后原形毕露。示例只能是通用模式不能是答案。第二条离线评测集必须按库划分。如果同一个库的样本同时出现在开发集和测试集里你实际上是在做少样本测出来的数字毫无意义。我要求开发阶段完全不碰测试库的 schema。第三条别信单次评测。大模型输出有随机性同一个测试集跑三次准确率能差好几个点。我的做法是固定随机种子跑三遍取平均波动超过 3 个点就说明方案不稳定需要回头查原因。第四条报错信息别做过度加工。我试过把数据库错误信息翻译成人话再回灌效果反而变差因为原始错误里的字段名、表名对模型定位问题很关键。原样回灌就够了最多加一句“请修正”。6. 效果评估与后续扩展6.1 搭一套自己的离线评测我强烈建议任何想做这条线的人第一件事不是写提示词而是搭评测。我的评测集构成是这样的每个库挑 40 到 60 条问题按难度分层——单表简单过滤占四成多表 JOIN 占三成聚合与子查询占两成剩下的留给刁钻问题嵌套聚合、窗口函数、同环比。每条样本我存四样东西问题原文、标准 SQL、标准结果集、难度标签。评测脚本做三件事跑生成、跑执行、按结果集比对。输出一份明细表包含每条样本是否正确、错误类型、耗时、token 消耗。这套东西搭起来大概要两天但它带来的价值是持续的。没有评测集的调优就是玄学你会陷入“改了提示词感觉好了一点”的自我安慰里永远说不清到底哪一版更好。6.2 从纯零样本到混合形态的演进路径跑通零样本之后我的演进路线是这样的第一阶段纯零样本目标是先摸清各个库的难度分布找出哪些库天生适合这条路。第二阶段加业务词典。把高频的术语、口径、字段别名整理成一份可维护的配置文件在提示组装时注入。这一步的投入产出比非常高我见过一个库加了词典之后准确率直接涨了十几个点。第三阶段针对性补示例。只对高频库、高频查询模式补少量示例一般每个模式 3 到 5 条就够。注意这里补的是模式示例而不是答案——比如教它这个库习惯怎么写时间过滤、怎么写多对多关联。第四阶段才考虑轻量微调或者模型蒸馏。这时候你已经有足够的数据积累和明确的优化方向了不会盲目投入。我个人认为绝大多数团队走到第三阶段就够用了。零样本的价值不在于它本身有多强而在于它让你在没有任何标注成本的前提下先看清楚问题到底出在哪。等你看清楚了再决定往哪儿加资源这个顺序不能反。最后分享一个实操上的小体会我一开始总想着把准确率从六成推到八成结果发现投入产出极不划算。后来改成把“低置信度”识别出来交给澄清机制或者人工兜底整体可用率反而上去了。用户对“问一句再答”的容忍度远高于对“自信地给出错误答案”的容忍度。这个取舍我觉得比任何一个提示词技巧都更重要。