基于PolarDB的RAG与NL2SQL一体化问答架构实践
2026/9/12 8:54:42 网站建设 项目流程

近几年做企业级大模型应用,一个绕不开的场景就是“既要能聊文档,又要能查数据”。业务方不会管你底层用的是RAG还是NL2SQL,他们只知道:你连我上季度华东区卖了多少钱都答不上来,凭什么说自己是智能助手。这篇内容我沉淀自一个真实落地的项目:在PolarDB上同时承载RAG知识库问答与NL2SQL取数,通过Agent Express这套混合路由架构把两条链路串成一体化服务。全文不聊PPT概念,只讲路由怎么设计、RAG怎么切块召回、NL2SQL怎么防呆防错,以及那些测试集上看不出来的坑。

1. 这两个能力为什么必须放在一起:业务取数场景的真实痛点

先说个我在多个项目里反复遇到的共性矛盾。单做RAG知识库,模型对文档语义的召回能力很强,你问“退货政策里对破损商品怎么处理”,它能从几百页PDF里给你找出来。但你要是问“上个月退货率最高的三个品类是什么”,RAG就抓瞎了——这不是文本相似度能解决的问题,它涉及精确的数值聚合计算。早期我们试过让LLM直接从知识库文档里“算”出这个数,结果模型一本正经地编造了一个百分比,这个幻觉如果被业务拿去做决策,风险非常大。

单做NL2SQL则反过来。它能把“上月各品类退货率”翻译成一段精准的SQL,在数据库里跑出真实结果,但它完全不懂非结构化知识。你问它“我们公司的退货政策是什么”,它只会一脸茫然,因为政策内容根本不在数据库表里。更麻烦的是,两套系统分开建设会导致入口分裂、权限分裂、数据口径分裂,业务方要记两个网址,IT要维护两套账号体系,出了问题还要两头排查。

所以这个项目的出发点很朴素:做一个统一的智能取数问答入口,让用户用自然语言提问,系统自动判断这个问题该走RAG链路还是NL2SQL链路,或者是两条链路的组合。这就是标题里“一体化”三个字的含义。PolarDB在这里承担了底座角色,Agent Express则是我们内部对这套智能体快速落地方案的项目代号——把意图识别、工具调用、结果合成这几段标准化,让路由层可以灵活编排不同的数据源。

一体化之后,业务价值是肉眼可见的。首先是数据实时性:NL2SQL直接查业务库,不用像传统BI一样先做数据同步,今天产生的数据今天就能问。其次是口径统一:SQL是由模型基于统一schema生成的,比人工写报表更不容易出现“各算各的”问题。最后是权限收敛:所有查询都走只读账号,所有敏感字段都在数据库层面或查询结果层面做过滤,比把数据导出到外部再喂给RAG安全得多。

2. PolarDB在路由架构里承担的角色:一个库同时扛起三份活

选型的时候我们对比过两种方案:一是用OpenSearch或Elasticsearch做向量存储,再用MySQL或PostgreSQL做业务数据存储,中间靠应用层串联;二是直接使用PolarDB,把向量检索和业务查询放在同一个数据库里。最终选了后者,原因不复杂——数据链路短。

PolarDB本身是云原生关系型数据库,兼容MySQL和PostgreSQL生态,但它这几年在AI方向上加了几个关键能力,恰好把RAG和NL2SQL这两条链路需要的基础能力都覆盖了。

2.1 三份活:全文检索、向量检索、聚合查询

RAG链路需要什么?需要把文档切块后做embedding,然后存进向量索引,查询时做相似度检索。NL2SQL链路需要什么?需要稳定跑聚合SQL,关联多张表,计算同比环比,而且查询性能要能扛住并发。除此之外,如果要做混合检索,最好还能做全文检索(BM25),因为纯向量检索在一些精确词匹配场景下并不好用。

这三个需求放在传统架构里,意味着至少引入两个中间件。而PolarDB在一个实例里同时提供了全文索引和向量索引,行存表之外的列存索引又能加速OLAP型聚合查询。也就是说,知识库的chunk向量、业务表数据、全文倒排索引都在同一个数据库实例里,应用层只需要连一个数据库,不需要做跨系统的数据同步和一致性补偿。

2.2 向量索引的选型细节

PolarDB的向量索引我们主要用了两类:一类是HNSW,适合对召回质量要求高、数据量在百万级以内的场景;另一类是IVFFlat,索引构建快、内存占用低,但召回精度略低。我们落地时,知识库的chunk数量大约在20万左右,直接用的HNSW,因为它的查询延迟稳定,参数也不难调。

有个小细节值得说一下:向量索引的dtype和distance函数一定要和embedding模型匹配。比如你用bge系列模型输出的是float向量,建索引时的距离函数一般用cosine或inner product。我们早期没注意这个问题,索引建成了L2距离,结果语义检索的结果排序完全不对,排查了半天才发现是距离度量选错了。

2.3 列存索引在取数查询里的加速效果

NL2SQL生成的SQL,很多是带GROUP BY和聚合函数的。这类查询在普通行存表上性能并不好。PolarDB的列存索引允许在同一张表上建立列存索引,优化器会走列存执行计划,配合并行查询(PX)能力,在几千万行的表上做聚合压测时,查询耗时从秒级降到了百毫秒级。对NL2SQL这类“查询模式高度不确定”的场景来说,列存索引比预聚合宽表更省事——你不需要提前猜业务会问什么维度,反正临时跑聚合也跑得动。

3. Agent Express核心机制:意图识别、置信度阈值与链路切换

Agent Express不是一个大而全的Agent框架,它更像是一套“路由容器”。我们这个项目的要求是:不要引入重框架,不要增加额外的服务部署负担,最好能作为独立的一层服务来编排RAG和NL2SQL。所以Agent Express的定位就变成了一个轻量级路由服务,负责接收用户问题,判断意图,调用两套工具,最后把结果整编返回。

3.1 三层路由:意图分类、实体识别、兜底规则

路由层不是简单地把问题分成“RAG还是NL2SQL”两类。我们分了三个梯度:

第一梯度是快速意图分类,用一个轻量级文本分类模型给问题打标,类别包括“知识问答”“数据查询”“闲聊”“歧义”。这个模型部署成本很低,单机CPU就能跑,P95延迟控制在20ms以内。第二梯度是实体识别,从用户问题里抽取业务实体,比如商品名称、时间范围、地区、指标名称,这些实体同时用于RAG的查询改写和NL2SQL的schema过滤。第三梯度是兜底规则,当分类模型置信度不高时,用规则库做二次判断——比如问题里出现“政策”“流程”“制度”等词,强制走RAG;出现“多少”“排名”“趋势”“同比”等词,倾向走NL2SQL。

这套三层设计解决了一个很实际的问题:直接把用户问题丢给大模型做Function Calling来选择工具,虽然语义理解强,但延迟高、成本大,而且在企业私有化部署场景下,不可能每次请求都让大模型做一次工具选择。轻量分类器加上规则兜底,能把99%的请求在毫秒级内判到场。

3.2 置信度阈值与双链路并行策略

路由层判断意图之后,并不是简单地把请求分发到单条链路。我们设计了两个评分维度:知识匹配度和数据匹配度。

当数据匹配度评分为高、知识匹配度评分为低时,只会走NL2SQL链路,比如“华东区上季度销售额环比增长多少”。当知识匹配度高、数据匹配度低时会走RAG链路,比如“退货后多长时间内可以换货”。当两个评分都高时,系统会并行调用RAG和NL2SQL,再把两路结果做融合。这类问题在业务里真的不少见,比较典型的是:“退货率超过10%的商品,根据我们的退货政策应该怎么处理”,既查了数据,又查了政策文档。

并行调用的链路并不是简单的文本拼接,而是先让NL2SQL链路返回表格结果,再让RAG链路检索相关制度条款,最后由生成模块把“数据事实”和“政策事实”组织成一段话,并明确标注信息来源,用户能看到哪些是数据库算出来的,哪些是文档里查到的。

3.3 路由层的返回格式统一

路由层对外暴露的接口格式我们统一成了一套结构:回答文本、结果表格、引用来源、链路标记(rag/nl2sql/hybrid)、执行耗时。这样不管内部走的是哪条链路,前端展示层只需要解析这一套结构。

这里有个经验:不要把路由层的返回格式设计成纯JSON字符串,最好直接用支持结构化输出的SDK来约束,否则大模型输出的JSON偶尔会有字段缺失或格式错误。我们早期踩过这个坑,后来对所有生成式环节都加了schema约束,再配合重试机制,格式错误率降到了千分之一以下。

4. RAG链路的工程落地:切块、多路召回与排序融合

如果你觉得RAG就是“把PDF丢进向量库,然后用相似度检索”,那后面这些内容建议认真看一下。这个项目里RAG链路踩过的坑,比NL2SQL多得多。

4.1 文档切块不能一刀切

我们知识库里大概有几百份公司制度文档,包括PDF、Word、Markdown。最初图省事,直接按照固定token长度切块(512),结果召回质量非常差。分析后发现,制度文档里有大量表格、条款编号,固定切块经常把一条完整条款拦腰截断,语义残缺的chunk喂给向量检索,召回结果自然不理想。

后来改用结构感知切块:先把PDF/Word解析成结构化文本,识别标题层级和段落边界,优先按标题和段落切,超过上限的段落再递归切。对于带表格的文档,单独把表格抽出成Markdown格式的文本块,让embedding模型能识别表格结构。这个改造做完,知识问答的召回命中率明显提升,尤其在涉及政策条款编号的问题上,效果提升特别明显。

切块时还有一个细节:一个chunk里如果同时包含多个主题,会造成语义混淆。所以我们在切块前会做一次句子级别的主题一致性检测,如果相邻段落主题差异大,就把它们切成两个独立的chunk。

4.2 从单路向量检索升级到“向量+全文”多路召回

纯向量检索的问题在于,模型对同义改写很敏感,但对精确专有名词不敏感。比如文档里写的是“SKU编码”,用户问的是“货号”,向量检索往往召回不全。为了补上这个短板,我们做了多路召回:一路是向量召回,一路是PolarDB的全文检索(BM25)。

向量召回用的是bge-large-zh-v1.5模型,把文本输出成1024维向量,在PolarDB的HNSW索引上做top20召回。全文召回用的是数据库内置的全文索引,在切块后的原始文本上做BM25检索,同样取top20。然后两路结果用RRF(Reciprocal Rank Fusion)做融合排序。

RRF的公式很简单:每个文档的融合得分等于两路排序位置的倒数之和,即score = Σ 1/(k + rank),k是一个平滑参数,一般取60。这么做的好处是不需要调权重,两路召回天然互相补充。

4.3 查询改写与重排

多路召回解决了“找得到”的问题,但“找得准”还需要额外处理。我们在RAG链路里加了一个查询改写模块:先把用户问题里的口语化表达改写为更接近文档语料的表述。比如“退钱多久到账”改写为“退款到账时间”,这样一来,向量召回和全文召回都能命中更多有效文档。

召回之后还有一个重排步骤,用的是cross-encoder模型,把所有候选chunk和用户问题拼接后做精细打分,取top5喂给大模型生成回答。这一步是RAG链路里最耗时的环节,但因为候选集从40个压缩到了5个,整体延迟还是可接受的。

4.4 知识库问答的生成策略

最终的生成环节,我们不只是把召回文本拼接起来扔给大模型。系统会把“用户问题+候选文档+引用要求”组织成一个结构化提示词,要求模型只能基于检索内容回答,不能自由发挥,每条回答后面必须附上引用来源。这样可以有效降低幻觉率,用户也能直接点开原文核对答案。

生成模型的temperature我们设得较低(0.2左右),因为知识问答场景更看重准确性和一致性,不需要太多创造性。这里补充一句,不要盲目迷信大模型的能力,RAG的上限取决于召回质量。召回里没有的信息,生成端无论如何也编不出来。

5. NL2SQL链路的关键设计:Schema增强与安全护栏

NL2SQL这条链路的难点不是让模型写出“看起来对”的SQL,而是让它在复杂业务环境下稳定生成“能跑对”的SQL。我们在这条链路上做了四件事:Schema增强、业务词根、示例注入、安全护栏。

5.1 动态Schema注入

最开始我们把整个数据库所有表结构一次性塞进Prompt,结果既浪费token,又让模型产生混淆——表太多,模型经常选错表或编造不存在的字段。后来改成动态Schema注入:先从用户问题里抽取可能涉及的表名,只把相关表的建表语句、字段注释、枚举值类型注入Prompt。

比如用户问“上个月各渠道的订单量”,系统会通过关键词匹配和向量匹配定位到订单表、渠道维度表,然后只注入这两张表的schema。这样做的好处非常明显:SQL生成的表名错误率大幅下降,Prompt的token消耗也减少了一半以上。

字段注释特别重要。我们要求建表时每个字段都要写业务注释,这些注释会原样出现在Schema注入内容里。比如字段is_refund的注释是“是否退款:1是,0否”,模型看到这个注释就能正确生成WHERE is_refund = 1,而不会写反。

5.2 业务词根词典

企业里充斥着“客单”“毛利”“复购”这类业务黑话,直接扔给模型,它很难映射到具体字段。我们维护了一个业务词根词典,包含词根、对应字段、计算口径、示例SQL。用户问题经过了实体识别之后,会先做一次词根替换,比如“客单”替换成“客单价(订单金额/订单数)”,模型再基于替换后的文本生成SQL,准确率明显提升。

这里有一个值得注意的口径问题:业务词的统计口径经常有歧义。比如“毛利”是“收入-成本”还是“收入-成本-分摊费用”?不同业务线定义不同。词典里除了映射关系,还要带上口径描述,并且在Prompt里提示模型:“如果用户问题没有明确说明口径,使用默认口径,并在回答里显著标注。”

5.3 SQL生成、执行校验与结果解释

NL2SQL的生成我们用的是大模型配合few-shot的方式。每个核心表都预置了2-3个典型问答对作为示例,模型会模仿示例的写法生成SQL。生成之后,不能直接拿去执行,要经过一道规则校验:检查SQL是否包含UPDATE、DELETE、DROP等危险操作(理论上只读账号已经挡掉了,但双保险还是要的);检查是否包含LIMIT,没有的默认加上;检查表名和字段名是否都存在于schema列表中。

执行阶段我们用了数据库的查询超时控制,单条查询超过10秒直接终止,避免慢SQL拖垮数据库。对于查询结果,还会在返回前做一次字段级脱敏,比如手机号、身份证号直接置空。这个环节属于安全护栏的一部分,千万别省。

最后一步是结果解释。SQL执行出来的是一张二维表,大部分业务用户看不懂。系统会基于结果表和原始问题生成一段自然语言摘要,比如“3月华东区销售额为1200万,环比增长8.1%,其中上海贡献最大,占比35%。”生成摘要时,大模型只能基于查询结果来表述,不允许自己“推算”新数字。

6. 实测效果与踩坑复盘:从80分到能上线的过程

任何架构最终都要用效果说话。我们构建了一个100条的评测集,覆盖知识问答、数据查询、混合问答三类问题。第一轮跑下来,路由准确率大概在84%,数据查询的回答准确率只有76%,距离可上线还有不小差距。经过三轮迭代,最终路由准确率为97%,数据查询准确率到了91%,知识问答命中率93%。下面是迭代过程中的关键监控指标。

指标第一轮第二轮第三轮
路由准确率84%92%97%
NL2SQL执行成功率78%88%94%
数据查询答案准确率76%85%91%
RAG召回命中率79%88%93%
P95端到端延迟4.8s3.6s2.9s

6.1 路由误判是最大的一类问题

第一轮评测里,路由错误占了总错误的一半以上。典型场景是用户问“库存周转天数这个指标是怎么定义的”,这个问题的本质是知识问答,但分类模型看到“指标”和“定义”这两个词,误判成了数据查询,结果NL2SQL链路肯定答不出来。

修复方法是在路由层加了一组否定规则:“如果问题里出现‘定义’‘怎么算的’‘包含哪些’等元问题描述,优先走RAG链路。”这类规则不复杂,但见效极快。另一个高频误判出现在混合问题上,比如“退货政策是什么,以及上月退货率前五的商品”,单靠分类模型会输出歧义结果,我们让路由层在这种场景下直接触发双链路并行,效果比强制单链路好得多。

6.2 NL2SQL最常见的三个坑

第一个坑是表名幻觉。模型在生成的SQL里经常引用不存在的表名或字段名,尤其是在数据库表特别多的情况下。动态Schema注入解决了大部分问题,但偶尔还会出现字段名写错的情况。我们在这里加了一个SQL语法校验,用数据库自带的方式先做EXPLAIN,如果解析失败就直接返回重新生成,不再执行真正查询。

第二个坑是聚合逻辑错误。比如计算“环比增长”,模型经常生成错误的子查询或窗口函数。解决办法是给few-shot示例里补充了这类计算场景的标准SQL写法,让模型有模板可循。像“时间同比”“时间环比”“占比”这种固定模式,示例注入比提示词描述更靠谱。

第三个坑是“查不到数据”时模型会编造结果。我们在Prompt里明确要求:“如果查询结果为空,必须在回答中明说‘未查询到符合条件的数据’,不能虚构。”同时,系统也会把结果字数等元信息传给生成端,进一步减少编造空间。

6.3 RAG链路的性能与质量平衡

RAG链路的延迟大头不在向量检索,而在重排环节。cross-encoder对40个候选chunk逐一打分,单次调用就要几百毫秒。我们把重排模型替换成了蒸馏后的小模型,延迟降低差不多一半,但精度只下降了不到1个百分点。这个替换是在大量评测集上验证过之后才上线的,如果凭感觉直接换,很容易被线上偶发问题打脸。

关于切块还有一点要补充:不同文档类型要设置不同的切块策略。规范制度类文档适合按条款切,做FAQ类文档适合按问答对切,产品手册类适合按主题层级切。我们在系统里预留了jstrategy字段,让不同文档可以选择不同切块策略,效果比全局一套配置好很多。

6.4 可观测性与持续迭代

这套系统上线后,我们把每个请求的路由置信度、链路选择、召回文档ID、SQL执行信息全部记录到日志表。每周做一次抽样分析,重点关注三个问题:有没有路由误判进日志但没有被用户投诉的;有没有SQL执行成功但答案明显不对的;有没有RAG召回了错误文档但生成端没发现的。

这套机制带来的价值是,算法模型的迭代不再靠拍脑袋,而是基于真实线上的bad case。我们把bad case回流到标注集,定期重新训练或优化规则,形成一个持续的优化闭环。做了这个之后,整体准确率大概每两到三周就能提升一个点,比一次性上线后就不再管的状态好得多。

最后说一个个人体会。这类混合路由架构的项目,技术上最难的并不是写几条链路,而是如何让系统在“不确定该走哪条路”时做出相对稳妥的判断。路由层多花一点功夫做规则兜底和双链路并行,远比在大模型能力上砸更多预算要划算。业务方真正在乎的不是你用了多少先进技术,而是系统能不能稳定答对问题。上线之后,我们在门口贴了一张纸,上面只写了一句话:如果回答里出现了数据库里没有的数字,请务必点“错误反馈”。这句话听起来很朴素,但它是这套系统持续变好的开始。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询