跳转至

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 被触发时,它执行以下流程:

  1. 上下文构建:从状态中读取 messages,通过 ContextBuilder.resolve_for_llm(prefer_react=True) 构建完整 prompt 上下文 —— 包含系统提示词、ReAct 协议提示词、工具描述和对话历史
  2. 模型调用:将工具 schema 作为 tools 参数传入 ModelConfig,调用模型推断
  3. 结果持久化:通过 _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 节点工厂依赖以下核心组件,它们的详细文档见各自专题: