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 如何展示,用户能不能编辑上一条消息。

一个更接近真实业务的数据结构可能长这样:

type ChatMessage = {
  id: string;
  role: 'user' | 'assistant' | 'system';
  content: string;
  status: 'ready' | 'streaming' | 'completed' | 'failed' | 'cancelled';
  runId?: string;
  error?: string;
  createdAt: number;
};

type GenerationRun = {
  id: string;
  conversationId: string;
  status: 'pending' | 'streaming' | 'completed' | 'failed' | 'cancelled';
  assistantMessageId: string;
  abortReason?: string;
};

消息用于展示和持久化,run 用于描述一次生成生命周期。把两者混在一起,后面处理取消、重试和并发时会非常痛苦。

上下文不是聊天记录

普通 IM 的历史记录主要用于展示。用户打开会话,看到过去聊了什么;服务端保存历史,保证多端一致。

AI Chat 的历史记录还有另一个作用:作为模型输入的一部分。但模型看到的上下文不一定等于页面上展示的完整聊天记录。

一次请求可能包含:

  • 系统提示词。
  • 当前用户消息。
  • 最近几轮对话。
  • 被压缩的历史摘要。
  • 文件检索结果。
  • 当前页面状态。
  • 用户偏好。
  • 工具调用结果。

因此,AI Chat 前端要接受一个事实:展示层的 messages 和请求层的 context 是两个不同模型。

页面可以展示 200 条历史消息,但本次请求可能只发送最近 10 条和一份摘要。用户看到的对话越长,越需要让产品在适当位置解释“模型这次参考了什么”。

Assistant Message 是草稿,不是已完成消息

普通 IM 中,对方发来的消息通常是完整的。即使网络慢,客户端也只是等待它出现。

AI Chat 中,assistant message 在很长一段时间里都是草稿。它每收到一个 delta 就变化一次。

这带来几个前端问题:

  • Markdown 可能暂时不闭合。
  • 代码块可能只有开头没有结尾。
  • 自动滚动不能打断用户向上阅读。
  • 复制按钮不能复制到不完整内容,或需要明确标注。
  • 生成中刷新页面如何恢复状态。
  • 中途停止后,半截内容算不算一条消息。

如果把 AI 回复当成普通消息,你很容易写出这样的逻辑:

setMessages([...messages, assistantMessage]);

然后在流式更新时反复替换整条消息。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 时会轻松很多。