1. 项目概述与方案选型
1.1 核心需求解析
我最初接触这个方向,是因为手上有一个带数十张业务数据表、几千份内部文档的模拟项目X。团队同事在写月度数据报告时,经常同时翻后台报表和知识库,来回切换。我们当时的诉求很朴素:能不能让用户用一句大白话提问,系统自动判断答案藏在数据库里还是藏在文档里,甚至两边结合起来回答?
这就是“RAG+SQL混合查询Agent”的典型场景。RAG(检索增强生成)负责把非结构化文档变成可检索的知识,SQL Agent则负责把自然语言翻译成结构化查询。两者看似目标不同,但组合起来能覆盖大多数企业信息查询需求。比如“上个月华东区退货率有多少”这种问法,答案肯定在订单表里;“退货流程应该联系哪个部门”这种问法,答案大概率在制度文档里。单一方案解决不了全部,混合查询才是正路。
1.2 为什么不用纯RAG或纯SQL
先把两个方案的边界说清楚。
纯RAG方案,是把文档切片、向量化,拿用户问题去向量库检索TopK片段,再拼给大模型生成回答。它的优点是灵活,适合开放性问题;缺点是只能回答“文档里明写的东西”,一旦用户问到具体数字、统计口径、跨表聚合,向量检索根本给不出精确结果。你检索到的可能是一段“本月退货率较上月上升”,但用户要的却是“具体上升了几个点、哪个区域最严重”。
纯SQL Agent方案恰好相反。它利用大模型把自然语言转换成SQL,然后连数据库执行查询,答案天然精确、可验证。但它的瓶颈也很明显:模型不理解业务上下文。用户问“重点客户流失情况怎么样”,模型不知道“重点客户”在表里对应哪个字段,是status='VIP'还是is_key=1;用户问“上月订单退单占比”,模型可能把refunded_at为空的数据当退单算进去,因为在文档里“退单”有另一套定义。很多调参经验都花在这种“业务口径靠猜”的问题上,短期内很难根治。
混合方案的处理逻辑其实只有一句话:能查表就查表,查不到表就翻文档,两边各拿一半就拼起来回答。它不追求让模型理解一切,而是先做意图路由,再针对不同意图调用不同工具,最后在生成阶段做结果融合。复杂度确实上升了,但每类问题的准确率和可解释性也上去了。
2. 混合查询Agent的架构设计与运行流程
2.1 三层架构拆解
我把整套系统拆成三层:接入层、路由层、执行层。
接入层负责接收用户问题,同时做两件事:一是对问题做基础清洗,比如去掉语气词、补全口语化缩写;二是做领域词归一化,把“上月”“最近一周”这类相对时间表达换算成datetime范围,方便后续SQL生成。这里注意不要过度清洗,比如把“退货流程是啥”改成“退货流程是什么”没问题,但不要试图自动补全主语,那一步反而容易引入噪声。
路由层是整个系统的核心,负责判断“这个问题该走SQL、RAG还是混合路径”。路由的准确率直接决定整体效果。我建议用“关键词规则 + LLM结构化分类”双保险:先用正则和词表做一次快速匹配,比如命中“多少、几个、率、排行、汇总”这些词就往SQL方向倾斜;匹配不到再用大模型做三分类(SQL、RAG、混合),并要求模型输出JSON格式。关键点是双保险做不到100%,要在路由层后加一个“校验闭环”——例如SQL方向需要先做“表可用性检查”,RAG方向需要先做“片段相似度阈值检查”,过不了就自动降级到另一条路。
执行层里放了两个工具:数据库查询器和向量检索器。数据库查询器内部走“自然语言 -> SQL生成 -> DB执行 -> 结果集摘要”四步;向量检索器走“问题Embedding -> TopK召回 -> 重排序 -> 文档片段返回”。两条路的结果在最后会合,进入一个结果融合模块。
2.2 一次查询的完整生命周期
拿“上个月华东区的退款总数是多少,退款政策有没有特殊说明”这个混合问题举例,整个过程分六步。
第一步,问题进入路由层,因为同时包含“退款总数”(SQL关键字)和“政策说明”(RAG关键字),被判定为混合查询。第二步,SQL子链路把“上个月华东区退款总数”解析成一张退货单表的聚合查询,执行后得到数值结果“312单”。第三步,向量检索器用整句问题去查文档库,把命中片段里关于“华东区大客户小额退款无需书面说明”的段落送回。第四步,系统不急着生成回答,而是把数值结果和文档片段同时放进“上下文窗口”。第五步,生成模型综合两边材料组织回复,形成“华东区上月共312笔退款,按文档说明,单笔低于200元的订单走简化流程即可”。第六步,回答落地时附上两条引用标记,一条标记来源表名和SQL条件,一条标记来源文档ID。
这套流程里最容易翻车的,其实是第四步。如果把SQL结果和文档片段直接拼接给模型,模型很可能把“312笔”和“简化流程”硬凑成因果关系。我的处理办法是,在上下文里加一条显式分隔符和提示语,比如“下面两部分内容之间没有必然关系,请分别引用”,同时限定模型:如果两边信息矛盾,必须在回答中明确指出“数字来自数据库,政策说明来自文档,二者请分别核实”。
2.3 矩阵响应策略:多数问题其实不算混合
很多人看到“混合查询”四个字,误以为所有问题都要同时走两条链路,这是最容易犯的工程错误。我实际跑下来,大概只有一两成的问题真正需要两边串联,绝大多数问题要么纯SQL、要么纯RAG就够了。
为了管理这种差异,我在路由层加了一个“判定矩阵”,按“是否包含数值统计意图”和“是否涉及文档知识”两个维度分类。落在这个矩阵里的行为如下表所示:
| 问题特征 | 路由方向 | 处理策略 |
|---|---|---|
| 有数值统计,无文档知识需求 | 纯SQL | 直接查询,不召文档 |
| 无数值统计,有文档知识需求 | 纯RAG | 直接检索,不生成SQL |
| 有数值统计,有文档知识与统计口径 | 混合 | 先SQL后RAG,两边对照 |
| 两边都弱(闲聊) | 兜底 | 走通用对话,接FAQ库 |
这个矩阵的价值,不只是让路由更准确,更重要的是让链路资源的消耗可预期。纯SQL请求不用跑向量化,纯RAG请求不用连数据库,整体耗时和费用都降下来一截。做这套的时候我有个体会:不要为了让方案听上去高级,就把每个问题都当混合问题处理,系统的高可用和低成本是靠“大部分请求走单路”撑起来的。
3. 核心模块实现全解析
3.1 SQL生成子Agent的实现要点
我建议先实现SQL子Agent,因为它的边界更清楚,也更容易出成果。
首先是表结构上下文的准备。直接把所有建表语句塞给大模型不现实,上下文太长,生成质量也不行。正确做法是给每个表准备好“字段注释模板”,按可能被问到的频率选前N张表作为候选。比如订单表的核心字段注释要写成order_status(订单状态:0待支付,1已支付,2已发货,9已关闭),不要把枚举值写在建表DDL里,而是单独维护一份“字段口径字典”,在构造Prompt时动态拼接。我实践的方案是给每张表建一个“最近提问热度”字段,每命中一次就刷新,定期清理三个月未命中的表,让候选表集合保持在10张以内,SQL生成质量肉眼可见地提升。
SQL生成Prompt里我始终保留三段结构:系统指令段、表结构段、少量示例段。系统指令段强调“只能输出合法SQL,不允许解释”;表结构段放字段注释和枚举值;少量示例段放3到5条“自然语言到SQL”对的例子。这段Prompt我反复调过,最后稳定下来的版本包含一个关键约束:明确指出“如果没有找到符合查询条件的字段,请输出NO_MATCHING_COLUMN,不要猜测字段名”。这个约束直接砍掉了大约三成的SQL幻觉——模型遇到不认识的字段名时会编一个列名,加了这句之后它会主动放弃。
执行SQL之后还有一个关键步骤:行数上限控制与危险操作拦截。先给数据库账号设置最小权限,比如禁了DELETE、UPDATE、DROP,只允许SELECT,然后在连接层再加一个“是否包含关键词”校验,包含DROP、TRUNCATE、ALTER就中止执行。同时给每一条生成的SQL加上LIMIT 200,防止聚合查询返回全表导致网络和内存打爆。实测下来,这类问题最常见的不是SQL写错,而是模型给GROUP BY后的结果列没加聚合函数,比如SELECT customer_name, COUNT(*) FROM orders GROUP BY customer_name没问题,但模型可能写SELECT customer_name, order_amount FROM orders GROUP BY customer_name,此时数据库会直接报错,这条需要后端捕获异常后让模型“基于错误信息重写一次”,不要直接返回报错给用户。
3.2 RAG检索子链路的切片与召回策略
RAG部分的核心挑战不在大模型,而在切片和召回。
切片我建议按“段落标题优先 + 滑动窗口兜底”的组合思路。先按文档结构切出自然段落,如果某个段落太长(超过阈值例如800字),再在二级标题处拆开;如果拆开后仍长,就用固定窗口长度400字、重叠80字的方案强制切分。这里有个容易忽略的坑:重叠窗口的引入不是为了“多召一点”,而是为了保住跨切片的连续句子。比如“本政策适用于所有华东区客户,但不适用于以下特殊情况”这句话,如果恰好落在两个切片的接缝处,没有重叠窗口,这句话就丢了上下文的一半。我在实测中发现,加80字重叠后,涉及制度文档的问答正确率提升了约10个百分点。
召回阶段,我最初直接按向量余弦相似度取TopK=4,效果不稳定。后来改成“向量召回50条 + 重排序模型取Top3”的两段式召回。重排序的意义在于摆脱单一Embedding模型的表示限制——向量召回保证“语义相关”,重排序保证“和问题字面/意图更接近”,两者结合才能把真正有用的一段顶到最前面。跑下来我的直观感受是:Top3的准确率比直接取Top5的高很多,因为后两条通常有一条是噪声。宁可少拿不可错拿。
这里再顺带说一个Embedding模型的选型问题。如果预算允许,首选付费商用接口的文本向量模型,省去部署麻烦;如果只能本地部署,选轻量级开源模型,但要注意不同模型对长文本的编码上限不同,切片长度调成模型上限的一半以下效果更稳。不要一上来就追求“最强模型”,先按“参数规模与自己机器显存匹配”的基准选,把长度和batch调好,后面再换大模型,迁移成本很低。
3.3 路由判定模块的工程化实现
我在项目里没有选择复杂的意图分类框架,而是用一套轻量可维护的“关键词先行 + LLM补充 + 规则兜底”方案。
第一步,关键词先行。我维护一张统计词表,命中即强制走SQL,比如:多少、几个、环比、同比、占比、排行、总量、平均、分位数、明细。再维护一张文档词表,命中即强制走RAG,比如:如何、办法、规定、流程、说明、版本、规定里、文档中。注意这里有个坑:像“流程”这个词,用户说的可能是“流程审批通过率”,那其实是SQL任务。解决办法是对关键词加前后词约束,例如“流程”只有在紧跟“什么”“怎么”“为啥”时才偏向RAG。
第二步,LLM补充。当关键词匹配拿不准时,我把问题、时间和可用表列表一起传给大模型,让它输出结构化JSON:{"intent": "sql" / "rag" / "hybrid" / "chat", "sql_tables": [...] }。为了控制成本,只有“拿不准”的问题才走到这一步,大约只占全部请求的两三成。
第三步,规则兜底。如果LLM输出格式异常或者分类置信度过低,直接降级为“RAG”,因为RAG对未知问题的回答比SQL更稳妥,至少不会跑出报错。这个设计思路我觉得值得坚持:兜底路径给用户的是“可能不够精确但不会报错”的体验,而报错体验是断送系统口碑最快的因素。
路由模块的核心价值不是把一个简单问题机械化地分流,而是通过轻量规则把绝大多数请求按低成本路径走掉,让高成本的大模型判定只发生在真正需要智能的场景。
4. 混合查询的融合策略与实现细节
4.1 结果融合的两种模式
当路由判定为“混合查询”时,SQL子链路和RAG子链路的结果要在展示给大模型前做一次融合。直接“两段文本拼接”是最省事但也是效果最差的方案,我实际踩坑后换了两种模式。
第一种是抗噪模式:把SQL结构化结果转换成“一句话摘要”,比如[SQL结果] 华东区上月退款总数312,涉及金额约45000元,然后让RAG召回的文档片段也做压缩,只保留“特殊政策”相关的部分,再一起喂给模型。这种模式适合“数字为主、规则为补充”的问题。
第二种是对比模式:不把两边结果合并给模型一次性回答,而是先让模型分别解释“数字说明了什么”和“文档条款说明什么”,最后再做一次归纳。比如SQL结果说“华东区退款312笔”,文档说“华东区大客户小额退款有简化流程”,那么模型就能输出“312笔中包含部分中小额退款,可走简化流程”。对比模式比抗噪模式晚返回2到3秒,但回答质量明显更高,适合用户明确追问细节的场景。
4.2 冲突信息怎么处理
SQL结果和文档信息冲突是混合查询的常态。典型情况是:订单表里refund_status=3被认为是“退款完成”,而制度文档里“退款完成”还要求财务复核签字后才能算。两者统计口径不一致,直接生成会让用户对数据可信度产生怀疑。
我采用的策略是“并列呈现,不做事实判定”。在生成回答的Prompt中加一条指示:“如果你认为数据库结果和文档内容存在口径差异,请以‘数据库统计结果’和‘文档定义’并列列出,不作真伪判断,并提示用户按实际业务口径取数。”这比让模型强行调和两种信息要安全得多——因为模型没有能力判断哪个口径正确,强行调和等同于替业务做了决定,这是越权行为。
4.3 动态上下文蒸馏
混合查询最棘手的工程问题是上下文长度。SQL子查询可能返回200行结果集,RAG可能召回3个1000字的片段,如果全部塞给生成模型,提示词窗口瞬间爆炸,而大部分内容其实是冗余的。
我后来加了一个“动态蒸馏”步骤:SQL结果先交给一个轻量级总结提示词,提取出“最值得回答用户问题的三句话”;RAG片段则先用重排序模型挑出和问题最贴近的段落,再做一次抽取式摘要,只保留与问题明确相关的句子。蒸馏之后,混合上下文的体积能压缩到原来的五分之一左右,模型生成的稳定性和速度都显著提升。这部分的Prompt要求别太自由,建议格式化为“数字事实列表”和“条款列表”,这样融合时模型更容易对齐。
5. 检索与查询的工程化落地细节
5.1 数据库连接与执行层的异常处理
实际开发里,SQL Agent最容易在数据库连接这个环节“暴雷”。如果用短连接直查,高并发下连接池频繁创建销毁,数据库压力倍增;如果复用固定连接,又可能因为SQL执行期间连接被占用,其他请求排队超时。我最后用的是连接池方案——初始化一个最小5最大20的连接池,SQL执行时限10秒,超时直接返回“查询超时,请简化条件”,不要把底层异常堆栈暴露给用户。
另外强烈建议把数据库账号权限调成一列清单模式:只读账号、不授予写权限、禁止访问敏感字段。这里的敏感字段过滤,我在查询改写阶段也要做一道:如果问题里的词命中了敏感字段名,直接禁止该查询生成,返回“该字段涉及敏感信息,暂不提供查询”。别期待大模型能自主判断数据权限,这是工程保障的问题。
5.2 向量库选型与运维细节
向量库选型不复杂。数据量小(十万级以内)用轻量本地向量库就够了,无需额外部署服务;数据量大或者团队稳定,再上服务化向量数据库。
但有几个运维细节容易被忽略。首先要固定文本切片的ID生成规则,建议使用文档ID加标题路径加序号三者的哈希串,这样无论是切分脚本重跑还是版本更新,同样的文档切片ID保持稳定,方便做增量更新而不是全量重建。其次要定期核对向量库中的文档与源文件页面的同步关系,文档更新后旧切片未清除是高频事故。我实践中加入了一个“版本号字段”,每次更新文档时递增版本号,向量化时只处理版本号大于旧值的页面。
5.3 查询耗时的分环节埋点
这个点很重要,也常被忽视。混合查询链路比单路查询长,用户等待时长每增加1秒,可用性感知就下降一分。所以我在路由、SQL生成、SQL执行、向量召回、重排序、LLM生成六个环节都埋了耗时计数。
跑了一段时间后,我发现最大的耗时瓶颈通常不在大模型,而在SQL执行和向量召回。SQL执行慢,常见原因是模型生成的SQL索引没命中,比如在DATE(created_at)上做条件过滤导致索引失效。我在表结构Prompt里明确提示“不要对索引列使用函数包裹,直接使用范围查询”,这个问题就改善很多。向量召回慢,常见原因是过滤条件执行得太晚——比如“查华东区退货制度文档”,如果先全局召回500条再做地区过滤,效果差效率低;正确做法是提前在元数据里打上地区标签,让向量库直接按地过滤再召回。
每个环节的耗时数据我都落表,然后用一个简单的规则做告警:任一环节P95耗时超过基线1.5倍就提醒复查。这套埋点体系帮我在系统上线初期的头两周定位了SQL生成异常慢的问题,很值。
6. 常见问题与排查技巧实战
6.1 SQL生成错误与字段名幻觉
这是Text2SQL的经典问题。现象是模型生成SQL时用了一个不存在的列名,或者搞错表关联关系,导致查询报错。我排查后总结了几种处理方式:
- 在表结构信息里加入“该表最常用查询字段示例”,不是让模型背,而是减少它对字段的臆测。
- 对每条生成的SQL执行前做一次正则校验,检查
SELECT后面的列名是否都有对应表的字段字典记录。 - 在SQL执行失败时,不要把报错直接返回给用户,而是把报错信息喂回给模型,让它用“修复模式”重写一次SQL。注意要给模型限定回复格式,要求只说修正后的SQL,不要解释。
- 如果同一问题连续两次重写仍失败,直接返回“当前暂不支持该维度查询”的提示,避免陷入无休止的修复循环。
我实测下来,加上“字段字典比对”这一步后,SQL生成错误率下降了约四成。
6.2 RAG召回噪声与相关性阈值
RAG链路最常见的问题是“召回了但内容不相关”。我没用固定阈值,而是动态算了一个“相对相关度基准”:对每个问题召回十条,计算这十条的得分均值和标准差,只要Top1的得分大于均值加0.5倍标准差就算通过,否则视为无可靠召回。这个策略比固定0.7、0.8之类的阈值实用得多,因为不同领域文档的向量得分分布差异巨大,固定值在一个语料上灵,在另一个语料上就崩。
6.3 混合查询时SQL和RAG结果矛盾
这部分前面提过处理策略,这里的排查顺序是:先回放路由日志,确认是否误判为混合;再检查文档版本是否更新到最新(历史文档),这一项是最常见的“伪矛盾”;如果文档没问题,再去核对SQL的统计口径,重点看条件过滤有没有把应当排除的订单也算进来。矛盾信息排查过程中,我发现一个很有用的做法:在系统后台把“SQL执行条件”和“文档召回片段”并列展示给管理员,供人工复核,相当于给系统加了一个“可复盘面板”。短期看是增加了一点开发量,但长期看收益很大。
6.4 成本控制与性能调优
混合查询跑在真实环境里,最现实的问题是成本。大模型调用每多一次,账单就多一笔;如果所有问题都无脑走混合链路,成本会是纯SQL方案的好几倍。我的对策是三层:
- 请求级别:先走关键词快速判定,能单路解决的单路解决,混合判定只在“确实双方都有指向”时触发。
- 缓存级别:相同语义的向量召回结果做缓存,比如问题脱敏后哈希命中,直接复用召回结果,而不是重新Embedding。SQL结果也可以缓存,但要注意带上时间条件,避免“上月”“本月”这类滚动时间窗口的缓存污染。
- 模型级别:路由用轻量模型,摘要用中等能力模型,只有最终生成回答用最强模型。混合查询对模型能力要求不低,但没必要让每一环节都用最强最贵的模型。
6.5 全链路日志与观测
踩了这么多坑之后,我最大的反思是:这套系统的可维护性,取决于日志有没有把“决策依据”留下来。我在每个关键节点都输出结构化日志:
- 路由结果:
问题ID、路由方向、命中关键词、置信度 - SQL生成:
输入Prompt摘要、输出SQL、错误信息 - SQL执行:
执行耗时、返回行数、截断标志 - 召回结果:
召回片段ID、得分、重排序得分 - 最终答案:
引用上下文来源、生成耗时
有了这些日志,几乎每一个线上问题都能在几秒内复现定位。没有日志的AI应用,排查问题就像摸黑走迷宫,所以我把日志建设当作功能需求写在排期里,而不是事后补丁。
7. 进阶体验与实践体会
7.1 从“能用”到“好用”的三个迭代方向
第一版系统把链路跑通之后,说实话只是“能用”,离“好用”还差很远。我后续做了三个方向的迭代。
第一个方向是追问能力。用户在收到RAG回答后,很自然地会追问“那如果是新客户呢?”——这个问题如果没有上下文继承,系统会当作全新问题处理,导致回答偏差。我给路由层加了一个“是否携带指代”的检测:如果问题里出现“那、这个、它、他”这类指示词,就先做一次对话历史摘要拼接,再重新路由。
第二个方向是会话记忆。混合查询的上下文天然包含“数据条件”与“文档引用”两类信息,如果用户在第一轮已经在问题里限定了“华东区”,第二轮再问“那华南呢”,系统应自动把地区条件替换掉,而不是叠加。我通过抽取用户问题里的“业务实体条件名”并维护一个会话级条件字典实现这个逻辑。
第三个方向是主动澄清。当路由判定置信度低时,系统不急着回答,而是反问用户“你问的‘退款完成率’是指订单退款完成比例,还是金额完成比例?”这个能力上线后,看似多了一次交互,但最终回答的准确率提升明显。主动澄清的实现不复杂,关键是给模型加一个“允许反问”的开关,让它觉得“拿不准时提问”是可接受的。
7.2 内容安全与合规底线的工程保障
混合查询系统最大的隐患在于超越了“检索信息”的边界。我在权限与合规层面做了两道硬约束。
第一道是数据权限隔离。无论SQL还是RAG,都必须经过“角色-数据范围”校验。比如普通用户查询时,SQL生成Prompt中自动拼接WHERE department = '当前用户部门'这类条件;RAG召回时,片段元数据里如果标明了permission=internal,非内部角色直接过滤掉。这个校验不能依赖大模型自觉,要下沉到数据访问层强制约束。
第二道是禁止生成“决策性建议”。用户问“这个客户应该怎么处理”,如果系统从文档中检索到“停止合作”条款,模型可以直接引用条款,但不能基于当前查询结果独立输出“建议停止合作”的结论。我在系统提示词中明确加了“禁止提供决策性建议,仅陈述数据事实与文档内容”。这条边界能让系统既提供有用信息,又不替人做决定,在很多场景下是关键的安全防线。
7.3 我的实操心得
最后分享几点我在整段开发经历中的真实感受。
第一,这套系统真正的工程难点,不在算法而在“衔接”。意图分类模型再准,关键词规则一乱,照样全军覆没;向量召回再牛,SQL字段映射错了,结果照样没法看。所有模块单独评估都很好,但集成起来效果差,大概率是每个环节之间的接口没设计好。
第二,评测集必须包含“边界问题”。只拿常规问题做评测,系统永远发现不了自己的弱点。我在内部建了一个“边界题库”,专门放一些模棱两可的问题,比如“上个月的订单,有什么特别需要关注的?”——这种问题SQL能出数字,RAG能出政策,但用户想要的到底是什么,连人都要猜。这类问题评测的正是路由模块在不确定场景下的兜底能力。
第三,混合查询不是终局方案。它更多是当前模型能力下的过渡形态。等模型更强、工具调用更稳、数据库语义更丰富之后,也许一套端到端方案就能解决大部分问题。但从当下的实战效果看,RAG加SQL混合Agent确实是把自然语言查询落地到企业数据环境的最务实路径——因为它在单点能力有限的前提下,把各环节的长处拼成了能用、能查、能复现的完整系统。
这几个月做下来,我最大的感受是:这类系统永远不会有一劳永逸的完美版本,它需要有耐心地调路由、调切片、调口径、调缓存。但每次看到用户用一句大白话,就从几十张表里拿到想要的数字,再看到旁边引用的文档依据,我还是觉得这套工程的价值足够明显。