Vanna:用 RAG 让数据库听懂人话的 Text-to-SQL 开源方案
📖 简介
📝 详细介绍
1. 开篇:一个真实的报表难题
今年年初,我们给公司 CRM 系统做数据分析平台。业务部门每天至少在钉钉群里发 30 次"帮我查一下上周华东区的回款金额"之类的需求。ETL 工程师小周每天的工作就是:看懂需求 → 翻 40 多张表的字段 → 写 SQL → 跑通 → 截图发群。平均一条查询从提出到拿到数据要 40 分钟,赶上月末盘点直接爆单,群里排队到下午。我接到的任务是:做一个内部工具,让业务人员自己提问,系统自动生成 SQL 并返回结果。
2. 需求拆解
这个需求看起来简单,拆开之后发现约束很多:
- 数据:40+ 张核心业务表(订单、客户、回款、产品),字段命名混乱(比如 `cust_id` 和 `customer_id` 混用),且有大量业务口径藏在文档里(比如"有效回款 = 回款金额 - 退款 - 冲销")。
- 性能:查询响应要控制在 15 秒内(首次生成 SQL + 执行),这个时间业务人员能接受。
- 成本:预算 0 元,老总明确说"不额外花钱买服务"。
- 部署约束:公司要求数据不能出内网,不能调用公网 API(OpenAI 之类的直接排除)。
3. 方案设计:为什么是 Vanna
我调研了三个方向:LangChain + SQL Agent、Chat2DB、Vanna。LangChain 的 Agent 很灵活,但需要我们自己写 SQL 工具链、管理对话上下文、处理模型幻觉,工程量大;Chat2DB 开箱即用但偏个人工具,二次开发做 API 集成很别扭,且不支持自定义训练语义。Vanna 的定位非常精确:Text-to-SQL 专用 RAG 框架。它的核心是把数据库 Schema(DDL)、业务文档、示例问题-SQL 对做向量化训练,查询时先把用户问题在知识库里检索相似片段,再丢给 LLM 生成 SQL。这正是我要的:把"业务口径"提前注入,而不是让模型每次裸猜。
最终取舍:放弃通用对话能力,专注"提问 → SQL → 结果"这一个闭环。用 Vanna + Ollama(本地 Llama3.1 8B)+ ChromaDB 向量库,完全不依赖外网。
4. 落地实现
4.1 数据准备
这步是整场战役的关键,我花了 3 天,比写代码时间长得多。先导出所有表的 DDL:
# 用连接串连到研发库,产出 schema.sql(约 3MB)
pg_dump --schema-only --no-owner -U readonly -h internal-db crm_core > schema.sql
然后从业务侧收集了一本《CRM 字段口径手册》(内部 Wiki),整理成纯文本 `business_glossary.txt`,每个字段单独起段,比如:
有效回款:回款金额 - 退款 - 冲销,在财务明细表 recharge_detail 中标记状态为 'valid'。
客户状态:active 表示在保,churn 表示流失,candidate 表示未签单。
4.2 构建 RAG 训练
我写了一个训练脚本,把 DDL、术语表和 20 条典型的"自然语言→SQL"样例喂给 Vanna。注意:这里重点不是模型,而是让 Vanna 的知识库把"问法"和"字段"绑定起来。
from vanna.chromadb import ChromaDB_VectorStore
from vanna.ollama import Ollama
class LocalVanna(ChromaDB_VectorStore, Ollama):
def __init__(self):
ChromaDB_VectorStore.__init__(self, config={"path": "./chroma_db"})
Ollama.__init__(self, config={"model": "llama3.1:8b", "host": "http://localhost:11434"})
vn = LocalVanna()
vn.connect_to_postgres(host="localhost", dbname="crm_core", user="readonly", password="***", port="5432")
# 训练三件套:DDL + 文档 + 样本
vn.train(ddl=open("schema.sql").read())
vn.train(documentation=open("business_glossary.txt").read())
# few-shot 示例,我整理了 20 条,这是最关键的一条
vn.train(question="上个月新增了多少个有效客户?",
sql="SELECT COUNT(DISTINCT customer_id) FROM customers WHERE status='active' AND created_at >= date_trunc('month', CURRENT_DATE - INTERVAL '1 month');")
这里用 `ChromaDB_VectorStore` 做本地向量库,用 Ollama 加载本地模型。8B 参数对 Text-to-SQL 足够了。
4.3 部署成内部 Web 服务
用 Flask 包了个极简 API,前端就一个对话框,后台逻辑很直接:生成 SQL → 执行 → 返回 DataFrame 转 JSON。
from flask import Flask, request, jsonify
import pandas as pd
import vn # 上面训练好的实例
app = Flask(__name__)
@app.route("/ask", methods=["POST"])
def ask():
question = request.json["question"]
sql = vn.generate_sql(question)
df = vn.run_sql(sql)
return jsonify({"sql": sql, "data": df.head(100).to_dict("records"), "cols": list(df.columns)})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080)
加上一个命令行 `curl` 测试:
curl -X POST http://localhost:8080/ask -H "Content-Type: application/json" -d '{"question": "华南区上季度回款前五的客户"}'
5. 效果与数据
上线两周后,我从后端日志抽了 312 条真实提问做了人工标注,对比上线前后(人工写 SQL 是历史数据外推估算):
| 指标 | 上线前(人工写 SQL) | 上线后(Vanna) |
|---|---|---|
| 单条查询平均耗时 | 约 40 分钟(含排队) | 11.6 秒(SQL 生成 4.2s + 执行 7.4s) |
| SQL 首轮生成准确率 | 100%(但依赖人等) | 76.3%(238/312 条直接可用,无需修改) |
| 人工介入率 | 100% | 23.7%(多为业务口径理解偏差) |
| 月度数据查询量 | 约 380 次 | 2176 次(业务开始主动用了) |
| 额外成本 | - | 0 元(纯本地 Ollama + ChromaDB) |
注意,准确率离"完全替代 DBA"还远,但把 40 分钟压缩到 12 秒,体验质变。
6. 踩过的坑
坑 1:字段别名对不上——"客户"到底是哪个 column?
现象:业务问"查一下有多少客户",生成的 SQL 是 `SELECT COUNT(*) FROM cust_info`,但实际库里 `customers` 表字段叫 `customer_id`,没有 `cust_info` 表。模型编造了表名。
排查:Vanna 的 RAG 检索文档时,命中的是 `business_glossary.txt` 里的"客户"条目,但那行写的是"客户主档表 customers",没写别名 `cust_info`。模型在生成时混淆了。
解决:我把每个表的常见别名、历史曾用名全部整理进 DDL 注释 + documentation 里,例如"customers 表,别名:客户表、crm_customer(旧名)。"重新训练之后,这类错误明显减少。
坑 2:生成的 SQL 能跑,但结果全错——多表 JOIN 笛卡尔积
现象:"查询每个销售人员的项目数量",模型生成的 SQL 用了 `LEFT JOIN` 但缺少 GROUP BY 的维度引用,返回了 18309 行,实际只有 16 个销售。业务看到数字后直接在群里说系统是垃圾。
排查:我加了日志,把生成的 SQL 打出来,发现模型对"项目数量"理解为"项目表的行数",JOIN 时 1 对多展开导致翻倍。
解决:双管齐下。第一,在 Vanna 的 `vn.train(question=..., sql=...)` 里补充了 5 条"分组汇总"的示例,明确"数量 = COUNT(DISTINCT column)"的模式;第二,在代码层加拦截:生成 SQL 后先 `EXPLAIN` 估算返回行数,如果超过阈值(比如 1 万行)就打回重新生成一次。
坑 3:Ollama 模型推理慢,首字延迟高
现象:本地 Llama3.1 8B 生成一条 SQL 平均要 20~30 秒,远超 15 秒目标。同事反馈"体验不如等小周写 SQL"。
排查:CPU 推理 + 无 GPU,8B 模型在长上下文中生成时间长。但 Vanna 的 RAG 会把少量相似样本拼进去,一次生成几百 token 很正常。
解决:换成 Qwen3-4B(体积下 2.4GB),在保证结构化 SQL 能力持平的前提下,生成延迟降到 4.2 秒。另外给 Ollama 加了 `OLLAMA_NUM_PARALLEL=2`,允许多个查询同时推理,避免排队。
7. 复盘与扩展
复盘下来,做对的决策有三个:选 Vanna 而不是 LangChain(RAG 和 SQL 生成天然耦合,省了大量调 prompt 的时间);本地小模型 + 向量库(零成本、数据合规);把 20 条业务样例变成训练语料(这比任何模型调优都管用)。
做得不够的:只做了"生成 → 执行"的单向链路,没有加数据血缘和操作确认环节。一些 DELETE 级的问题如果被误生成,会很危险。后续应该加一个"危险 SQL 拦截器"(检测无 WHERE 的 UPDATE/DELETE,直接
AI 项目推荐
大模型- 标签
- #Text-to-SQL #数据库 #RAG #数据分析
- 浏览
- 👁️ 16
- 发布日期
- 2026-08-30