Solon v4.1.0

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

</> markdown
2026年9月16日 下午12:09:08

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

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

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

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

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

1、先理解什么是一个 Turn

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

典型过程如下:

第 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:给推理循环安装保险丝

最基础的配置方式如下:

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

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

currentTurn <= maxTurns:继续推理
currentTurn >  maxTurns:停止执行

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

Agent error: Maximum turns reached (8).

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

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

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

3、autoRethink:接近上限时先反思,再决定下一步

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

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

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

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 的触发与扩容规则

当前实现的触发阈值为:

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

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

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

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

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

例如,初始配置为:

.maxTurns(8)
.autoRethink(true)

运行过程大致是:

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

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

初始上限: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 错误。

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

//示例
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)》。

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

构建时配置

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

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

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

单次请求调整

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

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 个推理回合,并在接近上限时自动反思:

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());

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

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

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

10、小结

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

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

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