Text-to-SQL 的核心挑战

自然语言问数看似简单,工程上却要同时解决:

挑战 表现 本专题策略
语义歧义 「收入」指毛利还是营收 指标元数据 + 域分类
时间缺失 「销售收入多少」没写周期 主动追问 missing_time_period
口径不一 完成率分母不同 bi_metric_extract_logic 固化
SQL 安全 注入、删库 validate_sql 四层校验
幻觉 模型编造表名 禁止 LLM 直接产 SQL

为什么用结构化元数据,而非纯 LLM 生成 SQL

LLM 生成 SQL 在 Demo 里很炫,在生产里常见问题:

  1. 表名、字段名随 prompt 波动,不可复现
  2. 同一问题两次可能生成不同 SQL,审计困难
  3. 企业宽表字段多、命名晦涩,上下文窗口装不下

因此本平台的生产路径是:检索命中指标 → 查 bi_metric_extract_logic → 规则拼接 SQL。LLM 留给解释与未来的复杂推理。

五步流水线

1
2
3
4
5
6
7
8
flowchart LR
A[1 域分类] --> B[2 指标检索]
B --> C[3 取数逻辑]
C --> D[4 时间解析]
D --> E[5 SQL 组装]
E --> F{缺周期?}
F -->|是| G[追问用户]
F -->|否| H[validate + execute]

步骤 1:域分类 classify_domain

bi_domain.domain_keywords 与关联指标名构建关键词表,对用户问题打分:

  • 完全匹配关键词:+3 分
  • 部分匹配:+1.5 分
  • 同分按域优先级排序
  • 无命中 → 未知 域,走更宽泛检索

虚构示例:问题含「销售收入、华东区」→ 命中「销售运营域」。

步骤 2:指标检索 retrieve_all

并行检索 metrics / bi_models / business_rules。metrics 优先走 MySQL:

1
2
3
4
5
6
7
-- 简化示意:按指标名、别名、关键词 LIKE 打分
SELECT metric_code, metric_name, domain_code
FROM bi_metric
WHERE status = 1
AND (metric_name LIKE '%销售收入%' OR alias LIKE '%销售收入%')
ORDER BY score DESC
LIMIT 5;

命中第一个指标 M_SALES_REVENUE(销售收入)后进入取数逻辑。

步骤 3:取数逻辑 _build_sql_from_meta_logic

bi_metric_extract_logic

1
2
3
4
5
6
SELECT l.*, t.table_name_cn
FROM bi_metric_extract_logic l
LEFT JOIN bi_table t ON t.table_code = l.table_code
WHERE l.metric_code = 'M_SALES_REVENUE'
ORDER BY l.is_default DESC, l.priority DESC
LIMIT 1;

关键字段(虚构宽表 dws_sales_metric_wide):

字段 示例值 含义
table_code dws_sales_metric_wide 结果表
index_name_value 销售收入 宽表中 index_name 过滤值
period_value 季度 默认统计周期
period_field period 周期类型字段
time_field stat_date 统计日期字段
default_filter is_valid = '1' 有效数据标记
value_field_map {"当期值":"value","同比":"val_yoy"} 展示字段映射
org_field region_name 组织/区域维度

步骤 4:时间解析

函数 _extract_time_period 将自然语言转为内部表示,再由 _resolve_result_table_time_condition 生成 WHERE 片段。

用户表述 解析结果 SQL 条件(示例)
今年 Q1 / 2026 年一季度 2026Q1 period='季度' AND stat_date='2026-03-31'
2026 年 2026年 period='年' AND stat_date='2026-12-31'
上月 2026-05 period='月' AND stat_date='2026-05-31'
本周 2026W23 period='周' AND stat_date=...
(未提及) 触发 missing_time_period 追问

步骤 5:SQL 组装

维度解析:用户提到「华东区」→ region_name 过滤;未提及则按 granularity 默认到省级汇总。

取值字段:默认「当期值」;若含「同比」追加 val_yoy,含「完成率」追加完成率字段。

完整虚构 SQL 示例(用户问:「华东区 2026 年一季度销售收入是多少?」):

1
2
3
4
5
6
7
8
9
10
11
12
SELECT
region_name AS `区域`,
value AS `销售收入_当期值`,
val_yoy AS `同比`
FROM `dws_sales_metric_wide`
WHERE `index_name` = '销售收入'
AND `period` = '季度'
AND `stat_date` = '2026-03-31'
AND `region_name` = '华东区'
AND is_valid = '1'
ORDER BY value DESC
LIMIT 1000;

代码级拼接逻辑(简化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
where_conditions = [
f"`index_name` = '{index_name}'",
time_condition,
f"`{org_field}` = '{region}'",
]
if default_filter:
where_conditions.append(default_filter)

sql = (
f"SELECT {', '.join(select_parts)} "
f"FROM `{table_code}` "
f"WHERE {' AND '.join(where_conditions)} "
f"ORDER BY value DESC LIMIT 1000"
)

缺时间周期时的追问

若命中指标但 _has_time_period(question) 为 false,不执行 SQL,返回:

1
2
3
4
5
6
7
8
9
10
11
12
{
"needs_clarification": true,
"fallback_type": "missing_time_period",
"clarification": {
"needed_fields": ["time_period"],
"questions": ["请问需要查询哪个时间周期的销售收入?"],
"suggested_questions": [
"华东区 2026 年一季度销售收入",
"华东区 2026 年销售收入"
]
}
}

与站内延伸

本章小结

  • 生产 Text-to-SQL = 检索指标 + 元数据模板,不是 LLM 写 SQL
  • 五步链路每一步可单测、可日志、可回放
  • 时间周期是最常见澄清点,必须产品化

思考题

  1. 若同一指标存在月表与季表两套取数逻辑,优先级应如何配置?
  2. value_field_map 为什么要 JSON 化,而不是在代码里写死字段名?

系列导航

主题
01 架构全景
02 本篇
03 RAG 指标库
04 多轮澄清
05 安全校验
06 Vue3 工作台
07 演示验证
08 反馈闭环

← 返回专题首页