大多数公司里的数据消费链路都比想象中长:业务同学有问题 → 找数据团队 → 数据同学理解需求、定位指标、写 SQL → 验证结果 → 在 BI 平台或表格里交付。简单问题往往也要走完这一圈,复杂问题还会再叠几轮澄清和返工。结果就是问题到答案之间隔了好几跳,而真正促使业务行动的"洞察",常常在这种延迟里被稀释掉。
ChatBI 项目目标很明确:让任何人用自然语言直接拿到结果,把"问题→数据→洞察"压缩到一次对话之内。
整体分四层,自上而下职责递进:
架构说清楚了"我们想做成什么样",但要把这套交互真正跑起来,第一个绕不开的问题就是——自然语言到底应该翻译成什么?是直接生成 SQL,还是走一层中间表示?下一节先来回答这个最底层的选择题。
大部分做 ChatBI 的第一选择是 Text-to-SQL:把表结构丢给大模型,让它直接输出 SQL。内部也做过一些讨论——把"自然语言到查询"这件事单看作一个生成问题,会忽略数据场景里更重要的约束:权限、口径、可解释性、以及数据安全。业界目前主流的三类实现方式各有各的取舍:
| 实现方式 | 优缺点 | 适用场景 |
|---|---|---|
| Text2SQL |
优点
缺点
|
面向结构化数据查询,用户用自然语言直接拿到数据库结果。常见于 BI 工具、报表系统、数据仓库查询,适合数据分析师/业务人员快速取数。 |
| Text2DSL |
优点
缺点
|
企业级 BI 工具:基于指标 & 维度构建报表与多维分析体系。 |
| Text2Python |
优点
缺点
|
面向数据科学、计算逻辑、分析任务。用户需要的不仅是查询结果,还包括复杂的数据清洗与预处理、数学建模、机器学习训练、可视化图表绘制等。 |
三种路线没有绝对的"最优",只有最适合自己场景的组合。ChatBI 的目标是服务全员的取数与分析——既要保证指标口径统一与权限可控(这是Text2SQL难做到的),又要支撑超出聚合查询的复杂分析(这是单一 Text2DSL 做不到的)。而Text2Python 依赖数据获取,所以我们最终选择的是Text-》DSL-》SQL-》Python:
这种分工把"准确"和"灵活"的边界划得很清楚:基础数据走 DSL,高阶计算走 Python。模型负责理解用户意图并选择路径,剩下的事情交给后端的两套确定性管道。
分工虽然清晰,但真正决定 ChatBI 是否可用的,是其中Text2DSL 这一环至关重要——它是所有数据链路的入口,也是准确率的瓶颈所在。Python 沙箱的二次计算只是在 DSL 结果之上的延伸,本身相对确定;而 Text2DSL 涉及自然语言到结构化查询的语义转换,是整个系统里最容易出错、也最值得深入打磨的环节。本文剩余篇幅将详细介绍一环的实现过程。
方法选择确定后,剩下的问题就只有一个:怎么把一句自然语言,可靠地翻译成一份合法的 DSL?"可靠"在这里有两层含义,缺一不可:
day BETWEEN 2026-05-23 AND 2026-06-21
category_zero_name = "服装"
shop_code, shop_name
mo_sales_quantity(消费品销售量,降序)
{
"subject": "最近30天服装品类店铺销量Top5",
"summary": "查询 2026-05-23 至 2026-06-21 期间,商品品类为服装的销售量,按店铺编号和名称分组,按销售量降序;Top5 在结果层截断。",
"query_id": "query-d94bd555d5",
"query_schema": {
"metrics": [
{ "id": "mo_sales_quantity", "name": "消费品销售量", "order": "desc" }
],
"dimensions": [
{ "id": "shop_code", "name": "店铺编号" },
{ "id": "shop_name", "name": "店铺名称" }
],
"filters": [
{ "dimension_id": "day", "operator": "between",
"values": ["2026-05-23", "2026-06-21"] },
{ "dimension_id": "category_zero_name", "operator": "=",
"values": ["服装"] }
]
}
}
这就是 Text2DSL 的工作产物:自然语言里每一个语义片段,都被精准映射到 DSL 的某个字段。看似简单,但其中每一行映射的稳定性,背后都对应着一项工程能力——指标识别、维度选择、时间解析、维度值匹配、能力边界判断("Top 5"为什么不能写进 DSL)。
5 个环节中,LLM 实际参与的只有第 2 步"知识检索"(候选重排)和第 3 步"语义理解"(DSL 生成)。其余环节——时间解析、校验重试、维度值映射——都是确定性的工程模块。
要解决的问题:"上周/最近 30 天/上月同期"在 SQL 里都要落成具体日期区间,而模型最容易在这里翻车——把"上周"算成过去 7 天,或者写出一个 2024 年的日期范围,SQL 不会报错,但结果完全错了。我们的判断是:时间不该让模型猜,应该用规则定死。
实现:TimeKnowledgeService(约 350 行纯规则)。覆盖四类正则:
输出固定格式的"时间知识块"作为 chat message 注入到后续步骤,整步不走 LLM。
要解决的问题:指标平台经过多年数据沉淀,目前累计 上千个指标、上百个维度。如果把全量字典塞给 LLM,会同时撞上两堵墙:
更现实的问题是:当前上千个指标里,指标使用率较低——历史遗留、口径已过期、或仅服务于个别小场景;与此同时,定义也不完整、不易于 AI 理解——很多指标只有一句中文释义,缺少口径公式、适用维度、同义词、典型用法。这两件事叠加,让"直接交给检索"这条路走不通——召回再准,命中的也可能是不该被选的指标。所以知识检索的前置工作不是写检索算法,而是先治理元数据本身,再在治理后的元数据上做两层过滤:先用业务规则把候选集从"全量"压到"相关数据集",再在数据集内做混合检索。
第一层 · 按业务线推进指标治理:不只是技术层面梳理清洗,更是一次与业务部门之间协同,将业务部门关心的数据做体系化治理:
这里没有发明新东西,而是回归数据仓库建设理论体系的本质——把数据建设标准化对齐到"数据域 → 业务过程 → 原子指标 → 派生指标"的四层分解模型,让模糊的业务表达逐层拆成对 AI 友好的结构:
这套模型把"指标"从孤立短语变成了可被检索、可被组合、可被校验的结构化对象——AI 才能在受约束的空间里准确选择。
第二层 · 数据集内的精准选择:压缩到几十个候选之后,仍然需要在其中选出本次问题真正命中的指标和维度。这一步我们前后落地了两套方案——一开始走的是工程化的"三路融合召回",后来切换到了"LLM 分类匹配"作为主流程。下图把两套方案放在一起对比:
为什么选择方案 2:方案 1 走的是经典 RAG 思路——但在数据查询场景里这条路并不好走。用户自然语言表达的语义里夹杂大量噪声(口语化、修饰词、跨概念混说、错别字),而我们治理后的知识库语义相对干净(标准名 + 业务过程 + 度量 + 同义词的结构化字段)。要让"含噪查询"准确命中"干净知识",业界确实有不少优化手段:Query 改写(把口语化表达规范化)、答案假设(HyDE,先让模型生成假想答案再去检索)、多路召回 + LLM 重排 等。但这些方案叠起来后,工程链路明显变长,每一路的权重和打分都需要长期调优,整体实施成本偏高;我们在离线评测里把上述手段都拼齐,召回准确率也只做到了 60%左右,距离生产可用仍有差距。
切到方案 2 之后,思路反过来:让 LLM 直接在干净的知识库里做分类匹配。LLM 本身就具备很强的语义理解和噪声过滤能力——读问题时会自动忽略口语化与修饰,只抓核心意图;与此同时治理后的元数据已能放进 prompt,不需要先召回再选。于是"召回 + 选择"被合并成"一次决策",链路从三段变两步,可解释性反而更强,对后续维度白名单、unsupported 反馈、问题改写等业务能力的扩展也更友好。
有了上一步选定的指标和维度准确召回,这一步就变得比较简单了,主要是让LLM转化成合理的DSL:
要解决的问题:将用户口语化表达的修饰词转化为 DB 里实际存储的维度值。例如用户嘴里的"衣服、服装类、服饰",在 category_first_name 字段里其实统一存为「服装」。让模型猜会出错,最稳的办法是用检索 + 同义词字典做映射。
[衣服, 服饰, 服装类],逐个尝试精确 match,命中即返回库内标准值「服装」这一层把"自然语言里的口语化值"翻译成数据库里实际存在的值——用户说"衣服卖得怎么样",最终落到 category_first_name = '服装',这是 DSL 真正能查到数据的最后一公里。
评估 Text2DSL 不能只看"看起来对"。我们做了两件事来量化能力:构建覆盖正反向场景的评测数据集,再为每个语义环节设定明确的准确率底线。前者决定"测得全不全",后者决定"测得准不准"。
评测集不只包含"模型应该答对"的正向场景,还要刻意构造"模型应该拒答或报错"的反向场景——后者往往是生产环境出大事故的来源。当前一期评测覆盖如下:
| 类型 | 评测维度 | 示例 |
|---|---|---|
| 正向维度 | 时间维度 | 当天、周、月、近 7 天、滚动周期、特定日期区间、节假日等 |
| 数据维度 | 指标平台治理及 ChatBI 元数据中标记 P0 的维度 | |
| 数据指标 | 消费品收入金额、第三方平台费、仓储物流费、手续费 … | |
| 查询类型 | 统计、分组、排序、筛选、Top / Bottom N、对比、占比、同比、环比 | |
| 复杂性 | 单意图、嵌套查询、关联查询、多意图、多图表展现 | |
| 多轮会话 | 实体指代(引用"刚才那个表")、跟进问题、条件追加、意图切换、下钻 | |
| 反向维度 | 数据安全 | 越权查询(无权限的数据,如 DAU)、敏感字段屏蔽(身份证 / 手机号) |
| 非查询意图 | 闲聊问答、偏暗黑类、指令操作类(如"发邮件")、报错排查 | |
| 数据范围外 | 查询一期未支持的范围 | |
| 超大数据集返回 | 查询订单明细、要求返回全量日志、几十万行的导出请求 | |
| 模糊 / 歧义表达 | 缩写词解析冲突、同音字错误、指代不清(如"那个数据")、口语化表达 | |
| 极端 / 异常输入 | 极大极小值、特殊字符、超长文本、非法日期格式(如"2 月 30 号") |
正向 6 类覆盖"能查的数据",反向 6 类覆盖"不能查的数据 / 不该响应的请求"。准确率不只是"答对了多少",也包括"该拒答时有没有拒答"——这是数据查询场景比普通 QA 更严格的地方。
对每个语义环节单独做准确率评估:明确每个环节当前的薄弱点,再有针对性地迭代优化,逐步把整体准确率推上去。
| 评测维度 | 描述与示例 |
|---|---|
| 指标识别准确率 |
能否将用户的口语化表达精确映射到指标库中的标准指标。 示例:"帮我查下昨天的消费品收入金额" → 准确映射为「消费品收入金额」指标。 |
| 维度识别准确率 |
能否准确提取分组维度,并处理维度的同义词或层级关系。 示例:"查各店铺的消费品销售量" → 准确映射为「店铺名称(shop_name)」维度。 维度同义词:"看看各品类卖得怎么样",品类 = 一级品类 = 商品品类,都要映射到「一级品类(category_first_name)」维度上。 |
| 过滤条件提取准确率 |
能否准确识别筛选条件、枚举值、逻辑关系(AND / NOT、大于 / 小于等)。 示例:"除了服装,销售量大于 1000 的店铺" → category_first_name != '服装' AND mo_sales_quantity > 1000。用户说"华东三省",模型要解析为 province IN ('浙江', '江苏', '上海')。
|
| 时间范围解析准确率 |
对绝对时间、相对时间、动态时间的解析能力。 示例:"上个月"、"今年一季度"、"过去 30 天"、"双十一期间"。 |
| 总结准确率 |
能否准确总结 DSL 已经处理的需求与未能在 DSL 内处理、需要交由后续环节完成的需求。 示例:"查询 2026-05-23 至 2026-06-21 期间,商品品类为服装的销售量,按店铺编号和名称分组,按销售量降序;Top 5 需要在结果层通过 Python 进行处理。" |
在上面定义的数据集和标准上,最新一版 Text2DSL 跑出来的实测结果如下(评测样本数 N = 140):
| 评测维度 | 底线 | 实测 |
|---|---|---|
| 指标识别准确率 | ≥ 95% | 98.58% |
| 维度识别准确率 | ≥ 95% | 98.58% |
| 过滤条件提取准确率 | ≥ 95% | 95.04% |
| 时间范围解析准确率 | ≥ 95% | 98.58% |
| 总结准确率 | ≥ 95% | 95.04% |
| 整体 DSL 准确率 | ≥ 90% | 93.57% |
整体准确率只有93.57%,瓶颈不在指标或维度本身,而集中在过滤条件的维值匹配(字面差异导致没有精确匹配到对应限定值)。后续将重点通过以下三个步骤持续进行优化: