Agent 研习舱

chapter 02 / 循环驱动层

Agent 循环

收集上下文 → 行动 → 检查 → 重复,直到满足停止条件

Agent 和普通的一问一答有什么本质区别?答案不在模型里,而在循环里。

Claude Code 团队给出过一个精炼的定义:Agent 就是重复工作循环,直到满足停止条件。最基本的形态只有五步:

收集上下文 → 行动 → 检查自己的工作 → 按需重复 → 回应

单回合的模型输出常常不够好;Agent 之所以显得「能干」,是因为它可以反复逼近——跑一次测试、看到报错、修改代码、再跑一次。循环就是让模型获得迭代机会的机制。

Anthropic · Claude Code 团队2026-07-07

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 的核心循环。五段代码依次展示:迭代预算、外部转向注入、模型调用与自然停止、工具执行与中断、以及预算耗尽的兜底——教科书式的「收集上下文 → 行动 → 检查 → 重复」。

  1. 1L17551767循环入口:迭代预算与边界优雅退出
    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(外部要求优雅终止),确保上一轮完整结束后才退出,退出前保存上下文。

  2. 2L17691787转向注入:循环运行中接收新指令
    # 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] 消息注入上下文。这就是「中断即状态」——外部输入被转化为循环内的一条消息,而非破坏性打断。

  3. 3L17951819自然停止:模型不再调用工具,就是答案
    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 进入下一轮把新指令消费掉——停止判断要让位于未处理的外部输入。

  4. 4L18211844行动与检查:执行工具、处理人机中断
    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 继续,这就是可恢复流。

  5. 5L18461861循环收尾:安全快照与预算耗尽兜底
            # 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 而不是硬编造一个答案。三种结局——完成、中断、预算耗尽——都被显式处理。

以上为 Apache-2.0 许可的 openJiuwen 源码节选,仅截取教学所需片段;完整实现见 GitHub(固定 commit)

对照开头的五步定义复盘一下:

  • 收集上下文:每轮开始时,context 里已累积了此前所有消息与工具结果(还有可能被注入的 [STEERING] 新指令);
  • 行动_call_model 产出 tool_calls_execute_tool_call 真正执行;
  • 检查:工具执行后连查三种中断信号;每轮完整结束后触发 AFTER_REACT_ITERATION 让护栏拍快照;
  • 按需重复for iteration in range(...) ——重复是有预算的;
  • 回应:模型不再调用工具时,内容即答案。

亲手转一转这个循环

下面的模拟器把上述循环做成了可单步执行的沙盘。三个场景对应三种典型结局——试试第二个「开放式重构」,体会一下没有明确完成标准的任务为什么必然烧穿迭代预算:

agentic loop simulator

教学模拟 · 预置轨迹,非真实 LLM 调用

任务:test_parser.py 里有一个测试挂了,找到原因并修复。

  1. — 点击「单步」或「自动播放」开始 —

循环的变体

基本循环之上有几个值得认识的变体形态(各自有概念卡片可深入):

  • Ralph Loop:跨上下文窗口的循环——每轮用全新的上下文重启 Agent,靠文件系统传递状态,暴力但惊人地有效;
  • 自主迭代模式:约束 + 指标 + 迭代,让 Agent 对着一个可度量的目标反复冲刺;
  • 双环进化:内环解决任务,外环改进「解决任务的方式」——这是第 6 章的主题。

循环的质量还取决于它周围的系统:干净的代码库、可自验证的 Skill、易达的文档。循环本身只是骨架,让每一轮「检查」有的可查,才是真正的功夫。

本章能力在 Harness 中处于什么位置?

harness positioning

本章能力在 Harness 中处于什么位置?

循环驱动层是 Harness 的「发动机」:它上承编排层(多 Agent 就是多个循环的组合)、下接模型内核(每轮的一次无状态调用),旁边靠状态与恢复层保证随时能停下再继续。本章的 ReActAgent 是内循环;下一章会看到套在它外面的任务外循环。

本章概念清单 / 点击进入概念卡片

章末测验

chapter quiz

0/4 已答

  1. Q1Claude Code 团队对 Agent 循环的定义中,最基本的形态是哪个顺序?

  2. Q2在 openJiuwen ReActAgent 的主循环里,「模型输出中没有 tool_calls」意味着什么?

  3. Q3循环运行中用户突然补充一条新指令,ReActAgent 是怎么处理的?

  4. Q4ReAct 循环用 for-else 结构处理「循环自然跑完却没 break」的情形,此时返回什么?