全文要点
企业级 Agent 能不能真正扛住生产环境的考验,关键并不在模型有多强,而在于它脚下的底座够扎实。真正拉开差距的,是两个平时不被重视、却又与业务流程高度正交的工程平面:一个是权限平面——要把「谁、在哪个域、能对哪类数据执行什么动作」回答清楚;另一个是事件平面——让协作过程中的每一次交付都变得可信、可送达、可消费。
其中最基础的一条判断,是把数据域和操作域拆成两条彼此正交的轴。运行域管「能不能做某个操作」,专业域管「能不能看到某一类数据」。两份声明最终只汇到 domain.yaml 这一个真源上,没写进去的一律默认拒绝,进程在启动阶段就直接 fail-fast。把隐患掐死在最早的时刻,远比事后补洞要省心。
再说 A2A。不少团队习惯把它当作消息的中转站,但这个理解偏浅了。A2A 的真实身份,是一次携带完整授权上下文的远程过程调用:请求头带齐六项,接收方权威地解析出目标域,四条消费路径共用一套门闸,只要拒绝就必须回执——只要其中任何一环漏了判断,就会留下一条可以绕行的暗道。
往深里看,工程纪律并不是架构的附属品,它本身就是架构的一部分。死掉的字段该删就删,每批改动都要能回滚,每条拒绝都应当可被观测,把 reason、code、subject、domain 四个字段一起带上。能做到可统计、可追溯,拒绝语义才算真正完整。
一、”提示词工程”为什么撑不起企业级
当 Agent 从演示环境走进真实生产,最先塌方的常常不是模型能力,而是那些从来没人认真打磨过的角落:权限、边界、审计,以及事件到底能不能被送达。
过去一两年里,大多数团队把精力投在了提示词、RAG 和编排框架上。可真正到生产环境里才发现问题:决定一个 Agent 平台能否上线的,是另一组从来没被认真对待的问题。财务 Agent 到底能不能读到工资明细?数据决策 Agent 有没有权限去触发一次系统重启?一次跨节点的委托一旦失败,调用方拿到的是清晰的拒绝语义,还是只能干等超时?
这些并非假想敌。这是我们给自家平台做架构审计时实打实踩过的坑。下面这组基线,每一条都对应着一个真实存在的缺口:当时的「域」只是个入口 profile 标签,只影响能力面,没有任何真实的权限语义,域和权限被混为一谈;会话列表查询明明该按域过滤,参数却被静默忽略,于是历史记录跨域串扰、隔离彻底失效;还有一个早已废弃的 dataScope 字段,声明了数据范围却零消费,是典型的死字段;A2A 当时只共享一个通信秘密,缺少域授权,鉴权很弱,生产配置里甚至传了非法枚举值;执行体注册只含了 capability 和 sceneId,完全没有权限声明,门闸没有输入,最小权限无从谈起;财务和表单执行体和主平台还跑在同一个进程里,敏感业务没有独立部署边界,想独立扩缩都做不到。
把这些串起来看,共同点很明显:没有一条是靠改提示词能解决的。提示词本身是概率性的,而企业权限要的却是确定性——要么允许,要么拒绝,不存在”大概率拒绝”这种东西。这一轮审计得出的结论最终汇成一句话:Agent 的行动必须被关进一个确定性的工程笼子里。
本文要讲清楚的,企业级 Agent 的底座,等于数据与操作域乘以授权平面乘以 A2A 协议乘以事件平面乘以工具平面。下面每一节对应的都是一个可以拿生产系统验证过的产物,所有数据路径来自真实运行环境——这套思路对我们这类需要把智能体落到生产环境的项目,具备直接的参考价值。
二、把全景铺开:两个贯穿始终的平面
业务本身是分层组织的:从入口,到编排,到能力,再到执行。但在这套分层之上,还有两个平面始终贯穿每一层——权限平面和事件平面。
理解这张全景图的核心钥匙,是”正交”二字。业务那几层解决的是”事情怎么做”,两个平面解决的则是”做得有多少可信度”。业务层可以随意重构,但平面不能缺位。后面的章节就顺着这个脉络推进,从权限平面的模型层开始,也就是数据域和操作域那一节。
三、数据域 × 操作域:两轴正交
整个权限平面的地基,是第一个设计决策——把「域」拆成两条彼此正交的轴。
直觉的做法,是把财务、人力、决策、维护全都塞进一张域表。但用着用着就会发现,它们回答的其实是两个不同的问题。决策域说的是”以什么身份去行动”,比如只做只读分析;财务域说的是”这些数据归谁管”,划的是敏感边界。把这两类东西揉在一张表里,跨域授权矩阵会瞬间爆炸。于是模型被固定成两轴。
先看运行域。它回答”能做什么操作”,也就是操作门闸,固定就三个取值——decision(决策)、maintain(维护)、assistant(助手),由平台枚举锁定,领域包不允许自行扩展。再看专业域。它回答”能触达哪类数据”,划出数据边界,这一侧是平行可扩展的——finance、hr、sales、dev、form 等等,都由各个领域包在 domain.yaml 里自己声明。核心好处在于:决策域可以读财务汇总,却读不到人力明细,这是两条独立的授权,而不是一条特例。
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 格式、基线是不是声明的子集、字段策略的维度是否合法。只要其中任何一步失败,进程就拒绝启动。再配合节点启动参数把节点域集合和白名单对上,一旦不一致同样拒绝启动——让”配置漂移”在部署期就暴露出来,而不是等到出事故才。
三个必须锁死的边界决策
第一,跨域提权一律不允许,不设立任何临时提权的分支。真要临时扩大权限时,必须走独立的账号或令牌——”临时提权再过期回收”这种设计,往往是漏洞的制造机。
第二,助手域的数据边界采用 PARTICIPATION 这个口径,也就是本人所有角色允许参与数据的并集。请假、报销这类个人事务归到这个域,这里不做跨域校验,但执行体依然由专业域来提供。
第三,专业域的剥离要有节奏:先做逻辑分群,再考虑物理剥离。P1 到 P5 还落在同一个实例上,用权限门阀来控制风险,直到 P6 才真正拆开进程。
这里要特别强调”语义唯一”这个原则。入口的能力面 entryProfile 和权限域 domain 是两个截然不同的概念,彼此禁止复用。前者决定这一次对话装载哪些能力,后者决定它有权触碰哪些数据。前面审计发现的六条基线缺口,几乎都能追溯到这两个概念被混用。
四、授权平面:PDP 与 PEP 的三层拦截
域模型解决的是”声明”,授权平面解决的则是”执行”:一个决策点(PDP),三个按层枚举的拦截点(PEP),外加一个字段出口。
在这个链路里,输入是 subject、domain、callerDomain、operation、dataDomain、resource,输出则是 ALLOW 或 DENY,同时带上数据范围过滤和字段掩码方案。最关键的工程约束是:每一层只信任上一层的输出,而且拒绝语义必须是固定枚举——DOMAIN_FORBIDDEN、OPERATION_DENIED、DATA_SCOPE_DENIED、SUBJECT_MISSING。每一条拒绝都携带 reason、code、subject、domain 这四项——可观测不是锦上添花,它本身就构成拒绝语义的一半。
数据出口这一层,最容易被做错的地方,是把”掩码”当成唯一手段。掩码——比如把卡号变成 ****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 和字段路径,绝不记录原值——因为审计日志本身,不能沦为新的泄漏源。
五、A2A:带身份的分布式协作协议
单体的权限做得再漂亮,一旦 Agent 之间开始互相委托,安全的边界就会从”进程内调用”变成”协议问题”。A2A 的本质,就每一次跨 Agent 的调用,都是一次携带完整授权上下文的远程过程调用。
群落化部署:域就是部署单元
权限域最终要落到部署拓扑上。每个域群落都有独立的目录、端口、数据和日志,域之间不共享进程。名册里的条目自带 domain 字段,A2A 路由可以按域去选择群落——同域优先,跨域必须经过授权。
于是我们可以看到一种自然的拆分:决策域 Insight Agent 跑的是只读聚合与报表工具集,只读最小权限在物理上就排除了写工具存在的可能;维护域的 codeagent 和 devagent 负责生成、编译、部署、诊断,天然高危,必须用独立实例隔离;财务域 finance Agent 独立部署,六个财务执行体数据敏感,需要独立扩缩;人力、销售、专利等其他专业域则各自独立实例,按域平行扩展、同构接入。助手域保持在 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 一律删掉——任何签名的错误都直接拒绝,不存在”先放行观察”的窗口期。
四条消费路径的纪律
分布式系统里最危险的漏洞,往往不是”没做校验”,而是”只在校验了其中三四条路径”。A2A 消息在本平台上有四条独立的消费路径。
第一条是 L2 入站,也就是 ooder/event/A2A/# 这条标准消息总线入口,由接收方来判定。第二条是 Studio 编排里的缝隙,E4-1 协调器与 L2 走同口径的判定,只拦请求类消息。第三条是 sendMessage 的本地直连 handler,这种不经总线的进程内调用,同样要过门闸。第四条是 RouteAgent 路由转发之后的本地消费路径,就是为了避免被绕过。
只要其中一条漏判,其余路径就能把它绕过——这不是个实现细节,而是必须写进设计评审清单的纪律。身份传播也是同样的逻辑:调用方身份由聊天入口统一写入 ThreadLocal,route() 进入时 bind,finally 里清除,防止线程池串了身份;再由两个唯一出站口 publishTargeted 和 publishGroup 统一补头——逐点手工塞头必然漏、必然漂移,这是审计给出的惨痛教训。幂等与回执沿用了既有的入站去重端口加回执节点,协议语义保持不变。这套”任务即资源、身份随调用”的思路其实和业界 A2A 协议的大方向一致,差别只在于,我们把授权语义下沉到了传输头,而不是任务属性。
六、事件平面:双流 SSE 与消费总线
权限解决”允许做”,事件平面解决的则是”做的过程和产物如何可信送达”。这一层最容易被当成”前端小问题”,可最终决定产品可信度的恰恰是它。
双流通道:一次性流 × 常驻订阅
Agent 对话的流量分两类:跟随一轮对话生灭的执行流,与独立于对话、异步到达的协作事件。如果让一条通道去硬扛这两种流量,就会出现”协作事件比流活得还久”,或者”一开新会话就把别会话正在跑的流杀掉”这类麻烦。于是传输层被固定成双流。
一边是一次性流,走 /chat/stream,每轮对话建立一条,到终态就关闭,承载 token、thinking、flow_tool_output、docview_render 这类执行产物;自动重连被明确禁止,因为重连等于向着重端再次发起同一次执行。另一边是常驻订阅,走 /convo/subscribe,做页面级的长连接,30 分钟超时自动重连,承载 conv 通道里的 a2a_message、a2a_failed、collab_request、im_shell_update 这类协作事件,由浏览器自动重连,纯监听不触发编排。会话隔离上,一次性流按 conversationId 维护多条并存流,open 只 supersede 同会话的旧流;切换会话时会先关掉旧连接,防止跨会话串流。
消费总线:域插件即消费者
前端不自己做”事件分发到各处 if-else”那套,而是把消费权交回给域插件:每个域既维护自己的状态、也负责触发渲染,观察和执行合二为一。总线的分发管道是固定的六步:watchdog 触碰、JSON 解析、前台门控、域分派、附加订阅、空消费可见。其中有两个设计点值得展开。
前台门控是看”事件所属的会话”来判定的,而不是看当前激活的那个会话。后台会话的普通事件不会被渲染、不会被消费,但会被交回应用层缓存,比如人工确认类的场景——这样多会话并行时既不互相干扰,也不丢语义,每类事件还能声明”始终处理”作为豁免。另一个是空消费可见:某类事件如果没有消费者,就记一条 warn 并计入 dropped。听起来是小事,可它恰恰让”后端新增了事件、前端却没人接”这种对齐缺口,在测试阶段就能暴露出来。
背压与重放:不丢内容,至少一次
流式 token 的消费用的是字符级背压:同一通道的 chunk 会被合并成一帧一次 DOM 写,缓冲达到 32KB 字符上限时,先冲刷已经缓存的内容再入队——是合并冲刷而不是丢弃中间段,静默地缺了文本在这里是不可接受的。长会话下空闲通道会定期清理,防止条目单调膨胀。
而产物型事件,比如图表渲染,则会撞上另一类问题:服务端日志显示推送成功,前端却因为 HTTP/2 断流或多标签页竞争而永久丢失。我们就真实复现过一次”服务端 delivered=true、页面上却看不到图”的情况。
最终的解法是做传输层的补偿:服务端按会话留存最近的事件载荷,带短 TTL、条数上限和全局自增的 seq;前端在本轮关闭或中断后按 seq 补拉,用”已见的最大 seq”去重,这样重放也不会重复渲染。纯内存不持久化,进程重启之后不会回放历史图表,和对话历史的口径保持一致。这里要有边界意识:重放缓冲是传输层的补偿机制,不是业务事件的溯源,它替代不了服务端持久化的产物目录。”至少一次投递 + 前端幂等去重”是流系统的标准姿势,缺一不可。
七、工具平面:入口画像与工具装载
工具面既是 Agent 的攻击面,也是注意力的面。三百个工具塞进上下文,模型的选择质量和注入风险会一起恶化。所以收敛不是限制,而是双赢。
收敛由三层机制完成。第一层是入口画像:每个入口声明自己可用的能力段,比如决策入口 = system + app + custom + flow,工具引擎按画像裁剪,全量 303 个工具在决策入口收敛到 236 个。第二层是场景:具体的对话场景只暴露本场景的工具面,比如通用问答场景只给 23 个。第三层是披露级别:业务域的工具默认走 ON_FLOW_SELECTED,必须流程匹配才注入,避免无关域的工具污染上下文;唯一的例外是结构性工具三件套——tools_index、tools_find、tools_load,它们永不裁剪。
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 的图形链路
架构好不好,最终得看产物。就拿数据决策 Agent 的”对话即图表”来说,它正好把前面所有平面都串了起来。
前端侧的扩展点设计值得单独拎出来说。文档面板暴露 registerView(format, renderer),绘制时先查外部视图插件,命中不了才回退内置渲染。动态图形以标准插件形态接入 ChatNext.Plugins,收到如那串 spec 之后,在运行时编译出 ECharts 配置——柱、折、面积、饼、环、散点、雷达、仪表、漏斗这九类图,由同一份 spec 驱动。而且生成的图表还会经过域权限口径的脱敏处理,比如公司名统一用 A 到 F 代称。
{ "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" }
这两组图是生产环境里的实测数据,都已经过域权限口径脱敏:前者验证数据域的只读聚合链路,后者验证执行域的流程台账统计。图形的产物链路是 spec 由 LLM 产出,经过能力层校验,再经 SSE 送达、总线分发,最后由通用视图插件来渲染——宿主对图表类型完全零耦合,重放缓冲则提供”至少一次”的投递补偿。
九、工程纪律:不留技术债的六个判据
架构方案十有八九会死在落地这一步。把”不留技术债”从一句口号变成可以据以判断的判据,每一批实施都必须回答六个问题。
单一真源:域、权限、字段策略只在 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 字段,按域启动守卫,并删掉了 observe 模式与”只记录”的窗口。
审计指标:把安全变成 SLO
最后,把这些工程纪律沉淀成可监控的指标。拒绝率按域和操作统计,单域每分钟超过 5% 就告警。越权尝试的出现直接触发告警。未被声明域的调用,只要大于 0 就列入迁移阻塞项。策略漂移由 B 层装载校验把关,装载失败直接拒绝启动。掩码命中量由 FieldMasker 采集,异常增高就拉出来排查。把这些指标固化下来,安全就不再是事后补救,而变成了可度量、可预警的 SLO。
写在最后
企业级 Agent 的下场,拼的从来不是模型的聪明程度,而是底座的确定性与纪律感。数据与操作域的正交、授权平面的严格拦截、A2A 带身份的协议、事件平台的可信送达,以及工具面的收敛,这五层缺一,整个底座就会在生产的某一处露馅。如果我们正计划把这类底座系统部署起来,与其在上线后补救,不如一开始就用好时代互联提供的稳定托管与独立部署能力,把边界和隔离在起步阶段就定下来。

时代互联资讯中心


