#3383·WeKnora

[Bug]: 智能推理回答跨轮重复渲染

Author: DerrickLinusCreated Sep 18, 2026Updated Sep 18, 2026
Labelsbug

Affected Component

Frontend UI

Bug Description

现象

智能推理智能体(ReAct)下,使用 DeepSeek-V4.1-Flash 等模型时,回答在流式渲染中反复从头叠加:回答到第三点时,界面从第一点重新开始渲染并与旧文本堆叠,内容随轮数成倍膨胀,最终长回答导致页面卡死。GLM 等思考型模型在答案被截断后续写轮次中也能触发同样的堆叠。

复现条件

不需要特定模型,只需要这个事件序列(SSE 层面确定性复现):

  1. answer 事件(event_id=r1)流出部分内容
  2. 没有tool_call 事件经过(轮次间无工具调用:流停顿重试 / length 截断续写轮 / 空答案 nudge 重试 / finalize 合成,都会形成这种序列)
  3. 新的 answer 事件(event_id=r2)开始流出

此时 r2 事件创建时继承 r1 的全部文本,再追加自身增量。

根因

frontend/src/composables/useChatStreamHandler.ts(v0.8.0+ / c8ebb9ed,L911-913)的 answer 分支:

typescript
// ~L911
if (!answerEvent.content && message.content && String(message.content).trim()) {
   answerEvent.content = message.content   // message.content = 所有未撤回 answer 事件的拼接
 }

message.contentrecomposeAgentAnswer 拼接(所有未 superseded 事件)。当存在前一轮未被撤回的 answer 事件时,新事件把它们的文本整个复制进来,随后追加自身增量——旧文本同时存在于旧事件和新事件中。即使旧事件之后被 tool_call 事件 supersede,被复制的文本也留在了新事件体内,再无任何机制可以撤回。

该逻辑由 08852ab1(#1657)一次性引入。后端侧的相邻问题(空停止提前发终止事件)已在 780f2beb(#2906)修复;本 issue 覆盖剩余的前端触发路径。

Related: #3362(同一条链的第三方现场报告:deepseek-v4 连续 12 次 length 截断续写、页面卡顿)。其配套 PR #3366 将回放路径的分片在服务端合并,但不覆盖本 bug——回放与实时路径上,不同 event_id 的 answer 事件连续到达时种子复制依然发生,且复制进事件体的旧文本使任何回放侧优化都仍要传输重复内容。

影响

  • 答案跨轮重复堆叠(视觉上"从头重新渲染")
  • 重试/续写场景下内容近指数膨胀 → 每条 chunk 触发全文 markdown 重解析 → 长回答页面卡死
  • 撤回(supersede)不变量被破坏

建议修复方向

收窄种子条件:仅当 agentEventStream 中不存在其他未撤回且有内容的 answer 事件时才继承 message.content(保留"首个 answer 事件到达前追赶其他路径已写入文本"的原始意图)。

Steps to Reproduce

操作步骤(最易触发的路径)

  1. 创建一个使用智能推理智能体(builtin-smart-reasoning,ReAct 模式)的会话,模型选择 DeepSeek 系列(任意 deepseek 模型均可,非模型特有)
  2. 提出一个必然产生长答案的问题,例如: “请写一份完整的 MDMP 八个步骤的培训大纲,每个步骤 800 字以上”
  3. 由于单轮完成预算为 4096 token(思考+正文共享),长答案中途触发 finish_reason=length,后端进入续写轮——两轮 answer 事件之间没有 tool_call 经过
  4. 观察渲染:回答到某一点时,界面从第一点重新开始渲染并与旧文本堆叠; 轮数越多重复越多,长回答下页面逐渐卡死
  5. 打开 DevTools → Network → agent-chat SSE 流可确认:新的 answer 事件 (新 event_id)到达时,旧 event_id 的 answer 事件未被任何 tool_call 事件 supersede

最小确定性复现(SSE 事件序列层面,与模型无关)

满足以下序列即可 100% 复现,与具体模型无关:

  1. answer 事件(event_id=r1)流出部分内容
  2. 期间没有 tool_call 事件经过(流停顿重试 / length 续写轮 / 空答案 nudge 重试 / finalize 合成,都会形成这种序列)
  3. 新的 answer 事件(event_id=r2)开始流出

-> r2 事件创建时把 r1 的全部文本作为种子整体继承,再追加自身增量 (useChatStreamHandler.ts L911-913)。

三个用例中"跨轮不继承"用例在当前代码上可稳定复现失败。

Expected Behavior

新 answer 事件只包含本轮增量;message.content 恒等于所有未撤回 answer 事件的拼接

Actual Behavior

r2 = r1 全部文本 + 本轮增量;旧文本即使之后被 supersede 也永久残留

WeKnora Version

commit c8ebb9ed

Deployment Method

Build from source

Operating System

Ubuntu24.04, Windows11

Relevant Logs

bash

Confirmation

  • I have searched existing issues and confirmed this is a new one