DAY 63 · HARNESS

托管 Agent Harness

harness 从「自己搭」变成「可以租」之后,哪些还该自己留着

2026-10-06 · BigCat

会话持久、上下文压缩、崩溃恢复可以外包;压缩策略、重试语义、工具调用顺序从此不在你手里。

前置 → 本站 Day 3 Harness 工程 · Day 58 Durable Agent Execution

// WHY THIS MATTERS

Day 3 讲自己搭 harness,Day 58 讲自己做持久化执行。2026 年下半年,这两层都能租了:Anthropic 的 Claude Managed Agents(4 月起公测)把 agent 循环、会话存储和沙箱做成托管服务;OpenAI 在 9 月 10 日公测 Agents API,把 Codex 的 harness 放到一个 API 后面,9 月 29 日 DevDay 又加上了托管浏览器里的 computer use。两家形态几乎一样:平台握着循环和会话日志,你提供工具、挑执行环境。这期不讲怎么调用,讲怎么划线:哪些交出去划算,哪些交出去就再也拿不回来,以及「自托管沙箱」这条中间路线到底解决了什么。

// POINT 01

租的是「脑」和「会话」,「手」可以自己选

托管 harness 的切分线不在模型和代码之间,而在「决定下一步」和「执行这一步」之间。

// 背景与原理

Anthropic 工程博客 2026 年 4 月那篇 Scaling Managed Agents 把架构拆成三块:brain(Claude + harness 循环),hands(沙箱和工具,统一成 execute(name, input) → string),session(放在上下文窗口之外的持久事件日志)。harness 因此是无状态的:实例挂了,新实例用 wake(sessionId) 拉回日志,从最后一条事件接着跑。脑和手解耦后不必每个会话都先起容器,文中给的数字是 p50 TTFT 降约 60%,p95 降超过 90%。

API 上对应四个对象:Agent(模型、system、工具、MCP、skills)、Environment(云沙箱或自托管沙箱)、Session、Events。OpenAI 那边同构:平台管会话、编排、上下文压缩和恢复,计算可以放在 OpenAI 托管沙箱、合作方沙箱或你自己的机器上。两家都不收 harness 本身的钱,按模型、工具和容器用量计费。差异在边角:OpenAI 9 月 29 日加的 computer use 跑在它托管的浏览器里,能访问哪些网站仍由开发者控制;Anthropic 一侧另有按 cron 触发的定时部署和多 agent 编排。选型时先对齐这些边角,核心循环两家差不多。

BRAIN · 平台agent 循环
上下文压缩
prompt caching
重试与恢复
SESSION · 平台追加写事件日志
SSE 流 + 历史回放
崩溃后 wake 续跑
HANDS · 可选云沙箱(平台)
自托管 worker(你)
custom tool(你)

虚线框 = 唯一能由你决定放在哪里的一层。前两块换厂商时带不走。

// 实战示例

# Claude Managed Agents 最小闭环(beta 头由 SDK 自动加:managed-agents-2026-04-01)
from anthropic import Anthropic
client = Anthropic()

agent = client.beta.agents.create(
    name="repo-janitor", model="claude-opus-5-5",
    tools=[{"type": "agent_toolset_20260401",
            "configs": [{"name": "web_fetch", "enabled": False}]}],
)
env = client.beta.environments.create(
    name="janitor-env",
    config={"type": "cloud",
            "networking": {"type": "limited", "allow_package_managers": True}},
)
session = client.beta.sessions.create(agent=agent.id, environment_id=env.id)

with client.beta.sessions.events.stream(session.id) as stream:
    client.beta.sessions.events.send(session.id, events=[{
        "type": "user.message",
        "content": [{"type": "text", "text": "列出仓库里超过 90 天未改动的 TODO"}]}])
    for ev in stream:
        if ev.type == "agent.tool_use":
            print("[tool]", ev.name)
        elif ev.type == "session.status_idle":
            break
失败模式:把它当成「更长的 Messages API」用。会话天生有状态:对话历史、沙箱状态、产出都存在服务端,所以 Managed Agents 目前不适用 ZDR 和 HIPAA BAA;据多家报道,OpenAI Agents API 公测期也只支持美国数据驻留、不支持 ZDR。有合规要求的工作负载,选型第一步就会被挡掉,跟性能无关。
// POINT 02

交出去的是控制权:给 harness 漂移装哨兵

托管 harness 会随模型一起悄悄升级,这是卖点,也是你回归测试失效的原因。

// 背景与原理

同一篇博客讲了个例子:Sonnet 4.5 快到上下文上限时会提前收尾,团队就在 harness 里加了 context reset;换到 Opus 4.5,这个行为消失,reset 成了多余的。结论是 harness 编码的是「模型自己做不到什么」的假设,而这些假设会过期。托管的价值正在这里:厂商替你追模型,删掉过期的补丁。

代价是对称的。压缩什么时候触发、留什么,工具失败重试几次,并行调用怎么排序,这些都由平台决定,文档也写明 beta 期间行为可能在版本之间调整。你自建时 harness 版本跟着 git 走;租来的 harness,同一个 agent 配置、同一个任务,下个月的工具调用轨迹可能不一样。能做的是把事件日志当观测面:定期跑一组固定任务,比对轨迹指纹。

你还能握住的旋钮其实不多,要清楚有哪些:agent 配置带版本号(创建时返回 version),改配置要当成发版来做;工具集可以逐个开关、配 permission policy;网页工具可以设域名白名单。这些是「你的那一半」,应该和代码一起进 git(CLI 的 ant apply 会把 ID 记进 claude-lock.json)。剩下那一半只能观测,不能固定。

// 实战示例

# 漂移哨兵:固定任务的工具调用序列 + 模型请求次数,和基线对比
import json, hashlib
TOOL_EVENTS = ("agent.tool_use", "agent.custom_tool_use", "agent.mcp_tool_use")

def trace(session_id):
    names, model_calls = [], 0
    for e in client.beta.sessions.events.list(session_id):
        if e.type in TOOL_EVENTS:
            names.append(getattr(e, "name", e.type))
        elif e.type == "span.model_request_end":
            model_calls += 1
    return {"tools": names, "model_calls": model_calls,
            "fp": hashlib.sha1("|".join(names).encode()).hexdigest()[:10]}

cur, base = trace(SESSION_ID), json.load(open("baseline.json"))
if cur["fp"] != base["fp"] or cur["model_calls"] > 1.5 * base["model_calls"]:
    print("harness/模型行为漂移:先看产出质量,再决定是否更新基线", cur)
失败模式:轨迹变了不等于变差,平台优化后步数减少是好事,所以哨兵只用来触发人工复核,不能直接判失败。另一个坑是拿事件日志当可移植的审计记录:事件 schema 是厂商私有的,流式增量(event_delta)不进历史,换厂商时这份日志既不能重放,也对不上新平台的语义。
// POINT 03

Build vs Buy:把副作用留在自己这边

能租的是循环,该自己留的是「对外部世界做了什么」那一层。custom tool 就是这条边界。

// 背景与原理

按任务形状判断。适合租:跑几十分钟到几小时、要中途暂停续跑、工具以 bash/文件/搜索这类标准件为主、你的差异化在 prompt 和工具而不在循环本身。必须自建:harness 本身就是产品(自定义压缩、领域特化的规划);要跨厂商切换模型;要能逐字节重放的审计;有 ZDR 或数据驻留要求。

中间态最常见:循环租,副作用自己握。custom tool 调用时平台不执行,会话以 stop_reason: requires_action 停下,等你回 user.custom_tool_result。这意味着写库、发邮件、扣款都在你的进程里执行,Day 58 的幂等键照样能用;哪天换厂商,这一层原样搬走。

// 实战示例

# custom tool 回路:平台只发请求,副作用在你这边做,用事件 id 当幂等键
events_by_id = {}
with client.beta.sessions.events.stream(session.id) as stream:
    for ev in stream:
        if ev.type == "agent.custom_tool_use":
            events_by_id[ev.id] = ev
        elif ev.type == "session.status_idle":
            if ev.stop_reason.type != "requires_action":
                break
            for eid in ev.stop_reason.event_ids:
                call = events_by_id[eid]
                out = ledger.run_once(key=eid,          # 重复投递只执行一次
                                      fn=TOOLS[call.name], args=call.input)
                client.beta.sessions.events.send(session.id, events=[{
                    "type": "user.custom_tool_result",
                    "custom_tool_use_id": eid,
                    "content": [{"type": "text", "text": out}]}])
失败模式:你的回路进程挂了,会话就一直停在 requires_action。平台替你持久化了会话,没替你持久化回路,所以回路要能从事件历史重建 events_by_id,再补发未完成的结果。另外,内置 bash 在沙箱里执行的副作用(比如 git push)不经过这条路,要么关掉对应工具,要么配 permission policy 要求确认。
// POINT 04

自托管沙箱:搬走的是执行,不是数据流

worker 跑在你机器上,文件和出网都留在你的边界里;但工具的输入输出照样要回到厂商的控制平面。

// 背景与原理

