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

不用LangChain!我手写了一个ReAct Agent,50行代码跑通推理+行动闭环 专栏第3篇前两篇我们理解了Agent的架构全貌和LLM推理原理但Agent到底是怎么自己想办法的今天我们抛开所有框架手写一个最精简的ReAct Agent——50行代码跑通Thought→Action→Observation闭环彻底搞懂Agent的运转机制。目录一、Agent的核心循环Thought → Action → Observation二、手写ReAct Agent50行代码跑通闭环三、逐行解析核心机制拆解四、Thought为什么不能省五、死循环Agent的头号敌人六、总结一、Agent的核心循环Thought → Action → Observation第一篇我们讲了Agent的四步闭环感知→规划→行动→反思。ReAct框架把这套循环具体化为一个更紧凑的执行循环还没找到答案已经找到答案用户问题 QuestionThought 思考分析问题决定下一步Action 行动调用工具Observation 执行结果工具返回的数据Final Answer 最终答案三步循环的含义步骤含义谁来做Thought分析当前情况决定下一步做什么LLMAction调用某个工具获取信息工具Observation工具执行后返回的结果工具术语说明Observation 字面是观察但在 ReAct 中它特指工具执行后返回的结果如搜索API返回的数据、计算器的计算结果不是观察这个动作本身。因此译为执行结果更贴切。关键洞察这个循环和第一篇的感知→规划→行动→反思是对应的——Thought 同时承担了感知和规划Action 是行动Observation 为下一轮的反思提供依据。二、手写ReAct Agent50行代码跑通闭环抛开 LangChain、LlamaIndex 等框架我们用最少的代码实现 ReAct 核心。你会发现Agent 的本质并不复杂。2.1 第一步定义工具工具是Agent的手脚没有工具Agent只能想不能做classTool:def__init__(self,name:str,func,description:str):self.namename self.funcfunc self.descriptiondescriptiondefrun(self,*args,**kwargs):returnself.func(*args,**kwargs)defsearch(query:str)-str:returnf搜索结果{query}的相关信息是...defcalculator(expression:str)-str:try:resulteval(expression)returnf计算结果{expression}{result}except:return计算错误请检查表达式TOOLS[Tool(nameSearch,funcsearch,description用于搜索实时信息、未知信息、最新资讯),Tool(nameCalculator,funccalculator,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_steps5):self.tools{tool.name:toolfortoolintools}self.max_stepsmax_steps self.llmcreate_llm_instance(temperature0)defrun(self,question:str):promptREACT_PROMPT.format(tool_descriptionsself.get_tool_description(),questionquestion)forstepinrange(self.max_steps):responseself.llm.invoke(prompt)resultresponse.content# 找到最终答案返回ifFinal Answer:inresult:returnresult.split(Final Answer:)[-1].strip()# 解析 Action执行工具tool_name,tool_inputself.parse_action(result)iftool_nameandtool_nameinself.tools:observationself.tools[tool_name].run(tool_input)# 把 LLM 输出 工具结果拼回 prompt继续循环promptpromptf\n{result}\nObservation:{observation}else:promptpromptf\n{result}\nObservation: 工具调用错误请重新思考return达到最大步数未能解决问题循环逻辑把问题喂给 LLMLLM 输出 Thought Action解析 Action执行工具得到 Observation把 LLM 的输出 Observation 拼回 prompt再次调用 LLM它基于新的上下文继续推理直到 LLM 输出 “Final Answer” 或达到最大步数三、逐行解析核心机制拆解3.1 parse_action从文本中提取工具调用LLM 输出的是自然语言文本我们需要从中解析出调哪个工具、传什么参数defparse_action(self,text:str):patternrAction: (\w)\[(.*?)\]matchre.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 停止条件什么时候结束循环两个停止条件正常结束LLM 输出包含Final Answer:表示它认为自己找到了答案强制结束达到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 基于这个理由选择下一步逻辑链更连贯。没有 ThoughtAction 的选择容易变得跳跃和随机。一句话总结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):seenset()foraction,inpinhistory:keyf{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梳理了ReAct 循环Thought → Action → Observation对应感知→规划→行动→反思手写实现工具定义 Prompt模板 循环逻辑核心代码不到50行上下文累积每步的输出和结果拼回 prompt形成 Agent 的短期记忆Thought 的价值推理显式化让 LLM 先想后做也便于调试死循环防御四层体系——预防、检测、干预、兜底参考资源《ReAct: Synergizing Reasoning and Acting in Language Models》Yao et al., 2023《Reflexion: Language Agents with Verbal Reinforcement Learning》Shinn et al., 2023Anthropic: “Building effective agents”2024