☰
掌柜问数:自然语言数据查询系统从架构到落地的实战解析
2026/10/22 9:07:23 网站建设 项目流程

先说个场景。我在上一家做零售数据服务时,每天被问得最多的一句话是:“数据啥时候能给?”业务方想看的不是什么复杂报表,就是“上周华东区的退货率怎么变了”“这次大促的转化比上次好还是差”,可这些听起来很简单的需求,走常规流程却经常要等一两天。后来我实在受够了这种低效,自己动手搭了一套叫「掌柜问数」的自然语言数据查询工具——业务人员不用再提工单,直接像跟人聊天一样问,系统自动把自然语言转成数据洞察:出 SQL、查结果、画图表、给结论。这篇就把我从零重构企业数据分析体验的思路、架构、实操细节、踩过的坑完整盘一遍,供正在做类似数据产品、BI 平台或者内部数据工具的朋友参考。

1. 想明白再动手:传统企业数据分析到底卡在哪

1.1 我见过最多的一种低效:需求来回踢皮球

没在企业里做过数据需求交付的人,可能很难想象一个“看个数”的需求能消耗多少沟通成本。业务方的原话通常是:“帮我看看最近转化情况怎么样”“活动效果行不行”“新客是不是变差了”。这些话在业务语境里很自然,但到了数据团队手里就全是模糊地带:转化是指 UV 到支付的转化,还是加购到支付的转化?活动效果是看 GMV 还是看增量?新客变差是和上周比还是和上月比?

我以前处理这种需求,平均要来回确认至少两次,每次等回复少则几个小时,多则一两天。好不容易对齐了,还得排期。数据团队手头往往同时压着十几个需求,简单查询也得往后排。一个紧急的运营决策,经常因为“数据没出来”硬生生变成拍脑袋决策。

所以我在做「掌柜问数」之前,最优先想的不是模型多先进、架构多复杂,而是先解决“来回踢皮球”这件事。目标很朴素:让业务方用自己最熟悉的说法提问,系统直接接管后续所有环节。换句话说,把“人找人要数据”变成“人直接问系统要答案”。

1.2 口径不一致:问“销售额”可能得到三个答案

比沟通低效更隐蔽的坑,是指标口径不一致。同样叫“销售额”,财务口径可能按开票时间算,运营口径可能按订单支付时间算,商品团队可能只算自营商品,渠道团队可能把分销也算进去。表面上大家说的是同一个词,底层 SQL 可能完全不同。

我在做自然语言查询时,踩过最痛的一次:业务方问“618 销售额多少”,系统按默认口径查了支付订单金额,但业务方实际想要的是包含退款前的原始成交金额,两边差了将近 20%。问题不是大模型回答错了,而是“销售额”这个词对应的底层定义没人告诉系统。

这个教训让我意识到,自然语言查询能不能落地,关键不是模型理解了多少口语,而是业务口径有没有被显式定义出来。如果口径没有沉淀成统一配置,那模型再聪明,也只是在错误的前提上给出看似正确的答案。

1.3 一个关键取舍:不追求“全自动”,只解决高频的 80%

市面上很多自然语言查询产品,宣传语都喜欢说“全自动生成报表”“AI 驱动决策”。但我在真正设计系统时,给自己的定位反而保守:不追求覆盖所有问题,先把 80% 的高频问题做到又快又准。

为什么这么取舍?因为数据分析场景的多样性是无限的。“销售额”这种高频问题,问法再多也能穷举常见的几十种;但“帮我分析一下为什么东北区退货率比华南区高”这种归因问题,涉及多表关联、维度下钻、外部因素,想全自动实现,成本和稳定性都是失控的。

所以我的设计原则是:系统先把“查数”这件确定性高的事做好,遇到模糊的、需要专业判断的问题,再通过引导式追问降级处理,或者转人工。事实证明,这个取舍让上线初期的准确率和服务稳定性都好了很多。工具首先得可靠,用户才愿意持续用。

2. 架构设计:「掌柜问数」怎么把自然语言变成数据查询

2.1 六段式处理链路:从用户提问到拿到结论

