☰
从零实现Agent循环:ReAct与反思机制实战教程
2026/10/17 7:40:50 网站建设 项目流程

我这段时间在跟进某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_KEY=sk-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_key=api_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( model=model, system=system_prompt, tools=tools, messages=messages, max_tokens=4096, temperature=0, ) 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(level=logging.INFO) logger = logging.getLogger("agent") # 在循环每一轮后输出 logger.info(f"[Round {round_num}] [model] calls tool: {tool_name} with input: {json.dumps(tool_input, ensure_ascii=False)}") logger.info(f"[Round {round_num}] [tool] returns: {json.dumps(result, ensure_ascii=False)}")

这看似简单的日志其实在调试时帮了我大忙。有一次模型总是循环调用同样的工具,我一眼就看出是因为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 f"TOOL_RESULT:{json.dumps(result, ensure_ascii=False, default=str)}"

这样模型收到的就是高度结构化的数据,解析压力小了很多。顺带说一句,default=str这个参数非常实用,能处理datetime、Decimal等非JSON序列化对象,避免因为一个序列化报错导致整个流程崩溃。

6. 从任务到实战的延伸思考

6.1 技能体系化的下一步

做完TASK1,我最大的感触是“技能”的本质不是模型更强了,而是工程的封装更好了。你在本地写好工具函数,把它暴露给模型,模型就学会了调用。这种模式和传统软件开发的区别在于:你不必再“指定顺序”,而是“给出选项”,模型自己知道怎么编排。

在实际业务中,这套体系可以扩展成很多方向:专业知识库检索(RAG)、多步骤订单处理流程、代码自动生成与执行、定时任务调度等等。我已经在自己的一个模拟项目里尝试把一批Python脚本封装成工具,让模型调度。说实话,效果超出预期,模型能识别出脚本之间的依赖关系并合理编排调用顺序,这在以前需要几十条硬编码规则才能实现。

6.2 人与模型配合的新界面

这个任务让我重新思考了开发者的角色。传统模式下,你是写逻辑的人,模型是“执行器”;在Agent模式下,你是“定义资源”的人——定义工具、定义约束、定义审核标准,把具体的路径选择权交给了模型。这和写传统软件的心智模型差异很大,但一旦适应,效率提升是显著的。

我在课程社区里看到很多同学对“反思循环”提出了疑问,觉得多花一次模型调用不太值得。我的实测结论是:在问题难度较高或对准确率要求较高的场景下,反思循环几乎是一种必需品。而面对简单查询,判断“本轮不需要反思”也是一个优化方向——可以设定触发条件,比如只有当工具调用次数大于1或模型回答置信度低于某个值时,才触发反思。这样既能控制成本,又能保证质量。

如果你正在学习Agent开发,我的建议非常简单:不要急着上重型框架,先亲手把TASK1这样的最小循环从零写一遍。你能在上面看到各类问题的原始面貌——模型输出格式漂移、上下文管理失控、循环终止条件模糊,这些知识点在未来使用任何高级框架时都会发挥巨大作用。

做完这个任务后,我做了个人体会的总结:Agent开发最核心的竞争力不在于模型的智商,而在于你设计工具和反馈机制的能力。换一个更强的模型,如果你工具暴露得乱七八糟,Agent照样表现稀烂;而一个中等模型,配上清爽的工具定义和良好的反思循环,也能稳定输出高质量结果。这一点,是我在TASK1中感触最深的地方。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询