前端如何设计 AI Chat 的状态
AI Chat 前端最容易被低估的部分,是状态设计。
很多 demo 只需要一个 isLoading:用户发送消息,按钮禁用,模型返回后展示答案。但真实产品里,用户会重复点击、切换页面、停止生成、刷新浏览器、网络断开、重试失败消息、同时打开多个会话。此时 isLoading 很快不够用。
前端状态的目标不是把 UI 做复杂,而是让用户和系统都知道:这一条消息现在处于什么状态,用户还能做什么,失败后如何恢复。
场景
在一个 AI 数据分析助手里,用户输入:
帮我分析 4 月份付费转化下降的原因。
前端会立刻插入一条用户消息,然后创建一条 assistant 占位消息。后端开始流式返回:
- 正在理解问题。
- 正在查询数据。
- 正在生成分析。
- 输出结论和建议。
用户在这个过程中可能做几件事:
- 看到生成太慢,点击停止。
- 网络中断后,看到半截答案。
- 发现答案失败,点击重试。
- 连续点两次发送。
- 切到另一个会话,再回来。
- 刷新页面,希望历史状态仍然合理。
如果前端只用一个全局 loading,就会出现明显问题:
- A 会话还在生成,B 会话的输入框也被禁用。
- 用户停止后,按钮变回可用,但后端仍在返回 chunk。
- 失败消息没有关联原始用户问题,重试不知道重试哪一轮。
- partial 内容和新一轮 retry 的内容混在同一个气泡里。
- 页面刷新后,前端不知道半截答案是 stopped、failed 还是 still running。
问题类型
前端状态问题主要有六类。
这些问题里,模型并不应该负责判断状态。模型可以解释失败原因或生成补救建议,但 UI 状态必须由前端和后端协议共同决定。
收益和成本判断
好的前端状态设计会带来直接收益:
- 用户知道系统是在生成、失败、停止还是等待重试。
- 失败不会让整段对话不可用。
- retry 可以复用原始问题,而不是让用户重新输入。
- stop 不只是视觉按钮,而是一次明确的取消操作。
- 多会话和多消息不会互相污染。
- 线上问题更容易通过 messageId 和 responseId 复盘。
成本也真实存在:
- 需要设计消息状态机,而不是简单数组。
- 需要前后端约定事件类型。
- 需要处理并发、取消、刷新恢复和错误分支。
- UI 要展示更多状态,但不能变得吵闹。
如果只是内部草稿生成工具,简单策略通常足够:发送中、生成中、失败可重试、可停止。用户能接受重新生成,失败代价低。
如果是客服、数据分析、代码助手、知识库问答或工作流助手,前端就需要工程化状态。因为用户会依赖历史记录,后端也需要知道每次 retry、stop 和 failed 的归属。
方案阶梯
前端状态可以从简单到复杂逐级升级。
第一层是全局 loading。适合最早期 demo 或一次性草稿生成,但很快会遇到多会话和失败恢复问题。
第二层是消息级状态。每条 assistant 消息知道自己是 streaming、completed 还是 failed。
第三层是 retry 和 stop。用户可以恢复失败,也可以停止过长生成。
第四层是稳定 ID。所有 chunk、retry、stop 都能归属到具体 response。
第五层是流事件协议。前端不再猜测文本流什么时候结束,而是根据 start、delta、done、error 更新状态。
第六层是刷新恢复。页面重新加载后,partial、stopped、failed 不会被误显示成 completed。
第七层是多版本和 trace。适合需要对比回答、复盘失败和定位线上问题的产品。
什么时候简单策略足够
以下情况可以先用简单前端状态:
- 功能只是草稿生成、摘要或润色。
- 同一时间只允许一条消息生成。
- 没有工具调用和业务副作用。
- 用户可以接受失败后重新生成。
- 不需要跨页面、跨设备恢复 running 状态。
- 历史记录不要求精确保留 failed、stopped 和 partial。
这时可以使用 streaming、completed、failed 三个核心状态,再加一个停止按钮和一个手动重试按钮。重点是先让失败可见,而不是一开始就做复杂版本树。
什么时候需要工程方案
以下情况应该升级:
- 存在多会话、多标签页或后台生成。
- 用户会频繁 retry、stop、刷新或继续追问。
- 对话中有工具调用、文件处理、代码执行或数据查询。
- partial 内容有保留价值,但不能当作完整答案。
- 需要定位 chunk 乱序、迟到事件或重复 done。
- 产品需要统计前端失败率、停止率和重试成功率。
这时前端要和后端约定 messageId、requestId、responseId、事件类型、错误 code 和恢复规则。UI 状态不再只是视觉效果,而是对话协议的一部分。
推荐状态模型
可以把一条 assistant 消息设计成下面的状态流转。
推荐至少保留这些字段:
这里的关键不是字段名,而是状态归属:
- 用户消息和 assistant 消息都应该有稳定 ID。
- 一次生成应该有 requestId 或 responseId。
- retry 应该能指向原始用户消息和上一条失败 assistant 消息。
- partial 内容必须明确标记,不能当成完整答案。
- 错误要有机器可读 code,也要有用户可读提示。
Retry 设计
重试不是“再调用一次接口”。
前端需要先判断重试对象是什么:
- 重试同一条失败 assistant 消息。
- 基于同一条用户消息生成一个新版本。
- 从某个失败步骤继续。
- 重新发送最后一条用户消息。
对于普通文本生成,最简单的策略是:失败的 assistant 气泡保留,点击重试后在原位置进入 retrying,再切回 streaming,成功后替换内容。
对于需要保留多个版本的场景,可以把 retry 生成新 assistant 消息,并标记 parentMessageId,让用户能看到不同版本。
Retry 的简单策略
以下情况简单策略足够:
- 没有工具调用。
- 失败发生在生成阶段。
- 重试不会改变业务状态。
- 用户只关心最终答案。
- 历史不需要保留多个版本。
做法是:原失败气泡显示错误和“重试”按钮;点击后清空或保留 partial,进入 retrying;新 chunk 到来后替换为 streaming;完成后变为 completed。
Retry 的工程方案
以下情况需要工程方案:
- 后端已经调用工具。
- 输出会写入数据库或生成业务记录。
- retry 需要从失败步骤继续。
- 需要保留多个答案版本。
- 需要避免重复扣费或重复执行。
这时前端必须把 retry 请求带上 conversationId、userMessageId、failedResponseId 和新的 requestId。不要只把当前 messages 数组重新发给后端。
Stop 设计
停止按钮也有两层含义。
第一层是 UI 停止接收流。用户不想继续看了,前端关闭连接,消息变成 stopped 或 failed。
第二层是后端取消任务。系统应该尽力通知后端取消模型请求、工具调用或队列任务。
对用户来说,stop 的合理表现是:
- 生成中显示停止按钮。
- 点击后立即进入 stopping 或 stopped 状态。
- 已生成内容保留,但标记为未完成。
- 可以继续追问,也可以重试。
- 如果后端最终返回取消确认,状态保持一致。
不要把 stopped 当作 completed。半截答案可以有参考价值,但不能假装完整。
Failed 设计
失败状态要避免两种极端:什么都不说,或把技术错误原样暴露给用户。
前端应该展示简短、可行动的信息:
- 网络断开:检查连接后重试。
- 生成超时:可以重试或稍后再试。
- 工具失败:当前无法查询数据,可以转人工或重试。
- 权限失败:当前账号没有访问权限。
- 内容过长:建议缩小问题范围。
同时,系统内部要保留详细错误:
- errorCode。
- provider status。
- traceId。
- failed stage。
- retryable。
- partial content。
用户文案和工程错误要分层,不要混在一个字符串里。
Streaming 设计
流式状态的核心是“每个 chunk 都有归属”。
前端处理流时至少要校验:
- chunk 属于当前 conversationId。
- chunk 属于当前 responseId。
- 当前消息仍处于 streaming 或 retrying。
- 收到 done 事件后才能变成 completed。
- 收到 error 事件后要停止追加,并进入 failed。
- 用户 stop 后,迟到 chunk 要丢弃或记录,不能继续显示。
推荐后端返回明确事件,而不是只返回文本:
方案选型表
反模式
第一种反模式:所有状态都挂在输入框上。
输入框是否可用,不等于某条消息是否在生成。多会话、多响应、后台恢复都会让全局 loading 失真。
第二种反模式:失败后删除 assistant 气泡。
用户会失去上下文,不知道失败发生在哪里。失败气泡应该保留,并提供重试、复制 partial、转人工或继续追问。
第三种反模式:retry 时直接追加一条新的用户消息。
这会让历史变成用户重复说了同一句话,也让后端难以判断是新请求还是恢复旧请求。
第四种反模式:stop 后继续接受 chunk。
迟到数据很常见。如果没有 responseId 和状态校验,停止后的内容可能继续写进 UI。
第五种反模式:把 partial 当最终答案缓存。
partial 可以保存,但必须标记。否则用户刷新后会看到一个没有失败提示的半截答案。
第六种反模式:错误文案只写“出错了”。
用户不知道能不能重试,也不知道问题是否来自权限、网络、超时或系统繁忙。错误提示至少要给出下一步动作。
工程清单
- 消息粒度:状态是全局、会话级,还是消息级。
- 状态枚举:是否覆盖 local_pending、queued、streaming、completed、failed、stopped、retrying、fallback。
- ID 设计:是否有 conversationId、messageId、requestId、responseId。
- 流式处理:start、delta、done、error、fallback 是否有明确事件。
- Retry:重试是否关联原始用户消息和失败响应。
- Stop:前端关闭连接后,是否通知后端取消。
- Partial:半截内容是否标记,刷新后是否能正确显示。
- 错误分层:用户文案和工程 errorCode 是否分开。
- 多会话:不同会话的状态是否互不影响。
- 并发保护:迟到 chunk、重复 done、重复 error 是否被处理。
- 可访问性:按钮禁用、加载状态和错误提示是否对键盘和读屏可用。
- 可观测性:前端是否记录用户 stop、retry、刷新和失败阶段。
小结
AI Chat 前端状态的核心,是把“生成一段话”拆成可管理的消息生命周期。
低风险场景可以从简单的 streaming、failed、retry、stop 起步。只要涉及流式输出、多会话、工具调用、刷新恢复或失败追踪,就应该引入消息级状态、稳定 ID、流事件协议和 retry/stop 的明确语义。
前端不需要解决所有后端可靠性问题,但它必须把状态表达清楚。否则用户看到的不是智能助手,而是一段不知道是否完成、是否失败、是否可信的动态文本。