已发布资料基础官方资料 + 个人实践
·16 分钟0 次阅读AI Agent 工程进阶
文章/AI 与智能体

Harness Engineering:模型、工具与运行状态怎么组织

AI Agent 工程进阶第 4 篇:区分 Agent loop、Harness、RunState、对话 Session、Sandbox 与 Verifier,并在同一个 GitHub 工程中实现可替换模型接口、审批暂停与恢复、工具超时、步数上限、检查点和事件追踪。

#Agent#Agent Engineering#Harness Engineering#Agent Loop#RunState
文章目录
  1. 一分钟概览
  2. 1. 先从最小 Agent loop 开始
  3. 2. Harness 不是更大的 Prompt
  4. 3. 五个组件分别负责什么
  5. Model:提出候选决策
  6. Harness:拥有控制流
  7. RunState Store:保存“这项工作做到哪里”
  8. Sandbox:拥有受限执行环境
  9. Verifier:判断真实结果
  10. 4. Session 到底指什么
  11. 5. 给模型一个窄协议
  12. 6. 运行状态必须是状态机
  13. WAITINGAPPROVAL 不是失败
  14. STOPPED 不是完成
  15. FAILEDVERIFICATION 不应降级为普通回答
  16. FAILED 需要错误分类
  17. 7. 审批的关键不是弹窗,而是事件顺序
  18. 为什么还需要 actionid
  19. 8. 超时、重试和停止不能混成一个开关
  20. 超时回答“这一次调用等多久”
  21. 重试回答“失败后是否再做一次”
  22. 停止回答“整个运行最多走多远”
  23. 9. Trace 是运行证据,不是调试打印
  24. 10. 跑起来:复现 Harness Lab 0.4.0
  25. 11. 六个案例分别证明什么
  26. 案例 1:只读查询能够正常完成
  27. 案例 2:写操作先暂停
  28. 案例 3:批准后恢复并只写一次
  29. 案例 4:工具超时显式失败
  30. 案例 5:循环达到三步后停止
  31. 案例 6:最终文本缺少证据
  32. 12. 不要误读 16.67% 到 100%
  33. 13. 做一次失败注入
  34. 14. 代码里哪些地方值得读
  35. 1. ModelDecision
  36. 2. RunState
  37. 3. MinimalHarness.drive
  38. 4. MinimalHarness.executepending
  39. 5. MinimalHarness.checkpoint
  40. 6. graderun
  41. 15. 怎样映射到现有框架
  42. 16. 运行时 Harness 之外,还有仓库 Harness
  43. 17. Harness 自己也会过时
  44. 18. 哪些时候不必自建 Harness
  45. 直接函数调用已经足够
  46. 成熟 SDK 已覆盖核心责任
  47. 业务流程本来就是确定性的
  48. 没有 Eval 时先别加复杂拓扑
  49. 19. 60 分钟练习:把你的 loop 变成最小 Harness
  50. 第 1 步:定义模型协议
  51. 第 2 步:定义 RunState
  52. 第 3 步:把策略放到工具前
  53. 第 4 步:增加两个硬停止条件
  54. 第 5 步:增加最小事件序列
  55. 第 6 步:做三个失败实验
  56. 20. 接入生产前的 Harness 检查清单
  57. 模型边界
  58. 工具与权限
  59. 状态与恢复
  60. 停止与错误
  61. 证据与运营
  62. 结语:Harness 的价值是把不确定性关进明确边界
  63. 参考资料
  64. OpenAI
  65. Anthropic 与 LangGraph
  66. 本文代码与证据

一个模型会调用工具,不等于一个 Agent 系统已经可以可靠运行。

最简单的工具调用 Demo 通常只有几行:

text
把用户问题发给模型
如果模型要求调用工具,就执行工具
把工具结果发回模型
重复,直到模型返回最终文本

这确实是 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。

一分钟概览

如果只保存这篇文章的结论,可以记住十点:

  1. Agent loop 是 Harness 的核心,但不等于完整 Harness。 Loop 负责 model -> tool -> model;Harness 还要管理权限、状态、预算、错误与验收。
  2. 模型提出动作,Harness 决定动作能否执行。 生产权限不能只靠 Prompt 中一句“危险操作先询问”。
  3. 副作用之前要先做策略检查。 需要人工审批时,系统应先保存待执行动作,再进入 WAITING_APPROVAL
  4. 恢复不是重新发送原 Prompt。 恢复要带回同一个 RunState、待处理 action_id、审批决定与已完成回执。
  5. Context、对话历史、RunState、Sandbox state 和文件 snapshot 是不同状态面。 它们不应由一个模糊的 session 字段包办。
  6. 每次运行都要有明确状态。 COMPLETEDWAITING_APPROVALFAILEDSTOPPEDFAILED_VERIFICATION 不能混用。
  7. 超时、步数和成本是 Harness 的硬边界。 模型不能靠“自觉”保证结束。
  8. 模型最终文本只是候选结果。 Verifier 应检查真实结果或证据,再决定是否完成。
  9. Harness 本身也会变成技术债。 每一层脚手架都在假设模型做不到什么,应通过 Eval 逐项验证和删减。
  10. 先用成熟 SDK,再决定是否自建。 自建最小 Lab 的目的,是理解边界和做适配层,不是重新实现所有生产能力。

图 1:Harness 位于模型和真实执行面之间,管理 Context、策略、RunState、超时、追踪与验证图 1:Harness 位于模型和真实执行面之间,管理 Context、策略、RunState、超时、追踪与验证

1. 先从最小 Agent loop 开始

OpenAI 在 Unrolling the Codex agent loop 中给出了一个很清楚的基础循环:

text
用户输入
  -> 模型推理
  -> 最终回答:结束
  -> 工具调用:执行工具,把结果追加到输入,再次推理

用伪代码表示,大致是:

python
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)

这段代码已经具备“循环”,但还没有回答:

text
execute_tool 之前有没有策略检查?
工具执行到一半崩溃怎么办?
final 是否等于真实完成?
while True 最多运行多久?
messages 之外还要保存什么?

所以可以先建立第一层关系:

text
Agent loop = 反复获取模型决定并处理工具结果

Harness = Agent loop
        + 输入组装
        + 工具分派
        + 策略与审批
        + 运行状态
        + 超时与停止
        + 追踪与验证

OpenAI Agents SDK 的 Runner 文档采用相似结构:Runner 调用当前 Agent 的模型,遇到 final 就结束,遇到 handoff 就切换 Agent,遇到 tool call 就执行并继续;超过 max_turns 会抛出明确异常。这里最值得借鉴的不是某个类名,而是:循环、工具执行、终止条件和状态恢复都有明确所有者。

2. Harness 不是更大的 Prompt

有些系统表面上有 Harness,实际只是把更多规则拼进系统提示词:

text
不要执行危险操作
遇到错误请重试
不要重复写入
最多思考十步
完成后请验证结果

这些文字可以帮助模型做更好的判断,却不能替代运行时约束。

例如:

python
if tool.side_effect and not approval_store.is_approved(call_id):
    pause_run()

与:

text
“请记得在写数据之前询问用户。”

不是同一强度的控制。前者是每次工具执行都必须经过的代码路径;后者依赖模型是否正确理解、是否记得,以及工具是否绕过了这段指令。

一个实用判断是:

规则可以写进 Prompt还必须写进 Harness
回答语气和格式通常不必
优先使用哪些资料需要时做来源与权限过滤
写操作先确认可以提醒必须有代码策略闸门
最多运行多少步可以提醒必须有计数器与停止状态
工具超时无法可靠执行必须由运行时控制
不重复扣款可以提醒必须依赖幂等键与外部回执
结果是否真的落库可以自检必须读取真实状态或独立验证

Prompt 解决“怎样引导模型”,Harness 解决“系统允许什么、记录什么、何时停止”。

3. 五个组件分别负责什么

最常见的设计问题,不是少写了一个类,而是把所有责任都交给 Agentsession。本文先把五个组件拆开。

图 2:Model、Harness、RunState Store、Sandbox 与 Verifier 的职责边界图 2:Model、Harness、RunState Store、Sandbox 与 Verifier 的职责边界

Model:提出候选决策

模型适合负责:

  • 理解当前任务和证据;
  • 在可用工具中选择下一步;
  • 生成结构化工具参数;
  • 根据工具结果继续推理;
  • 生成候选最终回答。

模型不应单独拥有:

  • 生产系统的最终权限;
  • 可靠的永久状态;
  • “是否真实完成”的最终裁决;
  • 不可绕过的时间、成本和并发限制。

Harness:拥有控制流

Harness 负责:

  • 组装本次模型输入;
  • 调用模型适配器;
  • 解释模型的结构化决定;
  • 在工具前执行策略与审批;
  • 保存和恢复运行状态;
  • 控制步数、时间和错误;
  • 记录事件并交给 Verifier。

换句话说,模型可以建议“调用 record_followup”,但 Harness 必须决定何时、以什么身份、在哪个环境、带什么幂等键执行。

RunState Store:保存“这项工作做到哪里”

本文配套 Lab 中的 RunState 保存:

text
run_id
case_id
status
model_cursor
steps
pending_action
approvals
completed_action_ids
messages
final_output
failure_code
events

这不是长期记忆,也不等于文件系统。它回答的是:

text
这一次运行正处于什么状态?
下一步待处理的动作是什么?
哪些动作已经完成?
恢复时必须带回哪些决定?

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_idprevious_response_id。文档特别提醒,不加设计地混合两种历史管理方式可能重复 Context。

因此我更愿意在代码里使用精确名字:

text
ConversationStore
RunStateStore
SandboxSessionProvider
WorkspaceSnapshotStore
MemoryStore

而不是让一个 session 对象同时保存所有东西。

本文 Lab 的取舍

RunStateStore 是内存实现,目的是展示接口和事件顺序;它不是生产持久化。真正上线时至少要考虑数据库事务、Schema 版本、保留时间、并发恢复和密钥脱敏。

5. 给模型一个窄协议

Harness 要可替换,模型接口就不应返回一团厂商特定对象。

本文 Lab 使用的最小协议只有两种决定:

python
@dataclass(frozen=True)
class ModelDecision:
    kind: str                 # "tool" or "final"
    action_id: str | None
    tool_name: str | None
    arguments: dict
    output: str | None

真实接入时,可以为不同提供方编写 Adapter:

text
OpenAI Responses adapter
Claude adapter
local model adapter
scripted test adapter
         |
         v
统一 ModelDecision

这层 Adapter 需要负责:

  • 把厂商输出转换成统一 toolfinal
  • 保留 provider response id 等追踪信息;
  • 把无法解析的工具参数变成显式协议错误;
  • 不在 Adapter 内偷偷执行工具;
  • 不把业务审批规则绑定到某个模型 API。

Lab 没有调用真实模型,而是用 ScriptedModelAdapter 顺序返回固定决定。这样做不是为了模拟智能,而是为了锁定变量:

当模型决定完全相同时,增加 Harness 后,审批、超时、停止、恢复与验证边界是否真的发生变化?

6. 运行状态必须是状态机

很多 Demo 只有布尔值:

python
done = True

但真实运行至少需要区分:

text
READY
RUNNING
WAITING_APPROVAL
COMPLETED
FAILED
FAILED_VERIFICATION
STOPPED

图 3:Harness 从运行到审批暂停、恢复、完成、失败或停止的状态机图 3:Harness 从运行到审批暂停、恢复、完成、失败或停止的状态机

几个容易混淆的状态:

WAITING_APPROVAL 不是失败

系统已经完成当前能做的工作,并留下可恢复的 pending action。它可以等几分钟,也可以等几天。

STOPPED 不是完成

达到 max_steps、预算耗尽或用户取消,只说明运行被边界终止。最终任务可能仍未完成。

FAILED_VERIFICATION 不应降级为普通回答

模型可以生成一段看似合理的最终文本,但独立检查未通过。把它标为 COMPLETED 会污染成功率,也会让后续恢复无从下手。

FAILED 需要错误分类

至少保留:

text
tool_timeout
tool_not_found
invalid_tool_request
approval_rejected
model_protocol

错误分类是以后设计重试、降级、告警和 Eval 的前提。只有一句 something went wrong,无法安全自动化。

7. 审批的关键不是弹窗,而是事件顺序

一个常见错误流程是:

text
执行写操作
  -> 发现危险
  -> 再询问用户是否允许

另一个隐蔽错误是:

text
模型提出写操作
  -> 程序结束并丢失完整状态
  -> 用户批准
  -> 重新发送原任务,让模型再生成一次工具调用

第二种做法可能生成新的参数和新的调用 ID,也无法确定上一次是否已经执行到一半。

更稳妥的顺序是:

text
action_proposed
  -> policy_checked
  -> checkpoint_saved
  -> approval_requested
  -> WAITING_APPROVAL
  -> run_resumed
  -> tool_started
  -> tool_finished
  -> checkpoint_saved

图 4:写操作从模型提议、策略检查、检查点、人工批准到幂等执行的时序图 4:写操作从模型提议、策略检查、检查点、人工批准到幂等执行的时序

OpenAI Agents SDK 的 Human-in-the-loop 文档采用类似思想:需要审批的工具会让运行产生 interruption;调用方把结果转成可序列化 RunState,记录批准或拒绝,再从原始顶层运行恢复。这里的工程重点有三个:

  1. 审批决定作用于具体 tool call,而不是一句模糊的“都同意”;
  2. 暂停状态可以序列化,恢复不依赖原进程对象仍然活着;
  3. 恢复的是原运行,不是新开一个相似任务。

为什么还需要 action_id

即使 Harness 正确保存状态,外部工具也可能出现这种时序:

text
业务写入成功
  -> 回执还没保存
  -> Harness 进程崩溃

恢复后,Harness 不知道写入是否发生。真正的 exactly-once 通常不能只靠 Agent 进程内变量保证,需要把稳定 action_id 当作幂等键交给业务系统:

python
record_followup(
    ticket_id="T-102",
    note="Send refund form",
    idempotency_key="write-followup-102",
)

业务系统应返回同一动作的已有回执,而不是再次产生副作用。

Lab 没有证明生产 exactly-once

配套实验使用内存回执账本,能证明同一进程内的接口和调用顺序,但没有覆盖数据库提交后进程崩溃、跨进程并发恢复或第三方 API 不支持幂等键等情况。本文把它称为“幂等接口演示”,而不是 exactly-once 保证。

8. 超时、重试和停止不能混成一个开关

超时回答“这一次调用等多久”

Harness 应记录:

text
tool
action_id
started_at
timeout
error_class

超时后进入什么状态,要看工具语义。只读查询通常可以重试;扣款、发信、删除等副作用必须先查询回执或使用幂等键。

重试回答“失败后是否再做一次”

本文 Lab 故意不实现自动重试。原因是重试策略依赖下一篇 Tool Engineering 要定义的工具契约:

text
是否只读?
是否幂等?
错误是否可重试?
退避多久?
调用是否已经产生副作用?

在这些字段缺失时写一个通用 retry=3,只是把偶发错误变成重复副作用。

停止回答“整个运行最多走多远”

即使每个工具都很快,模型也可能反复查询。Harness 至少要有:

text
max_steps
deadline
token or cost budget
user cancellation

本文实验实现 max_steps。每次模型决定都消耗一步;达到上限后进入:

json
{
  "status": "stopped",
  "failure_code": "max_steps"
}

这比让脚本在安全兜底处抛出未知错误更适合评测和恢复。

9. Trace 是运行证据,不是调试打印

日志常见写法是:

text
calling tool...
done

这对恢复和审计几乎没有帮助。本文 Lab 记录一组有顺序的事件:

text
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 可以检查:

python
checkpoint_saved < approval_requested
policy_checked < tool_started
run_resumed < tool_started
final_received < verification_finished

这比检查“日志里出现过 approval”更严格。事件都存在,不代表顺序正确。

生产 Trace 还需要增加:

  • trace_idrun_idtool_call_id
  • 模型和 Prompt/Context 版本;
  • 延迟、Token、成本;
  • 输入输出摘要或哈希;
  • 错误分类和重试序号;
  • 数据脱敏与保留策略。

这些会在后续 Agent Tracing 专题中继续展开。

10. 跑起来:复现 Harness Lab 0.4.0

本篇代码固定在 GitHub commit 10b57afb。固定 commit 很重要:后续文章会继续修改同一个工程,本文命令仍应得到同一组结果。

bash
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

环境要求:

text
Python 3.10+
零第三方依赖
不需要 API Key

先运行完整测试:

bash
python -m unittest discover -s tests -v

在这个检查点应看到:

text
Ran 29 tests
OK

再运行 Harness 对照:

bash
python run_lab.py harness-eval \
  --output reports/local/harness-review

Windows PowerShell 可以写成一行:

powershell
python run_lab.py harness-eval --output reports/local/harness-review

核心输出如下:

json
{
  "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
}

命令会生成:

text
reports/local/harness-review/
  harness-comparison.json
  harness-comparison.md
  harness-failures.md
  harness-runs.jsonl

建议按这个顺序阅读:

  1. harness-comparison.md:先看两个策略在哪些边界不同;
  2. harness-failures.md:看 inline control 为什么失败;
  3. harness-runs.jsonl:选一个案例读完整 RunState 和事件序列;
  4. datasets/harness-cases.jsonl:回头看成功标准怎样声明。

仓库也保留了本文核验时的固定报告:

11. 六个案例分别证明什么

图 5:inline loop 与 minimal Harness 在六个边界案例中的对照结果图 5:inline loop 与 minimal Harness 在六个边界案例中的对照结果

案例 1:只读查询能够正常完成

text
lookup_policy
  -> tool result
  -> final with source=
  -> COMPLETED

两个策略都通过。这个控制案例很重要:增加 Harness 不能破坏原本能完成的基础任务。

案例 2:写操作先暂停

模型提出:

json
{
  "action_id": "write-followup-101",
  "tool": "record_followup"
}

预期结果:

text
status = WAITING_APPROVAL
side_effect_count = 0
checkpoint_saved < approval_requested

inline control 直接写入,所以失败;Harness 保存 pending action 后暂停。

案例 3:批准后恢复并只写一次

运行先暂停,再把 RunState 序列化为 JSON、重新加载、批准并恢复。

预期结果:

text
approval_requested
  < run_resumed
  < tool_started

side_effect_count = 1

这个案例主要检查恢复接口和事件顺序,不代表已经覆盖所有分布式幂等故障。

案例 4:工具超时显式失败

slow_lookup 返回 simulated_latency_ms=1200,Harness 限制为 500

text
status = FAILED
failure_code = tool_timeout

inline control 不执行超时策略,所以继续得到 final。

这里的延迟是确定性元数据,没有真的 sleep 1.2 秒。这样测试快且稳定,但只验证“超时契约如何传播”,不验证线程、进程或网络请求能否被真实取消。

案例 5:循环达到三步后停止

脚本模型连续请求四次查询。Harness 配置:

text
max_steps = 3

第三步工具完成后,下一次模型调用前进入:

text
status = STOPPED
failure_code = max_steps

inline control 会继续到最终文本,因此不满足边界契约。

案例 6:最终文本缺少证据

模型直接返回结论,但没有 source=。Harness 调用独立验证器后进入:

text
status = FAILED_VERIFICATION
failure_code = missing_evidence

这让“模型停止输出工具”与“任务被验收”成为两个不同事件。

12. 不要误读 16.67% 到 100%

图中 1/6 -> 6/6 很醒目,也最容易被滥用。

它只能说明:

text
在 6 个合成案例中,
minimal-harness-v1 满足本文声明的状态、事件顺序和副作用次数;
inline-loop-v1 只满足只读案例。

它不能说明:

  • Harness 让模型准确率提高了 83.33 个百分点;
  • 真实用户任务成功率达到 100%;
  • 这个自建 Harness 比 OpenAI Agents SDK、Claude Agent SDK 或 LangGraph 更好;
  • 多写这些类就能获得生产可靠性;
  • 六个案例足以覆盖所有故障。

inline loop 是为了暴露“省略边界会发生什么”的教学控制组,不是对成熟框架的公平基准。真正选择框架时,应比较:

text
你的任务集
相同模型与预算
相同工具和权限
相同成功标准
相同故障注入

13. 做一次失败注入

正常路径通过后,把步数上限改成明显不合理的 1

bash
python run_lab.py harness-eval \
  --max-steps 1 \
  --output reports/local/harness-step-one

这个命令应退出 1,报告中会出现:

text
candidate case pass rate = 4 / 6
regression = read-only-answer
approved_resume_writes_once = false
gate_passed = false

为什么一个更严格的边界反而导致回归?

因为只读案例本身需要两次模型决定:

text
第 1 步:请求 lookup_policy
第 2 步:读取工具结果后生成 final

max_steps=1 在模型有机会生成 final 之前就停止。这里能看到一个重要事实:

边界不是越紧越好,而是要与任务复杂度和失败成本一起评测。

在生产中,步数上限还可以按任务类型分层:

text
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

检查暂停后是否保留:

text
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 SDKLangGraph自建运行时
模型循环Runnergraph node / agent noderunner loop
模型与工具边界Agent + toolsnode/tool bindingadapter + registry
对话延续Sessions 或服务端 conversationthread-scoped stateConversationStore
暂停与恢复interruptions + RunStateinterrupt + checkpointerRunStateStore
SandboxSandbox Agents / 外部执行面自行集成SandboxProvider
步数限制max_turnsrecursion/step limitmax_steps
TraceSDK tracinggraph execution traceEventRecorder
验证guardrail / evaluator / app logicverifier nodeVerifier port

LangGraph 的 Persistence 文档把 checkpointer 与 store 分开:checkpointer 保存 thread 的图状态,用于中断、恢复和容错;store 保存跨 thread 的应用数据。这个区分与本文“运行进度不等于长期记忆”的原则一致。

选择框架时,优先问:

text
它替我拥有了哪些责任?
这些责任的状态能否导出和测试?
审批、恢复和错误能否形成稳定事件?
我能否替换模型、工具和持久化实现?
它省掉的代码,是否也隐藏了我需要的控制点?

16. 运行时 Harness 之外,还有仓库 Harness

OpenAI 在 Harness engineering: leveraging Codex in an agent-first world 中讨论的范围更大:

  • 仓库是 Agent 能检索和验证的系统记录;
  • 架构依赖方向由 lint 和结构测试机械执行;
  • 日志、命名、文件大小和平台要求变成可检查规则;
  • 人类 review 中反复出现的判断被反馈到文档、工具和测试;
  • 测试、验证、反馈处理和恢复共同支持更高自治。

这时 Harness 不只是一个 Python runner,而是:

text
运行时控制面
+ 仓库结构
+ AGENTS.md / 文档
+ lint / typecheck / tests
+ preview / CI
+ review 与修复闭环
+ 可持续清理技术债的任务

两种范围可以这样理解:

text
Runtime Harness:
让一次 Agent 运行可控制、可恢复、可追踪

Environment Harness:
让 Agent 长期工作的代码库和反馈系统可理解、可约束、可验证

本文 Lab 只实现第一层。当前博客项目中的 AGENTS.md、内容检查、lint、build、浏览器预览和技术文章 review skill,已经是第二层的一部分。

17. Harness 自己也会过时

Harness 很容易不断增加:

text
更多 Planner
更多 Reviewer
更多重试
更多摘要
更多规则
更多 Agent

复杂度增长后,人们常把所有成功都归功于 Harness,却没有验证哪一层真正有用。

Anthropic 在 Harness design for long-running application development 的复盘中指出了一个很实用的原则:Harness 的每个组件都编码了“模型自己做不到什么”的假设;随着模型能力变化,这些假设可能错误或迅速过时。它采用的做法不是一次砍掉所有结构,而是逐项移除并评估影响。

因此 Harness 也要版本化:

text
harness_version
model_version
context_policy_version
tool_contract_version
eval_dataset_version

每次增加或删除一层,都问:

  1. 它解决哪个已观察失败?
  2. 哪个 Task 和 Grader 能击中这项能力?
  3. 它增加多少延迟、成本和状态复杂度?
  4. 更强模型上线后,它仍然有净收益吗?
  5. 能否删掉而不引入回归?

没有 Eval 的 Harness 会从“安全网”变成“看不见的技术债”。

18. 哪些时候不必自建 Harness

不建议因为看到新名词,就从头写一套运行时。

直接函数调用已经足够

如果任务是:

text
固定输入
-> 一次模型调用
-> 无工具或只有只读工具
-> 确定性后处理

普通应用代码可能比 Agent loop 更清楚。

成熟 SDK 已覆盖核心责任

如果需要工具循环、Session、Trace、审批和恢复,优先评估 OpenAI Agents SDK、Claude Agent SDK、LangGraph 或已有内部工作流平台。自建 Adapter 和业务策略层,通常比自建整个执行引擎风险更低。

业务流程本来就是确定性的

订单状态流转、审批链和支付编排如果有清楚规则,传统状态机或工作流引擎通常应该拥有主控制权。模型可以负责分类、信息提取或建议,而不是接管全部状态转换。

没有 Eval 时先别加复杂拓扑

如果还无法定义成功、失败和副作用证据,增加 Planner、Reviewer 或多 Agent 只会制造更多难以解释的路径。

19. 60 分钟练习:把你的 loop 变成最小 Harness

不要先实现完整框架。选择一个已有的工具调用 Demo,完成下面六步。

第 1 步:定义模型协议

把 Provider 输出转换成:

text
tool(action_id, name, arguments)
final(output)

验收:工具执行函数不再直接依赖 Provider 的原始 response object。

第 2 步:定义 RunState

至少包含:

text
run_id
status
step
pending_action
completed_action_ids
failure_code

验收:状态可以 JSON 序列化并重新加载。

第 3 步:把策略放到工具前

为一个写工具增加:

text
side_effect = true
needs_approval = true

验收:没有批准时副作用次数为 0。

第 4 步:增加两个硬停止条件

先做:

text
max_steps
tool timeout

验收:停止和超时返回不同状态与错误码。

第 5 步:增加最小事件序列

至少记录:

text
action_proposed
policy_checked
tool_started
tool_finished
run_finished

验收:可以用代码检查 policy_checked < tool_started

第 6 步:做三个失败实验

text
写操作未批准
工具超时
模型持续调用工具

验收:三个实验都有明确状态,且没有被统计成成功。

20. 接入生产前的 Harness 检查清单

模型边界

  • Provider 输出先转换成内部协议
  • 无效工具名和无效参数有明确错误
  • 模型 Adapter 不直接持有业务写权限
  • 模型、Prompt 和 Context 策略都有版本

工具与权限

  • 每个工具声明只读或副作用
  • 副作用前执行策略检查
  • 高风险工具支持人工审批
  • 幂等键和真实回执由业务系统支持
  • 密钥不进入模型 Context 或不受信任 Sandbox

状态与恢复

  • 对话历史、RunState、Sandbox state 与 snapshot 分开
  • RunState 可序列化并有 Schema 版本
  • 暂停前保存 pending action
  • 恢复使用同一个 action id
  • 并发恢复有租约、锁或幂等保护

停止与错误

  • max_steps
  • 有 deadline 或 timeout
  • 错误按可重试、不可重试和结果未知分类
  • STOPPEDFAILEDCOMPLETED 分开
  • 用户取消可以传播到工具执行层

证据与运营

  • Trace 有稳定 run id 和 event order
  • 副作用记录真实 outcome,不只记录模型意图
  • Verifier 独立检查完成条件
  • 敏感输入输出有脱敏和保留策略
  • 每个 Harness 组件都有对应失败样本和 Eval

结语:Harness 的价值是把不确定性关进明确边界

模型的优势,正是它可以面对不完整信息,提出下一步并适应变化。工程系统不能通过消灭这种不确定性来获得可靠性,也不该假装 Prompt 可以覆盖所有现实边界。

Harness 更实际的作用是:

text
允许模型自由提出候选动作
但由代码决定动作如何执行

允许任务暂停很久
但保留可恢复的运行状态

允许工具失败
但让失败分类、停止和回执可见

允许模型给出最终文本
但由真实证据决定任务是否完成

如果只记住一句:

模型负责“下一步可能做什么”,Harness 负责“系统现在允许做什么,以及做完后怎样证明”。

下一篇进入 Tool Engineering。我们会在同一个 GitHub 工程里,把目前写死的三个工具改成有 Schema、权限等级、幂等语义、dry-run、结构化错误和可重试声明的 Tool Registry,再用错误参数、重复写入、权限越界和超时案例验证工具契约。

参考资料

OpenAI

Anthropic 与 LangGraph

本文代码与证据

继续阅读