AI Chat 为什么不是普通聊天软件
很多前端工程师第一次做 AI Chat,会自然地把它理解成一个聊天窗口:左边是对方消息,右边是我的消息,底部有输入框,发送后追加一条消息。
这个类比有帮助,但也很容易误导工程设计。普通 IM 解决的是“人和人之间可靠传递消息”,AI Chat 解决的是“用户触发一次模型生成过程,并把过程、结果和上下文管理好”。
它看起来像聊天软件,本质上却更接近一个带自然语言入口的生成任务系统。
普通 IM 的核心是消息传递
普通聊天软件里,一条消息通常有明确的生命周期:
- 用户输入内容。
- 客户端发送到服务端。
- 服务端保存并分发。
- 对方客户端收到消息。
- 消息状态从发送中变成已送达或失败。
消息的内容在发送时基本已经确定。服务端不需要“思考”消息怎么写,也不会一边生成一边把半成品推回来。
普通 IM 的难点更多在通信可靠性、离线消息、已读回执、多端同步、消息撤回、文件上传、群聊权限和推送通知。
AI Chat 当然也需要保存消息,但它最关键的问题不是“消息有没有送到”,而是“这次生成是否正确开始、持续、停止、失败、重试和落库”。
AI Chat 的核心是生成过程
在 AI Chat 中,用户发出一条消息后,并不是另一个人立即回一条已经写好的消息。应用会把当前用户输入、历史上下文、系统提示词、工具状态、文件引用等内容组装成一次模型请求。
模型返回的 assistant message 不是瞬间存在的,它是在生成过程中逐步形成的。
这意味着前端不能只关心 messages 数组,还要关心正在发生的 run。一次 run 可能成功、失败、取消、超时、被重试,也可能在生成中触发工具调用。
消息不是唯一状态
普通 IM 的消息状态通常可以简化为:
- sending
- sent
- failed
AI Chat 里仅靠这几个状态远远不够。一个 assistant message 可能处于:
- queued:请求已创建但还没开始。
- streaming:正在接收增量内容。
- tool_calling:模型要求调用工具。
- paused:等待用户确认或外部任务完成。
- completed:生成完成。
- cancelled:用户中途停止。
- failed:网络、服务端或解析失败。
- partial:有部分内容,但没有正常完成。
这些状态会影响 UI 的每个角落:输入框是否禁用,停止按钮是否出现,重试按钮挂在哪条消息上,自动滚动是否继续,半截 Markdown 如何展示,用户能不能编辑上一条消息。
一个更接近真实业务的数据结构可能长这样:
消息用于展示和持久化,run 用于描述一次生成生命周期。把两者混在一起,后面处理取消、重试和并发时会非常痛苦。
上下文不是聊天记录
普通 IM 的历史记录主要用于展示。用户打开会话,看到过去聊了什么;服务端保存历史,保证多端一致。
AI Chat 的历史记录还有另一个作用:作为模型输入的一部分。但模型看到的上下文不一定等于页面上展示的完整聊天记录。
一次请求可能包含:
- 系统提示词。
- 当前用户消息。
- 最近几轮对话。
- 被压缩的历史摘要。
- 文件检索结果。
- 当前页面状态。
- 用户偏好。
- 工具调用结果。
因此,AI Chat 前端要接受一个事实:展示层的 messages 和请求层的 context 是两个不同模型。
页面可以展示 200 条历史消息,但本次请求可能只发送最近 10 条和一份摘要。用户看到的对话越长,越需要让产品在适当位置解释“模型这次参考了什么”。
Assistant Message 是草稿,不是已完成消息
普通 IM 中,对方发来的消息通常是完整的。即使网络慢,客户端也只是等待它出现。
AI Chat 中,assistant message 在很长一段时间里都是草稿。它每收到一个 delta 就变化一次。
这带来几个前端问题:
- Markdown 可能暂时不闭合。
- 代码块可能只有开头没有结尾。
- 自动滚动不能打断用户向上阅读。
- 复制按钮不能复制到不完整内容,或需要明确标注。
- 生成中刷新页面如何恢复状态。
- 中途停止后,半截内容算不算一条消息。
如果把 AI 回复当成普通消息,你很容易写出这样的逻辑:
然后在流式更新时反复替换整条消息。Demo 可以跑,但长回答、代码块、快速重试、多会话切换都会暴露问题。
更稳定的方式是把“assistant draft”看成一个正在被写入的资源,更新时基于 message id 做局部合并,并由状态机决定它何时变成 completed。
错误不是发送失败这么简单
普通 IM 里,发送失败通常意味着“这条消息没发出去”。用户重发即可。
AI Chat 的失败可能发生在不同阶段:
- 用户消息已经保存,但模型请求没发出去。
- 模型请求已发出,但流还没打开。
- 已经收到一半内容,然后网络断开。
- 服务端事件格式解析失败。
- 工具调用失败。
- 模型输出触发安全策略。
- 请求被用户主动取消。
每一种失败对 UI 的处理都不同。
如果已经生成了一半内容,用户可能希望保留这段内容并继续追问。如果是用户主动停止,不能把它展示成红色错误。如果是服务端超时,重试应该复用同一条用户消息,而不是再插入一条重复问题。
所以 AI Chat 的错误恢复必须围绕 run 设计,而不是只给消息加一个 failed 标记。
重试也不是重新发送同一条消息
普通 IM 重试通常是把同一条消息重新发一次。
AI Chat 的重试有几种语义:
- 重新生成上一条 assistant 回复。
- 从某条 user message 开始重建后续对话。
- 保留半截回复并让模型继续。
- 切换模型后重试。
- 修改 Prompt 或上下文后重试。
这些行为看起来都叫“重试”,但工程含义完全不同。
前端需要把重试入口和语义设计清楚。例如,assistant 消息上的“重新生成”通常表示删除或废弃当前 assistant message,再基于上一条 user message 创建一个新的 run。用户消息上的“编辑并重发”则可能会截断这条消息之后的所有对话。
UI 不是消息列表,而是生成控制台
一个成熟的 AI Chat UI 至少要表达这些控制:
- 当前是否正在生成。
- 本次生成是否可停止。
- 哪条 assistant message 正在被写入。
- 错误发生在哪个阶段。
- 哪些消息可重试、可编辑、可复制。
- 当前回复是否引用文件、工具或外部数据。
- 用户滚动离开底部后是否继续自动滚动。
它不是简单的 MessageList + InputBox,而是一个小型任务控制台。
前端工程师的关键判断是:哪些状态属于消息,哪些状态属于生成任务,哪些状态属于会话,哪些状态只是 UI 局部状态。
工程设计建议
第一,把 message、conversation 和 run 分开建模。
conversation 表示会话,message 表示展示与持久化单元,run 表示一次模型生成。这样停止、重试、恢复和日志追踪都有明确对象。
第二,为 assistant message 设计过程状态。
不要只用 role: 'assistant' 判断渲染。渲染组件应该知道它是 streaming、completed、failed 还是 cancelled。
第三,让 API 协议返回事件,而不是只返回文本。
前端不只需要文本 delta,还需要 run id、message id、错误、done、工具调用、用量统计等结构化事件。
第四,重试时明确语义。
“重试发送用户消息”“重新生成助手回答”“继续生成半截回答”应该是不同操作,不要在代码里都叫 retry()。
第五,持久化部分结果。
长回答生成到一半失败时,部分内容可能仍然对用户有价值。是否保存、如何标记、能否继续,需要产品和工程共同决定。
常见坑
坑一:只用一个 messages 数组管理所有状态。
这样一开始最简单,但后面会把请求状态、流状态、错误状态、滚动状态全部塞进消息对象里,组件越来越难维护。
坑二:把取消当成错误。
用户主动停止生成是一种正常操作。它应该进入 cancelled 或 partial 状态,而不是显示成失败。
坑三:重试时重复插入用户消息。
重新生成 assistant 回复时,通常不应该再追加一条相同的 user message。否则历史会膨胀,模型上下文也会被重复问题干扰。
坑四:前端假设模型一定会完整返回。
真实网络里,流可能中断,服务端可能超时,页面可能卸载。UI 必须能展示半成品和恢复入口。
坑五:把页面展示历史直接发给模型。
展示历史和模型上下文不是同一个东西。前端即使不负责最终上下文组装,也应该知道这条边界存在。
小结
AI Chat 长得像聊天软件,但工程核心不是 IM,而是生成生命周期管理。
普通聊天软件关心消息传递,AI Chat 还要关心上下文组装、流式输出、assistant 草稿、取消、重试、部分完成、工具状态和用户预期。前端如果从第一天就把 message 和 run 分开,把状态机设计清楚,后续做 Streaming、重试和稳定 UI 时会轻松很多。