Skip to content

大模型写的 SQL,为什么一跑就报错?——一套 RAG 地基,长出「问答 / SQL / 报表」三个能力

第一篇把最小 RAG 链路跑通第二篇把检索的坑踩透第三篇把它从"能跑"调成"能用"(混合检索 + 重排序 + 多轮)。这是这个系列的完结篇:同一套检索器,后面换段 prompt,还能长出 Text-to-SQL 和报表解读。 全程还是 Node.js + 原生 fetch。但这一篇真正的主角不是某个新算法,而是一个词:复用

🎯 本文目标

读完你应该能讲清楚三件事:为什么直接让大模型写 SQL"一跑就报错"、RAG 怎么用一招(grounding)把它治好;报表解读凭什么比普通 BI 多值一份钱,那份钱到底值在哪;以及贯穿全文最值钱的一点——为什么加三个新功能,检索代码一行都不用改


前三篇讲了什么,这篇收个什么尾

这个系列到这里整整四篇,是一条完整的成长线:

  • 第一篇《前端也能搞懂 RAG —— 手写最小链路:embedding、余弦相似度、迷你向量库、拒答兜底。解决"RAG 到底是什么"。
  • 第二篇《前端手写 RAG 踩坑实录 —— 接上真实文档后的四个坑:切太碎、切太大、连接被重置、高分≠能回答。
  • 第三篇《前端 RAG 工程化 —— 给检索补第二条腿:混合检索(向量 + BM25 + RRF)、规则重排序、多轮记忆。
  • 本篇 —— 前三篇一直在打磨"检索"这一件事。这一篇要证明:这块打磨好的检索地基,价值远不止问答一个出口。

前三篇的概念(RAG 原理、余弦相似度、分块、混合检索)本篇不再重复,涉及处直接给链接。

🧭 一句话交代场景

还是那个数据治理助手:一张订单表 orders,字段口径(amount 含不含税、order_date 记的是下单还是支付时间)散落在建表注释和同事的记忆里。前三篇让它能"回答字段是什么意思"。这一篇,让同一个助手再学会两件事——帮你写查数的 SQL帮你读懂一份报表


🧩 第 1 章:一个反直觉的起点——加三个功能,检索代码一行没动

🎯 本章目标

先把这一篇的"总纲"立起来:为什么"问答 / SQL / 报表"看着是三个功能,本质上却是同一个东西。

先别急着写 SQL。这一篇能成立,全靠一个你可能没意识到的事实。

回想第三篇最后那个检索器 searcher,它干的事拆开看就三步:

用户问题 → ① 混合检索粗召回 20 条 → ② 重排序精排出 5 条 → ③ 把这 5 条塞进 prompt 交给 LLM

现在关键的问题来了:问答、写 SQL、读报表,这三件事在上面这条链路里,到底哪一步不一样?

答案是——只有第 ③ 步的最后半句"塞进哪个 prompt"不一样,前面①②两步一模一样。

功能① 检索② 重排③ 最后那段 prompt
字段问答一样一样"根据资料回答问题"
Text-to-SQL一样一样"根据资料写一条 SQL"
报表解读一样一样"根据资料解读这份数据"

看懂这张表,这一篇就懂了一大半。检索器是地基,prompt 是插头——地基把"从一堆元数据里找到最相关的几条"这件最难、最脏的活儿干完了,剩下的不过是换个插头,告诉 LLM"拿这些料去干哪件事"。

所以下面两章的代码会短得让你意外:因为真正的重活,前三篇早就干完了。

javascript
// 三个功能的骨架,长得几乎一样——不同只在最后一行传哪个 prompt
async function 某功能(searcher, question, ...) {
  const candidates = await searcher.search(question, 20)  // ① 检索:三个功能共用
  const hits = rerank(question, candidates, 5)            // ② 重排:三个功能共用
  return await chat(question, hits, 某个专用Prompt(hits)) // ③ 只有这里换插头
}

💡 这才是"工程化"的真正样子

初学者觉得"做三个功能 = 写三套代码"。但一个好地基的价值,恰恰是让新功能变成"换个 prompt"这么廉价。你在面试里能说出"我这三个能力共用一套检索、只有出口 prompt 不同",比罗列"我会 Text-to-SQL、会报表分析"有分量得多——前者证明你有架构判断,后者只是功能清单。

🧭 承上启下

总纲立好了。现在换上第一个"插头"——让这套检索器去写 SQL。它要解决的,是一个几乎人人都撞过的坑。


🗄️ 第 2 章:Text-to-SQL——治好大模型「猜字段名」的病

🎯 本章目标

