从零实现一个最小 AI AgentReAct 循环 工具调用200 行 Python 直接跑收藏榜上 Agent 原理拆解系列在收收藏Agent 工具链也是最近最大的主题群——但多数人被框架文档劝退。其实 Agent 的最小骨架只有三样东西LLM 工具 循环。本篇不依赖任何框架200 行以内写一个能多步推理、调用工具、自己修参数的 ReAct Agent代码直接跑。结论先放这儿问题答案Agent 是什么LLM 工具 循环没有更多什么任务需要单次问答不需要要根据中间结果调整动作的才需要要不要上框架先跑通最小循环上规模后再说 LangGraph最要命的三坑死循环、上下文膨胀、错误没喂回去一条铁律先记住模型只输出意图执行权永远在你的代码里。模型“决定调用哪个工具、传什么参数”真正执行的是你写的函数——这条既是安全边界也是调试入口。一、最小骨架三样东西LLM负责推理和决策工具集一组函数 各自的说明注册在一张表里循环模型输出“我要调工具 X”→ 你的代码执行 → 结果喂回去 → 模型看结果继续直到给出最终答案对比一下普通 function calling 是一问一答模型最多调一次工具Agent 循环是做完一步、看结果、再决定下一步。“A 公司主要合作方的最大竞争对手的 CEO 是谁”这种多跳问题靠一次调用答不了必须循环。二、ReAct想一步做一步ReAct Reasoning Acting。每轮让模型先输出思考为什么调再输出动作调哪个工具、传什么参工具结果作为观察喂回去。每轮只输出 JSON两种格式之一工具调用或最终答案。选 JSON 而不是自造格式的原因主流平台都有原生 JSON mode输出格式有保证还省 token——重试是最贵的浪费这个在之前的省 token 六技巧里写过。三、完整代码单文件直接跑# agent.py — 最小 ReAct Agent无框架依赖 # 依赖pip install openai export OPENAI_API_KEYsk-xxx import json from openai import OpenAIclient OpenAI()---------- 工具注册 ----------TOOLS {}def tool(name, description): def deco(fn): TOOLS[name] {fn: fn, description: description} return fn return decotool(calculator, 计算四则运算表达式输入字符串如 123) def calculator(expression: str) - str: # ponytail: eval 受限白名单生产换 ast 解析防注入 if not set(expression) set(0123456789-/(). ): raise ValueError(表达式含非法字符) return str(eval(expression))tool(search_notes, 在知识库里检索与问题相关的片段) def search_notes(query: str, k: int 3) - str: # ponytail: 占位假检索换成你的向量库 / GraphRAG 检索 notes [RAG 通过检索相关片段再喂给模型省 token 又缓解长上下文遗忘, Agent 循环每步都是一次 LLM 调用累积延迟和幻觉风险都会放大, 提示词缓存要求稳定内容在前、变化内容在后命中率才高] hits [n for n in notes if any(w in n for w in query)] return \n.join((hits or notes)[:k])---------- Prompt ----------def tool_schemas(): return \n.join(f- {n}: {t[description]} for n, t in TOOLS.items())SYSTEM f你是一个严谨的助手可以调用工具每轮只调用一个。 可用工具 {tool_schemas()}每轮只输出一个 JSON 对象两种格式之一需要调用工具 {{thought: 一句话说明为什么调用, action: {{name: 工具名, args: {{}}}}}}已经有答案 {{final: 给用户的完整回答}}---------- 循环 ----------def run_agent(question: str, max_steps: int 8): messages [ {role: system, content: SYSTEM}, {role: user, content: question}, ] called [] # 死循环检测同样的调用出现第二次就打断 for step in range(1, max_steps 1): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, response_format{type: json_object}, ) out json.loads(resp.choices[0].message.content) if final in out: return out[final], step name, args out[action][name], out[action].get(args, {}) sig json.dumps(out[action], ensure_asciiFalse, sort_keysTrue) if sig in called: return f检测到重复调用 {name}提前终止, step called.append(sig) try: result TOOLS[name]fn except Exception as e: result f工具执行失败{e} # 错误当观察喂回去模型常能自己改对参数 messages [ {role: assistant, content: json.dumps(out, ensure_asciiFalse)}, {role: user, content: f[工具 {name} 返回]\n{str(result)[:2000]}}, ] return 达到最大步数仍未得到答案, max_stepsifname main: answer, steps run_agent(帮我算一下 (12872)*3再用检索确认 RAG 是什么) print(f[{steps} 步] {answer})不花 API 调用的自检验证工具注册和错误路径assert calculator in TOOLS assert calculator(12*3) 7 try: calculator(__import__(os)) assert False, 白名单没拦住 except ValueError: pass四、五个踩坑第一周都会遇到死循环模型反复用同样参数调同一个工具。双保险max_steps 硬上限 重复调用签名检测上面代码里的 called 列表上下文膨胀工具返回大 JSON 直接塞 messages三四轮就把窗口吃满。解法返回值裁剪再喂代码里 str(result)[:2000]更讲究的做法是滚动摘要参数类型漂移模型传k: 3字符串而函数要 int。解法工具入口统一强转或在工具说明里写死类型示例错误不喂回去工具抛异常直接崩掉整个循环。把错误作为观察喂回去模型经常能自己改对参数重试——这一条对成功率提升最明显JSON 输出抖动在 prompt 里反复喊“请输出 JSON”不如原生 response_format格式有保证还省 token五、什么时候上框架这个最小循环没有的东西持久化、断点续跑、人工审批节点、多 Agent 协作、可视化。任务上规模后再补需要断点续跑 / 人工确认节点 → LangGraph复杂状态机 → 框架的状态管理但顺序别反先跑通最小循环知道每一层在解决什么问题再上框架——否则框架只是把黑盒变大出问题都不知道去哪查。总结Agent LLM 工具 循环。铁律压成三句模型只输出意图执行权在你的代码里工具结果也是上下文裁剪后再喂循环必须有上限死循环是第一周就会遇到的事
