不只是调包!深度揭秘数据仓库 + 向量索引 + 流式校验的生产级落地

一、 架构哲学:从“人找数”到“数找人”的范式转移

传统的 BI 工具本质是“人找数”,用户需要理解表结构、关联逻辑和 SQL 语法。掌柜问数的核心哲学是“数找人”,其架构设计围绕三个核心解耦展开:

  1. 存储与计算解耦:数仓负责高效存储与计算,Agent 负责逻辑翻译。
  2. 结构与语义解耦:MySQL 存结构,向量库存语义,解决“字段名是英文,业务说中文”的矛盾。
  3. 生成与校验解耦:LLM 负责生成,规则引擎负责校验,确保生产环境安全。

整体架构图(概念版)


mermaid

1graph TD
2    User[业务用户] -->|自然语言提问| Gateway[API Gateway]
3    Gateway --> Agent[问数智能体 (LangGraph)]
4    
5    subgraph "大脑:LangGraph Workflow"
6        Agent --> Parser[意图解析与槽位填充]
7        Parser --> Retriever[多路元数据召回]
8        Retriever --> Generator[SQL 生成器]
9        Generator --> Validator[安全与逻辑校验]
10        Validator --> Executor[SQL 执行引擎]
11    end
12    
13    subgraph "记忆:元数据知识库"
14        Retriever -->|向量检索| Qdrant[Qdrant 向量库]
15        Retriever -->|关键词检索| ES[Elasticsearch]
16        Retriever -->|结构化查询| MySQL[MySQL 元数据库]
17    end
18    
19    subgraph "基石:星型数仓"
20        Executor -->|查询| DW[Data Warehouse]
21        DW -->|事实表| Fact[fact_order]
22        DW -->|维度表| Dim1[dim_region]
23        DW -->|维度表| Dim2[dim_customer]
24    end
25    
26    Executor -->|结果| Formatter[数据可视化渲染]
27    Formatter -->|报表/图表| User
28

二、 数仓建模:不仅仅是星型,更是“维度建模的艺术”

数仓是地基,但地基不仅要稳,还要“好懂”。我们在经典星型模型基础上做了业务化增强

1. 事实表设计:退化维度与快照

除了基础的 fact_order,我们引入了周期快照事实表 fact_order_monthly_snapshot

  • 痛点:用户常问“截止到昨天的累计销售额”,直接查询明细事实表性能极差。
  • 解法:预计算每日/每月的汇总数据,Agent 根据时间粒度自动选择查快照表还是明细表。

2. 维度表设计:规范化与层级注入

  • 层级注入(Hierarchy Injection):在 dim_region 中不仅存 province,还预计算了 province -> city -> district 的 JSON 路径。当用户问“华南地区”时,Agent 能自动展开为 province IN ('广东', '广西', '海南'),无需 LLM 推理省份归属。

3. 优化后的 DDL 示例


sql

1-- 事实表:增加冗余字段以减少 JOIN
2CREATE TABLE fact_order (
3    order_id BIGINT PRIMARY KEY,
4    customer_id BIGINT,
5    product_id BIGINT,
6    region_id BIGINT,
7    date_id INT,
8    -- 冗余常用维度字段,避免高频 JOIN
9    customer_level VARCHAR(20), -- 客户等级(来自dim_customer)
10    product_category VARCHAR(50), -- 商品大类(来自dim_product)
11    region_name VARCHAR(50), -- 省份名(来自dim_region)
12    order_amount DECIMAL(18, 2),
13    order_quantity INT
14);
15
16-- 时间维度:增加业务周期标识
17CREATE TABLE dim_date (
18    date_id INT PRIMARY KEY,
19    calendar_date DATE,
20    year INT,
21    quarter VARCHAR(10),
22    month INT,
23    -- 业务特有标识
24    is_fiscal_year_end BOOLEAN, -- 是否财年末
25    is_promotion_day BOOLEAN -- 是否大促日
26);
27

架构思考:这种空间换时间冗余设计,本质上是为了降低 Agent 的推理负担。LLM 不擅长复杂的多表关联逻辑,我们通过数仓设计把“多表 JOIN”变成“单表过滤”,大幅提升生成准确率。


三、 元数据知识库:构建数据的“语义大脑”

这是掌柜问数最核心的护城河。单纯把 Schema 扔给 LLM 等于让盲人摸象。我们需要构建一个多模态、分层级的元数据索引体系。

