会话持久、上下文压缩、崩溃恢复可以外包;压缩策略、重试语义、工具调用顺序从此不在你手里。
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。两家形态几乎一样:平台握着循环和会话日志,你提供工具、挑执行环境。这期不讲怎么调用,讲怎么划线:哪些交出去划算,哪些交出去就再也拿不回来,以及「自托管沙箱」这条中间路线到底解决了什么。
托管 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 编排。选型时先对齐这些边角,核心循环两家差不多。
虚线框 = 唯一能由你决定放在哪里的一层。前两块换厂商时带不走。
# 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
托管 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)
event_delta)不进历史,换厂商时这份日志既不能重放,也对不上新平台的语义。能租的是循环,该自己留的是「对外部世界做了什么」那一层。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 要求确认。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 才往这个环境派会话
model_calls 计数就是为此准备的。