LangChain ReAct 的两层架构-周红伟

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

一、LangChain ReAct 的两层架构

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

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

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

python 复制代码
# 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() 的精简版------这是循环体的核心:

python 复制代码
# 来自 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() 方法中:

python 复制代码
# 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:

python 复制代码
# 来自 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 机制实现的:

python 复制代码
# 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 最小示例

python 复制代码
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 的核心价值。

相关推荐
云和数据.ChenGuang5 分钟前
Python 重写(Override)深度解析:从 MRO 到 super() 的完整机制
java·开发语言·人工智能·python·langchain·1024程序员节
晶捷软件1 小时前
从 IT 架构视角看 ERP 选型:用友、金蝶与鼎捷生态(含晶捷智能)的技术差异
架构·智能制造·erp·制造业信息化
@大迁世界1 小时前
Firefox 本地模式:localhost 再见了 ?
前端·架构
深蓝AI1 小时前
总榜第一不等于最划算:Intelligence Index v5 换代后,模型选型要算的三笔账
程序员·架构
天远数科1 小时前
零信任架构实战:基于天远企业股权变更构建自动化商业银行对公授信存续期股权异动与资产抽逃预警网关
运维·人工智能·架构·自动化
AI你一生一世2 小时前
当“暂停“成为架构决策:从签证系统停摆看高可用设计的边界
架构·系统架构·流量控制·幂等性·高可用设计·优雅降级·状态一致性
云和数据.ChenGuang4 小时前
Python 单继承与多继承深度剖析:从 MRO 到实战取舍
开发语言·人工智能·python·langchain
云宏信息7 小时前
超融合和存算分离怎么选?从性能、可靠性、扩展、成本四个维度讲述如何精准匹配架构选型 | 附企业降本轻量云方案
运维·服务器·架构·云计算
全栈偏科生7 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
langchain
snpgroupcn7 小时前
能源行业SAP系统升级的技术约束分析:资产数据连续性与切换窗口的工程处理(上篇)
架构·sap