搞懂为什么裸让大模型写 SQL 会"一跑就报错",以及 RAG 的 grounding 怎么一招治好它。

现象:它写的 SQL 很漂亮,就是跑不了

你让 ChatGPT"帮我写条 SQL,统计最近 30 天每天的销售额",它会秒回一条工整的:

sql
SELECT DATE(create_time) AS day, SUM(sales_amount) AS total
FROM orders
WHERE create_time >= NOW() - INTERVAL 30 DAY
GROUP BY day;

语法挑不出毛病。可你把它贴进公司的数据库一跑——报错:Unknown column 'sales_amount'。因为你库里那个字段根本不叫 sales_amount,它叫 amount;"创建时间"也不叫 create_time,叫 order_date

为什么会这样:模型在"猜",因为它没见过你的库

这不是模型笨。是它压根不知道你的表长什么样。它训练时见过千千万万个数据库,"销售额大概叫 sales_amount、创建时间大概叫 create_time"是它学到的统计习惯。轮到你这张表,它没有任何信息,只能按习惯猜一个最像的名字

猜出来的字段名语法完全合法、命名也很"专业"——但和你的真实库对不上。这就是大模型写 SQL 最致命的问题:它不会告诉你"我不知道你的字段名",它会一本正经地编一个。 这也正是第二篇坑 4那个主题的翻版:看着对,其实错。

解法:写之前,先把真实字段"喂"给它

病根是"模型不知道真实字段名",那解法就直白了:写 SQL 之前,先用检索把相关表的真实字段捞出来,塞进 prompt,再让它写。 这一步在 RAG 里有个专门的词,叫 grounding(接地)——把模型飘在空中的"猜测",摁到你真实数据这块"地"上。

代码短得几乎没有新东西——检索、重排都是第三篇的原件,新增的只有一段 prompt:

javascript
// text-to-sql.js —— 基于检索到的元数据生成参考 SQL(只生成、不执行)
import { chat } from '../chat.js'
import { rerank } from './rerank.js'
import { buildHybridSearcher } from './metadata-qa-hybrid.js'

// ★ 全部的"新东西"就这一段 prompt——把真实字段圈进来,把 LLM 的自由发挥摁死
function buildSqlPrompt(hits) {
  const context = hits.map((h, i) => `[元数据${i + 1}] ${h.text}`).join('\n')
  return `你是一个数仓 SQL 助手。根据下面的元数据,为用户的问题写一条参考 SQL。
规则:
1. 只能使用元数据里出现过的表名和字段名,绝不臆造不存在的字段。
2. 如果元数据里缺少必要的表或字段,直接回答"元数据不足,无法生成可靠 SQL",不要硬编。
3. 时间范围、状态过滤等关键条件要写清楚。
4. 先输出 SQL(单独成段,方便复制),再用一句话说明你用了哪些表和字段。

可用元数据:
${context}`
}

// 生成参考 SQL:检索 + 重排(第三篇原件)→ 换上 SQL 专用 prompt
export async function generateSQL(searcher, question) {
  const candidates = await searcher.search(question, 20)   // ① 检索:没改
  const hits = rerank(question, candidates, 5)             // ② 重排:没改
  const sql = await chat(question, hits, buildSqlPrompt(hits))  // ③ 换插头
  return { sql, sources: hits }
}

跑一下,这次它写出来的是:

sql
SELECT order_date AS day, SUM(amount) AS total_sales
FROM orders
WHERE order_date >= CURDATE() - INTERVAL 30 DAY
GROUP BY order_date;

amountorder_date ——全是你库里真实存在的字段。同一个模型、同一个问题,差别只在于:这一次,它写之前"看过"你的表。

为什么这么写,三个要点:

  • buildSqlPrompt 的规则 1、2 是命门。 "只能用元数据里的字段""缺了就说不足"——这两条把模型的"猜"摁死了。没有它俩,Text-to-SQL 立刻退化回"凭习惯猜列名"。规则是把 grounding 从"给了资料"升级成"强制只用资料"的最后一道锁。
  • 检索复用 search + rerank,和第 1 章那张表完全对上。 Text-to-SQL 不是新链路,是问答链路"换了个出口 prompt"。你甚至能把这个 generateSQL 和问答的主流程并排放,会发现只有 chat 那一行的 prompt 不同。
  • 返回里带 sources 前端可以展示"这条 SQL 用到了这些字段",让用户一眼核对——AI 写的 SQL 不该是黑盒,得让人能验。

深一层:为什么是"检索喂字段",而不是微调、也不是把整个库塞进去

到这你可能会问:让模型认识我的库,还有别的路吧?有,但都不如 RAG 划算。对比一下就懂为什么这是标准姿势:

做法怎么让模型认识你的库问题
微调一个模型拿你的 schema 去训练贵、慢,字段一改就得重训,杀鸡用牛刀
把整个库结构塞进 prompt每次把所有表所有字段都发过去表一多 prompt 就爆 token,且无关字段是噪声,干扰模型
RAG(本文)只检索跟这个问题相关的几张表/字段便宜、实时、精准——问啥取啥,改字段只需重建库

RAG 的精髓在中间那列:它不是把整本字典背给模型,而是每次只翻到相关的那一页。 问"销售额"就只捞 ordersamount 附近的元数据,其他表根本不进 prompt。省 token、降噪声、还实时——字段口径改了,重建一次库就生效,不用碰模型。这就是为什么"先检索元数据再生成 SQL"是业界落地 Text-to-SQL 的标准姿势,而不是微调。

⚠️ 一条必须守住的红线:只生成,不执行

你注意到没有——从头到尾,这段代码没有连数据库、没有跑这条 SQL。它只把 SQL 当文本生成出来,交给用户自己去执行。这不是偷懒,是故意划的安全边界

  • LLM 生成的 SQL 可能有性能陷阱(全表扫描)、甚至误伤(一个不该有的 DELETE)。让它直连数据库自动执行,等于把方向盘交给一个偶尔会走神的实习生。
  • "只生成、把执行权留给人",既拿到了 AI 提效的好处,又把风险锁在了"人点一下确认"这道闸后面。

面试被问到 Text-to-SQL,能主动说出"我只做生成不做执行、执行权留给用户",比只会讲怎么生成,更能体现工程上的分寸感。

🧭 承上启下

SQL 这个插头装好了。换第二个——让同一套检索器去读一份报表。这次要回答一个更"值钱"的问题:你这个 AI 解读,凭什么比公司现成的 BI 工具(Business Intelligence,做数据报表和可视化看板的软件,如 Tableau、Power BI、帆软)强?


📊 第 3 章:报表解读——比普通 BI 多知道一件事:字段口径

🎯 本章目标

搞懂 AI 报表解读的差异化到底在哪——不在"会算涨跌幅",在"懂业务口径"。

现象:普通 BI 只会告诉你"跌了 12%"

假设有这么一段销售数据:

日期          amount
2026-06-01    12000
2026-06-02    11800
2026-06-03     9500
2026-06-04     6200
2026-06-05     6100

任何一个 BI 工具、甚至一行 Excel 公式,都能告诉你"从 6-01 到 6-05 跌了约 49%""6-03 到 6-04 有个陡降"。但这些只是把数字重新念了一遍——涨跌幅是纯算术,谁都会,没有信息增量。

差异化:你的助手知道 amount 是"税后金额"

真正拉开差距的是这个:你的助手知道每个字段的业务口径。 它检索得到 orders 的元数据,知道——

  • amount税后金额(不是流水,是实际到手);
  • status 里的 refunded 代表退款

于是它能说出普通 BI 说不出的话:"6-03 起销售额连续下滑,因 amount 是税后口径,这波下跌需警惕是否与退款率上升有关,建议交叉核对 status=refunded 的记录。"

同样一份数字,普通 BI 停在"发生了什么",你的助手能往前迈一步猜"为什么"——因为它懂字段背后的业务含义。 这一步,就是那份"多出来的钱"值的地方。

实现:两路信息一起喂——数据 + 元数据

实现上,就是把两样东西同时交给 LLM:用户粘的报表数据,和检索来的字段口径元数据。代码骨架你已经很熟了——还是检索、重排、换 prompt:

javascript
// report-insight.js —— 报表数据 + 字段元数据 一起喂,做"结合口径"的解读
import { chat } from '../chat.js'
import { rerank } from './rerank.js'
import { buildHybridSearcher } from './metadata-qa-hybrid.js'

function buildInsightPrompt(hits, dataText) {
  const context = hits.map((h, i) => `[字段元数据${i + 1}] ${h.text}`).join('\n')
  return `你是一个数据分析助手。结合字段口径元数据,解读下面这份报表数据。
规则:
1. 先点出关键趋势(涨跌幅、拐点)和明显异常值。
2. 结合元数据解释数字含义——比如某字段是税后金额、某状态代表退款,要在解读里用上。
3. 不要编造元数据里没有的口径,没有就只做数字层面的描述。
4. 控制在 150 字内,先给结论再给依据。

字段口径元数据:
${context}

报表数据:
${dataText}`
}

export async function interpretReport(searcher, question, dataText) {
  const candidates = await searcher.search(question, 20)  // ① 检索:还是没改
  const hits = rerank(question, candidates, 5)            // ② 重排:还是没改
  const text = await chat(question, hits, buildInsightPrompt(hits, dataText))
  return { text, sources: hits }
}

为什么这么写,三个要点:

  • 元数据放前、数据放后,分两段给。 元数据是"口径字典"(拿来查的参考),数据是"被解读的对象"(拿来分析的正文)。在 prompt 里分成两段、标好名字,LLM 才不会把"参考资料"和"待分析数据"搅在一起。这是喂多路信息时的一个通用小技巧。
  • 规则 3 是防线:没口径就只描述数字,别编。 这跟第一篇的"拒答"、第三篇的"元数据不足就说不足"是同一种精神——宁可少说一句,不可瞎说一句。模型有种讨好倾向,会为了"显得自己结合了元数据"而硬编一个口径出来,规则 3 就是摁住这个冲动。
  • 检索还是那一套 search + rerank 数一下——这已经是第三次复用同一个检索器了(问答、SQL、报表)。第 1 章那张表不是空谈,是真的三个功能共用一套地基。

💡 数据太大怎么办(一个能加分的扩展点)

如果用户粘几百行报表,全塞进 prompt 会爆 token。工程上的处理是:先聚合再喂——按天/按类汇总成几十行,或只取"前 N 行 + 整体统计摘要",让 LLM 看浓缩版而不是原始流水。本文的 Demo 数据小,没做这步,但面试时能主动提一句"数据大了我会先聚合降 token",是个体现工程意识的细节。

🧭 承上启下

三个插头都装完了。退一步看这四篇一路走来,会发现真正沉淀下来的资产,其实不是这三个功能本身。


🎁 结语:真正的资产,是那套"地基",不是三个功能

💎 只记一句话也够

这一篇表面在做 Text-to-SQL 和报表解读,内核只讲了一件事:一套打磨好的检索地基,能用"换个 prompt"的成本,不断长出新能力。地基,才是你真正的资产。

把这四篇连起来看,是一条特别清晰的线:

  1. 第一篇:搭出最小地基(能检索)。
  2. 第二篇:把地基的坑填平(检索得准)。
  3. 第三篇:给地基加固(混合检索 + 重排 + 多轮,检索得稳)。
  4. 这一篇:证明地基的复利——同一套 searcher,接问答是问答,换个 prompt 是 SQL,再换个 prompt 是报表解读。

前三篇花的所有力气,到这一篇开始连本带利地还。你会发现第 2、3 章真正的新代码,各自只有一段 prompt——因为难的、脏的、值钱的活儿(怎么从一堆元数据里精准捞出相关的几条),前三篇早干完了。新功能之所以这么便宜,正是因为地基这么贵。

这就是我最想透过这四篇留给你的东西——它跟具体的 RAG、SQL 都没关系,是一种通用的工程直觉:

先在最难的那件事(这里是"检索准")上把地基打扎实,让后续的每个新需求都退化成"在地基上换个薄薄的出口"。这样你加功能的速度会越来越快,而不是越来越慢。

面试里,比起说"我会 RAG、会 Text-to-SQL、会报表分析"这一串功能清单,你若能说出——

"我做的是一套检索地基,问答、Text-to-SQL、报表解读三个能力共用同一套混合检索 + 重排序,只有最后的出口 prompt 不同。所以加新能力对我来说是换个 prompt 的成本,不是重写一套。"

——高下立判。前者是"我用过很多工具",后者是"我懂怎么把系统搭成能持续生长的样子"。功能会被问到死角,但这种'把重活沉淀成可复用地基'的判断力,换任何技术栈都值钱。

RAG 系列到此收尾。四篇加起来代码也就几百行,没有一处黑魔法。真正稀缺的从来不是"知道有 embedding、有 BM25、有 RRF",而是能把它们组织成一个越用越省力的系统,并讲得清每一步为什么这么搭。 那,才是这四篇真正想给你的。

🚀 还能往哪走

这套地基的复利还没榨干:把三个能力接进前端 Demo,就是一个能演示的"数据治理助手";给报表解读接上第一篇流式输出,解读就能边生成边显示;再往上,让 LLM 自己决定"这个问题该调问答、SQL 还是报表解读",就迈进了 Agent 的门——那又是另一套故事的开始了。


原创声明

本文首发于我的个人博客 https://rjy92.github.io/。如需转载请注明出处。

👀 本文阅读 ···

基于 VitePress 构建

本站总访问量 ····访客数 ···