[Bug]: 智能推理回答跨轮重复渲染
Affected Component
Frontend UI
Bug Description
现象
智能推理智能体(ReAct)下,使用 DeepSeek-V4.1-Flash 等模型时,回答在流式渲染中反复从头叠加:回答到第三点时,界面从第一点重新开始渲染并与旧文本堆叠,内容随轮数成倍膨胀,最终长回答导致页面卡死。GLM 等思考型模型在答案被截断后续写轮次中也能触发同样的堆叠。
复现条件
不需要特定模型,只需要这个事件序列(SSE 层面确定性复现):
answer事件(event_id=r1)流出部分内容- 没有
tool_call事件经过(轮次间无工具调用:流停顿重试 / length 截断续写轮 / 空答案 nudge 重试 / finalize 合成,都会形成这种序列) - 新的
answer事件(event_id=r2)开始流出
此时 r2 事件创建时继承 r1 的全部文本,再追加自身增量。
根因
frontend/src/composables/useChatStreamHandler.ts(v0.8.0+ / c8ebb9ed,L911-913)的 answer 分支:
// ~L911
if (!answerEvent.content && message.content && String(message.content).trim()) {
answerEvent.content = message.content // message.content = 所有未撤回 answer 事件的拼接
}message.content 由 recomposeAgentAnswer 拼接(所有未 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
操作步骤(最易触发的路径)
- 创建一个使用智能推理智能体(builtin-smart-reasoning,ReAct 模式)的会话,模型选择 DeepSeek 系列(任意 deepseek 模型均可,非模型特有)
- 提出一个必然产生长答案的问题,例如: “请写一份完整的 MDMP 八个步骤的培训大纲,每个步骤 800 字以上”
- 由于单轮完成预算为 4096 token(思考+正文共享),长答案中途触发
finish_reason=length,后端进入续写轮——两轮 answer 事件之间没有 tool_call 经过 - 观察渲染:回答到某一点时,界面从第一点重新开始渲染并与旧文本堆叠; 轮数越多重复越多,长回答下页面逐渐卡死
- 打开 DevTools → Network → agent-chat SSE 流可确认:新的 answer 事件 (新 event_id)到达时,旧 event_id 的 answer 事件未被任何 tool_call 事件 supersede
最小确定性复现(SSE 事件序列层面,与模型无关)
满足以下序列即可 100% 复现,与具体模型无关:
- answer 事件(event_id=r1)流出部分内容
- 期间没有 tool_call 事件经过(流停顿重试 / length 续写轮 / 空答案 nudge 重试 / finalize 合成,都会形成这种序列)
- 新的 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
Confirmation
- I have searched existing issues and confirmed this is a new one
Source: Tencent/WeKnora