Agent Harness 学习
← 返回对照矩阵

main loop

循环

主循环形态

「继续做下一步」这个决定,由什么结构做出?

横向读这一行

四家在同一个问题上分成两派:openJiuwen 用有界迭代预算(for range),其余三家都是无界循环,靠外部条件退出。但四家都在循环顶部处理同一件事——用户在模型跑的时候插话(steering / pending input),这说明「可打断」已经是主循环的默认要求,而不是附加功能。

对比
openJiuwen带代码

有界迭代预算的 for 循环

唯一一个用固定迭代上限的实现:max_iterations 默认 5,循环体是 range 而不是 while。每轮开头先消费 force_finish(由 rails 在 AFTER_REACT_ITERATION 设置),让优雅中止落在迭代边界而不是打断执行到一半的一轮;随后注入 rails 放行的 steering 消息。真正的长任务由外层 Task Loop 承担,ReAct 只负责「一步做对」。

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

    # Inject the steering messages the rails let through
    # before the next model call.
    steering = await self._drain_steering_batch(ctx)
openjiuwen/core/single_agent/agents/react_agent.py:2051-2066@ fd6c47854201
Codex带代码

无界 loop,回合内反复取用户插话

run_turn 内部是一个没有计数上限的 loop。每次进入循环先尝试排空 input_queue —— 也就是模型还在跑时用户从界面提交的消息。注释直白地写出了这里的张力:界面允许用户随时插话,但模型不一定接得住。退出靠 hook 返回、任务完成或取消令牌,而不是迭代次数。

let mut next_step_context = Some(first_step_context);
loop {
    // Note that pending_input would be something like a message the user
    // submitted through the UI while the model was running. Though the UI
    // may support this, the model might not.
    let pending_input = if can_drain_pending_input {
        sess.input_queue
            .get_pending_input(&sess.active_turn)
            .await
            .0
    } else {
        Vec::new()
    };
codex-rs/core/src/session/turn.rs:300-312@ 4beea50e26dd

idle / running / maintenance 三相状态机

循环本身只有一行:while (await this.turn()) {}。真正的复杂度在它外面的相位机——Agent 在 idle、running、maintenance 三种相位之间转换,唤醒(wake)在非 idle 相位会被闩锁下来等待收敛后重放。这让「模型正在跑的时候来了新消息」变成一个显式的状态转换问题,而不是循环体里的一段 if。

private async kick(): Promise<void> {
  try {
    while (await this.turn()) {}
  } catch (_error) {
    // Reported failures and cancellation are contained at the driver boundary.
  } finally {
    if (this.phase.kind === 'running') {
      const { turn, wakeRequested } = this.phase
      this.setPhase({ kind: 'idle', lastTurn: turn })
      if (wakeRequested && this.inbox.hasPending) this.wakeDriver()
    }
  }
}
packages/core/agent-loop/src/agent.ts:210-223@ b150a551b8d4
Pi带代码

显式内外两层 while,注释直接写明分工

最容易读懂的一份:外层循环负责「Agent 本来要停了,但排队消息又来了」,内层循环负责「还有工具调用要处理」。openJiuwen 用 ReAct + Task Loop 两个模块表达的双层结构,Pi 用同一个函数里的两个 while 表达,语义几乎一一对应——这是双层循环属于问题本身、而非某家设计偏好的直接证据。

// Check for steering messages at start (user may have typed while waiting)
let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || [];

// Outer loop: continues when queued follow-up messages arrive after agent would stop
while (true) {
    let hasMoreToolCalls = true;

    // Inner loop: process tool calls and steering messages
    while (hasMoreToolCalls || pendingMessages.length > 0) {
packages/agent/src/agent-loop.ts:166-174@ bfb004d4418f

design space / 设计空间分析

四家在「谁来决定还要不要做下一步」上给出了从有界预算到显式状态机的一段光谱,openJiuwen 和 deepseek harness 分别占住两端。

openJiuwen 是唯一用固定迭代上限的实现:ReAct 循环体是 range 而不是 while,max_iterations 默认 5,真正的长任务交给外层 Task Loop 承担,ReAct 只负责「一步做对」。循环体足够短,是因为它把「做多少步」这个问题挪到了循环外面。

把四格并排读,「继续还是停」这个决定被放在不同的结构层级里:openJiuwen 把它压缩成循环边界上的一个数字判断(for + max_iterations),每轮开头还要先检查 rails 在上一轮设置的 force_finish,让优雅中止落在迭代边界上;deepseek harness 把它挪到循环外面的相位机里,循环本身只剩 while (await this.turn()) {} 一行,复杂度在 idle/running/maintenance 三态转换和被闩锁、等收敛后重放的 wake 请求里;codex 干脆不在循环层面设判断,loop 本身没有计数上限,退出交给 hook 返回、任务完成或取消令牌;pi 用同一个函数里的两层 while 把这个问题拆成两半——外层问「是否还有排队消息」,内层问「是否还有工具调用要处理」。

编辑判断

四家的差异更像是对「Harness 该管多宽」的不同回答,而不是循环写法本身的优劣:把循环收窄成一步、把编排交给上层的做法(openJiuwen),扩展性更好但要求上层足够厚;把循环做成状态机的做法(deepseek harness),单体能力更强但可替换性更差;codex 和 pi 是中间地带——前者不设状态机,把继续/停止的判断权交给外部的 hook 和取消令牌,后者用双层 while 把两种「还要不要继续」的问题在同一个函数里分开问,不假外部结构。这一段是本站的编辑判断,不是各项目的官方表述。

尚未收敛:有界迭代、相位状态机、hook 退出、双层 while 这四种结构在长任务和多轮打断场景下实际的鲁棒性差多少,本站目前没有可比的一手测试数据,只停留在读源码层面的结构比较。

back to course / 回到课程

矩阵展示的是「各家怎么做」。这个问题本身为什么存在、有哪些经典权衡,在课程里讲:

2

Agent 循环

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