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

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

术语说明:厂商对 Harness 的覆盖范围并不完全相同。本系列采用便于学习的较窄定义:Harness 是模型之外、一次运行之内的执行环境;Agent Loop 是驱动模型与 Harness 反复交替的流程。篇末社区教程只作延伸阅读。

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

1.1 Harness 到底是什么

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

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

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

模型准备验证支付模块,提出“运行 payment tests”。Harness 接手后,会按顺序完成下面这些工作:

  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 view、预算请求必须可重现、可观测
Tool registry名称、schema、能力、版本、结果类型不能让模型发明真实接口
Policy / permissionallow、deny、ask、作用域安全边界需要确定性
Sandbox / executor文件、进程、网络、资源限制提示词不能隔离副作用
State / transcriptrun 状态、事件、结果引用、checkpoint恢复与审计需要持久事实
Observer / verifier进度、指标、执行证据自我报告不能成为唯一真值

Harness 负责记录并暴露 evidence;一次 run 是否继续由 agent loop 决定,跨任务和跨版本的结果质量是否成立则由 evals 判断。Verifier 可以在 harness 内执行测试,却不能因此独占产品真值。

2.2 一次 Tool Call 要经过七个关口

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

工具执行七关:解析、查找、校验、策略、沙箱、执行、结果归一化
每个关口都产生明确事件。这样失败才是可路由状态,而不是一段含糊异常文本。
  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()
  }
}
最小 dispatch:它只处理一个 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 等状态,model view 只接收足够决定下一步的 observation。恢复时,Runtime 先读取账本并与真实环境对账,必要时再按引用打开 artifact;它不会把一段聊天记录当成副作用是否发生的权威答案。

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 回答“即便允许,动作实际能触达什么”。批准运行测试不等于允许访问整个主目录;允许读取仓库不等于允许把内容发到任意网络端点。二者必须组合。

Harness 安全分层图:能力可见性、策略决策、人工审批、沙箱限制和审计证据
安全不是一个 deny 按钮,而是一组逐步收窄 authority 的层。
  • Capability scope:先不把无关工具暴露给模型。
  • Policy decision:按动作、资源、主体和环境匹配规则。
  • Human approval:只在高风险或含糊边界暂停,并展示可理解的 diff。
  • Sandbox:用文件系统、网络、进程和凭据隔离限制 blast radius。
  • Audit:记录谁提出、哪条规则允许、实际发生了什么。

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

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

Anthropic 的 Managed agents 把 session 看作围绕模型、harness 和 sandbox 的有状态环境。工程上至少要区分: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 或人工升级,不能直接再执行一次。

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 警告blast radius 无边界
大结果和秘密怎样处理?原样回填 context泄漏与窗口污染
进程中断后以什么事实恢复?只重放聊天重复副作用与错误状态
每个退出是否有可机读原因?只有 final 文本Outer loop 无法安全接管

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

官方资料

延伸阅读