尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
手把手实现ReAct Agent:学习计划生成实战与避坑指南
从零开始搭一个像样的Agent难点往往不在某个框架怎么用而在“循环”这个东西到底怎么写。我连续记录了十几天AI Agent学习笔记今天第14天终于把一个完整的ReAct Agent跑通了。这个Agent的目标很具体让AI根据用户输入的一句话自动生成一份可执行的学习计划。它会自己拆解目标、调用工具查日期和课程库、按周排期、最后输出一份Markdown计划文档。整个过程走的是ReAct路线也就是Reasoning与Acting交替进行。这篇文章把完整实现的思路、代码和实测轨迹都记录下来尤其会讲清楚那些文档里不会写的细节和坑适合正在学Agent、想动手写完一个能跑通的任务型Agent的开发者参考。1. 为什么非要ReAct把推理和行动塞进同一个循环1.1 一个Prompt做不到的事在学Agent之前我对LLM的用法还停留在“一次调用、输出结果”。但当你让AI真正去做成一件事比如生成学习计划你很快会发现单次Prompt有硬伤模型不知道今天几号就没法按日期排期模型不知道课程库里有什么课就只能凭记忆编课程名模型生成完计划也没能力把文件保存下来。这些事单靠Prompt词写的再花哨也没用因为模型缺少对外部世界的影响能力。ReAct的方式是把整个过程拆成一个循环模型先思考下一步该干什么然后执行一个具体动作看到动作的结果再继续思考。这个循环一直持续到模型认为任务已经完成。这套思想其实不复杂。想想人是怎么做菜的先看菜谱想清楚要做红烧肉Thought去冰箱拿五花肉Action发现肉没解冻Observation于是决定先放微波炉解冻Thought再执行解冻Action。ReAct就是把这种“想一步、做一步、看一步”的过程模型化让LLM在其中承担“大脑”的角色各种工具承担“手脚”的角色。1.2 三角色循环Thought、Action、ObservationReAct里的三个核心角色每个都有明确分工Thought模型的推理过程解释“我为什么这么做”这一部分通常不展示给用户但对调试很有用。Action模型决定要执行的动作例如调用某个工具、访问某个函数需要带参数。Observation工具执行之后返回的结果作为新的输入重新喂给模型。整个循环用伪代码看就是while not finished: response llm(conversation_history, tools) if response.contains_final_answer(): return response.final_answer else: tool_name response.action tool_args response.arguments result execute_tool(tool_name, tool_args) conversation_history.append(observationresult)这个循环之所以有效是因为每一步的观察结果都会改变模型下一步的推理方向。模型第一次可能决定查日期看到日期后决定查询课程库看到课程库结果后再决定怎么排计划。没有这个循环模型只能一次性输出一个看似合理但没有任何外部信息支撑的计划。1.3 两种落地方式怎么选我这次实现参考了两种ReAct落地方式。第一种是走LLM原生Function Calling能力模型返回结构化的工具调用请求代码里直接判断tool_calls字段是否为空。这种方式解析稳定基本不会出现格式错乱适合生产环境。第二种是纯文本协议方式也就是让模型输出固定的Thought/Action/Action Input文本再用正则或字符串解析。这种方式对模型要求更高但兼容任何模型不需要特别训练支持Function Calling。我这次选了Function Calling方向原因是稳定性更好。文本协议我前期试过效果其实也OK但模型偶尔会在Action后的解释里混入不该出现的文字解析器一崩就是连锁反应。两者我也做了个简单对比对比维度Function Calling方式纯文本协议方式解析稳定性高模型返回结构化字段中依赖格式约束和解析器模型兼容性需要支持工具调用任意模型均可调试直观度工具调用和普通回复区分清楚需要看原始文本实现复杂度略高略低适合学习原理项目建议生产项目首选学习原理或对接老模型时用如果你的目标是把ReAct机制彻底搞懂我建议两种方式都写一遍尤其是纯文本协议那版会让你对交互格式的理解深入很多。如果只想要一个能跑的结果直接上Function Calling就行。2. 整体设计给学习计划生成Agent搭骨架2.1 需求拆解在动手写代码之前我先花时间把“生成学习计划”这个需求拆清楚了。用户可能输入这样一句话我零基础想用3个月系统地学习机器学习帮我安排一个学习计划。如果直接让LLM输出计划它大概率会给出一个看起来很专业、但完全没有考虑现实约束的文档。我期望这个Agent产出的是一份包含阶段划分、每周具体任务、参考资料、自检方式的可执行计划。为了达到这个目标Agent至少要回答这几个问题今天是几号3个月后是哪天几号开始执行课程库里有哪些适合零基础用户的课程每个课程多长时间学习目标如何分解到周每周任务怎么做最终计划如何落地成一份可保存、可阅读的文档这四类问题分别对应了“获取日期信息”“查询课程库”“推理规划”“调用输出工具”四个能力需求。这也是ReAct Agent和普通对话机器人的核心区别它不是想起来什么说什么而是为了完成任务主动调用工具、获取信息再基于信息做决策。2.2 三个核心模块的职责边界整个Agent我拆成了三个模块职责边界一开始就别扯到一起工具注册表维护所有可用工具的名称、描述、参数Schema、执行函数。模型不允许调用未注册的工具。循环控制器负责维护对话历史调用LLM判断模型输出是工具调用还是最终回答执行工具并把结果写回历史。任务记忆也就是对话上下文。包含System Prompt里的角色设定、用户目标、每轮工具调用的输入和输出。这里最需要注意的是任务记忆的维护。工具调用结果如果不回写到对话历史里模型就相当于失忆了下一步没法基于观察结果推理。很多新手Agent写出来总是一顿操作猛如虎最后回答却完全没用到工具查询结果基本都是这个环节漏了。2.3 为什么选循环而非单次多步调用这个选型的核心动机是可观测性和可控性。如果采用单次调用让模型一次性输出完整计划那中间任何一步出错你只能看到最终结果根本不知道是日期算错了还是课程信息编造了。而用循环结构每一轮Thought和Observation都留在对话历史上我作为开发者可以完整追踪Agent的每一步决策。另一个原因是容错。循环结构天然支持重试如果工具返回异常可以把异常信息作为Observation回传给模型让模型决定换个参数重试或者改用别的方案。这种自适应能力是单次调用完全不具备的。3. 完整实现手写一个学习计划ReAct Agent3.1 工具层设计与注册工具层是Agent的“手脚”。我在这个Demo里注册了三个工具不多不少刚好覆盖一个完整ReAct流程需要的信息获取和输出落地能力get_current_date获取当前日期用来推算学习周期的起止时间。search_courses按主题、难度关键词查询课程库返回匹配的课程列表。save_plan将Markdown格式的最终计划保存到本地文件返回文件路径。每个工具都按Function Calling的标准格式定义重点是把description写得足够精确。这里有个实操经验模型的工具选择能力很大程度上依赖description质量描述太含糊模型就不知道该在什么时候调哪个工具。TOOLS [ { type: function, function: { name: get_current_date, description: 获取当前日期。当需要推算学习周期、截止日期或排期时调用此工具。, parameters: { type: object, properties: {}, required: [] } } }, { type: function, function: { name: search_courses, description: 按主题和难度查询课程库中的可用课程。返回课程名称、时长和难度。, parameters: { type: object, properties: { topic: { type: string, description: 学习主题如机器学习、Python基础 }, level: { type: string, enum: [beginner, intermediate, advanced] } }, required: [topic] } } }, { type: function, function: { name: save_plan, description: 将生成的学习计划以Markdown格式保存到本地文件。, parameters: { type: object, properties: { content: { type: string, description: 完整的Markdown格式学习计划内容 }, filename: { type: string, description: 文件名需以.md结尾 } }, required: [content, filename] } } } ]search_courses的真实实现就是一个字典加过滤逻辑但为了真实感我给它模拟了一个课程库COURSE_DB [ {name: 机器学习导论, topic: machine_learning, level: beginner, weeks: 6}, {name: Python快速入门, topic: python, level: beginner, weeks: 4}, {name: 线性代数与概率统计基础, topic: math, level: beginner, weeks: 5}, {name: 机器学习实战项目, topic: machine_learning, level: intermediate, weeks: 8}, {name: 深度学习进阶, topic: deep_learning, level: advanced, weeks: 10} ] def search_courses(topic, levelNone): result [c for c in COURSE_DB if topic.lower() in c[topic]] if level: result [c for c in result if c[level] level] return result如果你在自己项目里做工具层我建议把工具执行函数做成统一的接口签名比如都是params字典进、字符串出。不要搞五花八门的返回类型因为Observation要拼进对话历史统一成字符串大大简化处理逻辑。3.2 Agent循环核心代码循环控制器是Agent的心脏。逻辑写出来其实就十几行但每行的位置都很有讲究。我先把完整代码贴出来再逐行拆。import json def run_agent(user_input, max_steps8): messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input} ] for step in range(max_steps): response call_llm(messages, toolsTOOLS) msg response.choices[0].message if msg.tool_calls: messages.append({ role: assistant, content: None, tool_calls: msg.tool_calls }) for tool_call in msg.tool_calls: tool_name tool_call.function.name arguments json.loads(tool_call.function.arguments) result execute_tool(tool_name, arguments) messages.append({ role: tool, tool_call_id: tool_call.id, name: tool_name, content: str(result) }) else: return msg.content return 已达最大步数限制任务未在预期内完成 def execute_tool(name, args): if name get_current_date: return get_current_date() if name search_courses: return search_courses(**args) if name save_plan: return save_plan(args[content], args[filename]) raise ValueError(f未知工具: {name})有几个细节我要特别指出。第一个是max_steps。循环必须有个上限否则模型一旦陷入反复调用工具的循环你的token账单会非常难看。我这次设8步对一个学习计划任务来说完全够用。第二个是tool_call的messages回写格式。assistant这条消息必须原样带上tool_calls字段然后在后续消息里用tool_role类型一一对应工具结果。这里有个常见坑忘记回写assistant的tool_calls消息直接写tool消息很多接口会报错或者模型无法正确关联。第三个是执行工具时要包一层异常处理。工具执行抛异常的情况很常见比如参数缺字段、文件路径不存在。我的做法是捕获异常后把异常信息作为Observation回传让模型自己决定怎么处理。这样就少了很多因为工具崩溃导致整个Agent挂掉的场景。try: result execute_tool(tool_name, arguments) except Exception as e: result f工具执行失败: {str(e)}3.3 学习计划任务的提示词设计System Prompt在这个Agent里承担双重角色既定义行为边界又定义任务目标。我写的是SYSTEM_PROMPT 你是一个学习规划助手。你的目标是根据用户的学习目标生成一份可执行的学习计划。 在生成计划之前你必须 1. 先调用 get_current_date 获取当前日期明确学习周期的起止时间。 2. 调用 search_courses 查询课程库只使用课程库中真实存在的课程。 3. 将完整计划保存为 Markdown 格式文档。 计划中必须包含 - 学习目标拆解至少拆成3个阶段 - 时间安排按周划分注明起止日期 - 每周学习内容与产出物 - 自检清单每个阶段结束后如何验证学习成果 只有当你确认计划生成完毕并保存成功后才输出最终的总结回答。 你的原始回复中不要出现Thought/Action等调试字段只输出给用户的最终计划说明。 这段Prompt我调过好几轮。第一版没有强调“只使用课程库中真实存在的课程”结果模型编出了两个根本不存在的课程名放进计划里这属于工具诈骗式的幻觉。加了这条约束后模型老实多了。第二版加了“只有保存成功后才输出最终回答”这个也重要否则模型会在调用save_plan之前就抢先输出总结导致计划根本没存下来。3.4 一次真实运行的轨迹复盘我在测试环境跑了一个案例输入是我零基础想用3个月系统地学习机器学习帮我安排一个学习计划。Agent的完整轨迹记录如下第一轮Thought用户是零基础目标是机器学习周期3个月。我需要先确认起始日期才能把3个月折算成具体的周数和截止时间。Actionget_current_dateObservation2025-06-10第二轮Thought今天是6月10日3个月后是9月10日左右。接下来查询课程库中适合初学者的机器学习课程看看课程库里有哪些可用资源。Actionsearch_courses(topicmachine_learning, levelbeginner)Observation[{name: 机器学习导论, level: beginner, weeks: 6}]第三轮Thought课程库里零基础机器学习课程只有机器学习导论但用户要学3个月只学一个6周课程明显不够。我需要同时查询Python基础课因为机器学习零基础通常需要先补编程基础。Actionsearch_courses(topicpython, levelbeginner)Observation[{name: Python快速入门, level: beginner, weeks: 4}]第四轮Thought现在的组合是Python快速入门(4周) 机器学习导论(6周)加起来10周仍然有空余。查询一下实战项目课程让计划后半段更充实。Actionsearch_courses(topicmachine_learning, levelintermediate)Observation[{name: 机器学习实战项目, level: intermediate, weeks: 8}]第五轮Thought如果加入机器学习实战项目总周期会超过3个月。我决定这样安排前4周学Python快速入门中间6周学机器学习导论最后的2周做一个综合性的学习收尾与自测。现在把完整计划生成为Markdown并保存。Actionsave_plan(...)Observation保存成功学习计划已保存到 /plans/machine_learning_plan.md第六轮Thought计划已成功保存任务完成。输出最终总结。Final根据你的要求我为你生成了12周的机器学习入门学习计划覆盖Python基础、机器学习核心理论与综合实战自测各阶段的具体安排和自检方式如下……这段轨迹展现了ReAct最有价值的部分模型在第三轮发现课程库存量不够时自己调整了检索策略没有硬编造课程。这是纯Prompt生成做不到的自适应行为。我拿到这份轨迹时最直观的感受是Agent不再是“一问一答”它真的在一个目标驱动下像人一样先搞清楚条件再行动。4. 实操中的坑与排查实战4.1 循环不终止Agent反复调用同一个工具我实际跑的时候遇到第一个大坑Agent在查到课程列表之后反复调用search_courses每次参数都一样来回好几次。原因是工具返回结果被原样塞进对话后模型觉得“信息还不够”但又不清楚接下来真正该做什么。解决思路有两层。第一层是靠max_steps兜底这个必须有哪怕只是防止账单爆炸。第二层是在System Prompt里加一句当工具返回数据不足以支撑决策时说明理由并基于已有信息做出最优判断继续推进任务。我改完这句之后循环次数明显下降了。还有一个方案是检测到连续两次相同的工具调用时主动打断并提醒模型但这个比较复杂我的项目里还没用上。4.2 工具参数解析失败模型返回的工具参数偶尔会不符合Schema。最常见的两种一是JSON格式里混入了多余逗号二是必填字段缺失。我加了一层轻量级的参数校验和修复def safe_parse_args(raw_args, required_keysNone): try: args json.loads(raw_args) except json.JSONDecodeError: args {} if required_keys: for k in required_keys: if k not in args: args[k] return args这里经验是不要指望模型每次都完美按Schema输出。线上项目最好的做法是把“参数校验失败”本身作为一个Observation返回给模型让它根据错误信息自我修正。我在Demo里做了更温和的处理缺的字段给空值但生产环境我建议还是发回重拟因为空值可能导致工具执行产生错误结果。4.3 上下文长度爆炸随着工具调用轮数增加对话历史里的Observation会越堆越多。尤其是我把整个课程库JSON都作为工具返回时几轮下来消息体积就很可观。模型上下文窗口有限历史一旦接近上限轻则态度变差重则报错。我的临时方案是给工具返回加上截断逻辑。比如search_courses返回结果超过5条时只保留前5条并追加提示“共N条结果已截断”。另一个思路是在Agent循环里对历史做摘要压缩这是后续要优化的方向。如果你在做生产级系统建议在进入Agent循环前估算token占用达到阈值就触发摘要压缩或遗忘机制。4.4 模型答非所问不按计划执行没加严格的流程约束之前模型经常跳过工具调用直接回答用户“我可以帮你制定一个学习计划首先是……”。这本质上是模型习惯了聊天不习惯干活。排查下来根因是System Prompt里的“必须调用工具”约束不够强。后来我在Prompt里明确了顺序“在生成计划之前必须先调用get_current_date不允许跳过此步骤否则认为你没有完成准备阶段。”同时把生成流程拆成了准备阶段和编排阶段模型就很少再跳了。如果还出现这种情况可以把temperature调低到0.2左右减少模型自由发挥的空间。5. 后续还可以怎么扩展5.1 对话式计划调整现在这个Agent只能一次性生成计划。扩展方向是让用户看完计划后说“第三周时间太紧了帮我减掉一半内容”Agent就能基于已有的对话历史先读取已保存的计划文件再针对第三周做局部修改。这需要新增一个read_plan工具让Agent能主动读取文本再修改。本质上是把单轮的ReAct扩展成多轮会话式ReAct核心循环不变只是把“用户输入”从一次变成持续交互。5.2 多Agent分工协作我这十几天的笔记里多Agent是另一个大方向。比如规划Agent负责拆解目标检索Agent专门负责查课程资料质检Agent负责检查计划是否包含自检机制。每个Agent各自维护一个小型ReAct循环通过消息交互完成协作。这套架构的好处是每个Agent职责单一System Prompt约束起来更容易问题定位也更清楚。代价是多Agent串行调用会增加latency和token消耗小项目不建议一上来就上这种复杂度。5.3 持久化与执行追踪学习计划生成出来只是第一步计划执行阶段更有价值。扩展思路是把用户的每日学习打卡回填到一个状态文件里Agent定期对比计划进度与实际进度生成调整建议。这样Agent就从“一次性规划工具”变成了“长期学习伴侣”对应的是记忆持久化和状态管理的问题。这也是我接下来几天要实验的方向后面有了结果再单独写一篇记录。这次完整实现ReAct Agent我个人最大的收获不是代码怎么组织而是真正理解了“目标驱动的循环”这个核心心智模型。如果你也正在学Agent我建议不要急着上框架先自己用几十行代码写完一个能跑的循环把Thought、Action、Observation的交互彻底吃透后面再接触任何复杂的Agent框架都会觉得顺理成章得多。
RELATED

