给大模型一根「写库的手」之前,先把权限这条命门讲清楚。本文拆解一个纯 Java AI 平台的三层权限模型:工具可见面 → 三档授权 → 业务复核,外加 HITL 人工确认。
一、问题:给 AI 一根「写库的手」,你敢吗?
大多数 Agent Demo 的权限模型是「全有或全无」:要么模型能看到并调用所有工具,要么什么都调不了。放到企业里立刻出问题:
- 普通员工让「小Z」查一下别的部门的用户信息,能查吗?
- 模型自己决定发布一条全公司公告,发不发?
- 一个只读的检索工具,会不会被诱导去调用删除接口?
BizBuddy 的做法是把「谁可见、能不能调、要不要人确认、执行时再校验」拆成三层,并且全程复用底座已有的组织机构与 RBAC,而不是给 AI 单独造一套权限。
二、底座:RuoYi-Vue-Plus 的组织机构与 RBAC
AI 能力要落地企业权限,前提是底座本身有一套完整的权限模型。RuoYi-Vue-Plus 提供的是国内中后台里最成熟的那一套:
| 概念 | 表 | 作用 |
|---|---|---|
| 用户 | sys_user |
账号主体 |
| 角色 | sys_role |
权限集合 |
| 菜单 / 按钮 | sys_menu |
权限标识(如 system:user:query) |
| 部门 | sys_dept |
组织树,数据权限的边界 |
| 岗位 | sys_post |
组织内的职务 |
| 数据权限 | sys_role_dept |
角色 → 可访问部门 |
| 字典 | sys_dict_* |
枚举与配置 |
鉴权用 Sa-Token ,接口上用 @SaCheckPermission("system:user:query") 这类注解;数据权限用 @DataPermission 按部门/角色自动拼 SQL 条件。超级管理员有通配权限 *:*:*。
关键点:这套模型对 AI 不是「另起一套」,而是「同一套」。 AI 工具在代码侧声明它需要的权限标识,运行时用同一套 PermissionService 校验。
三、三层权限模型
第 1 层:工具可见面(装配期决定)
一个工具能不能被模型看见,由装配期决定,运行期无法绕过(工具的 JSON Schema 在装配时就固定了)。可见面来自三处叠加:
- 智能体绑定 :
ai_agent_tool里「智能体 × 工具」的绑定与决策; - 会话资源集 :对话底栏
+里选的工具 / 技能 / MCP 工具(下一轮生效); - 平台内置只读工具 :
get_current_time/get_current_user/get_current_dept/get_current_permissions四个「无害只读」工具,无需绑定即可用,默认放行。
decision = 禁止的工具根本不注册进 Toolkit------模型看不到,谈不上被诱导调用。这比「注册后运行时拒绝」安全得多。
第 2 层:三档授权(运行期规则)
对进入可见面的工具,按「智能体 × 工具」的三档决策生成权限规则(ai_agent_tool.decision):
| 决策 | 值 | 行为 |
|---|---|---|
| 允许 | 0 |
直接执行 |
| 需确认 | 1 |
命中即暂停,等人工确认(HITL) |
| 禁止 | 2 |
不注册(模型不可见) |
决策有多个来源,且「越显式越优先」:
会话资源集直接选中 > 场景包内决策 > 智能体绑定决策
- 请问------同一工具被「直接选中」时,档位取选中时的默认:只读 → 允许,写入 → 需确认;
- 用户个人的「记住该工具」信任,按 (userId, sessionId) 槽位在每轮对话时并入,不写进共享的装配实例------避免某个用户点的「允许」让所有人都免确认。
第 3 层:业务权限复核(执行期)
即使前两层都放行,工具在执行时仍会再校验一次业务权限。以「发布系统公告」工具为例:
java
@Override
public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
Long operatorId = resolveOperatorId(param); // 从 RuntimeContext 取操作人
if (operatorId == null) {
return Mono.just(ToolResultBlock.error("无法识别当前操作人,已拒绝发布公告"));
}
if (!hasNoticePermission(operatorId)) { // 复核菜单权限
return Mono.just(ToolResultBlock.error("当前用户无「通知公告新增」权限,已拒绝发布公告"));
}
// ... 参数校验、落库
}
private boolean hasNoticePermission(Long operatorId) {
Set<String> perms = permissionService.getMenuPermission(operatorId);
return perms.contains("*:*:*") || perms.contains("system:notice:add");
}
这道复核的意义是纵深防御 :万一工具可见面或档位配置出错,工具自己也会用提问用户的实际权限兜底。写操作三档与业务权限同时满足才能落地。
四、HITL:写操作的「暂停---确认---恢复」
企业里最敏感的从来不是「读」,而是「写」。BizBuddy 的 HITL 走的是 AgentScope 框架原生机制,而不是自己发明的提示词约定:
- 装配期为写工具写入
ASK规则; - 模型调用该工具时,框架暂停 整个 Agent,并发出
RequireUserConfirmEvent; - 前端弹出确认卡片,用户点「允许 / 拒绝」;
- 允许则以
ConfirmResult恢复执行;拒绝则不执行。
java
// 工具侧无需手写确认逻辑------权限判定沿用 ToolBase 的默认实现,
// 由框架按装配期写入的 PermissionContextState 规则判定。
public class CreateNoticeTool extends ToolBase { /* ... */ }
这套机制带来两个工程好处:
- 确认是「链路的暂停点」而不是「工具内的 if」:会话状态被完整保存,恢复后上下文不丢;
- 专家链路内置写工具短期去重 :一次编排里(含 HITL 等待与重试),「同工具 + 同入参」的写调用在 TTL 内只真正执行一次,避免重复发布公告这类重复副作用。
五、组织机构是怎么参与 AI 的
RBAC 不只是「有没有权限」,还包括「能看到谁的数据」。AI 侧主要在三个地方用到组织机构:
5.1 专家可见范围
ai_expert_agent.visible_scope 决定专家对谁可见:
| 取值 | 含义 |
|---|---|
0 |
全部用户可见 |
1 |
仅本部门(owner_dept_id)可见 |
2 |
仅创建者本人(create_by)可见 |
候选专家清单每轮现算 :启用 ∩ 提问用户可见 ∩ 专家角色。
5.2 配额(按提问用户)
专家可配置日 / 月 Token 配额 与请求次数配额 (daily_token_quota / daily_request_quota / monthly_token_quota)。配额按提问用户计,防止「一个账号把预算跑光」。
5.3 数据权限的「异步线程坑」
RuoYi 的 @DataPermission 依赖 Sa-Token 的线程上下文 拿当前用户。但 AgentScope 的工具运行在 Reactor 线程上,线程上下文不可用,直接调用会踩到空指针。
因此 BizBuddy 的 AI 工具遵循两条纪律:
- 操作人从
RuntimeContext取 ,而不是LoginHelper; - 权限校验用不依赖线程上下文的
PermissionService;对涉及数据权限的写操作,额外提供不挂@DataPermission的服务方法,并显式做归属校验。
六、审计:每次决策都可追溯
权限不是「配完就完了」,还要能复盘。平台把以下五类记录全部落库:
- 会话与消息(
ai_message); - 工具调用(
ai_tool_call_log:工具名、入参、出参、状态、耗时); - 权限决策与确认;
- 链路追踪(
ai_trace:装配快照含工具清单、模型、策略); - 编排运行(
ai_route_run:小Z 的路由决策与专家调用)。
出问题时可以精确回答:这一轮模型看到了哪些工具?调用了什么?用户确认了吗?用的是哪个模型?
七、落地清单
把上面这套压缩成一张清单,方便你对照自己的项目:
- 可见面用装配期裁剪,禁止的工具根本不注册,而不是运行时拦截;
- 三档授权(允许 / 需确认 / 禁止)比布尔开关更贴近企业语义;
- 决策来源要有优先级,越显式越优先,且用户级信任不能污染共享实例;
- 写操作默认「需确认」,并让确认成为链路的暂停-恢复点;
- 执行期再复核一次业务权限,纵深防御;
- 组织机构参与可见范围与配额,让「部门」成为真实边界;
- 异步线程里不要依赖线程上下文,显式传递操作人;
- 全链路留痕,否则排障时无从下手。
相关仓库
作者:AI架构师张磊