

Abstract · 全文要点企业级 Agent 的成熟度不在模型,而在底座。
差异点集中在两个与业务正交的工程平面:权限平面(谁、在哪个域、能对哪类数据做什么)与事件平面(协作过程如何可信地送达与消费)。
数据域与操作域必须两轴正交。运行域管「能做什么操作」,专业域管「能看哪类数据」;全部声明收敛到 domain.yaml 单一真源,未声明一律默认拒绝,启动期 fail-fast。
A2A 不是消息转发,而是带身份的授权调用。六个协议头、接收方权威解析目标域、四条消费路径同口径门阀、拒绝即回执——任何一条漏判都会成为绕过通道。
工程纪律是架构的一部分。死字段即删、分批可回滚(P0–P6)、所有拒绝可观测:reason + code + subject + domain,可统计、可追溯。
当 Agent 从 Demo 走进企业,最先崩掉的不是模型能力,而是那些从来没人认真设计的部分:权限、边界、审计与事件可达性。
过去两年,绝大多数团队对「企业级 Agent」的投入集中在提示词、RAG 与编排框架上。但在真实生产环境里,决定一个 Agent 平台能否上线的是另外一组问题:财务 Agent 能不能读工资明细?数据决策 Agent 能不能触发一次重启?一次跨节点的委托失败了,调用方是拿到明确的拒绝语义,还是干等到超时?
下面是我们对自身平台做架构审计时的现状基线——每一条都是曾真实存在的缺口,而不是假想敌:
审计发现的事实 | 直接后果 |
|---|---|
「域」只是入口 profile 标签,仅影响能力面 | 没有任何权限语义,域与权限两个概念混用 |
会话列表查询不按域过滤(参数被静默忽略) | 历史记录跨域串扰,隔离失效 |
dataScope 是弃用字段且零消费 | 死字段——声明了数据范围,运行时无人执行 |
A2A 仅共享通信秘密(strict/loose),无域授权 | 鉴权弱;生产配置还传了非法枚举值 audit |
执行体注册仅含 capability/sceneId,无权限声明 | 门阀没有输入,最小权限无从谈起 |
财务/表单执行体与主平台同进程 | 敏感业务无独立部署边界,无法独立扩缩 |
这些问题的共同特征是:它们都不可能靠改提示词解决。提示词是概率性的,而企业权限要求的是确定性语义——允许或拒绝,没有「大概率拒绝」。OWASP 对 LLM 应用威胁的归纳(过度代理、敏感信息泄漏、供应链风险)本质上都在说同一件事[6]:Agent 的行动必须被关进一个确定性的工程笼子里。
本文的立场:企业级 Agent 底座 = 数据与操作域(模型)× 授权平面(执行)× A2A 协议(协作)× 事件平面(可达)× 工具平面(面收敛)。下文每一章都对应一个可验证的落地产物,全部数据与代码路径来自生产系统。
先把全景铺开。业务分层(入口→编排→能力→执行)之上,有两个贯穿所有层的平面:权限平面与事件平面。

