chapter 02 / 循环驱动层
Agent 循环
收集上下文 → 行动 → 检查 → 重复,直到满足停止条件
Agent 和普通的一问一答有什么本质区别?答案不在模型里,而在循环里。
Claude Code 团队给出过一个精炼的定义:Agent 就是重复工作循环,直到满足停止条件。最基本的形态只有五步:
收集上下文 → 行动 → 检查自己的工作 → 按需重复 → 回应
单回合的模型输出常常不够好;Agent 之所以显得「能干」,是因为它可以反复逼近——跑一次测试、看到报错、修改代码、再跑一次。循环就是让模型获得迭代机会的机制。
Loop engineering: Getting started with loops ↗
循环工程入门:Agent 重复「收集上下文→行动→检查→重复→回应」直到满足停止条件。定义了回合、目标、时间、主动四类循环,并强调用明确的完成标准(/goal + 评估器)延长有效迭代。
循环的四个可设计维度
「设计循环,而非提示 Agent」近来成为共识。把一个循环显式设计出来,要回答四个问题(见概念卡片循环四类型学):
| 维度 | 问题 | 例子 |
|---|---|---|
| 触发 | 循环由什么启动? | 用户消息、定时器、事件钩子 |
| 停止 | 什么时候算完? | 任务完成、迭代上限、预算耗尽、人叫停 |
| 原语 | 每轮用什么动作? | 工具调用、子代理、代码执行 |
| 任务 | 适合哪类工作? | 可自验证的任务收益最大 |
其中停止条件是最容易被忽视、也最能体现工程水平的一环。一个循环若只有「任务完成」一种结局,它在开放式任务上就会失控——你需要迭代上限、超时、token 预算这些「保险丝」,以及最重要的:预算耗尽时如实报告失败,而不是硬编一个答案。
一个真实的循环长什么样
概念说完了,看真码。openJiuwen agent-core 的 ReActAgent 是一个生产级的 ReAct(Reasoning + Acting)循环实现——下面五段代码正好走完「收集上下文 → 行动 → 检查 → 重复 → 回应」的每一步,还包含了教科书通常不讲的部分:外部转向、人机中断、优雅退出。
source walkthrough
ReActAgent 的主循环:一个真实 Agent 循环的完整骨架
openJiuwen-ai/agent-core @ 1e3a5c7a3d · openjiuwen/core/single_agent/agents/react_agent.py:1755-1861 · 提取于 2026-07-18
这是 openJiuwen agent-core 中 ReActAgent 的核心循环。五段代码依次展示:迭代预算、外部转向注入、模型调用与自然停止、工具执行与中断、以及预算耗尽的兜底——教科书式的「收集上下文 → 行动 → 检查 → 重复」。
- 段 1L1755–1767循环入口:迭代预算与边界优雅退出
if invoke_inputs.result is None: for iteration in range(start_iteration, self._config.max_iterations): logger.info(f"ReAct iteration {iteration + 1}/{self._config.max_iterations}") # Honor force_finish requests set at iteration boundary # (e.g. by rails on AFTER_REACT_ITERATION). This lets a # graceful abort take effect at the top of the next # iteration, after the previous one fully completes. boundary_finish = ctx.consume_force_finish() if boundary_finish: await self.context_engine.save_contexts(session) invoke_inputs.result = boundary_finish.result break循环不是 while True,而是 range(start_iteration, max_iterations)——迭代预算是第一道停止条件。注意 start_iteration 可以不为 0:从中断恢复时会从上次的轮次继续。每轮开头先检查 force_finish(外部要求优雅终止),确保上一轮完整结束后才退出,退出前保存上下文。
- 段 2L1769–1787转向注入:循环运行中接收新指令
# Inject pending steering messages # before the next model call. steering = ctx.drain_steering() if steering: combined = "\n".join(steering) await context.add_messages( UserMessage( content=( f"[STEERING] " f"{combined}" ) ) ) ai_message = await self._call_model( ctx, context, tools, )用户在 Agent 干活时补充一句「换个方向」怎么办?Harness 的答案:steering 队列。新指令不打断当前轮,而是在下一次模型调用前作为 [STEERING] 消息注入上下文。这就是「中断即状态」——外部输入被转化为循环内的一条消息,而非破坏性打断。
- 段 3L1795–1819自然停止:模型不再调用工具,就是答案
if not isinstance(ai_message, AssistantMessage): invoke_inputs.result = ai_message if isinstance(ai_message, dict) else {} break await context.add_messages( AssistantMessage( content=ai_message.content, tool_calls=ai_message.tool_calls, reasoning_content=ai_message.reasoning_content, usage_metadata=ai_message.usage_metadata, finish_reason=ai_message.finish_reason, ) ) if not ai_message.tool_calls: # If steering arrived while the # model was generating, continue # the loop so the next iteration # drains and injects it. if ctx.has_pending_steering(): continue await self.context_engine.save_contexts(session) result = {"output": ai_message.content, "result_type": "answer"} invoke_inputs.result = result break第二道停止条件:模型输出里没有 tool_calls,说明它认为任务完成,直接产出答案。有意思的细节是中间那个 if:如果模型生成期间恰好来了 steering 消息,就先不停止,continue 进入下一轮把新指令消费掉——停止判断要让位于未处理的外部输入。
- 段 4L1821–1844行动与检查:执行工具、处理人机中断
results = await self._execute_tool_call(ctx, ai_message.tool_calls, session, context) finish = ctx.consume_force_finish() if finish: await self.context_engine.save_contexts(session) invoke_inputs.result = finish.result break hitl_interrupt, sub_agent_outputs = self._after_execute_tool_call_for_hitl( results, ai_message.tool_calls, ai_message, iteration, original_query=ctx.extra.get("_original_query", ""), ) if hitl_interrupt: await self._commit_interrupt(hitl_interrupt, context, session, invoke_inputs, sub_agent_outputs) break workflow_interrupt = self._after_execute_tool_call( results, ai_message.tool_calls, ai_message, iteration, original_query=ctx.extra.get("_original_query", ""), ) if workflow_interrupt: await self._commit_interrupt(workflow_interrupt, context, session, invoke_inputs) break「行动」之后必有「检查」:工具执行完,连续检查三种要求暂停的信号——外部 force_finish、需要人确认的 HITL 中断、等待用户输入的工作流中断。中断不是异常,而是被 _commit_interrupt 持久化成状态:写入占位消息、保存上下文、记录恢复点。下次 invoke 时从 start_iteration 继续,这就是可恢复流。
- 段 5L1846–1861循环收尾:安全快照与预算耗尽兜底
# Iteration fully succeeded (LLM + all tools + ToolMessages # all written). Fire AFTER_REACT_ITERATION so rails (e.g. # SnapshotRail) can capture a safe-state snapshot. await ctx.fire(AgentCallbackEvent.AFTER_REACT_ITERATION) else: # The loop exhausted its iteration budget. Honor a # force_finish requested by the final iteration's # AFTER_REACT_ITERATION hook (e.g. graceful abort), # which has no next iteration-top to consume it. boundary_finish = ctx.consume_force_finish() await self.context_engine.save_contexts(session) if boundary_finish is not None: invoke_inputs.result = boundary_finish.result else: result = {"output": "Max iterations reached without completion", "result_type": "error"} invoke_inputs.result = result每轮完整成功后触发 AFTER_REACT_ITERATION 事件,让 SnapshotRail 之类的「护栏」拍下安全快照——这是 Harness 在循环外围织的网。最后的 for-else 是 Python 特性:循环自然跑完(没 break)意味着迭代预算耗尽,此时如实返回 error 而不是硬编造一个答案。三种结局——完成、中断、预算耗尽——都被显式处理。
对照开头的五步定义复盘一下:
- 收集上下文:每轮开始时,
context里已累积了此前所有消息与工具结果(还有可能被注入的[STEERING]新指令); - 行动:
_call_model产出tool_calls,_execute_tool_call真正执行; - 检查:工具执行后连查三种中断信号;每轮完整结束后触发
AFTER_REACT_ITERATION让护栏拍快照; - 按需重复:
for iteration in range(...)——重复是有预算的; - 回应:模型不再调用工具时,内容即答案。
亲手转一转这个循环
下面的模拟器把上述循环做成了可单步执行的沙盘。三个场景对应三种典型结局——试试第二个「开放式重构」,体会一下没有明确完成标准的任务为什么必然烧穿迭代预算:
agentic loop simulator
教学模拟 · 预置轨迹,非真实 LLM 调用
任务:test_parser.py 里有一个测试挂了,找到原因并修复。
- — 点击「单步」或「自动播放」开始 —
循环的变体
基本循环之上有几个值得认识的变体形态(各自有概念卡片可深入):
- Ralph Loop:跨上下文窗口的循环——每轮用全新的上下文重启 Agent,靠文件系统传递状态,暴力但惊人地有效;
- 自主迭代模式:约束 + 指标 + 迭代,让 Agent 对着一个可度量的目标反复冲刺;
- 双环进化:内环解决任务,外环改进「解决任务的方式」——这是第 6 章的主题。
循环的质量还取决于它周围的系统:干净的代码库、可自验证的 Skill、易达的文档。循环本身只是骨架,让每一轮「检查」有的可查,才是真正的功夫。
本章能力在 Harness 中处于什么位置?
harness positioning
本章能力在 Harness 中处于什么位置?
循环驱动层是 Harness 的「发动机」:它上承编排层(多 Agent 就是多个循环的组合)、下接模型内核(每轮的一次无状态调用),旁边靠状态与恢复层保证随时能停下再继续。本章的 ReActAgent 是内循环;下一章会看到套在它外面的任务外循环。
本章概念清单 / 点击进入概念卡片
章末测验
chapter quiz
0/4 已答
Q1Claude Code 团队对 Agent 循环的定义中,最基本的形态是哪个顺序?
Q2在 openJiuwen ReActAgent 的主循环里,「模型输出中没有 tool_calls」意味着什么?
Q3循环运行中用户突然补充一条新指令,ReActAgent 是怎么处理的?
Q4ReAct 循环用 for-else 结构处理「循环自然跑完却没 break」的情形,此时返回什么?