整个处理链路我是按流水线来设计的,把“自然语言到数据”的转化拆成六个环节,每个环节各司其职,方便单独调试和优化。

  1. 用户提问:业务人员在对话框里输入自然语言问题,比如“上个月华东区的销售额环比变化多少”。
  2. 实体抽取与意图识别:从问题里提取指标、维度、时间范围、过滤条件、对比意图。这一步最常见,也最影响后续效果。
  3. 语义映射:把抽取出来的业务词映射到指标字典和维度字典上,比如“销售额”映射到“支付订单金额总和”,“华东区”映射到“region = '华东'”。
  4. SQL 生成:基于映射结果和预设的查询模板,由大模型生成结构化 SQL,再经过规则校验器检查。
  5. 查询执行与安全拦截:只读账号连接数仓,强制加 LIMIT,超时限制,敏感字段脱敏,然后真正执行查询。
  6. 结果解释与可视化:把查询结果整理成一句人话结论,并自动推荐合适的图表类型。

这个链路最关键的设计决策,是把“理解语言”(第 2、3 步)和“生成 SQL”(第 4 步)拆开。直接让模型一步生成 SQL 不是不行,但一旦出问题,你很难判断是语言理解错了、映射错了还是 SQL 写错了。拆开之后,每步都能单独记录日志、单独优化,排查问题的效率高很多。

2.2 指标语义层:让模型不直接面对裸表

很多 NL2SQL 方案喜欢直接丢给模型一堆表结构和字段注释,让模型自己摸索。我试过之后果断放弃了——现实业务里的表动辄几十个字段,名字还经常是拼音缩写,模型看得多了就开始“编”。真正可靠的路径,是在模型和裸表之间加一层指标语义层。

我的做法是维护一份指标字典,每个指标定义清楚这几个要素:

  • 指标的默认口径 SQL 片段
  • 允许搭配的维度
  • 聚合方式
  • 同义词和常见问法

大概长这样:

{ "indicator": "avg_order_value", "display_name": "客单价", "aliases": ["客单价", "平均客单价", "每单均价", "ARPU"], "aggregation": "avg", "metric_expression": "SUM(pay_amount) / COUNT(DISTINCT order_id)", "available_dimensions": ["date", "region", "channel", "product_category"], "default_date_range": "last_30_days" }

模型拿到手的不是原始表结构,而是这一层已经“清洗过”的指标抽象。它只需要知道“客单价怎么算”和“能跟哪些维度组合”,不需要自己去理解一堆底层表的关联关系。这就像你请人帮忙查资料,给的不是一屋子散乱的文件夹,而是一份目录清晰的档案手册——对方效率高得多,也不容易翻错。

2.3 权限、性能与安全兜底:不能指望大模型自觉

大模型生成 SQL 这件事,本身就带有随机性。你再怎么在提示词里强调“只能查询”,模型还是可能在极端输入下产生不期望的语句。所以安全这块,我的原则是:不把安全寄托在模型自觉上,全部用规则做硬拦截。

我这个项目里设置的兜底策略主要有几层,都在 SQL 执行前把关:

安全策略具体做法
只读账号数仓查询统一走只读账号,从数据库权限层面杜绝写入可能
关键字黑名单检测 DELETE、UPDATE、DROP、ALTER 等关键字,直接拒绝执行
强制 LIMIT自动给查询追加 LIMIT 2000,防止全表扫描拖垮数仓
时间范围约束没有显式时间条件的查询,按指标默认范围补上,比如最近 90 天
敏感字段脱敏手机号、身份证等敏感字段在结果层用掩码处理
超时熔断查询超过 30 秒自动取消,避免长时间占用资源

这套策略看着简单,但上线后真的帮了大忙。有一次模型在一个测试用例里生成了带子查询的笛卡尔积语句,直接给系统拦住了,没把数仓查挂。所以我的建议是:任何自然语言查询工具,先把安全拦截写在第一优先级,再谈效果优化。

3. 实操落地:核心环节的逐步实现

3.1 第一步:把大白话里的“指标、维度、时间”挖出来

实体抽取是整个系统里我最先做的模块。业务方的问法千奇百怪,但落到数据查询上,抽取的信息其实就四类:指标、维度、时间范围、过滤条件。

