Skip to content

fix(ai-chat): 思考型模型的 reasoning 增量实时转发 + 断连提示条(dev-board#364) - #702

Merged
zeweihan merged 1 commit into
masterfrom
worktree-agent-a52f149f758069cab
Sep 2, 2026
Merged

fix(ai-chat): 思考型模型的 reasoning 增量实时转发 + 断连提示条(dev-board#364)#702
zeweihan merged 1 commit into
masterfrom
worktree-agent-a52f149f758069cab

Conversation

@zeweihan

@zeweihan zeweihan commented Sep 2, 2026

Copy link
Copy Markdown
Owner

dev-board#364:K3 思考 281 秒才动,前端零提示分不清死机还是思考。

诊断:

  • 后端到 provider 本来就是流式,OpenRouter 从第 4 秒起就在推 reasoning 增量;但 openai4j 0.23 的 Delta 没有 reasoning 字段,langchain4j 0.36 只在 content 非 null 时 onNext,OpenRouter 给的是 content:"",几百秒思考期编排器收到一连串空串——空串刷新了看门狗、emitText 又直接 return,前端一个字节都没有。281 秒是 K3 真实思考时长,不是传输延迟。
  • 首 token 前没有显著的非流式前置步骤(标题异步、上下文组装亚秒到数秒、压缩只在长历史触发)。
  • 心跳 15s 与 45s 判死重连本来就有,但只写 console.warn,UI 无提示。

修复:新增 OpenRouterStreamingChatModel(自有 HTTP+SSE 行协议,复用 langchain4j 消息/工具编组与 OpenAiStreamingResponseBuilder)作为 OpenRouter 流式唯一通道;ReasoningStreamingHandler 增 onReasoning/onKeepAlive;AgentStreamHandler 转发 SSE reasoning_delta(不进正文不落库不回喂),思考增量与保活注释刷新看门狗,streamedAnyReasoning 与 streamedAnyToken 分开。前端 reasoning_delta 实时渲染进思考卡;新增 linkStatus,重连期间输入区渲染提示条(两套 locales)。

验证:后端 OpenRouterStreamingChatModelTest / AgentStreamHandlerReasoningTest 等 6 组定向用例 + 全量 2766 例;前端 reasoning-stream.test.mjs 9 例、test:project-home 305/305。主会话病灶还原:onReasoning 置空→2 红、前端去掉 reasoning_delta 分支→1 红。主会话另用真实 OpenRouter 跑新通道:普通文本流(usage/finish 正确)与工具调用流(参数正确拼出)通过;K3 真流未跑成——开发 key 余额仅够约 1500 token,K3 预留 131072 被 402 拒绝,错误路径正确抛出。reasoning 解析靠 mock 用例 + 子 agent 对 K3 的真机探测(80 个 reasoning chunk)。

已知边界:reasoning 不持久化、刷新后不回放;未主动请求 reasoning 参数;Office 插件窗格未接该事件(未知事件被忽略)。

🤖 Generated with Claude Code

病灶:选 Kimi K3 让 AI 修订文档,「思考了 281 秒才开始工作」期间前端零提示。
真机探测(OpenRouter kimi-k3 流式):第 4 秒起每个 chunk 都是 content:"" +
reasoning:"…",前面夹 ": OPENROUTER PROCESSING" 保活注释。openai4j 0.23 的 Delta
没有 reasoning 字段、okhttp-sse 吞掉注释行、langchain4j 只对非 null content 调
onNext——于是几百秒思考期间编排器收到的全是 onNext(""):看门狗被空 token 喂活、
前端一个字节都收不到。281 秒是模型真实思考时长,不是首 token 传输延迟。

后端
- 新增 OpenRouterStreamingChatModel(自有 HTTP + SSE 行协议;复用 langchain4j 的
  消息/工具编组、openai4j 的请求序列化、OpenAiStreamingResponseBuilder 的片段组装),
  ChatModelFactory.streamingModel 成为两个流式通道的唯一构建口径;Ollama 不变。
  HTTP 200 里 data 事件送来的 {"error":…} 也当错误,不再按空回复静默收尾。
- 新增 ReasoningStreamingHandler(onReasoning / onKeepAlive,default 空实现)。
  AgentStreamHandler 实现它:reasoning → SSE reasoning_delta(不进正文、不落库、
  不回喂模型),两者都刷新看门狗;streamedAnyReasoning 与 streamedAnyToken 分开,
  看门狗时限选择认前者、可重放判定只认后者。首字节 60s 保持不变。
- SseEmitterService 心跳扫描提成 heartbeatSweep() 供测试直接调用。

前端
- useAgentStream 认 reasoning_delta → appendReasoning(顶层思考卡 / 最后一个过程卡的
  思考条目,与 <thinking> 标签同落点,不过标签解析器);新增 linkStatus
  (live / reconnecting + 次数),ChatInterface 输入区在重连期间渲染
  chat.linkReconnecting 提示条(zh-CN / en-US)。等待首 token 的读秒本来就从发送起算。

测试(先红后绿,红用还原病灶实测)
- OpenRouterStreamingChatModelTest(假服务端回放真机片段形状:reasoning/正文/保活三通道、
  tool_calls 拼装、429 状态码、流内 error 对象、旧 handler 兼容)
- AgentStreamHandlerReasoningTest(reasoning_delta 事件、思考期间看门狗不按零字节掐、
  保活刷新看门狗)
- SseEmitterServiceTest.heartbeatSweepReachesEveryLiveConnection
- StreamingTransportFailureTest 改走 streamingModel;ChatModelFactoryTest 的流式类型断言改成 OpenRouterStreamingChatModel
- frontend/tests/project-home/reasoning-stream.test.mjs(ThinkingCard 真实响应式读秒、
  reasoning_delta 分派、linkStatus 与提示条、前端判死阈值 = 后端心跳 × 3)
- 领域文档 .claude/agents/ai-chat.md 同步

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@zeweihan
zeweihan enabled auto-merge (squash) September 2, 2026 00:47
@zeweihan
zeweihan merged commit 9eb5058 into master Sep 2, 2026
6 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Sep 2, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant