专栏第3篇:前两篇我们理解了Agent的架构全貌和LLM推理原理,但Agent到底是怎么"自己想办法"的?今天我们抛开所有框架,手写一个最精简的ReAct Agent——50行代码,跑通Thought→Action→Observation闭环,彻底搞懂Agent的运转机制。


目录


一、Agent的核心循环:Thought → Action → Observation

第一篇我们讲了Agent的四步闭环:感知→规划→行动→反思。ReAct框架把这套循环具体化为一个更紧凑的执行循环:

还没找到答案

已经找到答案

用户问题 Question

Thought 思考
分析问题,决定下一步

Action 行动
调用工具

Observation 执行结果
工具返回的数据

Final Answer 最终答案

三步循环的含义

步骤 含义 谁来做
Thought 分析当前情况,决定下一步做什么 LLM
Action 调用某个工具,获取信息 工具
Observation 工具执行后返回的结果 工具

💡 术语说明:Observation 字面是"观察",但在 ReAct 中它特指工具执行后返回的结果(如搜索API返回的数据、计算器的计算结果),不是"观察"这个动作本身。因此译为"执行结果"更贴切。

关键洞察:这个循环和第一篇的"感知→规划→行动→反思"是对应的——Thought 同时承担了感知和规划,Action 是行动,Observation 为下一轮的反思提供依据。


二、手写ReAct Agent:50行代码跑通闭环

抛开 LangChain、LlamaIndex 等框架,我们用最少的代码实现 ReAct 核心。你会发现,Agent 的本质并不复杂。

2.1 第一步:定义工具

工具是Agent的"手脚",没有工具,Agent只能"想"不能"做":

class Tool:
    def __init__(self, name: str, func, description: str):
        self.name = name
        self.func = func
        self.description = description
    
    def run(self, *args, **kwargs):
        return self.func(*args, **kwargs)

def search(query: str) -> str:
    return f"搜索结果:{query} 的相关信息是..."

def calculator(expression: str) -> str:
    try:
        result = eval(expression)
        return f"计算结果:{expression} = {result}"
    except:
        return "计算错误,请检查表达式"

TOOLS = [
    Tool(name="Search", func=search, description="用于搜索实时信息、未知信息、最新资讯"),
    Tool(name="Calculator", func=calculator, description="用于数学计算,输入是数学表达式")
]

工具的设计很简单:一个名字(让LLM知道叫什么)、一个函数(实际执行的逻辑)、一段描述(告诉LLM什么时候用)。

2.2 第二步:设计Prompt模板

Prompt 是 Agent 的"灵魂",它告诉 LLM 应该按什么格式输出:

REACT_PROMPT = """
你是一个优秀的AI助手,能够通过思考(Thought)、行动(Action)和执行结果(Observation)循环来解决问题。

可用工具:
{tool_descriptions}

严格按照以下格式回答:

Question: 输入的问题
Thought: 你对当前问题的思考,下一步该做什么
Action: 工具名称[工具参数]
Observation: 工具返回的结果
... (重复 Thought/Action/Observation 直到你知道答案)
Thought: 我现在知道最终答案了
Final Answer: 你的最终答案

开始!

Question: {question}
"""

关键点:格式约束是 ReAct 的核心——强制 LLM 先想(Thought),再做(Action),而不是直接给答案。

2.3 第三步:实现循环

这是整个 Agent 最核心的部分,只有不到 20 行:

class ReActAgent:
    def __init__(self, tools, max_steps=5):
        self.tools = {tool.name: tool for tool in tools}
        self.max_steps = max_steps
        self.llm = create_llm_instance(temperature=0)
    
    def run(self, question: str):
        prompt = REACT_PROMPT.format(
            tool_descriptions=self.get_tool_description(),
            question=question
        )
        
        for step in range(self.max_steps):
            response = self.llm.invoke(prompt)
            result = response.content
            
            # 找到最终答案,返回
            if "Final Answer:" in result:
                return result.split("Final Answer:")[-1].strip()
            
            # 解析 Action,执行工具
            tool_name, tool_input = self.parse_action(result)
            if tool_name and tool_name in self.tools:
                observation = self.tools[tool_name].run(tool_input)
                # 把 LLM 输出 + 工具结果拼回 prompt,继续循环
                prompt = prompt + f"\n{result}\nObservation: {observation}"
            else:
                prompt = prompt + f"\n{result}\nObservation: 工具调用错误,请重新思考"
        
        return "达到最大步数,未能解决问题"

循环逻辑

  1. 把问题喂给 LLM
  2. LLM 输出 Thought + Action
  3. 解析 Action,执行工具,得到 Observation
  4. 把 LLM 的输出 + Observation 拼回 prompt
  5. 再次调用 LLM,它基于新的上下文继续推理
  6. 直到 LLM 输出 “Final Answer” 或达到最大步数

三、逐行解析:核心机制拆解

3.1 parse_action:从文本中提取工具调用

LLM 输出的是自然语言文本,我们需要从中解析出"调哪个工具、传什么参数":

def parse_action(self, text: str):
    pattern = r"Action: (\w+)\[(.*?)\]"
    match = re.search(pattern, text)
    if match:
        return match.group(1), match.group(2)  # (工具名, 参数)
    return None, None

比如 LLM 输出 Action: Search[2024年中国人口],解析后得到 ("Search", "2024年中国人口")

3.2 prompt 拼接:上下文如何累积

这是理解 ReAct 循环的关键——每一步的 LLM 输出和工具结果都会拼回 prompt,让 LLM "记住"之前发生了什么:

第1轮 prompt:
  Question: 2024年中国的人口乘以2等于多少?

第2轮 prompt(拼接后):
  Question: 2024年中国的人口乘以2等于多少?
  Thought: 我需要先查一下2024年中国的人口
  Action: Search[2024年中国人口]
  Observation: 搜索结果:2024年中国人口约为14.1亿

第3轮 prompt(再拼接):
  Question: 2024年中国的人口乘以2等于多少?
  Thought: 我需要先查一下2024年中国的人口
  Action: Search[2024年中国人口]
  Observation: 搜索结果:2024年中国人口约为14.1亿
  Thought: 我现在知道人口是14.1亿,需要乘以2
  Action: Calculator[14.1 * 2]
  Observation: 计算结果:14.1 * 2 = 28.2

每次循环,prompt 变长一点,LLM 的上下文也丰富一点——这就是 ReAct 的"记忆"机制。

3.3 停止条件:什么时候结束循环

两个停止条件:

  1. 正常结束:LLM 输出包含 Final Answer:,表示它认为自己找到了答案
  2. 强制结束:达到 max_steps,防止无限循环

⚠️ max_steps 永远不要设成无限! Agent 陷入死循环是生产环境的头号问题,后面我们会详细讲。


四、Thought为什么不能省?

有人可能会想:能不能跳过 Thought,直接让 LLM 输出 Action?

答案是:不能。 原因有三:

4.1 Thought 让 LLM 先想后做

没有 Thought 约束时,LLM 倾向于直接输出答案——哪怕它其实不确定。强制先写 Thought,相当于逼它在行动前"停下来想一想"。

4.2 Thought 是调试的唯一线索

当 Agent 出错时,Thought 是你理解"它为什么这么做"的唯一窗口:

# 有 Thought —— 能看出问题出在哪
Thought: 用户问北京天气,我应该调用天气API
Action: Search[北京天气]           ← 搜索了天气 ✓

# 没有 Thought —— 出错了也不知道为什么
Action: Calculator[北京天气]       ← 为什么用计算器查天气???

4.3 Thought 让循环更稳定

Thought 为 Action 提供了"理由",LLM 基于这个理由选择下一步,逻辑链更连贯。没有 Thought,Action 的选择容易变得跳跃和随机。

💡 一句话总结:Thought 是 ReAct 的"推理显式化"——把 LLM 的内心独白变成可观测、可调试的文本。


五、死循环:Agent的头号敌人

手写 Agent 跑起来后,你会发现一个头疼的问题:它容易陷入死循环

5.1 五种典型死循环模式

A-B循环
搜索A→搜索B→搜索A→搜索B...

原地打转
搜索A→搜索A→搜索A...

无进展思考
我需要X→我需要X→我需要X...

来回修正
调用工具→参数错了→调用工具→参数错了...

无用递归
把同一个问题拆成它自己...

5.2 四层防御体系

Level 1 预防——在Prompt里写规则

在 System Prompt 里加入:
「禁止重复做同一个动作!」
给 Few Shot 示例,每个例子都有变化的动作

Level 2 检测——代码层面识别循环

# 精确匹配:同样的工具+同样的参数,执行了两次
def detect_exact_dup(history):
    seen = set()
    for action, inp in history:
        key = f"{action}|{inp}"
        if key in seen:
            return True
        seen.add(key)
    return False

# 连续同类检测:同一个工具连续用了3次
def check_progress(history):
    recent_actions = [h[0] for h in history[-3:]]
    if len(set(recent_actions)) == 1:
        return "连续3次相同动作,建议强制换动作!"
    return "继续"

Level 3 干预——把循环这件事告诉LLM

❌ 错误做法:检测到循环就抛错

✅ 正确做法:在 Observation 里明确指出它在循环:
「你已经连续搜索「北京天气」3次了!
  搜索结果不会有变化,请你:
  1. 换一个搜索关键词
  2. 或者换一个工具
  3. 或者直接给出答案」

LLM 看到这个提示,90% 的情况会改变策略——这比直接抛错有效得多。

Level 4 兜底——硬性限制

  • max_steps = 10(永远不要设成无限!)
  • 每步设置超时时间
  • 达到限制后转人工或返回兜底答案

5.3 为什么人不会陷入死循环?

因为人有元认知——知道自己正在做什么、有没有进展。给 Agent 加上"元认知"(循环检测 + 明确反馈),它就不会傻呵呵地无限循环了。


六、总结

本文从零手写了一个 ReAct Agent,梳理了:

  1. ReAct 循环:Thought → Action → Observation,对应感知→规划→行动→反思
  2. 手写实现:工具定义 + Prompt模板 + 循环逻辑,核心代码不到50行
  3. 上下文累积:每步的输出和结果拼回 prompt,形成 Agent 的"短期记忆"
  4. Thought 的价值:推理显式化,让 LLM 先想后做,也便于调试
  5. 死循环防御:四层体系——预防、检测、干预、兜底

参考资源

  • 《ReAct: Synergizing Reasoning and Acting in Language Models》(Yao et al., 2023)
  • 《Reflexion: Language Agents with Verbal Reinforcement Learning》(Shinn et al., 2023)
  • Anthropic: “Building effective agents”(2024)
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