LangChain ReAct 的两层架构-周红伟
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 的核心价值。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)