ChatBI 技术系列 · 从“能查”到“查准”

项目背景:从"对话即取数"到"洞察即行动"

大多数公司里的数据消费链路都比想象中长:业务同学有问题 → 找数据团队 → 数据同学理解需求、定位指标、写 SQL → 验证结果 → 在 BI 平台或表格里交付。简单问题往往也要走完这一圈,复杂问题还会再叠几轮澄清和返工。结果就是问题到答案之间隔了好几跳,而真正促使业务行动的"洞察",常常在这种延迟里被稀释掉。

ChatBI 项目目标很明确:让任何人用自然语言直接拿到结果,把"问题→数据→洞察"压缩到一次对话之内。

  • 快:数据获取时间从 小时 压缩到 分钟,自然语言直达结果。
  • 全:数仓、飞书表格、业务库等多源数据统一接入,跨数据源查询无感知。
  • 准:统一指标口径,避免"同一个指标多份结果",LLM输出结果可追溯。

整体分四层,自上而下职责递进:

接入层
Web / Desktop / CLI
对话 偏好设置
IM Channels
飞书
平台层
Backend Services
会话管理 记忆管理 工具 & Skill 管理 数据集 Token 限额 权限管控
Agent
Data Flow
上下文管理
长期记忆 · 短期记忆 · 上下文压缩与续写
流程编排
Lead Agent 意图路由 · Subagent 派发
Sandbox
Python / Bash 沙箱执行 · 二次计算 · 文件读写隔离
System Prompts
SOUL.md 行为规则 · Prompt.md DSL 协议 · SKILL.md 工作流
Tools & Skills
Skills
问数 解释数据含义 · 回答"该怎么查",不直接取数
查数 多维交叉查询 · 趋势、拆解、对比的结构化结果
分析 归因 / 异动 / 分布等深度分析,输出洞察结论
报告 周期性数据报告自动生成与推送
Tools
本文重点
知识检索指标 / 维度 / 标签搜索 · 同义词扩展
语义理解意图识别 · 指标维度选择 · DSL 生成
数据查询表查询 · 标准指标 · 画像 · 埋点
可视化表格 · Markdown · 图表推荐
数据层
数据集 · Datasets
数据表 标准指标 飞书文档 / 文件 外部行业数据 平台数据(AB & Metis & 画像 & KBI)
知识库 · Knowledge Base
指标维度定义 常用数据查询 常用数据模板 外部知识
  • 接入层(Entry):面向用户的多端入口。Web 端覆盖完整的对话、过程回放、用户设置;IM 端(飞书)以会话气泡形式接入,复用同一套 Agent 内核。这一层只做通道适配与协议转换,所有业务能力都从平台层往下取,新增渠道不会影响 Agent 行为。
  • 平台层(Platform):承载所有"非智能"的基础设施——会话管理、长短期记忆、工具与 Skill 注册、数据集元数据、Token 限额、权限管理。这些能力确定性强、可被复用,与具体业务和模型无关。Agent 层在工作时只是消费它们,不重复实现。
  • Agent 层(Intelligence):整个系统的智能核心。
    • Data Flow:上下文管理、流程编排、Sandbox、System Prompts 等 Harness 能力,把可被规则化的工作(时间解析、字段校验、参数注入、重试兜底等)尽可能从模型链路里摘出来,变成确定性工程。
    • Tools & Skills:模型实际调用的能力集合。Skills 是面向用户的四类技能(问数、查数、分析、报告);Tools 是支撑这些技能的底层工具,其中"知识检索 + 语义理解"是从自然语言到结构化 DSL 的数据获取核心环节。
  • 数据层(Data):所有数据与知识的统一抽象,刻意拆成两类、走不同检索通路:
    • 数据集是被查询的对象——数据表、标准指标、飞书文档、外部行业数据、平台数据(AB / Metis / 画像 / KBI)、文件等,命中后产出真实数据。
    • 知识库是辅助模型理解的语料——指标维度定义、常用数据查询、常用数据模板、外部知识,用于让模型"知道该查什么、该怎么查",不直接产出数据

架构说清楚了"我们想做成什么样",但要把这套交互真正跑起来,第一个绕不开的问题就是——自然语言到底应该翻译成什么?是直接生成 SQL,还是走一层中间表示?下一节先来回答这个最底层的选择题。

方法选择:Text2DSL + Python

大部分做 ChatBI 的第一选择是 Text-to-SQL:把表结构丢给大模型,让它直接输出 SQL。内部也做过一些讨论——把"自然语言到查询"这件事单看作一个生成问题,会忽略数据场景里更重要的约束:权限、口径、可解释性、以及数据安全。业界目前主流的三类实现方式各有各的取舍:

实现方式 优缺点 适用场景
Text2SQL
优点
  • SQL 是通用语言,基础模型语料丰富,生成效果较好
  • 数据库自带执行环境,无需单独构建
  • 支持复杂语义查询,如对比、排名等
缺点
  • 数据权限、安全很难保障,依赖上层服务建设
面向结构化数据查询,用户用自然语言直接拿到数据库结果。常见于 BI 工具、报表系统、数据仓库查询,适合数据分析师/业务人员快速取数。
Text2DSL
优点
  • 结构化语义,可解释性好,支持跨平台
  • 安全、权限可控性强
缺点
  • 自定义描述形式,通用模型对复杂语义表达不够直接
企业级 BI 工具:基于指标 & 维度构建报表与多维分析体系。
Text2Python
优点
  • 灵活性最强,可调用任意库(pandas / numpy / matplotlib 等)
  • 能完成 SQL 难以胜任的复杂逻辑与计算
  • 适合分析型工作流:数据查询 + 数据处理 + 图表输出
缺点
  • 需要 Python 沙箱执行环境(安全性、性能都有风险)
  • 对非技术用户而言,理解与 debug 难度大
  • 与数据库交互需额外封装(通常 Text2Python 内部还会再调 Text2SQL)
面向数据科学、计算逻辑、分析任务。用户需要的不仅是查询结果,还包括复杂的数据清洗与预处理、数学建模、机器学习训练、可视化图表绘制等。

三种路线没有绝对的"最优",只有最适合自己场景的组合。ChatBI 的目标是服务全员的取数与分析——既要保证指标口径统一与权限可控(这是Text2SQL难做到的),又要支撑超出聚合查询的复杂分析(这是单一 Text2DSL 做不到的)。而Text2Python 依赖数据获取,所以我们最终选择的是Text-》DSL-》SQL-》Python

  • Text2DSL:承担基础数据查询。模型只在已知指标和维度的语义空间内组装查询描述,后端把 DSL 翻译成 SQL 执行——口径统一、权限可控、可解释。
  • Text2Python:承担二次计算与高阶分析。DSL 查询拿到原始数据后,在沙箱里用 Python 完成环比、占比、归因、分布等派生计算,以及 SQL 难以胜任的分析任务。

这种分工把"准确"和"灵活"的边界划得很清楚:基础数据走 DSL,高阶计算走 Python。模型负责理解用户意图并选择路径,剩下的事情交给后端的两套确定性管道。

分工虽然清晰,但真正决定 ChatBI 是否可用的,是其中Text2DSL 这一环至关重要——它是所有数据链路的入口,也是准确率的瓶颈所在。Python 沙箱的二次计算只是在 DSL 结果之上的延伸,本身相对确定;而 Text2DSL 涉及自然语言到结构化查询的语义转换,是整个系统里最容易出错、也最值得深入打磨的环节。本文剩余篇幅将详细介绍一环的实现过程。

Text2DSL:把自然语言变成可执行的查询

方法选择确定后,剩下的问题就只有一个:怎么把一句自然语言,可靠地翻译成一份合法的 DSL?"可靠"在这里有两层含义,缺一不可:

  • 能准确识别出可查询的 DSL:用户问得清楚、系统能力覆盖得到的需求,要稳定输出口径正确的查询结构,不能时对时错。
  • 能准确反馈不能处理的需求:超出能力边界、维度不支持、指标不存在、表达歧义的情况,要明确告诉用户"哪里不行、为什么不行",而不是胡乱猜一个看似合理的结果。

一个具体例子:从一句话到一份 DSL

USER 最近 30 天,服装品类店铺销量 Top 5 是哪些?
语义理解 + 知识检索 ↓
最近 30 天 filter · 时间 day BETWEEN 2026-05-23 AND 2026-06-21
服装品类 filter · 维度值 category_zero_name = "服装"
店铺 dimensions shop_code, shop_name
销量 metrics mo_sales_quantity(消费品销售量,降序)
Top 5 post-process DSL 仅取数,Top N 在结果层截断
组装为结构化 DSL ↓
{
  "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)。

核心链路:一个数据问题的完整旅程

1
时间解析
TimeKnowledgeService
规则引擎,不经过 LLM
2
知识检索
指标 / 维度精准识别
两步式 RAG(含 LLM 重排)
3
语义理解
在受限语义空间内
LLM 结构化生成 DSL
4
校验 & 重试
逐字段结构化校验
最多 3 轮错误注入重试
5
维度值映射
DimValueRetriever
三级匹配 + 同义词扩展
确定性工程环节   LLM 参与环节

5 个环节中,LLM 实际参与的只有第 2 步"知识检索"(候选重排)和第 3 步"语义理解"(DSL 生成)。其余环节——时间解析、校验重试、维度值映射——都是确定性的工程模块。

时间解析:把模糊时间表达转化为精确时间知识

要解决的问题:"上周/最近 30 天/上月同期"在 SQL 里都要落成具体日期区间,而模型最容易在这里翻车——把"上周"算成过去 7 天,或者写出一个 2024 年的日期范围,SQL 不会报错,但结果完全错了。我们的判断是:时间不该让模型猜,应该用规则定死。

LLM 推算上周五日期耗时 252 秒、13125 tokens 的对话截图
把"上周五是几号"交给 LLM 推算:耗时 252.71s、消耗 13125 tokens,还要先列出"日常口语含义"和"自然周含义"两种语义假设,再附一段星期推算验证——一道纯日历题被生生做成了演绎推理。规则化的优势在这里一眼就能看到。

实现:TimeKnowledgeService(约 350 行纯规则)。覆盖四类正则:

  • 相对周期:上/本/下 + 周/月/季/年(含连续相对,如"上上月")
  • 周内日期:"上周一"、"本周五"
  • 最近 N 天/周/月:支持中文数字、连续区间
  • 节假日:内置 2015-2026 节假日词典,含农历春节/中秋等

输出固定格式的"时间知识块"作为 chat message 注入到后续步骤,整步不走 LLM

知识检索:从海量指标维度里筛出候选

要解决的问题:指标平台经过多年数据沉淀,目前累计 上千个指标、上百个维度。如果把全量字典塞给 LLM,会同时撞上两堵墙:

  • Token成本:全量指标只算名称和定义,单次 prompt 就要 5w+ tokens,每次问答都是巨大的成本和延迟。
  • 召回准确率:指标平台里光"GMV"相关的就有几十个指标(消费品GMV / 消费品GMV含站内 / 站内GMV / 赛事GMV / 会员GMV 等)。让模型在 大量指标里二选其一,错配概率远高于在精准的范围内选择。

更现实的问题是:当前上千个指标里,指标使用率较低——历史遗留、口径已过期、或仅服务于个别小场景;与此同时,定义也不完整、不易于 AI 理解——很多指标只有一句中文释义,缺少口径公式、适用维度、同义词、典型用法。这两件事叠加,让"直接交给检索"这条路走不通——召回再准,命中的也可能是不该被选的指标。所以知识检索的前置工作不是写检索算法,而是先治理元数据本身,再在治理后的元数据上做两层过滤:先用业务规则把候选集从"全量"压到"相关数据集",再在数据集内做混合检索。

第一层 · 按业务线推进指标治理:不只是技术层面梳理清洗,更是一次与业务部门之间协同,将业务部门关心的数据做体系化治理:

  • 范围治理:由业务方共同梳理本业务线真正在用、有口径背书的指标和维度,剔除停用、重复、口径模糊的字段,把每个业务线下的"可用集合"明确下来。
  • 标准化定义:原本面向人的指标说明("GMV、含退款")对模型并不友好。统一标准结构「标准名 · 业务释义 · 计算口径 · 同义词 · 适用维度 · 常见用法」,让AI做分类和选择效果更好。

这里没有发明新东西,而是回归数据仓库建设理论体系的本质——把数据建设标准化对齐到"数据域 → 业务过程 → 原子指标 → 派生指标"的四层分解模型,让模糊的业务表达逐层拆成对 AI 友好的结构:

数据域 订单域 最高层归类,界定语义所在的业务边界 业务过程 下单 · 支付 · 退款 域内的关键业务事件,避免"作业环节"这类模糊词 原子指标 消费品去退后销售量 业务过程 度量 = 最小可复用计算单元 派生指标 上周 · 服装类 · 按店铺 · 消费品去退后销售量 原子指标 修饰词 时间周期 统计维度

这套模型把"指标"从孤立短语变成了可被检索、可被组合、可被校验的结构化对象——AI 才能在受约束的空间里准确选择。

第二层 · 数据集内的精准选择:压缩到几十个候选之后,仍然需要在其中选出本次问题真正命中的指标和维度。这一步我们前后落地了两套方案——一开始走的是工程化的"三路融合召回",后来切换到了"LLM 分类匹配"作为主流程。下图把两套方案放在一起对比:

方案 1 · 三路融合召回 先用确定性检索筛候选,再交给 LLM 1 用户问题 "最近 30 天服装品类店铺销量 Top 5" 2 三路并行召回 路 1:用户问题 BM25 + KNN → ES RRF 融合 路 2:同义词扩展后 → BM25 + KNN 路 3:LLM 抽标准名 → fuzziness AUTO 兜底 3 融合 + 去重 合并三路 → 截取 K=10 条候选 输出 K=10 个候选指标 / 维度(含相关度排序) 由下一步 LLM 在候选里做最终选择 问题:召回准准确度不高,"不支持的维度"等业务反馈难以直接给出 方案 2 · LLM 分类匹配 把治理后的元数据交给 LLM 直接选 1 选择数据集下标准指标与维度 指标名称 · 业务过程 · 度量 · 定义 · 同义词 · 适用维度 2 llm step 1:精准指标识别 输出 score + reason,阈值 score ≥ 0.8 语义优先,同义词与名称同权 3 召回指标相关的维度 维度名称、定义,样例 4 llm step 2:精准维度识别 支持/未被支持的维度 识别问题中的维值("服装") 标准指标维度重写需求 输出 已锁定的 指标 + 维度 + 关键词 附改写后问题 & 不支持维度说明,直送 DSL 组装 语义理解强 · 上下文感知 · 用户反馈友好

为什么选择方案 2:方案 1 走的是经典 RAG 思路——但在数据查询场景里这条路并不好走。用户自然语言表达的语义里夹杂大量噪声(口语化、修饰词、跨概念混说、错别字),而我们治理后的知识库语义相对干净(标准名 + 业务过程 + 度量 + 同义词的结构化字段)。要让"含噪查询"准确命中"干净知识",业界确实有不少优化手段:Query 改写(把口语化表达规范化)、答案假设(HyDE,先让模型生成假想答案再去检索)、多路召回 + LLM 重排 等。但这些方案叠起来后,工程链路明显变长,每一路的权重和打分都需要长期调优,整体实施成本偏高;我们在离线评测里把上述手段都拼齐,召回准确率也只做到了 60%左右,距离生产可用仍有差距。

切到方案 2 之后,思路反过来:让 LLM 直接在干净的知识库里做分类匹配。LLM 本身就具备很强的语义理解和噪声过滤能力——读问题时会自动忽略口语化与修饰,只抓核心意图;与此同时治理后的元数据已能放进 prompt,不需要先召回再选。于是"召回 + 选择"被合并成"一次决策",链路从三段变两步,可解释性反而更强,对后续维度白名单、unsupported 反馈、问题改写等业务能力的扩展也更友好。

语义理解与校验重试:从识别结果到合法 DSL

有了上一步选定的指标和维度准确召回,这一步就变得比较简单了,主要是让LLM转化成合理的DSL:

  • 组装:拿 LLM 输出的指标 / 维度 / 维值关键词,套上 schema 拼成 DSL。模型决策空间已经被前置的检索和白名单收得很窄,这一步几乎是机械组装。
  • 校验 + 错误注入重试:7项强制检查(Json格式、维度/指标至少存在一项、 非空、必填等);任何1项不过则将错误信息反馈进下一轮 prompt让LLM自动反思纠错。

维度值映射:把"服装"落到具体的库内值

要解决的问题:将用户口语化表达的修饰词转化为 DB 里实际存储的维度值。例如用户嘴里的"衣服、服装类、服饰",在 category_first_name 字段里其实统一存为「服装」。让模型猜会出错,最稳的办法是用检索 + 同义词字典做映射。

  • ① 精确匹配(含同义词扩展):把"服装"通过同义词字典扩成 [衣服, 服饰, 服装类],逐个尝试精确 match,命中即返回库内标准值「服装」
  • ② 模糊唯一命中:对 dim_name 走 BM25 模糊匹配(例如用户写"服裝"繁体、"服裝类"夹杂错字),仅当结果唯一时采用,避免把"服装"和"运动服"混淆
  • ③ 混合兜底:keyword(boost 10)+ match(boost 5)+ fuzziness AUTO(boost 2)三路加权求和,取 top1。例如"穿的"这种极口语化表达,靠模糊评分兜底回「服装」

这一层把"自然语言里的口语化值"翻译成数据库里实际存在的值——用户说"衣服卖得怎么样",最终落到 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%,瓶颈不在指标或维度本身,而集中在过滤条件的维值匹配(字面差异导致没有精确匹配到对应限定值)。后续将重点通过以下三个步骤持续进行优化:

  • 按维值重要度排序:引入维值的业务权重(销量贡献、出现频次、是否 P0 商品 / 店铺等),高价值维值优先进入候选集,避免被长尾噪声挤掉。
  • 按语义相似度召回:用 embedding 检索代替纯字面匹配,把"健身包粉色"与「干湿分离游泳健身包-雾粉色(升级款)」这类语义近似但字面差异大的维值召回到 Prompt 中,让大模型在受控候选里做最终选择。
  • 重要度 × 相似度 联合打分:两路结果加权融合后取 Top K 注入 Prompt——既保证高频核心维值不被漏掉,又能覆盖语义近邻的长尾值。