相关推荐

IQ调制与解调:现代无线通信的基石

IQ调制与解调:现代无线通信的基石

很多朋友刚接触无线通信时都会有个困惑:课本里讲调制,无非就是把信号往载波上“搬”,乘个正弦波就行,可为什么一到工程实现,到处都是I、Q两路?发射机里要两路DAC,接收机里要两路ADC,…

📅 2026/10/18 4:02:11
基于文本挖掘的A股上市公司漂AI指数构建与十五年演变分析

基于文本挖掘的A股上市公司漂AI指数构建与十五年演变分析

不知从什么时候起,A股上市公司的公告和年报里,如果不出现几次“人工智能”“算力”“大模型”,好像就不好意思跟投资者打招呼。2023年ChatGPT点燃全球AI热潮之后,这种趋势更加夸张:一家主营配电箱的公司可以一口气发五…

📅 2026/10/18 4:02:11
AI工具链如何重塑内容生产:从选题到发布的完整工作流与实战避坑指南

AI工具链如何重塑内容生产:从选题到发布的完整工作流与实战避坑指南

2026年的内容行业,一个绕不开的词就是AI工具链路。我自己做内容运营这几年,明显感觉到单靠“勤奋”已经很难跟上节奏了。同样一篇深度稿,有人从下午写到大半夜,有人从选题到初稿只花了一支烟的功夫——差距不在文笔,而…

📅 2026/10/18 3:57:11
MORE NEWS

更多资讯

📰

Spring AI 实战:从 ChatClient 到多轮对话与工程化落地

第一次在 Spring 项目里接大模型,我其实是有点抗拒的。倒不是说抗拒 AI 本身,而是抗拒调一次接口要写的那一堆样板代码:不同模型服务商给的 SDK 风格不一样,参数命名不一样,返回结构也不一样。今天接这家,明…

📰

从Q-learning到DQN:网格寻宝中的强化学习实战

如果你在小区里见过那种“找不到路就原地打转,被同一把椅子卡住三次”的扫地机器人,大概就能秒懂强化学习要解决的核心问题——不是“知道路怎么走”,而是“在没有地图、没有向导的情况下,靠自己试错走出一条路来”。我最近把“强…

📰

AI芯片编译器后端:pto-isa寄存器分配与内存访问模型实践解析

做算子移植的时候,我在CANN的 pto-isa 工具链里调了一个非常折腾的寄存器分配问题。那段时间几乎天天对着llc -debug-onlyregalloc的输出分析虚拟寄存器的干涉图,折腾两周之后发现,问题的根子不在寄存器分配算法本身,而是出在我对…

📰

gRPC自定义健康检查:从进程存活到业务就绪的工程实践

1. 项目概述:为什么健康检查不是“配个探针就完事”的小事 gRPC 自定义健康检查,这六个字背后藏着的是微服务架构落地过程中最常被轻视、却最容易在凌晨三点把你从床上拽起来的硬骨头。我见过太多团队,在服务刚上线时用默认的 health.proto…

📰

gRPC自定义健康检查:从协议原理到生产落地

1. 项目概述:为什么健康检查不是“配个探针就完事”?gRPC 自定义健康检查,这六个字背后藏着的不是一句配置命令,而是一整套服务治理的底层逻辑。我第一次在某跨平台系统里遇到健康检查失效的问题时,整个服务网格里十几…

📰

会议录音不敢往云端传?实测6款录音转文字工具的隐私保护方案(2026选型指南)

做会议纪要、客户访谈、问诊记录、课题访谈的人,最近两年几乎都会遇到同一个纠结:录音转文字确实省事,但把一段包含战略讨论、客户报价、当事人陈述的音频丢进某个云端服务器,心里总是不踏实。更麻烦的是,很多工具在安…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