---
title: "react - 回合、反思与扩容续航"
---



`ReActAgent` 会在 Reason（推理）与 Action（行动）之间循环：模型判断下一步，工具执行动作，执行结果再回到模型，直到模型给出最终答案。

这个闭环让 Agent 能完成多步骤任务，但也带来一个现实问题：如果模型反复调用工具、持续尝试无效路径，循环应该在什么时候结束？

Solon AI 提供了两个相互配合的配置：

- `maxTurns`：设置一次任务的初始最大推理回合数；
- `autoRethink`：接近回合上限时，是否注入自我反思指令并自动增加可用回合数。

一个负责“守住边界”，另一个负责“在必要时续航”。

### 1、先理解什么是一个 Turn

在 `ReActAgent` 中，一个 Turn 指一次 Reason 推理回合，而不是一次工具调用。

典型过程如下：

```text
第 1 回合 Reason -> 调用搜索工具 -> Observation
第 2 回合 Reason -> 调用详情工具 -> Observation
第 3 回合 Reason -> 生成 Final Answer -> End
```

上面的任务使用了 3 个 Turn。一次 Reason 可以产生一个或多个工具调用，但 Action 阶段本身不会单独增加 Turn 计数。

因此，`maxTurns(8)` 的含义不是“最多调用 8 次工具”，而是“初始最多允许进行 8 个 Reason 推理回合”。

> v4 中统一使用 `maxTurns`。旧版的 `maxSteps`、`getMaxSteps()` 等名称已经迁移或标记为废弃。

### 2、maxTurns：给推理循环安装保险丝

最基础的配置方式如下：

```java
ReActAgent agent = ReActAgent.of(chatModel)
        .name("order_helper")
        .role("订单问题排查助手")
        .defaultToolAdd(orderTools)
        .maxTurns(8)
        .build();
```

在默认的标准模式下，`autoRethink` 为 `false`。每次进入 Reason 阶段时，Agent 会先增加 Turn 计数，再检查是否超出上限：

```text
currentTurn <= maxTurns：继续推理
currentTurn >  maxTurns：停止执行
```

也就是说，配置 `maxTurns(8)` 时，第 1～8 回合可以正常推理。如果第 8 回合仍然选择继续行动，下一次准备进入 Reason 时会触发保护，不再请求模型，并以类似下面的结果结束：

```text
Agent error: Maximum turns reached (8).
```

这项配置主要解决三类问题：

1. **防止循环失控**：模型反复使用同一个工具，或者在几个无效策略之间来回切换；
2. **限制成本**：减少不可控的模型请求、Token 和工具调用开销；
3. **控制延迟**：避免单次请求长时间占用线程、连接和外部资源。

当前源码中，`ReActOptions` 的默认 `maxTurns` 为 `8`，`autoRethink` 默认为 `false`。生产项目仍建议显式配置，不要让任务边界依赖默认值。

### 3、autoRethink：接近上限时先反思，再决定下一步

有些任务天然无法预先确定需要多少回合。例如：

- 多来源资料检索与交叉验证；
- 日志、数据库和配置联合排障；
- 需要根据中间结果动态选择下一工具；
- 长链路代码分析或数据核验。

如果只设置较小的 `maxTurns`，任务可能在即将找到答案时被强制终止。此时可以开启 `autoRethink`：

```java
ReActAgent agent = ReActAgent.of(chatModel)
        .name("ops_helper")
        .role("生产问题排查助手")
        .defaultToolAdd(logTools)
        .defaultToolAdd(databaseTools)
        .maxTurns(12)
        .autoRethink(true)
        .build();
```

开启后，接近当前上限时框架会做两件事：

1. 自动增加后续可用的 Turn 数；
2. 向 Working Memory 追加一条自我反思指令，让模型重新检查目标、历史 Observation 和当前策略。

反思指令会要求模型重点判断：

- 当前方向是否偏离用户的核心目标；
- 最近的尝试是否产生了有效新线索；
- 是否应该更换思路或换一个切入角度；
- 如果受客观条件限制无法完成，是否应该整理已知信息并给出最终答复，请求用户协助。

所以，`autoRethink` 不是简单地“多跑几轮”。它会在续航前主动推动模型复盘和收敛。

### 4、autoRethink 的触发与扩容规则

当前实现的触发阈值为：

```java
thresholdTurn = Math.max(maxTurns - 1, (int) (maxTurns * 0.8));
```

