---
tags: [eino, ai-development, go, quickstart, a2ui, sse]
create time: 2026-04-29 16:00
---

# 第十章：A2UI 协议（流式 UI 组件）(最终章)

## 概述

本章作为 ChatWithEino Quickstart 的最终章，引入 **A2UI 协议**——把 Agent 的事件流以 JSONL/SSE 的形式推送到前端，渲染为可增量更新的 UI 组件树。你将掌握 Agent 到 Web 的端到端集成方案，理解为什么 AI 应用需要从纯文本走向结构化、可交互的 UI 呈现。

> [!tip] 一句话理解 A2UI
>
> **A2UI = Agent 输出 × UI 组件映射**。它定义了"Agent 做了什么"如何变成"用户看到了什么"：文本 → Text 组件、工具调用 → Chip 卡片、进度更新 → 实时更新……一切通过声明式的组件树实现。

---

## 代码位置

- 入口代码：[main.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/main.go)
- Agent 构建：[agent.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/agent.go)
- 服务端路由：[server/server.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/server/server.go)
- A2UI 子集实现：[a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go)
- A2UI 事件流转换：[a2ui/streamer.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/streamer.go)
- 前端页面：[static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html)

## 前置条件

与第一章一致：需要配置一个可用的 ChatModel（OpenAI 或 Ark）。

## 运行

在 `examples/quickstart/chatwitheino` 目录下执行：

```bash
go run .
```

输出示例：

```
starting server on http://localhost:8080
```

启动后浏览器访问 `http://localhost:8080` 即可看到完整的 A2UI 交互界面。

### （可选）启用 Skills 能力

最终 Web 版使用的 Agent 构建逻辑与第九章对齐：当 `EINO_EXT_SKILLS_DIR` 指向一个合法 skills 目录时，会自动注册 `skill` 中间件，模型就能按需调用 `skill` 工具加载文档。

```bash
go run ./scripts/sync_eino_ext_skills.go -src /path/to/eino-ext -dest ./skills/eino-ext -clean
EINO_EXT_SKILLS_DIR="$(pwd)/skills/eino-ext" go run .
```

## A2UI 的定位与边界

> [!important] A2UI 不属于 Eino 框架本身

A2UI 是一个**业务层的 UI 协议/渲染方案**，不是 Eino 的核心 Component。本章把它集成进前面章节逐步构建出来的 Agent，是为了提供一个端到端、可落地的完整示例：从模型调用、工具调用、工作流编排，到最终把结果以更友好的 UI 方式呈现出来。

**真实业务场景中，你完全可以根据产品形态选择不同的 UI 形式：**

| 场景 | UI 形式 | 说明 |
|------|---------|------|
| Web / App | 自定义组件、表格、卡片、图表 | 最典型的 B/S 架构应用 |
| IM / 办公套件 | 消息卡片、交互式表单 | 飞书、钉钉等平台的富消息 |
| 命令行 | 纯文本或 TUI | Console 版 Agent 的原生形式 |

Eino 关注「可组合的智能执行与编排能力」，而「如何呈现给用户」属于业务层可以自由扩展的一环。

## 从纯文本到结构化的 UI：为什么需要 A2UI

> [!question] 思考一下
>
> 如果你要为一个 AI 聊天产品设计更丰富的交互体验，纯文本回复会遇到哪些瓶颈？

**纯文本输出的局限：**

- ❌ 无法展示结构化数据（表格、列表、卡片等）
- ❌ 无法实时更新（进度条、状态变化等）
- ❌ 无法嵌入交互元素（按钮、表单、链接等）
- ❌ 无法支持多媒体（图片、视频、音频等）

**A2UI 的定位：**

- ✅ **协议映射**：Agent 输出 → UI 组件的声明式映射关系
- ✅ **流式渲染**：组件实时更新，无需等待完整响应
- ✅ **增量更新**：基于 dataKey 的数据绑定，文本流可逐 token 更新

**简单类比：**

- **纯文本输出** = "终端命令行"（只能显示文本）
- **A2UI** = "Web 应用"（可以显示任何 UI 组件）

## A2UI v0.8 子集（本示例的边界）

本 quickstart 并没有实现一个"完整的 A2UI 标准库"，而是实现了一个 **A2UI v0.8 的子集**：目标是把 Agent 的事件流，以稳定、可增量渲染的 UI 组件树方式推给浏览器。

当前实现的 A2UI 消息类型与组件类型，以 [a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go) 为准。

### A2UI 消息类型：信封结构

每一行 SSE（`data: {...}`）承载一个 A2UI Message，Message 是一个"信封结构"，每次只会出现一个字段：

> [!note] 关键代码片段
>
> 注意：这是简化后的代码片段，不能直接运行，完整代码请参考 [a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go)。

```go
type Message struct {
    BeginRendering   *BeginRenderingMsg
    SurfaceUpdate    *SurfaceUpdateMsg
    DataModelUpdate  *DataModelUpdateMsg
    DeleteSurface    *DeleteSurfaceMsg
    InterruptRequest *InterruptRequestMsg
}
```

| 消息类型 | 作用 | 触发时机 |
|----------|------|----------|
| `BeginRendering` | 告诉前端"开始渲染一个 surface"，指定根节点 ID | 新会话开始时 |
| `SurfaceUpdate` | 新增/更新一批组件（组件是树，用 id 互相引用） | 创建/修改 UI 结构时 |
| `DataModelUpdate` | 更新 data bindings（用于流式文本增量渲染） | assistant 生成文本时 |
| `InterruptRequest` | 通知前端展示批准/拒绝入口 | Agent 需要人类审批时 |
| `DeleteSurface` | 删除某个 surface | 清理/重置会话时 |

### A2UI 组件类型

本示例 UI 组件只实现了 4 种（见 [a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go)）：

```mermaid
classDiagram
    class ComponentTree {
        <<abstract>>
        +id string
        +children []string
    }
    class TextComponent {
        +text string
        +dataKey string
        +usageHint string
    }
    class ColumnLayout {
        +spacing float64
        +align ItemsAlign
    }
    class RowLayout {
        +spacing float64
        +align ItemsAlign
    }
    class CardContainer {
        +title string
        +border bool
    }
    ComponentTree <|-- TextComponent
    ComponentTree <|-- ColumnLayout
    ComponentTree <|-- RowLayout
    ComponentTree <|-- CardContainer
    note for TextComponent "支持 dataKey\n流式绑定"
    note for ColumnLayout "垂直布局容器"
    note for RowLayout "水平布局容器"
    note for CardContainer "内容容器\n无布局功能"
```

各组件职责：

| 组件 | 用途 | 特性 |
|------|------|------|
| `Text` | 文本渲染 | 支持 `usageHint`（caption/body/title），当存在 `dataKey` 时文本来自 `DataModelUpdate` |
| `Column` | 垂直布局 | children 是组件 ID 列表 |
| `Row` | 水平布局 | children 是组件 ID 列表 |
| `Card` | 卡片容器 | children 是组件 ID 列表，仅做视觉分组 |

> [!warning] Card ≠ 布局容器
>
> `Card` 不提供任何布局控制（不排布子元素的位置），它只是一个有视觉边界的容器。如需布局请用 `Column` / `Row`。

## A2UI 的实现链路

最终 Web 版的核心链路是三阶段管道：

1. **后端运行 Agent**：得到 `*adk.AsyncIterator[*adk.AgentEvent]`
2. **事件 → A2UI JSONL/SSE 流**：转换为 A2UI 消息推送给浏览器（见 [a2ui/streamer.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/streamer.go)）
3. **前端解析并渲染**：读取 SSE 流并渲染组件树（见 [static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html)）

### 服务端路由（高层）

与 A2UI 相关的关键接口（见 [server/server.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/server/server.go)）：

| 方法 | 路径 | 响应 | 说明 |
|------|------|------|------|
| GET | `/` | HTML | 返回前端页面 |
| POST | `/sessions/:id/chat` | SSE 流 | Agent 运行结果实时渲染 |
| GET | `/sessions/:id/render` | JSONL | 回放历史消息 |
| POST | `/sessions/:id/approve` | SSE 流 | interrupt 批准后继续执行 |

### 事件流转换

服务端把 `Runner.Run(...)` 的事件流交给 `a2ui.StreamToWriter(...)`，后者负责：

```mermaid
flowchart TD
    A["AgentEvent 输入"] --> B{事件类型}

    B -->|"user"| C["渲染 User 气泡"]
    B -->|"assistant"| D["创建 DataModelUpdate\n流式追加文本"]
    B -->|"tool call"| E["渲染 ToolCall Chip 卡片"]
    B -->|"tool result"| F["渲染 ToolResult Chip 卡片"]
    B -->|"interrupt"| G["发送 InterruptRequest\n暂停等待人类审批"]

    C --> H["SSE JSONL 输出"]
    D --> H
    E --> H
    F --> H
    G --> H

    style D fill:#fff3e0
    style G fill:#fce4ec
```

核心处理逻辑：

- **User 输出**：渲染为用户消息气泡
- **Assistant 流式 token**：创建 `DataModelUpdate`，通过 `dataKey` 绑定到一个 `Text` 组件上，实现"边生成边渲染"
- **Tool Call / Tool Result**：渲染为独立的 chip 卡片
- **Interrupt**：发送 `InterruptRequest`，暂停等待人类批准后 resume

### 前端集成：Fetch + SSE（不是 WebSocket）

前端通过 `fetch('/sessions/:id/chat')` 发起请求，然后从 `res.body` 读取流式字节，按行切分并解析 `data: {...}` 的 JSON：

> [!note] 前端代码片段
>
> 注意：这是简化后的代码片段，不能直接运行，完整代码请参考 [static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html)。

```javascript
const res = await fetch(`/sessions/${id}/chat`, {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({message}),
});

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
  const {done, value} = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, {stream: true});
  const lines = buffer.split('\n');
  buffer = lines.pop();
  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed.startsWith('data:')) {
      const jsonStr = trimmed.slice(5).trimStart();
      processA2UIMessage(JSON.parse(jsonStr));
    }
  }
}
```

> [!tip] 为什么用 SSE 而不是 WebSocket？
>
> - **SSE**（Server-Sent Events）是单向的、HTTP 兼容、天然支持断线重连语义
> - 对于"服务器推送 UI 事件，客户端只需消费"的场景，SSE 比 WebSocket 更轻量
> - 如果未来需要双向交互（如键盘快捷键、实时光标同步），才考虑升级到 WebSocket

## A2UI 流式渲染流程

```mermaid
sequenceDiagram
    participant U as 用户
    participant FE as 前端浏览器
    participant BE as 后端 Server
    participant AG as Agent
    participant LM as LLM

    U->>FE: 输入消息
    FE->>BE: POST /sessions/:id/chat
    BE->>AG: Runner.Run()
    AG->>LM: 发送请求
    LM-->>AG: token 流式返回
    loop 每个 AgentEvent
        AG-->>BE: AgentEvent
        alt assistant token
            BE->>FE: DataModelUpdate (dataKey 绑定)
        else tool call
            BE->>FE: SurfaceUpdate (Chip 卡片)
        end
        FE-->>U: UI 增量更新
    end
    AG-->>AG: 可能需要 Interrupt
    AG->>BE: InterruptRequest
    BE->>FE: InterruptRequest
    FE-->>U: 展示审批按钮
    U->>FE: 点击批准
    FE->>BE: POST /sessions/:id/approve
    BE->>AG: Resume 继续执行
```

## 从 Quickstart 到生产落地

> [!tip] 可扩展的设计思路
>
> A2UI 的协议层和 Agent 层是解耦的。你可以只做其中的任意一部分。

| 方向 | 替换方案 | 适用场景 |
|------|---------|----------|
| 不同 UI 形态 | React/Vue 组件、移动端原生组件、TUI | 产品定位差异 |
| 传输协议升级 | gRPC Streaming / GraphQL Subscription | 需要更强的双向交互 |
| 渲染引擎切换 | 服务端 SSR → 客户端动态渲染 | CDN 加速需求 |
| 多模态扩展 | 嵌入图片、图表、代码高亮 | 数据类/分析型 Agent |

## 本章小结

| 核心概念 | 说明 | 关键点 |
|----------|------|--------|
| **A2UI 协议** | Agent 到 UI 的映射协议 | 声明式组件树 + 数据绑定 |
| **消息信封** | BeginRendering / SurfaceUpdate / DataModelUpdate | 每种消息对应不同生命周期 |
| **组件系统** | Text / Column / Card / Row | 4 种基础组件覆盖常见 UI 场景 |
| **SSE 流** | AgentEvent → A2UI JSONL → 前端渲染 | 单向推送、增量更新 |
| **中断协作** | InterruptRequest + approve 机制 | 人机协同的关键路径 |

> [!success] 学习成果
>
> 完成本章后，你应该能够：
> - 理解 A2UI 协议的设计思路和适用场景
> - 掌握 Agent 事件流到前端 UI 的完整转换链路
> - 使用 SSE 实现增量渲染的前端集成方案
> - 知道如何将这个骨架扩展到不同的产品形态中

## 下一章预告

这是 Quickstart 系列的终章。后续如果你想深入：

- [[Eino/quick_start/chapter_09_skill_console]] — 回顾第九章的 Skill 知识注入能力
- [[Eino/README]] — 探索 Eino 框架的系统性学习路径

## 扩展思考

### 其他组件类型（可选实现方向）

| 组件 | 说明 | 实现难度 |
|------|------|----------|
| **图表组件** | 折线图、柱状图、饼图 | 中高（需接入图表库） |
| **地图组件** | 地理信息可视化 | 中（依赖地图 SDK） |
| **时间线组件** | 事件顺序排列展示 | 低（已有 Column 即可） |
| **树形组件** | 层级数据结构展示 | 中（递归渲染逻辑） |
| **标签页组件** | 多面板 Tab 切换 | 低（State 管理即可） |

### 高级交互能力

| 能力 | 说明 | 技术要点 |
|------|------|----------|
| **组件交互** | 点击、拖拽、输入反馈 | 前端事件 → API 调用 |
| **条件渲染** | 根据数据决定组件显隐 | 服务端根据状态发送不同 SurfaceUpdate |
| **组件动画** | 平滑过渡效果 | CSS transition / animation |
| **响应式布局** | 自适应屏幕尺寸 | 媒体查询 + 弹性布局 |

## 关联笔记

- [[Eino/quick_start/_index]] — Quickstart 系列统一入口
- [[Eino/quick_start/chapter_08_graph_tool]] — Graph Tool 复杂工作流编排
- [[Eino/quick_start/chapter_09_skill_console]] — Skill 知识与指令注入
- [[Eino/quick_start/chapter_07_interrupt_resume]] — Interrupt 与 Resume 机制
