Skip to content

feat: ReActAgent流式降级优化——可配置重试、非流式回退包装为流式、周期性SSE心跳 - #121

Open
openjiuwen-sync-bot[bot] wants to merge 5 commits into
openJiuwen-ai:730from
openjiuwenai:sync/pr-244
Open

feat: ReActAgent流式降级优化——可配置重试、非流式回退包装为流式、周期性SSE心跳#121
openjiuwen-sync-bot[bot] wants to merge 5 commits into
openJiuwen-ai:730from
openjiuwenai:sync/pr-244

Conversation

@openjiuwen-sync-bot

@openjiuwen-sync-bot openjiuwen-sync-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

Paired: GitHub #121GitCode !244

背景

ReActAgent 流式模式下,当模型返回空响应时直接回退非流式调用,存在以下问题:

  1. 无重试机制:空响应(模型不支持流式或瞬时异常)直接回退,未尝试重试
  2. 客户端超时断开:非流式 callModel 阻塞期间无 SSE 事件,客户端长时间无数据收发后超时断开
  3. 非流式响应无流式体验:回退非流式后,用户无法看到逐字输出效果
  4. SystemPromptBuilder NPE:section 列表含 null 时排序抛 NPE

流程图

流式降级整体流程

流式降级整体流程.png

周期性心跳时序

sequenceDiagram
    participant Client as 客户端 SSE
    participant Agent as callModelStreamWithRetry
    participant HB as ScheduledThreadPoolExecutor
    participant Model as callModel

    Agent->>Model: callModelStream attempt 0..N
    Model-->>Agent: 空响应
    Agent->>Client: 心跳 - 流式响应为空,正在重试

    Note over Agent: 全部重试失败,回退非流式

    Agent->>Client: 心跳同步 - 正在以非流式模式获取响应
    Agent->>HB: scheduleAtFixedRate 5s间隔
    Agent->>Model: callModel 阻塞

    loop 每5秒
        HB->>Client: SSE progress 心跳事件
    end

    Model-->>Agent: AssistantMessage
    Agent->>HB: cancel and shutdown finally
    Agent->>Client: writeNonStreamAsStreamChunks 分块发送
    Agent-->>Agent: 返回 Optional.of aiMessage
Loading

改动内容

1. 流式可配置重试 + 非流式回退包装(ReActAgent.java

  • 新增 callModelStreamWithRetry:流式返回空时按 streamMaxRetries(默认 2 次)重试,间隔 streamRetryDelayMs(默认 1000ms)
  • 流式异常(IllegalStateException)不重试,避免部分 chunk 已发送导致重复
  • 重试全部失败后回退非流式 callModel,将响应包装为流式 chunk 逐块发送(writeNonStreamAsStreamChunks,分块大小 200 字符)
  • 有 tool_call 时 content 通过流式发送(因循环 continue,writeStreamResult 不会发送此轮 content);无 tool_call 时由 writeStreamResult 统一发送避免重复

2. 周期性 SSE 心跳(ReActAgent.java

  • 回退非流式 callModel 前,先同步发送一次心跳,确保客户端立即收到切换通知
  • callModel 阻塞期间用 ScheduledThreadPoolExecutorHEARTBEAT_INTERVAL_MS(5s)周期发送 SSE progress 事件,防止客户端超时断开
  • callModel 返回后 finally 取消心跳并关闭执行器,避免线程泄漏
  • 重试等待期间也发送心跳(单次,因默认间隔 1s < 5s 周期无意义)
  • writeStreamHeartbeat 包 try-catch,emitter 关闭时静默忽略,避免中断定时器线程

3. SystemPromptBuilder NPE 修复(SystemPromptBuilder.java

  • build() 排序前 removeIf(section -> section == null),防止 Comparator.comparingInt NPE
  • 循环内再次 null 检查,双重防护

4. 新增配置项(ReActAgentConfig.java

配置项 类型 默认值 说明
streamMaxRetries int 2 流式空响应重试次数(不含首次调用)
streamRetryDelayMs long 1000 流式重试间隔(毫秒)

5. Clean Code 重构

  • 提取 writeToolCallsAndUsage 方法消除 tool_calls/usage_metadata 重复构建逻辑(DRY)
  • ScheduledExecutorService 改为 ScheduledThreadPoolExecutor 声明消除多余强转
  • 心跳消息字符串提取为 HEARTBEAT_NON_STREAM_MSG 常量消除硬编码重复

测试

新增 ReActAgentStreamDegradationTest(9 个测试):

  • 正常流式不受影响
  • 空响应重试后成功
  • 重试全部失败后非流式回退 + 流式包装
  • 回退响应含 tool_call 时流式正确发送
  • 重试和回退期间心跳发送
  • 12 秒长耗时场景周期性心跳验证heartbeatSentPeriodicallyDuringLongCallModel):验证 ≥2 个心跳且间隔 [3s, 8s]

新增 SystemPromptBuilderTest(NPE 场景覆盖)。

全部 9 + 9 = 18 个测试通过。

验证

  • mvn compile + mvn test-compile 通过(1882 源文件)
  • ReActAgentStreamDegradationTest 9/9 通过
  • SystemPromptBuilderTest 9/9 通过
  • git diff --check 无 trailing whitespace

guoxiuyan1 added 3 commits August 12, 2026 17:42
- 移除流式降级逻辑,改为可配置重试(streamMaxRetries/streamRetryDelayMs)
- 新增 callModelStreamWithRetry/writeNonStreamAsStreamChunks
- 新增SSE心跳机制,修复SystemPromptBuilder NPE
- 保留origin/730的shouldFailTaskOnToolError等既有改动
- 非流式回退callModel阻塞期间按5s周期发送SSE心跳(原仅发一次)
- 使用ScheduledThreadPoolExecutor显式构造+命名线程(遵循G.CON.12)
- writeStreamHeartbeat增加异常防护(emitter关闭时静默忽略)
- 修复重试等待段InterruptedException未恢复中断标志问题
- 提取writeToolCallsAndUsage方法消除tool_calls/usage_metadata重复构建逻辑(DRY)
- ScheduledExecutorService改为ScheduledThreadPoolExecutor声明消除多余强转
- 心跳消息字符串提取为HEARTBEAT_NON_STREAM_MSG常量消除硬编码重复
- 移除不再使用的ScheduledExecutorService import
- 新增heartbeatSentPeriodicallyDuringLongCallModel测试验证5s周期心跳
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


guoxiuyan1 seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

1 similar comment
@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


guoxiuyan1 seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@openjiuwen-collaboration-bot

openjiuwen-collaboration-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

head_sha: 2316a6ca631b7e87be5652f50c2da63c9eb657f4

变更摘要

本次 PR 主要针对 ReActAgent 流式模式进行了降级优化:流式调用返回空响应时不再直接回退非流式,而是引入可配置的重试机制(streamMaxRetriesstreamRetryDelayMs),重试全部失败后再回退非流式调用,并将非流式响应包装为流式 chunk 逐块发送;同时在重试等待和长耗时非流式调用期间通过 ScheduledThreadPoolExecutor 周期性发送 SSE 心跳事件,防止客户端超时断开。此外修复了 SystemPromptBuilder.build() 在 section 列表含 null 时排序抛 NPE 的问题,并提取了 writeToolCallsAndUsage 方法消除重复代码。

主要改动

  • 流式可配置重试与非流式回退包装:新增 callModelStreamWithRetry 方法,流式返回空时按 streamMaxRetries(默认 2)和 streamRetryDelayMs(默认 1000ms)重试;重试耗尽后回退 callModel,通过 writeNonStreamAsStreamChunks 将非流式响应按 STREAM_CHUNK_SIZE(200 字符)分块包装为流式 SSE 发送,有 tool_call 时 content 通过流式发送以避免与 writeStreamResult 重复。

  • 周期性 SSE 心跳机制:新增 writeStreamHeartbeat 方法,回退非流式前同步发送一次心跳;callModel 阻塞期间使用 ScheduledThreadPoolExecutorHEARTBEAT_INTERVAL_MS(5 秒)周期发送 progress 事件;重试等待期间同样发送心跳,并在 finally 中取消心跳并关闭执行器防止线程泄漏。

  • 新增配置项:在 ReActAgentConfig 中新增 streamMaxRetries(int,默认 2)和 streamRetryDelayMs(long,默认 1000)两个字段及 configureStreamRetry 便捷配置方法,允许调用方按需调整流式重试策略。

  • SystemPromptBuilder NPE 修复:在 build() 排序前添加 removeIf(section -> section == null),并在 getSectionsForBuild()build() 的循环内增加 null 检查,双重防护避免 Comparator.comparingInt 因 null 元素抛出 NPE。

  • Clean Code 重构:提取 writeToolCallsAndUsage 方法消除 tool_calls/usage_metadata 的重复构建逻辑;ScheduledExecutorService 改为 ScheduledThreadPoolExecutor 声明消除多余强转;心跳消息字符串提取为 HEARTBEAT_NON_STREAM_MSG 常量。

@openjiuwen-collaboration-bot

openjiuwen-collaboration-bot Bot commented Aug 12, 2026

Copy link
Copy Markdown

head_sha: 2316a6ca631b7e87be5652f50c2da63c9eb657f4

代码审查

我已经完成了对所有 6 个文件的逐一审查。以下是审查总结:


审查总结

发现的问题

优先级 数量 说明
P2 1 toText(null) 返回字面量 "null" 被作为流式内容发送
P3 1 心跳线程为非守护线程,极端情况下可能阻止 JVM 退出

各文件审查结果

文件 结果
src/main/java/.../ReActAgent.java 发现 2 个问题(P2 + P3)
src/main/java/.../ReActAgentConfig.java 无问题
src/main/java/.../SystemPromptBuilder.java 无问题
src/test/.../ReActAgentReactiveTest.java 无问题
src/test/.../ReActAgentStreamDegradationTest.java 无问题
src/test/.../SystemPromptBuilderTest.java 无问题

整体风险评估

中等风险。P2 的 toText(null) 问题在模型返回 tool_call 但无文本 content 时会被触发(LLM API 常见行为),导致用户看到无意义的 "null" 文本。该问题修复简单,建议合并前修正。P3 的心跳线程问题在正常生产环境中触发概率极低,属于防御性改进。其余变更(配置项、NPE 修复、测试)质量良好,逻辑正确。

类型 数量
🔴 阻塞 0
🟡 建议 1

💬 仅评论

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: 2316a6ca631b7e87be5652f50c2da63c9eb657f4

TASK STATUS DETAILS
CodeCheck ❌FAILED Click here
AntiPoison ✅SUCCESS Click here
Software Composition Analysis ✅SUCCESS Click here
Maven Build ✅SUCCESS See CHECK tab

@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: 2316a6ca631b7e87be5652f50c2da63c9eb657f4

TASK STATUS DETAILS
CodeCheck ❌FAILED Click here
AntiPoison ✅SUCCESS Click here
Software Composition Analysis ✅SUCCESS Click here
Maven Build ✅SUCCESS See CHECK tab

- 修复toText(null)返回字面量"null"导致SSE误发送问题:在调用toText前检查getContent()是否为null
- G.ERR.02: catch RuntimeException改为catch BaseError精确捕获writeStream异常
- G.MET.01: 提取fallbackToNonStreamWithHeartbeat方法,callModelStreamWithRetry从65行降至35行
- G.CON.12: ThreadFactory改为块lambda设置daemon和UncaughtExceptionHandler
- 新增nullContentWithToolCallsDoesNotSendLiteralNull测试覆盖content=null+tool_calls场景
@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: dcd80c535e85554b77e5d08a8ebfa7cc3c7cb38c

TASK STATUS DETAILS
CodeCheck ❌FAILED Click here
AntiPoison ✅SUCCESS Click here
Software Composition Analysis ✅SUCCESS Click here
Maven Build ✅SUCCESS See CHECK tab

- G.CON.10: 用shutdownNow()替代shutdown()+awaitTermination(),消除Thread.currentThread().interrupt()调用
- G.CON.12: 用Executors.defaultThreadFactory().newThread()替代new Thread(),线程创建由受管控的ThreadFactory完成
@openjiuwen-collaboration-bot

Copy link
Copy Markdown

head_sha: a5b073e56aa3465d319ea94fadab988531e7f647

TASK STATUS DETAILS
CodeCheck ✅SUCCESS Click here
AntiPoison ✅SUCCESS Click here
Software Composition Analysis ✅SUCCESS Click here
Maven Build ✅SUCCESS See CHECK tab

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant