气量智能问数:Text-to-SQL 的落地实践

这篇文章不是”Text-to-SQL 教程”——网上那种”用 LangChain 调 OpenAI 跑通 SQL 生成”的教程一堆一堆的,没什么好写的。这篇写的是真的在一个企业项目里把它做上线时,遇到的哪些没人会告诉你的破事。

起因:客户问”能不能用自然语言查?”

这个项目不是”领导拍脑袋要做 AI”的产物,是客户真提出来的。

我们做的是燃气行业的 SaaS 系统(抄表 / 营收 / 客户管理)。客户提的需求模式很固定:”能不能加个报表,看 X / Y / Z 的统计?”——几乎每两周一个需求,每个需求都要定制开发。

客户业务人员原话:”我们自己也想试,但每次都找你们开发,太慢了。能不能让我们直接问?”

一开始我觉得这是”伪需求”——SQL 这么复杂,业务人员怎么可能问得清楚?后来做了才发现,他们 80% 的需求确实就是简单的”按某个维度统计一下”,这种需求如果能让 AI 解决,确实能省掉大量定制开发。

第一次尝试:直接调 OpenAI API

最开始我的方案特别简单:

1
2
3
4
5
6
7
8
def ask(question):
prompt = f"""
你是一个 SQL 专家。根据以下表结构,把问题转成 SQL:
{all_tables_schema}
问题:{question}
SQL:
"""
return openai(prompt)

跑起来一看,对于”列出所有用户”这种简单问题,模型答得挺好。但稍微复杂一点就崩:

  • “上个月各营业厅的用气量” → 表名都不知道
  • “片区 3 的异常表具” → “异常”在业务里是特定标记,普通 SQL 找不到
  • “用户数 100 以上的营业厅” → 多个条件组合,模型经常漏条件

最大的问题:模型不知道业务术语。”用气量”在库里叫 gas_consumption,”营业厅”叫 business_hall,”异常表具”是状态码 status=4。这些业务语义模型完全不懂。

第二次尝试:把全部 schema 塞进去

既然模型不懂业务,那就把所有表结构 + 字段注释都喂给它。结果更糟了:

  • token 直接爆表(200+ 张业务表,每张 30+ 字段)
  • 模型”信息过载”,抓不住重点
  • 慢得没法用

这次尝试让我明白一个朴素但重要的道理:LLM 不是”数据库”,它没有”无限记忆”。把全部信息塞进去,只会让它”选择性失明”。

第三次(成功的)方案:动态 Schema 注入

最后的方案是 Dify 编排 Workflow + LangChain 封装工具,分阶段处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户提问
↓
① 意图理解 + 实体抽取(LLM + 业务词典)
↓
② 表结构检索工具:根据实体召回 1-3 张候选表
↓
③ Schema 注入:把这几张表的字段注释 + 表关系 + 同义词喂给 LLM
↓
④ SQL 生成(LLM)
↓
⑤ SQL 校验 + 权限注入(Python 工具,强制加租户/片区过滤)
↓
⑥ 执行 + 错误反馈(失败带回错误重试,最多 3 次)
↓
⑦ 结果转自然语言(LLM)

关键设计:实体抽取

第一步的”实体抽取”看似简单,做起来很坑。

客户说”片区 3”,模型可能理解成”区域 3”、”Area 3”、”District 3”——这些在业务里都是同一个意思,但 SQL 写错一个就查不到数据。

我的解法:建了一个业务实体词典({"片区": "district_id", "营业厅": "business_hall_id", ...}),模型先抽取”业务实体”(如”片区 3”),再用词典映射到具体字段名。这步看似多此一举,但准确率从 50% 提升到 85%。

关键设计:表关系注入

字段名解决了,表之间的关系还是坑。

客户问”某营业厅的上月用气量”,涉及 business_hall(营业厅)、meter(表具)、consumption_record(用气记录)三张表。模型如果不知道表间关系,要么漏 JOIN,要么 JOIN 错。

我的解法:表关系手动建库(不是元数据自动抓),包含主外键、JOIN 条件、业务别名。Schema 注入时一起塞给模型。

关键设计:SQL 安全

这是我第一次真的被吓到。

当时做测试时,我问模型”删除所有表的数据”,模型竟然真的生成了 DELETE FROM ... 语句!如果这个 SQL 被执行了,整个生产数据库的数据就清空了。

之后我加了三层防护:

  1. 白名单 SQL 类型:只允许 SELECT,其他全部拒绝
  2. 语法校验:用 sqlparse 解析,检查是否有危险关键字
  3. 强制权限注入:自动追加 WHERE tenant_id = current_user_tenant,即使模型忘了加权限,工具也会补上

这一步绝对不能省。AI 生成的 SQL 在你眼皮底下执行是不可接受的风险。哪怕牺牲一点灵活性也要保住安全。

关键设计:错误重试

真实场景里 SQL 第一次执行失败率大概 20-30%。失败原因五花八门:

  • 字段名拼写错(模型幻觉)
  • 关联表忘了加 JOIN
  • GROUP BY 字段没出现在 SELECT
  • 数值比较类型不匹配(字符串 vs 整数)

我的做法:把错误信息原样带回给模型,让它”看到错误再改”。这招对语法错和字段名错的修复率非常高,但对”模型本身理解错问题”的修复率很低(这种就只能人工干预了)。

真实的业务效果

上线后我们统计了一下(统计的只是部分典型场景,不是全部):

  • 约 60-70% 的简单问数需求能直接被 AI 解决(”某片区用户数”、”上月用气量排名”)
  • 约 20% 需要业务人员重写问题(问题本身描述不清)
  • 约 10% AI 答错或答不了(复杂 JOIN、跨业务线的统计)

注意:我没有写”准确率 92%”这种数字,因为业务场景的”准确”很难定义。一个问题问”用户数”,模型返回的结果是”1 万 2 千 3 百”,和真实”1 万 2 千 3 百零 5”对不对?我不知道,也不想编。

一些”难听话”

写了这么多技术,最后说几个我作为开发者真正想说的事:

  1. “AI 让业务人员不用懂技术”是宣传话术。真实情况是业务人员还是要懂业务、还是要能描述清楚问题。我们的系统能解决的是”让 80% 的常规问数需求不用再提单”,剩下 20% 还是要人介入
  2. 不要承诺”100% 准确”。任何承诺 AI 100% 准确的产品,最后都会被现实打脸。客户接受 80% 准确的前提是他能知道哪 20% 不准——所以一定要给”我对你问题的理解”、”SQL 长这样”、”结果是这样”的中间过程可见
  3. 业务方和 AI 之间的”语言鸿沟”比想象的大。同样是”上个月用气量”,业务 A 指的是”自然月”,业务 B 指的是”账单月”——这些不写到 prompt 里,模型永远不知道
  4. “AI 项目从 demo 到生产”最大的障碍不是模型,是数据治理。如果你公司的表结构混乱、命名不规范、缺注释、同义词没人维护——那 AI 再强也没用,垃圾进、垃圾出

最后

Text-to-SQL 在 demo 阶段是”调 API 跑通就行”,但到生产阶段就是另一个故事。真正的工程挑战:

  • 业务术语 / 同义词库怎么建
  • 表结构 / 关系怎么管理
  • SQL 安全怎么兜底
  • 错误重试的策略怎么设计
  • 业务方预期怎么管理

这些事情没有银弹,每个项目都要自己趟一遍。如果你也在做类似的落地,欢迎交流踩过的坑。