00_小布问数项目介绍

3 阅读7分钟

小布问数:一个生产级 NL2SQL 智能问答系统的设计与实践

一、项目定位:解决什么业务问题

在企业数据化运营中,一个长期存在的矛盾是:业务人员需要频繁查数据,但大多数人不会写 SQL;DBA 和数据分析师能写 SQL,但人力有限、响应慢。

典型场景:

  • 运营同学想查"上周华东地区各品类的 GMV",需要在 IM 里找 DBA,排队等待
  • 产品经理想验证一个假设,因为不会写 JOIN 而放弃
  • 管理层想看实时数据,但报表系统无法覆盖临时问题

小布问数(o-data-agent-service)正是为了解决这个"最后一公里"问题而生:让业务人员用自然语言直接查询数据仓库,系统理解问题、生成 SQL、执行并返回结果。


二、核心挑战:NL2SQL 为什么难

很多人误以为 NL2SQL 就是"把问题丢给 GPT,让它写 SQL"。但在真实业务环境中,这件事远比想象的复杂:

2.1 语义鸿沟

用户说"销售额",数据库字段可能叫 order_amount;用户说"华东",它可能是 dim_region.region_name 的一个取值。LLM 不可能凭空知道这些映射关系。

2.2 Schema 爆炸

真实数据仓库常有数百张表、数千个字段。把全部 Schema 塞进 Prompt,Token 会爆炸,LLM 也会"迷失在上下文中"(Lost in the Middle)。

2.3 SQL 的严格性

自然语言允许模糊,但 SQL 不行。一个字段名拼错、一个 JOIN 条件缺失,整个查询就失败。更危险的是,SQL 可能"能执行但结果不对"。

2.4 可解释性与可控性

生产系统不能是一个黑盒。用户需要知道"系统为什么给出这个结果",运维需要知道"哪一步出错了"。


三、整体方案:分层解耦 + 多路召回 + 校验闭环

小布问数的整体架构可以概括为三层:

┌──────────────────────────────────────┐
│  接入层:FastAPI + SSE 流式响应       │
├──────────────────────────────────────┤
│  智能体层:LangGraph 12 节点工作流    │
├──────────────────────────────────────┤
│  数据层:MySQL + Qdrant + ES 多引擎  │
└──────────────────────────────────────┘

3.1 多路召回:把"大海捞针"变成"精准定位"

系统不是直接把问题丢给 LLM,而是先通过三路召回找到最相关的表、字段、指标、取值:

  • 字段召回(Qdrant 向量) :解决"销售额 → order_amount"的语义映射
  • 取值召回(ES 全文) :解决"华东 → region_name='华东'"的精确匹配
  • 指标召回(Qdrant 向量) :解决"GMV → SUM(order_amount)"的业务概念映射

三路召回并行执行,然后融合成结构化的表-字段 Schema 注入 Prompt。

3.2 LLM 过滤:做精准的"减法"

召回可能带回 10+ 张表、50+ 个字段。系统让 LLM 做二次过滤,只保留真正需要的表和字段,大幅降低生成 SQL 时的 Token 消耗和干扰。

3.3 校验 + 修正:保证 SQL 能跑且跑得对

生成的 SQL 先用 MySQL 的 EXPLAIN 做语法和引用校验:

  • 校验通过 → 直接执行
  • 校验失败 → 把错误信息交给 LLM 做最小必要修正

同时,评测体系通过 Execution Accuracy(执行结果对比)来衡量"跑得对不对",而不仅仅是"能不能跑"。


四、技术栈选型:为什么选这些

组件选型原因
工作流编排LangGraph状态图工作流比 ReAct Agent 更可控,适合 NL2SQL 这类确定性流程
Web 框架FastAPI原生支持异步、依赖注入、自动文档
向量检索Qdrant轻量、高性能、支持中文 Embedding
全文检索Elasticsearch中文分词成熟,适合字段取值精确匹配
关系型数据库MySQL 8.0元数据存储 + 数据仓库查询
EmbeddingBGE-Large-ZH中文语义理解强,1024 维
大模型DeepSeek Chat代码和结构化输出能力强,OpenAI 兼容
分词Jieba中文关键词粗提取,可控性好
配置管理OmegaConf支持 YAML + Schema 校验

五、12 节点工作流全景

START
  │
  ▼
extract_keywords(jieba 分词)
  │
  ├──▶ recall_column(字段召回)────┐
  ├──▶ recall_value(取值召回)─────┼──▶ merge_retrieved_info(信息融合)
  └──▶ recall_metric(指标召回)────┘
              │
              ▼
    filter_table / filter_metric(LLM 过滤)
              │
              ▼
    add_extra_context(时间/数据库环境)
              │
              ▼
    generate_sql(LLM 生成 SQL)
              │
              ▼
    validate_sql(EXPLAIN 校验)
       /              \
 error=None        error!=None
     │                  │
     ▼                  ▼
execute_sql      correct_sql
     │                  │
     └────────┬─────────┘
              ▼
            END

每个节点只负责一个明确的子任务,节点之间通过 State 传递数据。这种设计让系统具备三个重要特性:

  1. 可观测:每个节点的进度都通过 SSE 推送给前端
  2. 可调试:哪个节点出错,很快就能定位
  3. 可扩展:新增节点或调整路由都很方便

六、元知识库:NL2SQL 的"字典"

元知识库是连接自然语言与数据库 Schema 的桥梁。它包含:

  • 表信息:表名、角色(事实表/维度表)、描述
  • 字段信息:字段名、类型、角色(主键/外键/维度/度量)、描述、别名、示例取值
  • 指标信息:指标名、描述、计算口径、关联字段、别名
  • 字段取值:枚举类字段的实际取值

这些数据通过 conf/meta_config.yaml 配置,然后一键构建到三个存储引擎:

YAML 配置
    │
    ├──▶ MySQL(结构化查询)
    │
    ├──▶ Qdrant(向量语义检索)
    │
    └──▶ Elasticsearch(全文精确匹配)

为什么要三种存储?因为不同类型的查询需要不同的检索方式:

  • "销售额" → 语义相似 → 向量检索
  • "华东" → 精确命中 → 全文检索
  • "order_amount 是什么类型" → 结构化查询 → MySQL

七、评测体系:从"能跑"到"跑得对"

NL2SQL 系统的真正考验不是"能不能生成 SQL",而是"生成的 SQL 执行结果对不对"。

小布问数的评测体系包含多个维度:

指标衡量什么严格程度
Valid SQL RateSQL 能不能执行宽松
Exact MatchSQL 文本是否一致严格
Execution Accuracy执行结果是否一致真实
Composite Score加权综合综合

其中 Execution Accuracy 是核心:它执行预测 SQL 和标准 SQL,忽略行序、处理浮点精度、标准化 NULL,最终判断两条 SQL 是否等价。

评测还内置了 EvalTracer,记录每个样本在各个节点的中间状态,支持归因分析:

准确率低
  ├── 召回字段缺失 → 优化召回
  ├── 过滤丢表 → 优化 filter Prompt
  ├── SQL 生成错误 → 优化 generate Prompt
  └── 校验失败 → 优化 correct Prompt

八、工程化设计:面向生产的细节

8.1 异步全链路

从 API 到数据库到 LLM 调用,全面使用 async/await,避免阻塞,支持高并发。

8.2 依赖注入

通过 FastAPI Depends 和 LangGraph Context 注入 Repository 和 Client,便于单元测试和 Mock。

8.3 连接池管理

MySQL、Qdrant、ES 都有专门的 ClientManager,应用启动时初始化,关闭时统一释放。

8.4 请求追踪

每个请求生成唯一 request_id,贯穿日志,便于问题排查。

8.5 配置驱动

元知识库、Prompt、LLM 参数都通过配置管理,不用改代码即可适配不同业务场景。


九、适用场景与扩展方向

小布问数不仅适用于 NL2SQL,其设计范式可扩展到:

  • 智能报表:自然语言生成报表配置
  • 指标平台:自然语言查询业务指标
  • 数据血缘分析:自然语言探索表关系
  • API 参数推断:自然语言调用内部数据 API

十、快速体验

# 1. 安装依赖
uv sync

# 2. 启动基础设施
cd docker && docker-compose up -d && cd ..

# 3. 构建元知识库
python -m app.scripts.build_meta_knowledge -c conf/meta_config.yaml

# 4. 启动服务
python main.py

# 5. 测试查询
curl -X POST http://localhost:8000/api/query \
  -H "Content-Type: application/json" \
  -d '{"query": "查询华北地区的销售额"}'

十一、写在最后

小布问数的核心价值不是"用 LLM 写 SQL",而是把 LLM 的不确定性封装在一套确定的工程流程中

  • 用多路召回降低 LLM 需要理解的上下文规模
  • 用 LLM 过滤做精准的上下文裁剪
  • 用 EXPLAIN 和自动修正兜底 SQL 质量
  • 用评测体系形成持续优化的闭环

在生产级 AI 系统中,工程化能力往往比模型能力更重要。希望这个项目能为正在构建 NL2SQL 系统的同学提供一些参考。


作者正在寻找 AI 工程方向的机会,欢迎交流。