电商问数-知识整理

数据仓库:

  1. 它存的是来自业务系统的大量历史数据。
  2. 它主要服务于统计分析,而不是直接服务于线上交易。
  3. 它的数据组织方式,通常会为了“分析方便”而重新设计。

数据仓库并不是把业务数据库原封不动搬过来,而是会在同步过程中进行整理、建模和加工,让后续分析更高效、更清晰。

维度建模:

建模方式 主要面向什么场景 核心特点 适不适合本项目
ER 建模 / 规范化建模 业务数据库、交易系统 强调减少冗余、保证一致性,典型是 3NF 设计 不太适合作为问数系统的主模型
维度建模 数据仓库、报表分析、BI、问数系统 把表拆成事实表和维度表,查询思路清晰 最适合本项目
Data Vault 建模 企业级数仓底层、复杂多源接入 强调可追溯、可扩展、便于持续接入新数据 更适合底层建设,不适合作为入门主线
宽表建模 / 反规范化建模 固定报表、高频分析查询 把常用字段尽量提前摊平,减少查询时的 join 可作为补充手段,但不是本项目重点

项目主线: 数据仓库负责存放分析数据,元数据知识库负责帮助智能体理解这些数据,智能体再基于两者完成查数。

系统最终要做到的,不是泛泛而谈地“解释问题”,而是真正生成可以执行的 SQL,并给出可靠结果。这就是“问数智能体”和普通聊天助手最大的区别

系统最小链路:

  1. 用户先提一个自然语言问题,例如“统计华北地区的销售总额”。
  2. 系统先从元数据知识库里找出和这个问题最相关的字段、字段值和指标定义。
  3. 大模型基于这些上下文生成 SQL。
  4. 系统先校验 SQL,必要时再自动纠错。
  5. SQL 通过后再真正执行,并把查询结果返回给用户。

 服务分类:

基础服务 当前项目使用什么 主要作用
数据仓库 / 元数据库 MySQL 在当前项目里,MySQL 同时承担两类角色:一套是教学数仓库 dw,负责存放分析数据;另一套是 meta 元数据库,负责保存 table_infocolumn_infometric_infocolumn_metric 等结构化元数据
向量数据库 Qdrant 保存字段信息和指标信息的向量,用于语义相似度检索
全文检索服务 Elasticsearch 保存字段取值索引,用于把“华北”“黄金会员”这类自然语言值映射到真实字段取值
Embedding 服务 TEI + BAAI/bge-large-zh-v1.5 负责把字段名、字段描述、指标描述、用户问题等文本转成向量,供 Qdrant 检索使用
智能体编排框架 LangGraph 负责把关键词抽取、多路召回、合并过滤、生成 SQL、校验 SQL、执行 SQL 串成可分步骤运行的工作流

概括为:MySQL 管结构化存储,Qdrant 管语义召回,Elasticsearch 管字段取值检索,TEI 负责向量化,LangGraph 负责把整条问数流程组织起来

两条主线: 

  1. 构建期:先把知识准备好
    1. 配置文件先告诉同步脚本:这次要纳入元数据知识库的有哪些表、哪些字段、哪些指标。
    2. 同步脚本再结合配置文件和数据仓库查询结果,组织出最终需要构建的元数据内容。其中,表和字段的业务语义、角色、别名等信息主要来自配置文件;字段类型、字段示例值以及部分真实取值,则由脚本再到数据仓库中查询补齐。
    3. 这些内容不会直接交给大模型,而是会先被整理并写入元数据知识库。
    4. 写入时会分三路落地: meta MySQL 保存结构化元数据,Qdrant 保存字段和指标经过向量化后的语义索引,Elasticsearch 保存部分字段的真实取值,方便后续全文检索。
  2. 查询期:再拿这些知识去回答问题
    1. 用户先提一个自然语言问题,比如“统计华北地区的销售总额”。
    2. 问数智能体先理解问题,再到元数据知识库里召回相关字段、相关指标和相关字段取值。
    3. 系统把这些召回结果整理成更适合大模型理解的上下文。
    4. 大模型基于这些上下文生成 SQL。
    5. 系统再做 SQL 校验,必要时纠错。
    6. SQL 通过后,才会真正到数据仓库执行,并把结果返回给用户。

如果你更喜欢用“模块 + 技术栈”的方式去记,可以直接看下面这张表:

模块 作用 当前项目使用什么
数据仓库 存放真实分析数据 MySQL(教学环境中用来模拟数仓)
同步脚本 从数据仓库抽取元数据 Python 脚本 + 配置驱动
元数据库 保存结构化元数据 MySQL
向量检索 召回相关字段和指标 Qdrant
全文检索 检索字段真实取值 Elasticsearch
向量化服务 把字段描述、指标描述、用户问题转成向量 TEI + BAAI/bge-large-zh-v1.5
智能体编排 把召回、过滤、生成 SQL、校验、执行串起来 LangGraph

