work hard work smart

专注于AI+Java后端开发。 不断总结,举一反三。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

构建企业级 Text-to-SQL Agent:基于 LangGraph 的智能数据查询系统设计

摘要:本文详细介绍了一个基于 LangGraph 构建的企业级 Text-to-SQL Agent 系统的架构设计。该系统通过多路召回、RAG(检索增强生成)、智能过滤和自修正机制,实现了从自然语言到 SQL 的高质量转换。文章将深入剖析系统的分层架构、各中间件的作用、核心流程设计以及关键技术创新。


目录


一、项目概述

1.1 背景与挑战

在企业数据分析场景中,业务人员经常需要从数据仓库中查询数据。然而,SQL 的技术门槛使得这一过程依赖专业的数据分析师或开发工程师,导致:

  • 效率瓶颈:简单的数据查询也需要排期等待
  • 人力成本:数据团队大量时间被重复性查询占据
  • 响应延迟:无法实现即时的数据探索和分析

Text-to-SQL 技术应运而生,但现有的开源方案(如 Vanna)在企业级场景中面临诸多挑战:

  1. 复杂的业务指标体系难以表达
  2. 大型数据仓库的元数据管理困难
  3. 单一召回策略导致准确率不足
  4. 缺乏 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 查询处理流程图

graph TB Start([用户查询]) --> A[关键词抽取<br/>jieba 分词] A --> A1[LLM 关键词扩展<br/>语义关联扩展] A1 --> B1[字段召回<br/>Qdrant 向量检索] A1 --> B2[指标召回<br/>Qdrant 向量检索] A1 --> B3[取值召回<br/>ES 全文检索] B1 --> C[合并召回信息<br/>查询MySQL元数据库补全字段/表/主外键] B2 --> C B3 --> C C --> D1[LLM 过滤表信息] C --> D2[LLM 过滤指标信息] D1 --> E[添加额外上下文<br/>时间信息/数据库信息] D2 --> E E --> F[LLM 生成 SQL] F --> G{SQL 校验<br/>EXPLAIN} G -->|成功| H[执行 SQL<br/>查询 DW MySQL] G -->|失败| I[LLM 校正 SQL] I --> G H --> J([返回结果]) style Start fill:#e1f5ff style J fill:#e1f5ff style G fill:#fff3cd style I fill:#f8d7da style A1 fill:#d4edda style C fill:#ffe6cc style H fill:#d4edda

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"]

为什么需要过滤?

  1. LLM 上下文窗口有限(通常 8K-128K tokens)
  2. 太多无关信息会降低生成质量
  3. 减少 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 元知识构建流程

graph LR A[meta_config.yaml] --> B[MetaKnowledgeService] B --> C1[保存表/字段到 MySQL] B --> C2[字段向量化到 Qdrant] B --> C3[取值同步到 ES] B --> C4[指标保存到 MySQL] B --> C5[指标向量化到 Qdrant] C1 --> D[元数据库] C2 --> E[向量数据库] C3 --> F[全文检索引擎] C4 --> D C5 --> E style A fill:#e1f5ff style D fill:#d4edda style E fill:#d4edda style F fill:#d4edda

构建步骤

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 自修正机制

graph LR A[生成 SQL] --> B{EXPLAIN 校验} B -->|成功| C[执行] B -->|失败| D[LLM 校正] D --> B style B fill:#fff3cd style D fill:#f8d7da

校正 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}

输出:

设计要点

  1. 角色设定:明确 LLM 的身份和任务
  2. 结构化上下文:YAML 格式注入表结构
  3. 严格约束:6 条规则防止幻觉
  4. 纯文本输出:避免解析问题

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 未来优化方向

  1. 多轮对话:支持上下文连续的查询
  2. 查询解释:返回 SQL 的业务含义解释
  3. 权限控制:基于用户的表和字段权限
  4. 性能优化:SQL 执行计划分析和优化建议
  5. 反馈学习:基于用户反馈持续优化 Prompt
  6. 多数据库:支持 PostgreSQL、ClickHouse 等
  7. 可视化:查询结果自动图表展示

8.4 技术启示

本项目的核心设计理念:

  1. 不要依赖 LLM 的隐式知识:通过 RAG 显式注入元数据
  2. 流程透明可控:每个节点独立可调试
  3. 容错设计:假设 LLM 会犯错,设计修正机制
  4. 分层过滤:从粗到精逐步缩小上下文
  5. 站在巨人肩膀上:基于 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


如果你觉得这篇文章对你有帮助,欢迎点赞、收藏和分享!如有问题,请在评论区留言讨论。🚀