15-08 AgentScope Java中ReactAgent的实现原理
✅AgentScope Java中ReactAgent的实现原理
ReAct 的论文核心思想就一句话:让 LLM 交替”思考(Reasoning)”和”行动(Acting)”,直到得出最终答案。落到工程里要解决五个问题:
怎么把 LLM 决定的工具调用真正执行掉(reasoning → acting)
怎么把工具结果再喂回 LLM(acting → reasoning)
什么时候停(max_iters / 没有 tool_use / HITL stop / 完成)
流式输出怎么保证既能展示给用户、又能在结束时拼成完整 Msg
任意一步暂停或崩溃后,下次能从中间状态恢复
ReActAgent 的所有复杂度都是在回答这五个问题。
Builder装配
要了解ReActAgent的实现原理,需要从他的build方法讲起:
1 | public ReActAgent build() { |
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
1 | public final Mono<Msg> call(List<Msg> msgs) { |
这条链定义了 所有 Agent 的统一行为契约。ReActAgent.doCall 拿到的是 PreCall hook 可能修改后的 input messages(hook 可以改写、可以注入新消息),返回的 Msg 又会被 PostCall hook 包一层(可以全局改写最终回答)。
核心逻辑在ReActAgent.doCall方法中实现。
doCall()
doCall进来之后,有四个分支:
1 |
|
分支 ① 是最常见路径:memory 干净,把用户消息入 memory,从 iteration 0 开始 reasoning。
分支 ② 是 HITL 恢复路径:用户上次暂停,这次空 call 进来 → 直接进 acting 把那批 pending ToolUse 跑掉。
分支 ③ 是用户主动注入 ToolResult 路径:典型场景是 ToolSuspendException 抛出后用户填表回填、或者用户想”伪造一个工具失败回复”让模型重新规划。validateAndAddToolResults 严格校验:ID 必须匹配 pending、不能重复、部分回填时不能夹带文本。
分支 ④ 兜底:进入这里说明 PendingToolRecoveryHook 被关掉而用户也没自己处理——直接报错而非”假装没看见”。
reasoning()
剥掉 reactor 噪音,逻辑大致是:
1 | private Mono<Msg> reasoning(int iter, boolean ignoreMaxIters) { |
acting()
1 | private Mono<Msg> acting(int iter) { |
迭代终止与 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() 是强制收尾机制:
1 | protected Mono<Msg> summarizing() { |
