AI Coding Harness
先给结论
- 普通 Agent loop 不是定时任务。它是“模型生成 → 工具执行 → 结果回送 → 再次生成”的即时反馈状态机。
- Codex Goal 也不是定时任务。固定源码显示,它在 thread 进入 idle 时触发
continue_if_idle,未完成就立即尝试开启下一个 turn。Codex Goal 文档把目标文本同时作为首轮 prompt 和完成标准。 - Claude Code
/goal不是定时任务。它在每个 turn 结束时调用一个无工具的小模型 evaluator 判断 yes/no;/loop才是每秒检查到期任务、在 turn 之间入队的 session scheduler。 - OpenCode 核心没有 Goal keeper 或持久 scheduler。固定
v1.18.16的SessionPrompt.runLoop是同步while (true);Goal 与定时运行来自社区插件、GitHub Actions 或 OS scheduler。 - “轻度到最高只是 prompt 不同”这一说法不成立。三者的公开实现都是结构化 effort、thinking budget 或 provider variant;服务端内部如何消费该信号没有公开,但客户端并不存在一组可合法列出的逐档隐藏 prompt。
- Codex 当前界面的 Ultra 不是 Max 的简单上一级。Max 给单个模型更多推理时间;Ultra 把单次请求按最大努力运行,并增加自动子 Agent 编排。官方模型文档也把两者分开定义。
- 三者对子 Agent 的主要差异在上下文种子。Codex 可以选无历史、完整历史或最近 N turns;Claude Code 普通 subagent 是 fresh,fork 才完整继承;OpenCode Task 是 fresh child session,没有复制完整父历史的核心开关。
Harness 是什么
单独调用一次模型,只得到一段输出。Coding Harness 还要维护上下文、注册工具、执行动作、归档结果、判断停止、压缩历史、控制权限,并在需要时派发子 Agent。更准确的分层是:
| 层 | 触发条件 | 典型动作 | 停止条件 |
|---|---|---|---|
| Tool loop | 用户输入或上一个 tool result | 模型请求、tool call、执行、结果回送 | 无 tool call、达到步数/预算、错误或用户中断 |
| Goal loop | turn 完成或 thread idle | 评估目标,未完成则开启下一 turn | 完成、严格阻塞、暂停、清除或预算耗尽 |
| Time loop | wall clock 到期 | 把一条 prompt 入队,再运行普通 tool loop | 一次性结束、recurrence 删除或 session 关闭 |
因此,while (true) 不自动意味着定时任务。只要循环由上一轮输出或工具结果立即推进,它仍是事件/反馈驱动;只有等待 wall clock、cron 或 recurrence rule 才是时间调度。
为了避免只记住流程箭头,可以从五个视图理解 Harness:
| 视图 | 要回答的问题 | 本文中的对象 |
|---|---|---|
| 动机 | 为什么模型不能只调用一次 | 工具行动、长期目标、定时复查、上下文隔离 |
| 物理 | 哪些运行实体真实存在 | Parent/child session、模型服务、tool runtime、scheduler |
| 逻辑 | 状态与控制量如何定义 | history、goal、effort、variant、steps、budget |
| 时序 | 什么事件触发下一步 | tool result、turn end、thread idle、clock due |
| 数据流 | 什么内容进入下一次请求 | messages、instructions、reasoning field、delegation prompt |
三种循环
Codex
本文固定 openai/codex@eb9dceba1a2e658142a456c5898836774835616b。源码中的 Goal 扩展在 on_thread_idle 调用 continue_if_idle;后者读取持久化 goal,确认状态仍是 active,再调用 try_start_turn_if_idle。这是一条明确的idle event → 状态检查 → 新 turn路径,没有 interval polling。
Codex 还公开了 Goal continuation 模板。它要求保留完整目标、核对 token budget、以当前 worktree 和外部状态为证据、逐项审计完成条件;只有证据证明全部完成才调用 update_goal(complete),同一阻塞连续出现至少三次才允许标记 blocked。模板全文可在固定源码的 continuation.md 查看。
Claude Code
Claude Code 官方说明把 agentic loop 概括为 gather context、take action、verify results。更底层看,每个 tool call 的 result 会进入下一次模型请求,直到模型不再调用工具。
/goal 在这个基础上增加一个 turn-end Stop evaluator:每当 Claude 完成完整 turn,小型快速模型读取完成条件与当前会话,只返回 yes/no 和理由;no 会把理由作为下一 turn 的指导,yes 才清除 Goal。它不是固定间隔唤醒。
Claude Code 的 /loop 才有时钟:scheduler 每秒检查 due task,在 Claude 空闲、两个 turn 之间低优先级入队。也就是说,时间触发只负责创建新输入,输入内部仍走普通 agentic loop。
OpenCode
本文固定 anomalyco/[email protected],commit a3647eb025c7615159d417dcc49fc39fdaeba65b。核心 SessionPrompt.runLoop 直接使用 while (true):取最后消息,处理 compaction/subtask,组织 system/messages/tools,调用模型;当最后 assistant 已 finish 且没有需要回送的 tool call 时退出。
Agent 的 steps 是最大迭代数而非定时周期。达到最后一步时,OpenCode 注入公开的 max-steps.txt,要求停止工具调用并总结。核心没有 /goal 对等 evaluator;官方 GitHub 集成则可以由 Actions on.schedule.cron 从进程外定时触发。
推理强度
不是五段 Prompt
公开实现展示的是以下数据流:
1 | UI / config / agent role |
Codex 固定源码在构造 ResponsesApiRequest 时,instructions 和 input 来自同一 Prompt 对象,推理档位单独写入 reasoning.effort。OpenAI 官方 Reasoning Guide也把 reasoning: { effort: "low" } 定义为 request parameter,并说明更高 effort 通常使用更多 reasoning tokens、延迟和成本。
Claude API 使用 output_config.effort;OpenCode 则把 variant 合并进 provider options,例如 OpenAI 的 reasoningEffort、OpenRouter 的 reasoning.effort、Anthropic 的 adaptive thinking 与 effort。OpenCode 的模型 family system prompt 与 variant 选择也是两条独立路径。
各档有什么差异
| 用户说法 | 常见配置值 | 独立公开 prompt | 公开差异 |
|---|---|---|---|
| 轻度 | low |
无 | 更重视速度、较低 token 与较低延迟;适合边界清楚的执行、检索和分类 |
| 中 | medium |
无 | 质量、可靠性、延迟与成本的平衡点;适合日常 agentic coding |
| 高 | high |
无 | 更适合复杂调试、多步规划、边界检查和高价值任务 |
| 极高 | xhigh / Extra High |
无 | 用于深度研究、长时异步或困难代码工作;只在支持模型上可用 |
| 最高 | max |
无 | 单个模型投入最大推理;延迟和 token 代价最高 |
| 编排模式 | Codex ultra / Claude Code ultracode |
不是单一档位 prompt | 最大或极高 effort 加 Harness 的自动分解、子 Agent 与动态 workflow |
档位集合依模型而变;同名 high 也不代表跨 OpenAI、Anthropic 或其他 provider 使用相同 token 数。它更像一个行为预算/质量—成本旋钮,不是稳定的 prompt 字符串。
同一个 high 在客户端可观察到的精确差异是请求字段,而非对话文本:
1 | Codex / OpenAI API : reasoning.effort = "high" |
有两个需要单独说明的 prompt 特例:
- Codex Goal continuation 有公开模板,但它控制跨 turn 继续工作的行为,不是 reasoning effort。
- Claude Code
ultrathink会增加一次性 in-context instruction,而 API effort 值不变;官方没有公开这条 instruction 的逐字文本,也不能据此推导各档 prompt。
Codex 默认配置
默认值为什么不宜写死
Codex 当前文档给出两个层次:ChatGPT 界面的默认 Power preset 是 gpt-5.6-sol + medium;本地 App、CLI、IDE 如果没有在 config.toml 固定模型,则使用“recommended model”。固定源码自带的模型目录快照又把 gpt-5.6-sol 标为默认候选并给出 low 默认 effort。远端目录、账号、表面和 preset 都可能改变,因此不能把某一对值写成跨版本永恒默认。
可复现的做法是显式固定。全局配置写入 ~/.codex/config.toml,项目配置写入受信任仓库的 .codex/config.toml。优先级从高到低是:CLI/--config、项目配置、profile、用户配置、系统配置、内建默认。
1 | # ~/.codex/config.toml 或项目 .codex/config.toml |
一次性覆盖可以用:
1 | codex --model gpt-5.6-sol \ |
实践上可以这样分配:
- 日常交互开发:
gpt-5.6 + medium。 - 复杂架构、疑难调试、深度研究:Sol +
high,仍不够再上xhigh/max。 - 批量搜索、只读扫描、格式化辅助:Terra 或 Luna +
low/medium。 - 可并行且能明确验收的任务:再考虑 Ultra;不能拆分的单难题用 Max 更合适。
子 Agent 管理
三方对照
| 维度 | Codex | Claude Code | OpenCode v1.18.16 |
|---|---|---|---|
| 普通 child context | fork_turns 默认 all;可选 none 或最近 N turns |
named subagent 默认 fresh,接收 delegation task、项目规则和自身 prompt | fresh child session,接收 task prompt并重新加载项目 rules |
| 完整继承 | fork_turns = "all" |
/subtask fork |
核心 Task 无复制完整父 history 开关 |
| 部分继承 | fork_turns = "N" |
没有同等 last-N 控制 | 没有同等 last-N 控制 |
| 不继承聊天 | fork_turns = "none" |
普通 named subagent | 普通 Task child |
| 恢复 child | 继续同一 thread / follow-up | agent ID resume | task_id resume |
| model | spawn 显式值、[agents] 默认、自定义 role 或继承 |
env > per-invocation > frontmatter > inherit | agent model;未设置时继承父 model |
| effort | spawn 显式值、[agents] 默认、自定义 role 或继承 |
frontmatter effort;省略继承 session |
agent variant / provider options;无独立 model 时继承父 variant |
| 嵌套 | 由 agent 工具和并发配置控制 | 默认可到主会话下三层,可用 env 改 | 默认 subagent_depth = 1,即 child 不能再 spawn |
这里最容易犯的错误是把“共享文件系统”叫作“继承上下文”。Fresh child 仍能看到工作区当前文件;它只是没有父聊天里那些讨论、工具输出和临时判断。项目规则也可能由 child 重新加载,而非从父 context 复制。
Codex 的优先级与限制
Codex V2 的 spawn_agent payload 直接支持:
1 | fork_turns = "none" | "all" | "3" |
其中 all 是完整 history fork;3 是最近三个 turns;none 创建无父历史的 child。完整 history fork 继承父 model 和 effort,不接受冲突的覆盖;需要换模型时应使用 none 或 last-N。
自定义 agent 放在 ~/.codex/agents/ 或项目 .codex/agents/。model/effort 的优先级为:自定义 agent 文件内值最高;否则是显式 spawn、[agents] 默认、父会话。若显式换了 model 却没有 effort 覆盖,则用新模型自己的默认 effort。
1 | name = "fast_explorer" |
Claude Code 的 fresh 与 fork
Claude Code 普通 subagent 有自己的 context window、system prompt、tools 和 permissions。它接收父 Agent 生成的 delegation message,而不是父聊天全部历史;需要保存探索状态时,应按 agent ID resume。
要完整继承,使用 /subtask fork。Fork 会继承 conversation history、system prompt、tools 和 model,但工具调用不会回灌父对话,最终只返回结果。因为 fork 绑定父 model,想用 Haiku 做低成本搜索时应定义 named subagent:
1 | --- |
OpenCode 的 child session
OpenCode Task 创建带 parentID 的 child session,但不会复制父 messages。它把 params.prompt 变成 child 的 user message;传 task_id 可以继续同一 child 的历史。
Task tool 自身没有单次 model / effort 参数。稳定的控制方式是预定义不同 subagent,分别指定 model 与 variant:
1 | { |
如果目标 Agent 没有独立 model,child 继承父 model 与 variant;一旦目标 Agent 换了 model,就使用目标模型的 variant 配置。permission.task 控制可调用的 subagent 类型,subagent_depth 控制嵌套深度。
实现骨架
下面的伪代码把“普通 tool loop、Goal keeper、Scheduler”完整分开,并显式覆盖状态、停止、异常和资源清理:
1 | def run_agent_turn(session, user_input, limits): |
这段骨架的重点不是语法,而是三种状态不能混用:模型不再发 tool call,只能证明一个 turn 停止;Goal evaluator 才能判断长期目标;scheduler 只判断何时入队。
选择建议
- 默认从中档开始。日常编码用 medium,比“所有任务永远 max”更容易获得稳定的延迟—质量平衡。
- 先提高任务契约,再提高 effort。没有完成条件、输入边界和验证命令时,max 只会让模型更认真地猜。
- 搜索与实现分开。大量日志、文件枚举和资料检索放 fresh child;需要继承架构讨论的实现任务才 fork 或带最近 N turns。
- 并行前先证明可分解。Ultra/Ultracode 适合相互独立、能单独验收的分支;顺序依赖强的单难题更适合 Max。
- 显式固定生产配置。滚动推荐模型适合交互体验,不适合基准、回归或可审计流水线。
证据边界
- Codex 固定源码版本为
eb9dceba,公开文档为 2026-08-12 的滚动版本。 - Claude Code 核对版本为
2.1.228;核心闭源,/goalevaluator 与ultrathinkinstruction 的逐字文本未知。 - OpenCode 固定
v1.18.16 / a3647eb;“核心无 Goal/scheduler”不排除社区插件、宿主 IDE 或未来版本补齐。 - 本文没有使用论文中的算法或实验结果,因此不补充论文 figure;机制图直接来自官方文档与固定源码的数据流。