第一章:AG-UI 基础与协议架构
为什么需要 AG-UI → 它与 MCP、A2A 有什么区别 → 它的架构如何设计 → 一次 Agent 请求是如何执行的。
你可以先记住一句话:
AG-UI 是 Agent 与用户界面之间的交互协议,它把 Agent 执行过程中产生的消息、工具调用、状态变化等信息,转换为前端能够理解和处理的标准化事件流。
1.1 AG-UI 协议概述
1.1.1 什么是 AG-UI?
AG-UI 的全称是 Agent--User Interaction Protocol,即智能体与用户交互协议。
我们先看一个实际场景。
假设你开发了一个基于 LangGraph 的智能体系统,用户输入:
帮我查询北京今天的天气,并告诉我是否适合出门。
Agent 可能需要完成以下步骤:
-
接收用户问题。
-
判断是否需要调用天气工具。
-
调用天气 API。
-
获取天气数据。
-
根据天气情况生成回答。
-
将结果返回前端。
如果只是一个简单聊天机器人,后端返回最终答案就可以。
但对于一个真正的 Agent 系统,用户可能希望实时看到:
-
Agent 是否开始执行。
-
Agent 正在调用什么工具。
-
工具是否执行成功。
-
Agent 正在生成什么内容。
-
整个任务是否已经完成。
这时候,传统的简单请求响应就不够灵活了。
AG-UI 的作用就是定义一套标准化事件,让前端能够理解 Agent 的执行过程。
关键区别:AG-UI 不只是传递最终答案,还标准化了 Agent 执行过程中发生的事件。
例如,后端可以依次发送:
RUN_STARTED
↓
TOOL_CALL_START
↓
TOOL_CALL_ARGS
↓
TOOL_CALL_END
↓
TOOL_CALL_RESULT
↓
TEXT_MESSAGE_START
↓
TEXT_MESSAGE_CONTENT
↓
TEXT_MESSAGE_END
↓
RUN_FINISHED
前端接收到不同事件后,可以执行不同操作。
例如收到 TOOL_CALL_START,显示"正在调用天气工具";收到 TEXT_MESSAGE_CONTENT,逐步展示回答内容。
注意,这只是其中一种合法的执行顺序。实际事件流会因 Agent 的执行过程而变化。
1.1.2 AG-UI 的产生背景
要理解 AG-UI 为什么出现,需要先理解 AI 应用的发展。
第一阶段:普通 LLM 聊天应用
早期应用主要是:
用户输入
↓
调用 LLM
↓
返回文本
前端只需要展示生成的文本。
即使采用流式输出,也主要是处理文本 Token 或文本片段。
第二阶段:Tool Calling Agent
Agent 开始具备调用外部工具的能力。
例如:
用户问题
↓
LLM 判断
↓
调用搜索工具
↓
获取搜索结果
↓
再次调用 LLM
↓
返回答案
这时前端除了展示文本,还需要展示工具执行状态。
第三阶段:复杂 Agent 系统
随着 LangGraph 等框架的使用,Agent 系统开始包含:
-
多步骤任务规划。
-
多 Agent 协作。
-
工具调用与执行结果。
-
状态更新。
-
人工审批。
-
长时间运行的任务。
不同 Agent 框架产生的执行事件和状态结构可能不同。
例如,LangGraph 有自己的执行和流式事件接口,其他框架也可能采用不同的数据结构。
如果每接入一个框架,都要重新开发前端通信逻辑,维护成本就会增加。
AG-UI 的设计目标之一,就是在 Agent 后端与用户界面之间建立统一的交互协议。
1.1.3 传统 Agent 前后端交互存在的问题
主要有四类。
问题一:事件格式不统一
假设 LangGraph 后端发送:
{
"event": "on_tool_start",
"name": "search"
}
另一个自定义 Agent 后端发送:
{
"type": "tool_begin",
"tool": "search"
}
两者表达的含义相近,但字段不同。
前端就需要分别编写解析逻辑。
AG-UI 通过标准事件类型和字段约定减少这种差异。
问题二:工具执行过程难以展示
普通文本流只告诉前端生成了什么文字。
但是它不能自然表达:
-
哪个工具开始执行。
-
工具调用参数是什么。
-
工具返回了什么。
-
工具调用属于哪条消息。
AG-UI 提供专门的工具调用事件来表达这些信息。
问题三:状态同步复杂
假设你的 Agent 维护一个任务状态:
{
"task": "分析项目",
"progress": 30,
"status": "running"
}
一段时间后进度变成 70%。
如果没有统一的状态同步机制,开发者就需要自己设计更新消息、字段合并和异常恢复逻辑。
AG-UI 提供 STATE_SNAPSHOT 和 STATE_DELTA 事件,用于传递完整状态或增量更新。
问题四:人工交互困难
例如 Agent 准备删除数据库中的某条记录。
这时候应该让用户确认:
是否允许删除这条记录?
Agent 需要暂停等待用户操作,然后继续执行。
这种交互不仅涉及前端按钮,还涉及后端执行状态、审批结果和任务恢复。
AG-UI 可以承担相关交互数据的传递,但具体的暂停、持久化和恢复逻辑仍然需要 Agent 框架或应用后端实现。
1.1.4 AG-UI 解决了哪些核心问题?
| 问题 | AG-UI 提供的能力 |
|---|---|
| 事件格式不统一 | 标准化事件模型 |
| 文本无法实时展示 | 文本消息增量事件 |
| 工具调用过程不可见 | 工具调用生命周期事件 |
| 前后端状态不同步 | Snapshot / Delta |
| Agent 执行状态不明确 | Run 生命周期事件 |
| 框架接入成本高 | 通用协议与适配器机制 |
这里有一个非常重要的认识:
AG-UI 是通信协议,不是 Agent 编排框架。
它不负责决定 Agent 应该调用哪个工具,也不负责规划任务。
这些仍然由 LangGraph、自定义 Agent Runtime 等后端逻辑负责。
1.1.5 AG-UI 的典型应用场景
例如:
场景一:AI 智能客服
实时显示回答、工具查询结果和工单处理状态。
场景二:RAG 智能问答
展示检索进度、文档来源、回答内容和任务状态。具体引用展示可以通过应用定义的数据或事件实现。
场景三:代码 Agent
展示代码分析进度、工具执行、测试结果和人工确认操作。
场景四:多 Agent 系统
展示不同子 Agent 的任务执行过程、状态变化和最终结果。
1.2 Agent 通信协议对比
MCP 连接工具,A2A 连接 Agent,AG-UI 连接用户界面
1.2.1 MCP:Agent 与工具之间的通信
MCP 全称是 Model Context Protocol。
它主要用于标准化 AI 应用与外部工具、资源和提示模板的集成方式。
假设你开发了一个 RAG Agent。
它需要访问:
本地文件。
数据库。
GitHub 仓库。
外部搜索服务。
没有 MCP 时,你可能为每一种工具编写不同的接入逻辑。
使用 MCP 后,应用可以通过统一的协议接口发现和调用 MCP Server 提供的能力。

