跳到主要内容

非阻塞 AI:让应用在智能体工作时保持响应的异步 UX 模式

· 阅读需 12 分钟
Tian Pan
Software Engineer

大多数团队都以相同的方式发现了同步 UI 的问题:用户点击"生成报告",然后浏览器标签页陷入沉默长达四十秒。没有加载动画,没有进度提示,只有一个冻结的按钮。一半用户刷新页面并重复提交。另一半用户认为产品出了故障,直接关掉标签页。

根本问题不在于智能体的延迟——而在于 LLM 驱动的智能体所处的时间尺度,打破了同步请求-响应 UX 所有内置的假设。单次 GPT-4o 调用平均需要 8–15 秒。一个搜索网页、读取三份文档、写初稿再格式化输出的多步智能体,可能需要两到四分钟。你无法通过优化智能体来让这感觉变快,必须重新设计后端与 UI 之间的契约。

本文涵盖让长时间运行的智能体工作对用户来说可以接受的完整技术栈——从前端的 token 流式传输到后端的任务队列,以及中间的取消语义和竞态条件修复。

延迟问题是结构性的,不会消失

在深入讲模式之前,有必要理解为什么延迟问题是结构性的。朴素的智能体链在每一层都会叠加延迟:

  • LLM 推理:P50 下每次调用 3–15 秒,负载下 P95 常超 30 秒
  • 工具调用:每次网页搜索、数据库查询或 API 调用增加 1–5 秒
  • 多步推理:包含工具调用的五步 LLM 链,常规耗时达两分钟
  • 结构化输出开销:约束模型输出 JSON schema 会取消流式传输并增加一次额外往返

传统的解决方案——更快的基础设施——在边际上有帮助,但不会改变数量级。3 秒响应时间会触发用户离开;聊天 UX 的可防御基准是 P95 低于 2 秒。再快的模型速度提升也不能把复杂的智能体链压到这个阈值以下。解决方案是改变用户在等待时的体验,而不是消除等待。

流式优先:让每个 Token 可见

对智能体 UX 改善最大的单一变化是在 token 级别流式输出。与其等模型生成完毕再刷新响应,不如在 token 产生的同时就流式传输给客户端。即使总完成时间超过 60 秒,用户通常在 2–3 秒内就能看到第一个 token——而一个可见增长的响应,与空白屏幕的感觉截然不同。

对大多数场景来说,Server-Sent Events(SSE)是正确的传输方式。它是单向的服务器到客户端通信,在标准 HTTP 上运行无需升级握手,自动处理重连,并与 Fetch API 原生集成。当你需要双向实时通信时(用户正在编辑文档,同时智能体也在写入),WebSocket 更合适;但对于"智能体产生输出,用户阅读"这一常见情况,SSE 更简单且足够用。

更难的问题是结构化输出。如果你要求模型返回 JSON——结果表格、行动项列表、代码变更集——原始流式传输会失效,因为你无法渲染不完整的 JSON。实用的解决方案是将响应以原始文本流式传输,使用增量 JSON 解析器(JavaScript、Python 和 Go 都有相应库),并在字段变得可解析时渲染已完成的字段。Vercel AI SDK 的 createDataStreamResponse 更进一步,允许在 LLM 开始响应之前就将结构化元数据(已检索文档、工具结果)交织到流中。

乐观更新:立即展示预期状态

流式传输处理的是输出生成阶段。乐观 UI 处理的是用户动作与第一个可见输出之间的时间段——对于复杂智能体来说,这段时间仍可能长达数秒。

模式很直接:用户提交请求时,立即更新 UI 以反映预期结果。在对话线程中显示消息,在任务列表中将任务标记为进行中,在表格中添加新行。如果后端操作成功(正常情况),UI 已经是正确的。如果失败,则回滚到之前的状态并显示错误。

React 的 useOptimistic hook 在框架层将这一模式形式化。你提供当前状态和更新函数;React 立即应用你的乐观差异,如果异步动作抛出错误则自动撤销。像 Linear 这样的产品广泛使用这种模式——在几毫秒内本地创建 Issue,而数据库写入在后台进行。

关键的设计约束是:乐观更新最适合失败率低且回滚代价小的操作。对于产生不可逆副作用的智能体任务(发送邮件、提交文件、处理付款),你不会想在操作实际完成之前乐观地显示"邮件已发送"。对中间的"进行中"阶段使用乐观状态,但要等到有确认后才显示成功状态。

传达语义进度的骨架状态

加载动画说的是"有事情在发生"。骨架状态说的是"这里正在加载大致这个形状的内容"。语义进度指示器说的是"智能体已完成搜索,现在正在写作"。每个层次都给用户提供更多信息,也能换来更多耐心。

关于骨架屏幕的研究结论一致:向用户展示内容将出现在哪里以及大概是什么形状,能显著减少感知等待时间。对于智能体工作流来说,语义进度胜过通用加载,因为它回答了用户真正关心的问题:"它开始了吗?它卡住了吗?"

Perplexity 和 Cursor 等产品使用的模式是在智能体工作时发出离散的进度事件,并在 UI 中渲染每个事件:

  • "正在搜索关于 X 的最新论文……"
  • "正在阅读 3 份文档……"
  • "正在起草回复……"
  • "正在格式化输出……"

这些不只是装饰。它们告诉用户系统正在朝着目标推进,而不是在一个尚未暴露的错误上空转。Windsurf 的"Flows"概念更进一步——每个智能体步骤都是一个显式的 UI 构件,可以独立暂停、检查和恢复。

实现这一模式需要智能体在执行过程中(而不仅仅是完成时)发出结构化事件。LangGraph 通过流式模式原生支持这一点;自定义智能体需要在结果通道旁边维护一个事件总线或进度通道。

取消而不留下残局

最容易被忽视的异步 UX 问题是取消。用户出于合理原因取消长时间运行的任务——改变了主意、输入了错误的参数,或者只是不想再等了。问题在于智能体已经完成的工作该如何处理。

这分为两类:

幂等副作用(只读数据库查询、网页搜索、分析):可以自由取消。工作被丢弃;世界没有任何改变。

非幂等副作用(已发送的邮件、已发起的 API 调用、已写入的文件、已处理的付款):取消是模糊的。智能体可能在收到取消信号之前就已经执行了该操作。你需要在每个改变状态的操作上设置幂等键,这样如果智能体在断开连接后重试,也不会产生重复效果。你还需要取消流程区分"我们在行动之前停止了"和"我们已经行动了;这是发生了什么"。

在前端,AbortController 是正确的原语。每次新的调用创建一个新的控制器;提交新请求时在新的 fetch 开始之前中止前一个。这可以防止经典竞态条件:用户在第一个请求完成之前输入第二个查询,而第一个响应最后到达,覆盖了正确的 UI 状态。

对于有大量在途工作的长时间智能体任务,"暂停"通常比"取消"更符合用户预期。一些 AI 代码编辑器现在明确地在智能体序列上暴露暂停/恢复——这让用户可以检查中间输出、重定向智能体并继续,而不是丢弃一切重新开始。

后端架构:任务队列优于 HTTP 长轮询

对于超过 10–15 秒的智能体任务,后端架构需要从同步 HTTP 转向带状态回调的异步任务队列。

模式如下:

  1. 客户端提交任务 → 服务器将任务入队,立即返回任务 ID(HTTP 202 Accepted)
  2. 客户端轮询任务状态端点,或订阅 WebSocket/SSE 频道获取更新
  3. Worker 处理任务,发出进度事件
  4. 完成时通知客户端并获取结果

BullMQ 在 Node.js 上是这一模式的实用选择——基于 Redis,原生 TypeScript,支持带指数退避的重试、任务优先级和 DAG 依赖(父任务只有在多个子任务全部完成后才能完成)。对于需要跨重启持久性和明确人工审批步骤的工作流,Temporal 是更结构化的选项。

在服务间通信场景中,Webhook 实现同样的目的。智能体完成任务时,向注册端点发送 Webhook;接收方服务可以触发下游处理,无需轮询。2025–2026 年的模式是将 Webhook 与 CloudEvents 作为负载 schema,加上临时签名密钥用于验证。

一个要注意的失败模式:没有确认的即发即忘。如果后端将任务入队后客户端断开连接,你需要任务继续执行,并且当客户端重连时结果仍可获取。在 Redis 中存储带 TTL 的任务状态,并暴露以任务 ID 为键的结果端点,即可实现这一点。即使浏览器刷新,客户端也可以随时重新轮询。

竞态条件比你想象的更常见

当用户与由异步智能体调用驱动的快速变化 UI 交互时,竞态条件是悄悄降低数据完整性却不产生明显错误的静默失败模式。

经典例子:一个在每次击键时触发智能体查询的搜索输入框。用户输入"机器学习部署",触发五个独立的查询。第五个查询最先返回正确结果。但随后第一到第四个查询乱序返回,每个都用过时数据覆盖 UI 状态。最终显示的结果是"机器学"的正确结果,而不是完整查询的结果。

修复需要在前端代码中有意识地处理顺序:

  • 为每个请求分配单调递增的序列号
  • 响应时只在序列号与最新请求匹配时应用更新
  • 使用 AbortController 在新请求开始时取消在途请求

更深层的变体发生在智能体执行写操作且 UI 状态与后端状态不同步时。最安全的模型是将后端视为真相来源,并在完成时重新获取,而不是增量修补本地状态。乐观更新适用于预测状态;确定状态应来自服务器响应。

领先产品的收敛方向

纵观 2025–2026 年在智能体 UX 上做得好的产品,收敛趋势令人印象深刻:

  • 流式传输被视为默认,而非可选
  • 侧边栏或分屏布局将进行中的工作与对话历史分开,让用户可以看到智能体输出积累,同时不失去上下文
  • 步骤级进度事件在 UI 中呈现,而不仅仅是一个通用的加载动画
  • 暂停/恢复正在成为多阶段任务的标准,取代强制取消再重启
  • 幂等性被视为基础设施要求,而非事后补充

仍然让用户沮丧的产品有一个共同缺陷:它们将智能体视为产生结果的黑盒,而不是可以被观察和中断的过程。向异步、可观察的智能体执行的转变不仅仅是 UX 美化——它让用户相信系统在正常工作而不是出了故障。

从小处着手

如果你正在将智能体能力集成到现有产品中,渐进式路径如下:

  1. 为任何阻塞 UI 的 LLM 调用添加 token 流式传输。仅此一项就能消除"冻结屏幕"问题。
  2. 为超过两步的智能体链添加进度事件。在每一步之前发出事件,而不仅仅是在结束时。
  3. 通过 AbortController 在前端添加取消,在后端添加优雅中断处理。明确测试非幂等路径。
  4. 一旦任务超过 30 秒,将长时间运行的任务迁移到任务队列。这是基础设施变更,但用户可见的改善是显著的。
  5. 从那里优化——乐观更新、骨架状态和暂停/恢复是随着智能体任务复杂度增加而越来越重要的用户体验改善。

一个用户放弃的同步智能体集成和一个用户信任的异步集成之间的差距,通常不在于模型本身。而在于应用是否将用户视为智能体工作的参与者,还是仅仅作为等待结果的被动接受者。

Let's stay in touch and Follow me for more thoughts and updates