Bakka's Blog

Agent 消息渲染基础

2026/7/28

基本想法

Agent 的回答不是一段文本,而是一场小型任务执行。所以不能只按“消息”来存,也不能只按“UI block”来存,真正要保住的是这次任务的边界。

用户问一句话之后,Agent 可能会思考、查记忆、搜文件、调用工具、等待用户确认、最后再总结。底层看起来是一堆事件:

thinking → tool call → tool result → text → finish

但对用户来说,这不是五件事,而是一件事。中间必须有一层把“过程”整理成“状态”。Agent 的执行过程本身就是事件流,而事件结束也不是返回一个 block 或者 Message,只是文本的碎片。

Reducer 的意义是把碎事件折叠成稳定状态。比如三个 text delta,UI 不应该看到三条消息,而应该看到一个正在增长的正文块。

  • Agent 回复本质上不是“内容”,而是“过程”。

    开始思考
    流式输出 reasoning
    决定调用工具
    工具参数还在流式生成
    工具执行中
    工具结果回来
    根据工具结果继续回答
    中途可能报错 / 等用户确认 / 被取消
    最后输出 Markdown
  • 一条回复不是一个消息,一条回复由多个 block 组成:

    AgentTurn {
      blocks: [
        { kind: 'reasoning' },
        { kind: 'tool' },
        { kind: 'markdown' },
      ]
    }

    也就是:type 不放在整条消息上,而放在可组合的内容块上。

  • Event 是事实来源:text delta、reasoning delta、tool call start、tool result、finish。

  • Reducer 负责归并:把事件折叠成当前 agent turn。

  • Block 是 UI 读模型:markdown、reasoning、tool、error 都是 block。

  • Renderer 是可插拔的:memory tool、web search tool、file tool 各自有自己的 UI。

AG-UI

Agent User Interaction Protocol(AG-UI)是一个代表性的实现,基于事件驱动架构,实现前端应用程序和 AI 代理之间的通信。

事件?

事件是对“已经发生的事实”的抽象。事件驱动进一步做的事情,是把这个“事实”变成系统协作的触发机制。

传统方式:A 发生变化 → A 主动调用 B,让 B 做某事。

事件驱动:订单服务只声明一个事实 OrderCreated(订单已创建),不需要关心 B 的调用,广播给订阅者就可以了,让订阅者自行判断。

只有事实和变化作为一个可传递的对象时,调用链才有可能解耦。这里的消息模型也使用了类似思想:把事实给到客户端,由客户端决定处理方式,不需要在意客户端是谁,仍然可以达到解耦效果。

Publisher
   |
   | emits ToolCallStarted
   v
Event Bus
   ├── UI Subscriber
   ├── Logger Subscriber
   ├── Analytics Subscriber
   └── Trace Subscriber

example

  1. 定义好 Agent 返回的事件,通过 WebSocket / SSE 连接通信,接收 Agent 传来的事件。
  2. 根据 Agent 事件,对最小内容单位 Message/Block 的 content 加上 Delta。
  3. UI 渲染处理完成后的 Message/Block,保证 UI 每次消费的是一个完整的消息单位,而不是碎片。
  4. 根据 Message / Block 的类型进行消息渲染。

类型建模

Reducer:把事件折叠成 UI 状态

模拟一个 Agent 事件流

最小状态管理

Renderer Registry