设计题 03:Text2SQL 数据问答
权重:★★★ 形式:45 分钟系统设计轮
素材来源:Own — HKJC Race Bot 的 Text2SQL 多阶段流水线、合成数据流水线、Verl 两阶段训练(SFT + GRPO)
相关知识点:02-post-training/(sft / grpo / data-engineering)、04-agent-architecture/tool-use.md、05-agent-evaluation/benchmarks.md、03-rag/embedding-vector-db.md
1. 题干#
「我们有个客户,业务数据都在一个关系型数据库里 —— 几十张表,几千万行。业务人员不会写 SQL,想问问题就直接得到答案,比如『上个季度华东区的退款率是多少』。你来做,怎么设计?」
注意这个题干有多模糊:没说 schema 多大、没说用户说的词和库里的值对不对得上、没说答错数字的代价、没说能不能改库。这是刻意的 —— 面试官在看你什么时候发现信息不足。
2. 第一步:澄清(这一轮最重要的动作)#
绝对不要听完就开始画「问题 → LLM → SQL」的直线。开场第一句应该是:
「在动手之前,我想先确认几件事 —— 这几个答案会决定要不要做整套流水线,还是单次生成就够了。」
必问清单(按对方案的影响排序)#
| # | 问题 | 为什么它会改变方案 |
|---|---|---|
| 1 | Schema 有多大?几十张表还是几百张? | 这是最关键的。十几张表可以全塞进 prompt,单次生成就够;上百张表塞不进、塞进也会选错表 → 必须做表列召回(recall)。不问这条就画多阶段流水线,是浪费 |
| 2 | 用户说的词和库里的值对得上吗? | 如果用户说「告东尼」而库里是「Tony Cruz」、说「华东」而库里存的是省份代码 → 必须有实体标准化层。这往往是整条流水线里最大的一块工作量,也是端到端生成最先崩的地方 |
| 3 | 查错了数字,和答不出来,哪个更糟? | 错了更糟 → 必须能拒答/求澄清,答案要附 SQL 或口径供核对;只要求尽量有答案 → 可以更激进。这条决定全系统的保守程度 |
| 4 | 只读还是可写? | 必须只读。要立刻说出:用只读账号、行级权限、防注入。写操作在这个场景是不该碰的红线 |
| 5 | 数据是实时的还是 T+1 的? | 实时 → 要走线上库(需限流 + 重查询保护);T+1 → 走只读副本/数仓,成本和安全都更好 |
| 6 | 用户是谁?内部业务人员还是外部公众? | 外部公众 → 合规、成本、防滥用、结果集脱敏要求都上一个台阶 |
| 7 | 查询类型分布是什么?点查、聚合、还是多表 join? | 聚合和多表 join 的难度远高于点查,也决定合成数据要按类型分层,不能只造简单查询 |
| 8 | 有没有历史查询日志或标注数据? | 有真实日志 → 优先用它做评测留出集和 few-shot;没有 → 才需要合成数据全覆盖。这条决定要不要自造训练数据 |
面试技巧:先问 1、2、3 —— 这三条决定流水线的骨架。拿到答案再问下一层。追问本身也是考点。
3. 参考架构#
┌─ 离线:数据与模型准备 ─────────────────────────────────┐
│ 表结构 / 数据字典 / 业务口径文档 │
│ ↓ │
│ 合成数据流水线(多策略) → (自然语言问题, GT SQL) 对 │
│ ↓ 执行验证(能跑通 + 返回非空 + 语义正确) │
│ ┌────────────┬────────────┬─────────────┐ │
│ │ SFT 训练集 │ few-shot 库│ 评测集(冻结)│ │
│ └────────────┴────────────┴─────────────┘ │
│ ↓ SFT →(可选)GRPO │
└───────────────────────────────────────────────────────┘
↓
┌─ 在线:查询流水线 ─────────────────────────────────────┐
│ 用户问题 │
│ ↓ ① 可回答性判断(是数据问题吗?能不能答?) │
│ ↓ ② 实体与关键词抽取 │
│ ↓ ③ 实体标准化(Elasticsearch 模糊匹配 → 闭集) │
│ ↓ ④ Schema 召回(相关表 / 列,不是全量) │
│ ↓ ⑤ few-shot 范例检索(语义相似的历史问题+SQL) │
│ ↓ ⑥ SQL 生成(生成模型) │
│ ↓ ⑦ 执行 + 校验(语法 / 执行 / 行数合理性 / 空集) │
│ ↓ ⑧ retry-and-fix(回注错误信息,最多 N 次) │
│ ↓ ⑨ 结果后处理(拼自然语言 + 附口径 + 附 SQL) │
│ 答案 │
└───────────────────────────────────────────────────────┘画图时要讲的三个点(这是把架构和「背组件」区分开的地方):
- ③ 实体标准化为什么是独立的、且必须最靠前。用户表达和库里的存储值之间存在字面差异(别名、简称、错别字、中英文),这不是语义相似问题,而是字符级近似匹配问题。所以在专有名词上,稀疏方法(BM25 + 编辑距离 + 同义词表)往往比稠密向量更稳 —— 而且实体名是有限闭集,可以显式维护同义词表。错了这一步,后面 SQL 生成得再准也是查错人。
- ④ 为什么要召回 schema 而不是全塞。全量 schema 既超 prompt 预算,又会引入大量无关表把模型带偏。只注入可能用到的表和列,是准确率的前置条件而非优化项。
- ⑧ retry-and-fix 不是「再生成一次」,而是把执行报错回注给模型。相当于让模型看到编译器反馈:报错信息(列名不存在、类型不匹配、聚合未分组)本身就是最有用的修正信号。必须限次数、必须有兜底。
4. 关键决策与权衡#
4.1 为什么端到端生成在垂域 schema 上会失败#
直接让 LLM 生成 SQL 有三个硬伤,不是一个:
| 问题 | 具体表现 | 对策 |
|---|---|---|
| 实体模糊匹配 | 用户说「告东尼」还是「Tony Cruz」,库里是标准写法 | 实体标准化(ES 模糊匹配 + 同义词表),先标准化再进 prompt |
| Schema 太大 | 全表结构塞不进 prompt;塞进去了也会选错表 | 表/列召回,只注入相关 schema |
| 语法与业务逻辑出错 | SQL 跑不通,或跑通了但业务含义错 | retry-and-fix + 结果后处理 + 口径校验 |
还有一个更隐蔽的问题必须主动说:即使模型能写出语法完全正确的 SQL,业务口径它不可能知道。「退款率」算不算已取消的订单?统计按哪个时区?金额含不含税?这些只能来自业务方,必须显式写进 schema 注释或 few-shot 范例里。这是 Text2SQL 项目里最容易被低估的部分 —— 它不是模型问题,是知识和治理问题。
4.2 为什么是多阶段流水线,而不是一个大模型#
- 收益是可控性与可调试性:每一步可独立评测。实体匹配错了只修实体匹配,不用重训 SQL 生成;召回错了只调召回,不动别的。每一步可独立替换是这个拆分的核心价值。
- 代价要说清:延迟更高、组件更多、端到端失败面更大(每一环都有失败可能)。
- 什么时候可以简化 —— 要主动说这个边界:如果 schema 很小(十几张表)、用户基本用标准名称查询、查询以点查为主 —— 那么单次生成 + schema 全塞 + 结果校验可能就够了,不需要整套流水线。主动划出这个边界,比一味推荐复杂方案更能体现判断力。
4.3 合成数据:怎么做,以及怎么不污染评测集#
垂域标注数据稀缺,且真实分布极度不均衡(简单问题多、复杂聚合少)。多策略是必须的,单一策略会造出同质数据,模型在边缘 case 上就崩:
- 主题引导生成(topic-guided):按业务主题覆盖,保证广度。
- 字段聚合(field-based aggregation):专门造统计/聚合类查询,补上真实分布里稀缺但难的那一类。
- 困难样本构造(hard-case construction):针对已知失败模式定向补数据。
关键技术点:合成数据必须是 (自然语言问题, ground-truth SQL) 成对的,且 ground-truth SQL 要实际执行验证过 —— 能跑通、返回非空、语义正确。否则你是在教模型写一种语法正确但语义错误的 SQL。
防污染(面试必被追问,且必须答得干净):
- 问题级 + SQL 级双重去重 —— 只做其中一层都不够。
- 按主题/实体切分,而不是随机切分 —— 随机切分在合成数据场景下几乎必然泄漏,因为同一模板生成的变体会被分到训练和测试两侧。
- 评测集冻结,且与训练数据的生成流水线物理隔离:不同的生成脚本、不同的种子、最好不同时期。
- 用真实用户日志的留出集做交叉验证,即使它不完全贴合。
如果确实存在污染风险而没处理 —— 直说,并说明「如果重做我会按主题/实体切分」。面试官吃这一套,硬撑不吃。
4.4 SFT + GRPO:哪里值得投,哪里不值得#
先说清楚什么情况下根本不该做微调 —— 这一步很重要,它把「会训模型」和「会判断要不要训」区分开:
- 如果请求量不大、schema 稳定、查询类型集中 → 强模型 + few-shot + retry-and-fix 就够了。微调的固定成本(数据、训练、评测、维护)摊销不回来。
- 微调值得做的条件:① 有稳定的高调用量,成本上摊得开;② 领域 schema 私有且模型没见过(在公开 benchmark 上强 ≠ 在你的表结构上强);③ 有真实的失败模式可以定向补数据。
SFT 值得做的时候:学 SQL 语法和业务口径模板,建立「能产出合理格式 SQL」的起点。这一步不做,后面的 RL 无从谈起。
GRPO 值得做的条件更苛刻:
- 必须有可自动验证的奖励。这恰恰是 Text2SQL 适合 RL 的原因:SQL 能执行、结果能和 ground truth 比对,奖励是客观的,不像开放式生成要额外训 reward model。
- 但只有在 SFT 已经让模型「大部分时候能产出合理 SQL」之后才有意义。否则一个 batch 里全是失败样本、组内奖励一致、梯度为零,训不动 —— 这是 GRPO 的结构性缺陷,prompt 难度管理在 GRPO 实践里是一等公民,很多复现训不动不是超参问题,是数据难度分布问题。
- 奖励设计的陷阱:执行成功不能单独作为奖励 —— 模型会学会生成永远能执行但返回空集或无意义的 SQL(例如恒真/恒假条件),因为这类 SQL 执行成功率最高。执行成功必须与结果正确性绑定,并对空结果、异常行数做惩罚。
必须主动说的结论:GRPO 不是「更高级所以更好」。它解决的是 SFT 缺乏「尝试—修正」探索能力的问题,代价是训练复杂度和算力。如果 SFT 后准确率已经够用,就不该上 RL。
4.5 明确不做什么#
- 不做任何写操作。只读账号 + 行级权限,不做 UPDATE/DELETE/DDL。
- 不把全量 schema + 全部 few-shot 塞进上下文。噪声会导致选错表,这是准确率的前置条件。
- 不做无限制的 retry。限次数 + 兜底(澄清追问或降级)。
- 不把原始结果行直接暴露给外部用户(PII、权限、数据量)。
5. 容量与成本直觉#
面试官常问「这大概多少钱、延迟多少」。不要求精算,但要有量级感觉:
- 离线合成数据的成本主要在「生成 + 执行验证」 —— 每条样本都要真的跑一次 SQL 做非空校验,这部分成本随数据量线性增长。数据量不需要堆到很大,质量与覆盖度比数量重要(呼应
02-post-training/sft.md的「质量压倒数量」)。 - 训练成本:SFT 是中等成本;GRPO 的主要算力在采样(每个 prompt 要采一组回答来算组内相对优势),所以它比 SFT 贵得多。这是「RL 值不值得」的成本侧论据。
- 在线:单次数据问答大概是 3–5 次模型调用加一次 SQL 执行,比闲聊贵一个量级。所以和设计题 02 一样,关键是复杂请求的占比。
- 优化杠杆:retry 次数上限、缓存高频查询(业务人员的问题重复度其实很高)、用小模型做可回答性判断和实体抽取、结果集截断。
- 重查询保护是硬要求:大表聚合可能扫全表。必须有查询超时、强制 LIMIT、以及资源隔离(只读副本),不能让它压垮生产库。说出这条是工程成熟度的信号。
6. 面试官会怎么往下挖#
第一层:澄清与动机#
- Q:你会先问我什么? ← 见第 2 节。先问 schema 规模和实体歧义程度。
- Q:为什么不直接用最强模型生成 SQL? ← 私有 schema 模型没见过、实体需要专门标准化、成本上要扛量。公开 benchmark 强不代表在你的表上强。
- Q:这些指标能不能离线预计算好,根本不用 Text2SQL? ← 好问题,要能接住。 如果查询模式是固定、可枚举的,预计算指标表 + 语义检索比 Text2SQL 稳得多、便宜得多。Text2SQL 的价值在长尾的、事先不可预知的查询。能主动说出这个替代方案,是很强的信号。
第二层:技术细节#
- Q:实体模糊匹配为什么用 ES 不用向量检索? ← 需要的是字符级相似(人名、地名),且实体名是有限闭集可用同义词表;稀疏方法在专有名词上往往更稳。也可以答「两者结合,向量负责语义召回、ES 负责精确匹配」—— 但只说你真做了的部分。
- Q:retry-and-fix 怎么防止越修越错或无限循环? ← 重试次数上限、是否回注上次错误信息、以及兜底策略(重试失败后返回什么 —— 降级到澄清追问,还是返回部分结果)。
- Q:schema 召回怎么评测? ← 很关键,召回错了后面全错。要有「正确表是否在召回集内」的单独指标。
- Q:合成数据会不会污染评测集?怎么保证? ← 见 4.3,答去重 + 按主题/实体切分 + 物理隔离。
- Q:GRPO 的奖励怎么设计?「执行成功」能单独作奖励吗? ← 不能,会被 hack(4.4)。
- Q:业务口径(比如「退款率」的定义)模型不知道怎么办? ← 来自业务方的显式知识,写进 schema 注释 / few-shot,并用口径校验做后处理。
第三层:工程与生产#
- Q:线上延迟多少、怎么优化? ← 流水线天然慢:并行化独立步骤、缓存高频查询、小模型做抽取。
- Q:怎么排查某一次错误回答? ← 链路追踪,每步输入输出都要留;SQL 生成失败的 case 归档。并且定期从线上失败 case 里挖新的困难样本回灌训练集 —— 这个闭环很加分。
- Q:用户的查询会不会压垮生产库? ← 只读副本 + 超时 + LIMIT + 资源隔离。
- Q:schema 变了(加表、改列)怎么办? ← 流水线要能感知 schema 变化并重建召回索引与数据字典;这也是流水线相对端到端更好维护的地方。
第四层:反思与边界#
- Q:什么时候你会说「这个需求不该用 Text2SQL」? ← 查询模式固定可枚举(该预计算)、schema 极不稳定、或者用户其实要的是探索式分析而不是取数。
- Q:如果用户问的是「预测下个季度会怎样」,你怎么处理? ← 边界意识(呼应 Race Bot 的原则):系统做的是历史数据的查询与统计,不做预测性建议。这类请求应在可回答性判断阶段识别并走安全分支。
- Q:重做这个项目,你会改什么? ← 可用的方向:① 合成数据的去重与评测切分更严格;② 把实体标准化和 SQL 生成解耦成可复用模块;③ 引入更系统的离线回归评测,防迭代回退。
- Q:这个项目里最容易被低估的工作量是什么? ← 实体标准化和业务口径对齐,不是模型。 模型只是链条中的一环。
7. 自测#
1. 端到端生成 SQL 在垂域 schema 上的三个硬伤分别是什么,各对应什么对策?
答案要点
① 实体模糊匹配 —— 用户说法与库里标准值不一致(「告东尼」vs「Tony Cruz」),对策是实体标准化:Elasticsearch 模糊匹配 + 同义词表,先标准化再进 prompt;② schema 太大 —— 塞不进 prompt 或塞进去了选错表,对策是表/列召回,只注入相关 schema;③ 语法与业务逻辑出错 —— 跑不通或跑通但语义错,对策是 retry-and-fix(回注执行报错)+ 结果后处理 + 口径校验。另需主动补充:业务口径(如「退款率」的定义)模型不可能知道,必须来自业务方并显式写入 schema 注释或 few-shot。
2. 合成数据怎么保证不污染评测集?说出三条流程约束。
答案要点
① 问题级 + SQL 级双重去重;② 按主题/实体切分而不是随机切分 —— 随机切分下同一模板的变体会被分到训练和测试两侧,几乎必然泄漏;③ 评测集冻结,且与训练数据的生成流水线物理隔离(不同脚本、不同种子、不同时期);④ 用真实用户日志的留出集做交叉验证。若确实存在污染风险而未处理,直接承认并说明重做时的切分方式。
3. GRPO 在什么情况下会「训不动」?为什么 prompt 难度管理是一等公民?
答案要点
GRPO 用组内相对优势替代 critic,当一组采样回答全对或全错时组内奖励一致、advantage 全为 0、梯度为零 —— 该 prompt 对训练毫无贡献,只消耗采样算力。因此太简单和太难的 prompt 都是无效样本,需要按 pass rate 过滤或分层采样,让每个 batch 的样本都落在「有对有错」的区间。很多 GRPO 复现训不动不是超参问题,而是数据难度分布问题。这也意味着应监控「零梯度组的比例」作为诊断指标。前置条件是 SFT 已让模型能产出合理 SQL,否则整批全负奖励、梯度没有信息量。
4. 把「SQL 执行成功」单独作为 RL 奖励会产生什么问题?正确做法是什么?
答案要点
奖励可被 hack:模型会学会生成永远能执行但返回空集或无意义的 SQL(例如恒真/恒假条件),因为这类 SQL 的执行成功率最高。正确做法是把执行成功与结果正确性绑定(执行结果与 ground truth 比对),并对空结果、异常行数做惩罚。奖励通常由三部分构成:可执行性 + 结果正确性 + 格式合规,且每一项的权重和理由都要能说清。
5. 换条件:schema 只有 10 张表、用户都用标准 ID 查询、查询以点查为主 —— 你的架构怎么改?
答案要点
砍掉流水线的大部分:schema 可以全量塞进 prompt,实体歧义低到可以省掉标准化层,点查为主的场景下单次生成 + 结果校验 + 有限次 retry-and-fix 可能就够了,few-shot 也只需少量。这不是能力退化,而是判断必要性:多阶段流水线换来的可控性,在 schema 小、歧义低的场景下买不到对应的收益,只是白白付出延迟和维护成本。同时应说明什么条件下会加回流水线(schema 扩张到上百张、实体歧义上升、聚合与多表 join 占比提高)。这也是本设计题的核心判断:复杂度由需求驱动,不由架构美感驱动。