最近在做企业内部数据问答项目时,被业务同学问得最多的一个问题就是:能不能让我自己用自然语言查数,别每次都要提工单等排期?“RAG+SQL混合查询Agent”就是我在这个项目里最终落地的方案。简单说,它先通过RAG把数据库表结构、字段口径、示例SQL这些“使用说明书”检索出来,再让Agent根据检索内容生成并执行SQL,最后把结果包装成业务能看懂的自然语言答案。这篇文章把从方案选型、知识库建设、代码实现到踩坑排查的完整过程写出来,适合正在做数据问答、智能报表、自然语言查数的数据开发、后端工程师和AI应用从业者参考。
我从一个跑通demo的简单原型,到真正能支撑团队日常查数的系统,中间踩了不少坑。这篇不是纯理论,我会尽量少讲空话,多给能直接抄作业的方案,包括知识库怎么写、检索怎么调、Agent怎么编排、SQL挂了怎么自愈,以及上线前一定要做的评估。如果你正准备做类似的东西,这篇可以少走很多弯路。
1. 为什么要把RAG和SQL查询揉在一起
1.1 单纯SQL查询的痛点
大多数企业内部的数据情况是这样的:表有几百张,字段命名千奇百怪,业务口径散落在不同人的脑子里。业务同学想看一个数,第一反应不是打开BI工具,而是直接截图发到数据群里问“这个月华东区销售额是多少,帮我拉一下”。数据开发每天大部分精力都花在这种需求翻译上:先理解业务问的是什么,再去找表,再确认口径,再写SQL,跑出来还要解释半天。
早期我们试过把SQL生成能力直接丢给大模型,让用户输入问题,模型输出SQL,然后自己执行。单表场景跑得很顺,比如“查一下用户表有多少行”,模型能写出来。但只要涉及多表关联、字段口径、时间条件,效果就急剧下降。原因是模型不认识你的库,它只是根据通用的SQL语法在“猜”,猜出来的表名和字段名在真实库里可能根本不存在。
后来我也试过把所有表结构一次性塞进prompt。表少的时候可以,几十张表的DDL加起来就是几万字,prompt直接超长,模型处理不过来,还会把注意力分散到不相关的表上。项目里一旦表数量超过30张,这个方案就彻底不可行了。所以问题的核心不是“模型不会写SQL”,而是“模型不知道你这个数据库到底长什么样”。
1.2 RAG能补什么:从“会查”到“知道怎么查”
大模型已经解决了“怎么写SQL”的问题,它缺的是“查哪张表、用哪个字段、什么口径才对”这一类具体的库内知识。RAG做的事情,就是把这些知识做成一个可检索的外部知识库,在每次查询时把最相关的内容取出来,作为Prompt的一部分给模型“临时补课”。
我用个比较简单的类比:假设团队来了一个新数据分析师,你不会直接让他去连数仓,而是先给他一套数据库使用手册。手册里写清楚了:要查销售额,用订单明细表,金额字段是实付金额,退款要排除;统计活跃用户,用用户表,状态字段为1才算;跨越表的关联,要用统一的用户ID。RAG就是把这本手册变成动态检索的知识库,模型每次处理新问题时,根据问题语义把对应章节拉出来,而不是整本手册硬背。
所以RAG在这个场景里补的是“数据字典”和“业务口径”,不是补SQL能力。这一点很重要,决定了后面整个知识库的设计思路:不是堆文档,而是把表结构、字段解释、示例问答这些“查询说明书”写清楚。
1.3 混合查询的典型使用场景
三个词对应三种能力:RAG负责找知识,SQL负责取数,Agent负责编排决策。这三者合在一起,能覆盖的场景挺广的。
最典型的是企业内部数据问答。业务同学在对话框里输入“上月华南区销量Top10的商品是哪些”,系统通过RAG检索出订单表、商品表和区域表的schema说明,Agent生成一条多表关联的SQL,执行后把结果转化成一句自然语言答案。
还有一类是数据开发自动化。我们有同事把周报里的固定指标,比如日活、转化率、GMV,全部整理成了“指标-口径-SQL模板”的示例库,Agent接上这个库之后,周报取数从每次半小时缩短到两分钟。
数据分析师做临时提数也适用。过去提一个多表关联的复杂需求,排期至少半天。现在分析师自己用自然语言描述需求,Agent生成SQL后先给分析师确认再执行,效率提升明显。这里的分工是:RAG决定了模型能不能找到正确查询路径,SQL负责真正从数据库取数,Agent负责判断什么时候该检索、什么时候该执行、报错了怎么处理。
2. 整体架构与核心设计思路
2.1 分层架构:检索层、编排层、执行层、反馈层
我最终落地的架构分四层,每一层职责单一,出了问题能快速定位。
第一层是检索层,负责接收用户问题,去向量库和关键词索引里召回相关的表结构文档、字段口径、示例SQL。第二层是编排层,由Agent的思维循环组成,它决定下一步要调用哪个工具、参数是什么、是否已经得到可以回答用户的信息。第三层是执行层,负责真正连接到数据库,校验SQL安全性,执行查询,限制超时和返回行数。第四层是反馈层,把数据库报错、查询结果、执行状态反馈给Agent,让它判断是继续修改SQL还是直接回答用户。
这个分层最大的好处是可控。每条查询经过了哪些环节都有日志,哪一层出了问题可以直接查。实际运行中,我见过太多项目把Agent逻辑和数据库连接混在一起写,出了问题很难排查。四层架构虽然前期多花点时间,但后续维护省心很多。
2.2 为什么不能直接用大模型生成SQL
有人可能会问,既然模型SQL能力强,为什么不能直接丢给大模型?我之前也这么踩过。问题在于,生成SQL除了语法,最关键的是要“选对表、选对字段、选对口径”。这三件事模型都不知道。
举个例子,用户问“这个月的有效订单有多少”,如果你的库里有一张订单表,里面同时存在“订单状态”“支付状态”“退款状态”三个字段,不同字段组合出来的“有效”含义完全不同。没有字段口径说明,模型大概率会用错字段。
全域表结构塞进Prompt也不行。一是上下文不够,二是模型会“看到什么信什么”,如果你把100张表都给它,它会倾向随机使用其中几张,命中率非常低。RAG的价值就在这里:它不是为了“让模型看到更多”,而是“让模型只看该看的部分”。把不相关的表过滤掉,模型生成SQL的准确率才能上来。
2.3 关键设计决策:入库还是全量塞进Prompt
我在选型时列过一个对比,这里整理成表格,应该能帮你少纠结:
| 方案 | 表数量少时效果 | 表数量多时效果 | 维护成本 | 推荐度 |
|---|---|---|---|---|
| 全部schema塞Prompt | 简单直接,效果不错 | 极易超长,准确率崩 | 低 | 仅限10张表以内 |
| 人工整理固定示例塞Prompt | 可解释性强,但覆盖有限 | 无法覆盖多业务域 | 低 | 不适合系统级应用 |
| 表结构入库+RAG检索 | 需要先建库,一次到位 | 可扩展到几百张表 | 中 | 推荐,能长期用 |
| 知识库+Agent动态规划 | 同上 | 支持多轮澄清、自愈 | 中高 | 效果最稳,落地最推荐 |
我在项目里最终选了“知识库+Agent动态规划”。因为企业内部表越来越多,今天加一张、明天调整一个字段,固定Prompt根本维护不来。知识库里的内容可以跟着元数据定期同步,Agent也能在复杂场景里自己决定是否澄清、是否重试。
2.4 与纯RAG文档问答的区别
很多人以为RAG+SQL就是“把RAG改了改”,真做起来才发现两者差异很大。纯RAG文档问答里,答案基本都在文档原文里,模型只需要概括总结。RAG+SQL不一样,检索出来的知识只是“查询说明书”,最终答案必须经过SQL执行才能得到。
所以链路里多了三件事:生成SQL、执行SQL、解析结果。这也带来一个全新的问题:SQL不是“像就行”,它必须可执行、结果必须可验证。模型写错一个字段名,整条链路就挂了,这就要求系统具备SQL校验和错误自愈能力。
另一个区别是,文档问答的偏差可以模糊地带过,但SQL查询的结果是精确数据,回答不能靠脑补。比如用户问“上个月GMV是多少”,系统要么返回执行结果,要么明确说“没有找到对应数据”。绝对不能根据文档内容编一个数字,这是这类系统最基本的底线。
3. 落地实操:知识库建设与向量化
3.1 需要准备哪些“知识”
RAG效果好不好,80%取决于知识库质量。我踩过最大的坑就是在模型和Agent上花了很多时间调参数,结果最后发现知识库里连一张表的中文说明都没写清楚。后期把知识库重新整理后,效果一下就上来了。
我建议至少准备三类知识。第一类是表结构说明,每张表一张卡片,写清楚表名、业务用途、主键、关键字段、和其他表的关联关系。第二类是字段口径字典,专门记录容易出错的字段,比如“销售额=实付金额,不含退款”。第三类是示例问答集,每类业务问题配一个“问题+正确SQL+答案解读”。
这里强调一下,知识库不需要把所有字段都写全。我一开始追求全,把每张表的所有字段都写进文档,费时费力,检索效果反而差。后来改成先覆盖高频字段和易错字段,使用频率低的字段等用户问到了再补。数据开发的同事习惯性会字段全量导出,这个习惯在RAG场景里要改。
3.2 怎么写表结构描述文档
写表结构文档的时候,最容易犯的错是把DDL直接搬进知识库。DDL只有字段名和类型,没有业务含义,模型看了也白看。我这里的做法是,每张表用固定模板写一段“人话说明”。
模板大概是这样的:
表名:dw_order_detail 业务域:交易 表用途:存储每笔订单的商品明细记录,一行代表一个商品购买条目 主键:order_id + product_id 关键字段: - order_id:订单ID - product_id:商品ID - sale_amt:商品实付金额,单位元,退款订单该值为0 - pay_status:支付状态,1=已支付,2=未支付 - refund_status:退款状态,0=无退款,1=有退款 关联关系: - 通过user_id关联 dw_user_profile 表 - 通过product_id关联 dm_product_info 表 更新频率:T+1 常见查询场景:销售额、退货率、商品排行这段文档看着简单,但每句话都有用。“一行代表一个商品购买条目”决定了聚合维度,模型如果不知道这个,很容易写错sum的粒度。“退款订单该值为0”就是典型的口径说明。
3.3 示例问答集怎么写
示例问答是让大模型快速掌握“你这套库怎么查”的最直接方式。我建议每个业务域准备5个左右高质量示例,而不是盲目堆量。批量生成的几十个低质量示例会把噪声带进上下文,反而干扰生成。
每个示例的格式我做成这样:
问题:上个月华东区销售额Top10商品 业务口径:销售额=商品实付金额,剔除退款订单,统计华东区订单 SQL: SELECT p.product_name, SUM(o.sale_amt) AS total_amt FROM dw_order_detail o JOIN dm_product_info p ON o.product_id = p.product_id JOIN dw_user_profile u ON o.user_id = u.user_id WHERE u.region = '华东' AND o.refund_status = 0 AND o.order_date >= date_sub(current_date, interval 1 month) GROUP BY p.product_name ORDER BY total_amt DESC LIMIT 10 答案解读:返回商品名称和销售额,按销售额倒序,取前10条示例里的业务口径是精髓,模型看到这个示例后,以后碰到类似问题时就会模仿这种写法。如果遇到日期类问题,我会在示例里用date_sub这种通用函数,让模型举一反三,而不是硬编码某个日期。
3.4 分块策略与向量化细节
分块不能按固定字数来切,这是我特别想强调的一点。表结构文档和示例问答都有强逻辑结构,硬切会切断关键关系。我的做法是“一个文档卡片作为一个知识块”,一张表描述就是一块,一个示例对也是一块。块之间用元数据字段区分,比如业务域、数据类型、是否包含SQL。
向量化时有两个细节影响较大。一是嵌入模型的选择,建议用对中文业务词和SQL代码结构理解更好的模型,如果是开源的向量模型,最好先用你的知识库微调过再上线,这个提升非常明显。二是混合检索,只靠向量召回不行,因为表名字段名这类专有名词的语义检索效果很不稳定。我在向量检索之外,同时跑BM25关键词召回,再把两路结果合并去重,作为最终召回内容。
如果表数量特别多,我会再加一级“先表级召回、再字段级补充”的两级检索:先用用户问题向量召回可能相关的表名,然后针对命中的表取出完整描述和关键字段说明。这样既保证了上下文精简,又不会漏掉核心信息。
4. 检索、SQL生成与Agent执行链路
4.1 召回策略:双路召回加Rerank
我在第3章已经提过双路召回,这里展开讲一下实际参数和思路。向量召回负责理解语义,比如“华东区销售额”能匹配到含“区域”“销售额”“实付金额”的文档;关键词召回负责匹配专有名词,用户问题里出现“dw_order_detail”这种表名时,向量检索可能反而找不到,关键词一搜就准。
双路结果合并时,我给两路分配权重,向量0.6、关键词0.4,然后按得分倒序取TopK。K值一般取5到10。K太小容易漏,K太大模型上下文会被无关信息挤占。上线后我通过日志观察发现,K=8的项目平均效果最好,但你最好自己按数据集测一下。
如果预算允许,合并后可以再接一个Rerank模型做精排。但说实话,我一开始没接Rerank,效果也还行,后面数据量大了才加上。对中小规模场景,双路召回加合理权重已经够用,可以先把Rerank列为里程碑再做。
4.2 Agent编排逻辑与工具集
Agent是这个系统的大脑,但大脑不能裸奔。我给Agent定义了三个核心工具,分别是检索知识库、执行SQL、请求澄清。工具列表如下:
| 工具名 | 作用 | 输入 | 输出 |
|---|---|---|---|
| retrieve_knowledge | 从RAG知识库召回相关表结构/口径/示例 | 用户问题 | 知识片段列表 |
| execute_sql | 连接数据库执行SQL | SQL语句 | 查询结果或报错信息 |
| ask_clarification | 向用户发起追问 | 澄清问题 | 用户补充信息 |
Agent的循环逻辑是这样的:先调用retrieve_knowledge获得上下文,判断知识是否充分;不充分就ask_clarification;充分则生成一条SQL,调用execute_sql执行;执行成功,把结果转化为自然语言答案;执行失败,把报错信息带回来,修改SQL再试。整个过程最多循环三次,超过就转人工。
这里有一个设计原则:Agent不能直接连接数据库,所有查询必须经过工具层收口。这样权限控制、查询审计、超时管理都在工具层统一处理,安全性和可维护性都要好很多。
4.3 SQL生成与执行的完整流程
我给大家写一个核心流程的伪代码,框架名我用通用写法替换,你可以直接参考这个思路落地:
# 核心Agent循环(伪代码,请替换为你实际使用的编排框架) def run_agent(user_question: str): history = [] for round_id in range(3): knowledge = retrieve_knowledge(user_question) history.append({ "role": "assistant", "content": f"召回知识:\n{format_knowledge(knowledge)}" }) action = agent_decide( user_question=user_question, history=history, tools=["retrieve_knowledge", "execute_sql", "ask_clarification"] ) if action["tool"] == "retrieve_knowledge": history.append(action["observation"]) continue if action["tool"] == "ask_clarification": user_question = wait_for_user_input(action["question"]) continue if action["tool"] == "execute_sql": sql = action["sql"] # 安全校验:只读、超时、行数限制 if not safety_check(sql): history.append({"role": "assistant", "content": "SQL存在风险,已拦截"}) continue result = execute_sql(sql, readonly=True, timeout=10, max_rows=1000) if result["status"] == "error": history.append({ "role": "assistant", "content": f"执行报错:{result['message']},请基于该报错修改SQL,不要重复尝试相同的SQL" }) continue final_answer = generate_answer(user_question, sql, result["rows"]) return final_answer return "查询失败,建议联系数据团队协助处理"流程里每一步都有明确的上下文传递,特别重要的一点是,SQL执行失败时一定要把数据库原始报错带回给Agent,并且加上“不要重复尝试相同SQL”的提示。否则Agent很容易陷入死循环,反复执行同一条错误SQL。
4.4 结果后处理:列名映射、数值格式化、溯源
SQL执行成功后拿到的是原始二维表,不能直接丢给用户。我做了三层后处理。第一层是列名映射,把agent生成SQL时的英文列名或缩写名称,按照知识库里的字段说明映射成中文业务名称。比如“total_amt”映射成“销售总额”。
第二层是数值格式化。根据字段口径给数字加上单位、百分比符号或千分位分隔符。比如金额类自动加“元”,比率类自动转成百分比。这个细节很影响体验,原始数据库里往往存的是分或者小数,业务同学要看的是“12.5万元”这样的表达。
第三层是回答包装和溯源。我先让模型把表格数据总结成一段自然语言,同时附上“数据来源:订单明细表dw_order_detail,统计范围为华东区,统计口径为实付金额剔除退款”这样一句话。溯源信息可以直接来自知识库片段,既增强了可信度,也方便业务同学确认口径。
5. 常见问题与排查技巧实录
5.1 表名和字段名幻觉
这是RAG+SQL场景里最典型的坑。现象是SQL语法很完整,但表名在库里根本不存在,或者字段名看起来差不离但实际上写错了。原因基本是知识库召回没命中,模型靠“记忆”猜了一个名字。
排查步骤我整理成了固定的操作路径:先看系统日志里本次query召回了哪些知识片段,如果完全没有召回到相关表结构,那就是检索层问题,去调召回参数或补充文档;如果召回了但模型还是写错,那就要检查Prompt里的约束是否明确,我会在System Prompt里写死:“只能使用知识片段中出现的表名和字段名,否则禁止生成SQL。”
另外我加了一道硬性防线:在SQL执行前,用数据库的元数据表清单做一次白名单校验,SQL里只要出现清单之外的标识符就拦截,不执行。这道防线在后来的线上运行中拦截了不少问题SQL。
5.2 召回结果太多或太少
召回太少,模型拿不到足够上下文,只能瞎猜。发生率最高的是新表刚加进知识库但没向量化好,或者专有名词太强导致向量检索失效。解决办法是加关键词召回,并把TopK适当调大。
召回太多,输出结果里塞满了无关内容,模型也容易被误导。比如用户问“华东区销售额”,结果把商品表、用户表、库存表都召回来了,模型可能就跑偏去关联库存表。我的处理方式是加强元数据过滤,先按业务域过滤掉不相关的表,再做向量检索,检索范围缩小后准确率明显提升。
一个建议是上线初期把每次用户问题的召回结果都记到日志里,定期抽查。很多问题不用看模型输出,看召回日志就已经能定位了。
5.3 Agent陷入死循环或重复查询
Agent循环次数要有限制,不能让它无限试下去。最开始我没设最大轮次,结果遇到一个复杂问题,Agent反复调用检索和SQL执行,把数据库压得报警。
现在的做法是设最大循环3次,每次内部增加一个动作计数器。执行SQL报错时,必须把报错信息传给模型,并且明确要求“不要重复执行同一条SQL,请修改后重试”。即使这样,我仍然留了一个“放弃并转人工”的工具,Agent一旦判断无法解决,就主动终结并返回“请联系数据团队”的提示。
5.4 执行报错后的自愈与兜底
数据库报错大体分为语法错误、字段不存在、表不存在、权限不足四类。前两类通过自愈循环基本能解决,第三类多半是召回没命中,第四类需要提示用户。
自愈的前提是错误信息可读。很多框架默认会截断数据库报错,模型看不到真正的报错原因,自然无法修改。我会把错误信息完整注入下一轮Prompt,同时再把召回的知识片段重新贴一遍,让模型在正确上下文中修改SQL。
兜底策略也很重要:两次自愈失败后,不要再尝试第三次,直接把原始问题和最终SQL状态返回给维护团队,人工介入修改知识库或SQL模板。这不是能力问题,而是很多业务需求本身就不清晰,需要人来判断。
5.5 权限与安全:只读连接、行级权限、查询审计
做数据问答系统,安全边界比功能效果重要。第一,Agent执行SQL必须使用只读账号,禁止DDL和DML语句,这个通过数据库账号权限强制限制。第二,执行层还要额外拦截insert、update、delete、drop、truncate这类危险关键词,双重保险。
行级权限也要考虑。比如地区经理只能查自己负责区域的数据。我在生成SQL之后,会根据用户角色的维度自动追加过滤条件,比如强制加上“region=当前用户所属区域”。这个逻辑最好下沉到执行层,而不是靠Agent自觉,因为Agent的上下文里没有用户角色信息。
最后是审计。每个查询都记录:提问用户、用户原问题、生成SQL、执行状态、返回行数、耗时。这套日志既能用来排查问题,也能分析数仓压力来源。有一次数据库突然负载升高,我就是靠查询日志定位到一个同事在循环测试复杂SQL,及时做掉了。
6. 评估优化与持续迭代经验
6.1 评测集怎么建
没有评测集,你根本不知道自己是改好了还是改坏了。我建评测集的原则是:只收录业务同学真实问过的问题,不自己编。上线第一天我找了三个业务组的同事,每人列20条日常问得最多的问题,凑成基础集,之后每个月从运行日志中挑新增的高频问题补充进去。
每条评测记录包含四个字段:用户问题、业务口径、正确SQL、期望答案要点。其中业务口径特别重要,它决定了SQL写法是否正确。比如“上个月有效订单”,口径写明“剔除退款后支付成功的订单”之后,正确答案才是可判定的。
评估指标我主要关注四类:SQL语法正确率、SQL执行成功率、结果数据正确率、端到端回答准确率。前两项是工程指标,后两项是业务指标。实际使用中我发现,SQL执行成功不代表数据正确,经常出现SQL能跑起来但统计口径不对的情况,所以结果数据正确率必须靠人工核验,不能自动判断。
6.2 常见优化路径:Query改写、缓存、动态示例
第一个优化是Query改写。用户提问经常很口语化,比如“最近卖得好的商品”,这里的“最近”如果不处理,SQL会很难写。我增加了一个改写环节,在召回前先把“最近”改写为具体日期区间,比如“最近30天”,再把改写后的问题同时用于检索和SQL生成。
第二个优化是缓存。企业内部高频问题非常集中,常见指标可能每天被问几十次。我在执行层加了一层结果缓存,对“相同用户问题+相同日期范围”直接返回缓存结果,减少数据库查询次数,响应速度也随之提升。
第三个优化是动态few-shot。不要固定几个示例塞在Prompt里,而是从示例问答集里检索与当前问题最相似的1到3个示例,动态拼进上下文。这个对模型模仿正确SQL写法的提升,比任何Prompt花活都明显。我现在把每个示例都做了向量索引,每次查询先跑一次示例召回。
6.3 踩坑总结与个人经验
第一,知识库质量优先级高于一切。我见过不少人花两周调Prompt,不如把表结构文档按模板重新写一遍效果提升大。第二,不要一上来就上Agent,先把“检索+SQL生成+执行”这条固定链路跑通,再加Agent的循环和自愈。第三,日志一定要全,尤其是“召回命中内容”和“执行返回结果”,这两个信息是排查所有问题的抓手。
还有一个值得记录的经验是,知识库别急着覆盖所有表。我一开始想“一张表都不能少”,结果文档质量参差不齐,把检索效果拉低了。后来改成先服务高频业务域,每个域只覆盖核心表和易错字段,效果稳定了再逐步扩充。
评测集要当成代码一样管理。每次改动知识库、Prompt或者检索策略,都用评测集跑一遍回归,二十条核心用例能过,基本可以安心上线。这个习惯帮我挡住了好几次“看起来优化了实际变差了”的改动。
我个人在实际操作中的体会是,RAG+SQL混合查询Agent本质上不是在做一个“更聪明的SQL生成器”,而是在解决“让机器理解数据库使用规则”的问题。知识库建设、字段口径梳理、评测集维护,这些数据处理上的笨功夫,才是整个项目效果好坏的主线。想先跑通一个Demo,一周就够;想真正让业务同学用起来,建议把一半精力放在知识库打磨上。最后分享一个小技巧:上线前找业务同学要20个他们日常最常问的问题,做成基准评测集,之后每次改动都拿这20题回归一遍,系统效果会很稳。