让 AI 查询生产库前的六项防护
文章提出六项数据库接入措施:独立只读角色、受限视图、查询超时、结果行数限制、只读副本和日志。示例使用 PostgreSQL,并声明所有代码片段已在 PostgreSQL 16 上运行验证。
AI 深度解读
让 AI 用自然语言查生产数据库,难点不只是把问题翻成 SQL,还要防止它读到不该看的数据、拖慢业务,以及给出一个看似可信的错误数字。原文作者开发 AI Agent Studio,建议接入前先做好六件事:独立只读账号、视图隔离、超时、限行、读副本和完整日志。
这套方案值得参考,因为它把约束落实到了数据库和查询工具里。不过,作者在 PostgreSQL 16 上跑通过示例,不等于验证了所有安全边界;文末的测试说明还有一处需要纠正。
先限制能读什么,再讨论 SQL 写得准不准
作者首先要求:不要复用应用的数据库账号。给 Agent 单独创建 ai_agent,只允许读取 reporting schema 中的对象。
下面这段 SQL 同时设置了权限范围、连接数和默认事务模式:
-- 单独存放允许 Agent 访问的对象
CREATE SCHEMA reporting;
CREATE ROLE ai_agent LOGIN PASSWORD 'use-a-long-random-password' CONNECTION LIMIT 5;
GRANT CONNECT ON DATABASE shop TO ai_agent;
GRANT USAGE ON SCHEMA reporting TO ai_agent;
GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO ai_agent;
-- 允许读取此 schema 中后续创建的视图和表
ALTER DEFAULT PRIVILEGES IN SCHEMA reporting
GRANT SELECT ON TABLES TO ai_agent;
-- 额外防护:事务默认以只读模式开始
ALTER ROLE ai_agent SET default_transaction_read_only = on;
要盯住的是 GRANT SELECT:真正限制写入的是权限。default_transaction_read_only = on 只是额外防护,会话可以通过 SET 关闭它;CONNECTION LIMIT 5 则避免 Agent 大量占用连接。
还有一个容易遗漏的前提:ALTER DEFAULT PRIVILEGES 只影响执行该命令的角色以后创建的对象。因此,应由实际创建视图的用户执行。密码放进 secrets manager,不要塞进提示词或配置文件。
接下来,作者建议只开放视图,不开放原始表。例如,reporting.orders_summary 从 public.orders 和 public.customers 取出订单编号、时间、状态、金额与国家,去掉姓名、邮箱和地址。
默认情况下,视图使用所有者的权限访问底层表,所以 Agent 可以读视图,却没有直接读取原始表的权限。如果创建时设置了 security_invoker = true,规则会改变,调用者需要具备底层表权限。
视图既限定数据范围,也可以帮助模型理解业务口径。
作者用 COMMENT ON VIEW reporting.orders_summary 说明每行对应一笔订单,total_amount 是含税美元金额;再用 COMMENT ON COLUMN reporting.orders_summary.status 列出 pending、paid、refunded、cancelled。不少 AI 工具会读取这些注释,但原文没有说明具体工具的支持情况。字段名也应直接表达含义,例如用 total_amount_usd 代替 amt2。
限行、超时和副本,分别解决什么问题
只读查询同样可能拖慢生产库。Agent 如果生成一个没有过滤条件的大表关联,权限系统不会因为它成本高就拒绝执行。
作者给 ai_agent 设置了两项限制:statement_timeout = '5s',限制单条语句执行时间;idle_in_transaction_session_timeout = '30s',处理事务开启后长期空闲的会话。角色设置对新会话生效,修改后需要重新连接。
原文认为,多数业务问题应在 5 秒内完成,超过这个范围应考虑正式报表或预计算视图。这是作者的工程取舍,并没有业务负载对照数据支持。
工具层还要限制返回量。下面的 Python 包装器将结果控制在 500 行,并拒绝包含额外分号的输入:
import psycopg2
MAX_ROWS = 500
def run_agent_query(conn, sql: str):
sql = sql.strip().rstrip(";")
if ";" in sql:
raise ValueError("Only one statement is allowed")
with conn.cursor() as cur:
cur.execute(f"SELECT * FROM ({sql}) AS agent_q LIMIT {MAX_ROWS}")
columns = [d.name for d in cur.description]
return columns, cur.fetchall()
conn = psycopg2.connect(dsn) # 使用 ai_agent 角色的凭据
conn.set_session(readonly=True, autocommit=True)
关键是 cur.execute 这一行:把查询放进子查询,再施加 LIMIT 500。普通 DELETE 不能直接塞进这个位置,多语句输入也会被分号检查拒绝。不过,字符串内部合法的 ; 同样会被拒绝,这是作者接受的取舍。
返回最多 500 行,不代表数据库只处理了 500 行。
聚合、排序和关联仍可能扫描大量数据,所以限行不能替代超时,也不能替代数据库权限。原文同样明确说,这个包装器只是护栏,不是安全边界。
如果已有读副本,作者建议将 Agent 连到副本,减少分析查询对主库结账等业务的影响,并增加一层只读约束。没有副本时,应更严格地设置超时、返回量,并考虑降低 CONNECTION LIMIT。但原文没有讨论复制延迟,涉及实时数据时仍需补充这个判断。
查得出来之后,还要证明答案可信
作者建议把用户问题、生成的 SQL、返回的答案一起记录,并附上时间戳和提问者。只保留答案,出错时很难判断是模型理解错了,还是查询口径有问题。
数据库侧还可以由超级用户执行 ALTER ROLE ai_agent SET log_statement = 'all';,记录该角色的全部语句。它补充的是数据库执行记录,不能代替问题和答案日志。
产品界面也应把 SQL 放在答案旁边。看到「收入 120 万美元」,用户很容易直接接受;看到查询条件只有 status = 'paid',熟悉业务的人才有机会指出退款应该怎样计算。
接入前,作者要求亲自用 ai_agent 登录,验证能按国家汇总视图中的金额,不能直接读取 public.customers、不能删除 public.orders,并确认 SELECT pg_sleep(6) 会触发 5 秒超时。
这里有个明显的表述问题:原文把 SET default_transaction_read_only = off; 也放在「应该全部失败」的代码区域,但前文已经说明这个设置可以被会话关闭。正确预期应是:即使关闭默认只读模式,删除仍应因缺少权限而失败。不能照搬这组注释作为验收标准。
权限之外,还要问 Agent 10 个已知答案的问题,例如上月收入、活跃客户数和最畅销商品。作者认为,错误通常指向字段命名含混或缺少合适视图,但没有给出错误分布和准确率数据。
我的判断
我赞同把数据库权限放在提示词之前。相比只告诉模型「不要修改数据」,独立账号和受限视图能让约束在模型输出错误时继续生效,也方便停用连接、定位查询来源。
不过,「代码在 PostgreSQL 16 跑通过」只能证明这组示例在作者环境下的表现,不能证明现有数据库没有其他权限来源。部署时仍要核对实际账号权限,不能仅凭复制成功就认定隔离完成。
这套方案还没有展开多租户或按用户授权的问题。如果所有人共用同一个账号和同一批视图,文章没有解释如何确保每个人只能看到自己的数据。
对于允许自由提问的场景,视图比暴露原始表更容易控制;如果需求只是固定的几个指标,预定义查询或业务 API 也可能更合适。原文没有比较这些方案的成本和准确率。作者产品还提供带来源的文档搜索,以及在 100+ 应用中执行操作的 Agent,但这些介绍不能作为数据库方案可靠性的证据。
能拿来做什么
- 在 Node 后端接入自然语言查询时,先建立独立数据库账号和受限视图,让模型只接触有明确业务含义的数据。
- 在查询执行层同时限制语句时间、返回行数和连接数,避免把
LIMIT 500当成查询成本上限。 - 给查询界面增加 SQL 查看入口,并关联保存问题、SQL、答案和提问者,让业务人员能核对统计口径。
- 上线前分别验收权限和答案:测试越权读取、删除与超时,再用 10 个已知答案的问题检查字段、视图和业务定义。