元知识数据库:

  元数据知识库存的是“有哪些表和字段、字段是什么意思、字段可能有哪些取值、指标怎么定义”这类关于数据的数据

  元数据库不等于元数据知识库,但它是元数据知识库的一部分。

在当前项目里,元数据知识库并不是某一个单独的库,而是由下面三部分协作组成的:

  • 元数据库:用 MySQL 保存最完整、最权威的结构化元数据。
    •   负责存放完整、权威的结构化元数据。它不是业务数据本身,而是“关于数据的数据”。在当前项目中,这部分主要放在 MySQL 里,并包含四张核心表,用于保存数据仓库中的表信息、字段信息、指标信息以及字段与指标之间的关联关系
    • 同步脚本:在实际工程里,元数据知识库不是手工一点点录入的,而是要通过同步脚本来构建和维护。
      • 读取配置文件,确定需要同步哪些表、哪些字段、哪些指标
      • 从数据仓库中读取对应表结构、字段类型和部分字段取值示例
      • 将完整元数据写入 MySQL
      • 为字段信息和指标信息构建向量索引
      • 为适合检索的字段取值构建全文索引
  • 向量索引:用 Qdrant 负责字段和指标的语义召回。
    • 把元数据库中部分字段信息和指标信息进一步构建成语义向量索引,让系统具备“语义相近也能找到”的能力。
    • 主要作用于两类信息:column_info,也就是字段信息;metric_info,也就是指标信息。
    • “召回率”可以简单理解为:对于一个用户问题,本来真正相关的字段或指标有若干个,如果系统能够把这些相关对象尽可能多地找出来,召回率就高;
  • 全文索引:用 Elasticsearch 负责字段取值的关键词匹配。
    • 为了让系统能把自然语言中的这些描述准确映射到数据库中的真实取值,我们还需要建立全文索引。
    • 并不是所有字段都适合做全文索引。一般来说,全文索引主要针对的是“文本类型的维度字段”,因为这类字段最有可能作为过滤条件出现

所以元数据知识库的作用,并不是直接替大模型生成 SQL,而是在生成 SQL 之前,先把“应该关注哪些表、哪些字段、哪些取值、哪些指标”准备好。没有这一步,后续 SQL 生成就很容易不稳定

问数智能体

  真正把用户问题变成查询结果的,是项目中的问数智能体。

为了先抓住主线,也可以把这一整段流程先压缩成 3 个阶段:

  1. 理解问题 先从用户问题里提取适合检索的关键词,并保留原始问题作为语义兜底。
  2. 准备 SQL 所需上下文 先粗召回相关字段、指标和值,再把它们整理、补齐、过滤成更适合生成 SQL 的上下文。
  3. 生成并执行 SQL 在上下文齐备后生成 SQL,再做校验、必要时纠错,最后真正执行。

1. 抽取关键词

在实现上,这一步会借助分词和关键词提取工具来完成. 从用户问题中尽量提炼出真正对数据库查询有帮助的核心词,把那些无关紧要、干扰检索的词先过滤掉,从而让后面的字段召回、指标召回和字段取值检索更准确

提取关键词的意义,在于先把问题中最适合检索的核心词提炼出来,提高后续召回的效率和准确性;而保留原始问题,则是为了避免关键词抽取过程中遗漏语义细节,作为完整语义的兜底信息。也就是说,关键词和原始问题并不是二选一的关系,而是“一个负责聚焦检索,一个负责补充完整语义”

2. 并行召回三类信息

  • 召回字段信息
  • 召回指标信息
  • 召回字段取值

把这一步看作一次粗召回。也就是说,这一步的目标不是立刻得到最终答案,而是先尽量把“可能相关的材料”多找一些出来,宁可先宽一点,也不要一开始就漏掉真正相关的信息

  • 召回字段信息
    •   image
    • 这一步主要去向量索引中检索与问题最相关的字段。比如问题里有“地区”“销售总额”这类表达,系统就要尽量把 region_nameorder_amount 等可能相关的字段找出来。
    • 为了提高召回效果,系统并不只依赖最初抽出来的关键词,还会借助大模型进一步做“关键词扩展”。也就是说,系统会让模型从“我要查字段信息”这个目标出发,补充一些更适合用于字段检索的词,例如把“销售总额”扩展成“销售金额”之类的表达。这样做的本质,是为了提高字段召回率  
  • 召回指标信息
    •   image
    • 这一步和字段召回类似,也是走向量检索,只不过目标换成了 metric_info 中的业务指标
    • 这里同样会做关键词扩展。因为用户问题里的说法,未必和指标名一模一样。通过扩展关键词,系统能更容易把“问题的说法”和“元数据里的指标定义”对上。
  • 召回字段取值
    •   image
    • 这一步主要走全文检索,目标是找到问题中提到的那些真实字段取值。
    • 这些信息不是字段名,而是字段里存的值,所以要去全文索引里查
    • 字段取值检索其实做成“全文检索 + 向量检索”的混合检索会更好,因为有些场景可能存在别称、拼音、不同书写方式等问题。但在当前项目中,为了先把主流程做清楚,这一步做了简化,主要采用的是全文检索

