给 AI 开一个只读账号:企业数据库问答Agent的权限设计实战
把AI接到生产数据库,最大的风险不是答错,而是答对了一次不该答的问题。本文给出四层权限设计:账号层、语义层、查询层与审计层,附可直接抄的只读账号配置与SQL拦截规则。
一个真实的事故预演
某团队做了个”问问数据”的内部Agent,接的是业务库。上线第一周,有人问:“上个月每个客户的订单总额,按客户名称排序。”
Agent 生成了正确的 SQL,也成功执行了。问题是,那个库里有三张表包含客户真实姓名和手机号,而提问的人属于运营组的实习生——他看到的东西,超出了他的权限。
这不算模型的错。它只是执行了一个语法正确的查询。错的是权限模型是围绕”人”设计的,而 Agent 是一个新的身份。
核心原则:Agent 不是用户,是第三方系统
把 Agent 当作新的服务账号来处理,而不是当作”用户的操作代理”。这意味着:
- 它有自己的凭证
- 它有独立的、最小化的权限
- 它的所有行为都可追溯到”哪个用户通过哪个 Agent 发起了什么”
只有第一层做到这点,后面的设计才有意义。
第一层:账号权限
建一个专用只读账号,并且只授权到视图层,而不是表层。
几条硬规则:
SELECT权限只授予业务视图(v_orders、v_customer_metrics),不授予原始表- 明确
REVOKE掉INSERT/UPDATE/DELETE/TRUNCATE - 禁止访问
information_schema全量元数据(否则等于给了你整个数据库的地图) - 设置
statement_timeout(如 10 秒),并设置work_mem上限,防止一次大查询拖垮库 - 限制返回行数:通过视图做
LIMIT,或使用只读副本 + 服务端游标
更进一步:敏感字段在视图里就该被脱敏或剔除。不要指望模型”不会查那一列”。
第二层:语义层(关键的一步)
只给表结构,模型会自己发挥。给它一个语义层,也就是事先定义好的指标与维度:
指标:GMV = SUM(order_amount) WHERE status IN ('paid','shipped')
维度:客户名称(已脱敏为 客户ID + 行业 + 区域)
时间口径:自然月,UTC+8
禁用:任何涉及手机号、身份证、地址原文的查询
这一层的作用有三个:
- 收敛歧义:不同人对”活跃用户”的定义不同,语义层说了算
- 隐式权限:语义层里没有的字段,模型根本不会去查
- 可评测:指标口径固定了,评测结果才可比
第三层:查询层拦截
在 Agent 和数据库之间放一道 SQL 网关,做静态检查:
- 只允许
SELECT - 禁止子查询里出现未授权的表名
- 禁止
;分隔的多语句 - 对
SELECT *直接拒绝(要求显式列名) - 解析执行计划,估算扫描行数,超阈值直接拒绝并提示用户缩小范围
- 对返回结果做 PII 正则扫描,命中即拦截并告警
这层检查必须用 SQL 解析器做(如 sqlglot),而不是正则匹配。正则会被注释、大小写、函数包裹绕过。
第四层:审计与回灌
每次查询记录:用户身份、原始问题、生成的 SQL、执行耗时、返回行数、是否被拦截。
这不仅是合规材料,更是改进素材:
- 被拦截最多的查询类型 → 补充语义层定义
- 用户重问最多的题 → 说明生成质量不足
- 高频查询 → 可以考虑做成固定报表,直接绕过 LLM
一个反直觉的经验
跑一段时间后你会发现,最有效的安全措施不是更强的提示词,而是更窄的权限和更少的表。
我们做过对比:限制到 12 个业务视图后,Agent 的”答错率”和”越权率”同时显著下降。原因很简单——可选空间小了,模型的出错空间也小了。
落地清单
- 建只读账号,只授权业务视图
- 建语义层,把敏感字段排除在定义之外
- 部署 SQL 网关,用解析器做静态检查
- 设置超时与行数上限
- 全链路审计日志,能追溯到人
- 每月复盘拦截记录,回灌到语义层
把这六件事做完,“AI 查数据库”才从一个危险的玩具,变成一个能放心开放给运营的工具。