外观
从零实现 ReAct
ReAct Implementation from Scratch
接下来,我们将用两种方式来实现一个 ReAct 智能体(Agent):
- 手动执行每一步,以获得更清晰的过程。
- 不做人工干预,完全自动化整个推理(Reasoning)与行动(Action)过程。
我们先来看手动执行的方式。
#1)手动执行的 ReAct
在这一节中,我们将从零实现一个轻量级的 ReAct 风格智能体,不借助 CrewAI 或 LangChain 这类编排框架。
我们会手动模拟智能体每一轮的推理、暂停、行动和观察,完全按照 ReAct 循环本该运作的方式来进行。
通过逐个单元格(cell)运行这些逻辑,我们能够完全看清并掌控整个思考过程,从而在每一步都可以调试和验证智能体的行为。
首先,我们加载环境变量(比如你的 LLM API key),并从 LiteLLM 中导入 completion(记得先安装它——pip install litellm)。LiteLLM 是一个轻量级封装库,可以用来调用 OpenAI 等 LLM,或者通过 Ollama 调用本地模型。

接下来,我们定义一个精简的 Agent 类,它封装了一个具备对话能力的 LLM,并维护完整的消息历史——使其能够逐步推理、读取系统提示词、记住之前的输入和输出,并支持多轮交互。
它的样子如下:

- system(str):这是系统提示词,用来设定智能体的个性和行为约束。如果传入了它,就会成为对话中的第一条消息,就像 OpenAI Chat API 中的做法一样。
- self.messages:这个列表充当对话记忆。无论是用户输入还是助手输出,每一次交互都会被追加进这个列表。这份历史记录对 LLM 在多轮对话中保持连贯至关重要。
- 如果提供了 system,它会以特殊的 "role": "system" 标识被加入消息列表。这确保了后续每一次 completion 调用,都是在系统指令的约束下进行的。
接下来,我们在这个类中定义一个 complete 方法:

这是你与智能体交互所用的核心接口。
- 如果传入了 message:
- 它会作为一条 "user" 消息被追加到 self.messages 中。
- 这模拟了人类提出问题或下达指令的过程。
- 然后,会调用 self.invoke()(我们稍后会定义它)。这个方法会把完整的对话历史发送给 LLM。
- 模型的回复(存储在 result 中)随后会以 "assistant" 角色被追加到 self.messages 中。
- 最后,这条回复会被返回给调用者。
这个方法在一次调用中做了三件事:
- 记录用户输入。2. 获取模型的回复。3. 更新消息历史,供后续轮次使用。
最后,我们来看下面的 invoke 方法:

这个方法负责向你的 LLM 提供方发起实际的 API 调用——在这里是通过 LiteLLM,使用 "openai/gpt-4o" 模型。
- completion() 是对 chat completion API 的一层封装。它接收完整的消息历史,并返回一个响应。
- 我们假设 completion() 返回的结构与 OpenAI 的格式类似:一个 choices 列表,其中每个 choice 都带有一个 .message.content 字段。
- 我们提取并返回这个 content 内容——也就是助手的下一条回复。
作为测试,我们可以快速运行下面这个简单的交互:

此时,如果我们问它关于上一条消息的内容,会得到正确的输出,这说明助手能够看到之前的上下文:

它准确地记住并反映出了之前的内容!
现在我们的对话类已经搭建好了,接下来进入最有意思的部分——定义一个 ReAct 风格的提示词。
在 LLM 能够表现得像一个智能体之前,它需要清晰的指令——不仅要告诉它回答什么,还要告诉它该怎么去回答。这正是下面定义的这个 system_prompt 所要做的事:

这不仅仅是一段提示词,更是一份行为协议——它规定了智能体应遵循的结构、应如何推理,以及应在何时停下来。
让我们逐行拆解一下。
你在一个循环中运行,并且每一次迭代只做一件事:
这是整段提示词的框架性语句。它告诉 LLM 不要急着给出答案,而是应该按照一套既定的模式、在循环中一步一步推进——这正是 ReAct 智能体的运作方式。
1)用「Thought」来描述你对输入问题的思考。
2)用「PAUSE」来暂停,并思考接下来要采取的行动。
3)用「Action」从你可用的行动列表中,决定要采取哪个行动。
4)用「PAUSE」来暂停,并等待该行动的结果。
5)「Observation」就是该行动所返回的输出。
这里,我们给了 LLM 一套推理模板。这些正是所有 ReAct 风格智能体中都会用到的基本组件。
我们来逐一拆解一下:
- Thought:智能体的内心独白——它此刻正在想什么。
- PAUSE(第一次):强制模型先停下来喘口气,而不是直接跳去行动——这模拟了多智能体环境中的异步步骤。
- Action:智能体从提供给它的工具列表中做出选择。
- PAUSE(第二次):再次等待,这一次等待的是工具的实际执行结果。
- Observation:这一部分会在工具运行之后,由你(也就是控制器或人类)注入到提示词中。
通过把整个过程拆分成这些显式的步骤,我们能够减少幻觉(hallucination)的产生,并确保智能体是在一个受控的循环中运作。
在循环的最后,你会给出一个 Answer。
这句话告诉智能体:一旦它掌握了所需的全部信息,就应该跳出循环,给出最终答案,不需要无休止地继续推理下去。
你可用的行动有:
math:
例如:math: (14 * 5) / 4
用 Python 语法来计算数学表达式。
lookup_population:
例如:lookup_population: India
返回指定国家已知的最新人口数据。
这相当于给智能体准备了一份精简的 API 参考文档。其中包含:
- 每个工具的名称。
- 如何调用它。
- 它会产生什么样的输出。
这一点至关重要。如果没有清晰的规范,LLM 可能会:
- 凭空捏造出并不存在的工具。
- 使用错误的语法。
- 误解某个工具本该完成的功能。
通过使用清晰的格式和示例,我们教会模型以一种安全、可预测的方式与工具对接。
这里给出一个供你参考的示例运行过程:
Question:日本人口的两倍是多少?
第 1 轮迭代:
Thought:我需要先查一下日本的人口。
第 2 轮迭代:
PAUSE
……
第 9 轮迭代:
Observation:250000000
第 10 轮迭代:
Answer:日本人口的两倍是 2.5 亿。
这个完整演示的示例为 LLM 提供了一个可以遵循的范式。更重要的是,它也为开发者(也就是你)提供了在每一步进行干预的方式——你可以注入工具结果,或者验证整个流程是否运作正确。
有了这份示例执行轨迹:
- 智能体知道该如何思考。
- 智能体知道该如何行动。
- 智能体知道该在何时停下来。
一旦你得到了答案,就停止循环,并把它输出给用户。
现在开始解答:
这最后几句话至关重要。
如果没有这个明确的停止信号,LLM 可能会无休止地继续下去。你在告诉它:「当你拼齐所有拼图碎片之后,直接说出答案并退出循环。」
这个 system_prompt 的威力,就在于它的结构:
- 它塑造的是一种智能行为模式,而不只是简单的问答。
- 它施加了很强的约束:先思考再行动,行动要在既定边界之内,并且要等待观察结果。
- 它把推理和执行分离开来,模仿了人类的运作方式。
- 它为多步骤问题创建了一个便于反馈的迭代循环。
提示词定义好之后,我们接下来实现这些工具。

最后,我们开始一次手动的 ReAct 会话:

这会产生以下输出:
第 1 轮迭代:
Thought:我需要先查一下印度的人口。
此时,作为用户的我们没有任何输入要给出,所以我们只需再次调用 complete() 方法:

这会产生以下输出:
第 2 轮迭代:
PAUSE
同样地,此时作为用户的我们也没有输入要给出,所以我们再次调用 complete() 方法:

这会产生以下输出:
第 3 轮迭代:
Action: lookup_population: India
现在它想要采取行动了。
此时我们仍然没有输入要给出,所以继续再次调用 complete() 方法:

这会产生以下输出:
第 4 轮迭代:
PAUSE
此时,它需要以 observation 的形式获得工具的输出结果。这里我们来手动介入,为它提供这个 observation:

这会产生以下输出:
第 5 轮迭代:
Thought:现在我需要查一下日本的人口。
我们让它继续执行下去:

这会产生以下输出:
第 6 轮迭代:
PAUSE
我们再次让它继续执行:

我们得到以下输出:
第 7 轮迭代:
Action: lookup_population: Japan
此时,它同样需要以 observation 的形式获得工具的输出结果。这里我们再次手动介入,为它提供这个 observation:

这会产生以下输出:
第 8 轮迭代:
Thought:现在我已经拿到了印度和日本的人口数据,我需要把它们加在一起。
我们再次让它继续执行:

我们得到以下输出:
第 9 轮迭代:
Action: math: 1400000000 + 125000000
根据之前设定的模式,接下来我们应该会看到一次暂停:

第 10 轮迭代:
PAUSE
它此时又在等待一个 observation,也就是日本人口和印度人口的总和。为此,我们再次手动介入,为它提供这个结果:

最终,在这一轮迭代中,我们得到了以下输出:
第 11 轮迭代:
Answer:印度人口与日本人口的总和是 1,525,000,000。
太棒了!!
通过这整个过程:
LLM 思考了该采取哪些步骤。
它选择了要执行的行动。
我们手动注入了工具输出,就像真实世界中的观察结果一样。
它不断循环,直到掌握足够的信息来生成最终答案。
这让我们清楚地理解了 ReAct 风格智能体中,推理与行动究竟是如何结合在一起的。
在下一部分,我们会把这一整套流程完全自动化——不再需要任何手动调用,而是构建一个完整的控制器,用代码来模拟整个循环。
#2)无需手动执行的 ReAct
既然我们已经理解了上面这套 ReAct 执行流程是如何进行的,接下来就可以轻松地将其自动化,去掉我们手动介入的部分。
在这一节中,我们会创建一个控制器函数,它会:
- 向智能体发送初始问题,
- 逐步读取它的思考(thought)和行动(action),
- 在需要时自动运行外部工具,
- 把观察结果(observation)反馈给智能体,
- 并在得到最终答案后停止循环。
以下就是实现这一切的完整代码:

让我们来拆解一下这整个循环。
我们首先定义 agent_loop() 函数:
它接收以下参数:
query:用户提出的自然语言问题。
system_prompt:也就是我们之前探讨过的那份 ReAct 系统提示词(定义了整个行为循环)。
接下来,在这个函数内部,我们初始化 Agent 以及可用的工具:

- 创建一个新的 MyAgent 实例,使用这份结构化的 ReAct 提示词。
- 定义一个字典,列出智能体可以调用的工具。这些工具名称必须与智能体在 Action: 行中所使用的名称完全一致。
接下来,我们定义一些状态变量:

current_prompt 存储着下一条要发送给 LLM 的消息。
previous_step 用来跟踪上一个阶段(比如 Thought、Action),以便更好地控制流程。
接下来,我们运行这个推理循环,它会持续进行,直到智能体给出最终答案。根据我们的提示词设计,这个答案应当以 Answer: 标记出来:

接下来,我们把 current_prompt 输入给智能体。

current_prompt 可能是:
- 最初的用户查询,
- 一个空字符串,让智能体继续推理,
- 来自某个工具的观察结果(observation)。
然后我们把智能体的输出打印出来,以便查看每一轮迭代的情况。
接下来,如果智能体给出了最终答案,我们就跳出循环。

另一种情况是,如果回复中包含 Thought: 这一行,我们会:
- 把当前步骤类型记录为「Thought」。
- 把 current_prompt 设为空字符串,进入下一阶段(也就是 PAUSE)。

接下来,我们捕获紧跟在 Thought 之后的第一个 PAUSE。这里不需要做任何其他事——我们只需继续进入下一步。

如果我们检测到 Action: 这一行,我们会:
- 记下我们现在处于行动(action)这一步。
- 用一个正则表达式来提取工具名称及其参数。

举例来说,对于 Action: lookup_population: India 这一行,正则表达式会提取出:
- 工具名 lookup_population。
- 参数 India。
接下来,我们执行这个工具,并捕获它的观察结果(observation):

- 如果工具名称是有效的,我们就把它当作一个 Python 函数来调用,并捕获其结果。
- 我们把输出格式化成 Observation: ... 的形式,以便智能体在下一步中使用它。
- 如果这个工具并不存在,我们会要求智能体重试。
这模拟了「工具执行 + 结果注入」的整个过程。
完成!
现在我们可以像下面这样运行这个函数:

这会产生以下输出,结果确实正确:

现在你已经拥有了一个完全可用的 ReAct 循环,而且完全不需要借助任何外部框架。
当然,在这个实现中,我们是用正则匹配和硬编码的条件判断,来解析智能体的行动并将其路由到正确的工具上。
这种方式在像这个演示这样高度受控的场景下运作良好。但它比较脆弱:
如果智能体的输出稍微偏离了预期格式(比如多了几个空格、大小写不一致,或者把某个行动标错了名字),正则表达式就可能匹配失败。
我们还假设了智能体永远不会调用不存在的工具,并且所有工具都会静默地成功执行。
在生产级别的系统中,你会希望做到:
- 加入更健壮的解析方式(比如使用输出为 JSON 的结构化提示词,或者函数调用(function calling))。
- 加入工具校验、重试机制和异常处理。
- 使用护栏机制(guardrails)或输出格式化器,来约束 LLM 被允许输出的内容。
但如果目的是理解 ReAct 风格循环在底层究竟是如何运作的,这就是一个干净又精简的起点。它让你能够完全透明地看清智能体推理和执行过程中每一个阶段究竟发生了什么。
这个循环展示了一个简单的智能体是如何思考、行动、观察的——而这一切都由你自己的 Python 加本地 LLM 技术栈驱动。