引言
Text2SQL(自然语言转 SQL)正在从学术研究走向生产应用。然而,工程实践中一个反复出现的问题是:LLM 拿到 schema 后仍然写不对 SQL。
典型的失败场景包括:
- 将
event_value_in_usd误解为"交易金额"(实际是事件参数的货币换算值) - 不知道两张表的 Join 条件,生成笛卡尔积
- 对分片表(sharded table)使用错误的通配符语法
- 生成全表扫描查询,在 TB 级表上产生高额费用
根本原因在于:INFORMATION_SCHEMA 提供的是物理结构(列名、类型),而 LLM 需要的是业务语义(这列是什么意思、怎么和其他表关联、查询时有什么注意事项)。
本文介绍一种基于 Open Knowledge Format(OKF)的解决方案:将数据资产的业务上下文以结构化 Markdown 文档的形式组织为"语义层",通过 RAG 检索注入 LLM context,从而显著提升 Text2SQL 的准确率。
一、问题分析:为什么纯 Schema 不够
以 Google Analytics 4 的 BigQuery 导出表 events_* 为例。该表的 INFORMATION_SCHEMA.COLUMNS 返回约 80 个字段定义,其中包含大量嵌套 RECORD(如 event_params、user_properties、items)。
当用户提问"上周有多少独立用户访问了网站?"时,LLM 面临以下决策:
- 用
user_id还是user_pseudo_id去重?(答案:user_pseudo_id,因为user_id大量为 null) - 用什么字段过滤时间范围?(答案:
event_date是 STRING 格式 YYYYMMDD,不是 TIMESTAMP) - 需要过滤特定的
event_name吗?(答案:对于"访问"可能需要page_view或session_start)
这些信息在 schema 中完全不存在。传统做法是在 system prompt 中硬编码表说明,但面对数十张表、数百个字段时,这种方式不可扩展。
二、OKF 作为 Text2SQL 语义层
2.1 OKF 提供了什么
Open Knowledge Format 是一种以 Markdown + YAML frontmatter 表示知识的开放格式。当其用于描述数据资产时,每个 Concept 文档可包含以下对 Text2SQL 有价值的信息:
| 信息类别 | 在 OKF 文档中的位置 | Text2SQL 中的作用 |
|---|---|---|
| 表的业务含义 | frontmatter description + 正文 |
帮助 LLM 判断该表是否与用户问题相关 |
| 字段语义 | # Schema section |
避免误解列名含义 |
| Join 关系 | # Joins section + 交叉链接 |
正确生成多表关联 |
| SQL 示例 | # Common query patterns |
作为 few-shot 示例 |
| 指标定义 | 独立 reference concept | 对齐业务术语与计算逻辑 |
| 注意事项 | 正文中的说明 | 避免性能陷阱和语义错误 |
2.2 实际 OKF 文档示例
以下是从 knowledge-catalog 项目中 GA4 bundle 的 tables/events_.md 中提取的关键片段:
markdown
---
type: BigQuery Table
title: Events table (Google Analytics BigQuery Export)
description: Contains Google Analytics event export data from the
`ga4_obfuscated_sample_ecommerce` dataset.
tags: [events, Google Analytics, BigQuery, ecommerce]
resource: https://bigquery.googleapis.com/.../events_*
---
# Overview
The `events_` table is a sharded BigQuery table containing Google
Analytics event export data...
# Schema
## event
- `event_date` (STRING): The date when the event was logged
(YYYYMMDD format in the registered timezone of your app).
- `event_name` (STRING): The name of the event.
- `event_value_in_usd` (FLOAT): The currency-converted value (in USD)
of the event's "value" parameter.
## user
- `user_pseudo_id` (STRING): The pseudonymous id (e.g., app instance ID)
for the user. A unique identifier assigned when they first open the app
or visit the site.
# Joins
- [Google Analytics Events to Google Ads Clicks](../references/joins/...)
--- join on `collected_traffic_source.gclid` to attach Google Ads data.
相比 INFORMATION_SCHEMA 返回的 event_date STRING NULLABLE,OKF 文档多提供了格式说明(YYYYMMDD)、语义解释、以及关联关系------这些正是 LLM 生成正确 SQL 所需的关键上下文。
三、系统架构
3.1 整体流程
scss
┌─────────────────────────────────────────────────────────────────┐
│ 离线阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 数据源 (BigQuery) │
│ │ │
│ ▼ │
│ OKF Enrichment Agent ──→ OKF Bundle (Markdown 文件) │
│ │ │ │
│ │ ▼ │
│ │ 向量化 + 索引 │
│ │ │ │
│ ▼ ▼ │
│ 人工 Review (Git PR) Vector Store │
│ │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 在线阶段 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 用户自然语言问题 │
│ │ │
│ ▼ │
│ ① 意图理解 + 关键词提取 │
│ │ │
│ ▼ │
│ ② RAG 检索 (语义搜索 + metadata filter) │
│ │ │
│ ├──→ 命中: tables/orders.md │
│ ├──→ 命中: tables/customers.md │
│ └──→ 命中: references/metrics/revenue.md │
│ │ │
│ ▼ │
│ ③ 组装 Context (Schema + Joins + Examples) │
│ │ │
│ ▼ │
│ ④ LLM 生成 SQL │
│ │ │
│ ▼ │
│ ⑤ 可选: SQL 验证 + 执行 │
│ │
└─────────────────────────────────────────────────────────────────┘
3.2 离线阶段:Bundle 生产与索引
Step 1: 生成 OKF Bundle
使用 knowledge-catalog 项目提供的 enrichment agent:
bash
python -m enrichment_agent enrich \
--source bq \
--dataset myproject.analytics \
--web-seed-file seeds.txt \
--out ./bundles/analytics
Agent 将为每张表、每个数据集生成独立的 Concept 文档,包含 schema 描述、常见查询模式和引用来源。
Step 2: 人工审校与补充
通过 Git PR 流程补充 Agent 无法自动获取的信息:
- 字段的业务含义澄清
- 性能注意事项("务必使用
_TABLE_SUFFIX限制分片范围") - 负面知识("不要用
user_id做去重,null 率超过 60%")
Step 3: 向量化与索引
将 Bundle 中每个 Concept 文件作为一个 document 入库:
python
for md_file in bundle_root.rglob("*.md"):
if md_file.name == "index.md":
continue
doc = parse_okf_document(md_file)
vector_store.add(
text=doc.body,
metadata={
"type": doc.frontmatter["type"],
"tags": doc.frontmatter.get("tags", []),
"title": doc.frontmatter.get("title", ""),
"concept_id": doc.concept_id,
}
)
frontmatter 字段作为 metadata,支持后续的结构化过滤。
3.3 在线阶段:检索与生成
Step 1: 意图理解
对用户问题进行初步分析,提取可能涉及的数据实体和指标:
makefile
用户问题: "上季度购买转化率最高的流量来源是哪个?"
提取: [购买, 转化率, 流量来源, 上季度]
Step 2: RAG 检索
执行混合检索策略:
python
# 语义搜索
results = vector_store.search(
query="购买转化率 流量来源",
filter={"type": {"$in": ["BigQuery Table", "Reference"]}},
top_k=5
)
# 基于链接的图扩展
for result in results:
linked_concepts = extract_links(result.body)
results.extend(fetch_linked(linked_concepts))
Step 3: Context 组装
将检索到的 Concept 内容按优先级组装:
css
[系统指令]
你是一个 SQL 生成助手。根据以下数据资产文档生成 BigQuery SQL。
[表文档 1: events_]
{events_.md 的完整内容或关键片段}
[表文档 2: ...]
{...}
[指标定义: 购买转化率]
{references/metrics/conversion_rate.md}
[用户问题]
上季度购买转化率最高的流量来源是哪个?
Step 4: SQL 生成
LLM 基于充分的业务上下文生成 SQL。由于 context 中已包含:
event_date是 YYYYMMDD 格式的 STRING- 流量来源在
collected_traffic_source.manual_source字段 - 购买事件对应
event_name = 'purchase' - 转化率的定义公式
生成的 SQL 准确率显著高于仅提供 schema 的方案。
四、OKF 相比其他语义层方案的优势
4.1 vs 硬编码 System Prompt
| 维度 | 硬编码 | OKF + RAG |
|---|---|---|
| 可扩展性 | 表多了 prompt 超长 | 按需检索,只加载相关表 |
| 可维护性 | 修改需改代码 | 修改 .md 文件即可 |
| 协作 | 耦合在代码中 | Git PR 流程 |
| 覆盖度 | 通常只覆盖核心表 | Agent 可全量生成 |
4.2 vs 自定义 JSON/YAML 配置
一些团队选择自定义 JSON 格式来描述表的语义信息。OKF 的优势在于:
- 正文可包含任意长度的自然语言:解释复杂的业务逻辑不受字段长度限制
- SQL 示例直接嵌入:fenced code block 天然支持
- 有社区规范:不是每个团队自造格式,互操作性更好
- 消费工具已存在:Obsidian 可浏览、MkDocs 可发布、LLM 可直接消费
4.3 vs dbt docs / Metabase 数据字典
dbt 的 schema.yml 和 Metabase 的数据字典也提供语义信息,但:
- dbt 绑定 dbt 工具链,非 dbt 项目无法使用
- Metabase 的字典锁在其平台内,无法被外部 Agent 消费
- 两者都不原生支持 SQL 示例、引用来源、交叉链接等富内容
OKF 作为独立于工具链的开放格式,可以从这些系统中导出,也可以被任何系统消费。
五、关键设计决策与权衡
5.1 粒度选择:一表一文件
OKF 推荐每个数据资产对应一个独立文件。对于 Text2SQL 场景:
- 优势:检索粒度清晰,避免无关表的噪声
- 代价:跨表关系信息分散,需通过链接图扩展补充
5.2 非结构化 vs 结构化的平衡
OKF 的 body 是自由格式 Markdown,不强制特定 section 结构。这意味着:
- 优势:可以容纳"这个字段在每周一凌晨会有空值,因为上游 ETL 的时间窗口问题"这类难以结构化的知识
- 代价:不同文件质量参差,需要人工或自动化质量检查
5.3 实时性问题
OKF Bundle 是静态文件。当源表 schema 发生变更时:
- 短期方案:定时重新运行 enrichment agent + diff 检测
- 长期方案:结合 schema change detection webhook 触发增量更新
六、实施建议
对于计划将 OKF 应用于 Text2SQL 场景的团队,建议按以下步骤推进:
第一阶段:核心表覆盖
- 选择 10-20 张最常被查询的核心表
- 使用 enrichment agent 生成初始 bundle
- 人工补充 Join 关系和查询示例
第二阶段:指标与术语对齐
- 将业务术语表转化为 reference concept
- 建立"用户说的词"到"实际字段/计算逻辑"的映射
第三阶段:RAG 管道搭建
- 将 bundle 入库向量数据库
- 实现 metadata filter + 语义搜索的混合检索
- 配置 context 组装模板
第四阶段:持续运营
- 基于 SQL 执行失败的反馈修正文档
- 定期重新 enrich 以同步 schema 变更
- 通过 Git PR 流程收集领域专家的知识补充
七、局限性与未来方向
当前方案存在以下已知局限:
-
依赖检索质量:如果 RAG 未命中正确的 Concept,后续生成必然出错。需要持续优化检索策略(query rewrite、re-ranking)。
-
缺乏执行反馈闭环:当前流程中 OKF 文档是静态的,无法根据 SQL 执行结果自动修正。未来可引入"执行失败 → 自动标注文档不足 → 触发 re-enrich"的反馈循环。
-
多表复杂查询的 context 膨胀:涉及 5+ 张表的查询,即使只加载相关 Concept,context 仍可能过长。需要结合摘要策略或分层推理。
-
权限与数据安全:OKF 文档可能包含敏感的 schema 信息。部署时需考虑文档级的访问控制。
八、结语
Text2SQL 的核心挑战从来不是"LLM 不会写 SQL",而是"LLM 不理解你的数据"。OKF 提供了一种务实的方案:将人类和 Agent 对数据的理解以最朴素的形式(Markdown 文件)固化下来,再通过 RAG 在需要时精准注入 LLM 的上下文。
这种方案的工程优雅性在于:它不需要引入新的基础设施组件------向量数据库和 LLM 调用链路在大多数 AI 应用中已然存在;它只需要一个新的"内容源",而 OKF bundle 恰好以最低的摩擦成本充当了这个角色。