agentscope-ai / agentscope-ai/agentscope-java

[Bug]:ReActAgent 调用不存在的工具时直接退出 ReAct 循环,未给模型重试机会

オープン
#3,102 コメント 2 件 リアクション 0 件 担当者 0 名 GitHub で見る
area/core/agent area/core/model bug
主要言語
Java
スター
5.6k
フォーク
1.3k
平均マージ
4日 12時間
マージ済み PR(30日)
77

説明

# ReActAgent 调用不存在的工具时直接退出 ReAct 循环,未给模型重试机会

## 问题描述

当 LLM 在流式响应中尝试调用一个不在 tools schema 里的工具(如把 Skill 名 `metric_condition_generation` 当工具调用),LLM 服务端会返回 error。agentscope 的 `OpenAIClient` 把这个 error 包装为 `OpenAIException` 抛出,`ReActAgent.reasoning()` 只捕获 `InterruptedException`,其他异常直接传播终止整个 `streamEvents` 流。

模型没有机会纠正错误、换一个工具或改用纯文本回复——整个对话直接以 error 结束。

## 复现场景

1. 配置了一个 Agent,有 tools 但没有 `metric_condition_generation` 这个工具
2. 模型在 reasoning 阶段幻觉式调用了 `metric_condition_generation`(这是一个 Skill 名,不是工具名)
3. LLM 服务端在流式响应中返回 error:`LLM is trying to invoke a non-exist tool: "metric_condition_generation"`
4. `OpenAIClient` 把 error 包装为 `OpenAIException` 并抛出
5. `ReActAgent.reasoning()` 的 `onErrorResume` 只捕获 `InterruptedException`,`OpenAIException` 直接传播
6. 整个 `streamEvents` 流以 error 终止,对话直接结束

## 实际结果

```
OpenAI API error in streaming response: LLM is trying to invoke a non-exist tool: "metric_condition_generation", you can add some few shots examples or adjust the prompt.
```

对话直接终止,用户看到错误,没有结果。

## 期望结果

当 LLM 尝试调用不存在的工具时,框架应该:
1. 捕获这个 error,不终止整个 ReAct 循环
2. 构造一个 `ToolResultBlock.error("工具 X 不存在,请使用已注册的工具或直接回复用户")` 作为该 tool_call 的结果
3. 把这个 error 结果加入上下文,继续下一轮 reasoning——让模型看到错误后自行纠正
4. 如果模型多次(如 2~3 次)仍然调用不存在的工具,才终止循环

## 根因分析

### 错误产生路径

```
LLM 返回 tool_call(name="metric_condition_generation")
→ LLM 服务端发现该工具不在 tools schema 里
→ 流式响应返回 error
→ OpenAIClient/OpenAIResponseParser 解析 error
→ 抛出 OpenAIException("OpenAI API error in streaming response: LLM is trying to invoke a non-exist tool: ...")
→ ReActAgent.reasoning() 的 model.stream() 报错
→ onErrorResume 只捕获 InterruptedException
→ OpenAIException 直接传播
→ streamEvents 流终止
```

### 涉及的代码

**OpenAIClient**(agentscope-extensions-model-openai):
- `OpenAIClient.stream()` 解析流式响应时,发现 `OpenAIResponse.isError()` 为 true
- 构造 `OpenAIException` 并通过 `SynchronousSink.error()` 抛出

**OpenAIResponseParser**(agentscope-extensions-model-openai):
- 同样有 "OpenAI API error in streaming response" 字符串

**ReActAgent.CallExecution.reasoning()**(agentscope-core):
- `model.stream().doOnNext(this::publishEvent).then(...)` 的 stream 因 error 终止
- `onErrorResume(InterruptedException.class, ...)` 只捕获 `InterruptedException`
- 其他异常(如 `OpenAIException`)直接传播,终止整个 reasoning 循环

### 对比 ToolExecutor 的处理

`ToolExecutor.invokeTool()` 在工具不存在时(`toolRegistry.getTool(name)` 返回 null),会构造 `ToolResultBlock.error("Tool not found: " + name)` 并正常返回,不抛异常。这给了模型重试的机会。

但 LLM 服务端在流式阶段就拦截了——工具名不在 tools schema 里,服务端直接返回 error,不走 agentscope 的 `ToolExecutor`。所以 `ToolExecutor` 的兜底逻辑在这个场景下不生效。

## 建议修复

### 方案 A:在 reasoning 的 onErrorResume 中捕获 OpenAIException

```java
// ReActAgent.CallExecution.reasoning() 里
.onErrorResume(OpenAIException.class, error -> {
// 构造一个 error ToolResultBlock,让模型在下一轮看到错误后自行纠正
Msg errorMsg = Msg.builder()
.role(MsgRole.ASSISTANT)
.content(TextBlock.builder().text("工具调用失败: " + error.getMessage()).build())
.build();
state.contextMutable().add(errorMsg);
// 继续下一轮 reasoning,给模型纠正的机会
return reasoning(iter + 1, true); // ignoreMaxIters=true 保证至少重试一次
})
```

### 方案 B:在 OpenAIClient 里不抛异常,改为返回 error ChatResponse

```java
// OpenAIClient.stream() 里解析到 error response 时
if (response.isError()) {
// 不抛异常,改为构造一个携带错误信息的 ChatResponse
return Flux.just(ChatResponse.error(response.getEffectiveErrorMessage()));
}
```

这样 `ReActAgent.modelCallStream` 的 `concatMap` 会正常处理这个 error ChatResponse,可以转换为 error event 发给下游,不会终止流。

### 推荐方案 B

方案 A 在 `reasoning` 层兜底,但 error 的语义不明确(是工具不存在还是网络错误?);方案 B 在源头处理,让 error 成为流中的一个正常事件,框架可以在 `concatMap` 里把它转成 error `ToolResultBlock`,和 `ToolExecutor` 的 "Tool not found" 处理对齐。

## 环境信息

- agentscope-core: 2.0.3
- agentscope-extensions-model-openai: 2.0.3
- JDK: 17
- 模型服务: 百炼 qwen-plus / antchat Qwen3.5-35B-A3B

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。