举一个真实问法:“上礼拜华东的退货咋样,跟大上周比呢”。这里指标是“退货率”,维度是“区域=华东”,时间范围是“上周”,对比对象是“上上周”。我需要把这句话转成两个查询条件:

  • 查询 A:时间=上周,区域=华东,指标=退货率
  • 查询 B:时间=上上周,区域=华东,指标=退货率

然后输出给后续模块做对比。

实体抽取我采用的是“词典 + 大模型”混合方案。先维护一份业务词典,把常见的时间词(“上周”“上月”“最近七天”)、维度值(“华东”“线下渠道”“女装类目”)和指标词(“退货率”“销售额”“客单价”)都列进去。抽取时先做词典匹配,把确定的部分锁定,再让大模型处理口语化表达和复杂组合。

这样做的原因很务实:词典匹配结果稳定、可解释、零成本,而大模型的优势在泛化理解。两者结合,既不会因为词典覆盖不全导致问题被漏掉,也不会因为模型抽错一个实体导致整个查询错误。

同义词扩展这块我也吃了不少亏。“客单价”有人叫“件单价”,有人叫“平均客单”,有人直接说“一单多少钱”。不把这些同义词收进词典,系统就会频繁答错。后来我专门建了一个同义词映射表,在词典层就把别名归一化到标准指标名,准确率提升非常明显。

3.2 第二步:设计一套不容易翻车的提示词

大模型生成 SQL 的效果,很大程度上取决于提示词里给了它什么。我的提示词结构经过好几轮迭代,最终稳定成五个区块:

你是企业数据查询助手,只能根据提供的指标字典和维度字典生成 SQL。 规则: 1. 只能使用指标字典中存在的指标和维度,禁止编造字段名。 2. 时间条件必须带上,默认最近30天。 3. 结果行数限制 LIMIT 2000。 4. 不要使用 DELETE、UPDATE 等非查询语句。 5. 如果用户问题缺少必要信息,输出需要澄清的问题清单。 指标字典: [指标1: 销售额 = SUM(pay_amount),可用维度:日期/区域/渠道/品类] [指标2: 退货率 = 退货订单数 / 支付订单数,可用维度:日期/区域/渠道] ... 维度字典: [区域: 华东/华南/华北/西南 ...] [渠道: 线上/线下/小程序/直播 ...] 示例: 问:上周华东销售额 答:SELECT SUM(pay_amount) FROM orders WHERE region='华东' AND pay_date >= '2024-xx-xx' AND pay_date < '2024-xx-xx'

这里最有价值的部分是few-shot 示例,也就是我给的“示例问答对”。我发现模型对少样本示例的依赖程度远超预期。刚开始我只给了一两个示例,很多带对比、带筛选条件的复杂查询生成得磕磕绊绊;后来我把高频问法整理成 15 个示例,覆盖环比、同比、同品类对比、分组排序等场景,准确率一下子涨了一大截。

另一个关键点是把指标字典压缩后再放进提示词。如果直接把全部指标定义都塞进去,提示词动辄几万字,模型容易“迷失”在长上下文里。我采取了两级策略:先按用户问题里的关键词粗筛指标,只把相关的三五个指标定义传给模型。这样既节省 token,也显著提升了命中率。

最后要反复强调“禁止编造字段”——大模型一旦遇到不认识的指标,倾向于自己发明一个相似字段。明确规则之后,模型更倾向于反馈“缺少该指标定义”,而不是硬生成一段可能错误的 SQL。

3.3 第三步:SQL 生成后的校验与改写

模型生成的 SQL 再怎么看起来合理,我也默认它“可能有问题”,必须过一遍规则校验器。整个校验器是纯规则实现,不依赖大模型,保证确定性和速度。

校验器主要做五件事:

  1. 检查非法关键字:包含非查询语句直接拒绝。
  2. 检查字段合法性:所有 select、where、group by 中出现的字段,必须在指标字典或维度字典中。
  3. 检查聚合函数匹配:指标定义的聚合方式与 SQL 中的聚合函数一致。
  4. 检查时间条件:必须有时间过滤,且时间范围合理。
  5. 检查查询复杂度:超过两张表关联就提示改写,避免大 join 导致性能问题。
def validate_sql(sql: str, allowed_fields: set) -> ValidationResult: # 1. 关键字黑名单 for kw in ["DELETE", "UPDATE", "DROP", "ALTER", "INSERT", "CREATE"]: if re.search(rf"\b{kw}\b", sql.upper()): return ValidationResult(False, "包含非查询操作") # 2. 提取所有字段名,逐一校验 fields = extract_fields(sql) for field in fields: if field not in allowed_fields: return ValidationResult(False, f"字段 {field} 不在指标字典中") # 3. LIMIT 兜底 if "LIMIT" not in sql.upper(): sql += " LIMIT 2000" return ValidationResult(True, sql)

校验失败的情况我会记录日志,定期回看,找出模型常犯的错误模式。比如有一段时间模型经常把退货率原表字段写错,后来我在提示词里补上了“退货率 = 退货订单数 / 支付订单数”这条定义,问题就消失了。把校验日志当成数据产品的“错题本”来用,迭代效率会非常高。

3.4 第四步:结果解释与图表推荐,让数据“说人话”

查询结果出来之后,系统不能只甩给用户一张表。自然语言入口的价值,在于数据知识向业务语言的回流——你用一个自然语言问题开头,也应该拿到一个自然语言回答。

结果解释我采用模板 + 动态填充的方式。比如环比下降类问题,模板是:“X 时间段内指标 A 为 B,较上一周期下降/上升 C(D%),主要变化发生在维度 E。”后面再拼上图表推荐,让用户直观看到趋势。

图表类型我总结了一套简单的推荐规则:

查询结果特征推荐图表
单指标连续多时间点折线图
多指标对比分组柱状图
分组占比饼图/环形图
带地理维度地图
明细数据表格

这套规则实测下来很稳,因为它是按结果结构而不是问题文字来推荐的,不会因为用户换了一个说法就推荐错图表。做过可视化的人应该深有体会——图表类型这事儿其实是有套路的,踩过的坑多了,自然就会收敛到那几种合理的组合上。

3.5 第五步:用回归测试集持续打磨准确率

做这类系统,最怕“感觉好像变聪明了,但说不出到底哪里变聪明了”。所以我从一开始就建了一个回归测试集:人工标注了 40 个高频业务问题,每个问题都有标准的 SQL 和结果格式。

每次改动系统,不管是大模型版本升级还是提示词调整,我都先跑一遍回归测试集。如果准确率下降,说明改动引入了回归;如果上升,就记录是哪些问题被解决了,作为后续优化的参考依据。

我记得项目刚起步时,这套回归测试集的准确率只有 42%,大量问题卡在实体抽取和指标映射上。后来经过三轮优化——先补齐同义词词典、再重写提示词示例、最后加了长问法改写模块——准确率提到了 83%。后续又通过持续补充测试集,覆盖率慢慢扩展到 90% 上下。

这里我想强调一点:不要用少量案例分析来评估系统效果,一定要用固定测试集做可重复的评测。没有量化指标,优化方向很容易被个别案例带偏。

4. 上线后避坑指南:常见问题与排查实录

4.1 用户问得太笼统,模型不知道要查什么

系统上线后,我们收到最多的一类失败案例,就是用户上来直接说“看一下数据”“最近怎么样”。这种问题缺少必要的指标和时间维度,指望模型猜是不现实的。强行猜,准确率反而不如坦诚地反问。

我在系统里专门做了一个澄清流程:当实体抽取环节发现关键信息缺失时,不直接生成 SQL,而是输出追问选项。比如用户问“最近怎么样”,系统会回复:“你想看哪个指标?我可以帮你查销售额、订单量、退货率或客单价。想看的时间范围是哪段?默认最近 30 天。”

一开始我担心这种反问会让用户觉得系统不够聪明,实际体验下来恰恰相反——用户很愿意接受这种引导式交互,反而觉得工具“懂业务”。模糊问题硬答才是灾难,给用户一个清晰的选择,比强行输出一个不确定的答案要强得多。

4.2 同义词和一物多名:被“客单”“件单”绊了一跤

上线初期有个高频问题一直答错:业务方问“客单多少”,系统会识别成“客单量”,而真实业务指标是“客单价”。更坑的是,不同部门对同一指标还有不同叫法,运营说“客单”,供应链说“件单价”,财务说“每单均价”,其实指向同一个指标。

