流式输出与用户体验
Agent 的推理过程往往需要数秒到数十秒,如果用户在等待期间看不到任何反馈,体验会非常糟糕。流式输出不仅让用户"看到 Agent 在思考",更是构建复杂交互(中断控制、进度反馈、增量渲染)的基础。本章讲解 SSE/WebSocket 技术选型、增量渲染策略、中断与取消机制、进度反馈设计,帮你打造流畅的 Agent 交互体验。
SSE 与 WebSocket 技术选型
Server-Sent Events(SSE)
SSE 是基于 HTTP 的单向推送协议,服务端可以持续向客户端发送文本事件流。特点:
- 单向通信:服务端 → 客户端,客户端无法通过同一连接向服务端发消息。
- 自动重连:浏览器原生支持断线自动重连,并携带
Last-Event-ID实现续传。 - 文本协议:每个事件由
data:、event:、id:字段组成,不适合传输二进制数据。 - 轻量简单:实现成本低,一个 HTTP 接口加
text/event-stream响应即可。
适合场景:Agent 文本输出流、状态推送、进度更新——绝大多数 Agent 流式交互用 SSE 就够了。
WebSocket
WebSocket 是双向持久连接协议,客户端和服务端可互发消息。特点:
- 双向通信:客户端可以随时向服务端发消息(如中断指令、追加输入)。
- 低延迟:建立连接后通信不需要 HTTP 请求开销。
- 二进制支持:可传输非文本数据。
- 复杂度高:需要自行处理心跳、重连、消息路由。
适合场景:需要客户端实时发指令的交互(如中途追加参数、实时协作编辑、多 Agent 协同场景)。
选型建议
90% 的 Agent 场景用 SSE。因为 Agent 流式交互的模式是"用户发一次请求 → Agent 持续输出",这是典型的单向推送。只有当你需要用户在 Agent 执行过程中频繁发指令(而非只在最后发一条)时,才需要 WebSocket。
一个常见的混合方案:SSE 推送输出 + HTTP POST 发送控制指令(如中断)。这比全 WebSocket 更简单,且避开了 WebSocket 的连接管理复杂度。
增量渲染策略
Markdown 增量渲染
Agent 输出的文本通常包含 Markdown 格式(标题、列表、代码块)。流式接收时,内容逐 token 到达,渲染面临两个挑战:
- 未完成的 Markdown 结构:收到
"## 分析结"时,不知道这是标题还是普通文本(后面可能还有"果")。 - 代码块边界判断:收到三个反引号时,不知道代码块是开始还是结束。
解决方案:
- 乐观渲染:假设未完成的结构会最终闭合,按当前最可能的格式渲染。如果后续 token 证明假设错误,重新渲染修正。大多数 Markdown 流式渲染库(如
markdown-it-streaming)采用此策略。 - 延迟渲染代码块:检测到代码块开始标记后,暂缓格式化渲染,直到收到完整的代码块内容或超时(如 500ms),再一次性渲染。这避免了代码块闪烁问题。
- 缓冲窗口:保留最近 N 个 token 不渲染,等结构确定后再输出。N 太大用户感觉延迟,太小仍可能误判。建议 N=3-5。
表格与结构化内容的渲染
表格是流式渲染的难点:一个 Markdown 表格的每一行都需要对齐,流式输出时列宽在变化中。建议:
- 对表格整体延迟渲染:检测到表格开始(
| ... |)后,缓冲整个表格直到检测到表格结束(空行或非表格行),然后一次性渲染为完整表格。 - 先展示文本摘要:在表格缓冲期间,先渲染表格前的文本内容并显示"正在生成数据表格..."的提示。
代码执行结果的混合渲染
当 Agent 同时输出文本和代码执行结果时,需要区分两种流:
- 文本流:Markdown 格式,需要增量渲染。
- 结果流:JSON/表格/图像,通常一次性到达。
建议用 事件类型标记 区分:SSE 事件中 event: text 标记文本 token,event: result 标记完整结果,前端按不同策略渲染。
中断与取消控制
中断的类型
Agent 执行中的中断分为三类:
- 用户主动取消:用户点击"停止"按钮,要求终止当前推理。
- 超时取消:执行超过预设时间上限(如 60 秒),系统自动终止。
- 错误中断:工具调用失败或模型输出异常,需要中途停止。
取消的实现
后端取消:收到取消指令后,后端需要:
- 停止向 LLM API 发送后续请求(部分 API 支持
cancel信号)。 - 终止正在执行的工具调用(取消子进程/容器)。
- 保存已生成的部分结果(而非直接丢弃)。
前端取消:
- SSE 场景:前端关闭 EventSource 连接,后端检测连接断开后触发取消逻辑。
- WebSocket 场景:发送
{"type": "cancel", "request_id": "xxx"}消息。 - HTTP POST 场景:调用
POST /cancel/{request_id}接口。
部分结果的保留与续传
用户取消后,已生成的部分回答不应丢弃。保存策略:
- 将已生成内容存入会话历史,标注
[部分生成]。 - 提供"继续生成"按钮,允许用户从断点续传——后端将已生成内容作为上下文,让 LLM 从断点继续。
- SSE 的
Last-Event-ID机制天然支持断点续传:重连时携带最后一个事件的 ID,后端从该 ID 之后继续推送。
进度反馈设计
为什么进度反馈很重要
Agent 执行复杂任务(多步推理+多轮工具调用)时,纯文字流式输出不够——用户需要知道"Agent 在做什么、做到哪一步了、还要多久"。
多层反馈体系
| 层级 | 内容 | 实现方式 |
|---|---|---|
| Token 级 | 逐字输出的文本内容 | SSE event: text |
| 步骤级 | "正在检索知识库..." "正在调用订单查询..." | SSE event: step |
| 进度级 | "步骤 2/5:执行数据库查询" | SSE event: progress |
| 状态级 | 思考中/执行中/已完成/出错 | SSE event: status |
步骤级反馈的实现
Agent 的每一步动作(检索、工具调用、推理)都应向前端推送步骤事件:
// SSE event: step
data: {
"step_name": "检索知识库",
"step_type": "retrieval",
"step_index": 1,
"total_steps": 4,
"detail": "正在查询与「退款政策」相关的 5 篇文档"
}
前端据此渲染步骤列表,显示当前执行到哪一步,已完成步骤显示结果摘要,未执行步骤显示灰色等待态。
进度估算
精确的"还要多久"很难估算,但可以给出粗略提示:
- 基于步骤数:已知 Agent 计划执行 4 步,当前在第 2 步,提示"约完成 50%"。
- 基于历史数据:记录同类任务的平均耗时,估算剩余时间。如"数据库查询通常需要 3-5 秒"。
- 避免虚假精度:说"大约还需 10-20 秒"比说"还需 12.3 秒"更可信——过高的精度会让用户对延误更不耐烦。
错误反馈
当某一步出错时,不要只显示"出错了"。应该:
- 告知用户哪一步出错:"在调用库存查询时遇到问题"。
- 说明错误性质:"库存服务暂时不可用"而非"Error 500"。
- 说明 Agent 的应对策略:"正在使用缓存数据替代实时查询,结果可能略有延迟"。
- 给用户选择:"您可以等待恢复、查看缓存数据,或稍后再试"。
本章小结
| UX 维度 | 关键策略 |
|---|---|
| 传输技术 | SSE(单向)vs WebSocket(双向),按场景选择 |
| 渲染策略 | Markdown 增量渲染 + 打字机效果,平衡实时感与可读性 |
| 进度反馈 | 状态标签 + 进度条 + 步骤计数,降低用户焦虑 |
| 错误处理 | 区分技术错误(静默重试)与业务错误(明确告知) |
UX 核心原则:用户不需要知道 Agent 在"思考",只需要知道"进展如何"。
📖 延伸阅读
- Server-Sent Events (SSE) —— MDN 官方文档
- WebSocket API —— MDN 官方文档
- OpenAI Streaming Guide —— 流式输出最佳实践
- React Markdown —— Markdown 增量渲染组件
