LangChain 的 ReAct 实现是学习这个模式很好的教材,因为它的代码结构清晰,同时暴露了一些设计上的取舍。我从源码层面来分解。

一、LangChain ReAct 的两层架构

LangChain 把 ReAct 拆成了两个正交的抽象:

AgentExecutor(执行器:负责"怎么跑")
    └── Agent(代理:负责"下一步做什么")
         └── LLM

Agent 只管一件事:拿到当前状态,输出下一步动作。它是一个函数签名:

# Agent 的核心接口(简化)
Agent.plan(
    intermediate_steps: List[Tuple[AgentAction, str]],  # 历史步骤
    callbacks: Callbacks,
    **kwargs
) -> Union[AgentAction, AgentFinish]
  • 返回 AgentAction → 执行器去调工具
  • 返回 AgentFinish → 循环结束,返回最终答案

AgentExecutor 是那个 while 循环,调用 Agent.plan(),执行工具,把结果喂回去,再调 Agent.plan(),直到 Agent 说"完了"。

这跟 OpenClaw 的 runEmbeddedAttempt(循环体)和 Agent 逻辑分离是同一个思路。

二、核心循环源码拆解

以下是 LangChain AgentExecutor._take_next_step() 的精简版——这是循环体的核心:

# 来自 langchain/agents/agent.py 的 AgentExecutor 类

def _take_next_step(
    self,
    name_to_tool_map: Dict[str, BaseTool],
    color_mapping: Dict[str, str],
    inputs: Dict[str, str],
    intermediate_steps: List[Tuple[AgentAction, str]],
    run_manager: Optional[CallbackManagerForAgentRun] = None,
) -> Union[AgentFinish, List[Tuple[AgentAction, str]]]:
    
    # ===== 第一步:调用 Agent,让它决定下一步 =====
    output = self.agent.plan(
        intermediate_steps,
        callbacks=run_manager.get_child() if run_manager else None,
        **inputs,
    )
    
    # ===== 第二步:判断是结束还是继续 =====
    if isinstance(output, AgentFinish):
        # Agent 说任务完成,返回最终结果
        return output
    
    # ===== 第三步:Agent 要求执行工具 =====
    actions: List[AgentAction]
    if isinstance(output, AgentAction):
        actions = [output]
    else:
        actions = output  # 可能返回多个 action 并行执行
    
    result = []
    for agent_action in actions:
        # 找到对应工具
        tool = name_to_tool_map[agent_action.tool]
        
        # 执行工具并获取观察结果
        observation = tool.run(
            agent_action.tool_input,
            callbacks=run_manager.get_child() if run_manager else None,
        )
        
        # 记录这一步的 (动作, 结果) 对
        result.append((agent_action, observation))
    
    return result

外层循环在 _call() 方法中:

# AgentExecutor._call() 的核心循环
async def _acall(self, inputs: Dict[str, str]) -> Dict[str, Any]:
    # 初始化
    intermediate_steps: List[Tuple[AgentAction, str]] = []
    
    # 主循环
    while self._should_continue(iterations, time_elapsed):
        # 执行一步
        next_step_output = self._take_next_step(
            name_to_tool_map, color_mapping, inputs, intermediate_steps
        )
        
        # 如果是 AgentFinish,结束循环
        if isinstance(next_step_output, AgentFinish):
            return self._return(next_step_output, intermediate_steps)
        
        # 否则,把新的 (action, observation) 追加到历史
        intermediate_steps.extend(next_step_output)
        iterations += 1
    
    # 达到最大步数限制
    return self._return(
        AgentFinish(
            return_values={"output": "Agent stopped due to max iterations."}
        ),
        intermediate_steps,
    )

三、ReAct 提示词构造:如何让 LLM 遵守格式

关键在于 LangChain 的 ReAct prompt 模板。它把工具描述、历史步骤、当前查询组装成一个严格格式的 prompt:

# 来自 langchain/hub 的 hwchase17/react 提示词模板

REACT_PROMPT = """Answer the following questions as best you can. 
You have access to the following tools:

{tools}

Use the following format:

Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question

Begin!

Question: {input}
Thought: {agent_scratchpad}"""

agent_scratchpad 是 LangChain 填进去的历史步骤,格式是严格的:

Thought: 我需要先查数据库
Action: database_lookup
Action Input: "user_id: 12345"
Observation: 查询失败:连接超时