这个问题的最优解不是在提示词里堆描述,而是在同义词映射表里显式建立别名关系。我整理了一张同义词表,把“客单、客单价、平均客单价、每单均价、ARPU、客单水平”全部映射到标准指标avg_order_value,一次配好,所有下游逻辑都能复用。

后来我总结出一个经验:业务场景里的同义词,比技术文档里写的要多得多,因为每个团队都有自己的语言习惯。上线前一定要找各业务线的人收集一版“口头黑话”,整理进词典里。这个工作不性感,但收益非常直接。

4.3 数据越查越慢:预聚合与查询兜底

随着用户量上来,查询延迟问题开始暴露。一个按“区域 × 周”聚合的查询,在明细大表上跑一次可能十几秒,体验很差。我采取了几层优化:

第一层是预聚合表。把最高频的指标按常用维度(区域、渠道、品类 × 日/周/月)提前算好,查询直接命中聚合表,秒级出结果。第二层是默认时间范围限制,没有显式时间要求的查询自动限定最近 90 天,避免全量扫描历史数据。第三层是缓存,相同问题的查询结果写入缓存,短时间重复提问直接返回。

这三层组合下来,平均查询耗时从最初的 6.8 秒降到了 1.2 秒左右。用户对这个速度的评价是“像在跟懂数据的同事聊天,回应还挺快”。做这类系统,性能问题不是后期的增量需求,而是上线第一天就应该考虑的核心体验指标。

4.4 大模型幻觉字段:从校验日志里定位

模型编造字段是 NL2SQL 系统里的经典问题。有一次用户问“最近一周的退款金额”,模型直接生成SELECT refund_amount FROM orders,但表里的真实字段是refund_fee。业务表字段充满了各种历史遗留命名,模型不可能全部知道。

这个问题我从两个方向解决。一是持续扩充字段词典,把容易混淆的字段名和常见别名都收录进去。二是把校验日志和召回日志打通——当模型生成的字段名不在字典里时,记录完整上下文,定期分析这些失败样本,找到字段名差异的规律,统一补进映射表。

这个策略执行了一个多月后,幻觉字段引发的错误明显下降。现在系统里有个机制:提示词里明确写了“如果找不到字段,不要猜测,使用 UNKNOWN_FIELD 占位并输出提醒”,模型遵循得比预期好很多。所以处理幻觉的核心是:给模型一条体面的“不知道”路径,而不是逼它硬猜。

4.5 排查速查表:一张表应对大部分上线事故

运营过程中我把最常见的问题整理成了一张速查表,遇到问题先查表定位,效率很高。

问题现象优先怀疑环节排查方向
答非所问实体抽取/意图识别看日志里抽取出的指标、时间、维度是否齐全
指标口径不一致指标语义层确认用户问的是哪个口径,检查指标字典是否配置正确
查询结果全部为空时间解析/过滤条件确认默认时间范围是否正确,是否存在筛选条件冲突
同一问题时好时坏大模型随机性增加 few-shot 示例,或尝试降低生成温度参数
查询超时查询性能检查是否命中预聚合表,是否需要加 LIMIT 或时间约束
字段不存在错误字段词典把错误字段名加入词典别名,或修正指标表达式里的字段引用

这套速查表上线以后,团队处理问题的平均时长从将近一小时压缩到了十五分钟左右。我觉得任何内部数据工具,都应该维护这样一份“故障处理手册”——它既是排查指南,也是经验沉淀的载体。

做这个项目的过程中,我最大的体会是:自然语言查询带来的体验提升再惊艳,也只是水面上的那一层。水面之下真正决定成败的,是指标口径的梳理、语义层的设计和安全兜底的完备度。如果你打算在自己团队里做类似的事,我建议先别急着接大模型,先把业务侧的高频指标和常见问法盘清楚,再动手建第一个可用的闭环——哪怕最初只支持十个指标,也比一上来就想支持所有问题要扎实得多。后续如果要扩展,可以考虑语音提问、定时订阅报表、异常告警和归因下钻,每个方向都能在现有链路之上做增量。但是从哪里出发最稳?我个人建议永远是:让最简单的那二十个问题,先做到又快又准。

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

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

立即咨询