从零实现Agent循环:ReAct与反思机制实战教程 我这段时间在跟进某AI教育平台和某AI公司合作推出的Agent实战系列课程刚完成了第一个任务。这个课程的定位很有意思它不像市面上那种泛泛讲概念的教学而是带着你从一个空白的工程目录出发一步步把Agent的骨架搭起来。如果你之前写过多轮对话机器人或者折腾过Function Calling但总觉得差点意思那这个任务正好能把你拉回正确的技术路线上。1. 项目拆解这个任务到底在做什么1.1 Agent技能的完整定义先说结论这个系列课程的核心不是“聊天的模型”而是“干活的Agent”。TASK1的目标非常明确——让你实现一个能正确调用外部工具的智能体循环Agent Loop并且给模型装配“技能”Skills。这里的“技能”不是神经网络里那个微调出来的技能而是工程意义上的能力封装你给Agent定义一批可调用的工具让它在推理时决定是否需要调用、调用哪个、传什么参数然后根据工具返回的结果继续推理直到完成任务。这背后其实就是业内常说的ReAct思想——推理Reasoning和行动Acting交替进行。课程里没有直接给你一个封装好的Agent框架而是让你自己实现这个循环。我一开始觉得有点重复造轮子但做完才发现只有亲手实现一遍你才能真正理解模型如何在“直接回答”和“调用工具”之间做决策工具返回结果之后上下文如何继续影响下一轮推理循环终止条件有哪些设计上的坑1.2 课程设计的层次感我看了整个系列的课程表TASK1只是入门篇但它依然拆成了三个层次第一层是基于ReAct的简单循环第二层是带反思机制Reflection的循环第三层才是完整工具应用开发。这种设计的好处是梯度很清晰你第一层先跑通最小闭环第二层再思考如何提升质量第三层再往生产环境迁移。TASK1要求的是前三层刚好覆盖了Agent从“能用”到“好用”的关键跳跃。第二个层次就是反思循环这才是课程的精髓。简单循环的问题很典型模型回答错了就错了没有任何纠错机制。反思循环加入了一个“评估者”角色——让模型重新审视自己上一轮的输出判断是否合理地使用了工具、答案是否完整、是否需要补充信息。这个思路和我在实际项目里做质检模块非常像只不过以前是人工审计现在是让另一个模型来审计。2. 环境准备与工具链选型2.1 API密钥管理的第一步任务要求调用某AI公司的模型API第一步当然是准备好API密钥。这里有个经验千万不要把密钥硬编码在脚本里哪怕你只是本地学习。我用的是python-dotenv来管理环境变量配置文件放在项目根目录的.env中并且确保.gitignore里忽略掉它。pip install python-dotenv anthropic然后建一个.env文件ANTHROPIC_API_KEYsk-ant-your-key-here加载方式from dotenv import load_dotenv import os load_dotenv() api_key os.getenv(ANTHROPIC_API_KEY)2.2 模型选择与参数理解课程里推荐使用某AI公司最新的高智能模型。我自己实测下来这个模型在函数调用方面确实强尤其是参数提取的准确性比我之前用过的几个开源模型好不少。但它的context window很大并不意味着你可以随便塞东西工具描述写得乱七八糟照样会翻车。初始化客户端的标准姿势from anthropic import Anthropic client Anthropic(api_keyapi_key) model claude-sonnet-latest这里的模型名我故意用了最新动态的占位写法你需要根据自己的API版本情况去官网查具体名称。温度temperature参数要谨慎调节。Agent任务不同于创意写作温度太高会导致模型不按工具格式输出建议初始值直接设为0。任务里没有硬性要求但你做第二轮反思循环时就会意识到温度一高反思结果就飘了结论不稳定根本没法自动化评估。2.3 为什么官方SDK比HTTP直连舒服我见过不少人喜欢用requests直接调HTTP接口但在Agent开发场景下我更推荐官方SDK。理由是流式输出的处理、tool_choice的强制模式、block类型的解析这些细节SDK都帮我搞定了。尤其是这个模型API的消息结构比传统OpenAI格式复杂了不少有一个“block”概念也就是每条消息是由多个内容块组成的可能是文本块也可能是工具调用块。直接用HTTP去拼结构会消耗大量时间在调试协议上反而忽略了Agent逻辑本身。3. 核心技能开发让Agent学会使用工具3.1 工具定义的精髓课程里强调了一点工具不是为了给模型“加功能”而是给模型“提供行动选项”。你要像设计API一样设计每一个工具。我做的第一个工具是一个简单的计算器函数用于演示词法解析。它的描述与参数Schema写得好不好直接决定模型能不能正确调用它。def calculate(expression: str) - float: 解析并计算一个简单数学表达式的值。支持、-、*、/。 import ast import operator as op operators { ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul, ast.Div: op.truediv, ast.Pow: op.pow, } # 词法解析 ...写工具描述时有3个实操经验描述必须说清楚“这个工具是干什么的”和“什么时候该用它”不含糊参数的JSON Schema不要偷懒type、description、required都写全示例值example能显著提高模型的参数填充准确率3.2 模型调用与工具分发的完整闭环Agent循环的核心部分我按照官方推荐的模式实现了一遍。这个模式写在官方文档里表述是“模型提供工具调用业务代码执行工具并返回结果循环继续”。关键点在于你需要在每次模型回答后检查是否有tool_use类型的block然后根据block中的name和input去路由到对应的本地函数。我这里演示一段伪代码结构你会有直观感受def run_agent(user_query: str, system_prompt: str, tools: list, max_iterations: int 10): messages [] messages.append({role: user, content: user_query}) for _ in range(max_iterations): response client.messages.create( modelmodel, systemsystem_prompt, toolstools, messagesmessages, max_tokens4096, temperature0, ) assistant_message response.content messages.append({role: assistant, content: assistant_message}) has_tool_use any(block.type tool_use for block in assistant_message) if not has_tool_use: break for block in assistant_message: if block.type tool_use: tool_name block.name tool_input block.input result dispatch_tool(tool_name, tool_input) messages.append({ role: user, content: [ { type: tool_result, tool_use_id: block.id, content: str(result), } ] }) return messages[-1]这段代码揭示了一个关键机制工具调用的结果不是简单地追加一条user消息而是必须以特定格式的tool_result块返回并且要用tool_use_id去关联之前的调用。如果你在这里做错了模型就会迷茫因为它根本不知道这个工具结果是对应哪一次调用。3.3 强制参数校验的底层设计在做TASK1时我还额外给自己加了一道工序对工具输入做运行时校验。模型可能返回格式不合法、参数缺失或者类型错误的调用你要是直接把这个畸形参数塞进本地函数大概率得到一堆难看的堆栈信息。我给dispatch_tool函数加了一个包装器先做类型检查和必填项检查只把合法的调用分发给真正的函数。这里列一下我经过几次踩坑总结出来的工具定义清单工具数量不要贪多一次任务装配5-8个足够工具名称用动词开头的蛇形命名如search_order、get_user_info描述中不要出现“maybe”“approximately”这类不确定词汇会影响模型的判断4. 实操过程与核心环节实现4.1 从“简单循环”跑通最小闭环我先实现了最简单版本的Agent循环搞了一个时间查询工具和一个日期计算工具。因为模型自己不具备实时时间感知能力它必须通过工具来获取“现在是什么时间”“今天是星期几”这类信息。这个Demo很形象地展示了工具对于Agent的必要性。当用户问“今天是几号”模型不会直接编一个答案而是主动生成一个工具调用块{ type: tool_use, id: toolu_..., name: get_current_date, input: {} }然后我的dispatch函数把当前日期返回给模型{ type: tool_result, tool_use_id: toolu_..., content: 2025-06-12 }模型再接收到这个结果后就能继续回答用户的问题。我这里建议你用一个比较“笨”的语言模型的旧版本来做对比——旧版模型会直接说“我不知道”而新一代模型至少知道去调用工具获取信息这就是能力等级的差异。4.2 反思循环从“完成”到“高质量完成”简单循环跑通后我开始实现第二个层次——Reflection Loop。课程设计的反思循环不只是一个额外的“review”步骤它是让整个Agent从“一锤子买卖”变成“可迭代优化”的关键。具体实现思路在模型自然结束对话也就是不再调用工具后追加一个“审核”提示让模型以批判视角检查自己的回答。如果发现回答有误、缺漏、推理链条断裂就返回一个“需要修正”的信号继续下一轮迭代如果审核通过就正式输出结果。我设计的反思提示词大概长这样请以严格审查员的身份检查你上一轮给出的回答。 检查标准 1. 是否使用了合适的工具来完成用户请求 2. 工具的输入参数是否正确 3. 最终回答是否基于工具返回的事实而非模型猜测 4. 是否完整回答了用户的所有子问题 如果以上所有项都通过输出PASS 否则输出FAIL 并说明原因然后重新执行正确的步骤。这个反思循环最大的价值在于它让Agent具备了“自主纠错能力”。没有反思循环时模型可能自信地给出一个错误结论有了反思循环模型在“交付”之前至少自我评估了一遍。代价是多消耗了一些token但换来的是答案质量的显著提升。4.3 日志与可观测性的工程实践进入反思循环后Agent的执行过程变长了我马上意识到需要观测工具调用链。TASK1虽然没有强制要求我还是给自己加了一个简单的日志系统把每一轮的thinking过程、tool_use、tool_result都打印出来加上时间戳。import logging import json logging.basicConfig(levellogging.INFO) logger logging.getLogger(agent) # 在循环每一轮后输出 logger.info(f[Round {round_num}] [model] calls tool: {tool_name} with input: {json.dumps(tool_input, ensure_asciiFalse)}) logger.info(f[Round {round_num}] [tool] returns: {json.dumps(result, ensure_asciiFalse)})这看似简单的日志其实在调试时帮了我大忙。有一次模型总是循环调用同样的工具我一眼就看出是因为tool_result传回的格式里混入了不可见的字符导致模型判断前后矛盾。如果没有日志这种问题排查起来会非常痛苦。5. 常见问题与排查技巧实录5.1 模型工具调用格式的“飘忽不定”很多初学者会遇到这类问题模型偶尔不返回合法的工具调用而是直接把工具名当成文本输出在content里。这时候看起来像是模型“不听话”实际上是你的系统提示词、工具描述或消息历史不够清晰。我的排查思路分三步走第一步检查工具描述看是否有歧义第二步检查示例消息确保历史消息中没有干扰模型判断的内容第三步考虑使用强制工具调用功能在API参数中设置tool_choice为特定工具名response client.messages.create( ..., tool_choice{type: tool, name: get_current_date} )强制工具调用适合那些你明确“这一步必须调用工具”的场景比如解析用户指令中的实体。课程里虽然没提但这个参数在实际项目中非常实用。5.2 context长度控制与成本优化第二个高频问题是上下文的膨胀。在反思循环里每一轮都会把tool_result完整塞回消息列表如果工具返回的内容本身特别大比如数据表、长文本Model的上下文窗口很快就被塞满了成本也会飙升。我建议的优化策略是只在消息里保留对后续推理有用的信息摘要而不是把所有原始数据都塞进去。可以在工具返回之前做一个摘要函数比如计算器返回结果只保留一个数字搜索工具返回结果只保留前N条记录的标题和摘要。这个摘要在调试期可以详细生产期必须收敛。成本优化也是TASK1可以延伸的亮点之一。某AI模型的定价逻辑中input给定上下文比output便宜很多通过工具返回精简摘要来缩短上下文直接降低了成本。我实测下来同样一类任务带摘要优化比不带摘要的省了大约30%-40%的token消耗。5.3 反思循环的死循环与重复调卡第三个头疼问题是反思循环可能进入死循环——模型反复说“FAIL我重新做”却不停调工具得不到实质结论。原因是反思提示词里缺少“终止条件”。解决之道是给反思器一个最大迭代次数并在提示词中声明“最多只能反思3次第3次无论结果如何都必须给出最终回答”。MAX_REFLECTIONS 3我在代码里加了这个计数器同时把计数器本身作为消息上下文注入模型模型就能感知到自己已经反思了几轮从而收敛行为。这个细节在官方示例里没有明说但我做过后发现非常有效。5.4 工具返回值格式的规范化最后要提一个很容易“带崩”Agent的隐性坑工具返回值格式五花八门。有的工具返回Python对象有的返回字符串有的返回JSON。如果dispatch层不做统一格式转换模型在解读时经常会出错尤其当返回的字符串里包含引号或换行符时更是容易让模型误解。我最终的dispatch函数统一把所有工具结果都转成纯字符串并且用delimiter包起来return fTOOL_RESULT:{json.dumps(result, ensure_asciiFalse, defaultstr)}这样模型收到的就是高度结构化的数据解析压力小了很多。顺带说一句defaultstr这个参数非常实用能处理datetime、Decimal等非JSON序列化对象避免因为一个序列化报错导致整个流程崩溃。6. 从任务到实战的延伸思考6.1 技能体系化的下一步做完TASK1我最大的感触是“技能”的本质不是模型更强了而是工程的封装更好了。你在本地写好工具函数把它暴露给模型模型就学会了调用。这种模式和传统软件开发的区别在于你不必再“指定顺序”而是“给出选项”模型自己知道怎么编排。在实际业务中这套体系可以扩展成很多方向专业知识库检索RAG、多步骤订单处理流程、代码自动生成与执行、定时任务调度等等。我已经在自己的一个模拟项目里尝试把一批Python脚本封装成工具让模型调度。说实话效果超出预期模型能识别出脚本之间的依赖关系并合理编排调用顺序这在以前需要几十条硬编码规则才能实现。6.2 人与模型配合的新界面这个任务让我重新思考了开发者的角色。传统模式下你是写逻辑的人模型是“执行器”在Agent模式下你是“定义资源”的人——定义工具、定义约束、定义审核标准把具体的路径选择权交给了模型。这和写传统软件的心智模型差异很大但一旦适应效率提升是显著的。我在课程社区里看到很多同学对“反思循环”提出了疑问觉得多花一次模型调用不太值得。我的实测结论是在问题难度较高或对准确率要求较高的场景下反思循环几乎是一种必需品。而面对简单查询判断“本轮不需要反思”也是一个优化方向——可以设定触发条件比如只有当工具调用次数大于1或模型回答置信度低于某个值时才触发反思。这样既能控制成本又能保证质量。如果你正在学习Agent开发我的建议非常简单不要急着上重型框架先亲手把TASK1这样的最小循环从零写一遍。你能在上面看到各类问题的原始面貌——模型输出格式漂移、上下文管理失控、循环终止条件模糊这些知识点在未来使用任何高级框架时都会发挥巨大作用。做完这个任务后我做了个人体会的总结Agent开发最核心的竞争力不在于模型的智商而在于你设计工具和反馈机制的能力。换一个更强的模型如果你工具暴露得乱七八糟Agent照样表现稀烂而一个中等模型配上清爽的工具定义和良好的反思循环也能稳定输出高质量结果。这一点是我在TASK1中感触最深的地方。