长任务很少和某个聊天窗口同时开始、同时结束。任务可能等待 CI、review 或授权,也可能因为时间和环境重启被拆成几次执行。要让后一次真正接住前一次,系统必须把“任务”从“这次对话”里拿出来。

读完这篇,你应该能回答:Outer Loop 是什么、为什么需要、最小版本有哪些部分;任务中断、并发处理和外部动作状态不明时分别怎样处理。

资料说明:本文用 OpenAI Symphony 的公开规范与 Elixir 实现解释跨运行调度,并以 Harness engineering 和 Anthropic long-running harness 补充设计经验。源码链接固定到核实快照;任务持久化、跨实例租约与副作用日志是按需求选择的工程设计,不代表 Symphony 已内置这些能力。篇末社区教程只补充教学视角。

一、先从失败故事看懂 Outer Loop

1.1 一个没有 Outer Loop 的任务怎样失败

第一次运行里,Agent 找到支付测试的失败原因,改了代码,但还没来得及运行完整验证就结束了。第二次运行启动时只收到最初任务,不知道前一次改过什么,于是重新调查;它看到创建 PR 的请求超时,以为失败,又创建了第二个 PR。

模型并不一定更笨。真正缺失的是一套跨运行控制:任务做到哪一步?哪次运行现在负责它?什么结果才能结束?中断后从哪里继续?一个外部动作究竟成功还是失败?

1.2 先认识四个最小概念

  • 任务卡(work item):真正要完成的长期工作,例如“支付测试修复并通过 review”。它可以存在几小时或几天。
  • 一次运行(run):Agent 针对任务卡进行的一次有限尝试。它可能完成,也可能中断或等待。
  • Agent Loop:一次运行内部“想一步、做一步、看结果”的循环。
  • Outer Loop:包在多次运行外面,负责让同一张任务卡被触发、接手、验证、恢复和重试的控制流程。

一句话记住:Agent Loop 管一次运行怎样前进;Outer Loop 管任务怎样跨多次运行活下去,直到完成或交给人。

1.3 一张任务卡怎样从进入系统走到完成

  1. 创建任务:测试失败事件或人工请求生成任务卡,写清目标和完成条件。
  2. 选择任务:系统从等待列表中挑出当前可以处理、优先级合适的一项。
  3. 启动运行:把目标、可用工具、权限和上次进度交给 Agent。
  4. 持续保存:运行过程中记录确认事实、代码改动、测试结果和未完成事项,而不是等最后才保存。
  5. 独立验证:CI、review 或业务检查确认结果;Agent 自己说“完成”只是一条候选结论。
  6. 决定下一步:验证通过就结束;失败就带着新证据再运行;缺权限就等待;多次尝试无效就交给人。

这六步描述一套完整工作系统应分配的责任;某个调度器可能只实现其中一部分。后面的 queue、lease、checkpoint 和 idempotency,都是在任务变多、运行会中断或存在外部动作后,为这条主干补上的保护机制。

二、把任务卡变成可运行的控制流程

2.1 任务为什么要离开聊天,成为独立任务卡

聊天记录适合保留对话,却不是任务本身。任务卡最先需要的只有稳定编号、目标、完成条件、当前状态、已完成和下一步。任务规模扩大后,再逐步加入优先级、依赖、当前处理者、尝试次数和外部对象引用。Run 只是任务卡上的一次处理记录,不应取代任务卡。

通用 Outer Loop 设计:运行期间持续记录,独立验证后决定完成、条件重试或等待人工
Inner loop 处理一个 work item;outer loop 决定何时创建、交给谁、是否接受结果以及下一步。这是通用设计图,不是 Symphony 的实现图。上方回路表示运行中的持续记录;下方回路只在需要时重试,任务通过独立验证后才完成。
任务:修复支付模块回归
状态:处理中
完成条件:支付测试通过,review 接受
已经确认:缺少过期订单校验
已经修改:payment.go
下一步:运行支付测试
最小任务卡:先让下一次运行能看懂目标、进度和下一步。并发、重试和副作用字段会在出现相应问题时再加入。

在这套设计中,任务卡的最新版本保存在 Outer Loop 使用的持久化存储中,而不是某次 Prompt 或聊天历史里。启动 Run 时,系统只把当前目标、权限和进度放进 Agent 的输入;Run 通过 checkpoint 写回新事实;verifier 通过后,Outer Loop 才把任务状态从“待验证”改为“完成”。后续 Run 和人类 reviewer 都从这张更新后的任务卡与 artifact 引用继续。

2.2 把刚才的六步变成可运行的责任

