可以把模型想成提出操作建议的工程师,把 Harness 想成车间。工程师可以说“启动这台机器”,但车间仍要确认机器是否存在、操作是否安全、谁有权限、运行范围多大,以及结果记录在哪里。

读完这篇,你应该能回答:模型输出一个工具调用后,究竟是谁执行?Harness 最少有哪些部件?Prompt 里写“不要联网”和真正关闭网络有什么区别?

术语说明:厂商对 Harness 的覆盖范围并不完全相同。本系列采用便于学习的较窄定义:Harness 是模型之外、一次运行之内的执行环境;Agent Loop 是驱动模型与 Harness 反复交替的流程。本文把支付测试流程作为工程设计示例,并用 2026 年 9 月 20 日核实的 OpenAI Codex 固定源码快照说明实际实现。七步检查、artifact、幂等和恢复状态是设计建议,不代表所有工具都实现了同一协议;篇末社区教程只作延伸阅读。

一、先看懂建议怎样变成真实动作

1.1 Harness 到底是什么

Harness 是包在模型外面、把模型建议变成受控真实动作的软件环境。Harness engineering 就是设计这套环境里的工具、安全规则、执行空间、状态记录和恢复方式。

它之所以必要,是因为模型本身只产生文字或结构化的动作提议。模型可以建议读取文件、修改代码、运行命令,但真正接触文件系统、进程、网络和凭据的是外层程序。把这层程序设计好,Agent 才不只是“会说”,而是能够可靠地“做”。

1.2 跟着一次运行测试,看清 Harness 的组成

模型准备验证支付模块,提出“运行 payment tests”。设计这条执行路径时,应逐项回答下面七个问题;实际代码可能合并步骤,也可能按策略跳过审批:

  1. 找到工具:确认系统确实提供“运行测试”这项能力,而不是让模型随意发明命令接口。
  2. 检查输入:确认测试目标、工作目录和参数有效,缺失时返回明确错误。
  3. 判断权限:这次任务是否允许启动进程?若动作风险更高,是否必须先让人批准?
  4. 限制范围:即使允许执行,也只开放需要的仓库目录、进程和网络范围。
  5. 真正执行:管理超时、取消和资源限制,启动测试进程。
  6. 整理结果:完整日志存到外部文件,只把退出码、关键错误和日志引用送回模型。
  7. 留下记录:记录动作何时开始、是否完成和改变了什么,供中断后核对是否需要继续;仅有日志不保证能恢复外部副作用。

少了其中任何一步都会出现具体问题:没有输入检查,错误参数进入真实环境;没有权限和隔离,Prompt 里的“请小心”挡不住危险动作;没有结果和状态记录,进程崩溃后只能靠模型猜之前发生了什么。

把这次 Tool Call 放回完整运行,就能看到一段反复过程:Harness 准备请求 → 模型提出动作 → Harness 检查并执行 → 结果成为 observation → observation 进入下一次请求 → 模型再决定下一步。Harness 负责每次动作的检查与执行;Agent Loop 决定是否还要再走一轮。

二、拆开这套行动环境的关键部件

2.1 把七步收拢成一套运行环境

模型擅长在不完整信息下提出计划;运行时则要用确定的代码处理不确定建议。什么工具对当前角色可见、参数是否符合 schema、哪些目录可写、网络能连到哪里、结果如何序列化,都应预先写进代码和策略,而不是让模型临场决定。

最小 Harness 只要能准备请求、提供工具、检查输入与权限、受限执行并返回结果。成熟系统为了可重现和可恢复,会再补上版本化工具目录、事件记录、进度观察、artifact 和 checkpoint。下面的表把这两层放在一起,便于工程评审。

Harness 职责总览:模型与受控工具交换结果,状态记录供恢复时核对
模型是决策组件,不是整个 agent。Harness 让决策能够安全地接触真实世界。
组件它记录或决定什么为什么不能交给模型决定
Request assembler模型、instructions、tools、本次 context、预算请求必须可重现、可观测
Tool registry名称、schema、能力、版本、结果类型不能让模型发明真实接口
Policy / permissionallow、deny、ask、可访问范围权限判断必须可重复
Sandbox / executor文件、进程、网络、资源限制提示词不能隔离副作用
State / transcriptrun 状态、事件、结果引用、checkpoint恢复与审计需要持久记录
Observer / verifier进度、指标、测试和执行结果自我报告不能成为唯一真值

