进阶 📋 7 个步骤 第 427 / 470 篇

给 Agent 一条只读且限域的数据通道:只读角色、行级权限与语义层

让 Agent 安全查数的三步走:PostgreSQL 只读角色与行级安全、用视图固化业务口径、有界词表上下文,以及 Cube 与 dbt Semantic Layer 选型与上线后要盯的五个指标。

2026.09.14· 26 分钟阅读· 约 2401 字· 🗄️ 数据权限 / 📐 语义层

让 Agent 查数,最常见的翻车方式不是它写不出 SQL,而是它写出的 SQL 跑得通、但答错了问题。根因通常有两个:一是没有统一的业务口径,二是给的权限太大。这篇教程把两件事一次解决——用只读角色 + 行级安全把权限收窄,再用语义层把口径固化下来,最后才把这条通道交给 Agent。

🎯 适合人群:要让 Agent 访问业务数据库的数据/后端开发者。需要一台能跑 PostgreSQL 的机器(本地 Docker 即可)。语义层部分会用到 Cube,同样可以用 Docker 起,全部可本地复现。

先理解:为什么「给 schema」不够

有个说法值得记住:把一个 400 张表的仓库 schema 丢给模型,它会编造 join;把一份有 80 个指标、50 个维度的有类型词表交给它,它就不会。差别不在模型大小,在于你给的「词汇量」是不是有界的。

你给的上下文Agent 的表现风险
全部裸表 + 全部列自由发挥,能跑但口径随意同一问题两次得到不同数字
白名单视图 + 文档化的列口径基本稳定仍需人工核对
语义层(有类型的指标 + 维度)口径与看板一致,可解释需要前期建模投入

下面按这个顺序推进:先把权限关小,再把口径固化,最后才开放给 Agent。

Step 1:建一个只读、限域的数据库账号

1 先关权限,再谈能力

原则很简单:Agent 用的账号只有 SELECT,且只覆盖它该看的对象。不要用应用主账号,更不要用超级用户。

-- 1) 建一个专用登录角色
CREATE ROLE agent_ro LOGIN PASSWORD '换成强密码';

-- 2) 允许连接,但只给只读权限
GRANT CONNECT ON DATABASE analytics TO agent_ro;
GRANT USAGE   ON SCHEMA public       TO agent_ro;
GRANT SELECT  ON ALL TABLES IN SCHEMA public TO agent_ro;

-- 3) 以后新建的表也自动只给 SELECT(关键,否则新表会漏配)
ALTER DEFAULT PRIVILEGES IN SCHEMA public
  GRANT SELECT ON TABLES TO agent_ro;

-- 4) 显式回收写入类权限(防手滑)
REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON ALL TABLES IN SCHEMA public FROM agent_ro;

只读账号也要配连接数与超时。一个跑飞的查询足以拖垮生产库。给这个角色单独设 statement_timeout(例如 30s)和 CONNECTION LIMIT,并优先连只读副本而不是主库。另外:只读不等于安全——如果表里有客户手机号、身份证号,Agent 依然能读出来,所以下一步必须做行级与列级控制。

Step 2:用行级安全把可见范围收窄

2 让「哪个区域的人只能看哪个区域的数据」由数据库执行

PostgreSQL 的行级安全(RLS)是在数据库层执行的,Agent 绕不过去——这正是它的价值所在:规则不写在提示词里,写在数据库里。

-- 开启行级安全
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;

-- 只对 Agent 角色生效的策略:只能看到与当前会话上下文一致的区域
CREATE POLICY agent_region_policy ON orders
  FOR SELECT TO agent_ro
  USING (region = current_setting('app.region', true));

-- 每次连接时由你的服务端设置上下文(不要交给模型决定)
SET app.region = 'AU';
💡 关键点:区域上下文由你的后端代码注入,不是让模型自己填。如果你让 Agent 自己决定「我是哪个区域的」,那这道防线就形同虚设——它会为了回答你的问题而选一个最方便的区域。

Step 3:把业务口径固化成视图

3 让「净收入」只被定义一次

指标口径最怕的是每个人(和每个 Agent)各算一遍。用视图把定义钉死,Agent 只能引用,不能重算:

-- 口径固化:净收入 = 成交额 - 退款 - 折扣
CREATE VIEW v_net_revenue AS
SELECT
  date_trunc('month', o.created_at) AS month,
  o.region,
  SUM(o.amount - COALESCE(o.refunded, 0) - COALESCE(o.discount, 0)) AS net_revenue
FROM orders o
WHERE o.status = 'completed'
GROUP BY 1, 2;

-- Agent 只能看到视图,看不到底层表的裸列
GRANT SELECT ON v_net_revenue TO agent_ro;
REVOKE SELECT ON orders FROM agent_ro;   -- 或者一开始就别授予