这六项不是只执行一次的直线流水线。保存进度从 run 启动前就开始,并贯穿执行与最终交接;否则进程若在验证前崩溃,系统恰好没有可恢复事实。后文会逐个解释并发领取、checkpoint 和外部动作记录,先用普通语言记住表里的责任即可。

阶段控制问题必须留下的事实
Trigger什么事件或人工请求创建任务?任务来源、是否重复、时间
Select work现在哪项任务可以开始?选择理由、当前处理者
Launch run本次 Agent 得到什么目标、工具和进度?运行配置、输入版本、上次进度
Verify哪些独立检查可以接受结果?检查证据、失败原因
Persist哪些进度和外部变化必须保存?恢复起点、动作记录
Retry / next结束、等待、换策略还是找人?决定理由、剩余尝试

2.2.1 Symphony 怎样接力支付任务

把支付问题放进 issue tracker 后,Symphony 负责轮询、挑选、启动 worker 和重新检查状态。规范把它定位为 scheduler/runner 与 tracker reader:改 ticket 状态、写评论与 PR 链接通常由 coding agent 通过工具完成;CI 与 review 的通过标准仍由团队工作流定义。成功交接可以停在 Human Review,不能直接等同于业务任务 Done。

同一次 worker 内,AgentRunner 在每个正常结束的 turn 后刷新 issue;仍活跃且符合路由条件时,继续使用当前 session,直到达到本次运行的 turn 上限。worker 正常退出且未进入等待输入状态后,调度器安排约 1 秒后的 continuation 检查,再读取 issue 决定是否启动新 worker。源码里的 completed 只是本地记账,派发条件并不拿它拦截后续运行。

支付 issue 仍活跃
→ turn 完成:刷新 issue;未到上限则在同一 session 继续
→ worker 达到 turn 上限:正常退出
→ 约 1 秒后重新检查:仍符合条件则启动新 worker
→ issue 进入工作流的非活跃交接状态:释放 claim
→ CI / review / 人工验收:按团队规则决定任务是否完成
运行与任务分开:前三个完成信号都不能单独证明支付修复已被接受;tracker 状态决定是否继续调度,验收规则决定结果是否合格。

2.3 怎样防止两个执行者同时写同一任务

先看只有一个调度器的情况。Symphony 用一个 GenServer 串行修改调度状态,派发前检查 claimed、running 与 blocked,再检查总并发、issue 状态和 worker 容量。这些检查与启动后的 claim 记录防止该调度器重复派发同一 issue;它们是进程内的集合和 map,不是多个独立调度器共享的租约服务。

若改成多个调度器共同领取同一张支付任务卡,才需要额外解决跨实例互斥与故障接管。常见设计是把限时处理权写入共享存储:执行者定期续期,过期后别人才能领取,这叫 lease;存储或执行外部写的接收方还要检查递增的 fencing token,拒绝旧执行者迟到的写入。仅在 Prompt 中要求“别重复执行”不能建立这个保证。

Symphony 的 claim、running 与 retry 位于调度器内存,重启后从 tracker 和保留的 workspace 重新调度
Symphony 的进程内调度状态与外部 issue、workspace 分开保存。下面的 lease、fencing 与副作用日志是扩展设计,图中没有把它们画成现有实现。

当部署确实需要这些扩展时,各机制分别保护以下条件:

  • Deduplication:相同外部事件只创建一个 work item。
  • Lease:同一时刻只有一个 run 可以写任务状态;只读调查可另设并行策略。
  • Heartbeat:区分长运行与已死亡 worker。
  • Fencing token:旧 lease 即使迟到,也不能覆盖新 worker 写入的状态。
  • Backpressure:根据资源、风险和人工审查容量限制启动。
queued
  -- worker-A claim, fence=1 --> running(A)
  -- lease expires -----------> claimable
  -- worker-B claim, fence=2 --> running(B)

complete(worker-A, fence=1)  # rejected: stale lease
complete(worker-B, fence=2)  # accepted
分布式设计示例,并非 Symphony 状态机:Queue 只决定谁先被看见;lease 让处理权过期;fencing token 让旧 worker 的迟到写被确定性拒绝。

三、进阶:跨运行恢复、重试与副作用

3.1 中断以后,下一次运行怎样接上

Checkpoint 就是下一次运行可以恢复的已保存起点。回到支付故事:Run 1 在结束前写下目标、已确认原因、payment.go 的修改、尚未执行的测试和日志引用;Run 2 启动后先核对文件与引用是否仍然有效,再从“运行支付测试”继续,而不是重新调查。

Symphony 的恢复范围更小:启动时重新建立空的调度状态,随后轮询 tracker 并复用保留的 workspace;规范明确不恢复旧进程的 retry timer、运行 session 或 live worker 状态。因此“工作目录还在”可以保住代码与文件,却不等于旧会话和重试次数仍在。团队仍需让 Agent 写下可核对的进展、测试与外部对象引用,后续运行再确认它们是否有效。

Anthropic 的 long-running harness 经验强调,让后续 agent 能从明确进展文件、git 状态和干净的任务切片继续。这样的 handoff 同时服务机器与人:结构化字段保证恢复,叙述记录决策与异常;原始 artifact 保留证据。

运行 N 保存进度、外部动作和原始证据,后续运行先对账再继续
通用交接设计:下一次 run 不应重新猜上次发生了什么,也不应盲信无原始结果的摘要。图中的进度与外部动作记录不是 Symphony 内置数据库;后续运行须先核对外部事实再继续。
Handoff 字段回答的问题恢复检查
Goal / done要完成什么?是否仍有效
Confirmed facts已经知道什么?来源是否可访问、新鲜
Effects已经改了什么外部状态?与真实世界对账
Attempts哪些路径失败、为什么?避免同策略重复
Open / next阻塞点和最小下一步?能力与权限是否足够
Artifacts日志、diff、测试和截图在哪里?引用是否完整

3.2 失败后不是简单地“再试一次”

Inner loop 的一次工具重试与 outer loop 的整次 run 重试不同。后者成本更高,也更容易重复副作用。每次重新启动前要问:失败类别是什么?新 run 会得到什么变化?旧效果是否已对账?还剩多少预算?如果答案只有“再试一次”,就不该自动重试。

需要跨进程重启限制总尝试次数或总花费时,重试预算应随 work item 持久化。Symphony 当前把 retry attempt 与 timer 保存在内存,失败重试使用带最大等待时间的指数退避;等待时间上限并不是总尝试次数上限,重启也不会恢复这些重试记录。可以按错误类别设置不同策略:基础设施短暂错误自动退避;权限阻塞直接等待审批;同一 invariant 连续失败转人工;高风险外部写在状态不明时先 reconcile,不能重放。

3.3 请求超时,不等于外部动作没有发生

“创建 PR”“退款”“发通知”都可能出现请求成功但响应丢失。Outer loop 若只看本地超时,会再次执行。在需要避免重复效果的工作流中,每个可重试副作用应携带稳定 idempotency key,并把 proposed、started、committed、observed 状态写入动作状态日志。恢复时先查询外部系统,再决定继续、补偿或完成。这是业务工作流应落实的设计,不能从 Symphony 的 issue claim 推导出创建 PR 或退款已经具备幂等性。

effect_key = "issue-1842:create-pr:v3"
status log: proposed → started → unknown
remote: PR #41 already exists

reconcile(effect_key)
  → find PR #41
  → status log: committed → observed
  → do not create PR #42
超时前后:天真的流程是 timeout → retry → 两个 PR;安全流程是 timeout → reconcile → 复用已有 PR。因此副作用发生前就必须持久化,不是 verify 之后才记录。
  • 把“我调用过”与“世界已经改变”分开记录。
  • 优先使用外部 API 的幂等键;没有时用业务唯一键和对账查询。
  • 补偿动作也要被建模和审计,不能假设所有效果可回滚。
  • 未知状态不是失败重试,而是 reconciliation 状态。

3.4 什么时候必须把任务交给人

OpenAI Harness engineering 强调人类注意力稀缺。Outer loop 应只在需要人的判断时打断:目标冲突、高风险授权、无法自动判定的质量,或是否继续投入预算。系统要给出精简但充分的说明:发生了什么、已验证什么、有哪些选项与影响、推荐动作和截止时间。

四、复盘:从最小版本逐步加保护

问题最低机制没有它会怎样
重复事件会创建重复工作吗?dedupe key并发重复副作用
多个调度器中哪个 worker 可以写任务?共享 lease + 接收方 fencing 检查双写或永久卡住
Run 结束后怎样判断完成?独立 verifier把 final 当结果真值
下一次 run 从什么继续?checkpoint + artifacts丢状态、重做调查
重试会改变什么?error class + persistent budget无限烧钱
外部效果状态不明怎么办?idempotency + action status log重复退款、PR 或通知
什么时候找人?escalation policy任务无人处理或人被频繁打断

Outer loop 把 run 变成了可运营的工作系统。但它仍需要一个外部真值来判断“verify”是否可信。最后一篇进入 Evals 与反馈:怎样测结果、定位失败层,并把证据变成下一次系统改动。

官方资料

延伸阅读