用自然语言问数据库 → 生成 SQL → 执行返回结果。核心特性:SQL 对错可被自动验证(执行结果与 gold SQL 比对),基于此用 DPO 全自动构造偏好数据并微调基座模型。
Spider test 留出集(2147 题 / 40 个库,全程未用于任何配置选择):82.25% → 85.61%, +3.35 点,McNemar p=0.0000。 7B 开源模型 + LoRA、单张 RTX 4090、零人工标注—— 偏好数据的对错全部由执行比对自动裁定。
| Spider dev (1034) | Spider test (2147, 留出) | |
|---|---|---|
| 基座(Qwen2.5-Coder-7B-Instruct,零样本) | 81.24% | 82.25% |
| + DPO adapter | 85.20% | 85.61% |
📄 REPORT.md —— 本次实验的技术复盘(结论、诊断链、方法论教训、阴性结果一览)
项目全貌见 INTRO.md,开发铁律见 CLAUDE.md,阶段规划见 ROADMAP.md, 每个阶段的发现与实验推理链见 EXPERIMENTS.md, 复现前建议先扫一眼本文末尾的 已知环境坑。
许可证:MIT。
需要 Python 3.11+。推荐用 uv:
uv venv
uv pip install -e ".[dev]"或者用 pip:
python -m venv .venv
# Windows: .venv\Scripts\activate Linux/macOS: source .venv/bin/activate
pip install -e ".[dev]"注:
vllm/bitsandbytes需要 Linux + NVIDIA GPU 环境,阶段 2 之前用不到; 阶段 0/1 只依赖标准库,装不上它们不影响数据加载和模块一开发。
- 打开 Spider 官网 https://yale-lily.github.io/spider,点击 Spider Dataset 下载链接
(Google Drive,文件名类似
spider_data.zip,约 1 GB)。 - 解压后把其中的内容放到本仓库的
data/spider/下,最终目录结构必须是:
data/spider/
├── train_spider.json # 训练集(约 7000 条)
├── train_others.json # 训练集补充(约 1659 条)
├── dev.json # 开发集(约 1034 条)
├── tables.json # 所有数据库的 schema
└── database/ # 每个 db_id 一个子目录
├── concert_singer/
│ └── concert_singer.sqlite
├── pets_1/
│ └── pets_1.sqlite
└── ...
如果解压出来多了一层顶级目录(如
spider/或spider_data/),把里面的文件 上移一层,保证data/spider/dev.json直接存在。Google Drive 不便访问时,可用 Hugging Face 上的镜像(例如
xlangai/spider数据集仓库),但注意必须同时拿到database/目录下的 sqlite 文件,只有 json 无法做执行验证。
data/spider/ 与 outputs/ 均不入 git。
# 需先 pip install -e . (或手动把 src 加进 PYTHONPATH)
python -c "from text2sql.data.spider import load_dev; print(len(list(load_dev())))"
# 期望输出 dev 集样本数(约 1034)
python scripts/check_data.py
# 打印 dev/train 样本数,并随机抽一条样本展示 question / gold SQL /
# sqlite 文件路径 / schema 表名pytest tests/test_db.py -v # 执行器 + 比对器 + execution_accuracy首次运行会自动下载基座模型(Qwen/Qwen2.5-Coder-7B-Instruct,约 15GB)。
这台机器上必须先
export VLLM_USE_FLASHINFER_SAMPLER=0——本机只装了 NVIDIA 驱动没装 CUDA toolkit,vLLM 默认的 FlashInfer 采样器要现场 JIT 编译 CUDA kernel、 找不到nvcc会崩在 KV cache 初始化阶段。详见文末 已知环境坑 坑 1。
export VLLM_USE_FLASHINFER_SAMPLER=0
python scripts/run_eval.py --limit 50 # 先在小子集上跑通
python scripts/run_eval.py # 全量 dev每次 run 会在 outputs/eval/ 下落盘:config_snapshot.json(可复现)、
predictions.jsonl(逐条预测)、summary.json(汇总指标)。
验证多候选采样(阶段 4 造偏好对的前提):
python scripts/check_sampling.py --n 8 --num-questions 3test split 与 dev、train 零重叠,全程未用于任何配置选择,因此下表是无偏估计, 也是唯一能和官方榜单直接比较的数字(榜单口径就是 test)。
| 配置 | Spider test EX | 配对检验 |
|---|---|---|
| 无 adapter(阶段 2 canonical 口径) | 82.25%(1766/2147) | — |
| + DPO adapter(T1e) | 85.56%(1837/2147) | +3.31,161 翻转,p=0.0000 显著 |
| + DPO adapter(T1f,标签已修) | 85.61%(1838/2147) | +3.35,164 翻转,p=0.0000 显著 |
T1f 与 T1e 的差别只是修掉了偏好数据里 141 条被误判的负例。 在 dev 上这个修复值 +1.35 点(p=0.0094 显著),在 test 上是 +0.05 点(p=1.0000)—— 完全没有复现。 test 样本量是 dev 的两倍、功效更高,所以以 test 为准: 这个修复在正确性上是对的,但没有可测量的准确率收益。详见
EXPERIMENTS.md发现 7.1。
对比同期公开结果(同为 test EX):榜首 ~91.2%、DAIL-SQL+GPT-4 86.2%、 本项目 85.61%、DIN-SQL+GPT-4 85.3%。 本项目用的是 7B 开源模型 + LoRA、单张 RTX 4090、零人工标注—— 偏好数据的对错全部由模块一执行比对自动裁定。
保留的限制:官方用 test-suite EX(多库实例),我们是单库比对。 实测扰动验证的标记率 <1%(见下),逐条抽查后真值约为其四分之一, 修正后约 84.8~85.4%,仍在同一档。
只在 test 上跑了两个臂是有意的——每多跑一个臂,留出集就多沾一点选择污染。
由模块一裁定的执行准确率(全量 dev 1034 条,order-insensitive 比对), 随各阶段推进更新:
| 配置 | execution accuracy | 相对 baseline |
|---|---|---|
| baseline(纯模块二,CREATE TABLE schema) | 79.69%(824/1034) | — |
| + schema 样例值(阶段 2 canonical,记作 D) | 81.24%(840/1034) | +1.55 |
| + schema linking | 80.85%(836/1034) | +1.16 |
| + few-shot(按问题相似度检索) | 80.17%(829/1034) | +0.48 |
| + self-correction | 81.72%(845/1034) | +2.03 |
| + few-shot(骨架检索 + 去重) | 83.08%(859/1034) | +3.39 |
| + 自修正 + 骨架 few-shot(当前 canonical) | 83.56%(864/1034) | +3.87 |
| 三组件全开(再加 schema linking) | 83.95%(868/1034) | +4.26 |
| + DPO adapter(阶段 4,无 agent 组件) | 85.20%(881/1034) | +5.51 |
| + DPO adapter + 自修正 + 骨架 few-shot(最终) | 85.40%(883/1034) | +5.71 |
| 无 agent | 有 agent | |
|---|---|---|
| 无 adapter | 81.24% | 83.56% +2.32(p=0.0097 显著) |
| 有 adapter | 85.20% +3.97(p=0.0000) | 85.40% +4.16(p=0.0000) |
| 增量 | 在没有对方时 | 在有对方时 |
|---|---|---|
| agent 组件 | +2.32(显著) | +0.19(p=0.8776 不显著) |
| DPO adapter | +3.97(显著) | +1.84(p=0.0163 仍显著) |
agent 组件的增益在 adapter 面前整个消失。 两者在修同一批题——T1e 修的是外键 连接路径,骨架 few-shot 正是靠结构相似的范例教同一件事。 工程结论:拿到 adapter 之后可以关掉 agent 组件,它们要多一次草稿生成、 多加载一个 embedding 模型、多编码 7000 条范例池,换来的 +0.19 点测不出来。
复现信息:Qwen/Qwen2.5-Coder-7B-Instruct(bf16)、贪心解码
(temperature=0)、max_new_tokens=512、seed=42、vLLM 0.24 单卡 RTX 4090。
baseline 产物在 outputs/eval_baseline/,canonical 配置产物在 outputs/eval_d_ddl_ex/,
DPO adapter 产物在 outputs/eval_t1e/(无 agent)与 outputs/eval_t1e_agent/(全开)。
harness 噪声底实测 3 条翻转(p=1.0),解读小差异前请先对照这个数字。
比噪声底更要紧的一条:本项目累计跑了约 20 次配对检验,α=0.05 下本就该预期 约 1 次假阳性。实测确实撞上了——一个 dev 上 p=0.0094 的结论(修标签值 +1.35 点) 在 test 上是 p=1.0000。对 dev 上 p 在 0.001~0.05 之间、且翻转基数小(<40)的结论, 应视为"待留出集确认"而非定论。
复现最终配置(adapter 训练 + 评估):
export VLLM_USE_FLASHINFER_SAMPLER=0 HF_HUB_DISABLE_XET=1
# 造偏好对(约 25 分钟,用 GPU)。build_pairs 按题粒度断点续跑,
# 若 outputs/dpo_pairs.jsonl 已存在会跳过已完成的题——想重造必须先删掉它,
# 否则会拿到用旧 max_pairs_per_question 造的那一份。
python scripts/build_pairs.py --pass short
python scripts/train_dpo.py # DPO 训练(约 50 分钟)
python scripts/run_eval.py --adapter-path outputs/dpo_adapter \
--no-self-correct --no-schema-linking --no-few-shot --output-dir outputs/eval_t1e
python scripts/compare_runs.py outputs/eval_d_ddl_ex outputs/eval_t1e上表 85.20% 那一行的实际产物:偏好数据
outputs/pairs3/dpo_pairs.jsonl(2254 条,max_pairs_per_question: 3),adapteroutputs/dpo_adapter_sft。 当前configs/dpo.yaml已是这套配置,全新环境按上面的命令跑即可复现。
踩坑提醒:
pkill -f build_pairs.py杀不掉 vLLM 派生的VLLM::EngineCore子进程,它会继续占着约 17GB 显存,让下一个 GPU 任务以莫名其妙的 OOM 失败 (WSL2 上可能伪装成CUDA driver error: device not ready)。 每次杀完用nvidia-smi --query-compute-apps=pid,used_memory --format=csv验一遍。
上表数字经过两次信任根修复后的重判(
python scripts/rejudge.py outputs/eval_*, 不重跑模型):
- UTF-8 解码缺陷(发现 A.3)——
wta_1的 2 题连 gold 自己都执行不了、必然判错。 修复后每臂各 +2 题,所有配对 p 值不变。- 空结果集不比列数(发现 3.1)——两边都返回 0 行时,"2 列"和"1 列"被判等价。 dev 上 49 题(4.7%)的 gold 返回空集。修复后阶段 2 各臂零翻转, 只有带自修正的臂受影响(−6 / −1),self-correction 的增益从 +1.06 降到 +0.48。
推理链和完整分析见 EXPERIMENTS.md。为排除混淆做了四臂:
| 臂 | schema 表示 | accuracy | vs A | McNemar p |
|---|---|---|---|---|
| A | CREATE TABLE | 79.69% | — | — |
| B | M-Schema,无样例值 | 77.18% | −2.51 | 0.0019 显著变差 |
| C | M-Schema + 3 样例值 | 80.17% | +0.48 | 0.6445 无差异 |
| D | CREATE TABLE + 3 样例值 | 81.24% | +1.55 | 0.0519 边缘 |
关键在于 A vs C 的 +0.48(p=0.64)是两个显著效应抵消的假象:M-Schema 排版 −2.51(p=0.0019)、样例值 +3.00(p=0.0001)。只跑「完整方案 vs baseline」两臂 会把真正有效的样例值和无效的排版一起丢掉。
三个组件均可独立开关。相对阶段 2 canonical(D = 81.24%)的增量:
| 组件 | accuracy | vs D | 翻转题 | McNemar p |
|---|---|---|---|---|
| + self-correction | 81.72% | +0.48 | 15 | 0.3018 |
| + schema linking | 80.85% | −0.39 | 10 | 0.3438 |
| + few-shot(问题检索) | 80.17% | −1.06 | 89 | 0.2891 |
| + few-shot(骨架检索) | 82.30% | +1.06 | 81 | 0.2664 |
| + few-shot(骨架 + 去重) | 83.08% | +1.84 | 85 | 0.0503 |
| + 自修正 + 骨架 few-shot | 83.56% | +2.32 | 80 | 0.0097 |
| 三组件全开 | 83.95% | +2.71 | 78 | 0.0020 |
单个组件全部不显著,组合起来才显著(+2.32,p=0.0097)——自修正打"SQL 跑不通" (约占错例 20%),骨架 few-shot 打"跑得通但结构错"(约 71%),两者打的不是同一批题。 单组件里唯一显著的对比是"骨架检索优于问题检索":+2.13 点,p=0.0169。
schema linking 默认关:单独开 −0.39、在组合里 +0.39,都在噪声底附近测不出来, 却要多加载一个 embedding 模型。不是有害,是复杂度没有回报(BIRD 上应重测)。
偏好数据全自动构造:train 7000 题各采 8 个候选,由模块一判分, "有对有错"的题组成偏好对。产出 1490 条对(覆盖 135 个库,难负例占 75.4%), 其中 240 个"判对"的候选被扰动验证识别为假阳性并降级。
相对 D = 81.24% 的增量(均为零样本、三个 agent 组件全关,与 D 同口径):
| 臂 | 对数 | lr | loss_type | accuracy | vs D | 翻转 | McNemar p |
|---|---|---|---|---|---|---|---|
| T1 | 1430 | 5e-6 | sigmoid | 80.95% | −0.29 | 5 | 0.3750 |
| T1b | 1430 | 5e-5 | sigmoid | 80.17% | −1.07 | 105 | 0.3291 |
| T1c | 2218 | 5e-5 | sigmoid | 82.59% | +1.35 | 112 | 0.2191 |
| T1e | 2218 | 5e-5 | [sigmoid, sft] | 85.20% | +3.97 | 89 | 0.0000 |
决定成败的是 chosen 的 NLL 项,不是学习率也不是数据量。 T1b 与 T1e 用的是 同一批数据、同一个学习率,唯一差别是加了这一项,结果从 −1.07 变成 +3.97。
机制:标准 DPO 只优化 chosen 与 rejected 的相对距离,允许把两者一起压低
(实测 logps/chosen 从 −6.44 掉到 −12.86,rewards/chosen 转负——模型比训练前
更不可能写出那条正确 SQL)。chosen 概率一塌,模型就只保留了偏好数据的表层统计
特征。量化对照:T1b 在 dev 上多用了 27.7% 的 JOIN(无差别堆 join),
T1e 只多 5.0%(走对连接路径),而平均 SQL 长度几乎不变。
被修好的题集中在外键连接路径上,例如 car_1 里 D 写
continents.ContId = car_makers.Country 跳过了 countries 表,
T1e 走完整的 car_makers → countries → continents 三跳。
前三个臂的诊断过程(lr 档位选错导致 no-op、数据量在 max_pairs=3 就饱和)
见 EXPERIMENTS.md 实验 5 系列。
我们的 execution accuracy 是单库比对的,官方 test-suite EX 在多个数据库实例上比,
更严格。用 scripts/verify_predictions.py 复查每条判对的预测
(复制数据库、随机删行、级联恢复引用完整性,要求仍与 gold 一致):
| run | 单库 | 扰动验证后 | 标记数 | 标记率 |
|---|---|---|---|---|
| D | 81.24% | 80.46% | 8 | 0.95% |
| T1e | 85.20% | 84.53% | 7 | 0.79% |
| T1e + agent | 85.40% | 84.72% | 7 | 0.79% |
adapter 的标记率比 baseline 更低,增益不但幸存还略增(+3.97 → +4.07)。
但逐条抽查 8 条标记后,只有 2 条是真假阳性(gold 与 pred 查了不同的表;
"单场最高"vs"总和最高")。其余 6 条是验证器的误杀,共同点是都带
ORDER BY ... LIMIT 1——扰动删行造出并列,两条语义相同但写法不同的查询
选中了不同的行。所以真值被夹在中间:D ∈ [80.46, 81.24],T1e ∈ [84.53, 85.20],
增益的结论不受影响。详见 EXPERIMENTS.md 发现 6.1 / 6.2。
按问题相似度检索会精准命中"同领域但不同 schema"的范例(train 里有别的体育场库,
问题措辞几乎一样、列名却不同),模型把范例里的 average_attendance 抄进了
只有 Average 列的库。改成按草稿 SQL 的结构骨架检索后,检索到的范例来自
无关领域,抄袭诱惑消失。三个检索信号排出单调的剂量-反应关系:
| 检索信号 | accuracy |
|---|---|
| 纯问题相似度(领域) | 80.17% |
| 混合 50/50 | 80.85% |
| 纯骨架(结构) | 82.30% |
复现模块三:
python scripts/run_eval.py --self-correct --no-schema-linking --no-few-shot --output-dir outputs/eval_sc
python scripts/run_eval.py --no-self-correct --schema-linking --no-few-shot --output-dir outputs/eval_sl
python scripts/run_eval.py --no-self-correct --no-schema-linking --few-shot --fs-retrieval question --output-dir outputs/eval_fs
python scripts/run_eval.py --no-self-correct --no-schema-linking --few-shot --fs-retrieval skeleton --output-dir outputs/eval_fs_skel_dedup
python scripts/diagnose_errors.py outputs/eval_fs_skel_dedup # 错例分桶
python scripts/diagnose_linking.py # schema linking 离线召回复现四臂:
python scripts/run_eval.py --schema-format ddl --schema-examples 0 --output-dir outputs/eval_baseline
python scripts/run_eval.py --schema-format m_schema --schema-examples 0 --output-dir outputs/eval_b_mschema
python scripts/run_eval.py --schema-format m_schema --schema-examples 3 --output-dir outputs/eval_c_mschema_ex
python scripts/run_eval.py --schema-format ddl --schema-examples 3 --output-dir outputs/eval_d_ddl_ex
python scripts/compare_runs.py A=outputs/eval_baseline D=outputs/eval_d_ddl_ex # 配对 McNemarexport VLLM_USE_FLASHINFER_SAMPLER=0
python scripts/build_pairs.py --limit 100 # 先小规模验证可用题占比
python scripts/build_pairs.py # 全量 train(7000 题 × 8 候选)
python scripts/build_pairs.py --gold-skeleton-hint # P3 臂:全错题用 gold 骨架提示补正样本
python scripts/train_dpo.py --limit 32 # 先跑通训练闭环
python scripts/train_dpo.py # 全量产物:outputs/dpo_pairs.jsonl(偏好对)、outputs/pairs_stats.json(统计)、
outputs/dpo_adapter/(adapter + config 快照)。训完把 configs/model.yaml 的
adapter_path 指到 adapter 目录再跑 run_eval.py,与 D = 81.24% 配对比较。
臂设计与预判见 EXPERIMENTS.md 实验 4(开跑前已登记)。
- 阶段 0:脚手架 + 数据加载器
- 阶段 1:模块一 执行器 + 结果比对器(信任根)— 28 单测全绿,dev 前 100 条自比 100%
- 阶段 2:模块二 SQL 生成 + baseline — 84 单测全绿,baseline 79.69%, 经 schema 格式四臂对照后 canonical 配置达 81.24%
- 阶段 3:模块三 自修正 + 检索增强 — 109 单测全绿,三组件可独立开关, canonical 升至 83.56%(+2.32,p=0.0097)
- 阶段 4:模块四 DPO 造数据 + 训练 — 142 单测全绿,全自动构造 1490 条偏好对, adapter 达 85.20%(+3.97,p=0.0000)
- 阶段 5:评估汇总 + 消融报告 — 2×2(adapter × agent)完成,最终 85.40%
复现时按顺序踩过的五个坑,每一个都实打实卡过时间。
坑 1:vLLM 崩在 KV cache 初始化 —— 必须 export VLLM_USE_FLASHINFER_SAMPLER=0
症状看起来像显存不足,实际不是。若机器只装了 NVIDIA 驱动、没装 CUDA toolkit
(nvcc 不存在),vLLM 默认的 FlashInfer 采样器要现场 JIT 编译 CUDA kernel 就会崩。
加上这个环境变量后日志会出现
FlashInfer top-p/top-k sampling disabled via VLLM_USE_FLASHINFER_SAMPLER=0,
之后 KV cache 初始化和 CUDA graph 捕获一切正常。注意力后端会自动退到 FLASH_ATTN,
不需要另设 VLLM_ATTENTION_BACKEND。
坑 2:HF 下载必须 export HF_HUB_DISABLE_XET=1
xet 后端会静默卡死——本项目曾经停了 18 小时没有任何输出。
判断下载有没有在推进要看文件 mtime,不要看进程是否还活着(它活着,只是不干活)。
坑 3:embedding 模型必须跑 CPU
vLLM 默认吃掉 85~90% 显存,再往 GPU 上塞 SentenceTransformer 必 OOM。
SchemaLinker 和 FewShotRetriever 都硬编码 device="cpu",别改。
MiniLM 在 CPU 上编码 7000 条范例约 30 秒,完全可接受。
坑 4:pkill 杀不干净 vLLM
pkill -f build_pairs.py 杀不掉 vLLM 派生的 VLLM::EngineCore 子进程,
它会继续占着约 17GB 显存,让下一个 GPU 任务以莫名其妙的 OOM 失败
(WSL2 上还可能伪装成 CUDA driver error: device not ready)。
每次杀完用 nvidia-smi --query-compute-apps=pid,used_memory --format=csv 验一遍。
坑 5:系统内存也会成为瓶颈 本机只有 15GB 系统内存。vLLM 常驻的同时再起 sentence-transformers + 7000 条范例池, 会被 OOM killer 直接干掉——表现是退出码 0、没有任何输出,极难和正常结束区分。
另外两条不是环境问题而是实现约束,改之前先看 EXPERIMENTS.md:
- DDL 里样例值注释的逗号必须在注释前面:渲染成
列定义, -- Examples: [...]。 写成",\n".join(...)会把逗号放到注释后面、被--吃掉,整段 SQL 报语法错。 回归测试test_comma_precedes_example_comment守着这一点。 - few-shot 范例池只能取 train:
build_pool_from_train从不碰 dev。 从 dev 检索范例等于让模型抄自己的答案。FewShotRetriever还有第二道防线 (排除与当前问题完全相同的条目)。