视图要配合同义词说明才有效。只给视图名,Agent 未必知道「净收入」对应 v_net_revenue。把指标名、同义词、口径说明写进列注释或单独的说明表,让它能查到「哪个视图对应哪个业务词」。这部分文档的深度,直接决定自然语言查询的准确率。

Step 4:给 Agent 一份「有界词表」而不是全部 schema

4 上下文里放什么,等于允许它想什么
交给 Agent 的上下文(推荐):
  · 允许访问的视图清单(通常 5-20 个)
  · 每个视图的业务含义与适用场景
  · 指标口径说明 + 常见同义词
  · 明确列出「不可访问」的对象,让它知道自己不该碰什么

不要交给 Agent 的:
  · 全库表结构
  · 任何含 PII 的列(手机号、证件号、地址)
  · 生产库的写连接串
💡 一个实用技巧:在系统提示里明确写「如果你需要的数据不在上述清单里,请回答『当前数据源不包含该信息』,不要猜测表名或列名」。这句话能显著降低编造列名的概率。

Step 5:成熟方案——用语义层替代手写视图

5 视图够用,但语义层多了三样东西

当指标数量超过几十个、消费方从 1 个变成 5 个以上时,手写视图会开始失控。这时该上语义层:一份指标定义,服务所有下游。两条主流路线:

方案定位权限模型适合谁
Cube(Cube Core 开源,后端 Apache 2.0)独立于转换工具的无头语义层,多 API 层(SQL / REST / GraphQL / MDX-DAX)并自带 MCP Server行级与列级安全在 Cube 内部执行,查询时检查需要跨多个 BI 工具与 Agent 的统一 API
dbt Semantic Layer(基于 MetricFlow,已改为 Apache 2.0)指标一致性,服务下游所有工具dbt 本身不执行行级安全,仍在数据库层面团队已经在写 dbt 模型

Cube 可以用 Docker 在本地起一个最小实例验证思路:

docker run -p 4000:4000 -p 15432:15432 \
  -v ${PWD}:/cube/conf \
  -e CUBEJS_DEV_MODE=true \
  cubejs/cube

# 然后在 model/cubes/ 下用 YAML 定义指标与维度,
# 例如给 orders 定义 revenue 指标、region 维度,
# 再用 access_policy 配置行级过滤(具体语法以官方文档为准)。
# 起好后访问 http://localhost:4000 打开 Playground 试查询。

语义层不是银弹,建模周期要提前算进去。企业级语义层通常需要数月的建模投入才能成熟;指标定义漂移(某个指标最近 30 天被改过)是上线后的主要风险。建议路径是:先给 Agent 只读权限 → 从小批文档良好的模型/视图开始 → 输出被证明可靠后再逐步扩大。反过来做(一上来就开放全库)几乎一定会出事。

Step 6:把通道接到 Agent

6 三条路,按场景选
接入方式做法适用
SQL 直连Agent 生成 SQL,用只读账号执行最灵活,也最需要你自己兜底核对
REST / GraphQL语义层把指标暴露成接口,Agent 只选指标和维度口径稳定、不想让模型写 SQL
MCP语义层或数据库以 MCP Server 形式提供工具Agent 只看到受治理的有限词表
💡 如果你的 Agent 已经在用 MCP,优先走 MCP 而不是 SQL 直连。MCP 暴露的是「指标 + 维度」这种有界词表,模型无法即兴发挥出你没定义的算法;SQL 直连则把口径正确性的责任全压回了提示词。

Step 7:上线后要盯的五个指标

7 指标的指标,才是健康度的证据
盯什么为什么
查询延迟 p50 / p95 / p99Agent 的响应体验取决于长尾,不是平均值
缓存命中率命中率掉下来,通常意味着有人在用非常规维度查询
编译失败数语义层编译失败的查询,往往是 Agent 在猜不存在的字段
定义漂移(近 30 天被改过的指标数)口径变了而没人通知,是「两个看板数字不一致」的经典根因
每个指标的消费方数量突然多出消费方,意味着有新的自动化流程在依赖它

最后一条底线:给 Agent 的账号永远不要有写权限。无论语义层做得多完善,写权限都不该出现在这条通道上。需要 Agent 触发写入时,走「Agent 提议 → 人或服务端校验 → 用另一个账号执行」的三段式,而不是把写权限直接交出去。

常见问题速查

你遇到的现象大概率原因 & 解决
Agent 报「列不存在」上下文里给了过期 schema,或它猜了列名。换成视图清单 + 明确禁止猜列名
同一个问题两次数字不同口径没固化。把指标写成视图或语义层指标,禁止模型自行组合
查询把库拖慢缺 statement_timeout,或没连只读副本
RLS 策略没生效表没 ENABLE ROW LEVEL SECURITY,或角色是表所有者(所有者默认绕过策略)
语义层上线后仍对不上看板指标定义漂移。先排查近 30 天有没有人改过该指标的 SQL 定义
← 返回教程中心