Harness 负责记录并提供工具结果、日志引用和测试输出;一次 run 是否继续由 agent loop 决定,跨任务和跨版本的质量是否改善则由 evals 判断。Verifier 可以在 harness 内执行测试,却不能单凭一次测试就代表全部产品质量。

2.2 一次 Tool Call 要经过七步检查与执行

Tool call 是模型输出的一段结构化提案。一个稳健 harness 不会把它直接映射成函数调用,而会把“解释、授权、执行、观察”拆开。

工具调用职责手册:解析、查找、校验、授权、按配置隔离、执行和结果归一化
设计目标:为重要检查和状态变化留下可关联的事件。图中七项是职责清单,不是 Codex 每次调用必经的七个实现阶段。
  1. Parse:确认 tool call 结构完整,关联到当前请求。
  2. Resolve:在当前可见 registry 中找到精确工具版本。
  3. Validate:按 schema 检查类型、范围、互斥和必填字段。
  4. Authorize:结合角色、资源、动作和环境决定 allow / deny / ask。
  5. Isolate:建立文件、进程、网络和凭据边界。
  6. Execute:管理超时、取消、并发、重试与幂等键。
  7. Normalize:把 stdout、结构结果、错误与 artifact 变成稳定 observation。
async function dispatch(call, run) {
  const tool = registry.resolve(call.name, run.toolsetVersion)
  if (!tool) return observe("unknown_tool", call.id)

  const args = tool.schema.parse(call.arguments)
  const decision = policy.decide(run.actor, tool.capability, args)
  if (decision === "deny") return observe("denied", call.id)
  if (decision === "ask") return pauseForApproval(run, call)

  const box = await sandbox.open(tool.limits)
  events.append("tool.started", { runId: run.id, callId: call.id })
  try {
    const raw = await executor.run(tool, args, { sandbox: box })
    return normalize(raw, artifactStore)
  } finally {
    await box.close()
  }
}
教学伪代码:省略工具专用校验、异步完成与取消分支;它只处理一个 tool proposal,不负责 while 循环。是否再次调用模型,是下一篇 agent loop 的责任。
{
  "call_id": "call_42",
  "tool": "run_tests@3",
  "arguments": { "target": "./payment", "mode": "read-write" },
  "policy": { "decision": "allow", "rule": "repo-test" },
  "sandbox": { "fs": "workspace", "network": "off", "timeout_s": 120 },
  "result": { "status": "failed", "exit_code": 1, "artifact_ref": "log://9f2" }
}
形状级示例:把决策、执行环境和结果证据绑定在同一 call id 上,才能追踪一条副作用。

在这套建议的设计中,同一个 call_id 应关联三种不同用途的记录:artifact store 保存完整日志;事件日志保存 proposed、allowed、started、completed 等状态;下一次模型请求只接收足以决定下一步的 observation。恢复时,Runtime 先读取事件日志并查询真实环境,必要时再按引用打开 artifact;它不会根据一段聊天文字猜测副作用是否已经发生。

在 Codex 中,还要区分工具返回与底层进程结束。命令结果头会分别显示 Process running with session ID … 或 Process exited with code …。支付测试返回会话标识,只说明进程仍在运行;控制器还要继续读取该会话的输出,拿到退出码和测试结果,才能判断是否通过。一次调用已获准、已经启动和测试已经完成,是三个不同事实。

多个调用也不总是串行经过图中路径。ToolCallRuntime读取工具的并行能力:支持并行的调用取得共享读锁,其他调用取得独占写锁,再进入 handler。这个门控制的是该运行时内的工具派发;它不证明并行工具没有业务依赖,也不覆盖所有已经留在后台运行的进程。读两个独立文件可以并发,修改支付代码后再测试则仍有先后依赖。

2.3 工具设计决定 Agent 会做哪些动作

Prompt 篇关心模型看到的工具说明,以及“应该怎么用”;本篇关心 registry、版本、schema validator、权限、dispatcher 和 artifact,以及“实际上能不能执行”。工具不是越多越好:两个语义重叠的写工具、一个万能 shell 和模糊的自由文本参数,会让动作空间膨胀。模型既要猜工具,又要猜参数,还要从非结构输出里判断成功。

2.3.1 一个好工具要同时服务模型与运行时

维度给模型的界面给运行时的界面
语义何时调用、何时不调用能力 id 与版本
输入少量清晰字段严格 schema 与规范化
输出足够决定下一步typed result、artifact 与错误分类
副作用明确会改变什么权限、幂等、补偿和审计
成本何时值得调用超时、速率、token 与资源预算

