
Architecture Design · AI-native UI
A/B/C 分层不是目的,而是为了让「一次用户交互」在三层各自得到正确的支撑:A 提供渲染与协议,B 提供领域语义与授权,C 提供确定性能力。同一个 B 之上,同时支撑页面模式(UI + 小助手)与对话模式(chatnext 原生),并通过五个接缝完成双向转换。
多数分层讨论从「代码归属」出发,结果是把架构讲成了目录结构。本文换一个起点:从一次用户交互往回推 —— 用户点一个按钮、或说一句话,系统需要什么,就分什么层。
一次完整的交互需要四件东西:看得见的界面、听得懂的语言、做得对的动作、留得下的痕迹。它们分别落在 A、B、C 三层,且任何一层缺位,交互就会在某处断掉。
交互要素 | 用户感受 | 由谁支撑 |
|---|---|---|
界面(看得见) | 「我知道现在在哪、能做什么」 | A:令牌 + ood.UI.* + .cls 渲染 |
语义(听得懂) | 「它理解我说的是什么业务」 | B:领域模型 + 视图契约 + 场景与工具白名单 |
动作(做得对) | 「结果是对的,而且不会越权」 | B 授权与规则 + C 确定性实现 |
痕迹(留得下) | 「出问题能查、能回滚」 | A 事件账本 + C 幂等与审计写入 |
这个起点带来的第一个结论
既然「界面」与「语义」是两类不同的东西,那么交互外壳天然可以替换,而语义与动作不能。这就是后文 A/B/C 分层的由来,也是「一个 B、两种交互」能够成立的根据。
A/B/C 分别回答三个不同的问题,并对应三种不同的变化频率。先看结论表,再看职责细节。
层 | 名称 | 回答什么问题 | 承载内容 | 变化频率 | 归属 |
|---|---|---|---|---|---|
A | 平台基线 | 怎么渲染、怎么传事件、怎么落盘 | 令牌 L0–L2、ood.UI.* 组件、SSE 事件域、buildView → .cls 三地址 | 月(平台统一升级) | ooder-pro · ouc-core · ood-codegen-* |
B | 领域宿主 | 业务是什么、谁能看、谁能改 | 实体/视图契约注解、modules.yaml、authz/roles.yaml、agents.yaml、flow-decl | 客户周期(可配置) | ooder-domain-{domain} |
C | 业务实现 | 具体怎么算、怎么取数、怎么落库 | 服务、场景、编排、外部适配、{X}APIImpl、六类技能载体 | 天级(可自助) | ooder-biz-{domain} |
判据(一句话记住三层)
能被平台反射读取的元数据,必须在 A/B 的注解里;与类无关、客户可改的运维面配置,必须在 B 的 YAML 里;任何「拼装产物」的代码都不许留在 C。
三层不是按技术栈切的,而是按交互链路切的:
A 只做渲染与协议,不含业务判断。因此换 A 不会改变业务,换业务也不会动 A。
B 决定用户看到哪些字段、能点哪些动作、谁能改什么 —— 它是「交互语义」的唯一出处。
C 只接收结构化入参、只返回结构化结果;不读界面、不返回 UI 片段、不吞异常。
A(平台基线) ←── B(注解 + 声明) ←── C(实现)
① B 不得依赖 A 的生成器;C 不得反向依赖 A 的内部实现
② C 不得依赖其它 C 包或非本域 B 包
③ ooder-pro 不得反向依赖任何 ooder-biz-* / ooder-domain-*
④ .cls 只由 buildView 产出,三地址(VFS / export / workspace)字节一致
图 1 两种外壳共用 A 的渲染能力、共享 B 的领域语义、调用同一份 C 能力
A 层的判断标准只有一条:它是否对所有业务都成立。成立才进 A,否则归 B 或 C。
能力 | 内容 | 为什么在 A |
|---|---|---|
令牌体系 | L0 原始值 → L1 语义色 → L2 组件令牌(theme-vars.css 唯一变量源) | 所有业务共用同一套视觉基线 |
组件与渲染 | ood.UI.*(表格/表单/树/页签/工具栏/菜单/对话框…) | 两种外壳用的是同一批组件 |
契约序列化 | C2UFactory.buildView → .cls 三地址 | 页面模块的唯一产出通道 |
事件与协议 | SSE 事件域、Bus、EventStore、A2A 任务协议 | 跨外壳统一的通信语义 |
可访问性基线 | 焦点环、键盘模型、role/aria-* 自动补齐 | 一次实现,全局生效 |
这些能力是「外壳可以依赖的约定」,而不是某个页面的修饰:
能力 | 对外契约 | 取值 |
|---|---|---|
密度三档 | setDensity('compact'|'cozy'|'comfortable') | 表头 26/30/38、行 24/28/34、字 12/13/14px |
列宽持久化 | 拖拽手柄 + colWidthKey | 存储键 ood-treegrid-cols:<key>,渲染后自动恢复 |
键盘模型 | 方向键 / Home / End / Enter / Esc | 表格、树、页签、菜单、分页统一可达 |
ARIA 自动补齐 | setARIA() 渲染后调用 | grid/row/columnheader/gridcell、tree/treeitem、tab/tabpanel |
三态覆盖 | showState('empty'|'loading'|'error') + clearState() | role=status + aria-live=polite |
页签形态 | setVariant('line'|'card'|'segmented') | line 2px 主色下划线;segmented 胶囊 |
页签滚动 | setScrollMode(true) | 页签保持自然宽度,滚轮纵向转横向 |
树能力 | search() / setLazyLoader() / 缩进 token | 命中高亮;懒加载占位;--ood-treebar-indent |
工具栏与菜单 | ToolBar 40/48 + 左标题右操作;MenuBar/PopMenu 语义 | 子菜单内 →/← 可切顶层,焦点不丢 |
C 层的目标只有一个:把业务能力做成可被任何外壳调用的确定性单元。
载体 | 契约强度 | 典型用途 | 与 UI 的关系 |
|---|---|---|---|
capability | 60 | 平台级工具函数(查询、计算、转换) | 无 |
chat_tool | 50 | 对话可调用工具 | 无(由 Agent 决定何时调用) |
chat_scene | 45 | 领域场景(提示词 + 工具白名单) | 弱(可被页面小助手复用) |
nlp_skill | 40 | 意图 → 动作映射 | 无 |
unified_action | 30 | 统一动作入口(按钮/命令共用) | 弱(同一动作两种触发) |
declarative | 20 | 声明式流程 / 规则 | 无 |
载体优先级不只是注册顺序,而是契约强度:越靠前越不依赖外部状态,越适合被任意外壳复用。「同一能力两种 UI 都能用」不是靠适配器堆出来的,而是定义时就排除了 UI 依赖。
层 | 回答 | 口径 |
|---|---|---|
目录层 Catalog | 「有哪些技能」,每 skillId 唯一 | 253(code 246 + declarative 7) |
驻留层 Residency | 「谁能执行」,(skillId, nodeId) 多份 | 499(246×2 宿主 + 7) |
技能存在性必须问目录层,不能用驻留行数代替 —— 否则换个外壳,统计口径跟着变。
codeAgent 6 个能力域执行体与前端 6 张快捷卡 1:1,但这层关系只用于寻址。执行体本身不依赖卡片:A2A 调用、流程节点调用、其它 Agent 委派都能直接命中。
C 端能力的「三不」约束
① 不读 UI 状态 入参只有显式 id / 结构化条件,没有「当前选中行」
② 不返回 UI 片段 返回结构化结果 + 产物引用,由外壳决定渲染
③ 不吞异常 失败必须带错误码与原因(禁止静默降级)B 是全文的重心:它既是领域逻辑的家,也是两种交互的唯一汇聚点。
类别 | 载体 | 内容 | 为什么在 B |
|---|---|---|---|
领域模型 | Java 注解 | @DBTable/@DBField/@Uid/@Ref | 平台反射装配元数据 |
视图契约 | Java 注解 | @ModuleAnnotation/@TreeGridViewAnnotation/@FormAnnotation/@PageBar | buildView 读注解出 .cls |
动作入口 | Java 注解 | @APIEventAnnotation(bindMenu, autoRun) | 按钮与对话走同一入口 |
模块与菜单 | YAML | module/modules.yaml | 客户端可改,不编译进类 |
授权 / 角色矩阵 | YAML | authz/roles.yaml:角色 → 动作 / 字段 / 数据范围 | 客户差异大,且必须可审计可回滚 |
执行体名册 | YAML | agents.yaml:capability / capabilityDomain / skillIds | 两种外壳共用同一份名册 |
流程 | JSON | flow-decl/{flowId}/definition.json | 人工门与上下文装载由流程声明 |
一条不能让步的边界
科室级 / 字段级 / 状态级的可见性与可操作性,必须在 B 的 YAML 中由业务人员可视化配置。它是客户侧事实、是组织 × 流程状态的笛卡尔积、需要可 diff 可回滚 —— 因此不交给模型自由生成。