Managed Agents 的 self_hosted 环境是一个工作队列:会话入队,你的 environment worker 只需出站 HTTPS,认领任务、下载 skills 和 memory store,模型在 Anthropic 侧请求工具,worker 在本地执行再回传结果。没有 worker 在线时会话排队而不是失败。官方文档说得很直白:工具输入输出仍流向 Anthropic 的控制平面,模型需要看到结果才能决定下一步。OpenAI 一侧,据报道官方也明确表示:自带沙箱改变的是工具在哪执行,不改变编排状态存在哪。

所以它解决的是三件事:内网服务可达、文件与进程不出边界、出网走你的网络策略。不解决的是:会话日志和工具结果的驻留、ZDR。还有些隐性限制:自托管环境只支持 memory_store 资源,挂 file 或 GitHub 仓库会直接 400;web_search/web_fetch 依然在 Anthropic 服务器上跑,不受你的网络策略约束。

另外要分清两根轴。自托管沙箱决定代码在哪执行,MCP tunnel 决定厂商怎么够到你内网里的 MCP server,两者互相独立:云沙箱可以经 tunnel 用私有 MCP,自托管会话也可以用公网 MCP。要让执行和工具访问都留在边界内,就两个一起用;不想开 tunnel,也可以在 worker 里把 MCP server 包成 custom tool 直接提供。

// 实战示例

# worker 主机:只放环境密钥(Console 里生成),不放 API key
export ANTHROPIC_ENVIRONMENT_KEY="sk-ant-oat01-..."
export ANTHROPIC_ENVIRONMENT_ID="env_..."
sudo mkdir -p /workspace && sudo chown "$USER" /workspace
ant beta:worker poll --workdir /workspace

# 你自己的机器:确认至少一个 worker 在拉队列
ant beta:environments:work stats --environment-id "$ANTHROPIC_ENVIRONMENT_ID"
# workers_polling ≥ 1 才往这个环境派会话
失败模式:把 API key 放在 worker 主机上。agent 的 bash 调用能读到同一台机器的环境变量,文档特意要求 key 不要放在 worker 上。再一个是把「自托管」当成合规答案报给法务:工具结果里只要出现客户数据,这份数据就已经进了厂商的会话存储,执行位置换了,数据流没变。

// 术语表

managed harness(托管 harness)
由平台运行的 agent 循环:调模型、路由工具、压缩上下文、持久化会话。
brain / hands / session
Anthropic 对托管架构的三分:决策循环、执行环境、循环之外的持久事件日志。
environment worker
跑在你基础设施上的进程,认领自托管环境队列里的会话并在本地执行工具。
custom tool
平台只发调用请求、由你的代码执行并回传结果的工具。
requires_action
会话因等待工具确认或 custom tool 结果而停下的 stop reason。
ZDR(Zero Data Retention)
零数据保留:请求处理完不在服务端留存内容,有状态会话天然与之冲突。
harness drift(harness 漂移)
配置不变,平台升级循环逻辑导致 agent 行为轨迹变化。

// 深入思考

「harness 假设会过期」是在论证租,还是在论证自建?
两边都能用。对租的一方,厂商比你更早知道新模型不再需要哪个补丁;对自建的一方,你更早知道你的任务需要哪个补丁。分界在于你的任务离厂商的主流负载有多远:越接近「通用编码/研究」,厂商的优化越会落到你头上;越偏领域,厂商的默认值越可能是噪声。
会话日志在厂商那里,算不算一种新型锁定?
算,而且比模型锁定更隐蔽。模型可以换,提示词可以迁,但几个月的会话历史、memory store、按厂商 schema 写的事件处理代码都带不走。类比数据库:换存储引擎容易,换数据格式难。对策是在自己这边镜像一份规范化的轨迹(任务、工具调用、结果、产出),只保留语义,不存厂商格式。
为什么两家不约而同把 harness 免费、按用量收钱?
harness 的边际成本低,但它决定了 token 怎么花:压缩得多保守、重试得多积极、并行开多宽,都直接影响账单。免费的循环加按量计费,等于平台同时握着质量旋钮和成本旋钮。这不一定是坏事,但意味着成本优化的主导权部分交给了对方,Point 02 的 model_calls 计数就是为此准备的。
如果 MCP 把工具层标准化了,下一个被标准化的会是会话层吗?
Day 60 讲 MCP 走向无状态,Day 61 讲 ACS 把护栏做成可移植策略。会话事件日志是剩下最明显的私有格式。要标准化,至少要统一事件类型、工具调用 id 语义和压缩记录方式。在那之前,「可移植」只能靠你自己的规范化镜像,这和分布式系统里先有各家 WAL、很久以后才有 CDC 标准的路径很像。

// 延伸阅读