首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >企业级 Agent 底层架构设计:数据与操作域 × A2A 分布式协作

企业级 Agent 底层架构设计:数据与操作域 × A2A 分布式协作

原创
作者头像
OneCode
发布于 2026-10-17 22:56:04
发布于 2026-10-17 22:56:04
340
举报
文章被收录于专栏:ooderAgentooderAgent

Abstract · 全文要点企业级 Agent 的成熟度不在模型,而在底座。

差异点集中在两个与业务正交的工程平面:权限平面(谁、在哪个域、能对哪类数据做什么)与事件平面(协作过程如何可信地送达与消费)。

数据域与操作域必须两轴正交。运行域管「能做什么操作」,专业域管「能看哪类数据」;全部声明收敛到 domain.yaml 单一真源,未声明一律默认拒绝,启动期 fail-fast。

A2A 不是消息转发,而是带身份的授权调用。六个协议头、接收方权威解析目标域、四条消费路径同口径门阀、拒绝即回执——任何一条漏判都会成为绕过通道。

工程纪律是架构的一部分。死字段即删、分批可回滚(P0–P6)、所有拒绝可观测:reason + code + subject + domain,可统计、可追溯。

01引子:为什么「提示词工程」到不了企业级

当 Agent 从 Demo 走进企业,最先崩掉的不是模型能力,而是那些从来没人认真设计的部分:权限、边界、审计与事件可达性。

过去两年,绝大多数团队对「企业级 Agent」的投入集中在提示词、RAG 与编排框架上。但在真实生产环境里,决定一个 Agent 平台能否上线的是另外一组问题:财务 Agent 能不能读工资明细?数据决策 Agent 能不能触发一次重启?一次跨节点的委托失败了,调用方是拿到明确的拒绝语义,还是干等到超时?

下面是我们对自身平台做架构审计时的现状基线——每一条都是曾真实存在的缺口,而不是假想敌:

审计发现的事实

直接后果

「域」只是入口 profile 标签,仅影响能力面

没有任何权限语义,域与权限两个概念混用

会话列表查询不按域过滤(参数被静默忽略)

历史记录跨域串扰,隔离失效

dataScope 是弃用字段且零消费

死字段——声明了数据范围,运行时无人执行

A2A 仅共享通信秘密(strict/loose),无域授权

鉴权弱;生产配置还传了非法枚举值 audit

执行体注册仅含 capability/sceneId,无权限声明

门阀没有输入,最小权限无从谈起

财务/表单执行体与主平台同进程

敏感业务无独立部署边界,无法独立扩缩

这些问题的共同特征是:它们都不可能靠改提示词解决。提示词是概率性的,而企业权限要求的是确定性语义——允许或拒绝,没有「大概率拒绝」。OWASP 对 LLM 应用威胁的归纳(过度代理、敏感信息泄漏、供应链风险)本质上都在说同一件事[6]:Agent 的行动必须被关进一个确定性的工程笼子里。

本文的立场:企业级 Agent 底座 = 数据与操作域(模型)× 授权平面(执行)× A2A 协议(协作)× 事件平面(可达)× 工具平面(面收敛)。下文每一章都对应一个可验证的落地产物,全部数据与代码路径来自生产系统。

02全景:六个架构平面

先把全景铺开。业务分层(入口→编排→能力→执行)之上,有两个贯穿所有层的平面:权限平面与事件平面。

图 1 · 企业级 Agent 底座全景:四个业务分层 + 两个贯穿平面。权限平面向下门阀每一次工具调用与跨域委托,事件平面向上保证每一件产物可信送达。

这张图的关键读法是正交性:①–④ 是「事情怎么做」,⑤⑥ 是「做得多可信」。业务层可以重构,平面不能缺位。后面各章依次展开:③④⑤⑥ 四个平面各一章,先从权限平面的模型层——数据与操作域——开始。

03数据与操作域:两轴正交的域模型

第一个设计决策,也是整个权限平面的地基:把「域」拆成两条彼此正交的轴。

3.1 运行域 × 专业域

直觉做法是把财务、人力、决策、维护都放进一张域表。但很快会发现它们回答的是两个不同的问题:决策域说的是「以什么身份行动」(只读分析),财务域说的是「数据归谁管」(敏感边界)。混在一张表里,跨域授权矩阵会立刻爆炸。因此模型固定为两轴:

运行域 RuntimeDomain(固定 3 个)

专业域 ProfessionalDomain(平行可扩展)

回答的问题

能做什么操作(操作门阀)

能触达哪类数据(数据边界)

取值

