← 大模型应用基础

流式对话的消息什么时候落库?中途失败、被截断或用户停止时怎么处理?

中等 流式输出 · 第 3 / 3 问 更新于 2026/09/29
大模型应用流式输出多轮对话SSE数据一致性
本题落地项目AI Agent 智慧医院智能导诊就诊系统

简化版

用户消息在调用模型之前落库,AI 回复在整段生成完、检查通过之后再落库。这样模型调用失败时用户的提问还在,库里也不会出现半截的 AI 回复。中途出问题分四种情况:模型报错,推一个错误事件,前端撤掉占位气泡,用户可以重发;输出被长度上限截断,前面的内容虽然已经显示,结果也不入库;用户点「停止」,前端停止读取,这一轮回答不落库;用户关掉页面,后端推送失败,要照样收尾、释放连接。库里的消息会被当作下一轮的历史带给模型,所以只存完整、可信的回复,是多轮对话不被污染的前提。

详细版

时刻用户消息AI 回复前端
请求进入、校验通过落库还没有显示用户消息 + 一个带光标的占位气泡
流式输出中已在库里只在内存里累积占位气泡逐段追加内容
正常结束已在库里整段落库收到结束事件,占位换成正式记录,刷新会话列表
模型报错已在库里不落库收到错误事件,提示原因、撤掉占位气泡
输出被截断已在库里不落库已显示的内容后面弹出截断提示
用户停止已在库里不落库停止读取,保留已显示的部分

后端要把「校验、写用户消息」放在请求线程里同步完成,再开异步线程做流式生成;异步线程里拿不到请求上下文,调用人这类信息要提前取好传进去。

完整版教学

一、落库时序为什么重要

流式对话里,一条 AI 回复是在十几秒里一段段产生的。什么时候把它写进数据库,决定了两件事:出问题时用户能看到什么,以及下一轮带给模型的历史是否干净。

多轮对话的历史通常直接从消息表里取最近几条。假设每 100 轮对话里有 3 轮在中途失败或被停止,如果边生成边写库:

每 1000 轮对话:约 30 条半截回复留在库里
这 30 轮之后的若干轮,模型都会读到一条话说一半的「AI:……」
模型可能接着半句话往下写,或者把它当成已经给出的结论

所以原则是:库里的 AI 消息只存完整、检查通过的版本。

二、用户消息为什么要先落库

用户的提问在调用模型之前就写进消息表,理由有三个:

  1. 失败时提问不丢。 模型报错、网络断开时,用户刷新页面还能看到自己问过什么,可以直接复制重发;
  2. 顺序天然正确。 用户消息的自增 ID 一定小于这一轮 AI 回复的 ID,按 ID 排序就是对话顺序;
  3. 会话元数据有了依据。 会话标题取首条提问、会话更新时间刷新,都可以在这一步完成。

先落库带来一个细节:取历史时,刚写进去的这条也会被查出来。它已经作为「本轮问题」单独放进提示词,要从历史里剔掉,否则模型会把同一句话读两遍。

三、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 智慧医院智能导诊就诊系统》,上面这些追问你都会迎刃而解。

本题落地项目地狱锤炼AI Agent 智慧医院智能导诊就诊系统项目简介:基于医学知识库 RAG、向量检索、数据库驱动工具中心、Spring Boot + LangChain4j、Function Calling,实现文档切分与 Embedding 向量化、按库收窄的内存余弦 topK 检索、SSE 真流式多轮预问诊、数据库驱动 ReAct 分诊 Agent 多轮自主取证、工具注册表真开关与调用日志、科室医生查库核验、挂号乐观锁扣减、Agent 执行时间线与 AI 调用观测、就诊运营,覆盖从预问诊、智能分诊、在线挂号到接诊写病历与运营复盘的完整就诊闭环。SpringbootLangChain4JAgentMCPRAG源码+SQL喂饭学习教程配套面试文档环境安装文档项目运行文档 学习这个项目 也可以学AI智能客服与工单处理系统登峰造极 查看项目 也可以学AI智能简历优化系统小试牛刀 查看项目