news 2026/6/11 12:28:53

不用LangChain!我手写了一个ReAct Agent,50行代码跑通推理+行动闭环

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
不用LangChain!我手写了一个ReAct Agent,50行代码跑通推理+行动闭环

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


目录

  • 一、Agent的核心循环:Thought → Action → Observation
  • 二、手写ReAct Agent:50行代码跑通闭环
  • 三、逐行解析:核心机制拆解
  • 四、Thought为什么不能省?
  • 五、死循环: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只能"想"不能"做":

classTool:def__init__(self,name:str,func,description:str):self.name=name self.func=func self.description=descriptiondefrun(self,*args,**kwargs):returnself.func(*args,**kwargs)defsearch(query:str)->str:returnf"搜索结果:{query}的相关信息是..."defcalculator(expression:str)->str:try:result=eval(expression)returnf"计算结果:{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 行:

classReActAgent:def__init__(self,tools,max_steps=5):self.tools={tool.name:toolfortoolintools}self.max_steps=max_steps self.llm=create_llm_instance(temperature=0)defrun(self,question:str):prompt=REACT_PROMPT.format(tool_descriptions=self.get_tool_description(),question=question)forstepinrange(self.max_steps):response=self.llm.invoke(prompt)result=response.content# 找到最终答案,返回if"Final Answer:"inresult:returnresult.split("Final Answer:")[-1].strip()# 解析 Action,执行工具tool_name,tool_input=self.parse_action(result)iftool_nameandtool_nameinself.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 输出的是自然语言文本,我们需要从中解析出"调哪个工具、传什么参数":

defparse_action(self,text:str):pattern=r"Action: (\w+)\[(.*?)\]"match=re.search(pattern,text)ifmatch:returnmatch.group(1),match.group(2)# (工具名, 参数)returnNone,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 检测——代码层面识别循环

# 精确匹配:同样的工具+同样的参数,执行了两次defdetect_exact_dup(history):seen=set()foraction,inpinhistory:key=f"{action}|{inp}"ifkeyinseen:returnTrueseen.add(key)returnFalse# 连续同类检测:同一个工具连续用了3次defcheck_progress(history):recent_actions=[h[0]forhinhistory[-3:]]iflen(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)
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 12:28:06

适合B2B企业的GEO服务商推荐?先看5类服务商怎么选

适合B2B企业的GEO服务商,不能简单用“发稿多不多”“报价低不低”“平台覆盖广不广”来判断。B2B企业更应该看服务商是否能处理复杂产品、长决策链、官网信源、AI回答监测和持续优化。以径硕科技JINGGEO这类全链路GEO服务商为参照,真正适合B2B企业的服务…

作者头像 李华
网站建设 2026/6/11 12:27:54

5步实现AI视频自动生成:Pixelle-Video深度解析

5步实现AI视频自动生成:Pixelle-Video深度解析 【免费下载链接】Pixelle-Video 🚀 AI 全自动短视频引擎 | AI Fully Automated Short Video Engine 项目地址: https://gitcode.com/GitHub_Trending/pi/Pixelle-Video 在数字内容创作爆炸式增长的今…

作者头像 李华
网站建设 2026/6/11 12:25:59

烟台别墅装修公司:施工质量好的靠谱企业解析

烟台别墅装修公司哪家靠谱?5个维度解析施工质量好的本地企业“烟台装修中,别墅项目的施工质量是靠谱企业的核心护城河,选对团队才能避免百万投入打水漂”别墅装修因面积大、工艺复杂、个性化需求高,对施工质量的要求远高于普通住宅…

作者头像 李华
网站建设 2026/6/11 12:25:59

告别卡顿!用ViewPager2和Fragment打造丝滑的驾考题库App(附完整源码)

用ViewPager2和FragmentStateAdapter重构驾考题库App:从卡顿到丝滑的进阶实践每次在驾考题库App中翻页时遭遇卡顿,都像科目二考试时突然熄火一样令人焦虑。传统ViewPager配合Fragment的组合在复杂题库场景下逐渐暴露出性能瓶颈,而ViewPager2的…

作者头像 李华
网站建设 2026/6/11 12:25:03

企业微信开发模式选型:企业自研还是调用第三方API接口?

很多做私域运营的朋友都有过这样的崩溃时刻:早上打开电脑,面前摆着五六台手机,每个屏幕上都有不同的客户在咨询,消息提示音此起彼伏,根本顾不过来。回复慢了,客户流失;回复错了,形象…

作者头像 李华
网站建设 2026/6/11 12:20:55

如何巧妙管理JetBrains IDE试用期:3种重置方案深度解析

如何巧妙管理JetBrains IDE试用期:3种重置方案深度解析 【免费下载链接】ide-eval-resetter 项目地址: https://gitcode.com/gh_mirrors/id/ide-eval-resetter 对于众多开发者而言,JetBrains系列IDE是日常编码工作中不可或缺的得力助手。从Intel…

作者头像 李华