大量输出不要整段塞回 context。Harness 可以把完整日志存成 artifact,只回填摘要、关键错误和可继续读取的引用。这同时改善 context budget 与恢复能力。

2.4 允许执行,不等于可以触达一切

Permission 回答“这个主体是否被允许做这个动作”;sandbox 回答“即便允许,动作实际能触达什么”。批准运行测试不等于允许访问整个主目录;允许读取仓库不等于允许把内容发到任意网络端点。二者必须组合。

审批决定是否允许动作,沙箱限制执行范围,审计记录结果;审批按策略触发
审批、隔离与记录各管一件事:审批按策略触发,沙箱范围由执行配置决定,审计保存证据而不授予或收回权限。
  • Capability scope:先不把无关工具暴露给模型。
  • Policy decision:按动作、资源、主体和环境匹配规则。
  • Approval:按明确策略触发,展示动作、目标和必要的 diff;审批也可能由已配置的自动审查处理。
  • Sandbox:用文件系统、网络、进程和凭据隔离限制最坏影响。
  • Audit:记录谁提出、哪条规则允许、实际发生了什么。

Codex 的 ToolOrchestrator::run把审批需求分为跳过、禁止和需要审批;严格自动审查模式还能对通常跳过的动作追加审查。随后才选择执行沙箱。审批与沙箱选择显示:批准不会自动移除隔离;反过来,配置也可能允许不使用进程沙箱。沙箱拒绝后的升级尝试有工具能力、审批策略、网络决策和允许范围等条件,不是“失败就无沙箱再跑一次”。因此应把支付测试需要的目录和网络访问写清,再读取实际策略决定走哪条路径。

三、进阶:让真实动作可恢复、可追查

3.1 进程中断后,不能只靠聊天记录恢复

为恢复运行,工程上至少要区分:UI 对话、模型可见 messages、append-only 事件、外部 artifact、checkpoint 与环境快照。它们生命周期不同,也服务不同恢复路径。

中断恢复时,最危险的做法是仅把旧聊天重新发给模型。运行时还需要知道哪些工具调用真正执行过、哪些在途、工作区是否改变、审批是否仍有效、外部对象是否已经创建。副作用状态应结合执行记录和外部系统查询核对,不能让模型根据文本猜,也不能把会话恢复等同于命令或付款的事务恢复。

tool.proposed → tool.validated → policy.allowed → tool.started
→ runtime.crashed → run.recovered → effect.reconciled
→ observation.ready
建议的恢复轨迹:若 call_42 只有 started 没有 completed,runtime 先查询环境。只读动作可受限重放;带幂等键的写动作先对账;非幂等且状态未知的动作必须进入 blocked 或人工升级,不能直接再执行一次。

持久化本身也有阶段。Codex 的 RolloutRecorder中,record_canonical_items把记录送入写入队列;persist和flush则等待写入任务确认,并把失败返回调用方。这说明“记录已被接收”与“写入已经确认”不能混用。它提供会话记录的写入机制,没有把任意工具副作用与记录提交合成一笔事务;恢复支付测试时,仍应重新核对文件和进程,涉及真实退款时更需要业务系统的幂等与对账。

3.2 用户和系统都要看见动作进行到哪里

只有最终文本和总 token 数,无法诊断 harness。需要把一次 run 串成 trace:request assembly、model events、tool proposal、policy decision、sandbox execution、observation、checkpoint 和 exit reason。敏感正文可以脱敏,但控制事件不能消失。

  • 稳定的 run / turn / call id 关联所有事件;
  • 记录 tool latency、排队、重试、取消和 artifact 大小;
  • 区分模型错误、工具错误、策略拒绝、环境错误和用户中止;
  • 保留退出原因与未完成工作,供 outer loop 判断下一步。

四、复盘:一个 Harness 至少回答六个问题

评审问题如果答不出来风险
当前模型究竟能看到哪些能力?没有版本化 registry能力漂移和错误选择
Tool call 在哪里被确定性校验?依赖模型自检非法参数进入真实执行
授权与隔离各由谁负责?只有 prompt 警告命令能访问超出任务需要的资源
大结果和秘密怎样处理?原样回填 context泄漏与窗口污染
进程中断后以什么事实恢复?只重放聊天重复副作用与错误状态
每个退出是否有可机读原因?只有 final 文本Outer loop 无法安全接管

Harness 把“模型能想到”变成“系统能安全做到”。下一篇进入它内部不断前进的控制流:Agent loop 怎样让一次运行正确收敛。

官方资料

延伸阅读