1.2.2 A2A:Agent 与 Agent 之间的通信
A2A 全称是 Agent2Agent Protocol。
它主要解决不同 Agent 系统之间的互操作和协作问题。
例如你有三个独立 Agent:
研究 Agent。
编程 Agent。
测试 Agent。
研究 Agent 可以把任务委派给编程 Agent,编程 Agent 再与测试 Agent 协作。
A2A 定义了 Agent 能力发现、消息交换、任务状态等通信机制。

1.2.3 AG-UI:Agent 与用户界面之间的通信
AG-UI 主要解决 Agent 执行过程如何传递给前端的问题。
例如:
用户发起任务后,后端 Agent 执行:
开始任务 调用工具 获得结果 更新状态 生成回答 结束任务AG-UI 将这些变化表示为标准事件。
前端 Client 负责接收事件、维护交互状态并更新界面。
1.2.4 AG-UI、MCP、A2A 的区别与联系
| 对比维度 | MCP | A2A | AG-UI |
|---|---|---|---|
| 主要目标 | 工具与上下文集成 | Agent 之间协作 | Agent 与 UI 交互 |
| 典型通信对象 | Host/Client ↔ Server | Agent ↔ Agent | Client ↔ Agent 后端 |
| 核心能力 | Tools、Resources、Prompts | 能力发现、任务、消息 | 事件流、工具展示、状态同步 |
| 主要关注点 | 外部能力接入 | 跨 Agent 协作 | 用户交互体验 |
| 是否负责模型推理 | 否 | 否 | 否 |
| 是否负责 Agent 编排 | 否 | 否 | 否 |
这里要注意:协议定义通信方式,不等于替你实现业务逻辑。
例如 MCP 负责工具接口,但工具具体如何执行仍由工具服务实现。
1.2.5 三种协议如何协同工作?
假设用户说:
帮我分析一个 GitHub 项目,找出代码问题,并生成优化建议。
可以设计这样的系统:
完整执行过程:
用户通过 React 前端提交分析任务。
前端通过 AG-UI 接入主 Agent。
主 Agent 通过 MCP 获取 GitHub 项目数据。
主 Agent 通过 A2A 委派任务给独立的代码分析 Agent。
代码分析 Agent 返回结果。
主 Agent 汇总分析报告。
AG-UI 将执行过程和结果传递给前端。
这就是三种协议的协同方式:它们并不互相替代,而是解决不同层面的通信问题。
1.3 AG-UI 整体架构
1.3.1 AG-UI 的整体架构设计
AG-UI 可以从三个主要部分理解:
Client:发起请求、消费事件、维护前端状态。
Agent Runtime:执行 Agent 逻辑,包括模型调用、工具调用和状态管理。
协议适配层:把后端执行结果转换成 AG-UI 标准事件,并通过通信通道传输。