图 1 · 企业级 Agent 底座全景:四个业务分层 + 两个贯穿平面。权限平面向下门阀每一次工具调用与跨域委托,事件平面向上保证每一件产物可信送达。
这张图的关键读法是正交性:①–④ 是「事情怎么做」,⑤⑥ 是「做得多可信」。业务层可以重构,平面不能缺位。后面各章依次展开:③④⑤⑥ 四个平面各一章,先从权限平面的模型层——数据与操作域——开始。
第一个设计决策,也是整个权限平面的地基:把「域」拆成两条彼此正交的轴。
直觉做法是把财务、人力、决策、维护都放进一张域表。但很快会发现它们回答的是两个不同的问题:决策域说的是「以什么身份行动」(只读分析),财务域说的是「数据归谁管」(敏感边界)。混在一张表里,跨域授权矩阵会立刻爆炸。因此模型固定为两轴:
运行域 RuntimeDomain(固定 3 个) | 专业域 ProfessionalDomain(平行可扩展) | |
|---|---|---|
回答的问题 | 能做什么操作(操作门阀) | 能触达哪类数据(数据边界) |
取值 | decision 决策 / maintain 维护 / assistant 助手 | finance / hr / sales / dev / form … |
声明方 | 平台枚举锁定,领域包不得扩展 | 各领域包 domain.yaml 自行声明 |
典型授权 | 决策域:query / export,无写无重启 | 财务域可声明可触达 [finance, hr] 两个数据域 |
两轴正交意味着「决策域可读财务汇总,但不能读人力明细」是两条独立授权,而不是一条特例。调用维度统一写作 rt:<运行域> 与 pd:<专业域>,基线、执行体权限、字段策略三处共用同一套键;请求同时携带两者时 pd: 优先,两者皆无 ⇒ 直接 DOMAIN_FORBIDDEN。
授权的最小单元不是「角色」,而是一次组合声明(Grant):主体在某维度下,对某数据域拥有某组操作,受某级数据范围与字段策略约束。
Subject(主体) ──< RoleBinding >── Role
│
└──< Grant >── ( Domain, DataDomain, Operations[], DataScope, FieldPolicies[] )全部声明收敛到 B 层的 domain.yaml——权限声明与域声明同文件,避免两份真源漂移;agents.yaml 只保留执行体身份。下面是财务域的声明骨架:
domain: finance
kind: professional # 领域包只能声明 PROFESSIONAL;运行域由平台枚举锁定
dataDomains: [finance, hr] # 本域可触达的数据域
operations: # 本域可声明的操作(枚举必须大写)
query: READ
export: READ
write: WRITE
restart: CONTROL
# 调用维度基线(最小权限):rt:<运行域> / pd:<专业域>
baseline:
"rt:decision": { dataDomains: [finance], operations: [query, export], scope: ALL }
"rt:maintain": { dataDomains: [], operations: [deploy, restart], scope: SELF }
"pd:finance": { dataDomains: [finance], operations: [query, write, approve], scope: DEPT }
# 执行体级收窄:只能比基线更严,不能更宽
agents:
reimburse:
callableBy: ["rt:assistant", "pd:finance"]
operations: [query, write]
# 字段策略:deny 优先;maskKeep>0 掩码保留末 N 位
fieldPolicies:
- field: "finance.bank.account_no"
allow: ["rt:decision", "pd:finance"]
maskKeep: 4
- field: "finance.employee.salary"
allow: ["pd:finance"] # 决策域只能看聚合,读不到个人工资明细这份 YAML 不是文档,是会被校验和执行的运行时契约。装载器扫描 classpath*:domain.yaml,未知字段即失败(防拼写静默失效);校验器检查 id 格式、基线是否为声明子集、字段策略维度合法性;任何一步失败,进程拒绝启动。再配合节点启动参数 -Dooder.authz.node-domains=<domain>:装载集合与白名单不一致,同样拒绝启动——让「配置漂移」在部署期暴露,而不是在事故里。
决策 | 结论 | 理由 |
|---|---|---|
跨域提权 | 不允许,无临时提权分支 | 确需时走独立账号/令牌;「临时提权 + 过期回收」是缺口制造机 |
助手域数据边界 | PARTICIPATION:本人所有角色允许参与数据的并集 | 请假、报销等个人事务归此域;不做跨域校验,执行体仍由专业域提供 |
专业域剥离节奏 | 先逻辑分群 → 再物理剥离 | P1–P5 同实例落权限门阀控制风险,P6 才拆进程 |
值得强调的是「语义唯一」原则:entryProfile(入口能力面)与 domain(权限域)是两个概念,禁止互相复用。前者决定 Agent 这一次对话装载哪些能力,后者决定它有权触碰哪些数据——审计中发现的六条基线缺口,几乎都能追溯到这两个概念的混用。
域模型解决「声明」,授权平面解决「执行」:一个决策点(PDP),三个按层枚举的拦截点(PEP),外加一个字段出口。

