第九章:A2UI 协议(流式 UI 组件)
本章目标:实现 A2UI 协议,将 Agent 的输出渲染为流式 UI 组件。
重要说明:A2UI 的边界
A2UI 并不属于 Eino 框架本身的范畴,它是一个业务层的 UI 协议/渲染方案。本章把 A2UI 集成进前面章节逐步构建出来的 Agent,是为了提供一个端到端、可落地的完整示例:从模型调用、工具调用、工作流编排,到最终把结果以更友好的 UI 方式呈现出来。
在真实业务场景中,你完全可以根据产品形态选择不同的 UI 形式,例如:
- Web / App:自定义组件、表格、卡片、图表等
- IM/办公套件:消息卡片、交互式表单
- 命令行:纯文本或 TUI(终端 UI)
Eino 更关注"可组合的智能执行与编排能力",至于"如何呈现给用户",属于业务层可以自由扩展的一环。
代码位置
- 入口代码:main.go
- A2UI 实装:a2ui/streamer.go
前置条件
与第一章一致:需要配置一个可用的 ChatModel(OpenAI 或 Ark)
运行
在 examples/quickstart/chatwitheino 目录下执行:
go run .
输出示例:
starting server on http://localhost:8080
从文本到 UI:为什么需要 A2UI
前八章我们实现的 Agent 只输出文本,但现代 AI 应用需要更丰富的交互。
纯文本输出的局限:
- 无法展示结构化数据(表格、列表、卡片等)
- 无法实时更新(进度条、状态变化等)
- 无法嵌入交互元素(按钮、表单、链接等)
- 无法支持多媒体(图片、视频、音频等)
A2UI 的定位:
- A2UI 是 Agent 到 UI 的协议:定义了 Agent 输出如何映射到 UI 组件
- A2UI 支持流式渲染:组件可以实时更新,无需等待完整响应
- A2UI 是声明式的:Agent 只需声明"显示什么",UI 负责渲染
简单类比:
- 纯文本输出 = “终端命令行”(只能显示文本)
- A2UI = “Web 应用”(可以显示任何 UI 组件)
关键概念
A2UI 组件
A2UI 定义了一系列 UI 组件类型:
type ComponentType string
const (
ComponentText ComponentType = "text" // 文本
ComponentMarkdown ComponentType = "markdown" // Markdown
ComponentCode ComponentType = "code" // 代码块
ComponentImage ComponentType = "image" // 图片
ComponentTable ComponentType = "table" // 表格
ComponentCard ComponentType = "card" // 卡片
ComponentButton ComponentType = "button" // 按钮
ComponentForm ComponentType = "form" // 表单
ComponentProgress ComponentType = "progress" // 进度条
ComponentDivider ComponentType = "divider" // 分隔线
)
A2UI 消息
每条 A2UI 消息包含:
type Message struct {
ID string // 消息 ID
Role string // user / assistant
Components []Component // UI 组件列表
Timestamp time.Time // 时间戳
}
A2UI 流式输出
A2UI 支持流式输出组件:
type StreamMessage struct {
Type string // add / update / delete
Index int // 组件索引
Component Component // 组件内容
}
流式更新类型:
add:添加新组件update:更新已有组件delete:删除组件
A2UI 的实现
1. 创建 A2UI Streamer
streamer := a2ui.NewStreamer()
2. 添加组件
// 添加文本组件
streamer.AddText("正在处理您的请求...")
// 添加进度条
streamer.AddProgress(0, 100, "加载中")
// 更新进度
streamer.UpdateProgress(0, 50, "处理中")
// 添加代码块
streamer.AddCode("go", `fmt.Println("Hello, World!")`)
// 添加表格
streamer.AddTable([][]string{
{"Name", "Age", "City"},
{"Alice", "30", "New York"},
{"Bob", "25", "London"},
})
3. 流式输出
// 获取流式消息
stream := streamer.Stream()
for {
msg, ok := stream.Next()
if !ok {
break
}
// 发送到前端
sendToClient(msg)
}
**关键代码片段(注意:这是简化后的代码片段,不能直接运行,完整代码请参考 cmd/ch09/main.go):
// 创建 A2UI Streamer
streamer := a2ui.NewStreamer()
// Agent 执行过程中添加组件
streamer.AddText("我来帮你分析这个文件...")
// 调用 Tool
streamer.AddProgress(0, 0, "读取文件")
result, err := tool.Run(ctx, args)
streamer.UpdateProgress(0, 100, "完成")
// 显示结果
streamer.AddCode("json", result)
// 流式输出
stream := streamer.Stream()
for {
msg, ok := stream.Next()
if !ok {
break
}
wsConn.WriteJSON(msg)
}
A2UI 与 Agent 的集成
在 Agent 中使用 A2UI
func buildAgent(ctx context.Context) (adk.Agent, error) {
return deep.New(ctx, &deep.Config{
Name: "A2UIAgent",
Description: "Agent with A2UI streaming output",
ChatModel: cm,
Backend: backend,
// 配置 A2UI Streamer
StreamingShell: backend,
})
}
在 Runner 中使用 A2UI
runner := adk.NewRunner(ctx, adk.RunnerConfig{
Agent: agent,
EnableStreaming: true,
})
// 执行 Agent
events := runner.Run(ctx, history)
// 将事件转换为 A2UI 组件
streamer := a2ui.NewStreamer()
for {
event, ok := events.Next()
if !ok {
break
}
if event.Output != nil && event.Output.MessageOutput != nil {
// 添加文本组件
streamer.AddText(event.Output.MessageOutput.Message.Content)
}
}
A2UI 流式渲染流程
┌─────────────────────────────────────────┐
│ 用户:分析这个文件 │
└─────────────────────────────────────────┘
↓
┌──────────────────────┐
│ Agent 开始处理 │
│ A2UI: AddText │
│ "正在分析..." │
└──────────────────────┘
↓
┌──────────────────────┐
│ 调用 Tool │
│ A2UI: AddProgress │
│ 进度: 0% │
└──────────────────────┘
↓
┌──────────────────────┐
│ Tool 执行中 │
│ A2UI: UpdateProgress│
│ 进度: 50% │
└──────────────────────┘
↓
┌──────────────────────┐
│ Tool 完成 │
│ A2UI: UpdateProgress│
│ 进度: 100% │
└──────────────────────┘
↓
┌──────────────────────┐
│ 显示结果 │
│ A2UI: AddCode │
│ 代码块 │
└──────────────────────┘
前端集成
WebSocket 连接
const ws = new WebSocket('ws://localhost:8080/ws');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
renderComponent(msg);
};
function renderComponent(msg) {
const { type, index, component } = msg;
switch (component.type) {
case 'text':
renderText(component.content);
break;
case 'code':
renderCode(component.language, component.content);
break;
case 'progress':
renderProgress(component.value, component.max, component.label);
break;
// ...
}
}
本章小结
- A2UI:Agent 到 UI 的协议,定义了 Agent 输出如何映射到 UI 组件
- 组件类型:文本、Markdown、代码、图片、表格、卡片、按钮、表单、进度条等
- 流式输出:支持实时添加、更新、删除组件
- 声明式:Agent 只需声明"显示什么",UI 负责渲染
- 前端集成:通过 WebSocket 实现实时通信
系列收尾:这个 Quickstart Agent 的完整愿景
到本章为止,我们用一个可以实际运行的 Agent 串起了 Eino 的核心能力。你可以把它理解为一个可扩展的"端到端 Agent 应用骨架":
- 运行时:Runner 驱动执行,支持流式输出与事件模型
- 工具层:Filesystem / Shell 等 Tool 能力接入,工具错误可被安全处理
- 中间件:可插拔的 middleware/handler,用于错误处理、重试、审批等横切能力
- 可观测:callbacks/trace 能力把关键链路打通,便于调试与线上观测
- 人机协作:interrupt/resume + checkpoint 支持审批、补参、分支选择等交互式流程
- 确定性编排:compose(graph/chain/workflow)把复杂业务流程组织为可维护、可复用的执行图
- 业务交付:像 A2UI 这样的 UI 集成,属于业务层自由选择的一环,用来把 Agent 能力以合适的产品形态呈现给用户
你可以在这个骨架上逐步替换/扩展任意环节:模型、工具、存储、工作流、前端渲染协议,而不需要推倒重来。
扩展思考
其他组件类型:
- 图表组件(折线图、柱状图、饼图)
- 地图组件
- 时间线组件
- 树形组件
- 标签页组件
高级功能:
- 组件交互(点击、拖拽、输入)
- 条件渲染
- 组件动画
- 响应式布局