拒绝写 SQL!基于 LangGraph 打造企业级 Text-to-SQL 引擎:从星型模型到智能体工作流的硬核拆解
在企业数字化转型的深水区,始终横亘着一座难以逾越的“巴别塔”:业务人员手握业务直觉却受困于技术门槛,数据工程师深陷重复取数的泥沼而无暇顾及深层分析。掌柜问数并非简单的“文本转SQL”工具,而是一套试图通过 Agentic Workflow(智能体工作流) 重构数据交互范式的工程实践。本文将摒弃教科书式的架构罗列,转而从“数据语义化”与“流程可控性”两个维度,深度剖析掌柜问数的核心设计,并结合 La
不只是调包!深度揭秘数据仓库 + 向量索引 + 流式校验的生产级落地
一、 架构哲学:从“人找数”到“数找人”的范式转移
传统的 BI 工具本质是“人找数”,用户需要理解表结构、关联逻辑和 SQL 语法。掌柜问数的核心哲学是“数找人”,其架构设计围绕三个核心解耦展开:
- 存储与计算解耦:数仓负责高效存储与计算,Agent 负责逻辑翻译。
- 结构与语义解耦:MySQL 存结构,向量库存语义,解决“字段名是英文,业务说中文”的矛盾。
- 生成与校验解耦: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 = "华东"。
- 实现:
- ES:建立
table_name,column_name,column_value的精确匹配索引。 - Qdrant:对字段的
comment、description以及历史高频查询日志进行向量化。 - Reciprocal Rank Fusion (RRF):融合两种检索结果,确保既准又全。
- ES:建立
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:聚合更多推荐



所有评论(0)