一个零依赖、可离线运行的精简 Text2SQL 全链路演示:问题 → Schema Linking → Prompt 构造 → SQL 生成 → 安全校验 → 执行 → 执行级评估。
我在企业环境里做过一个智能经营分析平台(制造业绩效/利润分析场景),但那是涉密项目, 代码和数据都不能外传。所以我把其中最能体现工程判断的几个环节抽出来,用公开可复现的 方式重新实现了一遍,放进这个仓库。
这个仓库不追求 SOTA 准确率,它想证明的是三件事:
- 我知道 Table 多了以后不能把全量 Schema 塞进 Prompt,并且能实现"召回 → 匹配 → 推理"的替代方案;
- 我知道生成 SQL 之前必须过安全闸门,并且能说清闸门该拦哪些东西;
- 我知道效果不能靠感觉验证,并且能用执行结果级比对把准确率算出来。
# 1. 初始化演示数据库(生成 data/university.db)
python -m app.main --init
# 2. 提一个问题,看完整链路(每步耗时、召回了什么、命中了哪条业务规则)
python -m app.main --ask "计算机学院学生的平均年龄"
# 3. 跑评估集,输出 Execution Accuracy
python -m app.main --eval
# 4. 看全量 Schema 与精简 Schema 的 Token 差多少
python -m app.main --compare --ask "计算机学院学生的平均年龄"
# 5. 单元测试
python -m unittest discover -s tests -v要求:Python 3.10+,无第三方依赖(只用 sqlite3 / re / json / urllib / unittest)。
可选:接入真实模型生成 SQL
# 任意 OpenAI 兼容端点即可
set LLM_API_KEY=sk-xxx
set LLM_BASE_URL=https://api.openai.com/v1
set LLM_MODEL=gpt-4o-mini
python -m app.main --ask "各院系学生的平均成绩" --driver llmtext2sql-mini/
├── app/
│ ├── schema_index.py # Schema 元数据 + TF-IDF 字符 n-gram 检索(可替换为真实 Embedding)
│ ├── linker.py # Schema Linking 三阶段:表召回 → 字段语义匹配 → Join 路径推理
│ ├── prompt.py # 动态精简 Schema 组装 + Token 估算
│ ├── generator.py # 两种 SQL 生成 driver:rule(离线)/ llm(OpenAI 兼容)
│ ├── guard.py # SQL 安全校验:单语句 / 只读 / 表白名单 / 禁止关键字 / LIMIT 收敛
│ ├── executor.py # SQLite 执行 + 值链接(value linking)
│ ├── runner.py # Pipeline 编排 + Execution Accuracy 评估
│ └── main.py # CLI
├── data/
│ ├── university.sql # 教务场景演示库(6 张表,含外键关系)
│ └── eval_questions.json # 14 条评估用例(simple / medium / complex 三层)
└── tests/test_pipeline.py # 15 个单元测试
全量 Schema 注入在表少时能用,表一多就会同时撞上三个问题:Token 成本高、无关字段成为噪声、相似字段互相干扰。 所以拆成三步:
| 阶段 | 做法 | 解决的问题 |
|---|---|---|
| 表级召回 | 表描述(表名+中文别名+字段说明)建索引,按问题取 Top-K | 从 N 张表缩到 3 张 |
| 字段语义匹配 | 向量相似度 + 业务规则黑白名单,规则可直接压过向量分 | "成绩"必须取 enrollments.score,不能被名字更像的字段抢走 |
| Join 路径推理 | 从锚表出发做 BFS,求最短 Join 路径与 Join 条件 | 3 表关联时不必猜 Join 怎么写 |
实测(--compare):同一个问题,全量 Schema 约 428 tokens,精简后约 185 tokens,省下约 57%。
关键取舍写在这里:召回阶段宁多勿漏,生成阶段宁少勿错。Linking 召回 3 张表,但生成时只
JOIN 真正需要的表——否则 COUNT(*) 会因为多余的 JOIN 被行数放大,直接把答案算错。
guard.py 的顺序是:单语句 → 必须是 SELECT/WITH → 危险关键字扫描 → FROM/JOIN 表白名单 → LIMIT 收敛(上限 200)。
为什么"表白名单"比"关键字黑名单"更重要:黑名单永远有漏网之鱼,而白名单是默认拒绝。
同一语义的 SQL 写法可以完全不同(别名、括号、JOIN 顺序),字符串比对会大面积误判。
所以 execution_match() 的口径是:列数相同 → 行数相同 → 结果按行排序后逐值比对(数值统一四舍五入到 4 位,规避浮点误差)。
python -m app.main --eval(rule driver,离线确定性生成):
simple 4/4
medium 4/4
complex 4/6
Execution Accuracy = 12/14 = 85.7%
故意保留了两条失败用例,因为它们正好说明规则生成器的能力边界:
| 用例 | 问题 | 失败原因 |
|---|---|---|
| C05 | 列出学生姓名和他所在的院系名称 | 需要跨表多列投影,规则生成器被刻意限制为"只投影锚表字段",于是漏掉了 departments.dept_name |
| C02 | 成绩最高的前 5 条记录对应的学生姓名和成绩 | 问的是"取最大值对应的整行",不是 MAX() 聚合值;规则生成器只会生成 MAX(score) |
这两类问题靠规则硬凑会越来越脆,正是真实系统里需要 LLM 的地方——所以这个仓库同时提供了
--driver llm,两者对比着看比只给一个数字更能说明问题。
| 真实项目里的做法 | 本仓库 | 简化点 |
|---|---|---|
| Embedding 向量检索 + 混合评分 | TF-IDF 字符 n-gram | 为了零依赖;接口一致,可替换 |
| 指标知识库 RAG(指标定义/公式/依赖) | RULE_CONDITIONS + RULES 规则表 |
规则是知识库的退化形态 |
| BFS Join 路径推理 | 同样的 BFS | 图规模小 |
| 行列级权限 + 字段脱敏 | 仅保留表白名单 + LIMIT 收敛 | 单用户演示库,无角色概念 |
| Plan-and-Execute 多步 Agent | 未实现 | 单问题单 SQL,复杂分析不在本仓库范围 |
| FastAPI + SSE 服务化 | CLI only | 用命令行让链路更透明 |
- 数据集不是 Spider/BIRD 原始数据。
data/university.sql的 schema 参照公开 Text2SQL 基准里 典型的教务多表 Join 场景设计,数据为演示构造,不含任何真实业务信息。如果要接真实基准, 替换data/与schema_index.py里的元数据即可(检索与生成逻辑不用改)。 - 评估集只有 14 条。小样本上的百分比只能当趋势看,不能当结论。
- rule driver 是确定性生成器,不是模型。它存在的意义是让链路可以在完全离线的情况下被验证。
- 没有做多轮对话上下文。每条问题独立处理。
MIT