按照我们的文档,开始使用 Claude Managed Agents。

工程博客上一个持续讨论的主题,是如何构建有效的 agent,以及如何为长时间运行的工作设计 harness。贯穿这些工作的一个共同点是,harness 会编码关于 Claude 不能独立完成什么的假设。然而,随着模型不断进步,这些假设需要被频繁质疑,因为它们可能会过时

仅举一个例子,在此前的工作中,我们发现,当 Claude Sonnet 4.5 感知到自己正在接近上下文限制时,会过早结束任务,这种行为有时被称为“上下文焦虑”。我们通过在 harness 中加入上下文重置来解决这个问题。但当我们在 Claude Opus 4.5 上使用同一个 harness 时,我们发现这种行为消失了。那些重置已经变成了累赘。

我们预期 harness 会继续演化。因此,我们构建了 Managed Agents:这是 Claude Platform 中的一项托管服务,可以代表你运行长周期 agent,并通过一小组接口工作,这些接口的设计目标是比任何特定实现都更持久,包括我们今天运行的那些实现。

构建 Managed Agents 意味着要解决计算领域的一个老问题:如何为“尚未被设想出来的程序”设计系统。几十年前,操作系统通过把硬件虚拟化为足够通用的抽象来解决这个问题,例如 process, file,这些抽象可以服务于当时还不存在的程序。抽象比硬件更持久。read() 命令并不关心它访问的是 20 世纪 70 年代的磁盘组,还是现代 SSD。上层抽象保持稳定,而底层实现可以自由变化。

Managed Agents 遵循同样的模式。我们虚拟化了 agent 的组成部分:session(记录发生过的一切的仅追加日志)、harness(调用 Claude 并把 Claude 的工具调用路由到相关基础设施的循环),以及 sandbox(Claude 可以在其中运行代码和编辑文件的执行环境)。这使得每个部分的实现都可以被替换,而不会干扰其他部分。我们对这些接口的形状有明确主张,但不对接口背后运行的东西抱有执念。

Managed Agents 组件示意图

我们一开始把所有 agent 组件都放进一个容器里,这意味着 session、agent harness 和 sandbox 共享同一个环境。这种方法有一些好处,包括文件编辑就是直接的系统调用,而且不需要设计服务边界。

但通过把所有东西耦合进一个容器,我们遇到了一个古老的基础设施问题:我们收养了一只 pet。在 pets-vs-cattle 的类比中,pet 是一个有名字、需要人工照料、你承受不起失去它的个体,而 cattle 是可以互换的。在我们的场景里,服务器变成了那只 pet;如果容器失败,session 就会丢失。如果容器没有响应,我们就不得不把它照料回健康状态。

照料容器意味着要调试没有响应、卡住的 session。我们唯一的窗口是 WebSocket 事件流,但它无法告诉我们故障发生在哪里,这意味着 harness 中的 bug、事件流中的丢包,或容器离线,表现出来都一样。为了弄清出了什么问题,工程师必须在容器里打开 shell,但因为那个容器通常也持有用户数据,这种方法本质上意味着我们缺乏调试能力。

第二个问题是,harness 假定 Claude 处理的任何东西都和它一起存在于容器中。当客户要求我们把 Claude 连接到他们的虚拟私有云时,他们要么必须把自己的网络和我们的网络做对等连接,要么必须在自己的环境里运行我们的 harness。当我们想把它连接到不同基础设施时,一个被烘焙进 harness 的假设就变成了问题。

我们最终得到的解决方案,是把我们认为的“大脑”(Claude 及其 harness)同时从“双手”(执行动作的 sandbox 和工具)以及“session”(session 事件日志)中解耦出来。每一部分都成为了一个接口,对其他部分做很少假设,并且每一部分都可以独立失败或被替换。

Harness 离开容器。 将大脑从双手中解耦,意味着 harness 不再生活在容器内部。它调用容器的方式,就像调用任何其他工具一样:execute(name, input) → string。容器变成了 cattle。如果容器死亡,harness 会把故障捕获为工具调用错误,并把它传回给 Claude。如果 Claude 决定重试,一个新容器可以用标准配方重新初始化:provision({resources})。我们不再需要把失败的容器照料回健康状态。

从 harness 故障中恢复。 Harness 也变成了 cattle。因为 session log 位于 harness 之外,harness 中没有任何东西需要在崩溃后幸存。当一个 harness 失败时,一个新的 harness 可以通过 wake(sessionId) 被重新启动,使用 getSession(id) 取回事件日志,并从最后一个事件继续。在 agent 循环期间,harness 会用 emitEvent(id, event) 写入 session,以便保留一份持久的事件记录。

大脑、双手与 session 解耦示意图

安全边界。 在耦合设计中,Claude 生成的任何不受信任代码,都和凭证一起运行在同一个容器里,所以一次提示注入只需要说服 Claude 读取自己的环境。一旦攻击者拿到这些 token,他们就可以启动新的、不受限制的 session,并把工作委派给它们。缩小作用域是一个显而易见的缓解措施,但这会编码一个关于 Claude 不能用受限 token 做什么的假设,而 Claude 正变得越来越聪明。结构性的修复,是确保 token 永远无法从 Claude 生成代码运行所在的 sandbox 中触达。

我们使用两种模式来确保这一点。认证可以和资源绑定在一起,也可以保存在 sandbox 之外的 vault 中。对于 Git,我们使用每个仓库的访问 token,在 sandbox 初始化期间克隆仓库,并把它接入本地 git remote。Git pushpull 可以在 sandbox 内部工作,而 agent 本身永远不处理 token。对于自定义工具,我们支持 MCP,并把 OAuth token 存储在安全 vault 中。Claude 通过专用代理调用 MCP 工具;这个代理接收一个与 session 关联的 token。然后,代理可以从 vault 中获取相应凭证,并调用外部服务。Harness 永远不会知道任何凭证。

Session 不是 Claude 的上下文窗口

长周期任务经常超过 Claude 上下文窗口的长度,而解决这个问题的标准方法都涉及关于保留什么的不可逆决定。我们在关于上下文工程的此前工作中探索过这些技术。例如,compaction 让 Claude 保存其上下文窗口的摘要,而 memory tool 让 Claude 把上下文写入文件,从而支持跨 session 学习。这可以和 context trimming 搭配使用,后者会选择性地移除一些 token,例如旧的工具结果或 thinking block。

但是,选择性保留或丢弃上下文的不可逆决定可能导致失败。很难知道未来轮次会需要哪些 token。如果消息被 compaction 步骤转换,harness 会从 Claude 的上下文窗口中移除被压缩的消息,而这些消息只有在被存储的情况下才可恢复。此前的工作探索过一种解决方法:把上下文存储为存在于上下文窗口之外的对象。例如,上下文可以是 REPL 中的一个对象,LLM 通过编写代码来过滤或切片它,从而以编程方式访问它。

Session 位于 Claude 上下文窗口之外的示意图

在 Managed Agents 中,session 提供了同样的好处,充当一个生活在 Claude 上下文窗口之外的上下文对象。但上下文不是存储在 sandbox 或 REPL 中,而是被持久地存储在 session log 中。接口 getEvents(), 允许大脑通过选择事件流中的位置切片来询问上下文。这个接口可以被灵活使用,允许大脑从上一次停止阅读的地方继续,在某个特定时刻之前倒回几个事件以查看前因,或者在某个特定动作之前重新阅读上下文。

任何取回的事件也可以在传入 Claude 的上下文窗口之前,由 harness 进行转换。这些转换可以是 harness 编码的任何东西,包括用于实现高 prompt cache 命中率的上下文组织和上下文工程。我们把 session 中可恢复的上下文存储,与 harness 中任意的上下文管理分离开来,因为我们无法预测未来模型会需要什么具体的上下文工程。接口把这种上下文管理推入 harness,并且只保证 session 是持久的、可供询问的。

许多大脑。 将大脑从双手中解耦,解决了我们最早期的客户抱怨之一。当团队希望 Claude 操作他们自己 VPC 中的资源时,唯一的路径是把他们的网络与我们的网络做对等连接,因为持有 harness 的容器假定每个资源都在它旁边。一旦 harness 不再位于容器中,这个假设就消失了。同样的改变也带来了性能收益。当我们最初把大脑放在容器里时,这意味着许多大脑需要同样多的容器。对每个大脑来说,只有在那个容器被预置之后,推理才能发生;每个 session 都要预先支付完整的容器设置成本。每个 session,即使是那些永远不会触碰 sandbox 的 session,也必须克隆仓库、启动进程、从我们的服务器获取待处理事件。

这段空耗时间体现为 time-to-first-token(TTFT),它衡量一个 session 在接受工作与产生第一个响应 token 之间等待了多久。TTFT 是用户最敏锐地感受到的延迟。

将大脑从双手中解耦,意味着容器只有在需要时才会由大脑通过工具调用 (execute(name, input) → string) 来预置。因此,一个不需要立刻使用容器的 session,就不必等待容器。只要编排层从 session log 中拉取到待处理事件,推理就可以开始。使用这种架构后,我们的 p50 TTFT 大约下降了 60%,p95 下降了超过 90%。扩展到许多大脑,只意味着启动许多无状态 harness,并且只在需要时把它们连接到双手。

许多双手。 我们还希望能够把每个大脑连接到许多双手。实际上,这意味着 Claude 必须推理许多执行环境,并决定把工作发送到哪里,这比在单个 shell 中操作是更困难的认知任务。我们一开始把大脑放在单个容器中,是因为早期模型无法胜任这一点。随着智能规模提升,单个容器反而变成了限制:当那个容器失败时,我们会失去大脑正在伸向的每一只手的状态。

将大脑从双手中解耦,使每一只手都成为一个工具,execute(name, input) → string:输入一个名字和输入,返回一个字符串。这个接口支持任何自定义工具、任何 MCP server,以及我们自己的工具。Harness 不知道 sandbox 是一个容器、一部手机,还是一个 Pokémon emulator。而且因为没有任何一只手与任何一个大脑耦合,大脑可以把手传递给彼此。

多个大脑与多个双手的示意图

我们面对的挑战是一个老问题:如何为“尚未被设想出来的程序”设计系统。操作系统通过把硬件虚拟化为足够通用、可以服务于当时还不存在的程序的抽象,已经持续了几十年。借助 Managed Agents,我们的目标是设计一个能够容纳未来围绕 Claude 出现的 harness、sandbox 或其他组件的系统。

Managed Agents 是同样精神下的 meta-harness,它不对 Claude 未来会需要的具体 harness 抱有观点。相反,它是一个拥有通用接口的系统,允许许多不同的 harness。例如,Claude Code 是一个优秀的 harness,我们在各种任务中广泛使用它。我们也已经展示过,面向特定任务的 agent harness 在狭窄领域表现出色。Managed Agents 可以容纳其中任何一种,并随着 Claude 的智能水平随时间匹配它们。

Meta-harness 设计意味着要对 Claude 周围的接口抱有观点:我们预期 Claude 会需要操纵状态(session)的能力,以及执行计算(sandbox)的能力。我们也预期 Claude 会需要扩展到许多大脑和许多双手的能力。我们设计这些接口,使它们能够在很长的时间跨度上可靠且安全地运行。但我们不对 Claude 需要多少大脑或双手,也不对它们位于何处做任何假设。

作者:Lance Martin、Gabe Cemaj 和 Michael Cohen。感谢 Nodir Turakulov 和 Jeremy Fox 围绕这些主题提供的有益讨论。特别感谢 Agents API 团队和 Jake Eaton 的贡献。