技术上,大模型确实让智能问数(自然语言转SQL、对话式数据分析)的门槛降了一大截。但做出来一个Demo和真正把它放进生产环境,完全是两回事。我之前接手过几个问数类项目,最深的感受是:在真实业务里,用户问得五花八门,表结构动辄几百上千张,光靠大模型裸跑一次生成SQL,效果惨不忍睹。后来我们把整个链路改成“多策略召回 + 精排”的架构,才算是真正把准确率和可用性拉到了能落地的水平。这篇就聊聊这套架构的设计思路、核心细节,以及我在实操中踩过的坑和排查实录。
1. 为什么“问数”必须走召回和精排这条路
1.1 先说说裸奔式问数的三个痛点
很多人第一次接智能问数时,最容易犯的错就是把问题直接丢给大模型,让它写SQL。在演示环境里,三张表、五个字段,模型表现得确实像那么回事。但一旦进入真实的业务环境,问题立刻暴露:
第一个痛点是表太多了。一个中型企业的数据仓库,光事实表加维度表动辄几百张,即使只开放一部分,把表结构、字段注释、枚举值全塞进Prompt,模型根本“看不过来”。大模型的上下文窗口是有上限的,就算硬塞进去,注意力也会被无关信息稀释,生成的SQL质量肉眼可见地下降。
第二个痛点是语义太模糊。业务方问“最近卖得好的产品有哪些”,这里“卖得好”到底是指销售额、销量、毛利还是增长率?如果没搞清楚意图,SQL写出来就可能是错的。而大模型擅长的只是“生成”,它不擅长“决策”——在众多可能的候选表、候选字段里做选择,这本质上是搜索和排序问题,而不是生成问题。
第三个痛点是稳定性太差。同一个问题,换个问法,或者今天问和明天问,生成的SQL可能就变了。用户能接受大模型“偶尔抽风”,但数据分析师没法接受每天跑出来的报表口径不一致。这种不一致的根源,就是整个流程缺少一个可控的、可预期的中间层。
1.2 用召回+精排解决“不知道哪张表”的问题
想通了上面三个痛点,架构调整的方向其实就清晰了:与其让大模型在全部元数据里猜,不如先做一个独立的“搜索引擎”,在所有表和字段里检索出最相关的候选,再让大模型只针对这些候选做生成。这套路跟搜索领域经典的两段式架构(粗排召回 + 精排)是同构的。
简单画一下逻辑:用户输入一个问题,先走“召回层”,从几百张表里快速找出Top N张候选表、每张表找出Top M个候选字段,还有相关的枚举值字典、表间关联关系这些元数据。然后再走“精排层”,把召回出来的候选塞给大模型,让它从中选择真正需要的字段、确定SQL骨架,最后生成完整的查询。
这么做的好处非常实在:一是Prompt体积大幅缩小,模型输入更干净,注意力更集中;二是候选集本身变成了一种“白名单”,模型根本接触不到无关表,误用表的概率大幅降低;三是整个链路变得可调试——召回归召回、精排归精排,问题出在哪一层一目了然。这一套思路后来也成了我们团队做智能问数项目的标配。
1.3 这个方案到底适合谁、解决什么问题
如果你是做企业级数据平台、BI增强分析、或者面向特定行业的自然语言查数工具,这套“召回+精排”的技术路线基本是必选项。它解决的核心问题是:在表海茫茫的企业数据环境里,怎么让大模型只盯着该盯的地方,生成可靠、稳定、口径一致的SQL查询结果。
对于只有几张表的小应用,你确实不需要这么复杂。但如果表超过几十张,或者业务方对查询准确性有硬性要求,建议你提前把召回和精排的架构搭起来,不然后面返工的成本极高。这篇文的受众,就是那些已经完成了“大模型能写SQL”这个里程碑、正准备往生产环境深水区迈进的团队。
2. 召回层设计:多策略比单策略强在哪
2.1 召回容易错,所以要多路并行
召回的核心目标就一个:尽量别漏掉正确答案所在的表和字段。我们称它为“多策略召回”,是因为单靠一种策略很难覆盖所有情况——基于关键词匹配对“销量”“销售额”这类词有效,但用户问“哪个品卖得最好”,关键词根本匹配不上;基于向量相似度的方式能抓语义,但对“订单金额”和“毛利”这种数字计算语义相近、业务意义不同的字段容易误召;单纯靠规则去解析表名、字段名,又适配不了各种各样的业务黑话。
所以最终的方案是让多路召回并行,把不同策略找出的候选集做并集,再一起送进精排层。我们的实践里,主力召回策略有四路:
策略一:SQL语义词法改写。这条路的核心是先把用户的自然语言问题做一次结构化改写,拆解出“指标/维度/过滤条件/时间范围”这几类关键信息。比如“三月份华东区销售额排名前5的产品”,我们要能从里面抽出指标是“销售额”、维度是“产品”、过滤条件是“区域=华东、时间=三月”。这一步为后续精准匹配提供了非常干净的输入信号。
策略二:单元测试式的元数据匹配。这个想法借鉴了代码测试的思路:把业务常用的指标口径预定义成元数据模板,用大模型对用户问题做“单元测试”,看看问题描述里的每个关键要素能跟哪张表的哪个字段“对上”。比如用户说“销售额”,我们就去查有没有定义了“sales_amount”且口径注释里写着“含税销售额”的字段。这个策略在字段注释质量高的场景下非常好用。
策略三:元命令扩展。所谓“元命令”,就是对特殊问法的预定义解释。比如“同比”“环比”会自动映射到时间维度表和自关联计算逻辑;“Top N”自动映射到排序字段和LIMIT逻辑;“去重”自动映射到DISTINCT或COUNT(DISTINCT)。这条策略解决的正是自然语言里的“半固定表达”问题,模型不需要每次从零推理这些约定俗成的业务语法。
策略四:混合向量检索(负样本增强)。用Embedding模型把字段名、字段注释、枚举值、甚至历史问过的SQL问题对都向量化,然后做相似度检索。这里有个关键点:除了正向样本,要把历史上出错的问题、用户改了又改的问题、以及模型生成错误的SQL都作为负样本存进去,目的是让向量空间里“相似但不正确”的语义也能被区隔开。
四路召回的结果汇总后要做一次“粗排”——按策略权重和匹配得分简单加权,选出Top K个候选表、每张表选Top个候选字段,并连同表之间的关系(比如外键关联)、枚举值字典一起作为“候选上下文”,传给下一步精排。
2.2 召回层要抽哪些元数据、怎么组织
很多团队在召回层翻车,不是策略不够多,而是元数据压根没准备好。这里分享一下我们最终沉淀出的元数据组织规范,按四个层次来构建:
第一层是表描述层,记录表名、业务描述、创建时间、更新周期、行数量级(用于估算成本)。表描述层的注释质量决定了召回的上限,如果注释里只有“table_order_info”这种缩写而不写“订单信息表(含未支付下单记录)”,召回效果一定是差的。
第二层是字段描述层,字段名、类型、单位、口径说明、枚举值列表,缺一不可。这里最容易被忽略的是“口径说明”,比如“金额”到底是含税还是不含税、是下单金额还是支付金额,这类信息直接决定SQL生成的正确性。
第三层是表关系层,外键关联、一对多/多对多关系、公共维度表(日历表、区域表、组织架构表)等。表关联关系缺失,模型就只能生成单表查询,遇到“销售和订单两张表一起查”就直接废掉。
第四层是业务口径层,把常用的指标定义沉淀成“指标字典”,比如“销售额 = SUM(订单金额) where 订单状态=已完成”。这一层要是能建起来,召回精度的提升是肉眼可见的。
2.3 召回结果是并行候选,不是字典查找
有一点特别想强调:召回层的产物不是“唯一定位到某张表”,而是一个候选集合。就算我们非常确定用户要查的是“订单明细表”,召回层提供的也应该是“订单明细表 + 关联合账单 + 退款表”以及每张表对应的字段候选。这个“冗余”是故意设计的——因为召回层追求的是“查全率”,宁愿多给也不能漏掉正确答案,至于到底选哪个,那是精排层的事。
实际测试下来,召回Top5的候选表如果包含正确答案,精排层最终选对的概率能做到85%以上。如果Top10里才包含正确答案,那精排层基本靠蒙。所以召回层说白了是在赌“正确答案别掉出候选集”——用多策略并集,就是为了降低这个概率。
3. 精排层技术:从候选到唯一的确定性收敛
3.1 精排不是简单排序,而是一次“结构化决策”
召回层给出了若干候选表、候选字段、候选关系,如果按字面意义上的“精排”来做,就是给每个候选打个分、排个序,选Top1。但自然语言问数这个场景,候选之间是有依赖关系的——选了订单表的“金额”字段,往往意味着还要选订单表(而不是退款表)作为主表;选了“区域=华东”这个过滤条件,意味着要考虑区域表是不是要关联进来。
所以我们的精排层实际做的是“候选集内的结构化选择”,核心流程分三步:
第一步,候选消歧。把召回层传来的所有候选按表为单位分组,让大模型先读每张表的字段描述和口径注释,判断这张表跟用户问题的相关性,并输出“确认使用/排除/保留存疑”三种倾向。这一步能把明显不相关的表过滤掉,大幅缩短后续推理路径。
第二步,骨架生成。让大模型基于消歧后保留的候选,先不写完整SQL,而是生成一个“查询骨架”——涉及哪些表、主表是哪张、关联条件是什么、需要哪些字段、过滤条件怎么映射、聚合粒度是什么。这一步相当于先把SQL的逻辑结构定了,避免模型在写SQL时东拼西凑。
第三步,SQL生成与自校验。拿到骨架后,再让模型逐字段填充,生成完整的SQL。生成完,必须走一遍“自校验”流程:把生成的SQL放到离线环境里跑一次,如果报错就根据报错信息反推修正(比如列名不存在、类型不匹配、缺少GROUP BY等);如果不报错但结果有明显的业务异常(比如某天的销售额突然变成0),就要alert出来让人工介入。
这套流程看起来慢(确实比裸跑多了一两轮LLM调用),但它把“生成SQL”这个高熵行为,转化成了几个低熵的确定性步骤,效果非常明显。我们内部评估指标上,Top-1的SQL正确率从裸跑的52%提高到了74%,乐观估计上限还能推到80%以上。
3.2 打分模型的选型:判别式与生成式混合
精排的第三步和第二步会用到LLM,那到底选哪个模型做这个环节?这个问题我们纠结了很久,最后沉淀的结论是“判别式小模型 + 生成式大模型混合”:
- 召回候选较多(超过30个)时,先用判别式模型(Cross-Encoder结构,比如基于预训练模型微调的匹配打分模型)对候选做一次快速打分,把候选集缩到10个以内。判别式模型的推理速度比LLM快一个数量级,适合做粗粒度的过滤。
- 候选集小于10个时,再交给生成式LLM做上面说的“候选消歧+骨架生成+SQL生成”。LLM在这里的价值不是信息检索,而是做需要“理解、推断、组合”的复杂决策,让它处理10个以内的候选,推理负担可控,输出质量也有保障。
选型上有两个容易踩的坑:一是把大模型用在召回层去逐候选打分,几百张表轮询LLM,延迟和成本都扛不住;二是只依赖判别式模型打分,完全不让LLM做结构化决策,那遇到语义复杂的问题模型就只会机械匹配,很容易错。混合架构确实是这个问题的最优解。
3.3 精排在业务场景里的几个典型偏差问题
精排层并不只是技术组件,它受业务规则的制约非常明显。分享几个我们在实际落地时发现的高频偏差:
指标同名不同义。用户问“成交额”,A表里的“成交额”指订单支付金额,B表里的“成交额”指已发货订单金额。这时候精排层如果只是按文本相关性打分,两个字段都能得高分,但业务上必须选A。我们最后手工建了一张“业务指标对齐表”,把用户常用表述和真正要用的物理字段做了绑定,经过映射后送进精排层作为硬约束。
维度扩展导致聚合错误。比如用户问“每个城市的销售额”,如果不加约束,模型很容易把“城市”映射到订单表里的城市字段,但实际上订单表里根本没有城市,需要通过门店表和区域表做关联。精排层必须能识别这种“候选里看起来有、实际路径不对”的情况,所以我们额外加入了“关联路径可达性判断”——候选目标字段必须能够通过候选表之间的外键关联从主表触达。
时间表达的口径漂移。“今年”到底指自然年还是财年?“最近7天”是否应包含今天?这种问题属于业务口径定义,不是模型能推理出来的。我们在精排层里把“时间条件映射”单独做成一个模块,用规则+字典先把问题里的时间表达换算成具体的SQL谓词,再拼进骨架里。
4. 实操过程与踩坑实录:一次完整的问数项目实践
4.1 直接可抄的召回与精排实现步骤
如果你现在准备动手搭一套,我把我们的实现过程整理成了一份步骤清单,可以按照这个顺序来:
第一步:元数据抽取代码先行。先不急着训练模型,把所有要开放查询的表结构、字段注释、表关系、指标口径统一抽取到一个元数据库(我们用MySQL,也可以放ES或专用的图数据库)。这一步花的时间最长,但决定了整个系统的上限。我们还做了一个“元数据健康分”指标,凡是注释缺失的字段直接在后台标红提醒。
第二步:搭建召回索引。把表名、字段名、注释、枚举值,以及历史问题时生成的SQL对,一起过Embedding模型,存入向量库(我们用开源的,也可以选商业的),同时还要对字段名和注释做分词建立倒排索引。
第三步:实现四路召回器。每路召回器都是一个独立模块,各自输出带得分的候选列表,最后用加权并集合并。我们的初始化权重是SQL语义词法0.3、单元测试0.25、元命令0.2、向量检索0.25,后面根据实际数据调。
第四步:训练精排打分模型。用历史问数日志(问什么、召回什么、最终选了哪些表和字段)构造训练数据,微调一个Cross-Encoder打分模型,作为精排的第一步过滤器。如果一开始没有历史日志,可以先不放这一步,直接用LLM消歧,等积累了数据再补。
第五步:设计LLM精排的Prompt模板。模板里固定包含:候选表集合、候选字段集合(含口径注释)、表关系图描述、业务约束提示。要求LLM按“候选消歧 → 骨架生成 → SQL生成 → 自校验”四步依次输出,并用JSON格式结构化返回。
4.2 核心环节实现:SQL语义词法改写模块
这一节以召回策略里的“SQL语义词法改写”为例,讲讲具体怎么落地。
这个模块的输入是用户原话,输出是结构化查询意图。我们一开始想省事,直接用正则+词典做关键词抽取,但识别“三月份华东区”这种混合表达就崩了。后来改成两步方案:
第一步,用预训练的语言模型做“语义角色标注”,抽取用户问题里的实体(月份、地区、产品、金额),以及实体之间的修饰关系。这一步的好处是在无标注数据情况下可以零样本跑通,我们把输出约束成固定的JSON Schema,比如 {"时间": "三月", "维度": ["区域"], "指标": "销售额", "约束": "区域=华东"}。
第二步,把抽取出来的实体和元数据库里的物理字段做“对齐”,这一步其实就是一次“小召回”——比如“华东”要能映射到区域字典表里的区域ID,“三月”要能映射到日期表里的月份字段。对齐结果作为结构化信号,跟其他召回策略的输出合并。
这个模块最大的价值不是它本身多准,而是它提供了一条“可解释”的召回路径——用户问什么,系统是怎么理解的,走到了哪张表的哪个字段,全链路透明,排查问题的时候非常管用。
4.3 工具选型与评估基准:我们怎么证明“它真的变好了”
做这个项目时,选型我会优先考虑团队已有的技术栈。我们的技术栈是Python为主,所以召回层用向量库处理向量检索,精排层的大模型走HTTP API调用,元数据和日志用存储库(MySQL/ES)保存。如果你团队以Java为主,那块也相对容易替代,核心思路是一致的。
关于评估,这是必须提前设计否则后面会被动的事情。我们自建了一套问数测试集,分四个维度:
- 表选择准确率:生成SQL是否用了正确的表
- 字段选择准确率:是否用对了业务字段(尤其指标准确率)
- SQL可执行率:生成的SQL在离线环境能否跑通
- 结果正确率:跑出来的结果和人工写出的答案是否一致
这套测试集不多,大概300条问题,但覆盖了日常高频问法、疑难问法、跨表关联、时间聚合等各种场景。每条问题都人工标注了“标准答案SQL”。每次改动召回或精排策略后,我们都会跑一遍全量测试集,用上面四个维度对比效果,确保改动是正向的,而不是只修好了A问题却弄坏了B场景。
评估还有一个副产品值得做:把测试集里失败的case收集起来,分析失败原因,分门别类(是召回漏了、精排选错了、还是模型生成的SQL有bug),然后反过来调整对应环节。我们做了三个月,积累了上百个失败case,系统的迭代方向就非常清晰了,不再是拍脑袋调参。
4.4 高频问题与排查技巧实录
最后一部分专门聊实际操作中遇到的坑,很多都是一行注释不写、一个配置没配就会让人排查半天的。
典型问题一:召回结果极其发散。用户问“订单金额”,向量召回把“退款金额”“优惠金额”“应付金额”全召回来了,导致精排层很难抉择。排查思路:一是看Embedding模型是不是领域适配度不够,业务术语不等于通用语义,后来我们重新用业务语料微调了Embedding模型;二是看字段注释质量,“订单金额”和“优惠金额”的注释如果都只写了“金额”,模型当然分不清,后来我们强制要求字段注释必须包含“计算口径+业务场景”。
典型问题二:精排层大模型“幻觉”出不存在的字段。候选集里明明没有“客单价”,模型却自己造了“AVG(order_amount/quantity)”这种列。这是大模型生成SQL最常见的错误之一。我们的方案是加了输出约束:要求模型生成SQL时,所有出现的表和字段名必须是候选集里给定过的,否则判为非法输出,重新生成。这个约束看起来简单,但能挡住绝大多数幻觉。
典型问题三:SQL可执行率很高,但结果不对。最典型的是用户问“上个月和这个月的销售额对比”,模型生成了两个独立聚合然后相减,结果数值完全不同。这种case靠自校验根本发现不了,因为SQL是可执行的,结果也不报错。我们的解法是在测试集里专门加入“对比类问法”,评估时单独统计这类问题的结果正确率,同时在业务层加入“指标口径校验”——如果两个指标都含有“销售额”但被分到两张表,而业务指标字典里它们对应的口径不同,就要提示用户。
典型问题四:冷启动没有历史日志,精排模型没数据训。这是很多项目刚起步时的死结。我们当时用“半自动标注”的方案:把问数测试集里的问题喂给召回层,把召回的候选表、候选字段人工确认对错,优先标200条,就用这200条先训一个粗糙的排序模型,效果比完全没有强很多。随着线上使用积累新日志,再持续补充标注数据,模型质量就滚起来了。
典型问题五:元数据不更新导致召回过期。业务表加了字段、改了注释,如果没同步到元数据库,召回层就永远用旧信息。我们后来做了一个定时同步任务,每天凌晨把元数据库全量刷新一次,并且对“新增字段”额外打标,让系统优先曝光新增的元数据,防止模型因为没见过新字段就当它不存在。
5. 关于大模型选型、成本控制与安全落地的补充思考
5.1 大模型选择:不是越强越好,而是够用且稳
在精排层,你到底用哪个基座模型?我的建议是千万别只盯着“最强的那个”。问数场景对模型的要求主要是两类能力:一是指令遵循能力,能严格按Prompt里规定的输出格式来;二是逻辑推理能力,能在候选集里做清晰的结构化决策。能同时满足这两个要求的,不一定是最贵的模型,反而是那个“你不给它自由发挥空间、它也不会乱来”的模型。
我们实操下来的体会是:把Prompt里每一步骤的约束写得极其具体(比如“不要输出解释,只输出JSON”),能够显著缩小较小模型和较大模型之间的效果差距,而成本差距可能是数倍以上。所以一开始先用中等规模的模型跑通全链路,如果效果确实不够再加复杂度,是更稳妥的路线。
5.2 成本控制:LLM调用次数要能算得到
企业问数场景的一大特点是有调用高峰,业务方经常在月末、季末集中查数。如果每个问题都要好几个LLM阶段调用,高峰期成本就非常感人。我们的经验是给每个问题预设“调用预算”——召回加精排最多不超过4次LLM调用,超过则走降级方案(纯规则+杀回召回层)。
降级方案在技术上不复杂,但要在产品层面提前设计好规则:比如调用量超阈值时,问数按钮变灰、提示“非高峰时段再来”,或者提供“仅支持历史模板问答”的受限模式。这些细节不做,线上容易直接被打爆。
5.3 数据安全与权限管控:问数系统的“生死线”
智能问数越准确越有价值,也越容易踩安全红线。数据权限必须无条件地在召回阶段就发现并拦截:用户在召回层只能看到他有权限访问的表和字段,一旦涉及无权访问的敏感表或敏感字段,召回结果直接置空,精排层连“候选”都不会包含。这样可以确保大模型不会生成连用户权限之外的SQL——因为连看都看不到。
另外推荐一个务实做法:所有生成的SQL必须走一个“安全执行网关”。这个网关负责三件事:一是校验SQL里涉及的表是否在用户权限白名单内;二是对高危操作(DELETE、UPDATE、全表SELECT无LIMIT)直接拦截;三是限制单次查询返回行数上限。已有的几个项目里,加了网关之后,出事概率大幅下降。这一步千万别省。
最后再分享一点实在的经验
整个项目做下来,我最想强调的其实是:智能问数的本质不是“让大模型生成SQL”,而是“让系统的每一步都变得可控、可解释、可评估”。多策略召回解决的是“模型不知道去哪找答案”的问题,精排解决的是“候选太多不知道选谁”的问题,而数据安全、成本控制、元数据治理这些工程问题,决定的是这个系统能不能在真实业务里活下来。
如果一个读者只从这篇文章里带走一件事,我希望是:先别急着让大模型直接写SQL,先把元数据治理做好,再把“召回+精排”的框架搭起来,最后再考虑用什么大模型。顺序不能反。
这行后续还可以往两个方向扩展:一是把精排层从“指标生成SQL”扩展到“指标解释与归因分析”,比如自动生成“为什么这个月销售额下降了10%”的诊断报告;二是引入强化学习,让系统根据用户对查询结果的反馈(比如用户改了几个字再查)自动调整召回权重和精排偏好。这些方向我们也在探索中,等有实际落地效果了,再跟你分享。