ReAct 的论文核心思想就一句话:让 LLM 交替"思考(Reasoning)"和"行动(Acting)",直到得出最终答案。落到工程里要解决五个问题: 1. 怎么把 LLM 决定的工具调用真正执行掉(reasoning → acting) 2. 怎么把工具结果再喂回 LLM(acting → reasoning) 3. 什么时候停(max_iters / 没有 tool_use / HITL stop / 完成) 4. 流式输出怎么保证既能展示给用户、又能在结束时拼成完整 Msg 5. 任意一步暂停或崩溃后,下次能从中间状态恢复 ReActAgent 的所有复杂度都是在回答这五个问题。
Builder装配
要了解ReActAgent的实现原理,需要从他的build方法讲起:
public
}
ReActAgent 自己不持有工具——所有工具都注册在 Toolkit 上,Agent 只是引用。Toolkit 深拷贝是关键。同一个 Toolkit 注册到多个 Agent 时,每个 Agent 拿到的是独立副本——这样 RAG/SkillBox 等子系统给 Agent A 注册的工具不会污染 Agent B。这是后面 RAG/SkillBox 能"自动注入工具"的安全前提。 置顺序也有意为之: - LongTermMemory 优先,因为 STATIC_CONTROL 模式会装 StaticLongTermMemoryHook,要早于其他 hook 拿到 PreReasoningEvent; - RAG 在 GENERIC 模式下也是装 GenericRAGHook,同样进 hooks 列表;AGENTIC 模式则注册 retrieve_knowledge 工具进 toolkit; - SkillBox 注册 SkillHook 同时把 skills 当工具暴露; - 它们都通过 同一个入口(toolkit + hook list)把能力插到 ReAct 主循环里——意味着所有"扩展能力"对主循环是透明的。 PendingToolRecoveryHook 在最早期注册(priority=10),所以 PreCallEvent 时它第一个跑,能在用户 hook 之前补救悬空的 ToolUse。
call()
AgentBase.call(List
public
resource
}
这条链定义了 所有 Agent 的统一行为契约。ReActAgent.doCall 拿到的是 PreCall hook 可能修改后的 input messages(hook 可以改写、可以注入新消息),返回的 Msg 又会被 PostCall hook 包一层(可以全局改写最终回答)。 核心逻辑在ReActAgent.doCall方法中实现。
doCall()
doCall进来之后,有四个分支:
@Override
protected
}
- 分支 ① 是最常见路径:memory 干净,把用户消息入 memory,从 iteration 0 开始 reasoning。
- 分支 ② 是 HITL 恢复路径:用户上次暂停,这次空 call 进来 → 直接进 acting 把那批 pending ToolUse 跑掉。
- 分支 ③ 是用户主动注入 ToolResult 路径:典型场景是 ToolSuspendException 抛出后用户填表回填、或者用户想"伪造一个工具失败回复"让模型重新规划。validateAndAddToolResults 严格校验:ID 必须匹配 pending、不能重复、部分回填时不能夹带文本。
- 分支 ④ 兜底:进入这里说明 PendingToolRecoveryHook 被关掉而用户也没自己处理——直接报错而非"假装没看见"。
reasoning()
剥掉 reactor 噪音,逻辑大致是:
private
model
toolkit
options
event
}
acting()
private
toolkit
.
}
迭代终止与 summarizing()
ReAct 循环靠以下条件之一终止: - isFinished(msg) 为 true(reasoning 没产生任何 ToolUseBlock)→ 直接返回 reasoning msg; - event.isStopRequested()(PostReasoning 或 PostActing 的 stopAgent)→ 返回携带 _STOP_REQUESTED 的 msg; - 工具 throw ToolSuspendException → buildSuspendedMsg 返回 TOOL_SUSPENDED; - iter >= maxIters → 进入 summarizing(); - 中断 → 走 onErrorResume + handleInterrupt。 summarizing() 是强制收尾机制:
protected Mono<Msg> summarizing() {
List<Msg> messageList = prepareSummaryMessages(); // memory + 一条强制 USER 提示
return notifyPreSummaryHook(messageList, opts)
.flatMap(e -> streamAndAccumulateSummary(e.getInputMessages(), e.getEffectiveGenerateOptions())
.flatMap(msg -> notifyPostSummaryHook(msg, opts)
.map(post -> {
Msg finalMsg = post.getSummaryMessage()
.withGenerateReason(GenerateReason.MAX_ITERATIONS);
memory.addMessage(finalMsg);
return finalMsg;
})))
.onErrorResume(this::handleSummaryError);
}