AI 数据问答权限:自然语言不能绕过指标边界
一、数据问答越自然,越容易让人忘记权限
AI 数据问答让用户直接输入"上周新增用户为什么下降",系统自动查数并解释。体验很顺,但风险也很明显。自然语言入口不能绕过 BI 权限、指标口径和数据分级。用户没权限看的字段,不能因为换成问句就被查出来。
为什么权限要放在查询生成之前而不是结果脱敏之后?因为 LLM 生成 SQL 的过程本身就会"看见"表结构,如果你的 Schema 里有一张
salary_detail表,模型在生成查询计划的时候就已经知道这张表的存在了。等结果回来再做行级过滤,数据已经被读出来、传过来了,审计日志里也会有这条查询记录。正确的做法是:在 Prompt 里只给模型他想看的那部分 Schema,让模型根本不知道有这张表的存在。这就是"Schema 裁剪"的核心价值——不是挡住数据,是连表名都不让模型看到。
数据问答系统必须把权限放在查询生成之前。模型只能在用户可见的数据域里生成 SQL 或查询计划。权限不能等查询结果出来后再脱敏,因为那时数据已经被访问了。
二、问答链路要先做语义解析和权限裁剪
一个安全的数据问答链路,应先识别用户意图和实体,再根据权限裁剪可用指标、维度和表。模型生成查询时,只能看到裁剪后的 Schema。
flowchart TD A[自然语言问题] --> B[意图和实体识别] C[用户权限] --> D[Schema 裁剪] B --> E[可用指标维度] D --> E E --> F[生成查询计划] F --> G[SQL 审核] G --> H[执行查询] H --> I[生成回答]SQL 审核也不能省。模型可能生成跨权限 Join、全表扫描或危险函数。审核层负责拦住不合规查询。
为什么 AI 生成的 SQL 必须经过审核层而不是直接执行?一个典型的翻车案例:用户问"上季度各部门收入对比",模型自动 Join 了财务表和 HR 表 —— 用户没有 HR 表的权限,但因为 LLM "觉得"需要部门名称字段,就自动跨了权限边界。SQL 审核层要在执行前做三件事:检查 JOIN 的表全在用户权限范围内、拦截
SELECT *这种全列拉取、禁止DROP/DELETE等危险语句。这不只是安全需求,更是合规红线。
三、查询计划比直接生成 SQL 更可控
相比让模型直接输出 SQL,可以先输出结构化查询计划,再由系统编译成 SQL。这样更容易校验指标和维度。
from dataclasses import dataclass @dataclass class QueryPlan: metric: str dimensions: list[str] filters: dict[str, str] def validate_plan(plan: QueryPlan, allowed_metrics: set[str], allowed_dims: set[str]) -> None: if plan.metric not in allowed_metrics: raise PermissionError(f"metric not allowed: {plan.metric}") denied = set(plan.dimensions) - allowed_dims if denied: raise PermissionError(f"dimensions not allowed: {denied}")这个校验很朴素,但能表达核心原则:模型不能凭自然语言扩大权限。所有字段都要经过系统确认。
为什么不用"允许列表"而用"拒绝列表"做权限校验?用拒绝列表(黑名单)的话,你今天禁了
salary,明天 HR 加了个salary_v2字段就绕过去了。正确做法是白名单模式:用户的allowed_metrics和allowed_dims是在登录时就确定好的集合,模型无论生成什么字段名,你都只跟这个集合做交集。不在集合里的直接拒绝,不存在漏网之鱼。
四、拒答也要解释清楚,不要只说没有权限
当用户无权查看某个指标时,系统应说明可见范围。例如“你可以查看部门汇总,但不能查看个人明细”。这样比冷冰冰的拒绝更容易被接受。
还要处理间接泄露。即使用户看不到个人明细,也可能通过多次过滤推断个体数据。系统应设置最小聚合粒度和查询频率限制。小样本结果需要隐藏或合并。
最后,所有问答要留审计日志。记录用户问题、生成计划、执行 SQL、返回行数和权限判断。出了问题,必须能复盘自然语言如何变成查询。
缓存也要遵守权限。数据问答为了提速,可能缓存问题到结果的映射。缓存 key 必须包含用户权限版本、指标版本和过滤条件,不能只按问题文本缓存。否则两个用户问同一句话,权限不同却命中同一份结果,就会造成越权泄露。
为什么缓存 key 要包含"用户权限版本"而不只是用户 ID?因为权限是动态的。今天 A 用户能看部门汇总,明天他调岗了,权限被收回。如果你的缓存 key 是
user_id:question_text,缓存 TTL 设了 30 分钟,这 30 分钟内 A 能看到的数据仍然在缓存里躺着。把权限版本号(比如 RBAC 的 revision 号)拼进 key,权限一变,缓存自动失效。
🚨 踩坑提醒
别把 Schema 全量喂给 LLM。很多开发者图省事,把整个数据仓库的所有表和字段列出来作为 Prompt 上下文。一个中型数仓两三百张表,字段上千个,模型不仅处理慢,还会"脑补"出跨表 Join。正确的做法是只给用户权限范围内的表,且每张表只暴露聚合后的指标,不要暴露明细字段。
PermissionError抛出来要解释原因,不要只甩 403。用户问"我的绩效排名是多少"被拒绝,如果只返回"无权限",用户会觉得系统有 bug。返回"当前角色仅支持查看部门级汇总数据,不支持查看个人明细"才是对的做法。拒答日志也要单独存,方便 HR/法务后续审查。小样本结果必须隐藏。即使用户有权看某个维度,当过滤后的结果行数 < 阈值(如 5 条),仍然存在间接泄露个体信息的风险。比如"部门里 35 岁以上、薪资最高的员工"——如果返回了唯一一个人,就等于点名了。系统中应设
min_result_threshold,低于阈值的显示为<5或直接不显示。
五、总结
AI 数据问答的安全边界必须放在查询生成之前。系统应先按用户权限裁剪 Schema,再让模型生成结构化查询计划,并通过审核层编译执行。自然语言提升了入口体验,但不能提升用户权限。数据问答越方便,越要把指标、维度和审计边界写清楚。