第 02 步:ReAct 与工具调用¶
这一节学什么¶
create_react_node(...)的核心链路action在 ReAct 里是什么意思- 怎么把 tool 注入 Agent
actions / messages / done / final_answer / rounds是怎么演化的
你会运行什么¶
示例代码:
examples/getting_started/step_02_react_with_tool.py
运行:
核心概念¶
这一节第一次进入 jianmu 最典型的主路径:
- LLM 决定是否调用工具
- ToolExecutor 执行工具
- 工具观察结果回到
messages - 最终收敛到
done=True和final_answer
如果你之前只看过最小聊天,这一步是第一次看到 Agent 真正变成“可行动”的运行时。
先说什么是 action¶
这一节第一次出现 actions 字段。
你可以先把它理解成:
- 模型当前这一步产出的“工具调用意图”
它还不是最终答案,而是:
- “我下一步想调用哪个 tool”
- “我准备传什么参数”
后面 ToolExecutor 会消费这些 actions,真正去执行工具。
所以在 ReAct 链路里,一个很重要的区别是:
Step 02的最小聊天:模型直接生成回复Step 03的 ReAct:模型可能先生成actions,再进入工具执行
为什么 ReActState 是这四个字段¶
这个示例的 State 是:
class ReActState(BaseModel):
messages: Annotated[list[Message], operator.add] = Field(default_factory=list)
actions: Annotated[list[ToolCall], Ephemeral(scope="step")] = Field(default_factory=list)
done: Annotated[bool, Ephemeral(scope="run")] = False
final_answer: Annotated[str | None, Ephemeral(scope="run")] = None
rounds: Annotated[int, Ephemeral(scope="run")] = 0
这五个字段不是随便选的,它们正好对应 ReAct preset 的默认状态协议。
messages¶
messages 是对话历史,也是 LLM 每一轮看到的主要输入。
它会包含:
- user message
- assistant message
- assistant tool call
- tool observation
- 最终回答
如果少了 messages,LLM 节点就没有稳定的对话入口,tool observation 也没有地方回流。
actions¶
actions 是 LLM 到 ToolExecutor 的中间交接字段。
LLM 节点从 assistant response 里解析 tool call,然后写到 actions。
ToolExecutor 再读取 actions,真正执行对应工具。
这里把它标成:
意思是它只在当前 step 内有效。 工具执行完以后,下一次 step 开始前会被清空,避免旧 tool call 被重复执行。
如果少了 actions,模型即使产出了 tool call,ToolExecutor 也拿不到要执行的动作。
如果它不是 step-scoped,就可能在下一轮被重复消费。
done¶
done 是 ReAct loop 的完成信号。
create_react_node(...) 里有一个 completion check:
如果少了 done,loop 不知道什么时候该停,只能跑到 max_iterations。
如果字段名改了,但没有同步配置 keys,completion check 也会找不到它。
final_answer¶
final_answer 是 ReAct 默认的最终文本结果槽。
在常见的文本回答路径里,LLM 节点会在完成时同时写:
done = Truefinal_answer = ...
这两个字段职责不同:
done负责停机final_answer负责承载最终文本
如果你的链路最终结果不是文本,而是别的结构化产物,那么是否写 final_answer 要看具体节点协议;
但对标准 ReAct 聊天链路来说,它通常仍然会有这个字段。
如果字段名改了,但没有同步配置 keys,最终文本结果也会写不到你预期的位置。
rounds¶
rounds 用来记录 ReAct/LLM 交互轮次。
它不是控制链路必须依赖的最核心字段,但对理解运行过程很有用。 你可以通过它看到模型到底调用了几轮,而不是误把行为树 tick 次数当成 LLM 轮次。
如果想改字段名怎么办¶
可以改,但要同步告诉 ReAct preset 新的 state keys。
默认情况下,ReActConfig.keys 使用的是:
messages -> "messages"
actions -> "actions"
final_answer -> "final_answer"
done -> "done"
rounds -> "rounds"
如果你的 State 想改成别的名字,比如 chat_history、pending_tools、answer,就需要传入自定义 StateKeys。
否则会出现一种典型问题:
- State 里有字段
- 但节点还在按默认名字读写
- 最后表现为工具不执行、final answer 不回写、loop 无法结束
入门阶段建议先保留这四个默认字段名。
等你理解了 ReAct 主路径,再考虑自定义 ReActConfig.keys。
如果确实要改,State 字段和 ReActConfig.keys 要成对修改。
例如你想把默认字段改成:
messages -> chat_history
actions -> pending_tool_calls
done -> completed
final_answer -> answer
rounds -> turns
可以这样写:
from jianmu.config import ReActConfig, StateKeys
class CustomReActState(BaseModel):
chat_history: Annotated[list[Message], operator.add] = Field(default_factory=list)
pending_tool_calls: Annotated[list[ToolCall], Ephemeral(scope="step")] = Field(default_factory=list)
completed: Annotated[bool, Ephemeral(scope="run")] = False
answer: Annotated[str | None, Ephemeral(scope="run")] = None
turns: Annotated[int, Ephemeral(scope="run")] = 0
root = create_react_node(
model_client=model_client,
tools=[CalculatorTool()],
config=ReActConfig(
keys=StateKeys(
messages="chat_history",
actions="pending_tool_calls",
done="completed",
final_answer="answer",
rounds="turns",
)
),
)
这段配置的意思是:
- LLM 节点从
chat_history读写消息 - LLM 节点把 tool call 写入
pending_tool_calls ToolExecutor从pending_tool_calls读取并执行工具- loop 用
completed判断是否完成 - LLM 的最终文本结果写到
answer - 轮次计数写到
turns
也就是说,StateKeys 不是额外状态,而是告诉内置节点“应该去哪个 State 字段读写”。
create_react_node(...) 在帮你装什么¶
create_react_node(...) 不是普通 helper,它其实在帮你快速装配一条标准 ReAct 主路径。
你可以先把它理解成至少做了 4 件事:
- 创建一个 LLM 节点,负责决定下一步是直接回答,还是先调工具
- 创建一个
ToolExecutor,负责执行模型产出的actions - 把 tool observation 回写到
messages - 用 loop 控制整条链路,直到
done=True或达到终止条件
也就是说,这一节不是简单地“给 LLM 加一个 tool 列表”,而是在第一次引入一条真正的 agent loop。
max_iterations 是什么¶
这个例子里显式传了:
它表示:
- 这条 ReAct loop 最多运行多少轮
这是一个非常重要的边界,因为 ReAct 不是一次性调用,而是:
- 规划
- 调工具
- 看 observation
- 再继续规划
如果没有类似 max_iterations 的限制,这条链路就可能无限循环。
默认 ReAct prompt 在这里怎么工作¶
这个例子虽然没有显式传 system_prompt,但 create_react_node(...) 仍然会给这条链路补上默认 ReAct prompt。
你现在可以先记住新的语义:
- 如果
system_prompt is None - 使用默认 ReAct prompt
- 如果你显式传了
system_prompt - 就表示你接管这一层 prompt,不再自动追加默认 ReAct prompt
这一节先知道“默认 ReAct prompt 会不会进入主路径”就够了。 更完整的 prompt 装配关系,下一节再展开。
运行后观察什么¶
Tool trace会展示 assistant 发出的 tool call,以及 tool observationmessages里会追加 tool observationrounds会增长,表示 ReAct 循环轮次- 结束时
done会变成True final_answer不是一开始就有,而是多轮推理后收敛出来
这个例子里建议你特别看三样东西:
Tool traceRoundsFinal answer
因为它们分别对应:
- 工具调用与 observation 回流
- 循环次数
- 最终收敛结果
注意:示例 State 里仍然有 actions 字段,但它是 Ephemeral(scope="step") 的临时字段。
它用于在单个 step 内把 LLM 产出的 tool call 交给 ToolExecutor 消费。
运行结束后它通常已经被清空,所以这个示例改为从 messages 中打印工具调用轨迹。
为什么日志里 tick 次数比 rounds 多¶
运行这个示例时,你可能会看到类似:
[Tick 1] Root Status: RUNNING
[Tick 2] Root Status: RUNNING
Iteration 1 incomplete; continuing to next round
...
[Tick 7] Root Status: SUCCESS
Rounds: 2
这不是异常。
要先区分两个概念:
tick是行为树调度次数rounds是 ReAct/LLM 交互轮次
create_react_node(...) 生成的不是一个单函数,而是一棵行为树:
LoopUntilSuccess(ReActAgent)
Sequence(ReActLoop)
AgentLLMNode
ToolExecutor
StateCondition(CheckCompletion)
所以一次 ReAct 运行通常会被拆成多个 tick:
- LLM 节点开始异步生成,root 处于
RUNNING - LLM 返回 tool call,
ToolExecutor执行工具 - 当前轮还没有
final_answer,进入下一轮 - LLM 带着 tool observation 再生成最终答案,并把
done置为True CheckCompletion看到done=True,root 变成SUCCESS
因此,Tick 7 不表示模型调用了 7 轮。
真正更接近“模型交互轮次”的是 Rounds: 2。
为什么日志里会说 Iteration incomplete¶
你还可能看到:
这里的 incomplete 不是工具执行失败,也不是任务失败。
在 ReAct loop 里,如果某一轮还没有把 done 置为 True,CheckCompletion 会返回 failure。
外层 LoopUntilSuccess 会把这个 failure 当成“本轮还没完成”,然后继续下一轮。
所以这句话更准确的含义是:
如果最后 root status 是 SUCCESS,并且有 final_answer,说明整个 ReAct 链路是正常完成的。
这一步学完后应该会什么¶
你应该能回答:
action在 ReAct 里是什么意思create_react_node(...)在主路径里帮你装了什么- 为什么 ReAct 需要
max_iterations - 为什么 ReAct 比最小聊天多了一层“规划 -> 工具执行 -> observation 回流”的机制
下一步¶
下一节回答一个常见问题:prompt 到底从哪里来。