传统前端状态管理已经不轻松:表单、请求、列表分页、权限、弹窗、缓存、路由,任何一类处理不好,页面都会变乱。
AI 应用又多了一层复杂度。一次用户输入不再只是触发接口请求,而可能启动意图识别、知识检索、模型生成、流式输出、工具调用、用户确认、失败重试和任务恢复。
传统交互更像:
用户点击按钮,前端发请求,接口返回结果,页面渲染反馈。
AI 交互更像:
用户提出目标,系统启动一个不确定任务;任务可能分阶段推进、中断、分叉、等待确认,最后形成一个可接受的结果。
所以 AI 应用状态管理的核心变化,不是多一个 loading,而是从“请求状态”转向“任务状态”。
难点是任务生命周期,而不是组件局部数据。
状态越清楚,用户越有控制感;状态越含糊,应用越像黑盒。
01|传统前端状态管理解决什么
传统前端状态大致有四类。
视图状态
比如当前 tab、弹窗开关、表格选中项、输入框内容、滚动位置。这类状态靠近 UI,生命周期短,通常局部 useState 就够。
服务端数据状态
比如用户信息、订单详情、搜索结果、权限数据。前端要处理请求中、成功、失败、缓存、重新请求、分页筛选和乐观更新。React Query、SWR、Apollo Client 擅长解决这一类问题。
业务流程状态
比如多步骤表单、审批、支付、上传、登录注册。它们有阶段、前置条件、失败回退、用户确认和服务端同步,比普通请求更复杂。
全局状态
比如登录态、主题、语言、当前组织、权限集合、WebSocket 连接、全局通知。难点在作用域:一旦边界不清,就会到处读、到处改、难以追踪。
这些状态并不简单,但大多数传统交互仍有一个确定前提:用户动作、请求响应和页面变化之间的关系相对清楚。AI 应用打破的正是这种确定性。
02|AI 应用新增了哪些状态
AI 应用不是在传统前端上加一个聊天框。它会引入几类新状态。
生成中
AI 生成常常是流式的,结果一边产生一边展示。前端需要区分请求是否建立、首 token 是否返回、是否仍在输出、内容是否完整、是否可引用、是否已持久化。
传统 loading 表示“还没有结果”;流式生成里,用户已经看到部分结果,但任务尚未完成。这是典型的中间态。
可取消
用户可能因为问题问错、生成方向不对、结果太长、成本太高或上下文切换而停止任务。取消时可能已有部分内容,前端要决定是否保留、如何标记、能否继续生成、后端是否真的停止,以及历史和计费如何记录。
“完成、失败、用户中断”不能混在一起。
工具调用
AI 应用不只生成文本,还可能查询数据库、检索知识库、读取文件、生成图表、创建任务、修改代码或发送邮件。
一旦有工具调用,状态就要表达:模型为何调用工具、调用了什么、参数是什么、执行是否成功、结果如何回到模型、是否需要用户确认。否则用户只能看到一个转圈和最终答案,无法判断过程是否可信。
待确认
当 AI 准备删除数据、发送邮件、修改代码、提交订单、调用付费接口或发布内容时,结果还没有真正生效,状态应该是“待确认”。
待确认要回答:建议做什么、影响范围是什么、用户能否编辑或拒绝、确认后是否可撤销、下一步进入哪个状态。动作越强,边界越要清楚。
不确定结果
AI 输出可能是草稿、建议、推测、摘要、解释、候选版本或待校验答案,不能一律当成最终事实。
可以按场景区分:
draft:草稿。 candidate:候选答案。 verified:已校验。 rejected:用户拒绝。 applied:已应用。
这不是文字游戏,而是产品责任边界。
可恢复
AI 任务失败可能发生在输入校验、检索、模型生成、工具调用、流式连接、内容审核、用户取消或前端断开等阶段。不同阶段对应不同恢复方式:原地重试、换模型、重新检索、补充信息、继续生成或重新开始。
因此错误状态至少要包含失败阶段、原因、是否可重试、从哪里重试、是否保留已生成内容,以及用户可执行动作。
03|为什么简单 loading 不够
很多 AI 应用一开始会写成这样:
type State = { loading: boolean; error?: string; data?: Message[];};Demo 阶段能跑,真实产品很快不够用。
它无法表达阶段
用户发送一句话后,系统可能要创建会话、保存用户消息、检索知识库、调用模型、流式返回、调用工具、继续生成、保存助手消息。它们都可以叫 loading,但交互完全不同。
检索慢,要告诉用户正在查资料;生成慢,要展示流式内容;工具慢,要说明正在执行什么;保存失败,应该只重试持久化,而不是重新生成全部内容。
它无法表达中断
流式生成中断后,loading 变成 false,但这不等于成功,也不等于失败。它可能是用户停止、网络断开、服务端超时、内容被拦截或页面切换。
如果只记录 loading: false,下一步操作就无从判断。
它无法表达部分结果
一篇文章生成到一半、一段 SQL 只给了结论、一次代码生成只完成文件 A,这些都不是“空”,也不是“完成”,而是“有部分结果但任务未完成”。
缺少这个状态,用户容易误用半成品。
它无法表达控制权
AI 应用里,用户需要知道能否取消、编辑、继续追问、重新生成、确认执行、撤销或保存当前结果。状态不清楚,按钮就会乱,系统也会显得不可靠。
04|从请求状态转向任务状态
AI 应用更适合用“任务”建模。一个简化状态可以是:
type AiTaskStatus = | "idle" | "submitted" | "planning" | "retrieving" | "generating" | "tool_calling" | "waiting_user_confirm" | "applying" | "completed" | "cancelled" | "failed";这些状态比 loading 复杂,但表达的信息更接近真实交互。
submitted | ||
planning | ||
retrieving | ||
generating | ||
tool_calling | ||
waiting_user_confirm | ||
applying | ||
completed | ||
cancelled | ||
failed |
失败状态建议带阶段:
type AiTaskError = { stage: AiTaskStatus; message: string; retryable: boolean; recoverFrom?: "restart" | "resume" | "retry_stage" | "edit_input";};这样前端才能判断是重新检索、重新保存、继续生成,还是让用户修改输入。
05|消息状态也要拆开
很多 AI 应用以消息列表为核心,但消息不是普通数组。一条助手消息也有生命周期:
type MessageStatus = | "pending" | "streaming" | "interrupted" | "completed" | "failed" | "replaced";几个关键状态要分清:
pending:助手消息占位已出现,但还没有内容。 streaming:内容正在写入,需要处理 Markdown 半成品、自动滚动、停止按钮和复制限制。 interrupted:生成被中断,不等于失败,也不等于完成,应允许继续、重试、追问或删除。 completed:内容完整生成,但还要看是否保存、引用、校验或应用。 failed:失败要区分首 token 前、生成中途和保存失败。 replaced:重新生成后旧版本是否保留、能否切换,需要明确版本关系。
任务和消息最好分开建模:一个任务可能生成多条消息,一条消息可能有多个版本,一个任务也可能调用多个工具。全部塞进 messages 数组,后续会很难维护。
06|工具调用状态要透明,但不能吵
Agent 类产品经常展示工具调用过程,展示过少会让用户不信任,过多又会制造噪音。前端状态应支持分层展示。
普通用户只需要关键阶段:
正在查找资料。 正在分析文件。 正在生成方案。 等待你确认发送。
专业用户可以展开细节:
使用了哪些检索结果。 调用了哪个工具。 参数摘要是什么。 返回了什么错误。 哪一步耗时最长。
高风险动作必须明确确认。涉及写操作、外部影响、金钱、权限和发布时,界面要说明将执行什么、影响范围、是否可撤销,以及确认后的结果。这里的状态不仅是技术状态,也是责任边界。
07|失败恢复要从一开始设计
AI 任务复杂后,失败不是一个点,而是可能发生在任何阶段。
不要把所有错误都变成“出错了,请重试”。阶段越清楚,恢复越精确,用户越不容易被迫从头来过。
08|一张 AI 应用状态分层表
可以把 AI 应用状态分成五层。
只有消息层,没有任务层,用户不知道系统正在做什么;只有任务层,没有恢复层,失败后只能重来;没有工具层,Agent 行为会变成黑盒;没有输入层记录,用户也难以理解答案基于什么生成。
09|一个更合理的前端状态模型
可以用一个简化模型表达 AI 任务:
type AiTask = { id: string; conversationId: string; status: | "submitted" | "retrieving" | "generating" | "tool_calling" | "waiting_user_confirm" | "applying" | "completed" | "cancelled" | "failed"; input: { text: string; attachments: Attachment[]; contextIds: string[]; }; outputMessageId?: string; currentToolCallId?: string; error?: { stage: string; message: string; retryable: boolean; recoverAction?: "retry" | "resume" | "restart" | "edit_input"; }; createdAt: number; updatedAt: number;};消息和工具调用可以独立建模:
type AiMessage = { id: string; taskId?: string; role: "user" | "assistant" | "tool" | "system"; content: string; status: "pending" | "streaming" | "interrupted" | "completed" | "failed"; version?: number; references?: Reference[];};type ToolCall = { id: string; taskId: string; name: string; status: "pending" | "running" | "waiting_approval" | "succeeded" | "failed"; paramsSummary: string; resultSummary?: string; error?: string;};重点不是照抄类型,而是避免用一个 messages 数组或一个 loading 统治所有阶段。任务、消息、工具、错误恢复要有边界。
10|状态越清楚,交互越稳定
状态建模会直接影响交互。
停止按钮在 retrieving、generating、tool_calling 阶段含义不同:可能是中断查询、停止模型输出,也可能是取消外部任务。如果不分阶段,用户点了也不知道到底停了什么。
重新生成同样不是一个动作。它可能是同上下文重来、重新检索后生成、换模型、基于编辑后的输入生成,或从部分结果继续。没有阶段和上下文记录,就只能粗暴“从头再来”。
页面切换和多任务并发也依赖任务模型。AI 任务可能比页面停留时间更长,用户回来后能否恢复、旧任务是否会污染新结果,都需要任务 ID、服务端状态和可恢复机制支持。
11|状态不是越多越好
状态要清楚,但不是越细越好。判断原则是:
只有当不同状态对应不同交互、恢复方式或责任边界时,才值得拆开。
比如“模型排队中”和“模型连接中”是否要分开,取决于用户是否需要看到、失败恢复是否不同、是否影响权限或计费。若只是内部实现细节,可以不暴露到主状态。
好的状态设计,不是枚举所有过程,而是表达关键差异。
12|给前端工程师的实践建议
做 AI 应用前端时,可以按五步检查。
- 先画任务生命周期
:用户发起任务后,哪些阶段可以取消、确认、重试,哪里会产生部分结果,哪里需要持久化。 - 把部分结果当成一等状态
:半截回答、未完成代码、已生成未应用的建议、已执行一半的工具任务,都要有明确标识。 - 高风险动作必须有确认态
:发消息、写数据库、删除文件、发布内容、修改配置、触发付款,都要区分“建议”和“执行”。 - 错误要带阶段
:至少记录在哪个阶段失败、是否可重试、是否保留已有内容、用户下一步能做什么。 - 状态要能支持调试
:记录输入、阶段、工具调用、检索摘要、模型返回、用户取消或确认动作、最终输出版本。
这些不是额外装饰,而是让复杂任务可追踪、可恢复。
结语:AI 前端不是多一个 loading 就够了
AI 应用会把前端带到更复杂的状态世界:任务提交、检索、生成、工具调用、等待确认、应用结果、完成、中断和恢复。
这些状态决定用户是否知道系统在做什么,能否及时停止,能否理解结果来源,能否从失败中恢复,也决定他们是否敢让 AI 执行真实动作。
前端价值不只是把模型输出渲染出来,而是把不确定任务组织成用户能理解、能控制、能恢复的产品过程。
下一篇继续讨论技术判断:
工程师如何建立自己的技术罗盘。
夜雨聆风