Day 19:第一阶段测验准备 —— 手动模拟 ReAct 循环
欢迎来到第十九天!在前面的学习中,我们已经掌握了 Function Calling(Day 11),理解了模型如何请求调用工具。但 Function Calling 是 API 层面的机制,模型只是“给出参数”,而推理过程是隐藏的。今天我们要深入 Agent 的核心范式——ReAct(Reasoning + Acting),并手动模拟一个 ReAct 循环,让模型在每一步都显式地输出“思考(Thought)”和“行动(Action)”,然后我们手动执行行动并将“观察(Observation)”反馈给模型。这将帮助你真正理解 Agent 是如何工作的,并为明天使用 LangChain 的现成 Agent 打下坚实基础。
一、今日学习目标
- 理解 ReAct 范式的核心思想:模型交替进行推理(Thought)、行动(Action)、观察(Observation),直到得出最终答案。
- 掌握如何通过精心设计的 Prompt 让模型按照 ReAct 格式输出。
- 动手实现一个手动模拟 ReAct 循环:模型输出 Thought/Action,程序执行工具,将 Observation 返回给模型,循环直到模型给出 Final Answer。
- 通过实验观察模型如何决定使用哪个工具、如何分析观察结果、如何修正错误。
- 对比 ReAct 与 Function Calling 的异同,为后续使用 LangChain 的 Agent 做准备。
二、详细实现步骤
步骤 1:理解 ReAct 范式
ReAct 是一种让大模型进行推理和行动的提示范式。它的核心思想是:模型不直接回答,而是先生成一个“Thought”,说明它下一步要做什么,然后生成一个“Action”,表示调用某个工具并给出输入;外部程序执行该工具,返回结果作为“Observation”;模型再根据 Observation 继续思考,如此循环,直到模型认为已经得到足够信息,输出“Final Answer”。
一个典型的 ReAct 交互如下:
Thought: 我需要知道北京今天的天气,所以应该调用天气查询工具。 Action: get_weather[北京] Observation: {"city": "北京", "temperature": "26°C", "condition": "晴"} Thought: 我已经得到了天气信息,可以回答用户了。 Final Answer: 北京今天晴,气温 26°C。与 Function Calling 相比,ReAct 将推理过程显式化,更容易调试和理解,并且可以灵活地控制循环。
步骤 2:设计 ReAct Prompt 模板
我们需要一个 System Prompt,指导模型按照规定的格式输出。这个 Prompt 需要包含:
- 可用工具列表及其描述。
- 输出格式说明:必须包含 Thought 和 Action,或者 Final Answer。
- 示例(Few-shot)帮助模型理解格式。
- 强调在不确定时要继续调用工具,不能凭空猜测。
我们以两个简单工具为例:
calculator(expression):计算数学表达式,返回结果。search(query):模拟搜索,返回一条预设的答案。
由于是手动模拟,我们不调用真实 API,而是用 Python 函数模拟。
步骤 3:准备模拟工具
新建manual_react.py,定义两个模拟工具函数:
defcalculator(expression:str)->str:"""模拟计算器,实际可以使用 eval 但为了安全,这里只处理简单表达式"""try:# 禁止危险操作ifany(cinexpressionforcin"abcdefghijklmnopqrstuvwxyz"):return"错误:表达式包含字母"result=eval(expression)returnstr(result)exceptExceptionase:returnf"错误:{e}"defsearch(query:str)->str:"""模拟搜索引擎,返回预设的搜索结果"""knowledge={"北京天气":"北京今天晴,26°C,湿度 40%。","中国首都":"中国的首都是北京。","人工智能":"人工智能是计算机科学的一个分支,研究如何让机器模拟人类智能。"}# 简单匹配forkey,valueinknowledge.items():ifkeyinquery:returnvaluereturn"未找到相关信息。"步骤 4:编写 ReAct System Prompt
这个 Prompt 至关重要,我们要让模型严格按照格式输出。提供一两个示例,展示如何使用工具以及最终回答。
react_system_prompt=""" 你是一个智能助手,可以通过调用工具来回答用户问题。你可以使用以下工具: 1. calculator:计算数学表达式,输入一个数学表达式字符串,返回计算结果。 2. search:搜索信息,输入关键词或问题,返回相关答案。 你必须按照以下格式输出(每行以 "Thought:"、"Action:"、"Observation:" 或 "Final Answer:" 开头): - 如果你想使用工具,输出: Thought: 你的思考 Action: 工具名[输入] - 当工具返回结果后,你会看到一行以 "Observation:" 开头的内容,那是工具的结果。 - 当你认为已经得到足够信息,输出: Thought: 我现在可以回答了 Final Answer: 最终答案 示例1: 用户:北京天气怎么样? Thought: 我需要查询北京的天气。 Action: search[北京天气] Observation: 北京今天晴,26°C,湿度 40%。 Thought: 我已经得到天气信息。 Final Answer: 北京今天晴,气温26°C,湿度40%。 示例2: 用户:计算 (12 + 7) * 3 的结果。 Thought: 这是一个数学计算,我应该使用计算器。 Action: calculator[(12 + 7) * 3] Observation: 57 Thought: 计算结果是57。 Final Answer: 结果是57。 现在开始回答用户的问题。记住:不要猜测,如果信息不足就使用工具。当工具返回结果后,继续根据 Observation 思考。最终必须输出 Final Answer。 """步骤 5:实现手动 ReAct 循环
我们需要一个循环,反复调用模型,解析输出中的 Action(如果有),执行工具,将 Observation 追加到对话历史中,直到模型输出 Final Answer 或达到最大步数。
importrefromopenaiimportOpenAIimportosfromdotenvimportload_dotenv load_dotenv()client=OpenAI(api_key=os.getenv("DEEPSEEK_API_KEY"),base_url="https://api.deepseek.com")defparse_action(text:str):"""从模型输出中提取 Action 和输入"""action_pattern=r"Action:\s*(\w+)\[(.*?)\]"final_pattern=r"Final Answer:\s*(.*)"action_match=re.search(action_pattern,text)final_match=re.search(final_pattern,text)ifaction_match:tool_name=action_match.group(1)tool_input=action_match.group(2)return("action",tool_name,tool_input)eliffinal_match:return("final",final_match.group(1))else:return("unknown",None,None)defrun_react_agent(user_input:str,max_steps:int=5):messages=[{"role":"system","content":react_system_prompt},{"role":"user","content":user_input}]step=0whilestep<max_steps:step+=1# 调用模型response=client.chat.completions.create(model="deepseek-chat",messages=messages,temperature=0.1,# 低温度保证格式稳定max_tokens=200)model_output=response.choices[0].message.contentprint(f"\n--- 第{step}步模型输出 ---")print(model_output)# 解析输出parsed=parse_action(model_output)ifparsed[0]=="final":print("\n=== 最终答案 ===")print(parsed[1])returnparsed[1]elifparsed[0]=="action":tool_name,tool_input=parsed[1],parsed[2]print(f"\n执行工具:{tool_name},输入:{tool_input}")# 执行工具iftool_name=="calculator":observation=calculator(tool_input)eliftool_name=="search":observation=search(tool_input)else:observation=f"错误:未知工具{tool_name}"print(f"Observation:{observation}")# 将模型输出和 Observation 追加到消息历史# 注意:我们应将模型输出作为 assistant 消息,Observation 作为 user 消息(或 system 消息)messages.append({"role":"assistant","content":model_output})messages.append({"role":"user","content":f"Observation:{observation}"})else:# 格式错误,提醒模型print("输出格式错误,提醒模型。")messages.append({"role":"assistant","content":model_output})messages.append({"role":"user","content":"你的输出格式不正确,请遵循格式:要么输出 Action: 工具[输入],要么输出 Final Answer: 答案。"})continueprint("达到最大步数,停止。")returnNone# 测试if__name__=="__main__":query="请帮我查一下北京天气,然后计算 25 * 4 的结果。"run_react_agent(query)步骤 6:运行并观察
运行脚本,你应该能看到类似以下的输出(实际可能有所不同):
--- 第 1 步模型输出 --- Thought: 用户需要两个信息:北京天气和计算结果。我需要先查天气,再计算。 Action: search[北京天气] 执行工具:search,输入:北京天气 Observation: 北京今天晴,26°C,湿度 40%。 --- 第 2 步模型输出 --- Thought: 我已经得到了天气信息,现在需要计算 25 * 4。 Action: calculator[25 * 4] 执行工具:calculator,输入:25 * 4 Observation: 100 --- 第 3 步模型输出 --- Thought: 我现在有了天气和计算结果。 Final Answer: 北京今天晴,26°C,湿度 40%;25 * 4 = 100。这个过程中,模型展现出了清晰的“思考-行动-观察”循环,甚至能处理多步骤任务。
步骤 7:实验不同任务
你可以尝试其他问题,比如:
- “人工智能是什么?用搜索工具查一下。”
- “计算 123 + 456 再乘以 2。”
- 故意让模型调用不存在的工具,观察它如何反应(你可以在执行时返回“未知工具”)。
三、常见问题与调试
Q1:模型输出的格式不稳定,有时缺少 Thought 或 Action 标签。
→ 可以加强 System Prompt 中的格式要求,增加更多示例;降低 temperature;如果格式错误,可以像代码中那样提醒模型重新输出。
Q2:模型总是直接给出 Final Answer,不调用工具。
→ 在 System Prompt 中强调“如果信息不足,必须使用工具,不要猜测”。也可以在用户消息中加上“请使用工具查询”。如果仍然不调用,可能需要提供更明确的 Few-shot 示例,展示工具调用。
Q3:循环无法终止,模型一直调用工具。
→ 设置最大步数(如 5),并在 System Prompt 中要求模型在获得足够信息后尽快输出 Final Answer。如果模型陷入循环,可以在 Observation 中加入提示,例如“你已经有了足够的信息,请给出最终答案”。
Q4:如何将手动 ReAct 与 Function Calling 结合?
→ 实际上 LangChain 的 Agent 底层就是结合了 ReAct 和 Function Calling。手动模拟的目的是理解原理,实际开发中建议使用框架。
Q5:工具执行出错怎么办?
→ 在 Observation 中返回错误信息,模型会根据错误调整下一步行动(例如修正表达式后重试)。
四、今日总结与作业
今天你完成了:
- ✅ 理解了 ReAct 范式的核心循环:Thought → Action → Observation。
- ✅ 设计了一个 ReAct 格式的 System Prompt,并实现了手动循环。
- ✅ 通过实验观察了模型进行多步骤推理和工具调用的过程。
- ✅ 学会了如何处理格式错误和工具执行错误。
今日作业(必做):
- 修改
run_react_agent函数,增加一个名为get_time的工具(模拟返回当前时间),并测试用户提问“现在几点了?用工具查一下。”。 - 让模型解决一个需要多次工具调用的复杂问题(例如“先搜索中国的人口,再用计算器乘以 2”),观察 ReAct 循环的步骤,记录模型是如何安排行动的。
- 思考:ReAct 循环中,Observation 应该以什么角色(user/assistant/system)添加到消息历史?为什么?请查阅 LangChain 的 ReAct 实现,明天我们将使用它。
明日预告:我们将正式使用 LangChain 的create_react_agent构建一个真正的 Agent,并给它配备计算器和搜索工具,体验框架带来的便利。
有任何问题欢迎随时提问!