小布问数:一个生产级 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 | 元数据存储 + 数据仓库查询 |
| Embedding | BGE-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 传递数据。这种设计让系统具备三个重要特性:
- 可观测:每个节点的进度都通过 SSE 推送给前端
- 可调试:哪个节点出错,很快就能定位
- 可扩展:新增节点或调整路由都很方便
六、元知识库:NL2SQL 的"字典"
元知识库是连接自然语言与数据库 Schema 的桥梁。它包含:
- 表信息:表名、角色(事实表/维度表)、描述
- 字段信息:字段名、类型、角色(主键/外键/维度/度量)、描述、别名、示例取值
- 指标信息:指标名、描述、计算口径、关联字段、别名
- 字段取值:枚举类字段的实际取值
这些数据通过 conf/meta_config.yaml 配置,然后一键构建到三个存储引擎:
YAML 配置
│
├──▶ MySQL(结构化查询)
│
├──▶ Qdrant(向量语义检索)
│
└──▶ Elasticsearch(全文精确匹配)
为什么要三种存储?因为不同类型的查询需要不同的检索方式:
- "销售额" → 语义相似 → 向量检索
- "华东" → 精确命中 → 全文检索
- "order_amount 是什么类型" → 结构化查询 → MySQL
七、评测体系:从"能跑"到"跑得对"
NL2SQL 系统的真正考验不是"能不能生成 SQL",而是"生成的 SQL 执行结果对不对"。
小布问数的评测体系包含多个维度:
| 指标 | 衡量什么 | 严格程度 |
|---|---|---|
| Valid SQL Rate | SQL 能不能执行 | 宽松 |
| Exact Match | SQL 文本是否一致 | 严格 |
| 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 工程方向的机会,欢迎交流。