decision 决策 / maintain 维护 / assistant 助手

finance / hr / sales / dev / form …

声明方

平台枚举锁定,领域包不得扩展

各领域包 domain.yaml 自行声明

典型授权

决策域:query / export,无写无重启

财务域可声明可触达 [finance, hr] 两个数据域

两轴正交意味着「决策域可读财务汇总,但不能读人力明细」是两条独立授权,而不是一条特例。调用维度统一写作 rt:<运行域> 与 pd:<专业域>,基线、执行体权限、字段策略三处共用同一套键;请求同时携带两者时 pd: 优先,两者皆无 ⇒ 直接 DOMAIN_FORBIDDEN。

3.2 领域模型与单一真源

授权的最小单元不是「角色」,而是一次组合声明(Grant):主体在某维度下,对某数据域拥有某组操作,受某级数据范围与字段策略约束。

代码语言:javascript
复制
Subject(主体) ──< RoleBinding >── Role
     │
     └──< Grant >── ( Domain, DataDomain, Operations[], DataScope, FieldPolicies[] )

全部声明收敛到 B 层的 domain.yaml——权限声明与域声明同文件,避免两份真源漂移;agents.yaml 只保留执行体身份。下面是财务域的声明骨架:

代码语言:javascript
复制
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>:装载集合与白名单不一致,同样拒绝启动——让「配置漂移」在部署期暴露,而不是在事故里。

3.3 三个必须锁死的边界决策

决策

结论

理由

跨域提权

不允许,无临时提权分支

确需时走独立账号/令牌;「临时提权 + 过期回收」是缺口制造机

助手域数据边界

PARTICIPATION:本人所有角色允许参与数据的并集

请假、报销等个人事务归此域;不做跨域校验,执行体仍由专业域提供

专业域剥离节奏

先逻辑分群 → 再物理剥离

P1–P5 同实例落权限门阀控制风险,P6 才拆进程

值得强调的是「语义唯一」原则:entryProfile(入口能力面)与 domain(权限域)是两个概念,禁止互相复用。前者决定 Agent 这一次对话装载哪些能力,后者决定它有权触碰哪些数据——审计中发现的六条基线缺口,几乎都能追溯到这两个概念的混用。

04授权平面:PDP / PEP 三层拦截

域模型解决「声明」,授权平面解决「执行」:一个决策点(PDP),三个按层枚举的拦截点(PEP),外加一个字段出口。

图 2 · PDP/PEP 授权链路:决策输入 subject + domain + callerDomain + operation + dataDomain + resource,输出 ALLOW / DENY(reason,code) + dataScopeFilter + fieldMaskPlan。

4.1 三层拦截与失败语义

层

拦截点

拦什么

失败语义

入口

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——可观测不是附加项,是拒绝语义的一半。

4.2 数据出口:掩码与移除是两条动作

字段级脱敏最容易被做错的地方,是把「掩码」当成唯一手段。掩码(maskKeep>0,如 ****5678)是「可读但脱敏」,但掩码会泄漏长度与存在性——攻击者知道这是一个 16 位卡号,本身就是信息。因此模型拆成两条互斥动作:

  • 掩码:maskKeep>0 → 保留末 N 位,其余替换;长度保持、幂等可重入;
  • 移除:blockedFields → 整字段删除,不可读就是不存在。

执行上坚持单点: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 + 字段路径,不记录原值——审计日志本身不能成为新的泄漏源。

05A2A:带身份的分布式协作协议

单体的权限做得再好,一旦 Agent 之间开始互相委托,安全边界就由「进程内调用」变成了「协议问题」。A2A 的本质是:每一次跨 Agent 调用,都是一次携带完整授权上下文的远程过程调用。

5.1 群落化部署:域即部署单元

权限域最终要落到部署拓扑上。每个域群落独立目录、端口、数据与日志,域间不共享进程;名册(agent-roster)条目自带 domain 字段,A2A 路由可按域选群落——同域优先,跨域必须授权:

群落

实例

承载与独立理由

决策域

insight-agent

只读聚合/报表工具集——只读最小权限,物理上不存在写工具

维护域

codeagent + devagent

生成/编译/部署/诊断,天然高危,独立实例隔离

财务域

finance-agent(独立)

6 个财务执行体,数据敏感、独立扩缩

其他专业域

人力/销售/专利等独立实例

按域平行扩展,同构接入

助手域

Studio 主实例

通用问答 + 路由,保持轻量,不落业务数据

5.2 协议:六个头,一个原则

