跳转至

AI Coding Harness

导言

AI Coding IDE 的核心并不只是一段 system prompt。模型负责判断,Harness 负责把判断变成连续、受控、可恢复的工程过程。本文固定 Codex 与 OpenCode 源码版本,并以 Claude Code 官方行为契约为界,回答三个问题:Goal 是否只是定时任务,推理强度是否只是换 prompt,以及三种工具如何控制子 Agent 的上下文、模型与推理强度。

AI Coding Harness 的三层循环

工具反馈、目标守护和定时唤醒是三个不同触发边界;把它们都叫“Loop”容易误判实现。

先给结论

  1. 普通 Agent loop 不是定时任务。它是“模型生成 → 工具执行 → 结果回送 → 再次生成”的即时反馈状态机。
  2. Codex Goal 也不是定时任务。固定源码显示,它在 thread 进入 idle 时触发 continue_if_idle,未完成就立即尝试开启下一个 turn。Codex Goal 文档把目标文本同时作为首轮 prompt 和完成标准。
  3. Claude Code /goal 不是定时任务。它在每个 turn 结束时调用一个无工具的小模型 evaluator 判断 yes/no;/loop 才是每秒检查到期任务、在 turn 之间入队的 session scheduler
  4. OpenCode 核心没有 Goal keeper 或持久 scheduler。固定 v1.18.16SessionPrompt.runLoop 是同步 while (true);Goal 与定时运行来自社区插件、GitHub Actions 或 OS scheduler。
  5. “轻度到最高只是 prompt 不同”这一说法不成立。三者的公开实现都是结构化 effort、thinking budget 或 provider variant;服务端内部如何消费该信号没有公开,但客户端并不存在一组可合法列出的逐档隐藏 prompt。
  6. Codex 当前界面的 Ultra 不是 Max 的简单上一级。Max 给单个模型更多推理时间;Ultra 把单次请求按最大努力运行,并增加自动子 Agent 编排。官方模型文档也把两者分开定义。
  7. 三者对子 Agent 的主要差异在上下文种子。Codex 可以选无历史、完整历史或最近 N turns;Claude Code 普通 subagent 是 fresh,fork 才完整继承;OpenCode Task 是 fresh child session,没有复制完整父历史的核心开关。

关于“列出各档 Prompt”

不能把猜测写成泄露出来的隐藏 prompt。本文会列出公开的 continuation/evaluator prompt、精确请求字段和配置;对 low、medium、high、xhigh、max 的“逐字 prompt”统一标为“不存在公开独立文本”。Claude Code 核心是专有 native binary,无法从官方仓库给出内建 /goal evaluator 的函数行号或隐藏指令。

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

Tool、Goal 与 Scheduled 三层循环

同一条 prompt 可以由用户、Goal keeper 或 scheduler 发起,但进入 Harness 后仍运行相同的模型—工具反馈循环。

三种循环

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 查看。

真正的定时任务

Codex 的 Scheduled Tasks 才有 recurrence、RFC 5545 RRULE、独立 chat 或原 chat 续跑等调度语义。Goal 解决“目标还没完成时继续”,Scheduler 解决“到某个时间再运行”。

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/opencode@v1.18.16,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

公开实现展示的是以下数据流:

UI / config / agent role
          ├── base system prompt ────────────┐
          └── effort / variant / budget ──┐  │
                                          v  v
                              structured model request
                                          v
                              provider / model runtime

Codex 固定源码在构造 ResponsesApiRequest 时,instructionsinput 来自同一 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、Reasoning Effort 与 Orchestration

角色 Prompt 决定“按什么规则工作”,effort 决定模型投入多少推理资源,Ultra/Ultracode 再决定 Harness 是否自动编排子 Agent。

各档有什么差异

用户说法 常见配置值 独立公开 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 在客户端可观察到的精确差异是请求字段,而非对话文本:

Codex / OpenAI API : reasoning.effort = "high"
Claude API         : output_config.effort = "high"
OpenCode + OpenAI  : reasoningEffort = "high"
OpenCode + Claude  : thinking.type = "adaptive", effort = "high"

有两个需要单独说明的 prompt 特例:

  1. Codex Goal continuation 有公开模板,但它控制跨 turn 继续工作的行为,不是 reasoning effort。
  2. 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、用户配置、系统配置、内建默认。

# ~/.codex/config.toml 或项目 .codex/config.toml
model = "gpt-5.6"
model_reasoning_effort = "medium"
plan_mode_reasoning_effort = "high"

[agents]
default_subagent_model = "gpt-5.6-terra"
default_subagent_reasoning_effort = "medium"
max_concurrent_threads_per_session = 4

一次性覆盖可以用:

codex --model gpt-5.6-sol \
  -c 'model_reasoning_effort="high"'

实践上可以这样分配:

  • 日常交互开发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 直接支持:

fork_turns = "none" | "all" | "3"
model = "gpt-5.6-terra"
reasoning_effort = "medium"
agent_type = "explorer"

其中 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。

