从 Codex Goal 理解 Loop Engineering

📅 2026/7/4 11:09:06
从 Codex Goal 理解 Loop Engineering
从 Codex Goal 理解 Loop Engineeringgoal是 Codex 里最适合解释Loop Engineering的例子它没有把 Agent Loop 写成一个“while 模型没完成就继续”的自然语言循环而是把长期任务拆成可持久化目标、可审计状态机、可拒绝的 idle continuation、可注入的 steering、可计量的预算和可观察事件。它说明的核心判断是Agent Loop 的工程化重点不是让模型更努力地循环。 而是把每一次继续、转向、停止、恢复和完成都关进明确的状态边界与证据边界。1. 先把名词说清楚Goal、Thread、Session、Turn理解 Codex goal 前要先避开一个常见误解goal没有自己的独立 session。它是挂在thread上的长期目标状态用户看到的“一个对话”更接近 Codex 的Thread而 core 里的Session是这个 thread 当前在本地进程里的运行时对象。源码里也能看到这层关系Session持有thread_id、active_turn和input_queue并且注释明确说 “A session has at most 1 running task at a time”见codex-rs/core/src/session/session.rs#L20-L40。ThreadManager::NewThread表示新建的 Codex thread注释还写着 “formerly called a conversation”见codex-rs/core/src/thread_manager.rs#L112-L118。ThreadGoal持久状态里第一列就是thread_id而不是某个单独的 goal session id见codex-rs/state/src/model/thread_goal.rs#L60-L71。SessionId和ThreadId可以互转说明部分命名仍保留旧语义但 goal 的锚点实际是ThreadId见codex-rs/protocol/src/session_id.rs#L55-L59和#L119-L125。可以用这张层级图理解用户看到的一个对话 ≈ Thread ├─ thread_goals这个对话上的长期目标状态 ├─ Runtime Session当前进程里承载这个 thread 的运行时对象 │ ├─ active_turn当前正在执行的 turn │ └─ input_queue用户 steer、goal steering、mailbox 等 pending input └─ Turns一次次执行回合 └─ ModelClientSession单个 turn 内和 Responses API/WebSocket 交互的模型会话所以几个问题可以先定性用户新建一个对话 新建的是新的 ThreadId默认不会继承旧 goal。 用户在同一个对话里创建 goal goal 写入这个 thread 的 thread_goals。 进程重启后 resume 同一个对话 runtime session 会重建但 active goal 可以从 thread_goals 恢复。 goal 自动续跑 不会开一个独立 goal session而是在同一个 thread idle 后尝试启动新的 regular turn。这也解释了后面“把 turn 绑定到 goal_id”的含义它不是把 goal context 塞进模型而是在运行时记账层记录“这个 turn 正在推进哪个 goal”这样后续 token usage、tool finish、turn stop 的消耗能归因到正确的goal_id。2. Goal Context 是什么怎么嵌入模型Goal 的上下文分两层。第一层是持久状态不直接等于模型 promptthread_id goal_id objective status token_budget tokens_used time_used_seconds created_at updated_at这些字段保存在ThreadGoal中见codex-rs/state/src/model/thread_goal.rs#L60-L71。它们回答的是“这个 thread 上长期目标的事实状态是什么”。第二层是模型可见的 hidden context。Codex 不会每个 turn 都无脑把完整 goal 状态塞进 prompt而是在需要 steering 的时候构造InternalModelContextFragment(sourcegoal)再把它转成ResponseItem进入模型输入见codex-rs/ext/goal/src/steering.rs#L37-L54。这个 hidden context 的外壳由 core 定义codex_internal_context sourcegoal ... /codex_internal_context它的 role 仍是user但 marker 表明它是 runtime-owned internal context见codex-rs/core/src/context/internal_model_context.rs#L61-L114。Goal context 主要在三个时机注入thread idle 后继续 continuation_steering_item(goal) → 告诉模型继续推进 active thread goal → 带 objective、tokens_used、token_budget、remaining_tokens 用户或 API 更新 objective objective_updated_steering_item(goal) → 告诉当前 active turn 新 objective 覆盖旧 objective 预算耗尽 budget_limit_steering_item(goal) → 告诉模型收束不要再开启新实质工作其中 continuation 模板明确写着objective 是用户提供的数据不是更高优先级 instructiongoal 会跨 turns 持续完成前必须逐项审计证据只有完成或严格 blocked audit 满足时才能update_goal见codex-rs/ext/goal/templates/goals/continuation.md#L1-L51。所以 goal context 的作用不是保存全部对话历史而是给下一次 sampling 一个方向性 steering事实状态在 thread_goals 执行归因在 GoalAccountingState 模型方向通过 InternalModelContextFragment(sourcegoal) 注入3. 先讲功能事实Goal 到底做了什么用户给 Codex 一个长期目标例如“把这个项目的 CI 修到全绿并提交可验证结果”。普通 chat loop 容易把这个目标留在上下文里让模型自己记住。但上下文会增长、会压缩、会被 tool output 污染模型也可能过早声称完成。Codex 的goal做了几件具体事情把目标写入 thread 级持久状态而不是只放在 prompt 中。给模型暴露get_goal / create_goal / update_goal三个 goal tools。在 turn start、tool finish、token usage、turn stop、thread idle 等 lifecycle hook 上记账和更新状态。在目标仍为active且 thread idle 时自动生成 continuation steering尝试启动下一轮 regular turn。在预算耗尽、错误、usage limit、complete、blocked 时停止自动推进。通过ThreadGoalUpdated等事件让 UI/SDK 看到目标状态变化。所以 goal 不是 TODO list。它是一个围绕 Agent Loop 的长期任务控制层。4. Loop Engineering 的定义在 Codex goal 这个例子里Loop Engineering 可以定义为把一个开放式 Agent 循环改造成可恢复、可转向、可限流、可验收、可审计的程序化运行系统。它至少包含 7 个问题目标是什么目标是不是一个可持久化、可验证的 objective 状态归谁目标、turn、工具结果、预算、UI 事件分别由谁拥有 何时继续下一轮是模型自己决定还是 runtime 在安全点决定 如何转向用户或系统改目标时当前 turn 怎么知道 如何限流token/time/budget 怎么进入状态机 何时停止complete、blocked、usage limited、budget limited 谁有权设置 如何证明状态变化、事件、验收结果能不能被外部观察Codex goal 的价值在于它把这些问题都落到了代码结构上而不是只靠 prompt 约束。5. 主线Goal 是内环之外的长期任务外环传统 Agent Loop 的内环是模型生成下一步model sampling → tool call → tool output → 模型继续生成 → assistant final这里的model sampling指的是模型根据当前上下文生成下一段输出的过程可能是自然语言回复也可能是一次 tool call。它不是一个额外模块而是每次“让模型继续往下想一步”的推理生成动作。Codex goal 没有替代这个内环。它把“长期任务是否继续”放在内环外侧用户或模型创建 goal → turn start 绑定 active goal → tool/token/turn lifecycle 持续记账 → turn stop 后 thread idle → runtime 判断是否 continuation → try_start_turn_if_idle 启动下一轮 regular turn → 模型最终 update_goal complete 或 blocked这就是“Goal 位于 Agent Loop 外侧”的含义。内环回答“下一步怎么执行”外环回答“这个长期目标是否还应该继续执行”。可以先用这张图把外环位置定住activebudget limitcompleteblocked/error用户目标Goal Contractthread_goalsTurn Start绑定 goal_idAgent Inner LoopTool FinishUsage Accounting状态检查Thread Idletry_start_turn_if_idleContinuation SteeringBudget SteeringCompleteBlocked这张图的重点不是节点数量而是边界Goal Contract是目标边界。thread_goals是状态边界。Agent Inner Loop是执行边界。try_start_turn_if_idle是自动化边界。Budget / Complete / Blocked是停止边界。有了这张图后面看工程动作时就不会把 goal 误解成“更长的 prompt”或“模型自己继续循环”。源码阅读路径放到文末避免在主线刚展开时打断叙事。6. Loop Engineering 的 6 个工程动作这 6 个动作本质上是在回答同一个问题如何让目标、运行时、模型输入和外部观察各自有清晰的状态边界而不是把所有东西都塞进 prompt。持久状态thread_goals 里的事实记录 运行时状态GoalRuntimeHandle 里的控制面 模型可见状态get_goal tool 与 goal hidden context 观察状态ThreadGoalUpdated 等事件这四层不能合并。目标事实不靠模型记忆执行控制不靠模型自觉外部观察也不靠 assistant 最终回复。每一层都有自己的 owner、生命周期和证据出口。6.1 把目标从上下文移到状态表如果 goal 只是 prompt 中的一段文字compact、tool output、用户 steer 都可能改变模型对目标的理解。Codex 用thread_goals把目标变成 thread 级状态。这一步解决的压力是目标漂移。没有状态表 目标 模型上下文中的一句话 有状态表 目标 objective status budget usage goal_idgoal_id很关键。外部更新时会带expected_goal_id避免旧 turn 或旧 mutation 误伤新 goal见codex-rs/ext/goal/src/api.rs#L153-L164。6.2 把模型权限关进 tool schema模型可以调用create_goal创建 active goal但update_goal只能标记complete或blocked见codex-rs/ext/goal/src/tool.rs#L221-L234。这一步解决的压力是模型越权。模型可以判断 我认为目标完成了 我被阻塞了 模型不能判断 我要暂停 我要恢复 我触发 usage limit 我触发 budget limit这些状态归用户或系统所有。Loop Engineering 的重点是分清“模型判断”和“runtime 裁决”。6.3 把每一轮执行绑定到 active goalon_turn_start会读取当前 thread goal。如果目标是Active或BudgetLimited就把当前 turn 绑定到goal_id见codex-rs/ext/goal/src/extension.rs#L201-L239。这一步解决的压力是责任归属。turn 不是孤立执行 turn 要知道自己是不是在推进某个 goal token/time/tool progress 也要知道应该记到哪个 goal 上Plan mode 会清掉当前 turn goal见codex-rs/ext/goal/src/extension.rs#L216-L221。这说明 Codex 区分“规划”和“执行”不会把 plan turn 当成 goal progress。6.4 把工具和 token 变成进度记账on_token_usage记录当前 turn 的 token usage见codex-rs/ext/goal/src/extension.rs#L326-L352。on_tool_finish会在工具完成后调用account_active_goal_progress如果目标进入BudgetLimited就注入 budget limit steering见codex-rs/ext/goal/src/extension.rs#L359-L402。状态库里的account_thread_goal_usage会增加time_used_seconds和tokens_used并在tokens_used token_delta token_budget时把状态转成budget_limited见codex-rs/state/src/runtime/goals.rs#L411-L523。这一步解决的压力是无界循环。没有记账 Agent 只知道“我还没完成” 有记账 runtime 知道这个 goal 已经消耗多少 token/time是否触顶6.5 把继续执行放到 idle gateon_thread_idle调runtime.continue_if_idle()见codex-rs/ext/goal/src/extension.rs#L154-L166。continue_if_idle做完状态检查后不直接开后台 worker而是构造 continuation steering然后调用thread.try_start_turn_if_idle(vec![item])见codex-rs/ext/goal/src/runtime.rs#L359-L415。try_start_turn_if_idle会拒绝input 为空。有 pending trigger-turn mailbox。当前是 Plan mode。已经有 active turn。构造默认 turn 后又出现 pending work 或 Plan mode。证据在codex-rs/core/src/session/inject.rs#L38-L129。这一步解决的压力是自动化抢占用户。错误做法 goal active → timer loop → 新 turn Codex 做法 goal active → thread idle → try_start_turn_if_idle → regular turn所以 goal automation 不是新执行系统而是重新进入 Codex 原本的 turn loop。6.6 把错误、预算和完成变成停止线Codex goal 有多条停止线update_goal complete模型声明完成。update_goal blocked模型声明阻塞。turn errorruntime 把 active goal 转为blocked。usage limitruntime 把 active goal 转为usage_limited。budget limit状态库把 active goal 转为budget_limited。paused用户或外部 API 暂停。状态定义在codex-rs/state/src/model/thread_goal.rs#L12-L41。stop_active_goal_for_turn在 turn error 或 usage limit 时先记账再用expected_goal_id更新状态并清掉 active goal见codex-rs/ext/goal/src/runtime.rs#L243-L333。这一步解决的压力是“坏循环不停跑”。错误不是被 prompt 忽略而是进入状态机。把这 6 个动作合起来看Codex goal 反驳的是一种看似简单的 prompt loop请持续推进目标直到完成如果遇到问题就停止。这类 prompt 把关键判断都交给模型目标是否仍有效、是否该继续、是否消耗过多 token、是否被用户新输入覆盖、是否真的完成、错误是否应该重试。Codex goal 的做法是拆权模型推进任务并声明complete / blocked工具执行具体动作runtime 绑定 turn、记账、续跑和错误停止state DB 保存目标事实和预算事实user/API 负责设置、暂停、恢复和替换目标UI/SDK 观察状态变化。所以差别不是“有没有让 Agent 继续做事”而是继续这件事有没有被工程化治理有 Codex Goal 目标被保存 → turn 绑定 goal_id → 工具和 token 形成 usage delta → idle 时 runtime 判断 continuation → busy 或 Plan mode 时拒绝 → complete / blocked / budget / error 都落状态 只靠 prompt 目标只在上下文 → 模型自己决定是否继续 → 工具输出挤占上下文 → 用户转向和旧目标混在一起 → 出错后可能继续重试 → 完成只靠 assistant 自述7. 用 Codex Goal 反推 Loop Engineering 框架如果从零构建一个长期 Agent Loop不要先写循环。应该按这个顺序构建7.1 定义目标契约压力长期任务不能靠一句模糊愿望驱动。状态objective、done evidence、scope、constraints、anti-cheat、verification。不变量没有可验证完成线不进入 autonomous loop。验收另一个 Agent 能只看 goal contract 判断目标是否完成。7.2 定义状态所有权压力模型、用户、系统都可能改状态必须分权。状态模型只能 set complete/blocked用户可以 pause/resume/edit objective系统控制 usage/budget/error。不变量模型不能伪造系统状态。验收状态转移表里每个状态都有唯一或明确的 owner。7.3 定义单执行线和 pending input压力多 turn 并发会破坏 worktree 和 history。状态active turn、pending input、mailbox、thread idle。不变量同一 session 同时最多一个 active task。验收运行中 steer 进入 pending inputautomation busy 时被拒绝。7.4 定义 continuation gate压力长期任务需要自动继续但不能抢用户输入。状态thread idle、Plan mode、pending trigger work、active turn。不变量自动化只能在安全空闲点启动普通 turn。验收busy、Plan mode、pending user work 时 continuation 不启动。7.5 定义预算和停止线压力长期任务如果没有停止线会把 token 和时间耗尽。状态tokens_used、time_used_seconds、token_budget、usage limit、turn error。不变量预算和错误由 runtime 裁决不由模型自说自话。验收预算触顶后不再开新实质工作只总结当前状态和下一步。7.6 定义可观测事件压力长跑任务必须能被 UI、人和其他客户端观察。状态ThreadGoalUpdated、metrics、analytics、turn attribution。不变量状态变化必须能追到 event_id 或 turn_id。验收create/update/blocked/budget/complete 都能被外部订阅者看到。8. 最后总结Codex goal 说明的 Loop Engineering 可以压缩成一句话把 Agent 的开放式循环改造成以 goal state 为锚点、以 turn lifecycle 为节拍、以 idle gate 为自动化入口、以 budget/error/complete 为停止线的可治理系统。它最值得学习的不是goal这个功能名而是这个构建顺序先定义目标契约 再定义状态所有权 再定义执行线边界 再定义 continuation gate 再定义预算和停止线 最后定义可观测事件这也是 Agent 产品从 demo 走向可靠系统的关键分水岭不是模型会不会循环而是循环本身是否被工程化治理。9. 附代码锚点如果要回到源码验证可以按这条最短路径读codex-rs/ext/goal/src/extension.rs#L98-L175读什么GoalExtension如何挂进 thread lifecycle。读完知道goal runtime 在 thread start 创建在 thread idle 调continue_if_idle在 thread stop 注销。codex-rs/ext/goal/src/runtime.rs#L23-L50读什么GoalRuntimeHandle拥有哪些运行时状态。读完知道goal runtime 持有thread_id、state DB、event emitter、metrics、thread manager、accounting state、enabled flag 和goal_state_lock。codex-rs/ext/goal/src/tool.rs#L180-L291读什么模型能用 goal tool 做什么。读完知道模型可以创建 goal也只能把 goal 更新为complete或blocked不能任意设置paused / budget_limited / usage_limited。codex-rs/ext/goal/src/runtime.rs#L359-L415读什么自动 continuation 的核心 gate。读完知道goal 只有在 tools visible、live thread 存在、DB 中目标仍 active、try_start_turn_if_idle通过时才自动继续。codex-rs/core/src/session/inject.rs#L38-L129读什么extension 发起 idle work 的共享闸门。读完知道Plan mode、busy active turn、pending trigger-turn mailbox 都会拒绝自动启动。codex-rs/state/src/model/thread_goal.rs#L12-L71读什么goal 持久状态 schema。读完知道状态不是一句话而是status / budget / usage / timestamps / goal_id这些可验证字段。