需要特别注意:这里的"协议适配层"是常见的工程实现方式,不代表 AG-UI 强制规定必须部署一个独立服务。
1.3.2 Agent、Runtime、Client 的职责
这三个概念容易混淆。
Agent:负责决策
Agent 的核心职责是根据任务和当前状态决定下一步做什么。
例如:
pythondef agent_decide(user_input): if "天气" in user_input: return "call_weather_tool" return "generate_answer"这是一个极简的决策示意,实际 Agent 通常依赖模型推理和工具选择机制。
Runtime:负责执行
Runtime 是 Agent 运行时环境。
它可能负责:
执行 Agent 的决策。
调用 LLM。
执行工具。
管理任务状态。
处理异常。
管理暂停与恢复。
例如 LangGraph 可以承担部分 Agent Runtime 的职责。
Client:负责交互
Client 主要负责:
向后端发送用户输入。
接收 AG-UI 事件。
将文本增量拼接为完整消息。
根据工具事件更新 UI。
根据状态事件更新前端状态。
显示执行成功或失败。
三者的关系可以简单理解为:
Agent 决定做什么,Runtime 负责执行,Client 负责展示和交互。
但在真实框架中,Agent 与 Runtime 的边界不一定完全独立。
1.3.3 AG-UI 的通信模型
AG-UI 的通信可以理解为:
客户端提交运行请求,服务端返回执行过程中产生的事件流。
一个典型实现会使用 HTTP 请求和 SSE 流式响应。
例如客户端发送:
{ "threadId": "thread-001", "runId": "run-001", "messages": [ { "id": "msg-001", "role": "user", "content": "帮我查询天气" } ], "state": {}, "tools": [], "context": [], "forwardedProps": {} }这是简化后的 AG-UI 运行请求示例,实际字段和校验要求以使用的 SDK 版本为准。
后端可以返回类似这样的 SSE 事件:
data: {"type":"RUN_STARTED","threadId":"thread-001","runId":"run-001"} data: {"type":"TEXT_MESSAGE_START","messageId":"msg-002","role":"assistant"} data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg-002","delta":"今天北京天气晴朗。"} data: {"type":"TEXT_MESSAGE_END","messageId":"msg-002"} data: {"type":"RUN_FINISHED","threadId":"thread-001","runId":"run-001"}这里有两个层次:
第一层:传输层
例如 SSE,负责将数据从服务端持续传输给客户端。
第二层:协议事件层
例如
TEXT_MESSAGE_CONTENT,负责告诉客户端这段数据表示什么。所以:
SSE 解决"数据怎么传",AG-UI 解决"传输的数据是什么、客户端如何理解"。
AG-UI 并不等于 SSE,它的事件模型也可以通过兼容的其他传输方式承载。
1.3.4 AG-UI 的完整请求与响应流程
假设用户在前端输入:
帮我查询北京天气。
下面是一条包含工具调用的典型执行链路。
具体拆开来看:
步骤一:Client 发起请求
用户输入问题,前端通过 AG-UI Client 向后端发送包含消息和运行上下文的请求。
步骤二:Runtime 开始执行
后端启动 Agent,并发出
RUN_STARTED事件。步骤三:Agent 决定调用工具
Agent 根据用户问题选择天气工具。
后端通过
TOOL_CALL_START、TOOL_CALL_ARGS、TOOL_CALL_END等事件,将工具调用信息发送给前端。步骤四:工具执行并返回结果
天气工具返回天气数据。
后端通过
TOOL_CALL_RESULT将结果表示为协议事件。步骤五:Agent 生成回答
Agent 根据天气数据生成自然语言回答。
后端依次发送:
TEXT_MESSAGE_START TEXT_MESSAGE_CONTENT TEXT_MESSAGE_CONTENT ... TEXT_MESSAGE_END前端不断拼接文本增量,实现类似 ChatGPT 的打字效果。
步骤六:任务完成
后端发送
RUN_FINISHED。前端据此将当前运行状态更新为已完成。
注意:如果执行失败,则可能发送
RUN_ERROR,而不是正常结束。
第一,AG-UI 不负责让 Agent 变聪明,而是让 Agent 的执行过程能够被前端标准化地感知和交互。第二,AG-UI、MCP、A2A 并不是竞争关系,它们分别关注 UI 交互、外部能力集成和 Agent 间协作。
第三,AG-UI 的核心是事件模型。你后面学习的工具调用、状态同步、Human-in-the-Loop、多 Agent 可视化,本质上都离不开事件的产生、传输与消费
第二章:AG-UI 核心原理
2.1 AG-UI 事件驱动机制
在传统的请求响应模式中,前端向后端发送请求,后端完成处理后返回结果。而在复杂的 Agent 系统中,一次用户请求可能涉及模型推理、工具调用、状态更新以及多轮交互。
如果前端只能获取最终结果,就很难实时了解 Agent 的执行过程。
AG-UI 采用事件驱动的交互模型,将 Agent 执行过程中产生的变化表示为标准化事件,前端通过消费这些事件实时更新用户界面。
例如,用户输入:
帮我查询北京今天的天气。
Agent 可能依次产生以下事件:
用户发送请求
↓
RUN_STARTED
↓
TOOL_CALL_START
↓
TOOL_CALL_ARGS
↓
TOOL_CALL_END
↓
TOOL_CALL_RESULT
↓
TEXT_MESSAGE_START
↓
TEXT_MESSAGE_CONTENT
↓
TEXT_MESSAGE_END
↓
RUN_FINISHED
在这个过程中,前端不需要等到整个任务执行结束,而是可以根据不同事件实时展示任务状态、工具调用过程和最终回答。
2.1.1 什么是事件驱动架构?
事件驱动架构(Event-Driven Architecture,EDA)是一种以事件为核心的系统设计方式。
所谓事件,就是系统中已经发生的某个动作或状态变化。
例如:
用户提交了一个任务。
Agent 开始执行。
Agent 发起工具调用。
工具返回结果。
Agent 生成了一段文本。
Agent 执行结束。
在事件驱动架构中,通常可以将系统分为三个部分:

2.1.2 AG-UI 事件模型与分类
AG-UI 定义了多种标准事件,可以按照用途分为以下几类。
| 事件类别 | 典型事件 | 作用 |
|---|---|---|
| 生命周期事件 | RUN_STARTED、RUN_FINISHED、RUN_ERROR |
描述一次 Agent 运行的开始、结束和失败 |
| 文本消息事件 | TEXT_MESSAGE_START、TEXT_MESSAGE_CONTENT、TEXT_MESSAGE_END |
实现文本流式输出 |
| 工具调用事件 | TOOL_CALL_START、TOOL_CALL_ARGS、TOOL_CALL_END、TOOL_CALL_RESULT |
描述工具调用过程 |
| 状态同步事件 | STATE_SNAPSHOT、STATE_DELTA |
同步 Agent 状态 |
| 自定义事件 | CUSTOM |
传递应用自定义数据 |
这些事件共同构成了 Agent 与前端之间的交互基础。
需要注意,AG-UI 事件不是 Agent 的全部内部执行日志。后端需要选择并转换希望通过协议暴露的执行信息。
2.1.3 RUN_STARTED、RUN_FINISHED、RUN_ERROR
这三个事件用于描述一次 Agent 运行的生命周期。
-
RUN_STARTED:Agent 开始执行
{
"type": "RUN_STARTED",
"threadId": "thread_001",
"runId": "run_001"
}
其中:
-
threadId:标识所属的对话线程。 -
runId:标识本次 Agent 执行。
前端收到该事件后,可以将运行状态设置为 running。
-
RUN_FINISHED:Agent 执行结束
{
"type": "RUN_FINISHED",
"threadId": "thread_001",
"runId": "run_001"
}
它表示本次运行正常结束。
注意,正常结束并不意味着业务目标一定成功。例如,Agent 也可能正常完成一次"未找到结果"的回答。
-
RUN_ERROR:Agent 执行失败
{
"type": "RUN_ERROR",
"message": "天气服务请求失败",
"code": "WEATHER_API_ERROR"
}
当 Agent 运行过程中出现无法正常完成的错误时,可以通过该事件向前端传递错误信息。
前端收到后,可以显示错误提示,并根据应用设计提供重试操作。
2.1.4 Agent 执行生命周期
2.1.5 事件顺序与事件处理机制
AG-UI 的事件并不是毫无顺序地发送。
例如,前端要正确拼接一条消息,通常需要按照以下顺序处理:
TEXT_MESSAGE_START ↓ TEXT_MESSAGE_CONTENT ↓ TEXT_MESSAGE_CONTENT ↓ TEXT_MESSAGE_END前端可以根据
messageId将同一条消息的文本片段关联起来。下面是一个简化的 JavaScript 事件处理示例:
javascriptconst messages = new Map(); function handleEvent(event) { switch (event.type) { case "TEXT_MESSAGE_START": messages.set(event.messageId, { content: "", status: "streaming" }); break; case "TEXT_MESSAGE_CONTENT": { const message = messages.get(event.messageId); if (message) { message.content += event.delta; } break; } case "TEXT_MESSAGE_END": { const message = messages.get(event.messageId); if (message) { message.status = "completed"; } break; } } }这段代码的核心思想是:
前端不需要等待完整消息,而是随着事件到达不断更新消息内容。
真实项目还需要考虑事件顺序校验、重复处理、断线恢复和异常处理。
2.2 AG-UI 流式通信机制
在上一节中,我们介绍了 AG-UI 的事件驱动机制,知道了 Agent 执行过程中会产生 RUN_STARTED、TEXT_MESSAGE_CONTENT、TOOL_CALL_START 等事件。
但是还有一个关键问题:
这些事件是如何从后端实时传输到前端的?为什么用户不需要等待 Agent 执行结束,就能看到生成的文字和工具调用过程?
这就是本节要讲的 AG-UI 流式通信机制。
先记住一个核心概念:
AG-UI 定义事件的结构和语义,SSE 等传输技术负责把事件从后端传递到前端。
2.2.1 SSE 与 WebSocket 的区别
1. 什么是 SSE?
SSE(Server-Sent Events)是一种基于 HTTP 的服务端向客户端持续推送事件的技术。
传统 HTTP 请求通常是:
客户端发送请求
↓
服务端处理请求
↓
服务端返回完整结果
↓
响应结束
而 SSE 可以让服务端保持响应连接,并在处理过程中持续发送数据:
客户端发送 HTTP 请求
↓
服务端建立流式响应
↓
发送事件 1
↓
发送事件 2
↓
发送事件 3
↓
发送结束事件
↓
响应结束
例如,用户让 Agent 分析一个 GitHub 项目。
Agent 可能需要几十秒才能完成分析。
如果采用普通的一次性响应,用户只能等待最终结果。
如果采用 SSE,后端就可以不断发送:
正在分析项目结构...
正在读取核心代码...
正在检查工具调用逻辑...
正在生成分析报告...
前端收到一段,就可以展示一段。
2. 什么是 WebSocket?
WebSocket 是一种支持客户端和服务端在同一连接上进行双向通信的协议。
连接建立后:
-
客户端可以主动发送消息。
-
服务端也可以主动发送消息。
-
双方可以持续交换数据。
例如,开发一个实时协作平台,用户需要不断发送操作,服务端也需要实时推送其他用户的操作。
这种场景比较适合 WebSocket。
3. SSE 与 WebSocket 对比
| 对比维度 | SSE | WebSocket |
|---|---|---|
| 通信方式 | 服务端向客户端单向推送 | 双向通信 |
| 底层协议 | HTTP | HTTP 握手后升级为 WebSocket |
| 数据格式 | 文本事件流 | 文本帧或二进制帧 |
| 浏览器原生接口 | EventSource | WebSocket |
| 自动重连 | EventSource 内置重连机制 | 通常由应用自行实现 |
| 客户端发送数据 | 使用独立 HTTP 请求 | 可通过同一连接发送 |
| 典型场景 | LLM 流式回答、事件通知 | 实时聊天、协作、双向控制 |
需要注意:SSE 的单向通信不代表整个应用只能单向交互。客户端仍然可以通过 HTTP POST 等请求向服务端发送消息。
4. AG-UI 为什么经常使用 SSE?
因为很多 Agent 应用具有以下特点:
-
用户提交一次任务。
-
Agent 在后端持续执行。
-
后端不断产生事件。
-
前端实时接收并展示。
-
Agent 执行结束,流式响应关闭。
这与 SSE 的通信模型非常契合。
例如:
不过,AG-UI 不等于 SSE,也不强制只能使用 SSE。AG-UI 主要定义事件及交互约定,具体传输方式可以由实现决定。
2.2.2 AG-UI 如何实现流式输出?
我们以一个简单场景来理解。
用户输入:
帮我介绍一下 LangGraph。
后端 Agent 需要调用大模型生成回答。
假设模型逐步生成:
LangGraph
是一个
用于构建
有状态 Agent 工作流
的框架。
AG-UI 不需要等待模型生成完整答案,而是可以把这些增量内容转换为标准事件。
-
整体执行流程

-
AG-UI 如何表示文本流?
一个典型的文本消息事件序列:
{
"type": "TEXT_MESSAGE_START",
"messageId": "msg_001",
"role": "assistant"
}
接下来不断发送内容:
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": "LangGraph"
}
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": " 是一个用于构建"
}
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": " 有状态 Agent 工作流的框架。"
}
最后发送:
{
"type": "TEXT_MESSAGE_END",
"messageId": "msg_001"
}
前端通过相同的 messageId 将文本片段拼接起来。
2.2.3 事件序列化与传输
上一节我们已经知道 AG-UI 可以持续发送事件。
但还有一个问题:
后端产生的事件对象,如何变成能够通过网络传输的数据?
这就涉及序列化。
1. 什么是事件序列化?
序列化(Serialization)就是把程序中的对象转换为可以传输或存储的数据格式。
例如,Python 中有一个事件对象:
javascript
event = {
"type": "RUN_STARTED",
"threadId": "thread_001",
"runId": "run_001"
}
将其转换成 JSON 字符串:
import jsonevent_json = json.dumps(event)
得到:
{"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001"}
2. SSE 的数据格式
SSE 使用 text/event-stream 类型的 HTTP 响应。
一个常见的 SSE 事件格式:
data: {"type":"RUN_STARTED","threadId":"thread_001","runId":"run_001"}
data: {"type":"TEXT_MESSAGE_START","messageId":"msg_001","role":"assistant"}
data: {"type":"TEXT_MESSAGE_CONTENT","messageId":"msg_001","delta":"你好"}
data: {"type":"TEXT_MESSAGE_END","messageId":"msg_001"}
data: {"type":"RUN_FINISHED","threadId":"thread_001","runId":"run_001"}
注意:
-
每个
data:行承载事件数据。 -
空行表示一个 SSE 事件块结束。
-
AG-UI 事件对象通常使用 JSON 表示。
-
SSE 是外层传输格式,JSON 是事件数据的序列化形式。
3. FastAPI 实现简单的 SSE 流式接口
下面用 FastAPI 模拟 AG-UI 的流式事件输出。
这是一份用于理解原理的简化示例,不是完整的 AG-UI SDK 服务端实现。
python
import asyncio
import json
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def generate_events():
events = [
{
"type": "RUN_STARTED",
"threadId": "thread_001",
"runId": "run_001"
},
{
"type": "TEXT_MESSAGE_START",
"messageId": "msg_001",
"role": "assistant"
},
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": "你好,"
},
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": "我是 AI Agent。"
},
{
"type": "TEXT_MESSAGE_END",
"messageId": "msg_001"
},
{
"type": "RUN_FINISHED",
"threadId": "thread_001",
"runId": "run_001"
}
]
for event in events:
data = json.dumps(event, ensure_ascii=False)
yield f"data: {data}\n\n"
await asyncio.sleep(0.5)
@app.get("/events")
async def events():
return StreamingResponse(
generate_events(),
media_type="text/event-stream"
)
运行:
uvicorn main:app --reload
然后使用:
curl -N http://127.0.0.1:8000/events
就可以看到事件逐条输出。
这里的 /events 是为了演示 SSE 而使用的 GET 接口。真实 AG-UI 运行通常需要通过 POST 提交消息和运行上下文,并由服务端返回流式响应。
2.2.4 前端如何消费事件流?
后端已经通过 SSE 发送事件,那么前端如何接收?
核心流程是:
接收 HTTP 响应
↓
读取响应数据流
↓
解析 SSE 事件块
↓
解析 JSON
↓
判断事件类型
↓
更新前端状态
↓
React 重新渲染
1. EventSource 与 fetch 的区别
浏览器提供
EventSourceAPI,可以方便地订阅 GET 类型的 SSE 接口。但是在 Agent 应用中,通常需要通过 POST 请求传递:
用户消息。
对话线程 ID。
当前状态。
工具信息。
运行上下文。
原生
EventSource不支持直接发送带 JSON 请求体的 POST。因此,使用
fetch加ReadableStream是常见实现方式。
2. 前端消费流式事件示例
下面演示如何通过 fetch 接收 SSE 事件。
javascript
async function consumeStream() {
const response = await fetch("/api/agent", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
message: "介绍一下 LangGraph"
})
});
if (!response.ok || !response.body) {
throw new Error("请求失败");
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += decoder.decode(value, {
stream: true
});
// 兼容 CRLF 换行
buffer = buffer.replace(/\r\n/g, "\n");
const blocks = buffer.split("\n\n");
buffer = blocks.pop();
for (const block of blocks) {
const data = block
.split("\n")
.filter(line => line.startsWith("data:"))
.map(line => line.slice(5).trimStart())
.join("\n");
if (!data) continue;
const event = JSON.parse(data);
handleEvent(event);
}
}
}
这段代码的主要逻辑是:
通过
fetch发起请求。通过
response.body.getReader()获取响应流。使用
TextDecoder将字节转换成文本。按 SSE 事件边界提取数据。
将 JSON 解析为事件对象。
交给
handleEvent()处理。这里需要特别注意:一次
reader.read()不一定对应一个完整的 AG-UI 事件。一个事件可能被拆成多个网络数据块,也可能多个事件一起到达。因此必须使用缓冲区进行拼接。
上面的代码仅处理常见的
data:事件格式。生产环境还需要完整处理 SSE 字段、跨数据块的换行、流结束残留数据等情况,实际项目建议使用成熟的 AG-UI Client SDK 或 SSE 解析器。
3. 根据事件类型更新前端
javascript
const messages = new Map();
function handleEvent(event) {
switch (event.type) {
case "RUN_STARTED":
console.log("Agent 开始执行");
break;
case "TEXT_MESSAGE_START":
messages.set(event.messageId, "");
break;
case "TEXT_MESSAGE_CONTENT": {
const oldText =
messages.get(event.messageId) ?? "";
messages.set(
event.messageId,
oldText + event.delta
);
console.log(messages.get(event.messageId));
break;
}
case "TEXT_MESSAGE_END":
console.log("消息生成结束");
break;
case "RUN_FINISHED":
console.log("Agent 执行完成");
break;
case "RUN_ERROR":
console.error(event.message);
break;
}
}
这里使用 Map 演示消息聚合。真实 React 项目通常还需要通过 useState、Reducer 或状态管理库触发 UI 更新。
2.2.5 流式传输中的异常处理
实际项目中,流式通信并不总是顺利完成。
可能出现:
-
网络连接中断。
-
Agent 执行超时。
-
工具调用失败。
-
服务端异常退出。
-
事件数据格式错误。
-
用户主动取消任务。
因此,异常处理是流式 Agent 系统的重要组成部分。
1. 区分业务错误与传输错误
这两种错误非常容易混淆。
业务错误:Agent 执行失败
例如调用天气 API 时发生异常。
服务端仍然能够正常通信,于是发送:
{
"type": "RUN_ERROR",
"message": "天气服务调用失败",
"code": "TOOL_ERROR"
}
前端收到后显示失败原因。
传输错误:连接中断
例如网络突然断开。
这时前端可能根本收不到 RUN_ERROR,只能发现响应流异常结束。
因此:
收到
RUN_ERROR表示服务端明确报告运行失败;连接断开则不能直接判断 Agent 是否已经执行失败。
2. 用户主动取消任务
如果用户在 Agent 执行过程中点击"停止生成",前端可以使用 AbortController 取消 HTTP 请求。
javascript
const controller = new AbortController();
const response = await fetch("/api/agent", {
method: "POST",
body: JSON.stringify({
message: "分析项目"
}),
headers: {
"Content-Type": "application/json"
},
signal: controller.signal
});
// 用户点击停止按钮
controller.abort();
但需要注意:
取消前端请求不一定等于取消后端 Agent 的实际执行。
如果希望真正停止任务,后端还需要将断开连接或取消信号传递给 Runtime,并正确清理工具执行和任务资源。
3. 超时与重试
如果 Agent 长时间没有产生事件,前端可以提示用户:
Agent 正在执行,请稍候。
如果超过预期时间,则可以提供取消或重试操作。
不过,不能简单地在断线后无条件重新发送请求。
因为某些 Agent 任务可能包含有副作用的工具调用,例如:
-
创建文件。
-
修改数据库。
-
发送邮件。
-
提交代码。
重复请求可能导致这些操作执行两次。
因此,生产环境需要考虑:
-
runId标识同一次运行。 -
服务端运行状态查询。
-
幂等控制。
-
检查点与恢复机制。
-
安全的重试策略。
AG-UI 的事件协议本身不保证断线后的自动恢复或业务操作的幂等性,这些能力需要由具体系统实现。
2.3 AG-UI 消息机制
在上一节中,我们学习了 AG-UI 如何通过 SSE 实现流式通信。但 SSE 只解决了一个问题:如何把后端产生的数据持续传输到前端?
接下来需要解决另一个问题:
前端收到这些数据后,如何知道一条消息什么时候开始、内容是什么、什么时候结束,以及这些消息属于哪一次对话?
这就是 AG-UI 消息机制要解决的问题。
AG-UI 使用标准化的消息事件描述消息的生命周期,主要包括:
-
TEXT_MESSAGE_START:标识一条文本消息开始。 -
TEXT_MESSAGE_CONTENT:传输文本内容或增量。 -
TEXT_MESSAGE_END:标识这条文本消息结束。
先通过一张图理解整体过程。
2.3.1 TEXT_MESSAGE_START
TEXT_MESSAGE_START 表示一条新的文本消息开始生成。
当 Agent 准备输出一条消息时,后端可以先发送该事件,通知前端创建消息上下文。
例如用户输入:
帮我介绍一下 LangGraph。
Agent 开始生成回答,后端发送:
{
"type": "TEXT_MESSAGE_START",
"messageId": "msg_001",
"role": "assistant"
}
| 字段 | 含义 | 作用 |
|---|---|---|
type |
事件类型 | 告诉客户端这是一条消息开始事件 |
messageId |
消息唯一标识 | 用于关联同一条消息的后续事件 |
role |
消息角色 | 例如 assistant |
这里最重要的是 messageId。
因为一次 Agent 运行中可能产生多条消息,前端必须知道后续文本应该追加到哪条消息中。
2.3.2 TEXT_MESSAGE_CONTENT
TEXT_MESSAGE_CONTENT 用于传输消息内容,通常以增量形式发送。
例如 LLM 最终需要生成:
LangGraph 是一个用于构建有状态 Agent 工作流的框架。
后端不需要等待整句话生成完毕,可以将内容拆分成多个增量事件。
第一个事件:
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": "LangGraph"
}
第二个事件:
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": " 是一个用于构建"
}
第三个事件:
{
"type": "TEXT_MESSAGE_CONTENT",
"messageId": "msg_001",
"delta": "有状态 Agent 工作流的框架。"
}
前端接收到这些事件后,将 delta 逐步追加到对应消息中。
delta 表示本次新增的内容。
例如:
| 事件 | delta | 前端累计内容 |
|---|---|---|
| 第一次 | LangGraph |
LangGraph |
| 第二次 | 是一个用于构建 |
LangGraph 是一个用于构建 |
| 第三次 | 有状态 Agent 工作流的框架。 |
LangGraph 是一个用于构建有状态 Agent 工作流的框架。 |
这种机制有三个优点:
-
降低用户等待完整回答的时间。
-
避免每次发送整条不断增长的消息。
-
支持前端实时渲染文本。
但需要区分:文本增量不一定等于一个 Token。 后端可以根据模型输出和缓冲策略决定每次发送多少文本。
消息增量必须追加到对应的 messageId,不能简单地追加到最后一条消息。
否则当系统出现多条消息或复杂事件交错时,可能导致内容混乱。
2.3.3 TEXT_MESSAGE_END
TEXT_MESSAGE_END 表示一条文本消息的流式传输结束。
例如:
{
"type": "TEXT_MESSAGE_END",
"messageId": "msg_001"
}
前端收到后,就知道 msg_001 这条消息已经完整。
| 对比 | TEXT_MESSAGE_END | RUN_FINISHED |
|---|---|---|
| 作用对象 | 一条文本消息 | 一次 Agent 运行 |
| 表示含义 | 消息内容结束 | 本次运行正常结束 |
| 是否意味着任务完成 | 不一定 | 表示本次运行结束 |
| 后续能否继续产生事件 | 可以 | 通常该次运行不再继续产生业务事件 |
TEXT_MESSAGE_END 结束的是一条消息,RUN_FINISHED 结束的是一次 Agent 运行。
2.3.4 消息增量拼接机制
假设后端依次发送:
TEXT_MESSAGE_START(messageId="msg_001")
↓
TEXT_MESSAGE_CONTENT(delta="你好")
↓
TEXT_MESSAGE_CONTENT(delta=",我是")
↓
TEXT_MESSAGE_CONTENT(delta=" AI Agent")
↓
TEXT_MESSAGE_END(messageId="msg_001")
前端需要维护:
{
"id": "msg_001",
"role": "assistant",
"content": "你好,我是 AI Agent",
"status": "completed"
}
本质上,前端是在根据事件逐步构建完整的消息对象。
问题一:重复拼接
如果同一个增量事件被重复处理,前端可能出现:
你好你好,我是 AI Agent需要在恢复和重放机制中考虑事件去重。
问题二:消息关联错误
如果只维护一个全局字符串,多条消息可能被错误拼接到一起。
应使用
messageId区分消息。问题三:异常中断
如果模型执行失败,前端可能收不到
TEXT_MESSAGE_END。因此不能仅依靠该事件判断整个运行是否正常,还需要处理
RUN_ERROR和连接异常。
2.3.5 多轮对话与消息历史管理
| 标识 | 作用 | 典型范围 |
|---|---|---|
threadId |
标识一条对话线程 | 多轮对话共享 |
runId |
标识一次 Agent 运行 | 每次运行独立 |
messageId |
标识一条消息 | 每条消息独立 |