当 `currentTurn >= thresholdTurn` 时，框架触发自动反思，并增加回合上限：

```java
addTurns = Math.max(10, initialMaxTurns / 2);
```

这里有两个细节值得注意：

- 阈值取两个结果中的较大值。对于常见配置，通常会在“当前上限的前一回合”触发；
- 每次增加的回合数至少为 10；初始值较大时，则增加初始 `maxTurns` 的一半。

例如，初始配置为：

```java
.maxTurns(8)
.autoRethink(true)
```

运行过程大致是：

```text
初始上限：8
第 7 回合：触发反思，上限增加 10，变为 18
第 17 回合：如果任务仍未结束，再次触发反思，上限增加 10，变为 28
……
```

如果初始值为 30，则每次增加 15 个回合：

```text
初始上限：30
第 29 回合：触发反思，上限变为 45
第 44 回合：如果仍未结束，再次触发反思，上限变为 60
……
```

这里的 `initialMaxTurns` 始终表示初始配置值；`getMaxTurns()` 则返回扩容后的当前有效上限。

### 5、重要边界：开启 autoRethink 后，maxTurns 不再是硬上限

这是使用这两个参数时最需要理解的一点。

当 `autoRethink(false)` 时，`maxTurns` 是明确的硬边界。超过后，Agent 会停止继续请求模型。

当 `autoRethink(true)` 时，框架会在接近上限时先扩容，再让模型继续执行。如果任务仍不收敛，后续还会再次扩容。因此，此时的 `maxTurns` 更准确地说是：

> 首个反思检查点和初始推理预算，而不是整个任务不可突破的总回合数。

`autoRethink` 希望由模型在反思后主动选择以下一种结果：

- 调整策略并继续行动；
- 已有信息足够，输出 Final Answer；
- 客观条件不足，说明限制并请求用户提供帮助。

但模型是否真正收敛仍具有不确定性。因此，对于有严格 SLA、费用预算或外部资源限制的系统，不能只依赖 `autoRethink`。还应该在应用层配置超时、取消机制、工具调用配额或任务级资源预算。

### 6、扩容不是免费续航：长任务应配合上下文压缩

`autoRethink` 增加的是后续可用回合数，但不会自动清空已经积累的 Working Memory。随着 Reason、Action 和 Observation 持续追加，模型输入会越来越长，进而带来几个问题：

- Token 消耗与调用成本持续上升；
- 单次推理延迟增加；
- 历史信息过多，可能干扰模型识别当前目标和关键线索；
- 最终可能超过模型的上下文窗口，触发 Prompt-Too-Long 错误。

因此，“开启一次自动扩容就必然导致上下文爆炸”并不准确；但如果任务可能多次扩容、长时间运行，上下文膨胀就是必须治理的风险。更稳妥的做法，是把自动续航与上下文压缩配套使用：

```java
//示例
CompressionStrategy compressionStrategy = new CompositeCompressionStrategy()
 .addStrategy(new KeyInfoExtractionStrategy()) // 事实看板
 .addStrategy(new HierarchicalCompressionStrategy()); // 滚动摘要
 
ContextCompressionInterceptor memoryGuard = new ContextCompressionInterceptor(100, 0.75D, compressionStrategy);

ReActAgent agent = ReActAgent.of(chatModel)
        .name("research_agent")
        .role("长任务研究助手")
        .defaultToolAdd(searchTools)
        .defaultInterceptorAdd(memoryGuard)
        .maxTurns(12)
        .autoRethink(true)
        .build();
```

`ContextCompressionInterceptor` 会在 Reason 开始前监控消息数量和估算 Token 数；超过阈值后，将较早的执行历史压缩为摘要，同时保留近期工作上下文。示例中的 `HierarchicalCompressionStrategy` 会滚动合并旧摘要与新增历史，更适合可能反复续航的长任务。

需要注意，上下文压缩解决的是“历史越来越长”的问题，不是任务终止问题，也不能替代超时、配额和取消机制。可以把它们理解为三层治理：

1. `maxTurns`：控制初始推理预算；
2. `autoRethink`：在任务仍有希望完成时反思并续航；
3. 上下文压缩：控制续航过程中的 Working Memory 规模。