1. 元数据的“三层结构”

  • L1 - 物理层:表名、字段名、数据类型(存 MySQL)。
  • L2 - 逻辑层:业务口径、计算逻辑、指标定义(存 MySQL + ES)。
    • 例如:order_amount 在逻辑层定义为“实付金额 = 商品金额 - 优惠券金额 + 运费”。
  • L3 - 语义层:同义词、业务场景、QA 对(存 Qdrant + MySQL)。
    • 例如:用户问“卖了多少钱”,语义层映射到 order_amount;问“退货率”,映射到 refund_count / order_count

2. 混合索引策略:为什么不只用向量库?

纯粹的向量检索存在“幻觉”“精确匹配失效”问题。

  • 场景:用户问“华东地区的销售”。
    • 向量检索:可能召回“华南”因为语义相似。
    • 全文检索(ES):精确命中 region_name = "华东"
  • 实现
    1. ES:建立 table_namecolumn_namecolumn_value 的精确匹配索引。
    2. Qdrant:对字段的 commentdescription 以及历史高频查询日志进行向量化。
    3. Reciprocal Rank Fusion (RRF):融合两种检索结果,确保既准又全。

3. 动态元数据维护

  • 痛点:数仓表结构变更后,Agent 不知道,导致生成错误 SQL。
  • 解法:监听数据库 Binlog 或定期跑 DESCRIBE TABLE,自动更新 MySQL 元数据,并触发向量索引的增量更新。

四、 问数智能体:基于 LangGraph 的可控生成流水线

这是本文的重头戏。我们拒绝“一把梭”的黑盒生成,而是将过程拆解为有向无环图(DAG),每个节点职责单一。

4.1 LangGraph 工作流设计

我们将工作流定义为 StateGraph,状态对象包含:


python

1class QueryState(TypedDict):
2    user_question: str          # 原始问题
3    dialect: str                # 数据库方言 (MySQL/PostgreSQL)
4    context: list[dict]         # 历史对话
5    retrieved_metadata: list     # 召回的元数据
6    candidate_sql: str          # 生成的SQL
7    validated_sql: str          # 校验后的SQL
8    execution_result: dict      # 执行结果
9    error_log: str              # 错误日志
10    final_answer: str           # 最终自然语言回答
11

4.2 核心节点实现与优化

节点 1:Query Router(问题路由)

不是所有问题都需要查数仓。


python

1def route_query(state: QueryState):
2    question = state["user_question"].lower()
3    if "你好" in question or "谢谢" in question:
4        return "greeting"
5    if "表结构" in question or "有哪些字段" in question:
6        return "metadata_lookup"
7    return "data_query"
8
节点 2:Contextual Retriever(带上下文的召回器)

独特见解:召回不仅仅看当前问题,还要看用户身份历史上下文


python

1def enhanced_retriever(state: QueryState):
2    user_id = state.get("user_id")
3    question = state["user_question"]
4    
5    # 1. 基于部门权限过滤表(RLS)
6    allowed_tables = get_tables_by_department(user_id) 
7    
8    # 2. 基于历史对话补全意图
9    # 如果上一句问了“2025年”,这一句问“上个月”,自动补全为“2025年2月”
10    refined_question = resolve_context(question, state["context"])
11    
12    # 3. 多路召回
13    vector_results = qdrant_client.search(refined_question, filter=allowed_tables)
14    keyword_results = es_client.search(refined_question)
15    
16    # 4. 结果去重与排序
17    merged_results = merge_and_rank(vector_results, keyword_results)
18    
19    return {"retrieved_metadata": merged_results}
20
节点 3:SQL Generator with Few-Shot(少样本生成)

工程优化:直接让 LLM 写复杂 SQL 容易出错。我们采用 Few-Shot + Chain-of-Thought 策略。

Prompt 模板优化


markdown

1# Role
2你是资深的数据分析师,擅长编写高性能的 {dialect} SQL。
3
4# Context
5以下是相关的表结构和字段含义:
6{schema_context}
7
8# Rules
91. 优先使用事实表中的冗余字段,避免不必要的 JOIN。
102. 日期过滤必须使用分区字段 date_id。
113. 金额字段保留 2 位小数。
124. 严禁使用 SELECT *。
13
14# Examples (Few-Shot)
15Q: 2025年Q1华东区销售额
16A:
17-- 思考:用户需要按区域汇总销售额,时间范围是2025年Q1,区域是华东。
18-- 步骤1:确定事实表 fact_order,维度表 dim_region。
19-- 步骤2:利用冗余字段 region_name 过滤,利用 date_id 关联时间维表。
20-- 步骤3:聚合
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