图 2 · PDP/PEP 授权链路:决策输入 subject + domain + callerDomain + operation + dataDomain + resource,输出 ALLOW / DENY(reason,code) + dataScopeFilter + fieldMaskPlan。
层 | 拦截点 | 拦什么 | 失败语义 |
|---|---|---|---|
入口 | A2aAuthzGuard | 调用方域是否有权调用该 agent/op | DOMAIN_FORBIDDEN(REJECTED) |
能力/工具 | CapabilityGuard | 本次操作是否在域的操作白名单内 | OPERATION_DENIED |
数据出口 | FieldMasker · DataScopeFilter | 数据域范围 + 字段级策略 | 行过滤 / 字段掩码(静默降级,不报错) |
前端 | /entries 域清单 | 当前用户可进入哪些域 | 403 面板 / 域条不渲染 |
每一层只信任上一层的输出,且拒绝语义枚举固定:DOMAIN_FORBIDDEN / OPERATION_DENIED / DATA_SCOPE_DENIED / SUBJECT_MISSING。所有拒绝必须携带 reason + code + subject + domain——可观测不是附加项,是拒绝语义的一半。
字段级脱敏最容易被做错的地方,是把「掩码」当成唯一手段。掩码(maskKeep>0,如 ****5678)是「可读但脱敏」,但掩码会泄漏长度与存在性——攻击者知道这是一个 16 位卡号,本身就是信息。因此模型拆成两条互斥动作:
执行上坚持单点:FieldMasker 是唯一字段 PEP,工具返回、DTO、A2A 响应全部走它;行级 DataScopeFilter 是六种范围(ALL / DEPT / DEPT_AND_SUB / SELF / PARTICIPATION / AGGREGATE_ONLY)的唯一执行点,其中 AGGREGATE_ONLY 下单行一律不可见——决策域看聚合报表的合法形态。A2A 出口再叠加一道 A2aResponseMasker,且脱敏失败必须 fail-closed:宁可返回错误载荷,绝不外发原文。
工程细节:路径匹配支持域前缀归一(finance.employee.salary 自动去前缀)与尾部 .* 子树,List 自动逐元素下钻;掩码审计只记 subject + domain + 字段路径,不记录原值——审计日志本身不能成为新的泄漏源。
单体的权限做得再好,一旦 Agent 之间开始互相委托,安全边界就由「进程内调用」变成了「协议问题」。A2A 的本质是:每一次跨 Agent 调用,都是一次携带完整授权上下文的远程过程调用。
权限域最终要落到部署拓扑上。每个域群落独立目录、端口、数据与日志,域间不共享进程;名册(agent-roster)条目自带 domain 字段,A2A 路由可按域选群落——同域优先,跨域必须授权:
群落 | 实例 | 承载与独立理由 |
|---|---|---|
决策域 | insight-agent | 只读聚合/报表工具集——只读最小权限,物理上不存在写工具 |
维护域 | codeagent + devagent | 生成/编译/部署/诊断,天然高危,独立实例隔离 |
财务域 | finance-agent(独立) | 6 个财务执行体,数据敏感、独立扩缩 |
其他专业域 | 人力/销售/专利等独立实例 | 按域平行扩展,同构接入 |
助手域 | Studio 主实例 | 通用问答 + 路由,保持轻量,不落业务数据 |
协议扩展遵循一条原则——调用方自报的身份是证词,接收方解析的身份才是证据。请求携带六个头:
x-ooder-domain: decision # 调用方运行域(rt:decision)
x-ooder-caller: finance # 调用方专业域(pd:finance,可空)
x-ooder-subject: u-1001 # 用户主体(缺失 → SUBJECT_MISSING)
x-ooder-operation: query # 本次操作 id(缺失 → OPERATION_DENIED)
x-ooder-data-domain: finance # 目标数据域(可空)
x-ooder-trace: convo-8f3a # 链路标识
# 拒绝响应:TASK_RESPONSE + 错误载荷,requestId 优先取请求头、
# 缺失则用请求 messageId —— 立即闭合调用方 pending,绝不让它干等到超时
{ "success": false,
"error": { "code": "DOMAIN_FORBIDDEN", "reason": "rt:decision 未获得 hr 数据域授权" } }关键在目标域的解析方向:接收方按 toAgentId 的命名空间(domain.yaml.namespace → AgentDomainPort)解析目标域,不信任调用方自报的目标域。命名空间冲突在注册期 fail-fast。鉴权模式收敛为 strict / loose 两档,历史遗留的 audit/off/test 直接删除——错误的签名一律拒绝,不存在「先放行观察」的窗口期。

图 3 · 一次跨域 A2A 调用的完整授权时序。无论允许还是拒绝,调用方都能拿到确定性的回执——「沉默超时」被从协议层面消灭。
分布式系统里最危险的漏洞不是没做校验,而是只在校验了 3/4 条路径。A2A 消息在本平台有四条独立消费路径:
消费路径 | 说明 |
|---|---|
L2 入站(ooder/event/A2A/#) | 标准消息总线入口,接收方判定 |
Studio 编排缝隙(E4-1 协调器) | 与 L2 同口径判定,只拦请求类消息 |
sendMessage 本地直连 handler | 不经总线的进程内调用,同样过门阀 |
RouteAgent 路由后本地消费 | 路由转发后的第二条本地路径,避免绕过 |
任何一条漏判都会被其余路径绕过——这不是实现细节,是必须写进设计评审清单的纪律。身份传播同理:调用方身份由聊天入口统一写入 ThreadLocal(route() 进入时 bind、finally 清除,防止线程池串身份),由两个唯一出站口(publishTargeted / publishGroup)统一补头——逐点手工塞头必然漏、必然漂移,这是审计给出的教训。幂等与回执沿用既有的入站去重端口 + 回执节点,协议语义保持不变。这一套思路与业界 A2A 协议的「任务即资源、身份随调用」方向一致[5],差别在于我们把授权语义下沉到了传输头,而非任务属性。
权限解决「允许做」,事件平面解决「做的过程与产物如何可信送达」。这是最容易被当作「前端小问题」、最终却决定产品可信度的一层。
Agent 对话的流量分两类:跟随一轮对话生灭的执行流,与独立于对话、异步到达的协作事件。一条通道硬扛两类流量,就会出现「协作事件比流活得久」或「切会话杀掉别的会话在跑的流」这类问题。因此传输层固定为双流:
一次性流 /chat/stream | 常驻订阅 /convo/subscribe | |
|---|---|---|
生命周期 | 每轮对话一条,终态即关 | 页面级长连接,30 分钟超时自动重连 |
承载 | token / thinking / flow_tool_output / docview_render 等执行产物 | conv 通道:a2a_message / a2a_failed / collab_request / im_shell_update |
重连策略 | 禁止自动重连——重连等于向重端重复发起同一执行 | 交由浏览器自动重连——纯监听不触发编排 |
会话隔离 | 按 conversationId 维护多条并存流,open 只 supersede 同会话旧流 | 切会话先关旧连接,防跨会话串流 |
参数上限 | >1000 字符自动切 POST(JSON 体),防 URI 截断 | 固定查询串 |
前端不做「事件 → 各处 if-else」的分发,而是把消费权交给域插件:每个域既维护状态、也触发渲染(观察与执行合一)。总线的分发管道是固定的六步:watchdog 触碰 → JSON 解析 → 前台门控 → 域分派 → 附加订阅 → 空消费可见。两个设计点值得展开:
流式 token 的消费用字符级背压:同一通道(processInst:activityInst:kind,thinking 与 token 天然分道)的 chunk 合并为一帧一次 DOM 写;缓冲达到 32KB 字符上限时先冲刷已缓冲内容再入队——合并冲刷而不是丢弃中间段,静默文本缺失不可接受。长会话下空闲通道定期清理,防止条目单调增长。
产物型事件(如图表渲染)则面临另一类问题:服务端日志显示推送成功、前端却因 HTTP/2 断流或多标签页竞争而永久丢失——我们真实复现过一次「服务端 delivered=true,页面无图」的案例。解法是传输层补偿:
边界意识:重放缓冲是传输层补偿,不是业务事件溯源;它不替代服务端持久化的产物目录。「至少一次投递 + 前端幂等去重」是流系统的标准姿势,缺一不可。
工具面是 Agent 的攻击面,也是注意力面。三百个工具塞进上下文,模型的选择质量与注入风险一起恶化——收敛不是限制,是双赢。
图 4 · 工具面上界逐级收敛(单位:工具数)
全量注册表 → 入口画像上界 → 场景级工具面

收敛由三层机制完成。第一层是入口画像:每个入口声明可用的能力段(如决策入口 = system + app + custom + flow),工具引擎按画像裁剪,全量 303 个工具在决策入口收敛到 236。第二层是场景:具体对话场景只暴露本场景工具面,如通用问答场景仅 23 个。第三层是披露级别:业务域工具默认 ON_FLOW_SELECTED——必须流程匹配才注入,避免无关域工具污染上下文;唯一例外是结构性工具三件套(tools_index / tools_find / tools_load)永不裁剪。
结构性三件套的存在,让 LLM 拥有了受控的工具自扩容能力:候选集恒等于「全局注册表 ∩ 执行口径」,模型先检索、再按名装载。我们在生产实测中验证过决策会话可以装载 platform_api_get / render_chart / flow_instance_list,而不会话场景之外的 bank_statement_query 则不可装载——边界与执行口径严格一致,不存在「模型看到了就能调」的缝隙。
但工具面收敛带出一个新问题:模型不知道它「看不见的东西」的契约。我们实测中,提示词让模型走一个不在工具面的查询工具,它就自行猜测 REST 路径,连续 404 后答复「取不到数据」。修正方式是把提示词写成精确工具契约:
先 tools_load(names=["platform_api_get", "render_chart"])
取数只允许以下已存在的只读路径,禁止猜测其它路径:
GET /api/bank-statement/db/companies
GET /api/bank-statement/db/companies/{id}/summary
步骤:① 取公司清单 → ② 逐家取 summary → ③ render_chart 画分组 bar
④ 若 summary 全部失败,退化为 statement_count 规模图(必须出图)这个案例的启示超出提示词技巧:权限边界收窄后,可感知性必须同步补齐——要么把查询能力升级为域内一等公民能力,要么把契约显式交给模型。「让模型猜」是最差的中间态。
架构好不好,最终看产物。以数据决策 Agent 的「对话即图表」为例,串起前面所有平面。

图 5 · 图形产物链路:spec 由 LLM 产出,经能力层校验、SSE 送达、总线分发,由通用视图插件渲染——宿主对图表类型零耦合;重放缓冲提供「至少一次」投递补偿。
前端侧的扩展点设计值得单独一提:文档面板暴露 registerView(format, renderer),绘制时先查外部视图插件,未命中才回退内置渲染。动态图形以标准插件形态接入(ChatNext.Plugins 注册),收到形如下式的 spec 后在运行时编译出 ECharts 配置——柱/折/面积/饼/环/散/雷达/仪表/漏斗九类图由同一 spec 驱动:
{ "chart": "bar", "title": "各公司收付款对比", "unit": "元",
"categories": ["公司A", "公司B", "…"],
"series": [
{ "name": "收款", "type": "bar", "data": [9736552.86, "…"] },
{ "name": "付款", "type": "bar", "data": [17694241.47, "…"] } ],
"source": "银行流水库 · 只读", "asOf": "2026-10-17" }下面两组图是生产环境实测数据(经域权限口径脱敏:公司名以 A–F 代称)。前者验证数据域的只读聚合链路,后者验证执行域的流程台账统计:

架构方案十有八九死于落地。把「不留技术债」从口号变成判据,每一批实施都必须回答六个问题。
判据 | 落地形态 |
|---|---|
单一真源 | 域/权限/字段策略只在 B 层 domain.yaml 声明,代码不硬编码 |
默认拒绝 | 未声明的操作/数据域一律拒绝;观察模式仅在灰度期存在,enforce 后分支删除 |
语义唯一 | entryProfile(能力面)与 domain(权限域)禁止互相复用 |
单点执行 | 一个 PDP、三个按层枚举的 PEP、一个 FieldMasker,业务不得自写第二套 |
可回滚 | 每批交付有明确回滚动作,数据结构变更前先备份 |
可观测 | 拒绝必带 reason + code + subject + domain;掩码审计不记原值 |
整个重构分七个批次推进,每批都有独立的验收门与回滚路径,累计 65+ 项单测、90 个模块全量编译通过:
批次 | 核心交付 | 随批清零的债务 |
|---|---|---|
P0–P1 | 域模型 ADR;PDP 骨架 + 影子对比(与现状一致率 100%) | 明确「域 ≠ entryProfile」 |
P2 ✅ | 三份 domain.yaml 接入;启动 fail-fast 校验;23 项单测 | 删除零消费的 dataScope 死字段 |
P3 ✅ | A2A 门阀 + 六头协议 + 拒绝回执;四条消费路径同口径 | 删除非法鉴权档 audit |
P4 ✅ | 前端域条过滤 + 深链 403;历史会话按入口隔离;身份注入 | 补齐被静默忽略的 entryProfile 过滤参数 |
P5 ✅ | FieldMasker / DataScopeFilter / A2A 响应脱敏(fail-closed) | 字段脱敏唯一出口建立,废除业务各自脱敏 |
P6 ✅ | 固定 enforce;名册带 domain;按域启动守卫(node-domains) | 删除 observe 模式与「只记录」窗口 |
指标 | 采集点 | 告警线 |
|---|---|---|
拒绝率(按域/操作) | 决策日志 | 单域 > 5% / 5min |
越权尝试 TopN | 决策日志 | 出现即告警 |
未声明域调用 | 决策拒绝日志 | > 0 即列入迁移阻塞 |
策略漂移 | B 层装载校验 | 装载失败即拒绝启动 |
掩码命中量 | FieldMasker | 异常增高即排查 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。