构建企业级 Text-to-SQL Agent:基于 LangGraph 的智能数据查询系统设计
Posted on 2026-08-06 13:05 work hard work smart 阅读(9) 评论(0) 收藏 举报构建企业级 Text-to-SQL Agent:基于 LangGraph 的智能数据查询系统设计
摘要:本文详细介绍了一个基于 LangGraph 构建的企业级 Text-to-SQL Agent 系统的架构设计。该系统通过多路召回、RAG(检索增强生成)、智能过滤和自修正机制,实现了从自然语言到 SQL 的高质量转换。文章将深入剖析系统的分层架构、各中间件的作用、核心流程设计以及关键技术创新。
目录
一、项目概述
1.1 背景与挑战
在企业数据分析场景中,业务人员经常需要从数据仓库中查询数据。然而,SQL 的技术门槛使得这一过程依赖专业的数据分析师或开发工程师,导致:
- 效率瓶颈:简单的数据查询也需要排期等待
- 人力成本:数据团队大量时间被重复性查询占据
- 响应延迟:无法实现即时的数据探索和分析
Text-to-SQL 技术应运而生,但现有的开源方案(如 Vanna)在企业级场景中面临诸多挑战:
- 复杂的业务指标体系难以表达
- 大型数据仓库的元数据管理困难
- 单一召回策略导致准确率不足
- 缺乏 SQL 校验和自修正机制
1.2 项目目标
本项目旨在构建一个生产级、企业级的 AI 数据查询助手,具备以下能力:
- ✅ 支持自然语言查询,零 SQL 基础即可使用
- ✅ 多路召回策略,提升复杂查询的准确率
- ✅ 智能上下文过滤,适应大规模数据仓库
- ✅ SQL 自动校验与修正,提高执行成功率
- ✅ 完整的业务指标管理,支持复杂口径定义
1.3 技术栈概览
| 技术组件 | 选型 | 作用 |
|---|---|---|
| Web 框架 | FastAPI | HTTP API 服务 |
| Agent 框架 | LangGraph | 工作流编排与状态管理 |
| LLM | Qwen-Plus(阿里云) | 关键词扩展、SQL 生成、智能过滤 |
| Embedding | BGE-Large-ZH | 中文语义向量化 |
| 向量数据库 | Qdrant | 字段和指标的语义检索 |
| 全文检索 | Elasticsearch | 字段取值的全文匹配 |
| 元数据库 | MySQL | 存储表结构、字段、指标定义 |
| 数据仓库 | MySQL | 存储实际业务数据 |
| 分词工具 | Jieba | 中文关键词提取 |
二、系统架构设计
2.1 整体架构
系统采用分层架构 + Agent 工作流的设计模式:
┌─────────────────────────────────────────────────────────────┐
│ 应用层 (FastAPI) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Query Router → 请求处理 → 响应返回 │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Agent 层 (LangGraph 工作流) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 关键词抽取 → 多路召回 → 合并 → 过滤 → 生成 → 校验 → 执行 │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 服务层 (Service) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ MetaKnowledgeService(元知识构建与管理) │ │
│ └──────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 数据访问层 (Repository) │
│ ┌──────────┬──────────┬──────────┬──────────────────┐ │
│ │ Column │ Metric │ Value │ Meta/DW MySQL │ │
│ │ Qdrant │ Qdrant │ ES │ Repository │ │
│ └──────────┴──────────┴──────────┴──────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │
│ ┌──────────┬──────────┬──────────┬──────────┬──────────┐ │
│ │ MySQL │ Qdrant │ Elastic- │ Embedding│ LLM API │ │
│ │ (元数据) │ (向量库) │ search │ Service │ │ │
│ └──────────┴──────────┴──────────┴──────────┴──────────┘ │
└─────────────────────────────────────────────────────────────┘
2.2 核心设计模式
状态机模式(LangGraph)
系统使用 LangGraph 的状态图(StateGraph)管理工作流:
# 状态定义
class DataAgentState(TypedDict):
query: str # 用户查询
keywords: list[str] # 提取的关键词
retrieved_column_infos: list # 召回的字段
retrieved_metric_infos: list # 召回的指标
retrieved_value_infos: list # 召回的取值
table_infos: list # 过滤后的表信息
metric_infos: list # 过滤后的指标信息
sql: str # 生成的 SQL
error: str # 校验错误信息
# 工作流图
graph_builder.add_edge(START, "extract_keywords")
graph_builder.add_edge("extract_keywords", "recall_column") # 并行
graph_builder.add_edge("extract_keywords", "recall_value") # 并行
graph_builder.add_edge("extract_keywords", "recall_metric") # 并行
graph_builder.add_edge("recall_column", "merge_retrieved_info")
graph_builder.add_edge("recall_value", "merge_retrieved_info")
graph_builder.add_edge("recall_metric", "merge_retrieved_info")
graph_builder.add_edge("merge_retrieved_info", "filter_table")
graph_builder.add_edge("merge_retrieved_info", "filter_metric")
graph_builder.add_edge("filter_table", "add_extra_context")
graph_builder.add_edge("filter_metric", "add_extra_context")
graph_builder.add_edge("add_extra_context", "generate_sql")
graph_builder.add_edge("generate_sql", "validate_sql")
# 条件边:校验成功执行,失败校正
graph_builder.add_conditional_edges("validate_sql",
path=lambda state: "run_sql" if state['error'] is None else "correct_sql")
graph_builder.add_edge("correct_sql", "run_sql")
graph_builder.add_edge("run_sql", END)
依赖注入模式
通过 Context 对象注入所有外部依赖:
class DataAgentContext(TypedDict):
column_qdrant_repository: ColumnQdrantRepository
metric_qdrant_repository: MetricQdrantRepository
value_es_repository: ValueESRepository
embedding_client: HuggingFaceEndpointEmbeddings
meta_mysql_repository: MetaMySQLRepository
dw_mysql_repository: DWMySQLRepository
优势:
- 清晰的依赖关系
- 易于单元测试(可 Mock)
- 灵活替换实现
Repository 模式
数据访问层抽象,解耦业务逻辑和数据源:
# 向量检索抽象
class ColumnQdrantRepository:
async def search(self, embedding, threshold=0.6, limit=20) -> list[ColumnInfo]
async def upsert(self, ids, embeddings, payloads)
# 全文检索抽象
class ValueESRepository:
async def search(self, keyword) -> list[ValueInfo]
async def index(self, value_infos)
# 关系型数据库抽象
class MetaMySQLRepository:
async def get_table_info_by_id(self, table_id) -> TableInfo
async def get_column_info_by_id(self, column_id) -> ColumnInfo
三、核心中间件解析
3.1 MySQL(元数据库)
作用:存储结构化的元数据信息
存储内容:
-- 表信息表
table_info (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255), -- 表名
role VARCHAR(50), -- 表角色(dim/fact)
description TEXT -- 表描述
)
-- 字段信息表
column_info (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255), -- 字段名
type VARCHAR(50), -- 数据类型
role VARCHAR(50), -- 字段角色(primary_key/dimension/measure/foreign_key)
examples JSON, -- 取值示例
description TEXT, -- 字段描述
alias JSON, -- 别名列表
table_id VARCHAR(255) -- 关联表
)
-- 指标信息表
metric_info (
id VARCHAR(255) PRIMARY KEY,
name VARCHAR(255), -- 指标名
description TEXT, -- 指标描述
alias JSON -- 别名列表
)
-- 指标-字段关联表
column_metric (
column_id VARCHAR(255),
metric_id VARCHAR(255)
)
设计亮点:
- 字段的角色分类(维度、度量、主键、外键)帮助 LLM 理解数据结构
- 别名列表增强语义匹配的召回率
- 示例值帮助 LLM 理解字段的实际取值
3.2 Qdrant(向量数据库)
作用:提供字段和指标的语义检索能力
为什么需要向量检索?
用户查询:"统计华北地区的销售总额"
传统的关键词匹配可能无法识别:
- "销售总额" = "GMV" = "成交总额"
- "华北地区" = "region_name"
解决方案:将字段信息向量化,通过语义相似度检索
集合设计:
# 字段向量集合
collection_name = "column_info_collection"
vectors_config = VectorParams(
size=1024, # BGE-Large-ZH 的向量维度
distance=Distance.COSINE # 余弦相似度
)
# 存储结构
PointStruct(
id=uuid,
vector=[0.123, 0.456, ...], # 1024 维向量
payload={
"id": "fact_order.order_amount",
"name": "order_amount",
"type": "DECIMAL",
"role": "measure",
"examples": [100.50, 200.00],
"description": "订单金额",
"alias": ["销售额", "订单金额", "收入"],
"table_id": "fact_order"
}
)
多路向量化策略:
同一个字段会被多次向量化,提升召回率:
# 以 order_amount 字段为例
向量化点:
1. 字段名:"order_amount"
2. 字段描述:"订单金额"
3. 每个别名:"销售额"、"订单金额"、"收入"
# 所有这些向量都指向同一个字段信息(payload 相同)
检索流程:
async def search(self, embedding, score_threshold=0.6, limit=20):
result = await self.client.query_points(
collection_name="column_info_collection",
query=embedding, # 查询词的向量
limit=limit, # Top 20
score_threshold=threshold # 相似度阈值 0.6
)
return [ColumnInfo(**point.payload) for point in result.points]
3.3 Elasticsearch(全文检索引擎)
作用:存储和检索字段的实际取值
使用场景:
用户查询中的具体值(如"华北地区"、"张三"、"Q3")需要通过全文匹配找到对应的字段。
索引设计:
# ES 索引结构
index_name = "data_agent"
mappings = {
"properties": {
"id": {"type": "keyword"},
"value": {
"type": "text",
"analyzer": "ik_max_word" # IK 分词器(中文)
},
"column_id": {"type": "keyword"}
}
}
# 文档示例
{
"id": "fact_order.region_name.华北地区",
"value": "华北地区",
"column_id": "fact_order.region_name"
}
为什么用 ES 而不是 Qdrant?
| 维度 | Elasticsearch | Qdrant |
|---|---|---|
| 检索方式 | 全文匹配(倒排索引) | 向量相似度 |
| 适用场景 | 精确值匹配 | 语义相似度 |
| 查询"华北" | ✅ 分词匹配 | ❌ 需要向量化 |
| 性能 | 亿级数据毫秒级 | 百万级优秀 |
| 分词支持 | IK 中文分词 | 无 |
典型流程:
用户查询:"华北地区的销售额"
↓
关键词提取:["华北地区", "销售额"]
↓
ES 检索 "华北地区":
- 匹配到 value="华北地区"
- 返回 column_id="dim_region.region_name"
↓
知道"华北地区"对应 region_name 字段
3.4 Embedding Service(向量化服务)
作用:将文本转换为向量表示
技术选型:BAAI/bge-large-zh-v1.5
为什么选择这个模型?
- ✅ 中文优化(北京智源研究院)
- ✅ 1024 维向量(平衡精度和性能)
- ✅ 开源可本地部署(数据隐私)
- ✅ MTEB 榜单前列
部署方式:
# Docker Compose
embedding:
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8
ports:
- "8081:80"
environment:
MODEL_ID: /models/bge-large-zh-v1.5
MAX_CONCURRENT_REQUESTS: "16"
MAX_BATCH_TOKENS: "16384"
volumes:
- ./embedding/bge-large-zh-v1.5:/models/bge-large-zh-v1.5
使用示例:
# 批量向量化(提升效率)
embeddings = await embedding_client.aembed_documents([
"order_amount",
"订单金额",
"销售额",
"收入"
])
# 返回 4 个 1024 维向量
3.5 LLM(大语言模型)
作用:关键词扩展、智能过滤、SQL 生成、错误修正
技术选型:阿里云 Qwen-Plus
配置:
llm:
model_name: qwen-plus
base_url: https://dashscope.aliyuncs.com/compatible-mode/v1
api_key: sk-xxx
temperature: 0 # 确定性输出
为什么 temperature=0?
Text-to-SQL 需要确定性和可重复性,不需要创造性。相同的输入应该产生相同的 SQL。
LLM 在系统中的 4 个关键作用:
1. 关键词扩展
# 输入
query = "统计华北地区 Q3 的 GMV"
# LLM 输出
extended_keywords = [
"华北", "华东", "华南", # 地区扩展
"Q3", "第三季度", "7月", "8月", "9月", # 时间扩展
"GMV", "成交总额", "订单总额", "销售总额", # 指标扩展
"同比增长", "环比" # 分析类型
]
2. 智能过滤表信息
# 输入:召回了 50 个字段,涉及 10 张表
# LLM 过滤后:只保留 5 张表的 15 个相关字段
filter_table_info_prompt = """
根据用户查询,从以下表信息中筛选出相关的表和字段:
用户查询:{query}
可用表信息:{table_infos}
输出 JSON 格式:
{{
"fact_order": ["order_amount", "order_quantity", "date_id"],
"dim_region": ["region_name", "province"]
}}
"""
3. SQL 生成
# 详见第五节:Prompt 工程设计
4. SQL 校正
# 当 SQL 校验失败时
correct_sql_prompt = """
生成的 SQL 存在错误:
【错误信息】
{error}
【生成的 SQL】
{sql}
请根据错误信息修正 SQL。
"""
3.6 FastAPI(Web 框架)
作用:提供 HTTP API 服务
核心设计:
from fastapi import FastAPI
app = FastAPI(lifespan=lifespan)
# 请求 ID 中间件(用于日志追踪)
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
request_id = uuid.uuid4()
request_id_context_var.set(request_id)
response = await call_next(request)
return response
# 查询路由
@app.post("/api/query")
async def query(request: QueryRequest):
# 调用 LangGraph 工作流
async for chunk in graph.astream(input=state, context=context):
yield chunk
优势:
- 异步原生支持(与 LangGraph 异步工作流完美配合)
- 自动 API 文档生成
- 高性能(基于 Starlette)
四、完整工作流程
4.1 查询处理流程图
4.2 详细流程解析
Step 1: 关键词抽取与扩展
# 输入
query = "统计华北地区的销售总额"
# Jieba 分词(保留名词、动词、地名等)
keywords = jieba.analyse.extract_tags(query, allowPOS=["n", "ns", "v", "a"])
# 输出:["统计", "华北地区", "销售总额"]
# 添加原始查询
keywords = list(set(keywords + [query]))
# 输出:["统计", "华北地区", "销售总额", "统计华北地区的销售总额"]
# LLM 语义扩展(关键创新点!)
extended_keywords = llm_extend_keywords(query)
# 输出:["华北", "华东", "华南", "销售额", "GMV", "订单总额", "收入"]
# 合并关键词
final_keywords = keywords + extended_keywords
Step 2: 三路并行召回(基于扩展后的关键词)
2.1 字段召回(Qdrant)
# 1. LLM 扩展关键词(已在 Step 1 完成)
extended = ["华北", "华东", "销售额", "GMV", "订单金额"]
# 2. 向量化(原始关键词 + 扩展关键词)
embeddings = embedding_client.embed(final_keywords)
# 3. 向量检索
for embedding in embeddings:
columns = qdrant_repo.search(embedding, threshold=0.6, limit=20)
# 召回:region_name, order_amount, province 等
2.2 指标召回(Qdrant)
# 同样流程,检索指标信息
metrics = metric_qdrant_repo.search(embedding)
# 召回:GMV(成交总额)、AOV(平均订单金额)
2.3 取值召回(Elasticsearch)
# 全文检索字段取值
for keyword in keywords:
values = es_repo.search(keyword)
# 召回:{"value": "华北地区", "column_id": "dim_region.region_name"}
Step 3: 合并召回信息(查询 MySQL 元数据库)
这是系统的关键创新点,需要多次查询 MySQL 元数据库补全信息:
async def merge_retrieved_info():
meta_mysql_repository = runtime.context["meta_mysql_repository"]
# 1. 构建字段映射
column_map = {col.id: col for col in retrieved_columns}
# 2. 补全指标关联的字段(查询 MySQL)
for metric in retrieved_metrics:
for col_id in metric.relevant_columns:
if col_id not in column_map:
# 从 MySQL 查询字段详情
column_map[col_id] = await meta_mysql_repository.get_column_info_by_id(col_id)
# 3. 补全取值对应的字段(查询 MySQL),并丰富示例
for value in retrieved_values:
col = column_map[value.column_id]
if value.value not in col.examples:
col.examples.append(value.value)
# 4. 按表分组
table_columns = group_by_table(column_map)
# 5. 补充主外键(查询 MySQL,确保 JOIN 能力)
for table_id in table_columns:
key_columns = await meta_mysql_repository.get_key_columns_by_table_id(table_id)
table_columns[table_id].extend(key_columns)
# 6. 构建 TableInfoState(查询 MySQL 获取表信息)
table_infos = []
for table_id, columns in table_columns.items():
table_info = await meta_mysql_repository.get_table_info_by_id(table_id)
table_infos.append(build_table_info_state(table_info, columns))
metric_infos = build_metric_infos(retrieved_metrics)
return table_infos, metric_infos
示例:
召回信息:
- 字段:order_amount, region_name
- 指标:GMV(关联字段:order_amount)
- 取值:华北地区(对应字段:region_name)
合并后:
- 补充 GMV 的关联字段:order_amount(已存在)
- 补充 region_name 的示例值:["华北地区"]
- 补充主外键:customer_id, product_id, date_id
- 最终:5 张表,20 个字段
Step 4: LLM 智能过滤
# 输入:20 个字段,5 张表
# 输出:2 张表,8 个字段
filter_table_result = llm_filter_tables(query, table_infos)
# {
# "fact_order": ["order_amount", "order_quantity", "region_id", "date_id"],
# "dim_region": ["region_id", "region_name", "province"]
# }
filter_metric_result = llm_filter_metrics(query, metric_infos)
# ["GMV"]
为什么需要过滤?
- LLM 上下文窗口有限(通常 8K-128K tokens)
- 太多无关信息会降低生成质量
- 减少 token 消耗,降低成本
Step 5: 添加额外上下文
# 时间信息
date_info = {
"date": "2026-05-20",
"weekday": "Wednesday",
"quarter": "Q2"
}
# 数据库信息
db_info = {
"dialect": "mysql",
"version": "8.0"
}
作用:
- 帮助处理时间相关查询(如"本月"、"Q3")
- 确保生成的 SQL 语法符合数据库版本
Step 6: SQL 生成
详见第五节的 Prompt 工程设计。
Step 7: SQL 校验与修正
# 校验(使用 EXPLAIN 在 DW MySQL 中验证 SQL 语法)
try:
await dw_mysql_repo.validate(sql) # EXPLAIN 语法校验
error = None
except Exception as e:
error = str(e)
# 条件分支
if error:
# 校正
corrected_sql = llm_correct_sql(sql, error, context)
sql = corrected_sql
# 执行 SQL(查询 DW MySQL 数据仓库)
result = await dw_mysql_repo.run(sql)
4.3 元知识构建流程
构建步骤:
async def build_meta_knowledge(config_path):
# 1. 读取配置
meta_config = load_config(config_path)
# 2. 处理表信息
for table in meta_config.tables:
# 2.1 查询 DW 获取字段类型和取值
column_types = dw_repo.get_column_types(table.name)
column_values = dw_repo.get_column_values(table.name, column.name)
# 2.2 保存到 MySQL
meta_repo.save_table_info(table)
meta_repo.save_column_info(columns)
# 2.3 向量化字段信息(名称、描述、别名)
for column in columns:
texts = [column.name, column.description] + column.alias
embeddings = embedding_client.embed(texts)
qdrant_repo.upsert(ids, embeddings, payloads)
# 2.4 同步取值到 ES(仅 sync=true 的字段)
if column.sync:
values = dw_repo.get_column_values(column.name, limit=100000)
es_repo.index(values)
# 3. 处理指标信息
for metric in meta_config.metrics:
# 3.1 保存到 MySQL
meta_repo.save_metric_info(metric)
# 3.2 向量化(名称、描述、别名)
embeddings = embedding_client.embed(texts)
metric_qdrant_repo.upsert(ids, embeddings, payloads)
五、关键技术创新
5.1 多路召回 + LLM 扩展
传统方案:直接使用用户查询词进行检索
本系统方案:
用户查询 → Jieba 分词 → LLM 扩展 → 多路召回
优势对比:
| 查询词 | 传统方案 | 本系统 |
|---|---|---|
| "GMV" | 检索 GMV | 扩展:["GMV", "成交总额", "订单总额", "销售总额"] |
| "华北地区" | 精确匹配 | 扩展:["华北", "华东", "华南", "地区"] |
| "Q3" | 无结果 | 扩展:["Q3", "第三季度", "7月", "8月", "9月"] |
5.2 智能上下文过滤
问题:大数据库可能有上百张表,直接注入 LLM 会导致:
- 超出上下文窗口
- 噪音干扰,降低准确率
- token 成本高
解决方案:LLM 二次过滤
# 第一次召回:可能 50+ 字段
retrieved_fields = recall(query) # 50 fields
# LLM 过滤:保留最相关的
filtered_fields = llm_filter(query, retrieved_fields) # 10 fields
# 生成 SQL
sql = llm_generate(query, filtered_fields)
效果:
- 上下文精简 80%
- SQL 生成准确率提升 15-20%
- token 成本降低 60%
5.3 SQL 自修正机制
校正 Prompt:
【角色】你是 SQL 调试专家
【错误信息】
Error Code: 1054. Unknown column 'sales_amount' in 'field list'
【原始 SQL】
SELECT sales_amount FROM fact_order WHERE region_name = '华北'
【表结构】
fact_order:
- order_amount (DECIMAL): 订单金额
- order_quantity (INT): 订单数量
请修正 SQL。
LLM 输出:
SELECT order_amount FROM fact_order WHERE region_name = '华北'
5.4 Prompt 工程设计
SQL 生成 Prompt:
【角色】
你是一个资深的数据库专家和数据分析师。你的任务是根据提供的【上下文信息】,
将用户的自然语言查询转换为语法正确、性能优化的 SQL 语句。
【上下文信息】
可用数据表信息如下:
{table_infos}
可参考的指标信息如下:
{metric_infos}
当前的时间信息如下:
{date_info}
数据库环境如下:
{db_info}
【任务要求】
1. 仅允许使用数据表信息中真实存在的表与字段名称,禁止编造、猜测或引入未提供的表和字段。
2. 若指标信息中存在相关指标定义,必须严格遵循其业务口径、计算逻辑、过滤规则与时间口径。
3. 生成的SQL只能用于查询,不能涉及数据写入、更新、删除等操作。
4. 生成的SQL的语法必须严格符合数据库环境中指定的数据库类型与版本。
5. 默认只生成一条SQL,不可生成多条SQL。
6. 输出必须仅包含一条完整 SQL 语句的纯文本,严禁使用 Markdown 代码块。
用户查询如下:
{query}
输出:
设计要点:
- 角色设定:明确 LLM 的身份和任务
- 结构化上下文:YAML 格式注入表结构
- 严格约束:6 条规则防止幻觉
- 纯文本输出:避免解析问题
5.5 元数据建模
字段角色体系:
columns:
- name: customer_id
role: primary_key # 主键
- name: region_name
role: dimension # 维度(用于分组、过滤)
- name: order_amount
role: measure # 度量(用于聚合)
- name: region_id
role: foreign_key # 外键(用于 JOIN)
作用:
- 帮助 LLM 理解字段用途
- 自动补充 JOIN 所需的外键
- 区分维度字段和度量字段
六、元知识管理
6.1 配置文件设计
# meta_config.yaml
tables:
- name: dim_region
role: dim
description: 地区维度表
columns:
- name: region_id
role: primary_key
description: 地区唯一标识
alias: [地区ID, 区域ID]
sync: false # 不需要同步取值到 ES
- name: region_name
role: dimension
description: 大区名称(华东、华南等)
alias: [地区, 区域, 大区]
sync: true # 同步取值到 ES
- name: fact_order
role: fact
description: 订单事实表
columns:
- name: order_amount
role: measure
description: 订单金额
alias: [销售额, 订单金额, 收入]
sync: false
metrics:
- name: GMV
description: 成交金额总和
relevant_columns:
- fact_order.order_amount
alias: [成交总额, 订单总额]
- name: AOV
description: 平均订单金额
relevant_columns:
- fact_order.order_amount
- fact_order.order_quantity
alias: [平均单价, 平均订单金额]
6.2 知识存储策略
| 知识类型 | 存储位置 | 检索方式 | 更新频率 |
|---|---|---|---|
| 表结构定义 | MySQL | SQL 查询 | 低(DDL 变更时) |
| 字段语义 | Qdrant | 向量相似度 | 低 |
| 字段取值 | Elasticsearch | 全文匹配 | 中(定期同步) |
| 指标定义 | MySQL + Qdrant | SQL + 向量 | 低 |
七、部署架构
7.1 Docker Compose 部署
version: '3.8'
services:
# MySQL(元数据库 + 数据仓库)
mysql:
image: mysql:8.0
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./mysql:/docker-entrypoint-initdb.d
environment:
MYSQL_ROOT_PASSWORD: xxx
MYSQL_USER: atguigu
MYSQL_PASSWORD: xxx
# Elasticsearch(全文检索)
elasticsearch:
build: ./elasticsearch # 带 IK 分词器
ports:
- "9200:9200"
volumes:
- es_data:/usr/share/elasticsearch/data
environment:
discovery.type: single-node
xpack.security.enabled: "false"
# Kibana(ES 可视化)
kibana:
image: kibana:8.19.10
ports:
- "5601:5601"
depends_on:
- elasticsearch
# Qdrant(向量数据库)
qdrant:
image: qdrant/qdrant:v1.16
ports:
- "6333:6333" # HTTP
- "6334:6334" # gRPC
volumes:
- qdrant_data:/qdrant/storage
# Embedding Service(向量化)
embedding:
image: ghcr.io/huggingface/text-embeddings-inference:cpu-1.8
ports:
- "8081:80"
volumes:
- ./embedding/bge-large-zh-v1.5:/models/bge-large-zh-v1.5
environment:
MODEL_ID: /models/bge-large-zh-v1.5
MAX_CONCURRENT_REQUESTS: "16"
volumes:
mysql_data:
es_data:
qdrant_data:
7.2 资源规划
| 服务 | CPU | 内存 | 磁盘 | 说明 |
|---|---|---|---|---|
| MySQL | 2核 | 4GB | 50GB | 元数据 + DW |
| Elasticsearch | 4核 | 8GB | 100GB | 字段取值索引 |
| Qdrant | 2核 | 4GB | 20GB | 向量数据 |
| Embedding | 4核 | 8GB | 10GB | 模型加载 |
| 应用服务 | 2核 | 4GB | - | FastAPI + LangGraph |
总计:14核 CPU,28GB 内存,180GB 磁盘
7.3 启动流程
# 1. 启动基础设施
cd docker
docker-compose up -d
# 2. 构建元知识
python app/scripts/build_meta_knowledge.py
# 3. 启动应用服务
python main.py
# 4. 访问 API 文档
open http://localhost:8000/docs
八、总结与展望
8.1 系统优势
| 维度 | 传统方案 | 本系统 |
|---|---|---|
| 召回策略 | 单一关键词匹配 | 多路召回 + LLM 扩展 |
| 上下文管理 | 全量注入 | LLM 智能过滤 |
| 容错机制 | 无 | SQL 校验 + 自修正 |
| 元数据管理 | 简单 | 精细化(角色、别名、指标) |
| 准确率 | 60-70% | 85-90%(预估) |
| 扩展性 | 低 | 高(分层架构) |
8.2 适用场景
✅ 适合:
- 企业级数据仓库(20+ 张表)
- 复杂的业务指标体系
- 需要高精度 Text-to-SQL
- 有持续维护资源
❌ 不适合:
- 小型数据库(< 10 张表)
- 快速原型验证(推荐 Vanna)
- 无 AI 开发经验团队
8.3 未来优化方向
- 多轮对话:支持上下文连续的查询
- 查询解释:返回 SQL 的业务含义解释
- 权限控制:基于用户的表和字段权限
- 性能优化:SQL 执行计划分析和优化建议
- 反馈学习:基于用户反馈持续优化 Prompt
- 多数据库:支持 PostgreSQL、ClickHouse 等
- 可视化:查询结果自动图表展示
8.4 技术启示
本项目的核心设计理念:
- 不要依赖 LLM 的隐式知识:通过 RAG 显式注入元数据
- 流程透明可控:每个节点独立可调试
- 容错设计:假设 LLM 会犯错,设计修正机制
- 分层过滤:从粗到精逐步缩小上下文
- 站在巨人肩膀上:基于 LangChain/LangGraph 生态,不重复造轮子
附录
A. 项目结构
data-agent/
├── app/
│ ├── agent/ # Agent 工作流
│ │ ├── nodes/ # 工作流节点
│ │ │ ├── extract_keywords.py
│ │ │ ├── recall_column.py
│ │ │ ├── recall_metric.py
│ │ │ ├── recall_value.py
│ │ │ ├── merge_retrieved_info.py
│ │ │ ├── filter_table.py
│ │ │ ├── filter_metric.py
│ │ │ ├── add_extra_context.py
│ │ │ ├── generate_sql.py
│ │ │ ├── validate_sql.py
│ │ │ ├── correct_sql.py
│ │ │ └── run_sql.py
│ │ ├── graph.py # 工作流图定义
│ │ ├── state.py # 状态定义
│ │ ├── context.py # 上下文定义
│ │ └── llm.py # LLM 配置
│ ├── clients/ # 客户端管理
│ ├── repositories/ # 数据访问层
│ ├── services/ # 业务服务
│ ├── entities/ # 实体定义
│ └── conf/ # 配置
├── prompts/ # Prompt 模板
├── conf/ # 配置文件
├── docker/ # Docker 配置
└── main.py # 应用入口
B. 关键依赖版本
# pyproject.toml
python = ">=3.12"
fastapi = ">=0.134.0"
langchain = ">=1.2.10"
langgraph = "latest"
qdrant-client = ">=1.17.0"
elasticsearch = ">=8,<9"
jieba = ">=0.42.1"
omegaconf = ">=2.3.0"
C. 参考资源
作者:AI 数据查询助手项目组
日期:2026-05-20
版本:v1.0
如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和分享!如有问题,请在评论区留言讨论。🚀
作者:Work Hard Work Smart
出处:http://www.cnblogs.com/linlf03/
欢迎任何形式的转载,未经作者同意,请保留此段声明!
浙公网安备 33010602011771号