AI 对话中的 Optimistic UI
解释通用乐观更新模式如何落到 AI 对话的临时消息、流式占位、确认、失败与重试状态,并明确与通用 Optimistic Update 的职责边界。
#synthesis
#evergreen
#frontend
#ai
[!info] related notes
- 所属 MOC: AI Agent Application MOC
- 通用概念: 乐观更新
- 相关: Message State Management, Chat UI
AI 对话中的 Optimistic UI
一句话定义
本页不是重新定义 Optimistic Update,而是解释 乐观更新 在 AI 对话状态机中的具体组合:用户消息先入本地状态,随后与服务端确认、assistant 流式占位、失败与重试共同收敛。
覆盖范围与排除项
- 覆盖:临时消息 ID、sending/sent/error 状态、assistant streaming 占位、失败重试与服务端确认后的状态收敛。
- 不覆盖:通用 Optimistic Update 定义与可逆性原则,统一由 乐观更新 维护。
- 不覆盖:完整聊天 UI 组件结构,见 Chat UI;完整消息状态模型见 Message State Management。
它解决什么问题
如果用户发送消息后要等后端确认才显示:
- 网络延迟导致 UI 卡顿
- 用户不确定消息是否发送成功
- 快速连续发送时体验差
Optimistic UI 让界面立即响应,后端异步确认。
核心原理
传统流程:
用户输入 → 发送请求 → 等待响应 → 显示消息
(延迟 200-500ms)
Optimistic 流程:
用户输入 → 立即显示消息 → 发送请求 → 后台确认/回滚
(延迟 0ms)
实现
function useChat() {
const [messages, setMessages] = useState([]);
const sendMessage = async (content: string) => {
// 1. 立即添加用户消息(乐观更新)
const tempId = `temp_${Date.now()}`;
setMessages(prev => [...prev, {
id: tempId,
role: 'user',
content,
status: 'sending', // 临时状态
}]);
// 2. 创建 assistant 消息占位
setMessages(prev => [...prev, {
id: `assistant_${tempId}`,
role: 'assistant',
content: '',
status: 'streaming',
}]);
// 3. 发送请求
try {
const response = await fetch('/api/chat', { ... });
// 成功:更新为正式 ID
setMessages(prev => prev.map(msg =>
msg.id === tempId
? { ...msg, id: response.messageId, status: 'sent' }
: msg
));
} catch (err) {
// 失败:标记为错误
setMessages(prev => prev.map(msg =>
msg.id === tempId
? { ...msg, status: 'error' }
: msg
));
}
};
return { messages, sendMessage };
}
常见设计模式
1. 临时 ID
乐观更新使用临时 ID,后端确认后替换为正式 ID。
2. 状态标记
sending → sent / error
3. 回滚机制
请求失败时回滚 UI 状态,显示错误提示。
常见坑
- 不做回滚: 请求失败后 UI 仍然显示已发送
- ID 不替换: 临时 ID 没有替换为正式 ID,后续操作引用错误
- 不做重试: 发送失败后没有重试机制
- 状态不一致: 多个乐观更新并发时状态冲突
学习检查
- AI 对话为什么比“点赞”更难做乐观更新?
- 用户消息临时 ID、服务端正式 ID 与 assistant 流式消息之间如何收敛?
- 请求失败时,哪些 UI 状态应该保留,哪些应该回滚或转为可重试?
- 什么内容属于通用乐观更新,什么内容只属于 AI 对话状态机?