Text-to-SQL / ChatBI 生产系统设计:语义层、安全边界与可信回答
“让用户用自然语言查询数据库”是大模型应用面试里极其常见、也特别容易答成 Demo 的题目。Demo 只要把表结构塞进 Prompt,要求模型返回 SQL,再执行即可;企业级 ChatBI 的难点则是:同一个“销售额”到底含不含退款、一个用户能看哪些行和列、生成的 SQL 会不会扫全表、结果能否解释、错误时怎样修复,以及怎样证明回答没有把口径讲错。
本页只聚焦 Text-to-SQL / ChatBI 的生产控制面。通用 JSON 约束见 结构化输出,数据源增量与权限变化见 企业 Connector 增量同步与撤权,模型调用的流控与成本治理见 LLM 准入与过载控制。
一、先反驳错误前提:这不是“翻译 SQL”
用户问“华东区本季度销售额环比怎么样”,系统至少需要解决:
- 实体消歧:华东区是销售组织、收货地址还是客户归属地?
- 指标口径:销售额是含税下单金额、支付金额、发货金额还是扣除退款后的净额?
- 时间语义:“本季度”采用自然季度还是财务季度,时区和数据截止点是什么?
- 权限边界:提问者可否看到所有地区、所有客户、利润字段或明细行?
- 查询代价:查询会否扫描数十亿行、跨多个数据仓库,或造成资源争用?
- 结果表达:环比结论的数值、分母、异常和引用的数据集版本能否被复核?
因此,ChatBI 的核心不是把自然语言映射到任意 SQL,而是将问题映射为一个受治理的分析意图,再由语义层、权限层和查询编译器生成受限查询。LLM 在这条链路中是意图理解与候选计划器,而不是数据库管理员。
二、先建设语义层:让业务词汇有唯一含义
直接把原始 schema 暴露给模型,通常会得到看似合理却口径错误的 SQL。建议为每个可用指标、维度和实体建立语义目录:
semantic_metric
-> name: net_revenue
-> definition: paid_amount - refunded_amount, excluding cancelled orders
-> grain: order
-> allowed_dimensions: region, product_category, paid_date
-> source_model: mart_order_revenue_v4
-> owner / freshness SLA / certification status
semantic_dimension
-> name: sales_region
-> aliases: 华东, 东区
-> business meaning / hierarchy / row-level policy这里的 definition 最好对应可审查的指标代码、受认证的视图或 dbt/BI 模型,而不是一段仅供阅读的自然语言说明。模型选择 net_revenue 后,由确定性编译器展开其计算逻辑。这样“销售额”口径变更只更新语义层,避免每个 Prompt、报表和 Agent 各自解释一次。
指标治理要点
| 问题 | 生产做法 |
|---|---|
| 同名不同义 | 指标 ID 与显示名称分离;名称冲突时要求澄清或给出候选口径 |
| 多表连接 | 在语义层声明允许 join 路径、基数和去重规则,不让模型自由猜 join key |
| 数据新鲜度 | 显示数据截止时间、延迟和是否为实时/日批;过期时降低结论强度 |
| 认证与责任 | 标识 certified 指标、owner、测试状态和弃用日期 |
| 单位与币种 | 将单位、汇率日期、精度和四舍五入规则作为类型信息,而非文本后处理 |
面试亮点:我会先问“销售额的业务定义是否已经固化在语义层”,而不是立刻讨论模型选型。没有指标治理,Text-to-SQL 的准确率没有可验证的上限。
三、数据目录检索:只给模型任务相关的 schema
大型数仓往往有数千张表,把完整 DDL 放进上下文既昂贵又会增加歧义。应建立可检索的数据目录,包含表/列说明、业务别名、指标定义、血缘、示例问题、认证状态、敏感等级和最近使用信号。
在线检索通常分为三步:
natural language question
-> intent extraction (metric / dimensions / time / comparison / ambiguity)
-> retrieve semantic candidates and approved query patterns
-> select a compact schema context with join graph and policies检索不应只用向量相似度。用户提到“退款”时,关键词、业务别名和指标依赖很关键;用户角色、租户、数据域和表的新鲜度也必须作为过滤条件。高风险或低置信度场景返回“请确认你指的是净销售额还是下单金额”,比模型擅自选一个更可信。
四、从自然语言到受限查询计划
推荐先输出一个中间表示(IR/DSL),再编译成特定引擎 SQL,而不是直接生成可执行 SQL:
{
"metric": "net_revenue",
"dimensions": ["sales_region"],
"filters": [{"field": "paid_date", "op": "between", "value": ["2026-04-01", "2026-06-30"]}],
"comparison": {"type": "previous_period"},
"limit": 100,
"explanation_required": true
}IR 的 schema 只允许已登记的指标、维度、比较方式和过滤运算符。服务端校验后再由编译器生成 SQL,统一加上行级权限、时间范围、资源组、LIMIT 和安全的参数绑定。这样能避免模型写入 DDL/DML、调用危险函数、拼接注入字符串或绕开租户条件。
对于确实需要自由 SQL 的高级分析师模式,也必须进入独立的 sandbox:只读服务账号、表级 allowlist、短超时、扫描字节上限、审计和显式风险提示;不要把“高级模式”变成无约束的生产数据库入口。
五、SQL 安全不止禁止 DELETE
SQL AST 解析器应在真正执行前做静态检查。常见规则如下:
- 只允许
SELECT/受控的WITH,拒绝多语句、DML、DDL、存储过程和外部网络函数; - 表、列、函数、join 路径必须来自语义目录和 allowlist;
- 强制注入租户、组织、地域或数据域的行级过滤,且禁止用户文本覆盖;
- 拒绝笛卡尔积、无界时间范围、未聚合的大明细扫描和高基数无限排序;
- 用查询引擎的
EXPLAIN/dry-run 估计扫描量、分区命中和成本; - 超过预算时改写为抽样、汇总、异步任务,或要求用户缩小范围;
- 为每次查询设置 statement timeout、并发配额、结果行数和导出字节上限。
“模型不会生成删除语句”不是安全策略。实际风险更多来自一个合法的 SELECT 扫描全库、把敏感列加入结果、错误 join 放大金额,或在查询日志中泄露用户提供的身份信息。
六、权限:先过滤数据域,再决定回答能否展示
权限至少有三层:
Question access: 用户能否调用这个数据域/指标?
Query access: 编译后的 SQL 是否带上正确的行、列、脱敏策略?
Answer access: 结果摘要、图表、明细下载和引用能否分别展示?例如区域负责人可以看本区域汇总,不能看其他区域明细;财务可以看毛利,销售只能看收入;一个允许回答“本季度增长 8%”的用户,也未必可以下载客户级明细。权限计算必须在数据库或可信查询网关中强制执行,不能只让模型“不要提到利润”。
如果检索到无权限的表描述、历史样例或缓存结果,同样可能泄露结构与数据。目录检索、Prompt 组装、语义缓存、日志与评测集都要继承权限标签。
七、纠错循环:让系统修复可诊断错误,而不是盲目重试
查询失败后,不能把数据库原始错误全文回灌给模型。错误消息可能泄露 schema,也可能诱导模型反复改写。应先映射为受控错误类型:
| 错误类型 | 例子 | 系统动作 |
|---|---|---|
| 意图歧义 | “活跃客户”定义不唯一 | 追问用户,展示定义差异 |
| 语义不可达 | 没有认证指标或允许的 join | 拒答并指向数据 owner |
| 编译失败 | 不支持的聚合组合 | 返回安全的错误码,可有限次修复 IR |
| 成本超标 | 预估扫描 20TB | 建议时间范围/聚合,或转异步审批 |
| 空结果 | 筛选条件无数据 | 展示已解释的过滤条件,建议放宽 |
| 执行异常 | 数据仓库暂不可用 | 重试或降级,不重新解释业务口径 |
纠错循环应该有最大轮数和 token/时间预算。每一轮记录输入 IR、校验错误和改写原因;若连续失败,交由人工数据分析师或返回可操作的澄清问题。系统不能把“不知道”伪装成一个随意修改后成功执行的查询。
八、结果对账与可信表达
SQL 执行成功并不代表答案可信。结果层应做:
- 类型和范围检查:金额、百分比、分母、时间边界、空值和异常值是否符合指标规则。
- 对账查询:对高价值指标使用独立聚合、历史快照或认证 BI 看板抽样比对。
- 结论约束:模型只能依据返回行和计算字段描述趋势;无数据时不能生成归因。
- 可解释结果卡:显示指标定义、过滤条件、时间范围、数据截至时间、查询 ID 与数据集版本。
- 图表契约:图表由结构化数据生成,模型给标题和说明,不允许让模型凭文本画数值。
对于“为什么下降”这类因果性问题,要明确区分描述统计、相关性和已验证因果。系统可建议按渠道、地区、品类分解,但不应把相关变化说成确定原因,除非接入了相应的实验或因果分析证据。
九、架构与状态机
Client
-> Auth / Policy Gateway
-> ChatBI Orchestrator
-> Semantic catalog & schema retrieval
-> LLM intent-to-IR service
-> IR validator / policy injector
-> SQL compiler -> query gateway -> warehouse
-> result validator / reconciliation
-> answer renderer / chart service
-> audit, trace, evaluation, feedback stores一次请求可进入以下状态:
received -> clarifying -> planning -> validating -> estimating
-> running -> reconciling -> answering -> completed
-> blocked / denied / over_budget / failed / cancelled每一步持久化 request_id、用户与数据域、语义层 revision、候选 IR、策略决定、编译 SQL 的哈希、查询 ID、成本、结果快照和最终解释。对长查询使用异步任务与可取消状态,流式返回进度时不得提前暴露未经权限过滤的中间数据。
十、评测体系:SQL 执行正确率不够
离线 golden set 应覆盖业务口径、同义表达、时间语义、权限边界、空数据、跨表 join、故意模糊问题和攻击输入。建议分层指标:
| 指标 | 说明 |
|---|---|
| intent / metric accuracy | 是否选对指标、维度、过滤和比较周期 |
| execution accuracy | SQL 或 IR 运行结果是否与标准结果等价 |
| semantic correctness | 结果虽能运行,业务口径是否正确 |
| policy violation rate | 是否发生越权表/列/行访问或导出 |
| cost compliance | 扫描量、时延、重试次数是否在预算内 |
| answer faithfulness | 文字结论是否被结果集与定义支持 |
| clarification quality | 对歧义问题是否正确追问而非猜测 |
注意 execution accuracy 的陷阱:两条 SQL 可能文本不同但结果等价,也可能在测试小数据上恰好结果相同。评测应比较语义 IR、关键中间结果和多组对抗数据。上线后将用户采纳、手动改写、空结果、成本超标、纠错失败和人工标注沉淀为 bad case;指标口径变更会使旧样本失效,因此评测集也必须版本化。
十一、系统设计答题框架
面试官让你“设计一个企业 ChatBI”时,建议按以下顺序组织:
- 澄清约束:面向业务员工还是分析师?是否允许明细下载?数据规模、刷新 SLA、合规地域和峰值 QPS 是什么?
- 定义成功指标:不只看 SQL 成功率,还看口径正确率、越权率、P95 延迟、扫描成本和人工澄清率。
- 语义层先行:由数据团队维护认证指标、维度、join 图、别名和 owner;模型只从许可目录选择。
- IR + 编译执行:LLM 输出受约束 DSL,服务端验证并注入策略,编译器产生参数化 SQL,网关 dry-run 后执行。
- 结果与解释分离:确定性服务负责数据和图表,LLM 只基于受控结果写摘要,并展示口径和数据截至时间。
- 可靠性与安全:预算、超时、限流、异步、幂等、RLS/CLS、审计、脱敏和外发控制。
- 闭环评测:黄金集、影子流量、用户反馈、失败分类和语义目录变更回归。
这样回答可以清晰体现:你没有把安全寄托在 Prompt,没有把业务口径寄托在模型记忆,也没有把“SQL 能跑”当成业务正确。
十二、高频追问与参考回答
Q1:为什么要用 DSL,直接让模型写 SQL 不更灵活吗?
DSL 用受控表达能力换取可校验性:可枚举指标和维度、统一注入权限、阻止危险语法、稳定做成本估算,并能跨 Trino、Snowflake、ClickHouse 等引擎编译。高级分析场景可提供 sandbox SQL,但不能让默认聊天入口拥有无限表达能力。
Q2:Schema RAG 和普通 RAG 有什么不同?
检索对象不是段落,而是具有结构关系与权限标签的表、列、指标、join 路径、样例与血缘。召回目标不是“最相似文本”,而是构成一个能回答当前问题、且合法可执行的最小 schema 子图。后续必须经语义和 AST 校验,不能把检索结果直接当真。
Q3:如何防止用户用自然语言绕过行级权限?
权限不由模型决定。身份与数据域由服务端传入,编译器或查询网关强制添加不可移除的谓词,数据库再用 RLS/视图做最后防线。Prompt、目录检索、缓存命中和最终答案都要使用相同的权限上下文。
Q4:SQL 成功但结果明显不合理时怎么办?
先检查指标粒度和 join 基数,典型问题是多对多 join 导致金额重复。对认证指标优先查询预聚合模型;对关键结果运行独立对账或阈值检测;发现异常时返回“结果需要复核”而不是生成自信结论,并将样本进入评测集。
Q5:怎样控制一次“帮我分析过去三年所有订单”的成本?
在执行前 dry-run 估算扫描量和队列时间,按用户/租户/任务设预算。超过阈值时建议按月聚合、限定地区、使用物化视图,或转为异步任务并等待审批;同时设置引擎级字节上限和超时。不能只依赖模型主动写 LIMIT,因为 LIMIT 未必减少底层扫描。
十三、60 秒项目讲法
“我将 ChatBI 设计为语义驱动的受限查询系统,而不是自然语言到任意 SQL 的翻译器。数据团队先维护认证指标、维度、join 路径和业务别名;用户问题经权限过滤后,模型只输出受 schema 约束的查询 DSL。服务端校验 DSL、强制注入行列权限与时间/成本策略,再由编译器生成参数化 SQL,并在 dry-run 后执行。结果层会校验粒度、金额范围和数据新鲜度,对关键指标做对账;模型只基于受控结果生成解释,同时展示指标口径、过滤条件和查询版本。我们用口径准确率、越权率、扫描成本、澄清质量和结论忠实度评测,而不仅仅看 SQL 能否执行。”
这段项目讲法覆盖了企业大模型应用岗最常见的连环追问:为什么需要 RAG、如何防越权、如何防幻觉、如何控成本、如何评估,以及当结果不可信时怎样降级。