一个模型会调用工具,不等于一个 Agent 系统已经可以可靠运行。
最简单的工具调用 Demo 通常只有几行:
把用户问题发给模型
如果模型要求调用工具,就执行工具
把工具结果发回模型
重复,直到模型返回最终文本
这确实是 Agent loop 的骨架。问题是,真实系统很快会遇到循环本身回答不了的事情:
- 写操作是否需要批准,谁来阻止它提前执行?
- 工具卡住以后,何时超时,是否重试,重试会不会重复扣款?
- 进程重启后,应该恢复对话、待审批动作,还是整个执行环境?
- 模型说“完成了”,谁检查数据库、文件或页面真的发生了变化?
- 循环一直调用工具时,谁负责按步数、时间或成本停止?
- 换模型、换工具实现或换 Sandbox 时,业务流程是否要跟着重写?
这些问题共同指向模型外的一层运行时:Harness。
本文是 AI Agent 工程进阶 第 4 篇。上一篇 Context Architecture 解决“下一次模型调用应该看到什么”;这一篇继续向外走一层,解决“谁组织调用、工具、状态、权限、停止和验收”。
| 项目 | 说明 |
|---|---|
| 内容类型 | 概念辨析、运行时设计与可复现实验 |
| 适合读者 | 已经写过工具调用 Agent,开始遇到审批、恢复、超时或状态混乱的开发者 |
| 阅读时间 | 约 18-25 分钟 |
| 跟做时间 | 45-60 分钟 |
| 环境要求 | Python 3.10+,零第三方依赖,不需要 API Key |
| 代码检查点 | 10b57af |
| 可带走产物 | 最小 Harness、6 个边界案例、RunState Schema、事件序列、失败注入和接入清单 |
| 资料核对日期 | 2026-07-31 |
| 实验边界 | 使用脚本模型与模拟延迟,评估运行时契约,不评估模型质量或真实进程取消 |
先说明本文怎样使用“Harness”
Harness 不是一个已经由所有厂商统一定义的协议。本文采用一个可操作的工程定义:位于模型之外,负责组装输入、驱动 Agent loop、分派工具、执行策略、保存运行状态、控制停止与恢复,并把结果交给验证器或人工的受信任运行时。
OpenAI 的 Codex 文章也把 harness 用来指承载核心 Agent loop 与执行逻辑的 Agent 软件;另一篇 “Harness engineering” 则把概念扩展到让仓库对 Agent 可读、可约束、可验证的整套工程环境。两种用法并不冲突,但范围不同。本文先把运行时边界讲清,后文再讨论仓库与运营层面的 Harness。
一分钟概览
如果只保存这篇文章的结论,可以记住十点:
- Agent loop 是 Harness 的核心,但不等于完整 Harness。 Loop 负责 model -> tool -> model;Harness 还要管理权限、状态、预算、错误与验收。
- 模型提出动作,Harness 决定动作能否执行。 生产权限不能只靠 Prompt 中一句“危险操作先询问”。
- 副作用之前要先做策略检查。 需要人工审批时,系统应先保存待执行动作,再进入
WAITING_APPROVAL。 - 恢复不是重新发送原 Prompt。 恢复要带回同一个
RunState、待处理action_id、审批决定与已完成回执。 - Context、对话历史、RunState、Sandbox state 和文件 snapshot 是不同状态面。 它们不应由一个模糊的
session字段包办。 - 每次运行都要有明确状态。
COMPLETED、WAITING_APPROVAL、FAILED、STOPPED与FAILED_VERIFICATION不能混用。 - 超时、步数和成本是 Harness 的硬边界。 模型不能靠“自觉”保证结束。
- 模型最终文本只是候选结果。 Verifier 应检查真实结果或证据,再决定是否完成。
- Harness 本身也会变成技术债。 每一层脚手架都在假设模型做不到什么,应通过 Eval 逐项验证和删减。
- 先用成熟 SDK,再决定是否自建。 自建最小 Lab 的目的,是理解边界和做适配层,不是重新实现所有生产能力。
图 1:Harness 位于模型和真实执行面之间,管理 Context、策略、RunState、超时、追踪与验证
1. 先从最小 Agent loop 开始
OpenAI 在 Unrolling the Codex agent loop 中给出了一个很清楚的基础循环:
用户输入
-> 模型推理
-> 最终回答:结束
-> 工具调用:执行工具,把结果追加到输入,再次推理
用伪代码表示,大致是:
messages = [{"role": "user", "content": task}]
while True:
decision = model(messages, tools)
if decision.kind == "final":
return decision.output
result = execute_tool(
decision.tool_name,
decision.arguments,
)
messages.append(result)
这段代码已经具备“循环”,但还没有回答:
execute_tool 之前有没有策略检查?
工具执行到一半崩溃怎么办?
final 是否等于真实完成?
while True 最多运行多久?
messages 之外还要保存什么?
所以可以先建立第一层关系:
Agent loop = 反复获取模型决定并处理工具结果
Harness = Agent loop
+ 输入组装
+ 工具分派
+ 策略与审批
+ 运行状态
+ 超时与停止
+ 追踪与验证
OpenAI Agents SDK 的 Runner 文档采用相似结构:Runner 调用当前 Agent 的模型,遇到 final 就结束,遇到 handoff 就切换 Agent,遇到 tool call 就执行并继续;超过 max_turns 会抛出明确异常。这里最值得借鉴的不是某个类名,而是:循环、工具执行、终止条件和状态恢复都有明确所有者。
2. Harness 不是更大的 Prompt
有些系统表面上有 Harness,实际只是把更多规则拼进系统提示词:
不要执行危险操作
遇到错误请重试
不要重复写入
最多思考十步
完成后请验证结果
这些文字可以帮助模型做更好的判断,却不能替代运行时约束。
例如:
if tool.side_effect and not approval_store.is_approved(call_id):
pause_run()
与:
“请记得在写数据之前询问用户。”
不是同一强度的控制。前者是每次工具执行都必须经过的代码路径;后者依赖模型是否正确理解、是否记得,以及工具是否绕过了这段指令。
一个实用判断是:
| 规则 | 可以写进 Prompt | 还必须写进 Harness |
|---|---|---|
| 回答语气和格式 | 是 | 通常不必 |
| 优先使用哪些资料 | 是 | 需要时做来源与权限过滤 |
| 写操作先确认 | 可以提醒 | 必须有代码策略闸门 |
| 最多运行多少步 | 可以提醒 | 必须有计数器与停止状态 |
| 工具超时 | 无法可靠执行 | 必须由运行时控制 |
| 不重复扣款 | 可以提醒 | 必须依赖幂等键与外部回执 |
| 结果是否真的落库 | 可以自检 | 必须读取真实状态或独立验证 |
Prompt 解决“怎样引导模型”,Harness 解决“系统允许什么、记录什么、何时停止”。
3. 五个组件分别负责什么
最常见的设计问题,不是少写了一个类,而是把所有责任都交给 Agent 或 session。本文先把五个组件拆开。
图 2:Model、Harness、RunState Store、Sandbox 与 Verifier 的职责边界
Model:提出候选决策
模型适合负责:
- 理解当前任务和证据;
- 在可用工具中选择下一步;
- 生成结构化工具参数;
- 根据工具结果继续推理;
- 生成候选最终回答。
模型不应单独拥有:
- 生产系统的最终权限;
- 可靠的永久状态;
- “是否真实完成”的最终裁决;
- 不可绕过的时间、成本和并发限制。
Harness:拥有控制流
Harness 负责:
- 组装本次模型输入;
- 调用模型适配器;
- 解释模型的结构化决定;
- 在工具前执行策略与审批;
- 保存和恢复运行状态;
- 控制步数、时间和错误;
- 记录事件并交给 Verifier。
换句话说,模型可以建议“调用 record_followup”,但 Harness 必须决定何时、以什么身份、在哪个环境、带什么幂等键执行。
RunState Store:保存“这项工作做到哪里”
本文配套 Lab 中的 RunState 保存:
run_id
case_id
status
model_cursor
steps
pending_action
approvals
completed_action_ids
messages
final_output
failure_code
events
这不是长期记忆,也不等于文件系统。它回答的是:
这一次运行正处于什么状态?
下一步待处理的动作是什么?
哪些动作已经完成?
恢复时必须带回哪些决定?
Sandbox:拥有受限执行环境
Sandbox 负责限制代码、文件、进程、网络或系统能力。它可以是容器、虚拟机、远程 workspace,也可以是操作系统提供的受限进程。
OpenAI 的 Claude Agent SDK 迁移指南提供了一个值得注意的架构选择:在它描述的 OpenAI Agents SDK 模式中,受信任 Harness 与计算 Sandbox 分开,Sandbox 是 Harness 可以调用的执行面;密钥、审批决定和业务系统访问留在受信任应用一侧。
这不是唯一部署方式,但原则很重要:
不要因为 Agent 要在 Sandbox 里运行命令,就把审批权和生产密钥也一起塞进去。
Verifier:判断真实结果
Verifier 不应只问模型“你完成了吗”,而应检查:
- 文件 diff 是否存在;
- 测试是否通过;
- 数据库记录是否真实写入;
- 页面是否能访问;
- 输出是否包含要求的来源;
- 副作用次数是否符合预期。
在本文 Lab 里,验证器很简单:最终文本必须包含 source=。这个规则不足以验证生产答案质量,但足以演示一个关键状态:模型返回 final 之后,Harness 仍可能进入 FAILED_VERIFICATION。
4. Session 到底指什么
“把它存进 Session”通常是一个危险的模糊句子,因为不同框架里的 Session 可能指完全不同的东西。
至少要区分下面四类:
| 状态面 | 保存什么 | 解决什么问题 |
|---|---|---|
| 对话历史 | 用户消息、模型输出、工具消息 | 下一轮对话怎样延续 |
RunState | 当前 Agent、待审批动作、步骤、已完成动作 | 暂停后怎样继续同一次运行 |
| Sandbox session state | 正在运行的执行环境及其连接状态 | 怎样重连或恢复计算环境 |
| Workspace snapshot | 文件与产物快照 | 怎样用已有工作区启动新环境 |
OpenAI 的 Sandbox Agents 文档明确把 RunState、Sandbox session state 和 snapshot 分开:
RunState恢复 Harness 一侧的模型项、工具状态、审批和当前 Agent 位置;- Sandbox session state 用于重连执行环境;
- snapshot 用保存的文件和产物创建新的 Sandbox session。
OpenAI Agents SDK 的 State and conversation management还区分了客户端保存的 history/session 与服务端 conversation_id、previous_response_id。文档特别提醒,不加设计地混合两种历史管理方式可能重复 Context。
因此我更愿意在代码里使用精确名字:
ConversationStore
RunStateStore
SandboxSessionProvider
WorkspaceSnapshotStore
MemoryStore
而不是让一个 session 对象同时保存所有东西。
本文 Lab 的取舍
RunStateStore是内存实现,目的是展示接口和事件顺序;它不是生产持久化。真正上线时至少要考虑数据库事务、Schema 版本、保留时间、并发恢复和密钥脱敏。
5. 给模型一个窄协议
Harness 要可替换,模型接口就不应返回一团厂商特定对象。
本文 Lab 使用的最小协议只有两种决定:
@dataclass(frozen=True)
class ModelDecision:
kind: str # "tool" or "final"
action_id: str | None
tool_name: str | None
arguments: dict
output: str | None
真实接入时,可以为不同提供方编写 Adapter:
OpenAI Responses adapter
Claude adapter
local model adapter
scripted test adapter
|
v
统一 ModelDecision
这层 Adapter 需要负责:
- 把厂商输出转换成统一
tool或final; - 保留 provider response id 等追踪信息;
- 把无法解析的工具参数变成显式协议错误;
- 不在 Adapter 内偷偷执行工具;
- 不把业务审批规则绑定到某个模型 API。
Lab 没有调用真实模型,而是用 ScriptedModelAdapter 顺序返回固定决定。这样做不是为了模拟智能,而是为了锁定变量:
当模型决定完全相同时,增加 Harness 后,审批、超时、停止、恢复与验证边界是否真的发生变化?
6. 运行状态必须是状态机
很多 Demo 只有布尔值:
done = True
但真实运行至少需要区分:
READY
RUNNING
WAITING_APPROVAL
COMPLETED
FAILED
FAILED_VERIFICATION
STOPPED
图 3:Harness 从运行到审批暂停、恢复、完成、失败或停止的状态机
几个容易混淆的状态:
WAITING_APPROVAL 不是失败
系统已经完成当前能做的工作,并留下可恢复的 pending action。它可以等几分钟,也可以等几天。
STOPPED 不是完成
达到 max_steps、预算耗尽或用户取消,只说明运行被边界终止。最终任务可能仍未完成。
FAILED_VERIFICATION 不应降级为普通回答
模型可以生成一段看似合理的最终文本,但独立检查未通过。把它标为 COMPLETED 会污染成功率,也会让后续恢复无从下手。
FAILED 需要错误分类
至少保留:
tool_timeout
tool_not_found
invalid_tool_request
approval_rejected
model_protocol
错误分类是以后设计重试、降级、告警和 Eval 的前提。只有一句 something went wrong,无法安全自动化。
7. 审批的关键不是弹窗,而是事件顺序
一个常见错误流程是:
执行写操作
-> 发现危险
-> 再询问用户是否允许
另一个隐蔽错误是:
模型提出写操作
-> 程序结束并丢失完整状态
-> 用户批准
-> 重新发送原任务,让模型再生成一次工具调用
第二种做法可能生成新的参数和新的调用 ID,也无法确定上一次是否已经执行到一半。
更稳妥的顺序是:
action_proposed
-> policy_checked
-> checkpoint_saved
-> approval_requested
-> WAITING_APPROVAL
-> run_resumed
-> tool_started
-> tool_finished
-> checkpoint_saved
图 4:写操作从模型提议、策略检查、检查点、人工批准到幂等执行的时序
OpenAI Agents SDK 的 Human-in-the-loop 文档采用类似思想:需要审批的工具会让运行产生 interruption;调用方把结果转成可序列化 RunState,记录批准或拒绝,再从原始顶层运行恢复。这里的工程重点有三个:
- 审批决定作用于具体 tool call,而不是一句模糊的“都同意”;
- 暂停状态可以序列化,恢复不依赖原进程对象仍然活着;
- 恢复的是原运行,不是新开一个相似任务。
为什么还需要 action_id
即使 Harness 正确保存状态,外部工具也可能出现这种时序:
业务写入成功
-> 回执还没保存
-> Harness 进程崩溃
恢复后,Harness 不知道写入是否发生。真正的 exactly-once 通常不能只靠 Agent 进程内变量保证,需要把稳定 action_id 当作幂等键交给业务系统:
record_followup(
ticket_id="T-102",
note="Send refund form",
idempotency_key="write-followup-102",
)
业务系统应返回同一动作的已有回执,而不是再次产生副作用。
Lab 没有证明生产 exactly-once
配套实验使用内存回执账本,能证明同一进程内的接口和调用顺序,但没有覆盖数据库提交后进程崩溃、跨进程并发恢复或第三方 API 不支持幂等键等情况。本文把它称为“幂等接口演示”,而不是 exactly-once 保证。
8. 超时、重试和停止不能混成一个开关
超时回答“这一次调用等多久”
Harness 应记录:
tool
action_id
started_at
timeout
error_class
超时后进入什么状态,要看工具语义。只读查询通常可以重试;扣款、发信、删除等副作用必须先查询回执或使用幂等键。
重试回答“失败后是否再做一次”
本文 Lab 故意不实现自动重试。原因是重试策略依赖下一篇 Tool Engineering 要定义的工具契约:
是否只读?
是否幂等?
错误是否可重试?
退避多久?
调用是否已经产生副作用?
在这些字段缺失时写一个通用 retry=3,只是把偶发错误变成重复副作用。
停止回答“整个运行最多走多远”
即使每个工具都很快,模型也可能反复查询。Harness 至少要有:
max_steps
deadline
token or cost budget
user cancellation
本文实验实现 max_steps。每次模型决定都消耗一步;达到上限后进入:
{
"status": "stopped",
"failure_code": "max_steps"
}
这比让脚本在安全兜底处抛出未知错误更适合评测和恢复。
9. Trace 是运行证据,不是调试打印
日志常见写法是:
calling tool...
done
这对恢复和审计几乎没有帮助。本文 Lab 记录一组有顺序的事件:
run_started
context_assembled
model_called
model_decision
action_proposed
policy_checked
checkpoint_saved
approval_requested
run_resumed
tool_started
tool_finished / tool_failed
final_received
verification_finished
run_finished / run_stopped / run_paused
每条事件包含单调递增的 seq,因此 Eval 可以检查:
checkpoint_saved < approval_requested
policy_checked < tool_started
run_resumed < tool_started
final_received < verification_finished
这比检查“日志里出现过 approval”更严格。事件都存在,不代表顺序正确。
生产 Trace 还需要增加:
trace_id、run_id、tool_call_id;- 模型和 Prompt/Context 版本;
- 延迟、Token、成本;
- 输入输出摘要或哈希;
- 错误分类和重试序号;
- 数据脱敏与保留策略。
这些会在后续 Agent Tracing 专题中继续展开。
10. 跑起来:复现 Harness Lab 0.4.0
本篇代码固定在 GitHub commit 10b57afb。固定 commit 很重要:后续文章会继续修改同一个工程,本文命令仍应得到同一组结果。
git clone https://github.com/RalfNick/ai-agent-learn.git
cd ai-agent-learn
git checkout 10b57afb771504951769f8341cdf1c97bc5a167f
cd phase-7-agent-engineering/agent-reliability-lab
环境要求:
Python 3.10+
零第三方依赖
不需要 API Key
先运行完整测试:
python -m unittest discover -s tests -v
在这个检查点应看到:
Ran 29 tests
OK
再运行 Harness 对照:
python run_lab.py harness-eval \
--output reports/local/harness-review
Windows PowerShell 可以写成一行:
python run_lab.py harness-eval --output reports/local/harness-review
核心输出如下:
{
"baseline": {
"strategy": "inline-loop-v1",
"passed_cases": 1,
"case_pass_rate": 0.1667,
"sensitive_action_control_rate": 0.0,
"checkpoint_before_pause_rate": 0.0
},
"candidate": {
"strategy": "minimal-harness-v1",
"passed_cases": 6,
"case_pass_rate": 1.0,
"sensitive_action_control_rate": 1.0,
"checkpoint_before_pause_rate": 1.0
},
"gate_passed": true
}
命令会生成:
reports/local/harness-review/
harness-comparison.json
harness-comparison.md
harness-failures.md
harness-runs.jsonl
建议按这个顺序阅读:
harness-comparison.md:先看两个策略在哪些边界不同;harness-failures.md:看 inline control 为什么失败;harness-runs.jsonl:选一个案例读完整RunState和事件序列;datasets/harness-cases.jsonl:回头看成功标准怎样声明。
仓库也保留了本文核验时的固定报告:
11. 六个案例分别证明什么
图 5:inline loop 与 minimal Harness 在六个边界案例中的对照结果
案例 1:只读查询能够正常完成
lookup_policy
-> tool result
-> final with source=
-> COMPLETED
两个策略都通过。这个控制案例很重要:增加 Harness 不能破坏原本能完成的基础任务。
案例 2:写操作先暂停
模型提出:
{
"action_id": "write-followup-101",
"tool": "record_followup"
}
预期结果:
status = WAITING_APPROVAL
side_effect_count = 0
checkpoint_saved < approval_requested
inline control 直接写入,所以失败;Harness 保存 pending action 后暂停。
案例 3:批准后恢复并只写一次
运行先暂停,再把 RunState 序列化为 JSON、重新加载、批准并恢复。
预期结果:
approval_requested
< run_resumed
< tool_started
side_effect_count = 1
这个案例主要检查恢复接口和事件顺序,不代表已经覆盖所有分布式幂等故障。
案例 4:工具超时显式失败
slow_lookup 返回 simulated_latency_ms=1200,Harness 限制为 500:
status = FAILED
failure_code = tool_timeout
inline control 不执行超时策略,所以继续得到 final。
这里的延迟是确定性元数据,没有真的 sleep 1.2 秒。这样测试快且稳定,但只验证“超时契约如何传播”,不验证线程、进程或网络请求能否被真实取消。
案例 5:循环达到三步后停止
脚本模型连续请求四次查询。Harness 配置:
max_steps = 3
第三步工具完成后,下一次模型调用前进入:
status = STOPPED
failure_code = max_steps
inline control 会继续到最终文本,因此不满足边界契约。
案例 6:最终文本缺少证据
模型直接返回结论,但没有 source=。Harness 调用独立验证器后进入:
status = FAILED_VERIFICATION
failure_code = missing_evidence
这让“模型停止输出工具”与“任务被验收”成为两个不同事件。
12. 不要误读 16.67% 到 100%
图中 1/6 -> 6/6 很醒目,也最容易被滥用。
它只能说明:
在 6 个合成案例中,
minimal-harness-v1 满足本文声明的状态、事件顺序和副作用次数;
inline-loop-v1 只满足只读案例。
它不能说明:
- Harness 让模型准确率提高了 83.33 个百分点;
- 真实用户任务成功率达到 100%;
- 这个自建 Harness 比 OpenAI Agents SDK、Claude Agent SDK 或 LangGraph 更好;
- 多写这些类就能获得生产可靠性;
- 六个案例足以覆盖所有故障。
inline loop 是为了暴露“省略边界会发生什么”的教学控制组,不是对成熟框架的公平基准。真正选择框架时,应比较:
你的任务集
相同模型与预算
相同工具和权限
相同成功标准
相同故障注入
13. 做一次失败注入
正常路径通过后,把步数上限改成明显不合理的 1:
python run_lab.py harness-eval \
--max-steps 1 \
--output reports/local/harness-step-one
这个命令应退出 1,报告中会出现:
candidate case pass rate = 4 / 6
regression = read-only-answer
approved_resume_writes_once = false
gate_passed = false
为什么一个更严格的边界反而导致回归?
因为只读案例本身需要两次模型决定:
第 1 步:请求 lookup_policy
第 2 步:读取工具结果后生成 final
max_steps=1 在模型有机会生成 final 之前就停止。这里能看到一个重要事实:
边界不是越紧越好,而是要与任务复杂度和失败成本一起评测。
在生产中,步数上限还可以按任务类型分层:
FAQ read-only: 3
ticket triage: 8
coding task: 30
high-risk write: 先审批,再使用独立预算
但不要在没有历史 Trace 和失败样本时凭感觉设一个很大的默认值。
14. 代码里哪些地方值得读
完整实现位于 agent_lab/harness.py。
推荐按以下顺序:
1. ModelDecision
先看模型与运行时之间的窄协议。真实 Provider Adapter 最终都应该收敛到类似结构。
2. RunState
检查暂停后是否保留:
pending_action
approvals
completed_action_ids
steps
failure_code
events
3. MinimalHarness._drive
这是 Agent loop 主体。重点看模型调用、步数递增、工具请求、final 验证和状态转换由谁控制。
4. MinimalHarness._execute_pending
这里把 pending action 转成真实工具执行,并处理批准、拒绝、超时和回执。
5. MinimalHarness._checkpoint
检查点保存的不是一句“已暂停”,而是完整、可 JSON 序列化的 RunState。
6. _grade_run
Grader 不只检查最终状态,还检查副作用次数、必需事件和事件顺序。
数据集位于 datasets/harness-cases.jsonl。先改数据集里的期望,再改实现,比先写大量分支更容易保持边界清楚。
15. 怎样映射到现有框架
理解最小 Harness 后,不必照着 Lab 自建生产框架。可以把同一组责任映射到成熟实现。
| 本文责任 | OpenAI Agents SDK | LangGraph | 自建运行时 |
|---|---|---|---|
| 模型循环 | Runner | graph node / agent node | runner loop |
| 模型与工具边界 | Agent + tools | node/tool binding | adapter + registry |
| 对话延续 | Sessions 或服务端 conversation | thread-scoped state | ConversationStore |
| 暂停与恢复 | interruptions + RunState | interrupt + checkpointer | RunStateStore |
| Sandbox | Sandbox Agents / 外部执行面 | 自行集成 | SandboxProvider |
| 步数限制 | max_turns | recursion/step limit | max_steps |
| Trace | SDK tracing | graph execution trace | EventRecorder |
| 验证 | guardrail / evaluator / app logic | verifier node | Verifier port |
LangGraph 的 Persistence 文档把 checkpointer 与 store 分开:checkpointer 保存 thread 的图状态,用于中断、恢复和容错;store 保存跨 thread 的应用数据。这个区分与本文“运行进度不等于长期记忆”的原则一致。
选择框架时,优先问:
它替我拥有了哪些责任?
这些责任的状态能否导出和测试?
审批、恢复和错误能否形成稳定事件?
我能否替换模型、工具和持久化实现?
它省掉的代码,是否也隐藏了我需要的控制点?
16. 运行时 Harness 之外,还有仓库 Harness
OpenAI 在 Harness engineering: leveraging Codex in an agent-first world 中讨论的范围更大:
- 仓库是 Agent 能检索和验证的系统记录;
- 架构依赖方向由 lint 和结构测试机械执行;
- 日志、命名、文件大小和平台要求变成可检查规则;
- 人类 review 中反复出现的判断被反馈到文档、工具和测试;
- 测试、验证、反馈处理和恢复共同支持更高自治。
这时 Harness 不只是一个 Python runner,而是:
运行时控制面
+ 仓库结构
+ AGENTS.md / 文档
+ lint / typecheck / tests
+ preview / CI
+ review 与修复闭环
+ 可持续清理技术债的任务
两种范围可以这样理解:
Runtime Harness:
让一次 Agent 运行可控制、可恢复、可追踪
Environment Harness:
让 Agent 长期工作的代码库和反馈系统可理解、可约束、可验证
本文 Lab 只实现第一层。当前博客项目中的 AGENTS.md、内容检查、lint、build、浏览器预览和技术文章 review skill,已经是第二层的一部分。
17. Harness 自己也会过时
Harness 很容易不断增加:
更多 Planner
更多 Reviewer
更多重试
更多摘要
更多规则
更多 Agent
复杂度增长后,人们常把所有成功都归功于 Harness,却没有验证哪一层真正有用。
Anthropic 在 Harness design for long-running application development 的复盘中指出了一个很实用的原则:Harness 的每个组件都编码了“模型自己做不到什么”的假设;随着模型能力变化,这些假设可能错误或迅速过时。它采用的做法不是一次砍掉所有结构,而是逐项移除并评估影响。
因此 Harness 也要版本化:
harness_version
model_version
context_policy_version
tool_contract_version
eval_dataset_version
每次增加或删除一层,都问:
- 它解决哪个已观察失败?
- 哪个 Task 和 Grader 能击中这项能力?
- 它增加多少延迟、成本和状态复杂度?
- 更强模型上线后,它仍然有净收益吗?
- 能否删掉而不引入回归?
没有 Eval 的 Harness 会从“安全网”变成“看不见的技术债”。
18. 哪些时候不必自建 Harness
不建议因为看到新名词,就从头写一套运行时。
直接函数调用已经足够
如果任务是:
固定输入
-> 一次模型调用
-> 无工具或只有只读工具
-> 确定性后处理
普通应用代码可能比 Agent loop 更清楚。
成熟 SDK 已覆盖核心责任
如果需要工具循环、Session、Trace、审批和恢复,优先评估 OpenAI Agents SDK、Claude Agent SDK、LangGraph 或已有内部工作流平台。自建 Adapter 和业务策略层,通常比自建整个执行引擎风险更低。
业务流程本来就是确定性的
订单状态流转、审批链和支付编排如果有清楚规则,传统状态机或工作流引擎通常应该拥有主控制权。模型可以负责分类、信息提取或建议,而不是接管全部状态转换。
没有 Eval 时先别加复杂拓扑
如果还无法定义成功、失败和副作用证据,增加 Planner、Reviewer 或多 Agent 只会制造更多难以解释的路径。
19. 60 分钟练习:把你的 loop 变成最小 Harness
不要先实现完整框架。选择一个已有的工具调用 Demo,完成下面六步。
第 1 步:定义模型协议
把 Provider 输出转换成:
tool(action_id, name, arguments)
final(output)
验收:工具执行函数不再直接依赖 Provider 的原始 response object。
第 2 步:定义 RunState
至少包含:
run_id
status
step
pending_action
completed_action_ids
failure_code
验收:状态可以 JSON 序列化并重新加载。
第 3 步:把策略放到工具前
为一个写工具增加:
side_effect = true
needs_approval = true
验收:没有批准时副作用次数为 0。
第 4 步:增加两个硬停止条件
先做:
max_steps
tool timeout
验收:停止和超时返回不同状态与错误码。
第 5 步:增加最小事件序列
至少记录:
action_proposed
policy_checked
tool_started
tool_finished
run_finished
验收:可以用代码检查 policy_checked < tool_started。
第 6 步:做三个失败实验
写操作未批准
工具超时
模型持续调用工具
验收:三个实验都有明确状态,且没有被统计成成功。
20. 接入生产前的 Harness 检查清单
模型边界
- Provider 输出先转换成内部协议
- 无效工具名和无效参数有明确错误
- 模型 Adapter 不直接持有业务写权限
- 模型、Prompt 和 Context 策略都有版本
工具与权限
- 每个工具声明只读或副作用
- 副作用前执行策略检查
- 高风险工具支持人工审批
- 幂等键和真实回执由业务系统支持
- 密钥不进入模型 Context 或不受信任 Sandbox
状态与恢复
- 对话历史、RunState、Sandbox state 与 snapshot 分开
-
RunState可序列化并有 Schema 版本 - 暂停前保存 pending action
- 恢复使用同一个 action id
- 并发恢复有租约、锁或幂等保护
停止与错误
- 有
max_steps - 有 deadline 或 timeout
- 错误按可重试、不可重试和结果未知分类
-
STOPPED、FAILED与COMPLETED分开 - 用户取消可以传播到工具执行层
证据与运营
- Trace 有稳定 run id 和 event order
- 副作用记录真实 outcome,不只记录模型意图
- Verifier 独立检查完成条件
- 敏感输入输出有脱敏和保留策略
- 每个 Harness 组件都有对应失败样本和 Eval
结语:Harness 的价值是把不确定性关进明确边界
模型的优势,正是它可以面对不完整信息,提出下一步并适应变化。工程系统不能通过消灭这种不确定性来获得可靠性,也不该假装 Prompt 可以覆盖所有现实边界。
Harness 更实际的作用是:
允许模型自由提出候选动作
但由代码决定动作如何执行
允许任务暂停很久
但保留可恢复的运行状态
允许工具失败
但让失败分类、停止和回执可见
允许模型给出最终文本
但由真实证据决定任务是否完成
如果只记住一句:
模型负责“下一步可能做什么”,Harness 负责“系统现在允许做什么,以及做完后怎样证明”。
下一篇进入 Tool Engineering。我们会在同一个 GitHub 工程里,把目前写死的三个工具改成有 Schema、权限等级、幂等语义、dry-run、结构化错误和可重试声明的 Tool Registry,再用错误参数、重复写入、权限越界和超时案例验证工具契约。
参考资料
OpenAI
- Unrolling the Codex agent loop
- OpenAI Agents SDK:Running agents
- OpenAI Agents SDK:Human-in-the-loop
- Sandbox Agents:Resume or seed future work
- Migrate from the Claude Agent SDK to the OpenAI Agents SDK
- Harness engineering: leveraging Codex in an agent-first world