agentscope-ai / agentscope-ai/agentscope-java

[Bug]: Zero-chunk model completion ends the ReAct run as silent success - no error, no retry, no fallback

未关闭
#2,962 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
主要语言
Java
星标
5.6k
派生
1.3k
平均合并
4 天 12 小时
30 天内合并 PR
77

描述

**Describe the bug**

When a model provider (or an LLM gateway in front of it) returns HTTP 200 with a stream that completes without any content-bearing chunk — e.g. only the `[DONE]` marker, or chunks that all fail to parse — the whole chain treats it as a normal successful completion:

1. `OpenAIClient` streaming: a `[DONE]`-only stream completes the Flux normally (`takeWhile` on the done marker); malformed chunks are parsed to null and silently skipped. No error is ever raised.
2. `ModelUtils.applyTimeoutAndRetry` — the shared wrapper all model implementations route through (openai / gemini / dashscope / ollama / anthropic) — has no empty-completion detection, so the error never enters the existing `retryWhen` / fallback machinery.
3. `ReasoningContext.buildFinalMessage()` returns `null` when no blocks were accumulated.
4. `ReActAgent.reasoning()`: `finalMsg == null` → `Mono.justOrEmpty(null)` → the subsequent `.flatMap` is skipped entirely, so `runPostReasoningPipeline` never executes.
5. The run ends "normally": no WARN, no error event, no fallback model selected, no assistant message persisted. To the caller it looks like the model simply chose to say nothing.

The empty-response reminder loop added for "message with no visible content" (#2750 / #2755) cannot catch this case: it lives inside `runPostReasoningPipeline`, which is unreachable when the final message is null. The two conditions are also semantically different — a thinking-only message is a model-behavior problem (the reminder loop is the right response), while a zero-chunk completion is an upstream transport/service anomaly (error + retry is the right response).

**To Reproduce**

1. Point an `OpenAIChatModel` at a stub endpoint that returns HTTP 200 with body `data: [DONE]\n\n` (or unit-level: invoke a `ReActAgent` whose model returns `Flux.empty()`).
2. Ask a plain question: `agent.call(...)`.
3. The run completes successfully — no assistant message, no error logged, no retry (even with `maxAttempts > 1`), and no fallback model engaged (`switchOnFirst` only switches on an error signal).

Observed in production (AgentScope 2.0.1, OpenAI-compatible endpoint behind an LLM gateway): a run whose chat call returned 200 + zero content chunks in 0.211s ended "successfully" with no reply; a retry two minutes later also got an empty stream (0.146s); a third attempt six minutes later produced a normal 5.26s response — a transient upstream fault, silently swallowed twice with no signal at any layer.

**Expected behavior**

A stream that completes without any content-bearing `ChatResponse` (no `TextBlock` / `ThinkingBlock` / `ToolUseBlock`) is an upstream anomaly, not a valid "the model said nothing" outcome. It should surface as a `ModelException` so the existing retry (`retryWhen`) and fallback chains can engage and the caller sees an error instead of a silent empty reply. `ModelUtils.applyTimeoutAndRetry` is the natural single point for this detection — shared by all model implementations — chained before `retryWhen`.

**Error messages**

None — that is the bug. The only trace is the INFO-level harness log (`AgentTraceMiddleware`: "empty completion (no text, no tool call) — ReAct loop will terminate") acknowledging that the loop terminates silently.

**Environment (please complete the following information:)**

- AgentScope-Java Version: 2.0.1 (production); verified still present on main @ `ea511ec2`
- Java Version: 17
- OS: any
- Provider: OpenAI-compatible endpoint (MiniMax) behind an LLM gateway

**Additional context**

- Distinct from #2750 / #2755: that fix handles "has message, no visible content" and requires a non-null final message; zero-chunk completions bypass `runPostReasoningPipeline` entirely, so the protection has a blind spot by construction.
- Contributing gap, likely worth its own issue: `OpenAIResponse.isError()` does not recognize MiniMax-style `base_resp.status_code != 0` / `input_sensitive` markers, so provider-side filtered responses are indistinguishable from normal empty streams. Related: #2641 asks for status exposure in middleware events.
- Keeping the two empty-outcome conditions separate matters for the fix: folding zero-chunk into the reminder loop would waste a full LLM call re-prompting what is actually a transport fault.

贡献指南

打开贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。