ReAct(Reasoning + Acting)是一种让 LLM 交替进行推理和执行动作的范式。这篇文章从零开始搭建一个可用的 Agent。
什么是 ReAct?
核心思想:不直接生成最终答案,而是生成一条 Thought → Action → Observation 链。每个 Thought 推理当前状态,每个 Action 调用工具,每个 Observation 提供新信息。
Thought-Action-Observation 循环
问题:东京人口乘以 3 是多少?
Thought 1: 我需要先查找东京人口,然后乘以 3。让我搜索一下。
Action 1: search("东京人口 2024")
Observation 1: 东京都人口约 1400 万。
Thought 2: 现在我有人口数据(14,000,000),需要乘以 3。
Action 2: calculate("14000000 * 3")
Observation 2: 42,000,000
Thought 3: 答案已得出。东京人口 × 3 = 4200 万。
Answer: 42,000,000
工具注册
每个工具定义名称、描述和参数 schema:
tools = {
"search": {
"description": "搜索互联网获取信息",
"parameters": {"query": "string - 搜索关键词"},
"fn": web_search
},
"calculate": {
"description": "计算数学表达式",
"parameters": {"expression": "string - 数学表达式"},
"fn": safe_eval
},
"code_execute": {
"description": "执行 Python 代码片段",
"parameters": {"code": "string - Python 代码"},
"fn": run_python
}
}
Prompt 设计
系统 prompt 是 Agent 表现的关键。它需要清晰定义工具格式并提供良好的推理链示例。一个设计良好的 prompt 在实践中可以将幻觉动作减少约 40%。
SYSTEM_PROMPT = """你是一个能够使用工具解决问题的 Agent。
你可以使用以下工具:
{tool_descriptions}
请按以下格式回答:
Thought: [你的推理过程]
Action: [工具名称]
Action Input: [工具参数]
Observation: [工具返回结果]
... (重复 Thought/Action/Observation)
Thought: 我知道最终答案了
Final Answer: [最终答案]
开始解决问题。
问题:{question}"""
核心循环实现
def react_loop(question: str, tools: dict, llm, max_steps: int = 6):
"""ReAct 推理循环"""
history = []
for step in range(max_steps):
# 构建 prompt
prompt = build_prompt(question, tools, history)
# LLM 生成推理
response = llm.generate(prompt)
# 解析 Thought / Action / Final Answer
parsed = parse_response(response)
if parsed.get("final_answer"):
return parsed["final_answer"]
# 执行工具
tool_name = parsed["action"]
tool_input = parsed["action_input"]
if tool_name in tools:
try:
observation = tools[tool_name]["fn"](tool_input)
except Exception as e:
observation = f"Error: {str(e)}"
else:
observation = f"Unknown tool: {tool_name}"
# 记录历史
history.append({
"thought": parsed["thought"],
"action": tool_name,
"action_input": tool_input,
"observation": observation
})
return "达到最大步数限制,未能得出答案"
错误恢复
当工具调用失败时,Agent 应将错误信息作为 Observation 接收并推理如何恢复——而不是盲目重试。这正是推理步骤的价值所在:模型可以分析为什么失败了,然后尝试不同的方法。
例如搜索超时:
Observation: Error: search timeout after 10s
Thought 2: 搜索超时了。让我换一种方式,也许可以直接用已有知识回答,
或者用更简短的搜索词重试。
Action 2: search("东京 人口")