关于压缩触发条件、摘要策略和参数选择，可继续阅读[《react - 上下文压缩（compression）》](https://solon.noear.org/article/1378)。

### 7、构建时配置与单次请求调整

#### 构建时配置

Builder 上的配置会成为该 Agent 的默认运行选项：

```java
ReActAgent agent = ReActAgent.of(chatModel)
        .name("research_agent")
        .role("资料检索与核验助手")
        .defaultToolAdd(searchTools)
        .maxTurns(10)
        .autoRethink(false)
        .build();
```

适合为某类 Agent 设置稳定的默认策略。

#### 单次请求调整

某个任务比平时更复杂时，可以只调整本次请求：

```java
ReActResponse response = agent.prompt("检索多个来源并交叉核验结论")
        .options(o -> o
                .maxTurns(16)
                .autoRethink(true))
        .call();

String answer = response.getContent();
```

请求级调整基于默认选项的副本执行，不会修改 Agent 的全局默认配置。这样可以让普通任务保持较小预算，只为少数复杂任务开启续航。

### 8、如何选择配置

可以按任务类型做一个简单划分：

| 场景 | 建议配置 | 原因 |
|---|---|---|
| 固定工具链、低延迟接口 | 较小 `maxTurns`，关闭 `autoRethink` | 边界清晰，延迟和成本可预测 |
| 在线客服、订单查询 | 中等 `maxTurns`，通常关闭 `autoRethink` | 大多数问题路径较短，失败后可让用户补充信息 |
| 研究、排障、代码分析 | 中等或较大 `maxTurns`，按需开启 `autoRethink` | 中间结果会动态改变后续策略 |
| 严格计费或严格 SLA | 关闭 `autoRethink`，同时设置外部超时与配额 | 必须保留真正的硬边界 |
| 后台异步复杂任务 | 可开启 `autoRethink`，同时保留任务级终止条件 | 允许续航，但不能无限消耗资源 |

经验上，不应一开始就把 `maxTurns` 设置得非常大。更稳妥的做法是：

1. 先根据正常任务链路设置一个合理初始值；
2. 通过 `ReActTrace.getTurnCount()` 和日志观察真实回合分布；
3. 分析任务是因为预算不足，还是因为工具描述、角色指令或 Observation 质量不佳；
4. 只有确实需要动态探索的任务，才开启 `autoRethink`。

如果 Agent 经常触发回合上限，增加 `maxTurns` 可能只是暂时掩盖问题。更应该检查工具职责是否清晰、工具结果是否可读、模型是否能识别结束条件，以及系统指令是否要求它在信息充分时及时输出最终答案。

### 9、完整示例

下面创建一个技术支持 Agent。默认允许 12 个推理回合，并在接近上限时自动反思：

```java
ReActAgent techAgent = ReActAgent.of(chatModel)
        .name("tech_support")
        .role("技术支持专家，负责结合日志和业务数据定位问题")
        .instruction("优先验证事实，避免重复调用同一工具；信息充分后立即给出结论。")
        .defaultToolAdd(logTools)
        .defaultToolAdd(databaseTools)
        .retryConfig(3, 1000L)
        .maxTurns(12)
        .autoRethink(true)
        .build();

ReActResponse response = techAgent
        .prompt("用户 9527 登录失败，请定位原因并给出处理建议")
        .call();

System.out.println(response.getContent());
System.out.println("实际推理回合数：" + response.getTrace().getTurnCount());
```

如果另一个请求必须在较短时间内返回，可以覆盖本次运行选项：

```java
ReActResponse response = techAgent
        .prompt("快速检查最近一次登录失败的直接原因")
        .options(o -> o
                .maxTurns(5)
                .autoRethink(false))
        .call();
```

这样，同一个 Agent 可以同时服务“深度排障”和“快速诊断”两类任务。

### 10、小结

`maxTurns` 和 `autoRethink` 控制的是两种不同的运行策略：

- `maxTurns` 定义初始推理预算；关闭自动反思时，它也是防止死循环的硬上限；
- `autoRethink` 在接近上限时注入反思指令并扩展预算，让复杂任务有机会调整策略、继续完成；
- 开启 `autoRethink` 后，`maxTurns` 不再代表不可突破的总回合数；
- 多次扩容会持续累积 Working Memory，长任务应配合上下文压缩控制消息与 Token 规模；
- 对严格成本和延迟场景，应关闭自动反思，并在应用层增加超时、取消与资源配额；
- 对研究、排障等探索型任务，可以开启自动反思，但仍应观察轨迹并设置任务级终止条件。

好的 Agent 不是“允许它一直思考”，而是在可控预算内尽快找到有效路径；接近边界时，知道复盘、换路，或者诚实地结束任务。