Skip to content

Repository files navigation

Text-to-SQL Agent with DPO

用自然语言问数据库 → 生成 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 数据集

  1. 打开 Spider 官网 https://yale-lily.github.io/spider,点击 Spider Dataset 下载链接 (Google Drive,文件名类似 spider_data.zip,约 1 GB)。
  2. 解压后把其中的内容放到本仓库的 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。

验证数据加载器(阶段 0 验收)

# 需先 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 表名

跑模块一单测(阶段 1 验收)

pytest tests/test_db.py -v      # 执行器 + 比对器 + execution_accuracy

跑评估(阶段 2 起)

首次运行会自动下载基座模型(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 3

留出集结果(Spider test,2147 题 / 40 个库)

test 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 上跑了两个臂是有意的——每多跑一个臂,留出集就多沾一点选择污染。

结果表(execution accuracy,Spider dev)

由模块一裁定的执行准确率(全量 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

2×2 消融:adapter 与 agent 组件的增益不叠加

无 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),adapter outputs/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_*, 不重跑模型):

  1. UTF-8 解码缺陷(发现 A.3)——wta_1 的 2 题连 gold 自己都执行不了、必然判错。 修复后每臂各 +2 题,所有配对 p 值不变。
  2. 空结果集不比列数(发现 3.1)——两边都返回 0 行时,"2 列"和"1 列"被判等价。 dev 上 49 题(4.7%)的 gold 返回空集。修复后阶段 2 各臂零翻转, 只有带自修正的臂受影响(−6 / −1),self-correction 的增益从 +1.06 降到 +0.48。

schema 表示格式对照(实验 2)

推理链和完整分析见 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」两臂 会把真正有效的样例值和无效的排版一起丢掉。

模块三消融(实验 3 / 3.1)

三个组件均可独立开关。相对阶段 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 上应重测)。

模块四消融(实验 5)

偏好数据全自动构造: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 系列。

单库比对的假阳性率(实验 6)

我们的 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。

检索信号的选择(实验 3.1)

按问题相似度检索会精准命中"同领域但不同 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   # 配对 McNemar

造偏好对与 DPO 训练(阶段 4)

export 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.yamladapter_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。 SchemaLinkerFewShotRetriever 都硬编码 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 范例池只能取 trainbuild_pool_from_train 从不碰 dev。 从 dev 检索范例等于让模型抄自己的答案。FewShotRetriever 还有第二道防线 (排除与当前问题完全相同的条目)。

About

执行验证驱动的 Text-to-SQL DPO:零人工标注,Spider test 留出集 82.25% → 85.61% (+3.35, p=0.0000)

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages