
1. 先从最小 Agent loop 开始
OpenAI 在 Unrolling the Codex agent loop 中给出了一个很清楚的基础循环:
用伪代码表示,大致是:
这段代码已经具备“循环”,但还没有回答:
所以可以先建立第一层关系:
OpenAI Agents SDK 的 Runner 文档采用相似结构:Runner 调用当前 Agent 的模型,遇到 final 就结束,遇到 handoff 就切换 Agent,遇到 tool call 就执行并继续;超过 max_turns 会抛出明确异常。这里最值得借鉴的不是某个类名,而是:循环、工具执行、终止条件和状态恢复都有明确所有者。
2. Harness 不是更大的 Prompt
有些系统表面上有 Harness,实际只是把更多规则拼进系统提示词:
这些文字可以帮助模型做更好的判断,却不能替代运行时约束。
例如:
与:
不是同一强度的控制。前者是每次工具执行都必须经过的代码路径;后者依赖模型是否正确理解、是否记得,以及工具是否绕过了这段指令。
一个实用判断是:
Prompt 解决“怎样引导模型”,Harness 解决“系统允许什么、记录什么、何时停止”。
3. 五个组件分别负责什么
最常见的设计问题,不是少写了一个类,而是把所有责任都交给 Agent 或 session。本文先把五个组件拆开。

Model:提出候选决策
模型适合负责:
-
理解当前任务和证据; -
在可用工具中选择下一步; -
生成结构化工具参数; -
根据工具结果继续推理; -
生成候选最终回答。
模型不应单独拥有:
-
生产系统的最终权限; -
可靠的永久状态; -
“是否真实完成”的最终裁决; -
不可绕过的时间、成本和并发限制。
Harness:拥有控制流
Harness 负责:
-
组装本次模型输入; -
调用模型适配器; -
解释模型的结构化决定; -
在工具前执行策略与审批; -
保存和恢复运行状态; -
控制步数、时间和错误; -
记录事件并交给 Verifier。
换句话说,模型可以建议“调用 record_followup”,但 Harness 必须决定何时、以什么身份、在哪个环境、带什么幂等键执行。
RunState Store:保存“这项工作做到哪里”
本文配套 Lab 中的 RunState 保存:
这不是长期记忆,也不等于文件系统。它回答的是:
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 可能指完全不同的东西。
至少要区分下面四类:
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。
因此我更愿意在代码里使用精确名字:
而不是让一个 session 对象同时保存所有东西。
本文 Lab 的取舍
RunStateStore是内存实现,目的是展示接口和事件顺序;它不是生产持久化。真正上线时至少要考虑数据库事务、Schema 版本、保留时间、并发恢复和密钥脱敏。
5. 给模型一个窄协议
Harness 要可替换,模型接口就不应返回一团厂商特定对象。
本文 Lab 使用的最小协议只有两种决定:
真实接入时,可以为不同提供方编写 Adapter:
这层 Adapter 需要负责:
-
把厂商输出转换成统一 tool或final; -
保留 provider response id 等追踪信息; -
把无法解析的工具参数变成显式协议错误; -
不在 Adapter 内偷偷执行工具; -
不把业务审批规则绑定到某个模型 API。
Lab 没有调用真实模型,而是用 ScriptedModelAdapter 顺序返回固定决定。这样做不是为了模拟智能,而是为了锁定变量:
当模型决定完全相同时,增加 Harness 后,审批、超时、停止、恢复与验证边界是否真的发生变化?
6. 运行状态必须是状态机
很多 Demo 只有布尔值:
但真实运行至少需要区分:

几个容易混淆的状态:
WAITING_APPROVAL 不是失败
系统已经完成当前能做的工作,并留下可恢复的 pending action。它可以等几分钟,也可以等几天。
STOPPED 不是完成
达到 max_steps、预算耗尽或用户取消,只说明运行被边界终止。最终任务可能仍未完成。
FAILED_VERIFICATION 不应降级为普通回答
模型可以生成一段看似合理的最终文本,但独立检查未通过。把它标为 COMPLETED 会污染成功率,也会让后续恢复无从下手。
FAILED 需要错误分类
至少保留:
错误分类是以后设计重试、降级、告警和 Eval 的前提。只有一句 something went wrong,无法安全自动化。
7. 审批的关键不是弹窗,而是事件顺序
一个常见错误流程是:
另一个隐蔽错误是:
第二种做法可能生成新的参数和新的调用 ID,也无法确定上一次是否已经执行到一半。
更稳妥的顺序是:

OpenAI Agents SDK 的 Human-in-the-loop 文档采用类似思想:需要审批的工具会让运行产生 interruption;调用方把结果转成可序列化 RunState,记录批准或拒绝,再从原始顶层运行恢复。这里的工程重点有三个:
-
审批决定作用于具体 tool call,而不是一句模糊的“都同意”; -
暂停状态可以序列化,恢复不依赖原进程对象仍然活着; -
恢复的是原运行,不是新开一个相似任务。
为什么还需要 action_id
即使 Harness 正确保存状态,外部工具也可能出现这种时序:
恢复后,Harness 不知道写入是否发生。真正的 exactly-once 通常不能只靠 Agent 进程内变量保证,需要把稳定 action_id 当作幂等键交给业务系统:
业务系统应返回同一动作的已有回执,而不是再次产生副作用。
Lab 没有证明生产 exactly-once
配套实验使用内存回执账本,能证明同一进程内的接口和调用顺序,但没有覆盖数据库提交后进程崩溃、跨进程并发恢复或第三方 API 不支持幂等键等情况。本文把它称为“幂等接口演示”,而不是 exactly-once 保证。
8. 超时、重试和停止不能混成一个开关
超时回答“这一次调用等多久”
Harness 应记录:
超时后进入什么状态,要看工具语义。只读查询通常可以重试;扣款、发信、删除等副作用必须先查询回执或使用幂等键。
重试回答“失败后是否再做一次”
本文 Lab 故意不实现自动重试。原因是重试策略依赖下一篇 Tool Engineering 要定义的工具契约:
在这些字段缺失时写一个通用 retry=3,只是把偶发错误变成重复副作用。
停止回答“整个运行最多走多远”
即使每个工具都很快,模型也可能反复查询。Harness 至少要有:
本文实验实现 max_steps。每次模型决定都消耗一步;达到上限后进入:
这比让脚本在安全兜底处抛出未知错误更适合评测和恢复。
9. Trace 是运行证据,不是调试打印
日志常见写法是:
这对恢复和审计几乎没有帮助。本文 Lab 记录一组有顺序的事件:
每条事件包含单调递增的 seq,因此 Eval 可以检查:
这比检查“日志里出现过 approval”更严格。事件都存在,不代表顺序正确。
生产 Trace 还需要增加:
trace_id
、 run_id、tool_call_id;-
模型和 Prompt/Context 版本; -
延迟、Token、成本; -
输入输出摘要或哈希; -
错误分类和重试序号; -
数据脱敏与保留策略。
这些会在后续 Agent Tracing 专题中继续展开。
10. 跑起来:复现 Harness Lab 0.4.0
本篇代码固定在 GitHub commit 10b57afb。固定 commit 很重要:后续文章会继续修改同一个工程,本文命令仍应得到同一组结果。
环境要求:
先运行完整测试:
在这个检查点应看到:
再运行 Harness 对照:
Windows PowerShell 可以写成一行:
核心输出如下:
命令会生成:
建议按这个顺序阅读:
harness-comparison.md
:先看两个策略在哪些边界不同; harness-failures.md
:看 inline control 为什么失败; harness-runs.jsonl
:选一个案例读完整 RunState和事件序列;datasets/harness-cases.jsonl
:回头看成功标准怎样声明。
仓库也保留了本文核验时的固定报告:
harness-comparison.jsonharness-failures.mdharness-runs.jsonl
11. 六个案例分别证明什么

案例 1:只读查询能够正常完成
两个策略都通过。这个控制案例很重要:增加 Harness 不能破坏原本能完成的基础任务。
案例 2:写操作先暂停
模型提出:
预期结果:
inline control 直接写入,所以失败;Harness 保存 pending action 后暂停。
案例 3:批准后恢复并只写一次
运行先暂停,再把 RunState 序列化为 JSON、重新加载、批准并恢复。
预期结果:
这个案例主要检查恢复接口和事件顺序,不代表已经覆盖所有分布式幂等故障。
案例 4:工具超时显式失败
slow_lookup 返回 simulated_latency_ms=1200,Harness 限制为 500:
inline control 不执行超时策略,所以继续得到 final。
这里的延迟是确定性元数据,没有真的 sleep 1.2 秒。这样测试快且稳定,但只验证“超时契约如何传播”,不验证线程、进程或网络请求能否被真实取消。
案例 5:循环达到三步后停止
脚本模型连续请求四次查询。Harness 配置:
第三步工具完成后,下一次模型调用前进入:
inline control 会继续到最终文本,因此不满足边界契约。
案例 6:最终文本缺少证据
模型直接返回结论,但没有 source=。Harness 调用独立验证器后进入:
这让“模型停止输出工具”与“任务被验收”成为两个不同事件。
12. 不要误读 16.67% 到 100%
图中 1/6 -> 6/6 很醒目,也最容易被滥用。
它只能说明:
它不能说明:
-
Harness 让模型准确率提高了 83.33 个百分点; -
真实用户任务成功率达到 100%; -
这个自建 Harness 比 OpenAI Agents SDK、Claude Agent SDK 或 LangGraph 更好; -
多写这些类就能获得生产可靠性; -
六个案例足以覆盖所有故障。
inline loop 是为了暴露“省略边界会发生什么”的教学控制组,不是对成熟框架的公平基准。真正选择框架时,应比较:
13. 做一次失败注入
正常路径通过后,把步数上限改成明显不合理的 1:
这个命令应退出 1,报告中会出现:
为什么一个更严格的边界反而导致回归?
因为只读案例本身需要两次模型决定:
max_steps=1 在模型有机会生成 final 之前就停止。这里能看到一个重要事实:
边界不是越紧越好,而是要与任务复杂度和失败成本一起评测。
在生产中,步数上限还可以按任务类型分层:
但不要在没有历史 Trace 和失败样本时凭感觉设一个很大的默认值。
14. 代码里哪些地方值得读
完整实现位于 agent_lab/harness.py。
推荐按以下顺序:
1. ModelDecision
先看模型与运行时之间的窄协议。真实 Provider Adapter 最终都应该收敛到类似结构。
2. RunState
检查暂停后是否保留:
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 自建生产框架。可以把同一组责任映射到成熟实现。
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,而是:
两种范围可以这样理解:
本文 Lab 只实现第一层。当前博客项目中的 AGENTS.md、内容检查、lint、build、浏览器预览和技术文章 review skill,已经是第二层的一部分。
17. Harness 自己也会过时
Harness 很容易不断增加:
复杂度增长后,人们常把所有成功都归功于 Harness,却没有验证哪一层真正有用。
Anthropic 在 Harness design for long-running application development 的复盘中指出了一个很实用的原则:Harness 的每个组件都编码了“模型自己做不到什么”的假设;随着模型能力变化,这些假设可能错误或迅速过时。它采用的做法不是一次砍掉所有结构,而是逐项移除并评估影响。
因此 Harness 也要版本化:
每次增加或删除一层,都问:
-
它解决哪个已观察失败? -
哪个 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 输出转换成:
验收:工具执行函数不再直接依赖 Provider 的原始 response object。
第 2 步:定义 RunState
至少包含:
验收:状态可以 JSON 序列化并重新加载。
第 3 步:把策略放到工具前
为一个写工具增加:
验收:没有批准时副作用次数为 0。
第 4 步:增加两个硬停止条件
先做:
验收:停止和超时返回不同状态与错误码。
第 5 步:增加最小事件序列
至少记录:
验收:可以用代码检查 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
Anthropic 与 LangGraph
-
Anthropic:Harness design for long-running application development -
LangGraph:Persistence
本文代码与证据
-
Agent Reliability Lab 10b57af -
本篇完整代码 commit
订阅智宅客
AI / 技术 / 数字生活方式,新文章第一时间送到邮箱。









