日期:2026-07-16
某头部零售客户的数据团队常年被"取数"需求淹没:业务侧一个临时分析要经历提需求、排期、写 SQL、核对、出图五步,平均响应周期约 2-3 天,且业务人员自己写的 SQL 经常查错表、连错维度,返工频繁。我们当时判断,痛点不在算力也不在数据质量,而在"自然语言到 SQL"这最后一公里的门槛。这个项目里我们采用了"大模型生成 SQL + 查询层强制权限校验 + 看板沉淀"的架构,把取数能力直接交到业务手里,同时让每一次自动查询都跑在受控的权限边界内。那条硬红线是:生成的 SQL 绝不能直接落地执行,必须先过一道表/列级权限与注入校验。
典型链路是:业务人员在对话面板用自然语言提问(如"上月华东区各门店的 GMV 和环比"),我们采用的 AI 网关把请求路由到大模型做 NL2SQL,模型产出 SQL 草稿与置信度;SQL 先送进查询代理层做白名单校验与参数化改写,确认只访问授权库表、无危险语句后,在只读从库执行;结果自动选图(折线/柱状/饼)并回填到可编辑看板。高频问句可一键"钉"成固定看板卡片,沉淀为经营驾驶舱。整个过程数据不出本地集群,模型只看到表结构语义描述,看不到真实业务数据行,从源头降低泄露面。
我们当初也试过把数据库样例行直接塞进 prompt 让模型"照着写",短期正确率确实高,但安全团队一票否决:样例里混进了真实订单与会员手机号,存在泄露风险。所以我们把 schema 知识库和真实数据彻底解耦——模型训练与推理期只接触表注释、字段类型、维度枚举值这类"骨架",真实"血肉"永远留在查询代理层之后的只读从库。这个解耦让同一个 NL2SQL 能力后来能平滑复用到某股份制银行的经营分析场景,因为权限与数据边界的架构是通用的,换库只换 schema 描述即可。
模型生成的 SQL 最大的两个风险是"写错"和"写坏"。写错靠置信度与执行前 dry-run 兜:生成后先在影子库 EXPLAIN 一遍,执行计划异常或命中预期行数偏差过大就打回重生成。写坏靠注入防护:所有 SQL 强制走参数化,且查询代理层用 AST 解析器白名单拦截 DROP/UPDATE/DELETE/INSERT 以及 information_schema 探测,任何非 SELECT 直接拒绝。配置示意:
sql_guard:
allow: [SELECT]
deny: [DROP, DELETE, UPDATE, INSERT, TRUNCATE]
forbid_schema_probe: true
paramize: true这套防护把我们遇到的恶意/异常 SQL 拦截率拉到 100%,未发生过一次越权写,也成为后续多租户接入的通用底座。
零售客户库表多达数百张,模型容易把"销售额"映射到错的事实表。我们把表结构、字段注释、常用维度关系抽成向量化的 schema 知识库,提问时先检索最相关的表/列片段注入 prompt,给模型"带地图"生成,而不是盲猜。同时让模型一并输出置信度与所引用的表清单,低于阈值(0.7)的请求不自动执行,转为"请确认是否指 XX 表"的交互澄清。实测带 schema 检索的生成一次性正确率比裸生成提升约 25 个百分点,误连维度表的情况几乎消失。
列级白名单不够——业务人员只能看自己区域的数据。我们在查询代理层做行级约束注入:根据用户身份,在生成 SQL 的 WHERE 后自动追加组织过滤条件(如 region IN (...)),且这个条件业务侧不可见、不可被模型生成的 SQL 覆盖。例:
-- 模型生成
SELECT store_id, SUM(gmv) FROM fact_sales WHERE dt='2024-11' GROUP BY store_id;
-- 代理层注入后
SELECT store_id, SUM(gmv) FROM fact_sales WHERE dt='2024-11' AND region IN ('East') GROUP BY store_id;这样即便模型"好心"帮用户查了全区域,也被边界条件拦在授权范围内,越权扫描在 SQL 执行前就被截断。
业务不信任"黑盒吐数"。我们要求模型在出图同时生成一段自然语言解读,并附上实际执行的 SQL 与所用表,用户可点开核对"这个数怎么来的"。看板卡片默认展示 SQL 来源,异常波动自动标注可能的原因维度。可解释性直接把业务侧的采纳率从约 40% 提到约 75%,也减少了大量"这数对不对"的来回确认。
该项目上线约两个月后的脱敏示意数据:
| 指标 | 改造前 | 改造后 | 提升幅度 |
|---|---|---|---|
| 问数自助率 | 约 15% | 约 70% | 提升约 55 个百分点 |
| SQL 一次性正确率 | 约 55% | 约 88% | 提升约 33 个百分点 |
| 平均取数耗时 | 约 2 天 | 约 10 分钟 | 降至约 1/280 |
| 越权查询拦截 | 依赖人工 | 约 100% | 全程自动拦截 |
案例片段(已脱敏): 某区域运营提问"帮我看全国门店上周销量",模型生成跨全量
fact_sales的查询。查询代理层识别其权限仅为华东区,注入region IN ('East')后执行,并返回提示:[GUARD] user=op_east role=region_limit -> inject WHERE region IN ('East'); deny cross-region scan用户侧看到的是仅华东区的结果与提示"已按您的区域权限过滤",实际未触达其他区域一行数据。
Text2SQL 真正难的不是"生成一条能跑的 SQL",而是"生成一条只对、且只在权限内的 SQL"。把权限校验、注入防护、可解释这三道关卡前置到查询层,自然语言问数才从演示玩具变成业务日常。