欢迎光临
我们一直在努力

Agent 面试手撕(01):从零实现 ReAct,几十行 Python 写出 Agent 核心循环

目录

一、什么是 ReAct?

二、手撕 ReAct 核心代码

2.1 定义工具

2.2 模拟 LLM 决策

2.3 手撕 Agent Loop

2.4 运行一下

三、面试官可能会追问什么?

四、最后总结

Hello,大家好,我是 1M55!👋

今天打算开启一个新系列——「Agent 面试高频手撕代码」。

不同于传统的 LeetCode 算法题,这个系列主要分享 Agent 开发中一些值得掌握的核心实现,比如 ReAct、Function Calling、Multi-Agent、Memory 等。

每篇尽量先讲清楚原理,再用 Python 手写核心代码,不依赖 LangChain、LangGraph 等 Agent 框架。

希望能够帮助到正在准备 AI / Agent 开发面试的朋友,也欢迎大家一起交流!

第一篇,我们先从最经典的 ReAct 模式开始。

一、什么是 ReAct?

ReAct 全称 Reasoning and Acting(推理与行动),是一种让大模型交替进行推理、执行动作、观察结果,再决定下一步的模式。

举个简单的例子。

用户提问:

帮我计算 5 × 6,再把结果加上 10。

Agent 可以先决定调用乘法工具,拿到结果 30 后,再决定调用加法工具,最后给出答案 40。

整个过程可以概括为:

Thought(思考)→ Action(行动)→ Observation(观察)→ 下一轮决策。

这三个阶段分别负责什么?

  • Thought(思考):模型结合用户问题和已有信息,判断下一步应该做什么。

  • Action(行动):模型决定调用哪个工具、传入什么参数,由程序真正执行。

  • Observation(观察):程序获取工具的执行结果,并反馈给模型,让模型继续决策。

如此循环,直到模型认为任务已经完成,输出 Final Answer。

这里需要注意:模型负责决策,Python 程序负责执行工具。

模型输出 add(30, 10),并不意味着它真的执行了 Python 函数。我们需要编写 Agent Runtime,根据模型的决策找到对应工具并执行。

另外,ReAct 不一定要显式输出完整的 Thought 文本。实际开发中,也可以通过结构化 Tool Calling 实现这样的决策与执行循环。

理解了这一点,我们就可以开始手撕了。

二、手撕 ReAct 核心代码

假设面试官给出题目:

不使用 LangChain 等框架,用 Python 实现一个简易 ReAct Agent,支持模型决策、工具调用、结果反馈和循环终止。

我们先约定,模型返回两种决策。

需要调用工具时:

{
"type": "tool",
"name": "multiply",
"args": {"a": 5, "b": 6}
}

任务完成时:

{
"type": "final",
"content": "最终结果是 40"
}

这样一来,Agent 只需要根据 type 判断是继续调用工具,还是返回最终答案。

2.1 定义工具

首先定义两个最简单的 Python 工具:

def add(a, b):
return a + b

def multiply(a, b):
return a * b

tools = {
"add": add,
"multiply": multiply
}

这里使用字典作为工具注册表,将工具名称映射到对应的 Python 函数。

例如:

tool_name = "multiply"
args = {"a": 5, "b": 6}

func = tools[tool_name]
result = func(**args)

print(result) # 30

其中 **args 表示把字典展开成函数的关键字参数。

这样,后续模型只需要返回工具名称和参数,Agent 就可以动态查找并执行相应函数。

2.2 模拟 LLM 决策

为了让代码不依赖真实模型 API,我们先用一个 mock_llm() 模拟模型决策。

def mock_llm(messages):
observations = [
msg["content"]
for msg in messages
if msg["role"] == "tool"
]

# 第一次:调用乘法工具
if len(observations) == 0:
return {
"type": "tool",
"name": "multiply",
"args": {"a": 5, "b": 6}
}

# 第二次:拿到乘法结果,继续加 10
if len(observations) == 1:
last_result = int(observations[-1])

return {
"type": "tool",
"name": "add",
"args": {"a": last_result, "b": 10}
}

# 第三次:返回最终答案
return {
"type": "final",
"content": f"最终结果是 {observations[-1]}"
}

这段代码模拟了模型的三次决策。

第一次调用 multiply(5, 6),第二次读取上一次的工具结果,再调用 add(30, 10),第三次输出最终答案。

注意,这里的 mock_llm() 是一个固定场景的规则模拟器,不是具有自主推理能力的真实大模型。

但它足够帮助我们理解 ReAct 的核心控制流程。

2.3 手撕 Agent Loop

接下来就是整道题最重要的部分。

def react_agent(question, max_steps=5):
messages = [
{"role": "user", "content": question}
]

for step in range(max_steps):

# 1. 模型决策
decision = mock_llm(messages)

print(f"第 {step + 1} 轮:{decision}")

# 2. 判断是否完成
if decision["type"] == "final":
return decision["content"]

if decision["type"] != "tool":
return "未知决策类型"

# 3. 获取工具及参数
tool_name = decision["name"]
tool_args = decision["args"]

# 4. 执行工具
func = tools.get(tool_name)

if func is None:
result = f"未知工具:{tool_name}"
else:
try:
result = func(**tool_args)
except Exception as e:
result = f"工具执行失败:{e}"

# 5. 记录模型的 Action
messages.append({
"role": "assistant",
"content": decision
})

# 6. 将 Observation 反馈给模型
messages.append({
"role": "tool",
"content": str(result)
})

return "超过最大执行步数"

到这里,一个简易的 ReAct Agent 就写完了。

是不是没有想象中复杂?

整个核心循环其实就做了几件事:

① 调用模型,获取下一步决策。

decision = mock_llm(messages)

每一轮都将当前 messages 传给模型,让模型根据已有上下文决定下一步。

② 判断是否需要结束。

if decision["type"] == "final":
return decision["content"]

如果模型已经能够回答用户问题,就直接返回。

③ 执行工具。

func = tools.get(tool_name)
result = func(**tool_args)

根据模型返回的工具名称,从注册表中找到真正的 Python 函数并执行。

④ 将 Observation 写回消息历史。

messages.append({
"role": "tool",
"content": str(result)
})

这是 ReAct 非常关键的一步。

因为工具返回的结果不一定是最终答案,它可能只是下一个动作的输入。

如果执行完 multiply(5, 6) 就直接 return 30,那 Agent 根本没有机会继续调用 add(30, 10)。

⑤ 限制最大执行轮数。

for step in range(max_steps):

防止模型反复调用工具却始终不返回最终答案。

2.4 运行一下

将上面三部分代码放进同一个 react_demo.py 文件,再添加:

if __name__ == "__main__":
answer = react_agent(
"先计算 5 × 6,再把结果加上 10"
)

print("最终答案:", answer)

运行:

python react_demo.py

按照当前 Mock 决策逻辑,预期输出:

第 1 轮:{'type': 'tool', 'name': 'multiply', 'args': {'a': 5, 'b': 6}}
第 2 轮:{'type': 'tool', 'name': 'add', 'args': {'a': 30, 'b': 10}}
第 3 轮:{'type': 'final', 'content': '最终结果是 40'}
最终答案: 最终结果是 40

可以看到,Agent 经历了两次工具调用和三次模型决策。

其中第二次工具调用使用了第一次返回的结果,最终由模型返回 final,结束循环。

三、面试官可能会追问什么?

Q1:为什么工具执行完不能直接 return?

因为工具结果不一定是最终答案。Agent 需要把工具结果作为 Observation 加入 messages,让模型决定是否还需要执行其他动作。

Q2:如果模型一直调用工具,不返回 Final 怎么办?

设置 max_steps 限制最大决策轮数,防止无限循环。在实际项目中还可以加入超时、预算限制等机制。

Q3:如果模型返回了不存在的工具怎么办?

通过工具注册表检查工具是否存在,不能执行模型任意指定的代码。可以把错误作为 Observation 反馈给模型,或者根据错误处理策略终止任务。

Q4:如果换成真实 LLM,需要改哪里?

主要是替换 mock_llm()。

真实模型需要接收消息历史和工具定义,并返回结构化工具调用或最终回答。Agent 再按模型 API 的协议记录工具调用、回传对应的工具结果。

这里我们自定义的 decision 和 messages 格式只是为了方便理解,不能直接当作所有模型 API 的原生格式使用。

不过,模型决策 → 工具执行 → Observation 回填 → 继续决策 这条核心控制思路是不变的。

四、最后总结

其实 ReAct 的核心没有那么神秘。

我们手写的 Agent Loop,本质就是:

while 任务未完成:
decision = llm(messages)

if 任务完成:
return 最终答案

result = 执行模型选择的工具()

messages.append(模型决策)
messages.append(工具结果)

真正要理解的,不是某个 Agent 框架的 API,而是模型如何通过工具与外部环境交互,又如何利用观察结果决定下一步行动。

当然,本文只是一个帮助理解原理的最简实现,距离生产级 Agent 还有参数校验、权限控制、上下文管理、异常恢复等工作。

但对于 ReAct 的基础手撕题,先把这个循环真正理解并独立写出来,我认为就抓住了最重要的部分。

好了,第一篇就到这里!

后面准备继续分享 Function Calling、Multi-Agent、Agent Memory 等 Agent 开发高频手撕题。

希望这个系列对你有所帮助,我们下篇见!

赞(0)
未经允许不得转载:171主机测评 » Agent 面试手撕(01):从零实现 ReAct,几十行 Python 写出 Agent 核心循环
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址