This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/Eino/quick_start/chapter_09_a2ui_protocol.md
T

253 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
Description: ""
date: "2026-03-16"
lastmod: ""
tags: []
title: 第十章:A2UI 协议(流式 UI 组件)
weight: 10
---
本章目标:实现 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 .
```
输出示例:
```
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):**
```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):**
```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 能力以合适的产品形态呈现给用户
你可以在这个骨架上逐步替换/扩展任意环节:模型、工具、存储、工作流、前端渲染协议,而不需要推倒重来。
## 扩展思考
**其他组件类型:**
- 图表组件(折线图、柱状图、饼图)
- 地图组件
- 时间线组件
- 树形组件
- 标签页组件
**高级功能:**
- 组件交互(点击、拖拽、输入)
- 条件渲染
- 组件动画
- 响应式布局