ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

手写ReAct Agent:从零构建大模型工具调用闭环

手写ReAct Agent:从零构建大模型工具调用闭环 做 AI Agent 开发很多人第一周做的事是到处收集提示词模板第二周开始尝试让大模型调用外部工具第三周才发现真正难的既不是提示词也不是 API 调用而是把“模型决策、工具执行、状态记忆、结果验证”这四个环节正确编排起来。市面上的智能体教程动辄几百集但多数内容停留在概念科普和平台演示。如果已经具备 Python 基础想真正动手写一个属于自己的智能体最优路径不是从头刷完所有视频而是先理解 Agent 的工作原理再跑通一个最小可运行案例最后再决定要不要引入 Dify、LangGraph、Hermes 这类框架。这篇文章会沿着这条主线展开先解释 AI Agent 的本质和 ReAct 推理循环再给出 7 天学习路径和环境准备步骤然后手写一个不依赖任何框架的最小 ReAct Agent接着讨论工具调用机制最后对比 Dify、Coze、LangGraph、Hermes 等方案的选型思路并补充常见的报错排查路径、安全防护清单和生产落地建议。读完以后你可以独立完成一个“大模型 工具 记忆 循环”的完整闭环而不是只会拖拽节点或者套模板。1. 智能体到底是什么先把“模型能力”和“智能体能力”拆开1.1 用一句话说清 Agent大模型本身是一个“凭上下文预测下一个 Token”的推理引擎。它很强但它不会主动去查数据库、不会调接口、不会记住上一次用户的偏好也不会在调用外部服务失败后自动换一条路径。Agent智能体的本质是在大模型外面包一层“目标执行闭环”收到任务 - 规划 - 调用工具 - 观察结果 - 再次决策 - 输出答案或继续执行换句话说Agent 的核心不是模型而是“模型 工具 循环控制”三者结合起来的执行框架。用户问“帮我订明天上午从北京到上海的高铁”普通聊天机器人只能给出“建议去 12306 查询”这种回答而 Agent 会尝试查询余票、比对车次、确认乘客信息、调用订票接口最后把结果反馈给用户。模型负责判断每一个步骤该做什么工具负责真正完成动作循环负责判断任务是否结束。很多人把 Agent 和大模型当成一回事这是一个容易踩的误区。大模型是大脑Agent 是“有手有脚有工具的大脑”。1.2 一个智能体必须包含的四个组件大脑LLM 模型负责任务拆解、决策、结果生成。目前常见的有 GPT、Claude、Qwen、DeepSeek、Llama 等选型时要关注上下文长度、工具调用能力和价格。工具模型可以调用的外部能力比如天气 API、计算器、数据库查询、搜索接口、内部业务系统、OCR、爬虫等。记忆短期记忆是当前会话里的上下文长期记忆一般落库到向量数据库或关系型数据库让 Agent 能跨会话记住用户信息和历史偏好。编排逻辑决定调用哪个模型、调用哪个工具、怎么拼接消息、遇到超时或错误怎么处理、最大循环多少轮、什么条件下必须终止。四者缺一不可。如果只做“模型 提示词”那不叫 Agent只是一个更聪明的聊天框。只有把工具执行和循环控制加进来模型才具备“行动能力”。1.3 ReAct 推理循环是绕不开的模式ReAct 是 Reasoning Acting 的组合意思是让模型一边推理一边行动。它是最常用的 Agent 工作模式核心过程可以描述为用户输入一个问题。模型根据自己的知识和当前可用工具先思考“解决这个问题需要什么步骤”。如果需要外部信息模型输出一个“工具调用请求”指定调用哪个工具、传什么参数。程序真正执行这个工具拿到返回结果。把工具返回结果作为新消息回传给模型。模型观察结果判断任务是否完成。如果没完成继续循环如果完成了输出最终答案。用现实中的助理来类比领导让你准备一份竞品分析报告。你先查资料、再整理、发现数据不够、继续查、补充图表、最后输出报告。你不是一次性写完的而是“搜索-观察-修正-再搜索”循环推进。ReAct 就是把这个过程程序化。在这个模式下模型不直接操作外部系统它只会“描述意图”。真正执行工具的是你写的代码这一点很关键。2. 学习 AI Agent 的正确顺序不要一上来就学框架2.1 为什么不要直接学 LangGraph 或 Dify很多初学者一上来就打开 LangGraph 文档或 Dify 拖拽界面结果发现节点概念懂配置也能配但一旦报错就完全不知道问题出在哪。因为框架把底层逻辑封装得太好你看到的只是表层。比如你在 Dify 里拖了一个“知识检索”节点又拖了一个“LLM”节点它们之间的消息传递是靠框架自动拼装的。一旦检索结果没有正确传给模型你只能看到最终回答不符合预期却说不清楚是工具描述写得不好、还是变量名写错了、还是模型没有触发工具调用。正确的顺序是先把裸循环跑通再用框架去减少重复劳动。手写一遍最小 ReAct Agent 之后你再看 LangGraph 的 StateGraph、Dify 的 Workflow就会觉得这些概念非常直观因为它们只是把你手写逻辑图形化、组件化了。2.2 7 天学习路径表用 7 天时间从零到能独立开发一个可用的 Agent是可行的前提是每天目标明确。下面这个路线可以作为参考天数学习目标核心内容可交付成果检验标准Day 1理解 LLM 与 Agent 的边界Token、上下文、system prompt、工具调用写一篇笔记说明 Agent 的四个组件能用自己的话讲清楚“模型和工具的关系”Day 2掌握 OpenAI 兼容接口调用请求结构、流式与非流式、错误处理一个最简对话脚本能打印出模型的完整返回 JSONDay 3实现最小 ReAct 循环工具定义、tool_calls 解析、消息回传一个包含 2 个工具的 Agent 脚本支持多轮工具调用并最终输出答案Day 4处理记忆与多轮会话上下文拼接、历史消息管理给 Agent 增加短期记忆新问题能引用上一轮内容Day 5了解主流框架Dify、Coze、LangGraph、Hermes对比框架差异能说清什么时候该用框架Day 6完成一个垂直场景 Agent比如销售咨询助手、天气助手一个带工具链的完整 Agent对 10 条真实问题有稳定回答Day 7部署和排查日志、接口服务化、失败回退用 FastAPI 包装 Agent能通过 HTTP 访问并记录日志2.3 前置知识清单开始前建议确认自己具备这些基础Python 基础语法函数、类、异常处理、装饰器不要求精通但至少写过一段完整脚本。HTTP 与 API 调用理解 GET、POST会使用 requests 或 httpx。JSON 基础能读懂嵌套 JSON并完成提取和赋值。提示词基础知道 system、user、assistant 三种角色的区别理解 few-shot 的基本写法。命令行基础会创建虚拟环境、安装依赖、设置环境变量。这些条件只要覆盖 60% 就可以开始。剩下的内容可以在写代码过程中边写边查。3. 环境准备本地模型、Python 虚拟环境和最小依赖3.1 学习环境建议学习阶段推荐在本地完成省去云端费用同时便于观察请求和响应的完整结构。环境要求如下依赖项推荐版本或配置说明操作系统Windows 10/11、macOS、Linux 均可尽量使用 64 位系统Python3.10 或 3.113.12 部分依赖可能尚未完全兼容优先 3.11模型服务OpenAI 兼容接口或 Ollama本地推荐 Ollama云端推荐使用兼容接口代码编辑器VS Code 或 PyCharm关键是能方便调试 Python包管理pip venv不要直接安装在全局环境如果本机显存不足也可以直接调用云厂商公网 API。为了便于调试建议先使用本地模型比如通过 Ollama 运行参数较小的模型跑通后再切换云端大模型。3.2 创建虚拟环境并安装依赖以 Windows 和 macOS 通用的方式为例mkdir ai-agent-demo cd ai-agent-demo python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate激活虚拟环境后安装依赖pip install openai python-dotenv需要说明的是这里的 openai 库不仅支持 OpenAI 官方接口也支持其他提供“OpenAI 兼容接口”的模型服务。后续代码只依赖它不引入其他重量级框架。3.3 模型配置方式把模型配置放到环境变量或 .env 文件中不要写死在代码里。在项目根目录创建 .env 文件API_KEYsk-xxxx BASE_URLhttp://127.0.0.1:11434/v1 MODEL_NAMEqwen2.5:7b使用 Python 读取import os from dotenv import load_dotenv load_dotenv() API_KEY os.getenv(API_KEY) BASE_URL os.getenv(BASE_URL) MODEL_NAME os.getenv(MODEL_NAME)如果是本地 OllamaOllama 的 OpenAI 兼容地址默认是http://127.0.0.1:11434/v1API Key 可以随便填一个非空字符串。如果是云端服务请按服务商提供的地址和密钥填写。3.4 环境检查清单在写业务代码前先确认环境是否可用。这是很容易被跳过的步骤但能省下大量排查时间python --version能正常输出且版本不低于 3.10。ollama list能看到已下载模型如果没有先执行ollama pull qwen2.5:7b下载。用一行脚本请求模型接口确认能拿到非空的回答。.env文件已经创建并且python-dotenv安装成功。4. 动手实现一个最小 ReAct Agent不借助任何框架4.1 定义两个基础工具为了让示例足够简单又可运行这里定义两个工具一个是获取当前日期时间一个是计算数学表达式。import datetime import json def get_datetime(): 返回当前日期和时间 now datetime.datetime.now() return now.strftime(%Y-%m-%d %H:%M:%S) def calculator(expression: str): 计算简单数学表达式例如 1 2 try: result eval(expression, {__builtins__: {}}, {}) return str(result) except Exception as e: return f计算出错: {e}注意这里的calculator使用eval仅用于学习演示。生产环境绝对不要这样写否则会引入严重的安全风险。后面会专门说明正确的工具设计方式。4.2 构造工具说明列表模型需要知道“有哪些工具”“每个工具是干什么的”“参数长什么样”因此要维护一个 JSON 结构的工具说明列表tools [ { type: function, function: { name: get_datetime, description: 获取当前日期和时间, parameters: { type: object, properties: {}, required: [] } } }, { type: function, function: { name: calculator, description: 计算数学表达式例如 17 * 9, parameters: { type: object, properties: { expression: { type: string, description: 要计算的数学表达式 } }, required: [expression] } } } ]4.3 写核心循环核心循环可以分为三步调用模型、判断是否有工具调用、执行工具并回传结果。这里给出完整脚本注意先阅读注释理解流程。from openai import OpenAI from dotenv import load_dotenv import os import json load_dotenv() client OpenAI( api_keyos.getenv(API_KEY), base_urlos.getenv(BASE_URL), ) # tools 列表见上方略 def run_agent(user_query: str, max_steps: int 8): messages [ { role: system, content: 你是一个智能体。当需要调用工具时必须返回工具调用请求。 工具调用结束后你会收到工具返回结果。 如果不需要调用工具直接回答用户。 }, {role: user, content: user_query} ] for step in range(max_steps): print(f\n--- Step {step 1} ---) response client.chat.completions.create( modelos.getenv(MODEL_NAME), messagesmessages, toolstools, temperature0, ) message response.choices[0].message # 如果没有工具调用说明模型已经给出最终答案直接结束 if not message.tool_calls: print(最终回答:, message.content) return message.content # 将 assistant 消息加入 messages模型工具调用的上下文必须原样回传 messages.append(message) # 执行每一个工具调用 for tool_call in message.tool_calls: function_name tool_call.function.name arguments tool_call.function.arguments args json.loads(arguments) if arguments else {} print(f调用工具: {function_name}, 参数: {args}) if function_name get_datetime: result get_datetime() elif function_name calculator: result calculator(**args) else: result f未知工具: {function_name} # 工具返回结果必须使用 roletool 的消息 messages.append({ role: tool, tool_call_id: tool_call.id, content: result, }) # 超出最大步数时给出提示避免死循环 print(达到最大步数强制退出) return None if __name__ __main__: print(run_agent(现在几点了17 乘以 9 等于多少))这就是一个最小可运行的 ReAct Agent。整个逻辑不依赖任何 Agent 框架只依赖模型的工具调用能力和你自己写的循环控制。4.4 这个最小实现最值得研究的几个细节第一为什么模型会返回tool_calls因为请求里传了验证过的工具列表模型经过训练后会在觉得需要外部信息时输出结构化的工具调用请求而不是普通文本。第二为什么要把 assistant 消息原样回传因为模型需要知道“自己上次说了什么、请求调用哪个工具”这部分上下文不能丢失。只回传一行“你调用了工具”是不够的必须保留完整的tool_call结构和 ID。第三为什么要循环而不是一次调用完成因为一个任务可能需要多个工具配合。比如先查当前时间再根据时间计算一个表达式整个过程可能需要两三轮才能完成。第四为什么要设置最大步数因为模型可能反复调用同一个工具、输出循环或陷入死循环。max_steps是最后一道保险学习阶段设置成 6 到 10 即可。5. 工具调用与提示词编排Agent 能落地的关键5.1 函数调用Function Calling的请求与响应结构上面代码里tools参数是关键。它告诉模型“你可以调用这些函数”但模型本身不会执行任何函数。真正执行函数的是你的代码执行完再把结果通过roletool的消息回传。这个过程通常称为函数调用或工具调用。一次典型交互分为三段第一段用户请求“现在几点了17 乘以 9 等于多少”第二段模型返回一个 tool_calls 结构包含函数名和 JSON 参数。{ tool_calls: [ { id: call_abc123, type: function, function: { name: get_datetime, arguments: {} } }, { id: call_abc456, type: function, function: { name: calculator, arguments: {\expression\: \17 * 9\} } } ] }第三段你的代码执行函数返回结果。{ role: tool, tool_call_id: call_abc123, content: 2026-03-12 14:30:00 }随后再次调用模型模型会综合这些信息生成最终回答。理解这个三段式结构是排查 Agent 问题的核心。很多模型回答异常本质上是 messages 中的角色、tool_call_id 或 content 拼接不正确。5.2 对大模型的约束应该放在 System Prompt 还是函数定义中System Prompt 适合放全局行为约束比如“不要编造工具返回值”“如果工具报错请直接说明错误”“回答要简洁”。而工具描述应该写到函数定义里因为模型会根据description判断何时调用该工具。推荐的写法system: 你是智能体。你需要根据用户问题判断是否调用工具。所有工具返回结果都以事实为准。 如果工具调用失败不要猜测直接把错误信息告诉用户。 function description: 计算数学表达式例如 17 * 9。参数 expression 是一个字符串形式的表达式。不要写“你一定要用这个工具”“绝不能直接回答”模型并不严格理解感叹号和绝对化表达清晰的描述反而更有效。5.3 工具设计规范工具设计质量直接决定 Agent 能否稳定工作。常见规范包括工具粒度适中一个工具只做一件事。比如“查询天气”和“发送天气通知”不要合并成一个工具。输入一定要校验模型传参可能出错比如把字符串传给数字参数。工具内部要防御性编码。返回值要结构化优先返回 JSON 或固定格式字符串方便模型理解。错误要显式返回工具执行失败时返回的结果里必须包含错误原因而不要抛异常中断整个程序。敏感操作要二次确认扣款、发消息、删除类操作必须有人工确认节点。尽可能幂等比如“关闭工单”重复调用同一个工单 ID 时不应该产生新的副作用。5.4 为什么不要让模型输出 JSON 再手动解析有些教程会让你写“如果模型认为需要查天气请输出 {tool: weather, city: 北京}然后我的代码用 json.loads 解析。”这种方式学习可以但生产不建议。原因是模型直接输出自然语言 JSON 时格式不稳定可能出现换行转义、字段缺失、中文标点等问题。函数调用机制由模型厂商在训练阶段做了对齐输出稳定性更高并且自带 tool_call_id方便消息回传。如果使用的是不支持函数调用的模型才需要退回到“提示词约束 正则/json 解析”的折中方案。6. 从手写循环到框架Dify、Coze、LangGraph、Hermes 怎么选6.1 什么时候该用框架什么时候不需要手写 ReAct 的最大好处是原理透明任何环节出错都能定位。但缺点是随着业务复杂代码会越来越长要处理记忆、多用户会话、并发、日志、权限、人审、监控。此时引入框架能显著减少重复工作。判断标准可以这样看场景是否推荐用框架推荐方案学习原理、毕业设计、原型验证不推荐手写 Python可视化工作流、快速搭建内部工具推荐Dify无代码/低代码搭建业务 Bot推荐Coze 这类平台复杂图状态、多分支编排、需要持久化和回放推荐LangGraph想基于开源项目二次开发快速获得管理端推荐开源 Agent 项目例如 Hermes 类项目6.2 主流方案对比维度手写 PythonDifyCozeLangGraph上手难度中等低很低较高可视化编排无有有弱可控性最高中低高知识库支持需自行实现内置内置需自行集成记忆需自行设计内置内置需自行处理生产级能力全部自己搭中到高中高适合人群想深入理解原理快速交付业务人员/原型需要复杂状态编排注意Coze 在不同地区提供的产品能力可能不一样实际使用时以官方文档为准。6.3 Hermes 这类开源项目在 Windows 上部署要注意什么网络上经常能看到“在 Windows 上部署 Hermes 智能体”的提问。这里不针对某一个项目做具体安装教学而是给出通用判断思路开源智能体项目到了生产环境本质上是一个 Web 服务或 API 服务部署时关注的是依赖、数据库、端口、环境变量四个问题。在 Windows 上部署这类项目时推荐顺序如下优先使用 WSL2 Docker Desktop 运行而不是直接裸跑 Python 脚本。原因是开源项目依赖较多常见的问题比如某个底层库不支持 Windows、端口被占用、环境变量加载路径不同在 Docker 容器里都能减少环境差异。查看项目文档中的 Docker 部署方式是否有docker-compose.yml。如果有修改.env文件中的数据库连接、端口映射和密钥。如果项目明确原生支持 Windows再考虑直接使用 Python 虚拟环境安装。不要把.env文件提交到 Git 仓库里面通常包含 API Key、数据库密码等敏感信息。学习阶段使用项目内置的 SQLite 即可不要一开始就切换到 PostgreSQL除非官方要求。如果遇到启动失败先看日志。常见的失败原因是数据库未启动、依赖版本不匹配、环境变量缺失真正复杂的逻辑问题反而少见。6.4 自建 Agent 最小的生产组件无论是否使用框架进入生产环境前至少要有这些组件对外 API 服务把 Agent 包装成 HTTP 接口例如用 FastAPI。请求去重和幂等同一个用户重试请求时不应重复扣费或重复发消息。结构化日志记录请求 ID、模型名、工具名、耗时、异常、退出原因。权限控制谁可以调用哪些工具必须有一个白名单或角色体系。敏感操作人审删除类、支付类、发送类操作需要人工确认。失败回退模型超时、工具异常时用户得到的是明确提示而不是空白页面。7. 运行验证、日志分析和常见报错7.1 用三组 Case 验证 Agent 是否真的可用写完成一个 Agent 后不能只看“能跑通”就结束应该系统性验证。推荐从三组 case 开始。Case 1多步工具调用。提问“现在几点了17 乘以 9 等于多少”。预期现象是模型先调用get_datetime再调用calculator最后综合结果回答。如果模型一次调用就能同时执行两个工具取决于模型能力和函数定义都属于正常表现。Case 2无关闲聊。提问“你好你是谁”。预期现象是模型不调用任何工具直接生成回答。如果模型仍然强行调用工具说明工具描述写得过宽或者 system prompt 把它逼得太紧。Case 3工具异常。提问“计算 1 除以 0”。预期现象是工具内部返回“计算出错”模型根据观察结果告诉用户“除数为零无法计算”。如果模型忽略工具返回结果硬编一个错误答案说明消息回传或观察能力有问题。三组 case 分别覆盖“正常工具调用”“无需工具调用”“工具异常处理”是 Agent 最基础的稳定性测试。7.2 排查链路从现象倒推原因出现 Agent 行为异常时按下面顺序排查模型是否触发了工具调用如果没有优先检查tools参数是否传入、工具描述是否与用户问题匹配、temperature是否过高。工具是否执行成功自己手动运行一次工具函数确认入参解析是否正确、函数内部有没有异常被吞掉。消息回传是否正确确认tool_call_id是否匹配assistant 消息是否原样保留role是否为tool。循环是否正常退出确认是否达到max_steps日志里是否反复调用同一个工具。最终回答是否准确检查模型是否把工具观察结果纳入判断还是凭空猜测。7.3 常见错误现象表现象常见原因检查方式处理建议模型不调用工具直接给出猜测答案tools 参数未传、描述不够清晰、模型版本不支持函数调用打印请求参数确认 tools 存在查看模型文档选用支持函数调用的模型优化工具描述工具执行报错但模型还在编答案异常被吞掉、工具结果未回传检查 tool message 是否真的 append 到 messages工具函数内部捕获异常并返回错误描述同一工具被反复调用陷入循环工具返回结果没有让模型确认“成功”或结果格式不清查看日志里每轮 observation 内容增加 max_steps让工具返回结果更明确模型报错提示 tool_call_id 不匹配assistant 消息没有原样回传检查代码是否重新构造了 assistant 消息直接 append 原始 message 对象中文 JSON 参数解析失败模型返回了中文标点或格式不规范打印原始 function.arguments使用支持函数调用的模型若不支持加强提示词约束7.4 结构化日志建议生产环境建议按 JSON 输出结构化日志方便接入日志平台{ request_id: req_001, user_id: u_001, model: qwen2.5:7b, input_tokens: 128, output_tokens: 64, tool: calculator, tool_duration_ms: 12, status: success, finish_reason: stop }日志至少要覆盖什么时候用户发起了请求、模型输出什么决策、工具执行多久、结果是成功还是失败、最终以什么原因退出。缺少日志的 Agent 项目进入生产后排查问题会非常痛苦。8. 生产落地要做到的防护和优化8.1 工具权限与人审机制工具权限是整个 Agent 系统安全性的核心。模型能调用的能力越多风险越大。常用策略是分级黄色工具只读类操作如查天气、查数据库视图、搜索文档允许模型自主调用。红色工具有副作用的操作如发送邮件、修改订单状态、删除数据必须经过用户确认或人工审批。黑色工具涉及资金、账号、隐私的操作一般不允许模型直接调用只能通过专用网关处理。实现时可以在工具描述里注明“此操作需要用户确认”也可以在代码层拦截模型返回工具调用请求后先进入确认节点用户点确认再执行。8.2 记忆管理的两种思路短期记忆就是当前 messages 列表。随着对话变长token 会越来越多需要做裁剪保留 system 消息、最近 N 轮对话把更早的内容压缩成摘要。长期记忆一般依赖外部存储。把用户的历史意图、偏好、重要事实提取为结构化记录存到关系型数据库或向量库。下次对话时先检索相关记忆再注入 system prompt。生产项目不要把所有历史全部塞给模型成本高且噪音大。先聚合提炼再按需注入。8.3 成本与延迟控制设置最大工具调用次数避免模型陷入循环造成大量消耗。对工具结果做缓存比如天气、股票、汇率这类数据可以缓存 5 到 15 分钟。使用路由策略简单问题走便宜小模型复杂任务才调用大模型。对长文本结果做截断工具返回内容超过阈值时只保留关键部分。记录每个请求的 token 消耗设置每日账单告警。8.4 发布前检查清单检查项确认内容工具安全是否有 eval 式危险函数是否校验输入敏感操作删除、支付、发送类操作是否有人审最大步数是否设置 max_steps防止死循环消息结构assistant、tool 消息是否按规范回传日志是否记录 request_id、工具名、耗时、异常配置外置API Key、模型名是否放到 .env不硬编码异常兜底模型超时、工具异常时用户是否能看到明确提示成本控制是否有 token 消耗监控和每日告警9. 本文最重要的一条实践建议AI Agent 开发的关键不在“会用哪个框架”而在于能不能把“模型决策、工具执行、结果验证、循环终止”这条链路控制好。如果你只能记住一件事那就是先把最小的 ReAct Agent 跑通再往里面加工具、加记忆、加框架。Dify、Coze、LangGraph、Hermes 都只是工具理解原理后学起来很快不理解原理换再多框架也解决不了 Agent 回答不稳定、工具调用失败、循环失控这些核心问题。下一步可以这样做把本文第 4 节的脚本完整运行一遍然后替换成两个自己的工具。比如一个查汇率的工具、一个查本机文件列表的工具然后尝试让 Agent 完成“查询汇率后计算 100 美元能换多少人民币”这类复合任务。跑通之后再考虑引入 FastAPI 包装成 HTTP 服务逐步补齐日志、权限和人审就是一条从学习到生产落地的完整路径。
返回列表