1. 从 Codex Goal 到 Loop Engineering:为什么你的 Agent 循环总在失控
如果你最近在折腾 Codex 的 Goal 功能,大概率会有一种感觉:它看起来只是给对话加了个“长期目标”,但实际用起来,任务的推进方式、停止条件、恢复逻辑都跟普通 chat loop 完全不是一回事。这背后其实藏着一个正在被反复讨论的概念——Loop Engineering,也就是把开放式 Agent 循环改造成可恢复、可转向、可限流、可验收、可审计的程序化运行系统。
Codex Goal 是理解 Loop Engineering 最好的入口。它没有把 Agent Loop 写成“while 模型没完成就继续”的自然语言循环,而是把长期任务拆成可持久化目标、可审计状态机、可拒绝的 idle continuation、可注入的 steering、可计量的预算和可观察事件。换句话说,它关心的不是让模型更努力地循环,而是把每一次继续、转向、停止、恢复和完成,都关进明确的状态边界与证据边界。
这篇内容面向想让 Codex 任务循环可控可复现的开发者。我会先讲清楚 Goal、Thread、Session、Turn 这几个容易混淆的名词,然后给出一个config.toml骨架,把状态机流转配置落成可复制的文件,最后演示一次从 Goal 触发到状态收敛的验证动作。你不需要读完 Codex 全部源码,但跟着配置走一遍,能对“Agent Loop 到底该怎么工程化”有一个可操作的认知。
2. 前置准备:TaoToken 接入与 Codex 环境确认
在动手写config.toml之前,先把模型接入这一层理顺。Codex 的 Goal 状态机本身是 runtime 行为,但它依赖模型在 turn 内完成 tool call、update_goal 等动作,所以模型端点的稳定性会直接影响状态收敛的可复现性。
我这边用的是 TaoToken 的 API 端点,官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api 。它的好处是兼容 OpenAI 风格的请求格式,Codex 这类工具在配置模型端点时不需要额外适配层。
你需要先拿到一个可用的 API Key。进入控制台创建密钥的路径是 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,创建完成后把 key 保存到环境变量里,不要直接写进config.toml明文。具体密钥管理页面在 https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。
环境变量建议这样设置:
export TAOTOKEN_API_KEY="sk-你的密钥" export TAOTOKEN_BASE_URL="https://taotoken.net/api"如果你还没装 Codex CLI,先确认版本。Goal 相关的 lifecycle hook 在较新版本里才完整暴露,旧版本可能只有基础的 chat 能力。可以用下面的命令检查:
codex --version确认版本后,把模型端点指向 TaoToken。Codex 的模型配置通常走~/.codex/config.toml,我们下一步就在这个文件里同时完成模型接入和 Goal 状态机配置。
3. config.toml 骨架:把 Agent Loop 状态机写进配置
Codex 的config.toml不只是模型参数文件,它同时承载了 Goal 状态机的运行时行为。下面这份骨架是我实测下来比较稳的结构,分成模型接入、Goal 状态机、continuation gate、预算与停止线四块。你可以直接复制后按需改。
# ~/.codex/config.toml # ---------- 模型接入 ---------- model = "gpt-5-codex" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY" wire_api = "responses" # ---------- Goal 状态机 ---------- [goal] enabled = true # 目标持久化到 thread 级状态表,而不是只放在 prompt 里 persist = true # 模型只能声明 complete / blocked,不能自行 pause / resume model_can_set = ["complete", "blocked"] # 用户或外部 API 才能设置的状态 system_owned = ["paused", "usage_limited", "budget_limited"] # ---------- continuation gate ---------- [goal.continuation] # 只在 thread idle 时尝试续跑 require_idle = true # Plan mode 下拒绝自动续跑 allow_in_plan_mode = false # 有 pending trigger-turn mailbox 时拒绝 allow_with_pending_mailbox = false # 已有 active turn 时拒绝 allow_with_active_turn = false # ---------- 预算与停止线 ---------- [goal.budget] # 单个 goal 的 token 上限 token_budget = 200000 # 时间上限,单位秒 time_budget_seconds = 3600 # 触顶后只允许总结,不允许开新实质工作 on_limit = "summarize_only" # ---------- 可观测事件 ---------- [goal.events] emit_thread_goal_updated = true emit_turn_attribution = true这份配置里最关键的是[goal.continuation]这一段。它对应的是 Codex 源码里try_start_turn_if_idle的拒绝条件:input 为空、有 pending trigger-turn mailbox、当前是 Plan mode、已经有 active turn,这四种情况都会让自动续跑被拒绝。把这些条件显式写进配置,好处是状态机的边界不再藏在代码里,而是变成可审计的声明。
[goal.budget]这一段对应的是状态库里的account_thread_goal_usage逻辑:每次 token usage 累加后,如果tokens_used + token_delta >= token_budget,状态就从 active 转成 budget_limited。配置里写死预算,比让模型自己“感觉差不多了就停”要可靠得多。
[goal]里的model_can_set和system_owned是权限分权的核心。模型可以判断“我认为目标完成了”或“我被阻塞了”,但不能判断“我要暂停”“我要恢复”“我触发 usage limit”。这些状态归用户或系统所有。Loop Engineering 的重点之一,就是分清“模型判断”和“runtime 裁决”。
4. 验证请求:从 Goal 触发到状态收敛的完整动作
配置写完后,需要跑一次完整的 Goal 生命周期,确认状态机真的按预期流转。下面用一个最小可复现的任务来演示:让 Codex 修一个测试文件里的失败用例,并提交可验证结果。
第一步,启动 Codex 并创建一个 goal。你可以用模型对话入口先确认端点连通: https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。如果只是想快速验证模型是否正常响应,这一步就够了。
第二步,在 Codex 里创建长期目标。Goal 的创建走create_goaltool,模型会把它写入当前 thread 的thread_goals。你可以用这样的 prompt 触发:
创建一个 goal:把 tests/test_parser.py 里失败的用例修到全绿, 并给出可验证的测试输出。完成前必须逐项审计证据。第三步,观察 turn 绑定。当 goal 创建后,下一次 turn start 会读取当前 thread goal。如果状态是 Active 或 BudgetLimited,runtime 会把当前 turn 绑定到 goal_id。这一步在配置里对应persist = true,它保证 token usage、tool finish、turn stop 的消耗能归因到正确的 goal_id。
第四步,触发一次 idle continuation。当 turn 结束、thread 进入 idle 后,runtime 会调用continue_if_idle。如果配置里require_idle = true且没有 pending mailbox,它会构造 continuation steering,然后调用try_start_turn_if_idle启动下一轮 regular turn。你可以在日志里看到类似这样的流转:
[goal] thread idle detected [goal] continuation steering constructed [goal] try_start_turn_if_idle accepted [goal] new regular turn started, bound to goal_id=goal_abc123第五步,验证状态收敛。当模型完成修复并调用update_goal complete后,状态从 Active 转为 Complete。如果中途 token 触顶,状态会转为 BudgetLimited,并且 runtime 会注入 budget limit steering,告诉模型收束,不要再开启新实质工作。你可以用下面的命令检查状态表:
codex goal status --thread-id <你的thread_id>预期输出类似:
goal_id: goal_abc123 status: complete objective: 把 tests/test_parser.py 里失败的用例修到全绿 tokens_used: 48213 token_budget: 200000 time_used_seconds: 312如果状态停在budget_limited而不是complete,说明预算设置偏紧,或者模型在无效路径上消耗了太多 token。这时候不要直接调大预算,先看 turn attribution,确认 token 花在了哪个 tool call 上。
5. 本篇常见错排查:状态机不收敛的几种典型情况
配置和验证跑通后,实际使用中还是会遇到状态机不按预期流转的情况。下面是我踩过的几个坑,按出现频率排序。
第一种,continuation 一直不触发。最常见的原因是 thread 没有真正进入 idle,或者有 pending trigger-turn mailbox。检查配置里allow_with_pending_mailbox是否为 false,以及是否有未处理的用户 steer。如果当前处于 Plan mode,allow_in_plan_mode = false也会让自动续跑被拒绝。Plan mode 会清掉当前 turn goal,这是 Codex 区分“规划”和“执行”的设计,不要把 plan turn 当成 goal progress。
第二种,goal 状态卡在 active 但模型不再推进。这通常是因为模型没有正确调用update_goal,或者 tool schema 里model_can_set配置过窄。确认配置里允许complete和blocked,这两个是模型唯一能设置的状态。如果模型反复声称完成但状态没变,检查update_goal的调用是否带了正确的expected_goal_id,旧 turn 或旧 mutation 误伤新 goal 的情况在并发场景下会出现。
第三种,token 消耗远超预期。Goal 的记账逻辑是累加式的,每次on_token_usage都会把当前 turn 的 usage 加到tokens_used上。如果发现消耗异常,先看time_used_seconds和tokens_used的比例。如果时间很短但 token 很高,可能是 tool output 太长,或者模型在重复读取同一个文件。这时候可以在配置里加一个 tool output 截断策略,或者把token_budget调低,让状态更早进入 budget_limited。
第四种,turn error 后 goal 没有转为 blocked。stop_active_goal_for_turn会在 turn error 或 usage limit 时先记账,再用expected_goal_id更新状态,并清掉 active goal。如果状态没变,检查 event emitter 是否正常,以及emit_thread_goal_updated是否为 true。外部观察依赖事件,事件丢了,UI 和 SDK 就看不到状态变化。
如果你在接入或排障过程中遇到端点层面的问题,可以先到 API Keys 页面确认密钥状态: https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite ,接入细节看文档: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 。如果只是验证模型是否正常响应,用模型对话入口最快: https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model-chat&utm_campaign=rewrite 。
6. 长期编码与 Agent 场景:把状态机配置沉淀成可复用资产
如果你打算把 Codex Goal 用在长期编码任务或 Agent 工作流里,单次配置是不够的。你需要把状态机配置沉淀成可复用资产,让不同的 thread 和 goal 都能继承同一套边界。
一个实用的做法是把config.toml里的[goal]段拆成独立的 profile 文件,按任务类型加载。比如修 CI 的任务用一套预算和 continuation 策略,重构任务用另一套。Codex 支持通过环境变量或命令行参数指定 profile,这样你不需要每次手动改配置。
对于需要长期运行的编码 Agent,建议把 Coding Plan 纳入考虑: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite 。它更适合有持续 token 消耗和状态恢复需求的场景,配合 Goal 的持久化状态,可以在进程重启后从thread_goals恢复 active goal,而不是从头开始。
如果你在用 Claude Code 或 Anthropic 风格的 Agent 工作流,TaoToken 也提供了对应的接入入口: https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_content=claudecode-anthropic&utm_campaign=rewrite 。状态机的设计思路是通用的,区别只在于 tool schema 和 lifecycle hook 的挂载点。
最后回到 Loop Engineering 本身。Codex Goal 最值得学习的不是 goal 这个功能名,而是它的构建顺序:先定义目标契约,再定义状态所有权,再定义执行线边界,再定义 continuation gate,再定义预算和停止线,最后定义可观测事件。这个顺序反过来,就是大多数 Agent 循环失控的原因——先写了循环,再补边界,结果边界永远补不齐。把config.toml当成状态机的声明文件来写,而不是当成模型参数来写,是让 Agent Loop 可控可复现的第一步。