|
|
---
|
|
|
title: "第十章:A2UI 协议(流式 UI 组件)"
|
|
|
---
|
|
|
|
|
|
本章目标:实现 A2UI 协议,将 Agent 的输出渲染为流式 UI 组件。
|
|
|
|
|
|
## 重要说明:A2UI 的边界
|
|
|
|
|
|
A2UI 并不属于 Eino 框架本身的范畴,它是一个业务层的 UI 协议/渲染方案。本章把 A2UI 集成进前面章节逐步构建出来的 Agent,是为了提供一个端到端、可落地的完整示例:从模型调用、工具调用、工作流编排,到最终把结果以更友好的 UI 方式呈现出来。
|
|
|
|
|
|
在真实业务场景中,你完全可以根据产品形态选择不同的 UI 形式,例如:
|
|
|
|
|
|
- Web / App:自定义组件、表格、卡片、图表等
|
|
|
- IM/办公套件:消息卡片、交互式表单
|
|
|
- 命令行:纯文本或 TUI(终端 UI)
|
|
|
|
|
|
Eino 更关注“可组合的智能执行与编排能力”,至于“如何呈现给用户”,属于业务层可以自由扩展的一环。
|
|
|
|
|
|
## 代码位置
|
|
|
|
|
|
- 入口代码:[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)
|
|
|
|
|
|
## 运行
|
|
|
|
|
|
在 `quickstart/chatwitheino` 目录下执行:
|
|
|
|
|
|
```bash
|
|
|
go run .
|
|
|
```
|
|
|
|
|
|
输出示例:
|
|
|
|
|
|
```text
|
|
|
starting server on http://localhost:8080
|
|
|
```
|
|
|
|
|
|
### (可选)启用 ch09 的 skills 能力
|
|
|
|
|
|
最终 Web 版使用的 Agent 构建逻辑与 Chapter 9 对齐:当 `EINO_EXT_SKILLS_DIR` 指向一个合法 skills 目录时,会自动注册 `skill` 中间件,模型就能按需调用 `skill` 工具加载 `eino-guide` / `eino-component` / `eino-compose` / `eino-agent`。
|
|
|
|
|
|
```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 .
|
|
|
```
|
|
|
|
|
|
## 从文本到 UI:为什么需要 A2UI
|
|
|
|
|
|
前八章我们实现的 Agent 只输出文本,但现代 AI 应用需要更丰富的交互。
|
|
|
|
|
|
**纯文本输出的局限:**
|
|
|
- 无法展示结构化数据(表格、列表、卡片等)
|
|
|
- 无法实时更新(进度条、状态变化等)
|
|
|
- 无法嵌入交互元素(按钮、表单、链接等)
|
|
|
- 无法支持多媒体(图片、视频、音频等)
|
|
|
|
|
|
**A2UI 的定位:**
|
|
|
- **A2UI 是 Agent 到 UI 的协议**:定义了 Agent 输出如何映射到 UI 组件
|
|
|
- **A2UI 支持流式渲染**:组件可以实时更新,无需等待完整响应
|
|
|
- **A2UI 是声明式的**:Agent 只需声明"显示什么",UI 负责渲染
|
|
|
|
|
|
**简单类比:**
|
|
|
- **纯文本输出** = "终端命令行"(只能显示文本)
|
|
|
- **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 消息:BeginRendering / SurfaceUpdate / DataModelUpdate / InterruptRequest
|
|
|
|
|
|
每一行 SSE(`data: {...}`)承载一个 A2UI Message,Message 是一个“信封结构”,每次只会出现一个字段:
|
|
|
|
|
|
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [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` 互相引用)
|
|
|
- `DataModelUpdate`:更新 data bindings(用于把流式文本增量更新到某个 Text 组件)
|
|
|
- `InterruptRequest`:当 Agent 触发 interrupt(例如审批)时,通知前端展示批准/拒绝入口
|
|
|
|
|
|
### A2UI 组件:Text / Column / Card / Row
|
|
|
|
|
|
本示例 UI 组件只实现了 4 种(见 [a2ui/types.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/types.go)):
|
|
|
|
|
|
- `Text`:文本渲染(支持 `usageHint` 区分 caption/body/title);当 `dataKey` 存在时,文本来自 `DataModelUpdate`
|
|
|
- `Column` / `Row`:布局(children 是组件 ID 列表)
|
|
|
- `Card`:卡片容器(children 是组件 ID 列表)
|
|
|
|
|
|
## A2UI 的实现:把 AgentEvent 转成 A2UI SSE
|
|
|
|
|
|
最终 Web 版的核心链路是:
|
|
|
|
|
|
- 后端运行 Agent,得到 `*adk.AsyncIterator[*adk.AgentEvent]`
|
|
|
- 把事件流转换为 A2UI JSONL/SSE 流输出给浏览器(见 [a2ui/streamer.go](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/a2ui/streamer.go))
|
|
|
- 前端解析 SSE 的 `data:` 行并渲染组件树(见 [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 /`:返回前端页面 `static/index.html`
|
|
|
- `POST /sessions/:id/chat`:返回 SSE 流(A2UI messages),把 Agent 运行结果边跑边渲染到 UI
|
|
|
- `GET /sessions/:id/render`:返回 JSONL(A2UI messages),用于“选中会话时回放历史”
|
|
|
- `POST /sessions/:id/approve`:处理 interrupt 的批准/拒绝并继续返回 SSE 流
|
|
|
|
|
|
### 事件流转换(高层)
|
|
|
|
|
|
服务端把 `Runner.Run(...)` 的事件流交给 `a2ui.StreamToWriter(...)`,后者负责:
|
|
|
|
|
|
- 对 user/assistant/tool 的输出做拆分
|
|
|
- 把 tool call / tool result 渲染成 “chip 卡片”
|
|
|
- 把 assistant 的流式 token 做成 `DataModelUpdate`,实现“边生成边渲染”
|
|
|
- 遇到 interrupt 时发送 `InterruptRequest`,并暂停等待人类批准
|
|
|
|
|
|
## 前端集成:fetch + SSE(不是 WebSocket)
|
|
|
|
|
|
- 前端通过 `fetch('/sessions/:id/chat')` 发起请求,然后从 `res.body` 读取流式字节,按行切分并解析 `data: {...}` 的 JSON(见 [static/index.html](https://github.com/cloudwego/eino-examples/blob/main/quickstart/chatwitheino/static/index.html))。
|
|
|
|
|
|
**关键代码片段(**注意:这是简化后的代码片段,不能直接运行,完整代码请参考** [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));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
## A2UI 流式渲染流程(概览)
|
|
|
|
|
|
```
|
|
|
┌─────────────────────────────────────────┐
|
|
|
│ 用户:分析这个文件 │
|
|
|
└─────────────────────────────────────────┘
|
|
|
↓
|
|
|
┌──────────────────────┐
|
|
|
│ Agent 开始处理 │
|
|
|
│ A2UI: AddText │
|
|
|
│ "正在分析..." │
|
|
|
└──────────────────────┘
|
|
|
↓
|
|
|
┌──────────────────────┐
|
|
|
│ 调用 Tool │
|
|
|
│ A2UI: AddProgress │
|
|
|
│ 进度: 0% │
|
|
|
└──────────────────────┘
|
|
|
↓
|
|
|
┌──────────────────────┐
|
|
|
│ Tool 执行中 │
|
|
|
│ A2UI: UpdateProgress│
|
|
|
│ 进度: 50% │
|
|
|
└──────────────────────┘
|
|
|
↓
|
|
|
┌──────────────────────┐
|
|
|
│ Tool 完成 │
|
|
|
│ A2UI: tool result │
|
|
|
└──────────────────────┘
|
|
|
↓
|
|
|
┌──────────────────────┐
|
|
|
│ 显示结果 │
|
|
|
│ A2UI: DataModelUpdate│
|
|
|
│ (流式更新 assistant)│
|
|
|
└──────────────────────┘
|
|
|
```
|
|
|
|
|
|
## 本章小结
|
|
|
|
|
|
- **A2UI**:Agent 到 UI 的协议,定义了 Agent 输出如何映射到 UI 组件
|
|
|
- **子集实现**:本示例只实现了 Text/Column/Card/Row 与 data binding
|
|
|
- **流式输出**:后端以 SSE 推送 A2UI JSONL,前端增量渲染组件树
|
|
|
- **事件到 UI**:把 `AgentEvent` 转为 `tool call / tool result / assistant stream` 的可视化输出
|
|
|
|
|
|
## 系列收尾:这个 Quickstart Agent 的完整愿景
|
|
|
|
|
|
到本章为止,我们用一个可以实际运行的 Agent 串起了 Eino 的核心能力。你可以把它理解为一个可扩展的“端到端 Agent 应用骨架”:
|
|
|
|
|
|
- 运行时:Runner 驱动执行,支持流式输出与事件模型
|
|
|
- 工具层:Filesystem / Shell 等 Tool 能力接入,工具错误可被安全处理
|
|
|
- 中间件:可插拔的 middleware/handler,用于错误处理、重试、审批等横切能力
|
|
|
- 可观测:callbacks/trace 能力把关键链路打通,便于调试与线上观测
|
|
|
- 人机协作:interrupt/resume + checkpoint 支持审批、补参、分支选择等交互式流程
|
|
|
- 确定性编排:compose(graph/chain/workflow)把复杂业务流程组织为可维护、可复用的执行图
|
|
|
- 业务交付:像 A2UI 这样的 UI 集成,属于业务层自由选择的一环,用来把 Agent 能力以合适的产品形态呈现给用户
|
|
|
|
|
|
你可以在这个骨架上逐步替换/扩展任意环节:模型、工具、存储、工作流、前端渲染协议,而不需要推倒重来。
|
|
|
|
|
|
## 扩展思考
|
|
|
|
|
|
**其他组件类型:**
|
|
|
- 图表组件(折线图、柱状图、饼图)
|
|
|
- 地图组件
|
|
|
- 时间线组件
|
|
|
- 树形组件
|
|
|
- 标签页组件
|
|
|
|
|
|
**高级功能:**
|
|
|
- 组件交互(点击、拖拽、输入)
|
|
|
- 条件渲染
|
|
|
- 组件动画
|
|
|
- 响应式布局
|