name = "fast_explorer"
description = "只读代码检索和证据定位"
developer_instructions = "返回文件、符号、固定行号和最小结论。"
model = "gpt-5.6-terra"
model_reasoning_effort = "low"
sandbox_mode = "read-only"

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:

---
name: cheap-researcher
description: 处理边界明确的代码和资料搜索
model: haiku
effort: low
tools: Read, Grep, Glob, WebSearch, WebFetch
---

只返回与任务直接相关的证据、来源链接和边界。

OpenCode 的 child session

OpenCode Task 创建带 parentID 的 child session,但不会复制父 messages。它把 params.prompt 变成 child 的 user message;传 task_id 可以继续同一 child 的历史。

Task tool 自身没有单次 model / effort 参数。稳定的控制方式是预定义不同 subagent,分别指定 model 与 variant:

{
  "$schema": "https://opencode.ai/config.json",
  "agent": {
    "cheap-explore": {
      "mode": "subagent",
      "description": "低成本代码检索",
      "model": "openai/gpt-5.3-codex",
      "variant": "low",
      "steps": 8,
      "permission": {
        "edit": "deny",
        "bash": "allow",
        "read": "allow",
        "grep": "allow",
        "glob": "allow"
      }
    }
  }
}

如果目标 Agent 没有独立 model,child 继承父 model 与 variant;一旦目标 Agent 换了 model,就使用目标模型的 variant 配置。permission.task 控制可调用的 subagent 类型,subagent_depth 控制嵌套深度。

三种子 Agent 上下文策略

Codex 暴露最细的 history fork 粒度;Claude Code 把 fresh named agent 与完整 fork 分成两条路径;OpenCode 以 fresh child session 和 task_id 恢复为主。

实现骨架

下面的伪代码把“普通 tool loop、Goal keeper、Scheduler”完整分开,并显式覆盖状态、停止、异常和资源清理:

def run_agent_turn(session, user_input, limits):
    session.append_user(user_input)
    steps = 0
    try:
        while steps < limits.max_steps:
            request = build_request(
                history=session.visible_history(),
                instructions=session.instructions,
                tools=session.allowed_tools,
                reasoning=session.reasoning_config,
            )
            response = stream_model(request)
            session.record(response)

            calls = response.tool_calls
            if not calls:
                return TurnResult(status="finished", text=response.text)

            for call in calls:
                if session.cancelled:
                    return TurnResult(status="cancelled")
                try:
                    value = execute_tool(call, timeout=limits.tool_timeout)
                    session.append_tool_result(call.id, value)
                except Exception as error:
                    session.append_tool_error(call.id, normalize_error(error))
            steps += 1

        session.append_system_notice("step budget reached; summarize and stop")
        return TurnResult(status="budget_exhausted")
    finally:
        session.close_completed_tool_handles()
        session.flush_event_log()


def continue_goal_when_idle(thread, goal):
    if not thread.is_idle() or goal.status != "active":
        return
    decision = evaluate_goal(thread.current_evidence(), goal.criteria)
    if decision.complete:
        goal.mark_complete(decision.evidence)
    elif decision.strictly_blocked:
        goal.record_blocker(decision.reason)
    else:
        thread.start_turn(goal.continuation_prompt(decision.reason))


def scheduler_tick(clock, scheduled_items):
    for item in scheduled_items.due_at(clock.now()):
        if item.session.is_idle():
            item.session.enqueue_low_priority(item.prompt)
            item.advance_or_delete_recurrence()

这段骨架的重点不是语法,而是三种状态不能混用:模型不再发 tool call,只能证明一个 turn 停止;Goal evaluator 才能判断长期目标;scheduler 只判断何时入队。

选择建议

  1. 默认从中档开始。日常编码用 medium,比“所有任务永远 max”更容易获得稳定的延迟—质量平衡。
  2. 先提高任务契约,再提高 effort。没有完成条件、输入边界和验证命令时,max 只会让模型更认真地猜。
  3. 搜索与实现分开。大量日志、文件枚举和资料检索放 fresh child;需要继承架构讨论的实现任务才 fork 或带最近 N turns。
  4. 并行前先证明可分解。Ultra/Ultracode 适合相互独立、能单独验收的分支;顺序依赖强的单难题更适合 Max。
  5. 显式固定生产配置。滚动推荐模型适合交互体验,不适合基准、回归或可审计流水线。

一句话判断

问“是不是定时任务”时,看它等待的是上一个事件还是墙上时钟;问“是不是 prompt”时,看它进入的是消息列表还是结构化请求字段;问“是否继承上下文”时,分别核对聊天历史、项目规则、文件系统和 child session 状态

证据边界

  • Codex 固定源码版本为 eb9dceba,公开文档为 2026-08-12 的滚动版本。
  • Claude Code 核对版本为 2.1.228;核心闭源,/goal evaluator 与 ultrathink instruction 的逐字文本未知。
  • OpenCode 固定 v1.18.16 / a3647eb;“核心无 Goal/scheduler”不排除社区插件、宿主 IDE 或未来版本补齐。
  • 本文没有使用论文中的算法或实验结果,因此不补充论文 figure;机制图直接来自官方文档与固定源码的数据流。

参考资料

评论