图 2 同一份 B 声明向上投影成两种交互;共享内核保证同源
场景特征 | 选择 | 理由 |
|---|---|---|
高频、结构化、批量录入 | 页面模式 | 表格的密度与批量效率不可替代 |
细粒度授权、字段级可见性 | 页面模式 | 约束需要「看得见地配置与核对」 |
目标导向、步骤不固定 | 对话模式 | 用户说不清路径,只要结果 |
跨系统取数 + 多步分析 + 出图 | 对话模式 | 流式过程与图表即时绘制是核心体验 |
需留痕的审批与对外承诺 | 两模式混用 | 对话发起 → 人工门确认 → 落回页面台账 |
既有系统改造(前端不能重做) | 页面模式 | 零侵入获得 NLP 与智能操作 |
「两种模式」听起来像两套系统。实际上 B 只做一件事:把同一份声明投影到五个可替换的接缝上,并在切换时按规则搬运状态。
接缝 | 页面模式(UI + 小助手) | 对话模式(chatnext 原生) | 共同来源(B/A) |
|---|---|---|---|
① 入口 | 菜单 / 工具栏 / 行命令 / 右下浮层 | 输入框 / 快捷卡 / @执行体 | modules.yaml 菜单 + entryProfile 档位 |
② 动作 | @APIEventAnnotation(bindMenu) 绑定按钮 | 同一注解暴露为工具 / 场景动作 | 同一份动作入口(unified_action) |
③ 上下文 | 页面级:类名 / 选中行 / 当前节点 | 会话级 + 任务级:conversationId / contextRef | 两者都读「画布通道状态」canvasChannel |
④ 产出 | .cls 组件树;表格刷新 / 表单回填 / 画布写指令 | Markdown / 图表 / 产物卡 / 设计器预览 | A 层同一批组件 + A2UI 渲染能力 |
⑤ 人工门 | 页面内行内二次确认(armConfirm) | 对话内确认卡(ConfirmBar,唯一出口) | 同一 human 事件域与超时策略 |
关键点
五个接缝里,②动作 与 ⑤人工门 完全共用实现,只是渲染位置不同;①入口 与 ④产出 由外壳各自实现;③上下文 通过「画布通道 + 对象标识」对齐。因此模式转换不是数据迁移,而是外壳替换 + 状态搬运。
状态 | 是否随行 | 载体 | 说明 |
|---|---|---|---|
业务对象标识(主键 / id) | 随行 | 页面级上下文 → contextRef | 从表格某行「带着它去问」 |
筛选条件 / 期间 / 主体 | 随行 | 标量上下文(只出标量) | 避免「问的是这个期间、答的是别的期间」 |
任务上下文(步骤 + 产物) | 随行 | contextRef(任务上下文存储) | 跨轮次承接,可回读 |
画布通道可达性 | 随行 | canvasChannel / canvasPage | 决定能否直接下发写指令 |
滚动位置 / 展开折叠 | 不随行 | 各外壳本地状态 | 属于瞬时展示状态,搬运反而打断 |
输入框草稿 | 不随行 | 各外壳本地存储 | 避免把页面查询词误当成对话输入 |
小助手读取当前页类名、选中行与画布通道状态,作为标量上下文下发;用户不必重复「我在看哪个部门」。
对话产出可用 .cls 片段或设计器预览呈现;用户点「展开」即进入页面模式,在同一对象上继续结构化操作。
两个动作都要求「对象标识」在两侧同构 —— 这正是 B 的视图契约(同一 id 语义)所保证的。
不变量 | 含义 | 保障方式 |
|---|---|---|
同一权限判定 | 按钮能点的,对话未必能点;反之亦然 —— 判定逻辑只有一处 | 动作入口唯一(@APIEventAnnotation / unified_action) |
同一审计留痕 | 无论谁触发,写操作都进同一份审计 | C 侧写路径统一,不区分调用方 |
同一幂等键 | 重复触发不产生重复数据 | 业务键在 C 侧定义(如去重键三段回退) |
同一失败原因码 | 报错文案可以不同,原因码必须相同 | UNDELIVERABLE / OP_NOT_IN_DOMAIN / COMPILATION_FAILED 等原样透出 |
同一人工门策略 | 红线节点的超时行为不因外壳而变 | 红线统一按 ESCALATE_HUMAN 处理 |
级别 | 内容 | 两种模式如何使用 |
|---|---|---|
会话级 | conversationId / sessionId / workMode | 对话:会话主线;页面:小助手复用隐式会话 |
任务级 | contextRef(步骤 + 产物) | 对话:跨轮次承接;页面:带入当前编辑对象 |
页面级 | 当前页类名、选中行、当前节点 | 对话:作为意图约束;页面:小助手直接读取 |
画布通道 | canvasChannel: connected|connecting|offline + canvasPage | 决定能否直接下发写指令,避免离线时假装成功 |
// 只出标量,不新造事件名;取不到即省略该键(不发空串占位)
context() → { canvasChannel: 'connected' | 'connecting' | 'offline', canvasPage: 'view.Fef' }
图 3 智能操作转换:意图、动作选择、执行共用;入口与出口由外壳决定
A2UI 不是「第三种 UI」,而是把 Agent 产出编译成可交互界面的补充层 —— 它在两种模式下都生效,用法不同。
能力 | 对话模式 | 页面模式 |
|---|---|---|
Markdown 渲染 | 对话正文主体(流式节流) | 浮层说明;可「展开到页面」 |
图表即时绘制 | LLM 下发 spec → 前端编译 ECharts option | 画进页面容器,替代静态报表 |
组件树渲染 | 产物卡 / 设计器预览(.cls 片段) | 主界面本身就是 .cls 组件树 |
HUMAN 交互 | 对话内确认卡(ConfirmBar) | 页面内 armConfirm 或浮层确认卡 |
无头操作 | 对话直接下发画布写指令 | 小助手读页面上下文后调用页面写通道 |
页面模式里 A2UI 是本体,对话模式里 A2UI 是补充;两者共用同一套渲染能力(令牌 + ood.UI.* + .cls),所以视觉与语义不会分裂。
区域 | 职责 | 承载组件 |
|---|---|---|
左栏 | 会话与业务入口(全部 / 待办 / 进行中 / 草稿 / 历史 / 业务域) | ConvoList / Shell |
中栏 | 对话主线:输入 → 流式 → HUMAN → 产出 | MessageList / Composer / ConfirmBar / ArtifactBar |
右栏 | 业务侧栏(流程办理台、工具、上下文、工作区) | 插件挂载点 + Panels / WorkspacePanel |
事件按域拆分并由 Bus + EventStore 分发:stream / tool / phase / human / flow / artifact / agent / collab / docctx / skill / conn。域侧只按事件名分派,不认识具体插件。
PluginResolver(profile, options)
① profile.plugins[id] × 注册表 → 实例化哪些插件
② flagMode: default-on | opt-in | truthy → profile 字段启用/显隐
③ mountPoint(profile 或 ctor.mountSpec) → 挂到哪个挂载点
④ subscribes: [{event, slot, method}] → 声明式订阅
⑤ defaultEnabled → 未声明时是否参与装配插件 | 目录 | 作用 |
|---|---|---|
BpmContextAdapter | plugins/bpm | 把流程 / 节点上下文注入对话 |
BpmHumanOps | plugins/bpm | 委托 / 退回 / 收交 / 办结 |
BpmViewAction | plugins/bpm | 流程视图动作 |
DesignerCardPlugin | plugins/bpm | 设计器产出卡 |
PropertyCardPlugin | plugins/bpm | 属性卡 |
CompanyPicker | plugins/finance | 主体(公司)选择 |
ConfigPanel | plugins/finance | 财务域配置面板 |
FlowSnapshot | plugins/flow | 流程快照渲染 |
PageChannelBridge | plugins/page | 画布通道状态(→ 无头操作) |
DynamicGraphicsPlugin | plugins/views | 动态图形(spec → 图表) |
三个设计取向
① 内核零业务名词:装配器只认 id / mountPoint / subscribes。② 域不认识插件:按事件名双向解耦。③ 缺失即降级:插件未加载只告警一次,主链路照常运行。
同一套内核通过 entryProfile 派生不同产品形态:finance-agent、codeagent-agent、dev-agent、edu-teacher、kb-agent、patent-agent、page-workbench、code-workbench 等,共 16 个档位。差异全部由 profile 声明(启用哪些插件、挂哪些面板、用什么文案),内核不写分支。

三栏工作台(3840×2160 原图):左=会话与业务入口;中=对话与输入(模式切换、模型选择);右=「流程办理台」插件面板(300 条流程实例 + 当前节点 + 操作结果)。右栏即插件挂载点,未在 profile 声明的插件不会出现

思考块 + Markdown:推理可折叠,正文按层次呈现

Markdown 表格:口径与明细用表格表达,可直接复制

@执行体:显式指定由谁执行,避免「猜」

工具调用:工具面以白名单暴露,调用过程可见

结果卡:结论 + 依据 + 可下载产物

技能市场:技能可安装与授权,与「C 端能力独立于 UI」一致
架构解决「能不能」,体验解决「顺不顺」。七层模型把「顺不顺」拆成可验收项。
L1 输入层Composer多模态 · 上下文徽标 · @执行体 · 中断与重试L2 语义层意图与澄清低置信追问 · 歧义用 CHOICE · 阈值单一真源L3 过程层流式可观测首字节 · 思考折叠 · 工具/阶段 · 可取消 · 失败带原因L4 决策层HUMAN 交互四类交互 · 超时按风险分级 · 红线默认升级人工L5 产出层MD/图表/产物MD 为核心 · source/asOf 强制 · 产物可下载L6 协作层A2A 协作委托台账 · 并行汇聚 · 越域带原因拒绝L7 可信层:事件账本 · 口径来源 · 可复现 · 变更留痕
图 4 七层体验模型:L3–L5 决定「敢不敢用」,L6–L7 决定「能不能规模化」
层 | 关键设计 | 度量 |
|---|---|---|
L1 输入 | 上下文徽标、@执行体、流式期可取消 | 二次补充率 ↓ |
L2 语义 | 低置信追问、CHOICE 消歧、阈值分层 | 澄清命中率 ↑,误切场景 ↓ |
L3 过程 | 首字节优先、思考折叠、工具/阶段可见、失败带原因 | 首字节 <1.5s,静默失败 0 |
L4 决策 | CONFIRM/INPUT/CHOICE/FILE_UPLOAD × AUTO_CONFIRM/AUTO_REJECT/ESCALATE_HUMAN | 红线人工介入 ≈100% |
L5 产出 | MD + 图表即时绘制 + 产物卡;source/asOf 强制 | 产出采纳率 ↑ |
L6 协作 | 委托台账、replyNodeId、AND_SPLIT/JOIN | 失败可定位率 100% |
L7 可信 | EventStore、审计摘要、.oodbak/发布台账 | 可复现率 ↑ |
边界一:授权约束不交给模型
科室级、字段级、状态级的可见性与可操作性,由业务人员在 B 的 YAML 中可视化配置。让模型生成授权,等于把审计能力交给被审计对象。
边界二:失败必须带原因
UNDELIVERABLE / OP_NOT_IN_DOMAIN / COMPILATION_FAILED 原样透出。静默降级会让两种模式都「对不上」,也就谈不上同源。
边界三:红线不自动放行
涉钱、涉合同、涉对外承诺的节点,即使调用方传了自动确认,超时也按 ESCALATE_HUMAN 处理 —— 这是代码里的约束,不依赖文档约定。
文中数值与契约来自源码与运行态实测;截图为真实运行环境归档(三栏工作台为 3840×2160 原图)。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。