AG-UI 是否负责保存聊天历史?
AG-UI 主要定义消息和运行上下文如何在客户端与 Agent 后端之间交换,不等于一个聊天历史数据库。
实际系统中的职责可以这样划分:
| 组件 | 主要职责 |
|---|---|
| AG-UI Client | 维护前端消息展示和交互状态 |
| Agent Runtime | 处理当前运行及上下文 |
| LangGraph Checkpointer | 根据配置持久化 Agent 状态 |
| PostgreSQL / Redis 等 | 根据业务需求存储对话、运行记录或缓存 |
其中 LangGraph 的 Checkpointer 可以用于保存和恢复图状态,但不应直接认为它自动满足所有聊天历史查询、审计和长期存储需求。
多轮对话完整执行流程
第一轮:
用户发送问题 A
↓
创建 thread_001
↓
创建 run_001
↓
Agent 执行
↓
返回消息 msg_001
↓
保存对话状态
第二轮:
用户发送问题 B
↓
复用 thread_001
↓
创建 run_002
↓
加载或传入历史上下文
↓
Agent 理解问题 B
↓
生成新的回答
↓
更新对话状态
这里有一个非常重要的工程细节:
复用相同的 threadId 并不自动保证 Agent 拥有历史记忆。
如果使用 LangGraph,需要正确配置 Checkpointer、线程标识和状态加载方式;如果没有持久化机制,也可以由客户端或服务端显式传入必要的历史消息。
同时,要避免既加载历史状态又重复追加相同消息,导致上下文重复。
多轮对话的工程问题
实际项目中还需要解决:
上下文过长:通过消息裁剪、摘要或检索减少 Token 消耗。
历史持久化:支持服务重启后恢复对话。
并发运行:避免同一线程中的并发请求覆盖状态。
数据隔离:确保不同用户不能访问彼此的线程。
消息去重:避免请求重试造成重复写入。
2.4 AG-UI 工具调用机制
在前面的章节中,我们已经学习了 AG-UI 的事件驱动机制、流式通信机制和消息机制。
但是,一个真正的 Agent 系统不仅需要生成文本,还需要调用外部工具来完成任务,例如:
-
调用搜索工具查询互联网信息。
-
调用数据库工具查询业务数据。
-
调用 Python 工具执行代码。
-
调用 MCP 工具获取外部资源。
-
调用 GitHub API 分析代码仓库。
在传统聊天应用中,前端通常只展示最终答案。但在 Agent 系统中,用户可能还希望看到 Agent 调用了什么工具、传入了哪些参数,以及工具返回了什么结果。
AG-UI 通过标准化的工具调用事件,让前端能够感知并展示 Agent 的工具调用过程。
先记住一句话:
Agent 负责决定调用什么工具,Runtime 负责执行工具,AG-UI 负责将工具调用过程以标准事件的形式传递给前端。
2.4.1 TOOL_CALL_START
TOOL_CALL_START 表示一次工具调用的信息流开始。
例如,用户输入:
帮我查询北京今天的天气。
Agent 判断需要调用 get_weather 工具。
此时,后端可以发送:
{
"type": "TOOL_CALL_START",
"toolCallId": "call_001",
"toolCallName": "get_weather",
"parentMessageId": "msg_001"
}
| 字段 | 含义 | 作用 |
|---|---|---|
type |
事件类型 | 表示工具调用开始 |
toolCallId |
工具调用唯一标识 | 关联同一次工具调用的所有事件 |
toolCallName |
工具名称 | 告诉前端正在调用哪个工具 |
parentMessageId |
可选的父消息 ID | 关联发起工具调用的消息 |
这里最重要的是 toolCallId。
因为一次 Agent 运行中可能存在多个工具调用。
例如:
Agent
├── call_001 → get_weather
├── call_002 → search_news
└── call_003 → query_database
前端需要根据不同的 toolCallId 分别管理工具状态。
2.4.2 TOOL_CALL_ARGS
TOOL_CALL_ARGS 用于传输工具调用参数。
与文本消息类似,工具调用参数也可以采用增量传输方式。
例如,Agent 需要调用:
get_weather(city="北京", date="today")
对应的参数为:
{
"city": "北京",
"date": "today"
}
如果模型采用流式输出,后端可能先发送:
{
"type": "TOOL_CALL_ARGS",
"toolCallId": "call_001",
"delta": "{\"city\":\"北京\","
}
随后发送:
{
"type": "TOOL_CALL_ARGS",
"toolCallId": "call_001",
"delta": "\"date\":\"today\"}"
}
前端根据 toolCallId 拼接增量参数。
最终得到:
{
"city": "北京",
"date": "today"
}
2.4.3 TOOL_CALL_END
OOL_CALL_END 表示一次工具调用信息的流式传输结束。
例如:
{
"type": "TOOL_CALL_END",
"toolCallId": "call_001"
}
当后端发送该事件时,表示这次工具调用的名称和参数等信息已经传输完成。
2.4.4 TOOL_CALL_RESULT
TOOL_CALL_RESULT 用于表示工具执行后返回的结果。
例如天气工具执行完成,返回:
{
"city": "北京",
"weather": "晴",
"temperature": 25
}
后端可以发送:
{
"type": "TOOL_CALL_RESULT",
"messageId": "tool_msg_001",
"toolCallId": "call_001",
"content": "{\"city\":\"北京\",\"weather\":\"晴\",\"temperature\":25}",
"role": "tool"
}
注意,content 在这里是字符串,因此结构化 JSON 结果需要先序列化。
2.4.5 工具调用完整执行流程

用户提交天气查询
↓
RUN_STARTED
↓
Agent 判断需要调用天气工具
↓
TOOL_CALL_START
↓
TOOL_CALL_ARGS
↓
TOOL_CALL_END
↓
Runtime 执行 get_weather
↓
TOOL_CALL_RESULT
↓
Agent 根据工具结果生成回答
↓
TEXT_MESSAGE_START
↓
TEXT_MESSAGE_CONTENT
↓
TEXT_MESSAGE_END
↓
RUN_FINISHED