协议扩展遵循一条原则——调用方自报的身份是证词,接收方解析的身份才是证据。请求携带六个头:

代码语言:javascript
复制
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 调用的完整授权时序。无论允许还是拒绝,调用方都能拿到确定性的回执——「沉默超时」被从协议层面消灭。

5.3 四条消费路径纪律

分布式系统里最危险的漏洞不是没做校验,而是只在校验了 3/4 条路径。A2A 消息在本平台有四条独立消费路径:

消费路径

说明

L2 入站(ooder/event/A2A/#)

标准消息总线入口,接收方判定

Studio 编排缝隙(E4-1 协调器)

与 L2 同口径判定,只拦请求类消息

sendMessage 本地直连 handler

不经总线的进程内调用,同样过门阀

RouteAgent 路由后本地消费

路由转发后的第二条本地路径,避免绕过

任何一条漏判都会被其余路径绕过——这不是实现细节,是必须写进设计评审清单的纪律。身份传播同理:调用方身份由聊天入口统一写入 ThreadLocal(route() 进入时 bind、finally 清除,防止线程池串身份),由两个唯一出站口(publishTargeted / publishGroup)统一补头——逐点手工塞头必然漏、必然漂移,这是审计给出的教训。幂等与回执沿用既有的入站去重端口 + 回执节点,协议语义保持不变。这一套思路与业界 A2A 协议的「任务即资源、身份随调用」方向一致[5],差别在于我们把授权语义下沉到了传输头,而非任务属性。

06事件平面:双流 SSE 与消费总线

权限解决「允许做」,事件平面解决「做的过程与产物如何可信送达」。这是最容易被当作「前端小问题」、最终却决定产品可信度的一层。

6.1 双流通道:一次性流 × 常驻订阅

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 截断

固定查询串

6.2 消费总线:域插件即消费者

前端不做「事件 → 各处 if-else」的分发,而是把消费权交给域插件:每个域既维护状态、也触发渲染(观察与执行合一)。总线的分发管道是固定的六步:watchdog 触碰 → JSON 解析 → 前台门控 → 域分派 → 附加订阅 → 空消费可见。两个设计点值得展开:

  • 前台门控按「事件所属会话」判定,不是当前激活会话。后台会话的普通事件不渲染、不消费,但交回应用层缓存(如人工确认类)——多会话并行时既不串扰也不丢语义;每类事件可声明「始终处理」豁免。
  • 空消费可见:某类事件没有任何消费者时记 warn 并计入 dropped。听起来小事,但它让「后端新增了事件、前端没人接」这种对齐缺口在测试期就暴露。

6.3 背压与重放:不丢内容,至少一次

流式 token 的消费用字符级背压:同一通道(processInst:activityInst:kind,thinking 与 token 天然分道)的 chunk 合并为一帧一次 DOM 写;缓冲达到 32KB 字符上限时先冲刷已缓冲内容再入队——合并冲刷而不是丢弃中间段,静默文本缺失不可接受。长会话下空闲通道定期清理,防止条目单调增长。

产物型事件(如图表渲染)则面临另一类问题:服务端日志显示推送成功、前端却因 HTTP/2 断流或多标签页竞争而永久丢失——我们真实复现过一次「服务端 delivered=true,页面无图」的案例。解法是传输层补偿:

  • 服务端按会话留存最近的事件载荷(短 TTL、条数上限、全局自增 seq);
  • 前端在本轮流关闭/中断后按 seq 补拉,以「已见最大 seq」去重——重放不会重复渲染;
  • 纯内存不持久化:进程重启后不回放历史图表,与对话历史口径一致。

边界意识:重放缓冲是传输层补偿,不是业务事件溯源;它不替代服务端持久化的产物目录。「至少一次投递 + 前端幂等去重」是流系统的标准姿势,缺一不可。

07工具平面:入口画像与工具装载

工具面是 Agent 的攻击面,也是注意力面。三百个工具塞进上下文,模型的选择质量与注入风险一起恶化——收敛不是限制,是双赢。

图 4 · 工具面上界逐级收敛(单位:工具数)

 全量注册表 → 入口画像上界 → 场景级工具面

收敛由三层机制完成。第一层是入口画像:每个入口声明可用的能力段(如决策入口 = system + app + custom + flow),工具引擎按画像裁剪,全量 303 个工具在决策入口收敛到 236。第二层是场景:具体对话场景只暴露本场景工具面,如通用问答场景仅 23 个。第三层是披露级别:业务域工具默认 ON_FLOW_SELECTED——必须流程匹配才注入,避免无关域工具污染上下文;唯一例外是结构性工具三件套(tools_index / tools_find / tools_load)永不裁剪。

7.1 tools_load:运行时的自画廊间

结构性三件套的存在,让 LLM 拥有了受控的工具自扩容能力:候选集恒等于「全局注册表 ∩ 执行口径」,模型先检索、再按名装载。我们在生产实测中验证过决策会话可以装载 platform_api_get / render_chart / flow_instance_list,而不会话场景之外的 bank_statement_query 则不可装载——边界与执行口径严格一致,不存在「模型看到了就能调」的缝隙。

但工具面收敛带出一个新问题:模型不知道它「看不见的东西」的契约。我们实测中,提示词让模型走一个不在工具面的查询工具,它就自行猜测 REST 路径,连续 404 后答复「取不到数据」。修正方式是把提示词写成精确工具契约:

代码语言:javascript
复制
先 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 规模图(必须出图)

这个案例的启示超出提示词技巧:权限边界收窄后,可感知性必须同步补齐——要么把查询能力升级为域内一等公民能力,要么把契约显式交给模型。「让模型猜」是最差的中间态。

08落地实证:数据决策 Agent 的图形链路

架构好不好,最终看产物。以数据决策 Agent 的「对话即图表」为例,串起前面所有平面。

图 5 · 图形产物链路:spec 由 LLM 产出,经能力层校验、SSE 送达、总线分发,由通用视图插件渲染——宿主对图表类型零耦合;重放缓冲提供「至少一次」投递补偿。

前端侧的扩展点设计值得单独一提:文档面板暴露 registerView(format, renderer),绘制时先查外部视图插件,未命中才回退内置渲染。动态图形以标准插件形态接入(ChatNext.Plugins 注册),收到形如下式的 spec 后在运行时编译出 ECharts 配置——柱/折/面积/饼/环/散/雷达/仪表/漏斗九类图由同一 spec 驱动:

代码语言:javascript
复制
{ "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 代称)。前者验证数据域的只读聚合链路,后者验证执行域的流程台账统计:

09工程纪律:不留技术债的六个判据

架构方案十有八九死于落地。把「不留技术债」从口号变成判据,每一批实施都必须回答六个问题。

判据

落地形态

单一真源

域/权限/字段策略只在 B 层 domain.yaml 声明,代码不硬编码

默认拒绝

未声明的操作/数据域一律拒绝;观察模式仅在灰度期存在,enforce 后分支删除

语义唯一

entryProfile(能力面)与 domain(权限域)禁止互相复用

单点执行

一个 PDP、三个按层枚举的 PEP、一个 FieldMasker,业务不得自写第二套

可回滚

每批交付有明确回滚动作,数据结构变更前先备份

可观测

拒绝必带 reason + code + subject + domain;掩码审计不记原值

9.1 分批实施:每批独立可发布、可回滚

整个重构分七个批次推进,每批都有独立的验收门与回滚路径,累计 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 模式与「只记录」窗口

9.2 审计指标:把安全变成 SLO

指标

采集点

告警线

拒绝率(按域/操作)

决策日志

单域 > 5% / 5min

越权尝试 TopN

决策日志

出现即告警

未声明域调用

决策拒绝日志

> 0 即列入迁移阻塞

策略漂移

B 层装载校验

装载失败即拒绝启动

掩码命中量

FieldMasker

异常增高即排查

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 01引子:为什么「提示词工程」到不了企业级
  • 02全景:六个架构平面
  • 03数据与操作域:两轴正交的域模型
  • 3.1 运行域 × 专业域
  • 3.2 领域模型与单一真源
  • 3.3 三个必须锁死的边界决策
  • 04授权平面:PDP / PEP 三层拦截
  • 4.1 三层拦截与失败语义
  • 4.2 数据出口:掩码与移除是两条动作
  • 05A2A:带身份的分布式协作协议
  • 5.1 群落化部署:域即部署单元
  • 5.2 协议:六个头,一个原则
  • 5.3 四条消费路径纪律
  • 06事件平面:双流 SSE 与消费总线
  • 6.1 双流通道:一次性流 × 常驻订阅
  • 6.2 消费总线:域插件即消费者
  • 6.3 背压与重放:不丢内容,至少一次
  • 07工具平面:入口画像与工具装载
  • 7.1 tools_load:运行时的自画廊间
  • 08落地实证:数据决策 Agent 的图形链路
  • 09工程纪律:不留技术债的六个判据
  • 9.1 分批实施:每批独立可发布、可回滚
  • 9.2 审计指标:把安全变成 SLO
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档