深入理解 AG-UI 协议:Agent 与前端实时交互的底层原理

第一章: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 可能需要完成以下步骤:

  1. 接收用户问题。

  2. 判断是否需要调用天气工具。

  3. 调用天气 API。

  4. 获取天气数据。

  5. 根据天气情况生成回答。

  6. 将结果返回前端。

如果只是一个简单聊天机器人,后端返回最终答案就可以。

但对于一个真正的 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 项目,找出代码问题,并生成优化建议。

可以设计这样的系统:

完整执行过程:

  1. 用户通过 React 前端提交分析任务。

  2. 前端通过 AG-UI 接入主 Agent。

  3. 主 Agent 通过 MCP 获取 GitHub 项目数据。

  4. 主 Agent 通过 A2A 委派任务给独立的代码分析 Agent。

  5. 代码分析 Agent 返回结果。

  6. 主 Agent 汇总分析报告。

  7. 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 的核心职责是根据任务和当前状态决定下一步做什么。

例如:

python 复制代码
def 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 运行的生命周期。

  1. RUN_STARTED:Agent 开始执行

    {
    "type": "RUN_STARTED",
    "threadId": "thread_001",
    "runId": "run_001"
    }

其中:

  • threadId:标识所属的对话线程。

  • runId:标识本次 Agent 执行。

前端收到该事件后,可以将运行状态设置为 running。

  1. RUN_FINISHED:Agent 执行结束

    {
    "type": "RUN_FINISHED",
    "threadId": "thread_001",
    "runId": "run_001"
    }

它表示本次运行正常结束。

注意,正常结束并不意味着业务目标一定成功。例如,Agent 也可能正常完成一次"未找到结果"的回答。

  1. 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 事件处理示例:

javascript 复制代码
const 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 应用具有以下特点:

  1. 用户提交一次任务。

  2. Agent 在后端持续执行。

  3. 后端不断产生事件。

  4. 前端实时接收并展示。

  5. Agent 执行结束,流式响应关闭。

这与 SSE 的通信模型非常契合。

例如:

不过,AG-UI 不等于 SSE,也不强制只能使用 SSE。AG-UI 主要定义事件及交互约定,具体传输方式可以由实现决定。

2.2.2 AG-UI 如何实现流式输出?

我们以一个简单场景来理解。

用户输入:

帮我介绍一下 LangGraph。

后端 Agent 需要调用大模型生成回答。

假设模型逐步生成:

复制代码
LangGraph
是一个
用于构建
有状态 Agent 工作流
的框架。

AG-UI 不需要等待模型生成完整答案,而是可以把这些增量内容转换为标准事件。

  1. 整体执行流程

  2. 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 的区别

浏览器提供 EventSource API,可以方便地订阅 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);
    }
  }
}

这段代码的主要逻辑是:

  1. 通过 fetch 发起请求。

  2. 通过 response.body.getReader() 获取响应流。

  3. 使用 TextDecoder 将字节转换成文本。

  4. 按 SSE 事件边界提取数据。

  5. 将 JSON 解析为事件对象。

  6. 交给 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 工作流的框架。

这种机制有三个优点:

  1. 降低用户等待完整回答的时间。

  2. 避免每次发送整条不断增长的消息。

  3. 支持前端实时渲染文本。

但需要区分:文本增量不一定等于一个 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

相关推荐
爬楼的猪1 小时前
一个轻量化的Codex Web UI
人工智能·ui·chatgpt·ai编程
copilot中国版21 小时前
Outlook Copilot|客户沟通
ui·copilot·outlook
Cx330❀1 天前
Qt 界面美化指南:从零掌握 QSS 核心机制与工程实践
开发语言·qt·ui·性能优化·图形渲染
兰亭妙微UI设计公司1 天前
兰亭妙微ui咨询公司分享:设计原则及组件详解
ui
兰亭妙微UI设计公司1 天前
兰亭妙微界面设计公司分享:DeepSeek AI智能助手应用UI/UX设计深度解析
人工智能·ui·ux
沫璃染墨2 天前
《Qt从零入门系列(十三):QSS样式表详解——从选择器与层叠规则到控件美化实战》
开发语言·css·c++·qt·ui·qt6.3·qss
WarPigs2 天前
UI路径配置SO>字典json工作流
ui·unity
stevenzqzq2 天前
mvi理解2-ui状态和业务状态
ui
刘科领3 天前
使用ollama & openweb-ui 本地搭建自己的AI
人工智能·python·ui·ai