流式对话的消息什么时候落库?中途失败、被截断或用户停止时怎么处理?
简化版
用户消息在调用模型之前落库,AI 回复在整段生成完、检查通过之后再落库。这样模型调用失败时用户的提问还在,库里也不会出现半截的 AI 回复。中途出问题分四种情况:模型报错,推一个错误事件,前端撤掉占位气泡,用户可以重发;输出被长度上限截断,前面的内容虽然已经显示,结果也不入库;用户点「停止」,前端停止读取,这一轮回答不落库;用户关掉页面,后端推送失败,要照样收尾、释放连接。库里的消息会被当作下一轮的历史带给模型,所以只存完整、可信的回复,是多轮对话不被污染的前提。
详细版
| 时刻 | 用户消息 | AI 回复 | 前端 |
|---|---|---|---|
| 请求进入、校验通过 | 落库 | 还没有 | 显示用户消息 + 一个带光标的占位气泡 |
| 流式输出中 | 已在库里 | 只在内存里累积 | 占位气泡逐段追加内容 |
| 正常结束 | 已在库里 | 整段落库 | 收到结束事件,占位换成正式记录,刷新会话列表 |
| 模型报错 | 已在库里 | 不落库 | 收到错误事件,提示原因、撤掉占位气泡 |
| 输出被截断 | 已在库里 | 不落库 | 已显示的内容后面弹出截断提示 |
| 用户停止 | 已在库里 | 不落库 | 停止读取,保留已显示的部分 |
后端要把「校验、写用户消息」放在请求线程里同步完成,再开异步线程做流式生成;异步线程里拿不到请求上下文,调用人这类信息要提前取好传进去。
完整版教学
一、落库时序为什么重要
流式对话里,一条 AI 回复是在十几秒里一段段产生的。什么时候把它写进数据库,决定了两件事:出问题时用户能看到什么,以及下一轮带给模型的历史是否干净。
多轮对话的历史通常直接从消息表里取最近几条。假设每 100 轮对话里有 3 轮在中途失败或被停止,如果边生成边写库:
每 1000 轮对话:约 30 条半截回复留在库里
这 30 轮之后的若干轮,模型都会读到一条话说一半的「AI:……」
模型可能接着半句话往下写,或者把它当成已经给出的结论
所以原则是:库里的 AI 消息只存完整、检查通过的版本。
二、用户消息为什么要先落库
用户的提问在调用模型之前就写进消息表,理由有三个:
- 失败时提问不丢。 模型报错、网络断开时,用户刷新页面还能看到自己问过什么,可以直接复制重发;
- 顺序天然正确。 用户消息的自增 ID 一定小于这一轮 AI 回复的 ID,按 ID 排序就是对话顺序;
- 会话元数据有了依据。 会话标题取首条提问、会话更新时间刷新,都可以在这一步完成。
先落库带来一个细节:取历史时,刚写进去的这条也会被查出来。它已经作为「本轮问题」单独放进提示词,要从历史里剔掉,否则模型会把同一句话读两遍。
三、AI 回复:整段写还是边收边写
| 做法 | 优点 | 缺点 |
|---|---|---|
| 结束后整段写 | 库里没有半截内容;一次写入,实现简单 | 服务在生成中途崩溃,这一轮内容全部丢失 |
| 边收边写(每 N 段更新一次) | 崩溃后能看到已生成的部分 | 库里会出现半截内容,历史和统计都要额外过滤;频繁更新数据库 |
| 先插一条「生成中」记录,结束时改状态 | 过程可追踪,失败原因可以记在这条记录上 | 取历史、做统计时必须按状态过滤 |
面向用户的聊天,整段写最常见。需要审计每次调用、或者要在对话记录里显示「这条回答失败了」时,用第三种,并且在取历史时只取状态为「完成」的记录。
四、四种中断分别怎么收尾
请求进入 ─▶ 校验 ─▶ 写用户消息 ─▶ 调模型(流式) ─▶ 截断检查 ─▶ 写 AI 消息 ─▶ 结束事件
│ │
│ 报错 │ 截断
▼ ▼
错误事件,不写 AI 消息 错误事件,不写 AI 消息
- 模型报错(Key 无效、欠费、超时):捕获异常,翻译成中文原因,推错误事件。前端撤掉占位气泡,因为库里没有这条消息,页面上也不该留一个空气泡。
- 输出被截断:结束原因在最后一帧才出现。检查要放在流式调用结束之后、写 AI 消息之前,抛出的异常同样走错误事件。页面上已经显示的半截内容不会自动消失,所以提示里要说清「这次结果不完整」。
- 用户点停止:前端中断读取,写 AI 消息那一步走不到。刷新之后只看到提问。
- 用户关页面:后端推送事件时会抛异常。这时仍然要执行收尾(关闭推送连接),否则连接会一直占着,直到超时才释放。
记忆钩子:用户消息「先写」,AI 消息「写对的」,出错时「都要收尾」。
五、前后端状态要对得上
前端在发送时先插两条本地消息:用户消息立即显示,AI 占位气泡带一个闪动的光标。之后按事件更新:
| 事件 | 前端动作 |
|---|---|
| 增量内容 | 追加到占位气泡 |
| 结束 | 去掉光标;用后端返回的正式记录(带数据库 ID)替换占位;刷新会话列表的摘要和条数 |
| 错误 | 弹出原因;撤掉占位气泡 |
还有一条容易漏的规则:一轮回复没结束时,不允许再次发送。否则两轮的用户消息先后落库,两轮 AI 回复并发生成,下一轮取历史时顺序和内容都会乱。
前端按空行拆事件、处理网络分片的细节,见「大模型应用为什么要做流式输出、取消和超时控制?」。
六、后端的线程边界
流式接口通常是「请求线程返回推送对象,异步线程里调模型」。两个线程各管一段:
请求线程:登录态校验 → 会话归属校验 → 内容校验 → 写用户消息 → 返回推送对象
异步线程:取模型配置和模板 → 拼历史 → 流式调用 → 截断检查 → 写 AI 消息 → 推结束事件
请求线程里的校验失败直接返回错误,不会进入流式;异步线程里取不到当前登录用户,写调用日志需要的用户 ID 要在请求线程里取好传进去。
七、常见误区与追问
- 误区:流式就应该边收边写库,最实时。 半截回复会进入下一轮的历史,还要在统计和展示时额外过滤。
- 误区:用户消息等 AI 回复成功后一起写。 模型失败时用户的提问也跟着丢了,用户只能凭记忆重打一遍。
- 误区:截断的回复已经显示了,存下来也无妨。 半截的改写、半截的报告存下来,用户以后可能当成完整结果使用。
- 误区:前端撤掉气泡就算处理完失败了。 后端的推送连接也要在失败路径上关闭,否则一直占用到超时。
- 误区:生成中允许用户继续发送体验更好。 两轮并发生成会打乱落库顺序和下一轮的历史。
- 追问:服务在生成中途重启,这一轮怎么办? 整段写的方案里这一轮内容丢失,用户消息还在,用户可以重发;需要恢复的场景改用「生成中」记录加状态。
- 追问:取历史时怎么避免把本轮问题读两遍? 从历史里剔掉刚写入的本轮消息,或从倒数第二条开始取。
八、加强记忆
流式对话的落库记「先写问、后写答、错了都收尾」。用户消息在调模型前写入,失败时提问不丢;AI 回复在整段生成完、截断检查通过后写入,库里没有半截内容,下一轮的历史才干净。模型报错、输出截断、用户停止三种情况都不写 AI 消息,前端按事件撤掉或保留占位;用户关页面时后端推送失败,也要关闭连接。一轮没结束不允许再发送。
项目实战落地
项目里怎么做的
《AI Agent 智慧医院智能导诊就诊系统》的预问诊对话用两张表:triage_session 存会话,triage_message 存每条消息,message_role 取「患者」或「AI」。发送消息的接口把同步段和异步段分开:
// 同步段(请求线程):归属校验、会话状态、内容校验、患者消息落库
TriageSession session = triageSessionService.requireOwnSession(request == null ? null : request.getSessionId());
// content 空 / 超 500 字 / 会话已完成 -> 直接抛,这些校验都在进入流式之前完成
triageMessageService.add(session.getId(), "患者", content);
SseEmitter emitter = new SseEmitter(120000L);
CompletableFuture.runAsync(() -> runStream(emitter, session.getId(), session.getUserId(), content));
return emitter;
异步段按顺序取模型配置、渲染模板、流式调用;整段生成完才把 AI 的完整文本写进 triage_message,再推 finish 事件;任一步异常就推 error 事件并关闭连接。前端收到 finish 后去掉光标、刷新左侧会话列表的摘要和消息数;收到 error 后弹出原因,并撤掉 AI 占位气泡。
另外两个项目处理的是另外两种中断:
- 《AI智能客服与工单处理系统》的「停止生成」按钮调用
abort()结束读取。客户的提问在发送阶段已经写进消息表,AI 回答要等整段结束才写入,中途停止时走不到这一步,刷新后只看到提问。 - 《AI智能简历助手》的流式功能先在页面放一条临时记录,
finish事件带回正式记录替换它;输出被长度上限截断时抛出异常,而插入记录的代码在流式调用之后,这次结果不会写进记录表。
为什么这样取舍
- 患者消息先落库。 模型调用失败时,患者这一轮的主诉仍然保存着,下次进会话能看到。
- AI 消息整段落库。 库里不会出现半截的 AI 回复。
- 失败时撤掉气泡。 库里没有这条 AI 消息,页面上也不该留下空气泡;撤掉占位后患者可以重发。
- 截断结果不入库。 半截的改写结果存下来,只会让用户以后误用。
面试官还会追问
- 已经出过分诊结果的会话,为什么不允许患者再继续追问?
- 患者还没建医疗档案时点「新会话」会怎样?页面上是怎么提前提示的?
学完《AI Agent 智慧医院智能导诊就诊系统》,上面这些追问你都会迎刃而解。