0基础学会Agent Harness工程(前置知识二):从ReAct到Agent Loop 0基础学会Agent Harness工程前置知识二从ReAct到Agent Loop本篇对应的官方文档ReAct 论文用于确认 ReAct 的原始问题与基本关系——reasoning 和 task-specific action 交错出现action 从外部环境获得新信息。Google ResearchReAct用于区分 reasoning trace 与真实环境动作并说明 observation 怎样反过来更新后续判断。AnthropicBuilding effective agents用于说明 Agent 为什么需要环境 ground truth、反馈循环和停止条件。OpenAI Agents SDKRunning agents用于观察现代 Runner 怎样在 final output、handoff、tool calls 和最大轮次之间管理循环。本篇主要内容前置知识一已经把 Model 与 Harness 分开但“模型提出动作、Harness 负责执行”仍只是一条静态分工。本篇沿“查看目录后再选择文件”这次任务定位单次调用在新环境状态处为什么会断开再解释 ReAct 中 reasoning、action、observation 如何形成反馈。随后严格区分 ReAct 方法范式、Tool Calling 结构化交接和 Agent Loop 控制结构并补上循环继续、正常完成、失败与强制停止的边界最后把抽象原理交给第 01 篇代码。下篇预告第 01 篇会把这条反馈链写成最小 Python 程序一个 Bash handler、一个持续增长的 messages 列表以及一个能够执行、回填并退出的 agent_loop()。一、模型为什么不能一次决定完整任务前置知识一已经建立了最重要的对象边界大模型接收上下文并生成输出Harness 则为模型提供工具、状态、执行与权限。我们也知道模型写出一条命令不等于命令已经执行训练时学到的常见项目结构不等于当前目录的真实结构。现在沿着这个结论再向前走一步。假设用户提出一个看起来很简单的任务看看当前目录中有哪些文件找到项目入口然后用两三句话说明这个项目怎样启动。如果只调用一次模型模型可以在回答前制定一个合理方案先查看目录如果有README.md读取启动说明如果有pyproject.toml检查脚本入口如果有main.py阅读主函数综合这些信息给出答案。问题是这个方案中的第二步依赖第一步结果。模型在看到目录之前不知道README.md是否存在看到目录之后也可能发现项目使用src/布局真正入口写在pyproject.toml的[project.scripts]中。任务不是“一次猜对五步”而是“每获得一份新事实就重新判断下一步”。可以把单次调用的断点写成用户目标→ 模型建议list_directory→ 调用结束→ 真实目录仍未进入模型上下文如果外部程序执行list_directory却不把结果交还模型断点只是向后移动了一格用户目标→ 模型建议list_directory→ Harness 执行动作→ 得到src/、tests/、pyproject.toml→ 模型仍然看不到结果断点并不在工具有没有运行而在结果有没有成为下一轮状态。真实目录已经存在于 Harness 一侧但返回模型的反馈箭头仍然是断开的。只要“未回填”仍然存在模型判断就停留在旧状态。把工具接进系统只是获得手能够把 observation 接回上下文才形成反馈。因此“系统有工具”还不够。工具结果必须成为下一次模型调用可以看到的新状态。我们可以用一组静态消息理解状态为什么重要。初始时模型只知道目标用户查看项目入口并说明启动方式。第一次动作执行后系统新增了事实目录结果src/、tests/、pyproject.toml。此时合理的下一步已经变化。模型不应该继续寻找并不存在的main.py而应读取pyproject.toml或搜索src/。也就是说动作不仅完成了一个步骤还改变了后续决策的依据。这里出现了 Agent 任务与普通一次性生成的核心差别一次性生成是在固定上下文上计算输出Agent 任务会通过动作获得新 observationobservation 改变上下文新上下文可能改变计划和下一步动作。为什么不能让模型一开始就输出完整计划然后由程序照着执行有时当然可以这就是固定 Workflow 或“先规划、后执行”的一种实现。但只要后续步骤依赖未知结果静态计划就必须允许修改。目录不存在、文件内容与预期不同、命令失败、权限被拒绝都会让原计划失效。真正需要 Agent 的场景往往正是路径不能在运行前完全确定。再看一个更明显的例子。模型预想读取README.md→ 找到安装命令 → 运行测试。真实目录返回README.md不存在但有docs/getting-started.md。如果系统机械执行旧计划第二步就会失败。如果系统把失败作为 observation 回填模型可以调整为读取docs/getting-started.md。失败不再只是终止信号而是一份会影响后续选择的新状态。所以模型不能一次决定完整任务不是因为它一定不会规划而是因为开放任务中的关键事实尚未出现。一个可靠系统需要反复完成四件事根据当前状态选择动作在模型外部执行或拒绝动作把结果作为 observation 加入状态根据更新后的状态继续或结束。这四件事构成反馈链。下一节要解释的 ReAct就是理解“为什么推理与行动要交错而不是各做各的”的一把钥匙。二、ReAct 为什么把行动结果重新交给模型ReAct 来自 Reasoning 与 Acting 的结合。ReAct 论文关注的核心问题是语言模型的推理能力和行动能力过去经常被分开研究。只推理时模型容易困在自身已有信息里只行动时系统又可能缺少对目标、计划和异常的高层判断。ReAct 让模型以交错方式生成 reasoning traces 与 task-specific actions使行动取得的外部信息能够支持后续推理推理又能指导下一次行动。对零基础读者来说不必先记论文实验和基准成绩。先分清三个对象Reasoning表示模型依据当前上下文形成的判断。它可以用于拆解目标、选择下一步、追踪进度或处理异常。需要特别说明的是现代模型的隐藏内部推理不需要、也不应被强制公开。本系列讲 reasoning 时关注的是系统可观察的决策依据和任务状态不要求展示模型私有思维链。Action表示面向外部环境的动作例如搜索、列目录、读文件、运行命令或调用 API。模型可以提出 action但真正产生副作用的是 Harness 或环境中的执行器。Observation表示动作执行后返回的环境信息例如目录列表、文件内容、命令输出、错误消息或权限拒绝。Observation 不是模型凭空生成的“可能结果”而应来自真实执行或明确的模拟环境。Google Research 对 ReAct 的说明给出一个非常清楚的边界reasoning trace 不直接改变外部环境action 才会带来 environment observationobservation 随后更新模型可以利用的上下文。把它压缩成一条链就是当前目标与状态→ reasoning下一步需要知道目录结构→ action请求列出目录→ environment执行目录读取→ observation返回真实文件列表→ 更新状态→ 下一次 reasoning四个对象形成的是有方向的闭环reasoning 只负责判断action 才进入环境environment 返回 observationobservation 再更新下一次 reasoning。闭环中任何一条边断开都会改变系统性质。没有 action 就得不到新事实没有 observation 回流就无法依据结果调整而没有新的 reasoning 则退化为预定动作序列。现在静态推演“找到项目入口”这次任务。第一轮模型只知道用户目标。它无法确认入口文件于是产生一个可观察的决策先获取目录结构。随后提出 actionlist_directory(path.)Harness 执行动作环境返回src/、tests/、pyproject.toml这份结果成为 observation。第二轮模型看到 observation 后不再猜测main.py而是判断项目采用包结构入口可能声明在pyproject.toml。它提出新的 actionread_file(pathpyproject.toml)第二次 observation 返回[project.scripts] demo demo.cli:main现在模型已经有足够事实形成 final answer项目入口是demo.cli:main对外命令名为demo。安装项目后可在终端运行demo启动。这条轨迹中最重要的不是工具名而是状态变化时刻模型已经知道什么仍然缺少什么下一步S0用户想找入口当前目录结构列目录S1存在pyproject.toml配置中的入口声明读配置S2入口为demo.cli:main已无关键缺口给出最终回答如果 observation 没有回填S0 永远不会变成 S1。程序即使在后台成功列出了目录模型仍会重复请求、继续猜测或者错误地认为动作已经完成。反馈链的价值就在于让外部事实真正参与下一次决策。失败也应当作为 observation 进入反馈。假设读取pyproject.toml时返回PermissionError: access denied这不是可以随手丢掉的日志。它告诉模型“目标文件存在但当前权限不允许读取”。下一步可能是请求用户授权、寻找其他公开说明或者停止并说明无法确认入口。只有模型看到失败才有机会根据失败调整。再假设工具返回一个空目录。模型可能判断当前工作目录错误请求查看父目录也可能直接询问用户项目位置。Observation 不保证任务成功它只保证后续判断建立在最新事实之上。这里还要避免一个常见误解ReAct 不是“让模型不停自言自语”。如果系统没有外部 action 和 observation再长的推理文本仍然不会获得新事实如果系统只执行动作但不把结果回填动作也无法改进后续判断。ReAct 的重点是 reasoning 与 acting 相互支撑。另一个误解是把每一步 reasoning 都当成必须展示给用户的完整思维过程。原始 ReAct 工作研究了显式 reasoning trace但现代产品可以只保存必要的任务摘要、动作理由或状态字段。对 Harness 工程来说真正不可缺少的是可验证的 action、来自环境的 observation 和可追踪的状态变化而不是暴露模型隐藏推理。这一节解决的是原理问题为什么需要把行动结果重新交给模型。它还没有回答具体协议怎样表达 action也没有回答谁来重复调用模型。为此需要把三个经常混用的概念拆开。三、ReAct Tool Calling 与 Agent Loop 的分工在很多教程里ReAct、工具调用和 Agent Loop 会在同一段代码中出现于是初学者容易把它们当成三个名字。它们确实可以协作但分别处于不同层。ReAct 是方法范式。它回答“为什么推理与行动要交错外部反馈怎样更新后续判断”它描述的是任务轨迹与信息关系不规定必须使用哪家 API也不要求 Python 函数必须怎样命名。Tool Calling 是结构化交接。它回答“模型怎样把‘我想调用某个工具’表达成程序可以解析的数据”相比让模型在自然语言里写“请执行 Get-ChildItem”结构化调用通常会明确工具名、参数和调用标识。Harness 可以校验这些字段再决定是否路由到真实 handler。但 Tool Calling 只表达调用意图。模型返回tool_name list_directoryarguments {path: .}。不代表目录已经读取。只有 Harness 找到相应 handler、通过权限检查并实际执行后环境才会产生 observation。Agent Loop 是运行控制结构。它回答“谁保存当前状态谁调用模型谁执行工具谁回填结果以及何时继续或停止”Loop 把模型调用、工具执行和 observation 回填接成可重复过程。它通常属于 Harness而不是模型参数的一部分。可以用三层图在脑中定位上层ReAct——任务如何在判断、行动和反馈中推进中层Tool Calling——行动意图如何跨过模型与程序边界下层Agent Loop——程序如何反复调用、执行、回填和停止三层可以同时服务一条任务轨迹但每层回答的问题不同。把它们上下排列能清楚看到“方法、交接、控制”之间的协作关系。“协作不等同”是阅读后续代码时的保护栏。看到tool_calls只能证明协议表达了 action还要继续寻找 handler、结果回填和退出条件才能确认 Loop 是否完整。三者之间是组合关系不是同义关系。一个系统可以有 Tool Calling却没有 Agent Loop。例如模型产生一次工具调用程序执行后直接把结果返回给用户不再询问模型。这是一次结构化工具使用但不是完整反馈循环。一个系统也可以有循环却不采用 ReAct。程序可能固定调用模型三次每次做同一个改写任务它有for循环却没有模型根据 observation 动态选择 action。ReAct 也不依赖某个特定 Tool Calling 字段。论文中的 action 可以是文本动作由环境解析现代 API 则常用结构化调用提高交接可靠性。具体表示变化不影响 reasoning、action、observation 的基本关系。前置知识一用下一步控制权区分 Workflow 与 Agent。现在可以把这个差别放进循环固定 Workflow 可能写成列目录 → 读 README → 读配置 → 总结每个箭头由开发者预先规定。即使其中某一步调用 LLM整体路径仍由代码控制。模型驱动 Agent 更接近当前状态 → 模型选择 action → 执行 → observation → 当前状态开发者定义工具和边界模型根据 observation 决定下一步。这里的“模型控制”也不是无限权力。Harness 仍然可以拒绝危险调用、限制目录范围、要求人工批准或在超出预算时停止。固定路径和反馈回环可以放在同一视野下比较。左侧的 A、B、C 在运行前已经确定右侧的下一步则会随着 observation 改变。因此“用了模型”不能直接判断是不是 Agent。真正要检查的是模型是否获得了新状态并据此控制后续过程。接着看 Loop 为什么不能只是一个裸while True。一个可控循环至少要识别几类出口。第一类是正常完成。模型认为已有信息足够返回 final answer不再请求工具。Loop 保存最终输出并结束。第二类是需要继续。模型产生一个或多个工具调用。Harness 校验并执行把每份结果与原调用配对追加到状态后再次调用模型。第三类是动作失败。参数无效、工具不存在、权限拒绝、进程报错都可能发生。生产系统不应把所有失败都吞掉后继续也不应让进程无说明崩溃。失败需要被分类并决定回填给模型、重试、请求人工处理或直接结束。第四类是强制停止。即使模型持续请求工具Harness 也必须能够依据最大轮次、时间、Token、费用或风险阈值终止。OpenAI Agents SDK 当前 Runner 文档明确列出max_turnsAnthropic 的工程建议同样强调最大迭代和受控环境。停止条件不是对模型不信任而是任何自动化系统都需要的控制面。四类出口围绕同一个“本轮模型输出”分叉。只有 Tool Call 分支会在 observation 回填后回到下一轮完成、失败和达到上限都必须进入明确的结束或处理路径。这使 Loop 从无限重复变成可审计的状态机。生产系统还会细分失败类型但最小课程骨架先保留这四个方向已经足够。前面的四类出口落实到控制结构后可以用下面这段静态伪代码表达状态、继续条件和停止条件state [user_goal] repeat: decision model(state, available_tools) state.append(decision) if decision is final_answer: return final_answer if turn_limit_reached: stop_with_limit_error for each action in decision: result validate_and_execute(action) state.append(observation_for(action, result))这段伪代码有意不写具体 API 字段。我们只看职责state保存到目前为止的任务事实model根据当前状态选择回答或 actionvalidate_and_execute属于 Harness不属于模型observation_for把结果接回对应动作final answer 和 turn limit 都能结束循环。如果删掉state.append(observation...)循环会失去反馈如果删掉 final answer 判断系统不知道正常完成如果删掉限制错误动作可能无限重复。Loop 的本质不是重复语法而是带状态反馈和退出条件的控制结构。到这里我们还没有展开 OpenAI-compatible 协议中的messages、tools、tool_calls、function.arguments和tool_call_id。原因是这些字段回答“某种接口怎样传递对象”而本篇先回答“为什么系统需要这些对象”。第 01 篇进入代码时字段将各自找到位置不会变成要死记的陌生 JSON。四、怎样从抽象循环走到第一个程序理解原理后下一步不是立刻堆出完整生产平台而是找到最小可运行骨架。我们需要把前面出现的抽象对象映射到代码责任抽象对象程序中的责任goal接收用户输入形成第一条状态available actions向模型描述可请求的工具model decision调用模型并读取普通回答或工具调用action execution根据工具名找到 handler 并执行observation把执行结果写回消息状态feedback loop带着扩展后的状态再次调用模型stop在最终回答、失败或限制条件下退出从原理走向代码时每个抽象对象都应找到唯一责任。第 01 篇会把 Goal 放进messages把 Action 读成tool_calls由 Bash handler 产生返回结果再用退出条件收口。这组映射不是完整实现却提供了阅读顺序先追踪状态再看调用意图随后确认真实执行与结果回填最后检查退出分支。为了让后面 20 个案例形成连续学习我们还会使用一张六层 Harness 代码地图。第一层是模型交互层。它负责把 system instructions、用户目标、历史状态和工具说明提交给模型再取得本轮响应。这里回答“模型看见了什么返回了什么”。第二层是循环与状态层。它负责保存每一轮消息、判断本轮是 final answer 还是 action、把 observation 加回状态并控制继续或退出。这里回答“任务为什么没有在第一次调用后结束”。第三层是工具执行层。它把模型提出的结构化 action 映射到真实 handler。第 01 篇只有一个 Bash handler后续才会扩展成多个原子工具和分发表。第四层是上下文与知识层。它处理历史不断增长、知识按需加载、记忆持久化和上下文压缩。最小循环暂时没有这些能力但工具结果越来越多后它们会成为新问题。第五层是任务与协作层。它处理计划、子智能体、团队消息、依赖图和并发。单个循环能完成小任务复杂任务则需要把工作拆开又重新汇合。第六层是安全与扩展层。它包含权限、审批、Hook、沙箱、审计和 MCP 等外部能力路由。模型能提出什么与系统允许执行什么必须保持分离。第 01 篇只进入前三层的一小部分用一个messages列表保存用户输入、assistant 消息和工具结果调用一次 OpenAI-compatible Chat Completions 接口向模型提供一个 Bash Tool Schema读取模型返回的tool_calls用 Bash handler 执行命令通过匹配的调用 ID 把结果追加为工具消息如果模型不再请求工具就输出最终回答并退出。这些结构会第一次出现在真实 Python 文件s01_agent_loop.py中。阅读时不应从第一行开始背配置而应先找到agent_loop()沿状态变化问四个问题进入循环前messages中有什么模型响应怎样被完整保存什么条件进入 handler什么条件直接结束handler 输出怎样回到下一轮这样读代码时每个字段都能对应前面已经理解的对象。例如messages不是“聊天 UI 的展示记录”而是反馈链的状态载体tools不是 Python 函数本身而是模型可见的能力说明tool_calls不是执行结果而是 action 意图roletool消息承载 observationtool_call_id则保证结果能找到它所对应的 action。这也解释了为什么第 01 篇从 Bash 开始。Bash 能快速触达文件系统和命令行用一个 handler 就可以观察完整闭环。如果第一篇同时加入读文件、写文件、搜索、权限、重试和并发读者很难判断究竟是哪一层让 Agent 动了起来。最小案例故意把其他能力拿掉只证明模型提出 action→ Harness 执行→ observation 回填→ 模型根据新状态继续当然一个 Bash handler 不等于安全的生产系统。Shell 命令可能读取错误目录、修改文件、启动子进程或产生巨大输出。字符串黑名单、简单超时也不能构成完整权限模型。本系列会把“能够执行”和“允许执行”始终分开。第 01 篇证明最小机制第 03 篇以后再逐步加入计划、子智能体、技能、上下文、任务系统、权限与 Hook。在进入代码前还可以用四条失败路径检查自己是否真正理解了 Loop。第一模型重复请求同一个动作。可能是 observation 没有回填也可能是结果不够清楚。Harness 需要记录状态并在生产环境中考虑重复检测和最大轮次。第二工具执行成功但调用 ID 丢失。模型可能无法确认哪份结果对应哪个 action。并行工具调用时这种错误更加明显。第三模型给出普通回答程序却继续循环。说明退出条件没有绑定 final answer 或“没有工具调用”的状态。第四工具失败后程序直接崩溃。说明错误没有形成受控 observation也没有交给停止策略处理。能够说明这四种失败分别发生在哪一层就已经具备阅读第一个 Agent Loop 的知识准备。回顾前置知识二的主线单次模型调用在动作后的真实状态处断开ReAct 说明 reasoning 与 action 为什么需要通过 observation 相互支持Tool Calling 负责结构化表达 actionAgent Loop 负责执行、回填、继续和停止Harness 把模型能力放进一个可运行、可控制的环境。下一篇不再停留在抽象图。第 01 篇会打开s01_agent_loop.py用一个具体请求——“查看当前目录中有哪些 Python 文件”——跟踪messages怎样增长、Bash handler 怎样被调用、工具结果怎样回填以及模型最终不再请求工具时程序怎样结束。那时看到的每一行代码都会落回本篇已经建立的反馈链。