智能体面试准备(十二):从零手写一个 ReAct Agent——把 B1 到 B11 焊成一台能跑的
智能体面试准备十二从零手写一个 ReAct Agent——把 B1 到 B11 焊成一台能跑的机器写在前面B2 讲过 ReAct 范式B11 讲过怎么评估 Agent。这篇是收官实战——不调任何框架LangChain/CrewAI 统统不用纯标准库从零写一个完整、可运行的 ReAct Agent。它包含 Thought/Action/Observation 循环、工具注册、解析器、最大步数护栏并内置一个假 LLM让它离线就能跑通你面试现场复制就能演示。一、ReAct 到底长什么样ReAct Reason Act。核心是一个循环Thought: 我需要先查一下 X Action: tool_name Action Input: 参数 Observation: 工具返回结果 Thought: 有了结果我再算一下 Y Action: another_tool ... Thought: 信息够了可以回答了 Final Answer: 最终答案模型不是一步出答案而是显式把思考写出来然后决定调什么工具看到结果再思考下一步直到能回答。这正是 B1 讲的自主智能体的最小形态也是 B9 状态机、B10 Agentic RAG 的底层循环。二、我们要实现什么模块职责Tool一个函数 名字 描述能被注册和调用Parser从模型文本里抽出 Action / Action Input / Final AnswerAgentLoop循环拼 prompt → 调 LLM → 解析 → 执行工具 → 回填 ObservationGuardrail最大步数、重复动作检测防死循环呼应 B9FakeLLM离线可跑的伪模型按规则产出合法 ReAct 格式三、完整可运行代码复制即跑纯标准库import re import math from typing import Callable, Dict, List # ---------------- 1. 工具定义 ---------------- class Tool: def __init__(self, name: str, desc: str, func: Callable): self.name, self.desc, self.func name, desc, func def _calc(expr: str) - str: # 极简计算器只允许数字和 - * / ( )防注入 if not re.fullmatch(r[0-9\-*/().\s], expr): return ERROR: 非法表达式 try: return str(eval(expr, {__builtins__: {}}, {})) except Exception as e: return fERROR: {e} def _search(q: str) - str: # 假搜索命中本地知识库 kb { 光速: 光速约为 3e8 m/s, 圆周率: 圆周率 pi 约等于 3.14159, 地球半径: 地球平均半径约 6371 km, } for k, v in kb.items(): if k in q: return v return 未找到相关结果 def _weather(city: str) - str: fake {北京: 晴 26℃, 上海: 多云 29℃, 深圳: 雷阵雨 30℃} return fake.get(city, f{city} 天气未知) TOOLS: Dict[str, Tool] { Calculator: Tool(Calculator, 计算数学表达式, _calc), Search: Tool(Search, 检索常识知识, _search), Weather: Tool(Weather, 查询城市天气, _weather), } # ---------------- 2. 解析器 ---------------- def parse_response(text: str): 从模型输出抽取动作优先找 Final Answer m re.search(rFinal Answer:\s*(.), text, re.S) if m: return (final, m.group(1).strip()) a re.search(rAction:\s*(\w), text) i re.search(rAction Input:\s*(.), text) if a and i: return (action, a.group(1).strip(), i.group(1).strip()) return (none, None) # ---------------- 3. 假 LLM离线可跑 ---------------- def fake_llm(question: str, history: str) - str: 规则式伪模型演示 ReAct 循环无需真实 API # 已拿到天气且拿到常识 - 给最终答案 if Weather in history and Search in history: return Final Answer: 北京今天晴 26℃光速约为 3e8 m/s来自检索。 if Action: Weather in history: return Thought: 拿到天气了再查一下光速常识。\nAction: Search\nAction Input: 光速 if Action: Search in history: return Thought: 已检索到常识现在查北京天气。\nAction: Weather\nAction Input: 北京 # 第一步决定先检索 return Thought: 用户问北京天气和光速我先检索光速常识。\nAction: Search\nAction Input: 光速 # ---------------- 4. Agent 主循环 ---------------- def run_agent(question: str, max_steps: int 6) - str: prompt_header ( 你是一个 ReAct Agent。可用工具 .join(f{t.name}({t.desc}) for t in TOOLS.values()) \n格式\nThought: ...\nAction: 工具名\nAction Input: 参数\n 或 Final Answer: 答案\n ) history for step in range(1, max_steps 1): inp prompt_header f\n问题{question}\n history out fake_llm(question, history) print(f\n[Step {step}] LLM 输出:\n{out}) kind parse_response(out)[0] if kind final: return parse_response(out)[1] if kind action: _, name, arg parse_response(out) tool TOOLS.get(name) obs tool.func(arg) if tool else fERROR: 未知工具 {name} history out f\nObservation: {obs}\n print(f - 调用 {name}({arg}) {obs}) else: history out \nObservation: 解析失败请按格式输出。\n return ERROR: 超过最大步数未得到最终答案护栏触发 # ---------------- 5. 运行 ---------------- if __name__ __main__: q 北京今天天气怎么样光速是多少 print(问题:, q) answer run_agent(q) print(\n 最终答案 ) print(answer)跑这段你会看到完整的 ReAct 轨迹Step1 决定 Search → 拿到光速 → Step2 决定 Weather → 拿到北京天气 → Step3 综合出 Final Answer。全程不联网、不依赖任何第三方库。这就是一个最小可运行的自主 Agent。面试加分指出eval计算器有注入风险虽已用__builtins__{}和正则过滤但生产应改用ast解析或numexpr并指出 fake_llm 仅用于演示真实环境换成openai/本地模型调用循环结构完全不变。这正是框架无关的核心价值——你理解了循环换任何 LLM 都能接。四、把 B1–B11 的零件对号入座这个 80 行的小东西其实把整个系列串起来了本 Agent 的部件对应前面哪篇Thought/Action/Observation 循环B2 ReAct 范式工具注册表 TOOLSB3 Function Calling / B4 MCP解析器 parse_responseB2 动作格式约束最大步数 max_steps 护栏B9 状态机三层护栏多工具协作SearchWeatherCalcB7 多智能体/工具协作假 LLM 可替换为真实 轨迹可记录B11 轨迹级评估检索工具A12 RAG / B10 Agentic RAG这段代码的history变量本质就是 B11 说的轨迹trajectory——把它存下来就能直接套 B11 的evaluate_agent算工具选择准确率和步数。一个闭环就通了。五、如何升级这个玩具 Agent面试延伸面试官常问这玩具怎么变生产级你按这个清单答换真实 LLMfake_llm→ 调 APIprompt 加强格式约束 few-shot。加反射Reflection每步后让模型自评这步对吗错就重试呼应 B5/B10 评估器。加记忆把 history 存向量库做长期记忆B6。加规划复杂任务先 Plan 再 ExecuteB5而非纯反应式。加并行/多 Agent拆子任务给不同 AgentB7。加评估接 B11 的 eval set 做回归。六、面试高频追问自测ReAct 和 Chain-of-Thought 区别 → CoT 只有思考没有行动ReAct 在思考间插入 Action/Observation能调用外部工具突破参数知识边界。Action 解析失败了怎么办 → 解析器兜底提示按格式重输或重试连续失败触发护栏终止。会死循环吗怎么防 → 会用 max_steps 重复动作检测B9 讲的三层护栏。为什么不直接让模型输出 JSON → 可以但自由文本 ReAct 更鲁棒、可解释生产常用函数调用B3替代文本解析更稳。小结从零手写 ReAct Agent 不难难的是理解它每个部件为什么存在循环是为了边想边查、解析器是为了让程序能驱动工具、护栏是为了不死循环、history 是为了可评估可复盘。这段代码把 B1–B11 全部串起来了——它是你面试时最能拿出东西的武器不仅能讲还能当场跑。B 系列到此完成认知→架构→实战的闭环B13 起进入两个完整 Agent 收官实战持久化记忆 Agent、MCP 实战。本篇 B12 完。Day 6 四篇 A11/A12/B11/B12 全部完成A 系列覆盖压缩与 RAG、B 系列进入评估与完整实现系列交叉引用完整。