3. 合并召回信息

三路粗召回完成后,系统拿到的是三份相对分散的结果:

  • 一批字段信息
  • 一批指标信息
  • 一批字段取值

这些信息此时还不能直接丢给大模型。因为它们是散的、碎的、不成体系的,而大模型在生成 SQL 时更需要的是结构化、成组的信息。

一步的核心目标,是把零散的召回结果整理成两份更适合后续使用的上下文:

  • 表信息及其字段信息
  • 指标信息及其相关字段说明

这里要特别理解几个关键动作:

  1. 召回到字段之后,可以根据字段里的 table_id 把字段重新按表归组,整理成“某张表有哪些可用字段”的结构。
  2. 召回到指标之后,要把指标相关字段也考虑进来,因为一个指标往往依赖一个或多个字段。
  3. 召回到字段取值之后,要把这些值重新挂回到它所属字段上,让字段的 examples 或候选取值信息更完整。

当这些信息被整理好之后,表信息通常会被组织成类似 yaml/table_infos.yaml 这样的结构。也就是说,系统并不是把“零散字段列表”直接交给大模型,而是把它们先整理成“按表分组、表下挂字段”的层次化信息

好处: 

  • 大模型能一眼看出有哪些表
  • 每张表是什么角色,是事实表还是维度表
  • 每张表下面有哪些字段
  • 每个字段的类型、角色、示例值、描述和别名分别是什么

4. 过滤指标信息和表格信息

  即使经过合并,当前拿到的信息也不一定就是最终最合适的结果。因为前一步的目标是“先组织起来”,而不是“已经足够精确”,

  • 有些字段、表、指标虽然被召回了,但和当前问题关系并不强
  • 有些字段是为了补全主外键关系被带进来的,但本身并不是问题关注的重点

因此,流程图接下来又分成两步筛选:

  • 过滤指标信息
  • 过滤表格信息

这两步本质上都是一次精筛。系统会借助大模型,结合用户原始问题,对召回结果再做一次语义判断,把无关项、冗余项剔除掉,只保留和当前查询意图最相关的表、字段和指标。

这一步非常重要,因为前面的召回更像是“宁可多召回一些,也不要漏太多”,而这里则是把“多出来的部分”再清洗一遍。先粗召回,再结构化整理,最后精筛保留。

5. 过增加额外上下文

过滤之后,系统已经拿到了比较干净的表信息、字段信息和指标信息,但在真正生成 SQL 之前,流程图里还有一步“增加额外上下文”。这一步做的事情是:在“业务上下文”已经齐备之后,再把“环境上下文”补齐

这一步补充的,主要是一些不直接属于表结构,但会影响 SQL 正确性的环境信息,例如:

  • 当前时间
  • 当前数据库类型
  • 当前数据库版本

这一步看起来不起眼,其实很实用。例如,用户如果问“统计去年的销售总额”或者“统计上个月的销售额”,那模型必须知道“当前时间”是什么,才有可能正确推导出时间范围。

6. 生成SQL

到这一步,系统终于具备了生成 SQL 所需的主要上下文:

  • 用户原始问题
  • 经过筛选后的表信息、字段信息和相关指标说明
  • 可能需要的字段取值,以及当前时间、数据库环境等补充上下文

这时再把这些信息一起交给大模型,才算真正具备了较高质量生成 SQL 的基础。

7. 校验SQL

SQL 生成出来之后,流程图并没有立刻执行,而是先进入“校验 SQL”。这一步的目的,是在真正执行之前先做一次预检查,尽早发现语法问题或明显错误。

8. 校正SQL

如果“校验 SQL”通过,流程图就会直接往下执行。如果校验失败,流程图右侧会走到“校正 SQL”。这一步会把前面生成 SQL 时用到的上下文,再加上数据库返回的报错信息,一起重新交给大模型,让它基于错误信息去修正这条 SQL。

9. 执行SQL

 

 

路径相关知识

config_file = Path(__file__).parents[2] / "conf" / "app_config.yaml"

它里面包含了三个知识点:绝对路径、相对路径、pathlib 路径操作

__file__ 是 Python 提供的一个特殊变量,它表示当前这个 Python 文件自身的路径

例如在 app/conf/app_config.py 里,__file__ 大致会是:

/Users/tools/Desktop/agent/shopkeeper-agent/app/conf/app_config.py
Path(__file__)
# 表示把当前文件路径包装成一个 Path 对象。
# 这样后面就可以更自然地做“找父目录”“拼子目录”这些操作。
parents[0] # 表示上一级目录
parents[1] #表示上两级目录
parents[2] # 表示上三级目录

 

posted @ 2026-05-21 10:55  幻影之舞  阅读(44)  评论(0)    收藏  举报