Thought: 主库超时了,试试缓存
Action: cache_lookup
Action Input: "user_id: 12345"
Observation: {"name": "张三", "email": "zhang@example.com"}

Thought: 数据拿到了,但还需要确认邮箱是否有效
Action: email_validator
Action Input: "zhang@example.com"
Observation: 邮箱有效

Thought: 现在我有所有需要的信息了
Final Answer: 用户张三,邮箱 zhang@example.com,状态正常

通过这种固定的文本格式,LangChain 把 Agent 的"记忆"和"推理链"直接写进了 prompt,让 LLM 在每次调用时都能看到完整的"思考-行动-观察"历史。

四、工具错误处理:LangChain 的关键设计

你上一个问题提到的"工具失败后切换"场景,在 LangChain 中是通过 ToolException 机制实现的:

# LangChain 工具定义示例
from langchain.tools import Tool, tool

@tool
def database_lookup(query: str) -> str:
    """查询主数据库。输入 SQL 查询语句。"""
    try:
        result = db.execute(query)
        return str(result)
    except ConnectionError:
        # 关键:返回错误信息文本,而不是抛出异常
        return "错误:数据库连接超时,主库不可用"

@tool
def cache_lookup(key: str) -> str:
    """查询 Redis 缓存。输入缓存键。"""
    try:
        result = redis.get(key)
        if result is None:
            return "缓存未命中,无此数据"
        return result
    except Exception as e:
        return f"缓存查询失败:{str(e)}"

工具不抛异常,而是把错误信息作为普通文本返回。这个设计极为重要:

做法 结果
抛异常 AgentExecutor 捕获异常,可能直接终止或重试同一工具
返回错误文本 LLM 看到错误信息,自己决定下一步策略

因为循环体把错误文本当成普通的 Observation 追加到上下文,LLM 在下一轮推理时就能看到"哦,主库超时了",然后自己决定"那我试试缓存"。

五、LangChain ReAct 与 OpenClaw 的对比

维度 LangChain OpenClaw
循环实现 AgentExecutor 显式 while 循环 同样的 while 循环,在 runEmbeddedAttempt
Agent 抽象 Agent.plan() 返回 Action/Finish 直接通过 tool_use stop_reason 判断
历史传递 文本格式的 agent_scratchpad messages 数组中的 tool_use/tool_result
工具定义 Python 类 + 装饰器 JSON Schema + handler 函数
错误恢复 工具返回错误文本,LLM 自主决策 相同策略
并行工具 通过返回 List[AgentAction] 支持 通过 Promise.all 并行执行

核心哲学是一致的:循环体极简,决策权交给 LLM,错误信息透明传递给模型。

六、一个完整的 LangChain ReAct 最小示例

from langchain.agents import AgentExecutor, create_react_agent
from langchain.tools import tool
from langchain_openai import ChatOpenAI

# 定义工具
@tool
def search_database(query: str) -> str:
    """搜索主数据库"""
    return "错误:连接超时"

@tool
def search_backup(query: str) -> str:
    """搜索备份数据库"""
    return "错误:权限不足"

@tool
def search_cache(query: str) -> str:
    """搜索缓存"""
    return "找到:用户张三,邮箱 zh@test.com"

tools = [search_database, search_backup, search_cache]

llm = ChatOpenAI(model="gpt-4o", temperature=0)

agent = create_react_agent(llm, tools, prompt=REACT_PROMPT)
executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,      # 打印每一步的 Thought/Action/Observation
    max_iterations=10, # 防止无限循环
    handle_parsing_errors=True,
)

result = executor.invoke({"input": "查询用户 12345 的邮箱"})

执行时 verbose 输出会是这样的:

> Entering new AgentExecutor chain...
Thought: 我需要查询用户 12345 的邮箱,先尝试主数据库
Action: search_database
Action Input: "user_id: 12345"
Observation: 错误:连接超时

Thought: 主库超时了,试试备份库
Action: search_backup
Action Input: "user_id: 12345"  
Observation: 错误:权限不足

Thought: 备份也没权限,最后试试缓存
Action: search_cache
Action Input: "user_id: 12345"
Observation: 找到:用户张三,邮箱 zh@test.com

Thought: I now know the final answer
Final Answer: 用户 12345 是张三,邮箱为 zh@test.com

> Finished chain.

这就是你之前问的那个"三次工具调用、两次失败、最终成功"场景在 LangChain 中的完整演绎。循环结构让模型在每一次失败后都有机会重新思考和调整策略,这正是 ReAct 的核心价值。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