基于PolarDB的RAG与NL2SQL一体化问答架构实践
近几年做企业级大模型应用一个绕不开的场景就是“既要能聊文档又要能查数据”。业务方不会管你底层用的是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。然后两路结果用RRFReciprocal 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.9s6.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回流到标注集定期重新训练或优化规则形成一个持续的优化闭环。做了这个之后整体准确率大概每两到三周就能提升一个点比一次性上线后就不再管的状态好得多。最后说一个个人体会。这类混合路由架构的项目技术上最难的并不是写几条链路而是如何让系统在“不确定该走哪条路”时做出相对稳妥的判断。路由层多花一点功夫做规则兜底和双链路并行远比在大模型能力上砸更多预算要划算。业务方真正在乎的不是你用了多少先进技术而是系统能不能稳定答对问题。上线之后我们在门口贴了一张纸上面只写了一句话如果回答里出现了数据库里没有的数字请务必点“错误反馈”。这句话听起来很朴素但它是这套系统持续变好的开始。