资讯动态

Vanna实践:结合RAG与向量数据库,破解Text2SQL落地难题

发布时间:2026/9/8 19:41:48 来源:尧图企业网站定制
差不多两年多没好好写过技术总结了。最近在项目里把 Text2SQL 这块重新拾起来翻了不少开源的方案绕了一圈发现Vanna这个框架挺有意思的它没有跟风去卷 Prompt 模板而是把重心放在了“让模型持续学习你的库表结构和业务口径”上实际用下来解决了不少之前靠堆 prompt 很难解决的痛点。这篇文章就围绕我自己的实践聊聊Vanna在设计上的思考、核心模块、落地部署时的关键步骤以及我踩过的那些坑。1. Vanna是什么一个把“经验”沉淀进数据库的 Text2SQL 框架1.1 先聊聊 Text2SQL 这两年为什么难落地先同步一下背景。Text2SQL 这活儿说白了就是让用户用大白话提问系统自动生成 SQL 去查库然后把结果返回给用户。像“上个月华东区各个品类的退货率是多少”这种问题如果全靠人去写 SQL一天也写不了几条还得等排期。所以大家自然希望有个模型能把这事儿干了。但这类需求真正落地的时候会撞上两个很典型的问题。第一个是业务语义太复杂。同一个“退货率”在 A 部门的口径是按“退货申请时间”统计在 B 部门可能按“实际退货入库时间”统计。你要是只丢给大模型一句“根据 orders 表和 returns 表计算退货率”它生成的 SQL 大概率只会在语法层面正确业务层面根本没法用。第二个是库表结构经常变。业务跑着跑着字段改名了、新表上线了、某个枚举值的含义变了模型本身并不知道。所以你会看到很多人用 RAG 方式把 DDL 文档塞给大模型但 DDL 只告诉模型表结构长什么样没法告诉它“退货率怎么算才对”也没法告诉它“orders.status closed 的订单才算有效订单”。这类知识靠通用大模型的预训练知识恰恰是最缺的。Vanna 的思路跟绝大多数框架不太一样它不试图让模型变成“通才”而是让你把“对于这个库怎么查”的经验给模型做一个定向训练。训练完的模型每次回答你的问题之前会先从已经灌进去的知识里检索相关信息再去生成 SQL而不是上来就硬写。1.2 Vanna 的独特设计训练与生成分离这套框架的核心机制概括成一句话就是把 Text2SQL 拆成“训练”和“生成”两个阶段。训练阶段你可以把三类信息喂给 Vanna数据库的 DDL 信息让模型知道有哪些表、哪些字段、字段之间的约束关系。业务文档注释例如“退款金额取 refund_amount 字段退货率 退货单数 / 订单数”。一批“问题-SQL”对照样本这是最实用的形式。比如你问“2024年3月各渠道订单量”它的标准 SQL 该怎么写直接把这对抄给 Vanna。这些信息会被切分、向量化之后存到向量数据库里。到了生成阶段用户再抛来一个新问题Vanna 会先从向量库里找出跟这个问题最相关的几段信息比如某个表格的建表语句、跟你问题最接近的历史问题以及它对应的 SQL把这些内容拼到上下文里一起交给底层的大模型去生成 SQL。这个流程看起来不复杂但它精妙地解决了一个问题LLM 本身不需要记得住你的业务口径它只需要在每次被调用时实时拿到最相关的上下文。这样就不需要频繁微调模型也不需要把整个系统 prompt 写得又臭又长。你积累的训练数据越多检索到的上下文越准生成的 SQL 就越贴近业务实际。1.3 与传统方案最直观的差异我最早接触 Text2SQL 时试过两种主流路线一种是写好几十条 Few-Shot 示例把 prompt 撑得特别大再配合 Function Calling 去调另一种是直接把 DDL 和文档全部硬塞进上下文里凑上下文窗口。这两种方案在 demo 环境里都能跑一上生产就露馅prompt 太长导致响应变慢、成本变高而且很容易让模型被带偏上下文里的信息太多太杂模型不知道应该重点看哪段容易生成语义不对的 SQL。Vanna 的“先检索、再生成”本质上属于 RAG 路线但它给我们的启发更多是工程架构上的知识不是模型自带的而是系统喂的系统喂什么模型就只能答什么。所以它的优势不是模型多聪明而是知识库可以随业务持续积累。你每补充一条问题-SQL 对都是在教模型一种新的问法长期用下来这个框架在团队内部会越来越“懂业务”。2. 框架核心模块拆解与选型思路2.1 第一层模型层LLM 适配模型层负责的事情很纯粹接收 Vanna 给出的“提示词 检索到的上下文”输出 SQL。Vanna 在早期版本中设计了一套 ABC 类抽象基类常见的大模型厂商 API 都有对应的实现类。如果你公司已经接入了 OpenAI 兼容格式的网关现在很多企业内部都有这种代理可以直接继承VannaBase或者使用它已经封装好的OpenAI_Chat类然后把base_url改成自己的网关地址即可。from vanna.openai.openai_chat import OpenAI_Chat class YourVanna(OpenAI_Chat): def __init__(self, configNone): super().__init__( modelyour-gpt-model, OpenAI_api_keyconfig.get(api_key), OpenAI_api_baseconfig.get(api_base), temperature0.2 )温度这个参数我强烈建议调低初始值设 0.2 以内。Text2SQL 对确定性要求极高temperature 高了就会出现“同样的问法每次生成的 SQL 都不太一样”的情况这对后面做结果缓存的场景很不友好。2.2 第二层向量存储层与知识库构建向量存储层是 Vanna 的知识仓库。推荐用ChromaDB或Qdrant如果公司正好有条件也可以直接用Milvus或pgvector。业务量不大的话本地跑一个 ChromaDB 就够用了整个向量库就是本地文件夹备份迁移都方便。数据存储的逻辑很简单。每次调用vn.train()传入的文本会被切片成若干段再用 embedding 模型转成向量。切分策略默认按 token 窗口和重叠窗口来但我个人体会是 DDL 这种结构化文本不太适合按通用长度硬切字段注释和表注释会被切断检索效果会变差。所以如果公司内部有自己的信息检索网关尽量在 embedding 这一步也走同一套向量模型这样检索出来的质量和内部搜索工具的一致性更高。2.3 第三层数据库连接与执行层模型生成 SQL 之后Vanna 需要真实执行它才能把结果返回给用户。框架内部通过各类连接类来跟不同数据库通信比如VannaSQLiteConnection对应 SQLiteVannaPostgresConnection对应 PostgreSQLVannaDuckDBConnection对应 DuckDB。如果业务库不在这些内置支持里面可以通过run_sql方法自行封装。这个点我特别提醒一下执行层一定要和业务系统解耦。Vanna 生成的 SQL 有可能是错的或者虽然是正确的但代价很高如果直接连生产事务库执行风险极大。最好是单独开一个只读账号或者配置独立的分析师查询库权限上做好限制才能避免测试阶段把线上业务拖垮。2.4 架构设计的取舍思路为什么不直接让大家用 LangChain 之类的框架去搭我个人的体会是LangChain 本身更偏向“给你积木自己搭”需要你自己处理的知识库分块、检索器、总结链等环节特别多工程量大且不好维护。Vanna 的设计则是一个很完整的垂直解决方案它把信息检索和 SQL 生成封装成一个可直接运行的 pipeline你要做的只是注入自己的模型服务、向量库和数据源。这看起来牺牲了一部分灵活性但它把这种“如何组织训练样例”的经验固化在框架流程里了相比从零搭建要省不少事。3. 实操复现搭一个能用的中文问数 agent3.1 环境准备与依赖安装以 Python 3.10 为例先把基础依赖装上pip install vanna如果你想用本地 Chroma 存向量再补一行pip install chromadb我用过几种组合目前比较顺手的是Vanna Chroma OpenAI 兼容协议网关因为公司内网连外网模型不现实走内网网关最稳。如果是个人学习接 DeepSeek 或者其他兼容 OpenAI 协议的模型也没问题Vanna 里很多类直接拿协议去套就行不需要写胶水代码。接好之后初始化一个 Vanna 实例from vanna.openai.openai_chat import OpenAI_Chat from vanna.chromadb.chroma_db import ChromaDB_VectorStore class MyVanna(ChromaDB_VectorStore, OpenAI_Chat): def __init__(self, config): ChromaDB_VectorStore.__init__(self, configconfig) OpenAI_Chat.__init__(self, configconfig) vn MyVanna( config{ api_key: your-key, api_base: http://your-internal-gateway/v1, model: your-model, path: ./vanna-store } )这里有个小细节类继承顺序建议把ChromaDB_VectorStore写在前面OpenAI_Chat写在后面否则某些方法查找时可能走到错误的分支。我遇到过两次因为继承顺序不对导致向量库的get_similar方法没有生效排查了半天才发现是 MRO 问题。3.2 初始化训练数据把关键知识灌进去Vanna 的train有三种主要方式# 1. 直接输入 DDL vn.train(ddl CREATE TABLE orders ( id INTEGER PRIMARY KEY, region TEXT, category TEXT, amount NUMERIC, created_at TIMESTAMP ) ) # 2. 输入业务注释文档 vn.train(documentation 退货率的计算口径退货单数除以订单总数按订单创建月份统计。 ) # 3. 喂问答对最推荐 vn.train( question2024年3月华东区各品类订单金额, sqlSELECT category, SUM(amount) FROM orders WHERE region华东 AND created_at 2024-03-01 AND created_at 2024-04-01 GROUP BY category )训练的时候它会把文本向量化后存入数据库不需要真正跑 SQL。但注意如果业务库表很多一次性把成百上千张表的 DDL 全塞进去检索时会浪费 token而且可能引入噪声。建议先按业务域筛选核心表只把高频使用的那几十张表灌进去。3.3 通过 ask 完成一条查询链路训练好之后用户的提问链路就非常短了response vn.ask(2024年3月华东区各品类订单金额)ask方法返回的并不是 SQL 字符串而是一个结构里包含多个字段比如question用户原始提问sql模型生成的 SQLdf执行查询后拿到的结果表plotly_code如果原始数据适合出图Vanna 还会附带生成一段画图代码。我之前以为它只是生成 SQL后来发现它在执行 SQL 之外还附带了图表能力做内部问数工具时这段代码可以直接在前面套一层可视化组件省掉了另外接图表库的功夫。ask里还有几个参数值得注意。如果让print_resultsTrue它会打印整个问答过程。这个参数在调试阶段很有用你能看到它做了几次检索、最后生成了什么 SQL、执行结果如何。另一个auto_trainTrue会在每次成功执行 SQL 后把这个真实可行的“问题-SQL”对自动补充到训练库里。这个特性我后来在项目里一直开着经过几个月积累它能自动留存很多用户实际验证过的问题效果比手工维护强太多了。3.4 部署服务端模式让业务系统能调用本地 Python 脚本调用 Vanna 只能自己在电脑上玩真正给团队用还得把它包装成一个服务。Vanna 官方其实自带一个Flask服务端示例可以直接跑起来然后通过 HTTP 接口去请求。实际开发中我还是倾向于自己封装一层 FastAPI因为可以方便地加上认证鉴权、操作审计、上下文日志等逻辑。一个简化版的 FastAPI 封装大致是这样from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class QueryRequest(BaseModel): question: str app.post(/ask) def ask_endpoint(req: QueryRequest): result vn.ask(req.question) return { sql: result[sql], data: result[df].to_dict(orientrecords), error: result.get(error) }这样前端页面或者 IM 机器人只需要发一个{question: ...}的 JSON 过来系统内部完成训练知识检索、SQL 生成、SQL 执行这几步把数据结果回传。后续如果要对接企业微信、钉钉机器人只需要把请求体剥出来做一层适配即可不需要再动核心逻辑。3.5 运营过程中必须关注的几个环节整个框架搭起来简单但真正跑得稳运营就得跟上。我自己的经验是头两周一定要有人专门盯着SQL 执行错误日志。如果用户的某些问法没检索到合适的上下文模型大概率会生成一个语法错误或语义错误的 SQL这时候要把错误样本整理出来人工修正后再补一批 question-SQL 对喂回去。持续做两三周模型的稳定度会有非常明显的提升。这里有个判断标准如果每次反馈都要一两个小时才能优化好说明训练数据还远远不够如果系统回答质量已经稳定在 90% 以上再把人工介入的流程简化成只处理那 10% 的边界场景即可。4. 常见问题排查与避坑实录4.1 问中文问题总是“听不懂”SQL 生成很飘这种情况八成不是模型的问题而是向量检索阶段没找到匹配的知识。可能原因有两个。第一个是训练样本里的“问题”质量和用户真实提问的措辞差异过大。你训练时写的是“华东区订单额”用户提问可能说的是“华东有多少单卖了多少钱”关键词对不上检索自然失效。解决方案简单粗暴把同一种业务需求的多种说法都尽量补进去。越贴近用户真实口语检索命中率越高。第二个是 embedding 模型对中文本语支持不好。有些通用 embedding 模型在中文语义上表现一般容易出现“意思相近但表达不同”的两句话向量距离很远的情况。建议选择对中文优化过的向量模型或者在内部模型网关切换测试几个版本观察检索命中率的差异。4.2 SQL 执行报错或者执行结果明显不对SQL 执行报错可以分两类看。一类是语法错误。模型生成的 SQL 某字段不存在或者表名写错了大概率是知识库里没有这个表的最新 DDL。直接去数据库里把最新 DDL 补进训练库即可。另一类是执行太慢。Vanna 在生成 SQL 后默认会真实执行它如果一张表几亿行又没走对索引查询可能挂很久。这时候建议在 SQL 执行前加一个超时控制。例如在底层连接对象上设置connect_timeout和statement_timeout避免某条慢 SQL 拖垮整个服务。还有一类是执行结果本身有问题比如返回的数据和业务方认知明显对不上。这种问题往往不是 Vanna 或大模型生成的语法错误而是训练数据里的“口径”不对。比如你给它的问答对里退货率按申请时间算但业务方要的是入库时间算它按旧口径执行自然不准。解决办法是把文档注释和问答对里对应的口径描述更新。4.3 ask 一直卡住没有响应排查思路按顺序来。先确认模型服务接口是否畅通。很多企业内部网关有并发限制或者对单一 token 的每分钟请求次数有限制。Vanna 在生成时会连续调用好几次 LLM一旦触发限流整个请求就卡住。日志里能看到 429 或者超时这种情况在代码里做一下重试包装即可。再确认向量数据库是不是因为并发访问被锁。ChromaDB 用的是本地文件存储如果服务是多进程部署而所有进程共享同一个./vanna-store文件夹容易出问并发读写问题。基础版可以用 SQLite 或者加文件锁更稳妥的做法是直接换pgvector之类支持并发读写的方案。4.4 Vanna 对大模型输出做了哪些策略限制Vanna 框架会把大模型生成的最终 SQL 先作为字符串截取出来再做基础语法检查。也就是说即便底层大模型输出了 Markdown 代码块或者带了解释性文字Vanna 也会用启发式规则尽量从中提取出 SQL。这个处理策略让它在面对不同模型时容忍度比较高。不过在服务端自己实现时不要过度依赖这种容错最佳做法是还是在调用模型时让输出格式严格只给 SQL减少一层解析风险。4.5 提供一些实用调优技巧如果觉得检索质量还是不稳可以从这几个参数切入调整。vn.temperature调低到 0.1 ~ 0.2前面说了生成的稳定性很重要。然后是top_k在generate_sql的提示词构造时可以指定从向量库里召回几条相关上下文默认在 5 到 10 条之间如果业务知识太杂适当地把top_k降到 5 或许效果更好。每次召回的 DDL 数量太多反而会让模型判断不了该参考哪一段。另外训练数据的更新要有节奏。每跑完一轮业务周期就把新的“问题-SQL”对补进去并把旧版本文档标记废弃。不要让训练库无限膨胀时间久了向量检索命中老口径的概率会增高反而影响生成质量。5. 部署模式与长期演进建议5.1 嵌入模式还是服务端模式Vanna 支持两种使用方式。嵌入模式就是在你自己的 Python 进程中直接调用 Vanna 对象适合个人分析或者工具型产品内部集成优点是链路短、易调试缺点是服务和执行引擎耦合在一起不容易扩展。服务端模式就是前面 FastAPI 封装的方案通过 HTTP 对外提供接口适合团队系统集成也方便把权限、审计、监控单独管理。我在实际项目中选择了从嵌入模式起步跑原型验证通过后再快速切到服务端模式整个过程用不了几天。这里有个架构上的取舍经验嵌入模式下 Vanna 可以直接使用你的本地数据库连接配置开发效率高一旦上了服务端模式就要为每个用户维护独立的数据库连接信息还要处理并发场景下的串行问题。5.2 多用户场景下的权限与隔离问题多用户使用问数服务时Vanna 本身并不关心你的权限体系它只是执行模型生成的 SQL。所以权限控制必须在两个层面做。第一层放在SQL 生成层面比如用户问其他部门的数据你要么在检索知识库阶段就过滤掉不该让他看到的表文档要么训练一批带“禁止查询”语义的样本让模型识别。第二层放在数据库执行层面给不同用户分配不同的只读账号通过数据库的行级权限或列级权限限制他们能看到的数据范围。前一层做得再好也经不起后一层缺失的风险安全底线必须兜在数据库上。5.3 怎样的团队适合引入这套方案统计部门需要频繁输出数据报表、业务运营要临时拉数看板、数据分析师人数不够用、或者企业数据仓库的表结构比较复杂这些场景都适合用 Vanna 先把高频取数做成自动化。反过来如果团队数据库表结构本身极其混乱、没有元数据管理、业务口径完全没有统一的文档那我建议先别急着上 Vanna。它本质上解决的是“已经清楚怎么查但大家不会写 SQL”的问题而不是“大家根本不知道数据怎么定义”的问题。数据治理还没做好的团队应该先治理数据口径再上 Text2SQL。5.4 长期使用后的维护思路在实际项目里稳定运行一段时间后Vanna 的定位会从一个“查数工具”变成一个“数据权限代理”。我在设计内部架构的时候会让它不对接核心生产表而是对接数仓的汇总层 / 数据集市层。这里面有一个很重要的原因大多数数据部门的底层模型表都是根据业务过程构建的字段命名相对晦涩不适合让模型去猜而汇总层和集市层的字段名已经足够业务化模型生成 SQL 的语义容易理解生成结果的正确率也会大幅提高。这套思路执行下来Text2SQL 的稳定性比我最初直接连底层模型表时要高很多。长期使用还有一个维护点训练知识库不是“灌一次就完”它是一个活的资产。表结构变更、口径调整、新业务上线都需要同步更新训练库里的 DDL 和文档。可以设置一个定时任务定期从元数据平台拉取表结构变更自动对比已训练的知识有变动时执行一次覆盖式的train(ddl...)把过期 DDL 自动替换掉。5.5 基于 Vanna 做二次开发的几个方向业务要往前深入纯靠 Vanna 自带能力有时不够。可以在这个框架上做二次开发的方向包括一是把 RAG 检索的质量管起来。Vanna 内部检索用相似度来拉取上下文优化检索结果可以引入关键词或过滤规则的逻辑只检索当前用户有权看到的表结构这种权限感知的检索方式比检索完再过滤更友好。二是往 SQL 生成的前后各加一道校验。前一道是“语义检查”用分类器判断用户问题是否涉及敏感统计口径、是否超出系统能力范围后一道是“结果断言”比如生成结果为空时自动重写问题再做一次查询跑不通时用错误日志触发新的生成回合。三是把 Vanna 变成“数据问答中台”的能力底座而不是一个孤立服务。中台上层对接 OA、IM、报表系统下层对接统一的 SQL 查询网关和元数据中心Vanna 只负责把自然语言翻译成 SQL 的这一步前后的业务流都往中台抽象。6. 写在最后的一点个人体会这套框架真正让我留下的原因不是某个模型跑分有多高而是它在工程实现上给了我一个非常舒服的节奏先跑通再积累最后越用越准。市面上很多 Text2SQL 的 Demo 做完就完了恰恰是因为整套链路缺少“持续学习”的环节而 Vanna 很自然地把这个环节做进了产品逻辑里面。我自己的项目发展路径是第一周接到需求后先跑通单机脚本第二周封装成 FastAPI 服务给数据分析团队试用第一个月把高频取数场景全部覆盖到第二个月开始在查询量比较大的几个数据域之上做 API 网关和权限隔离。走到这一步后团队的临时取数需求减少了大约一半分析师终于可以分出精力去做真正有价值的专题分析。最后再分享一个很小的技巧想让 Vanna 生成的 SQL 更贴合你们公司已有的查询规范最高效的办法不是写一堆 prompt 约束它而是把你们团队里写得好、审得严的 SQL 挑出 50 到 100 对整理好之后一次性喂进训练库。它学得远比你想的牢靠因为这些样例本身就是业务的“标准答案”。凡是能沉淀成标准答案的经验都值得反复喂给它直到它彻底学会为止。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价