React cn
ReAct(Reasoning + Acting)是 Jianmu 中最常用的 Agent 运行模式:LLM 观察当前状态,决定是调用工具还是给出最终答案;若有工具调用,则执行工具并将观察结果回传;如此循环直到任务完成。Jianmu 将这一模式封装在 create_react_node() 工厂函数中,只需提供模型客户端和工具列表即可获得完整的行为树子树。
行为树组装:三层嵌套的循环结构¶
create_react_node() 返回的是 LoopUntilSuccess 装饰器节点,其内部包含一个带记忆的 Sequence 组合节点。这个 Sequence 按顺序执行三个子节点,构成了 ReAct 循环的每一次迭代。
flowchart TD
LUS["LoopUntilSuccess<br/>max_iterations=N<br/>abort_condition: Token Budget"]
SEQ["Sequence (memory=True)<br/>ReActLoop"]
ALN["AgentLLMNode<br/>LLM 推理 → 发出工具调用 或 最终答案"]
TE["ToolExecutor<br/>读取 actions → 执行工具 → 写入观察"]
SC["StateCondition<br/>检查 done 标志"]
LUS --> SEQ
SEQ --> ALN
SEQ --> TE
SEQ --> SC
ALN -- "actions ≠ [] → done=false" --> TE
ALN -- "actions = [] → done=true" --> SC
TE -- "追加 tool observation 到 messages" --> SC
SC -- "done=false → FAILURE → LoopUntilSuccess 重试" --> LUS
SC -- "done=true → SUCCESS → 循环终止" --> LUS
Sequence 的 memory=True 确保三个子节点在多次迭代间保持状态连续性,每当 LoopUntilSuccess 触发重试时,行为树的 tick 从 Sequence 的第一个子节点重新开始。
工厂函数签名与参数映射¶
工厂接受丰富的可选参数,允许对模型行为、工具集合、执行约束和上下文构建进行精细控制。
| 参数 | 类型 | 默认值 | 作用 |
|---|---|---|---|
name |
str |
"ReActAgent" |
根节点名称,子节点以此为前缀自动命名 |
namespace |
Optional[str] |
None |
状态命名空间隔离 |
model_client |
ModelClient |
必填 | 统一的模型调用外观 |
tools |
Optional[Seq[Tool]] |
None |
静态工具实例列表 |
tool_providers |
Optional[Seq[ToolProvider]] |
None |
动态工具提供者(如 MCP) |
tool_provider_context |
Optional[Any] |
None |
传递给 ToolProvider 的上下文 |
tool_runner |
Optional[Any] |
None |
显式工具运行器覆盖 |
constraints |
Optional[Any] |
None |
Guard/执行约束 |
context_builder |
Optional[ContextBuilderProtocol] |
None |
Prompt 上下文构建器 |
config |
Optional[ReActConfig] |
None |
完整的 ReAct 配置对象 |
工具组装流程:静态 tools 与动态 tool_providers 收集的工具通过 ToolSet.from_providers().with_tools() 合并 —— 后注册的同名工具覆盖先注册的。合并后的工具集同时用于生成 LLM 的 function-calling schema 和人类可读的工具描述文本。
这里还有一个当前公开 API 需要说清的点:工厂接收的是已经解析好的 ModelClient,而不是裸 provider 对象。常见写法是 model_client=ModelClient.resolve(...),这样重试、fallback、流式合并和可观测链路才都走受支持的主路径。
ReActConfig:配置参数的全貌¶
ReActConfig 继承自 Pydantic BaseModel,所有字段都有基于 jianmu.yaml 的默认值解析机制。
| 字段 | 类型 | 默认解析源 | 说明 |
|---|---|---|---|
model |
str |
get_config().models.react |
模型名称(独立于 default model) |
temperature |
float |
get_config().llm.temperature |
采样温度 |
max_tokens |
Optional[int] |
get_config().llm.max_tokens |
单次响应的最大 token 数 |
top_p |
Optional[float] |
get_config().llm.top_p |
Nucleus 采样参数 |
top_k |
Optional[int] |
get_config().llm.top_k |
Top-K 采样参数 |
timeout |
float |
get_config().llm.timeout |
单次模型调用超时(秒) |
max_budget_tokens |
Optional[int] |
get_config().llm.max_budget_tokens |
整个循环的总 token 预算 |
max_iterations |
int |
get_config().limits.max_iterations |
最大迭代轮次 |
resilience |
ModelResilienceConfig |
默认实例 | 重试与 fallback 策略 |
system_prompt |
Optional[str] |
None |
覆盖系统提示词 |
tool_choice |
Optional[Any] |
None |
Provider 特定的工具选择指令,仅应用于每次 ReAct 运行的首轮模型调用 |
extra_params |
Dict[str, Any] |
{} |
传递给模型 API 的额外参数 |
keys |
StateKeys |
StateKeys() |
状态字段名映射 |
默认 jianmu.yaml 中 max_iterations 和 max_rounds 均为 10,意味着 ReAct 循环默认最多执行 10 轮工具调用迭代。
tool_choice 约束的是 ReAct 如何启动,而不是持续约束每一次模型请求。首个
逻辑轮次中的 Provider 重试与 fallback 仍会保留该选择;进入后续轮次后则不再
发送,让模型可以读取工具 observation,自主决定继续调用工具或输出最终答案。
为保持兼容,已有的 extra_params={"tool_choice": ...} 也采用相同的首轮语义,
但推荐新代码使用显式字段。该行为依赖 ReAct state contract:
keys.rounds 映射的字段必须使用 Ephemeral(scope="run"),从而让 fresh
execution 从零开始,同时让 checkpoint restore 保留已保存的值。
三节点协作:AgentLLMNode → ToolExecutor → StateCondition¶
第一站:AgentLLMNode —— 推理与工具决策¶
AgentLLMNode 继承自 SimpleLLMNode,在基础 LLM 调用之上增加了工具调用解析和循环感知能力。每次 update_async 被触发时,它执行以下流程:
- 上下文构建:从状态中读取
messages,通过ContextBuilder.resolve_for_llm(prefer_react=True)构建完整 prompt 上下文 —— 包含系统提示词、ReAct 协议提示词、工具描述和对话历史 - 模型调用:将工具 schema 作为
tools参数传入ModelConfig,调用模型推断 - 结果持久化:通过
_persist_result和_persist_usage将结果写入状态
_persist_result 是 ReAct 循环的核心状态转换逻辑:
# 关键决策:是否有工具调用?
actions = [extract_tool_call_from_dict(tc) for tc in response_msg.tool_calls]
updates = {
"actions": actions, # 工具调用列表 → ToolExecutor 读取
"rounds": rounds + 1, # 迭代计数递增
"done": not actions, # 无工具调用 = 任务完成
"final_answer": content if not actions else "", # 最终答案
}
当 LLM 发出工具调用时,done=False;当 LLM 认为任务完成时,done=True 且 final_answer 被填充。这个 done 标志正是下游 StateCondition 判断循环是否结束的依据。
第二站:ToolExecutor —— 工具执行与观测追加¶
ToolExecutor 读取 AgentLLMNode 写入的 actions 列表,逐个(或并行)执行工具调用,并将每个工具的返回结果作为 tool 角色消息追加到 messages 中。
执行模式选择(_resolve_execution_mode):
| 模式 | 触发条件 | 行为 |
|---|---|---|
serial |
默认 / 单工具 / 工具标记 non-parallel-safe | 逐个顺序执行 |
parallel |
execution_mode="parallel" 或自动检测到全部 parallel-safe |
asyncio.gather 并发执行 |
auto |
配置默认值 | 单工具 → serial;多工具且全部 parallel_safe → parallel |
Guard 预检机制(_preflight_actions):在工具实际执行之前,ToolExecutor 通过 GuardEnforcer 对每个待执行工具进行策略检查。如果返回 Decision.PENDING(需要审批),节点返回 Status.RUNNING 并触发挂起机制 —— 此时 LoopUntilSuccess 保持等待,直到审批回调恢复执行。
观测消息格式:每次工具执行结果被转换为 Message(role="tool", content=...) 追加到会话历史中,确保下一轮 LLM 调用能看到所有之前的工具执行结果。
工具效应聚合(_write_tool_effects):执行后收集每个工具的 effect_tags,合并到 tool_effects 状态字段中,用于下游节点或约束检查。
第三站:StateCondition —— 完成的守卫¶
StateCondition 是 ReAct 循环的终止判断节点。它通过 predicate 模式检查状态中的 done 标志:
StateCondition(
predicate=lambda state: bool(
getattr(state, keys.done, None)
or (getattr(state, "model_extra", None) or {}).get(
f"{namespace}.{keys.done}" if namespace else keys.done
)
),
)
当 done=True → Status.SUCCESS → Sequence 成功 → LoopUntilSuccess 成功退出。
当 done=False → Status.FAILURE → Sequence 失败 → LoopUntilSuccess 调度下一轮迭代。
该 predicate 同时检查 Pydantic 模型的主字段和 model_extra(extra="allow" 时的溢出存储),确保在启用命名空间隔离时也能正确读取 done 标志。
LoopUntilSuccess:循环驱动引擎¶
LoopUntilSuccess 是 Jianmu 自定义的 py_trees Decorator 节点,位于 jianmu/node/composites.py。它将子节点的 FAILURE 转换为重试信号,是 ReAct 循环的物理执行引擎。
核心状态机¶
stateDiagram-v2
[*] --> Initialising: 首次 tick
Initialising --> Evaluating: iteration_count=0
state Evaluating {
[*] --> CheckAbort
CheckAbort --> ChildRunning: abort_condition()=False
CheckAbort --> Failed: abort_condition()=True
ChildRunning --> ChildSuccess: child=SUCCESS
ChildRunning --> ChildFailure: child=FAILURE
ChildRunning --> ChildRunning: child=RUNNING
ChildSuccess --> [*]: return SUCCESS
ChildFailure --> CheckBudget: iteration_count++
}
CheckBudget --> Retry: iteration_count < max_iterations
CheckBudget --> Failed: iteration_count >= max_iterations
Retry --> Evaluating: signal() → 重新 tick
Failed --> [*]: return FAILURE
三个关键机制¶
1. 失败即重试:当子节点(Sequence)返回 FAILURE 时,LoopUntilSuccess.update() 将子节点状态重置为 INVALID,调用 state_manager.signal() 唤醒 ReactiveRunner 进行下一轮 tick,返回 RUNNING 状态。这确保了行为树引擎不会将子节点失败视为循环的最终失败。
2. 迭代上限:max_iterations(默认 10)防止无限循环。当迭代次数超过上限,装饰器写入终止元数据到 InteractionKeys.TERMINATION 并返回 FAILURE。测试验证了这一行为:
# 来自 test_react.py 的集成测试
root = LoopUntilSuccess("agent", max_iterations=10, child=loop_body)
runner = ReactiveRunner(root, state)
await runner.run(max_ticks=20)
# 验证: 两轮迭代后 done=true, rounds=2, 4条消息(用户+AI+工具+AI)
3. Token 预算中止:create_react_node() 在 LoopUntilSuccess 上注入了一个 abort_condition:
def _check_budget_with_sm() -> bool:
sm = loop_node.state_manager
current_usage = sm.get(keys.usage, namespace=namespace) or {}
total_tokens = current_usage.get("total_tokens", 0)
if total_tokens >= config.max_budget_tokens:
return True # 触发中止
return False
每次 tick 之前,LoopUntilSuccess.update() 先检查 abort_condition。若累计 token 超过 max_budget_tokens,循环立即终止并返回 FAILURE,同时记录错误日志。Token 使用量由 AgentLLMNode._persist_usage() 跨轮次累积,存储在 StateKeys.usage(默认 "llm.usage")字段中。
状态契约:ReAct 循环中的数据流¶
ReAct 循环依赖一组约定的状态字段,由 StateKeys 定义并在 ReAct 宿主相关节点间共享。用户定义的 State Schema 必须与这些键名对齐;在公开 API 里,这些名字通常通过 ReActConfig.keys / ToolExecutorConfig.keys 路由,而不是把原始字符串分散写在各节点里。
| 状态字段 | 默认键名 | 写入者 | 读取者 | 生命周期 |
|---|---|---|---|---|
messages |
"messages" |
AgentLLMNode, ToolExecutor | AgentLLMNode(上下文构建) | 持久(Reducer: operator.add) |
actions |
"actions" |
AgentLLMNode | ToolExecutor | Ephemeral(scope="step") |
done |
"done" |
AgentLLMNode, ToolExecutor | StateCondition | Ephemeral(scope="run") |
text_output |
"text_output" |
LLM 类节点 | 可选下游读取方 | 视节点用法而定 |
final_answer |
"final_answer" |
AgentLLMNode, ToolExecutor | 调用方 | Ephemeral(scope="run") |
skill_result |
"skill_result" |
BT skill runtime | skill-aware 调用方 | 常见配置中为 Ephemeral(scope="run") |
rounds |
"rounds" |
AgentLLMNode | 遥测/日志 | Ephemeral(scope="run") |
usage |
"llm.usage" |
AgentLLMNode | abort_condition | Ephemeral(scope="run") |
streaming_output |
"streaming_output" |
AgentLLMNode | UI 层 | Ephemeral(scope="run") |
tool_effects |
"tool_effects" |
ToolExecutor | Guard 检查 | 持久 |
关键设计:actions 使用 Ephemeral(scope="step") 确保每轮迭代后自动清零,避免 ToolExecutor 重复执行旧调用。done、final_answer 和 rounds 使用 Ephemeral(scope="run"),在每次 fresh Agent.run() 前重置 execution-scoped 状态。rounds 的生命周期属于 ReAct 必须遵守的契约,因为 tool_choice 等首轮行为依赖 rounds == 0。
典型 State Schema 示例¶
来自官方 Demo 的标准 ReAct 状态定义:
class ReActState(BaseModel):
task: Optional[str] = None
messages: Annotated[List[Message], operator.add] = Field(default_factory=list)
done: Annotated[bool, Ephemeral(scope="run")] = False
final_answer: Annotated[Optional[str], Ephemeral(scope="run")] = None
actions: Annotated[List[ToolCall], Ephemeral(scope="step")] = Field(default_factory=list)
rounds: Annotated[int, Ephemeral(scope="run")] = 0
streaming_output: Annotated[str, Ephemeral(scope="run")] = ""
messages 字段使用 operator.add 作为 Reducer,确保每次 append_port_messages 调用将新消息追加到列表末尾而非覆盖。
完整调用链:从工厂到运行¶
sequenceDiagram
participant User as 调用方
participant Factory as create_react_node()
participant LUS as LoopUntilSuccess
participant ALN as AgentLLMNode
participant TE as ToolExecutor
participant SC as StateCondition
participant State as StateManager
User->>Factory: create_react_node(model_client, tools, config)
Factory->>Factory: ToolSet.merge(static + providers)
Factory->>Factory: tools_desc = toolset.describe()
Factory->>Factory: tools_schema = toolset.schemas()
Factory->>ALN: AgentLLMNode(name, model_client, tools_schema, tools_desc, ...)
Factory->>TE: ToolExecutor(name, tools, constraints, ...)
Factory->>SC: StateCondition(predicate=lambda state: bool(state.done))
Factory->>LUS: LoopUntilSuccess(max_iterations, abort_condition)
Factory-->>User: 返回 LoopUntilSuccess 根节点
Note over User,State: 运行时循环(每一轮迭代)
LUS->>ALN: tick → update_async()
ALN->>State: 读取 messages
ALN->>ALN: ContextBuilder.build() → 完整 prompt
ALN->>ALN: model_client.invoke(messages, config)
ALN->>State: 写入 actions, rounds, done, final_answer, usage, messages
ALN-->>LUS: SUCCESS
LUS->>TE: tick → update_async()
TE->>State: 读取 actions
TE->>TE: _preflight_actions() → Guard 检查
TE->>TE: _execute_actions_serial/parallel()
TE->>State: 写入 tool observation messages + tool_effects
TE-->>LUS: SUCCESS
LUS->>SC: tick → update()
SC->>State: 读取 done
alt done == true
SC-->>LUS: SUCCESS → 循环结束
else done == false
SC-->>LUS: FAILURE
LUS->>LUS: iteration_count++, signal()
LUS-->>LUS: 返回 RUNNING → 下一轮
end
工具提供者集成:静态 + 动态工具装配¶
create_react_node() 支持两种工具提供方式并存:
root = create_react_node(
model_client=ModelClient.resolve(env_override=True),
tools=[CalculatorTool()], # 静态工具
tool_providers=[CalculatorToolProvider()], # 动态提供者
)
ToolProvider 协议定义了三个生命周期方法:
- initialize() → 初始化资源(如 MCP 连接)
- get_tools(**kwargs) → 返回当前可用工具列表
- close() → 释放资源
工具装配顺序:先收集所有 provider 的工具(后注册覆盖同名),再合并静态工具(静态工具优先级最高)。这使得 MCP 远程工具和内建工具可以无缝混合使用。
上下文构建器:Prompt 装配策略¶
ReAct 节点的上下文构建器由 AgentLLMNode._resolve_context_builder() 选择,调用 ContextBuilder.resolve_for_llm(prefer_react=True):
- 若提供了显式
context_builder→ 直接使用 - 若运行时提供了
prompt_runtime→ 使用运行时 prompt - 否则 → 使用默认 ReAct 预设,自动组合
persona_default_prompt+react_protocol_prompt+ 工具描述
默认 ReAct 协议的核心理念是结构化函数调用——不要求 LLM 手动格式化工具调用文本,而是依赖 OpenAI-compatible function calling 机制:
"When you need to use a tool, the system will handle the tool calling automatically. You do NOT need to format tool calls as text."
当提供显式 context_builder 时,system_prompt 参数被忽略 —— 由 context builder 全权负责 prompt 装配。
遥测事件:可观测的 ReAct 循环¶
ReAct 循环中的每个关键阶段都会发出运行时事件,供遥测系统(TelemetryHub)消费:
| 事件 | 发出者 | 触发时机 |
|---|---|---|
reply.started |
AgentLLMNode | LLM 调用开始 |
model.call.started |
AgentLLMNode | 模型 API 调用开始 |
text.delta |
AgentLLMNode | 流式输出增量更新 |
model.call.completed |
AgentLLMNode | 模型调用完成(含 token 用量) |
reply.completed |
AgentLLMNode | 本轮推理完成 |
tool.call.started |
ToolExecutor | 工具调用开始 |
tool.call.completed |
ToolExecutor | 工具调用成功完成 |
tool.call.failed |
ToolExecutor | 工具调用失败 |
此外,LoopUntilSuccess 在达到最大迭代次数时写入 InteractionKeys.TERMINATION 元数据,使主机/运行时可以检测到异常终止。
使用示例:三行代码启动 ReAct Agent¶
最简用法:
from jianmu.node.presets import create_react_node
from jianmu import Agent
from jianmu.config import ReActConfig
root = create_react_node(
model_client=my_model_client,
tools=[CalculatorTool()],
)
agent = Agent(root, state_schema=ReActState)
result = await agent.run(input_data={"messages": [human("What is 25 * 4 + 10?")]})
复杂场景 —— 多工具 + 自定义系统提示 + Token 预算:
root = create_react_node(
name="ResearchAgent",
model_client=model_client,
tools=[CalculatorTool(), DuckDuckGoSearchTool(), WeatherTool()],
config=ReActConfig(
model="gpt-4o",
max_iterations=15,
max_budget_tokens=32000,
system_prompt="You are a research assistant. Always verify facts with tools.",
tool_choice={
"type": "function",
"function": {"name": "duckduckgo_search"},
},
),
)
与相邻节点的关系¶
ReAct 节点工厂依赖以下核心组件,它们的详细文档见各自专题:
- LLM 节点:AgentLLMNode 与 SimpleLLMNode:AgentLLMNode 的内部实现、SimpleLLMNode 的区别及上下文构建细节
- 工具与技能节点:ToolExecutor、SkillNode 与约束联动:ToolExecutor 的 Guard 集成、并行执行策略与 ToolNode 对比
- Guard 体系:工具策略检查、预算控制、频率限制与确认拦截:
preflight_actions中的 Guard 检查与审批挂起机制 - Plan-Execute 节点工厂:另一种预设模式,适用于需要先规划后执行的多步骤任务
- 行为树执行内核:LoopUntilSuccess 继承的 Decorator 基类和 py_trees 扩展机制
- ReactiveRunner:事件驱动的异步 tick 调度:
signal()机制如何驱动循环的下一轮 tick