跳到主要内容

原文:02-managed-agents.md 来源:https://www.anthropic.com/engineering/managed-agents

用 Claude Agent SDK 构建 Agent:解耦"大脑"与"双手"

发布于 2026 年 4 月 8 日

Harness(执行框架)会编码一些假设,而这些假设会随着模型变强而过时。Managed Agents 是我们托管的长任务 Agent 服务,围绕着即使 Harness 变化也能保持稳定的接口构建。

工程博客的一个持续话题是:如何构建有效 Agent,以及如何为长任务设计 Harness。一个反复出现的线索是:Harness 编码了关于"Claude 不能独立做什么"的假设。但这些假设需要不断被审视——因为模型在持续进步,假设很容易过时。

举一个例子:在之前的工作中我们发现 Claude Sonnet 4.5 会在感知到接近上下文上限时过早收尾任务,这种行为有时被称为"上下文焦虑(context anxiety)"。我们通过在 Harness 中加入上下文重置(context reset)来解决。但当我们在 Claude Opus 4.5 上使用同一套 Harness 时,发现这个行为消失了——重置变成了无用的负担。

我们预计 Harness 会持续演进。所以我们构建了 Managed Agents:Claude Platform 上的一项托管服务,通过一组小而稳定的接口为你运行长任务 Agent。这些接口被设计为能跨越任何具体实现而长期存在——包括我们今天运行的实现。

构建 Managed Agents 意味着要解决计算领域的一个老问题:"如何为还未被构想出来的程序设计系统?" 几十年前,操作系统通过将硬件虚拟化为抽象(如 进程文件)来解决——这些抽象足够通用,能容纳还不存在的程序。抽象比硬件更长寿。read() 命令并不关心它访问的是 1970 年代的磁盘还是现代 SSD。底层实现自由变化,上层抽象保持稳定。

Managed Agents 遵循同样的模式。我们将 Agent 的组件虚拟化为:

  • session(会话):所有发生事件的只追加日志
  • harness(执行框架):调用 Claude 并将其工具调用路由到对应基础设施的循环
  • sandbox(沙箱):Claude 可运行代码、编辑文件的执行环境

这让每个组件的实现都可以替换,而不打扰其他组件。我们对这些接口的形态有自己的主张,但对接口背后运行什么不做规定。


不要养"宠物"

我们最初将所有 Agent 组件放进单个容器,session、harness 和 sandbox 共享一个环境。这种方式有它的好处——文件编辑直接通过系统调用完成,也无需设计服务边界。

但把所有东西耦合进单容器之后,我们撞上了一个老式的基础设施问题:我们养了个宠物(pet)。在"宠物 vs. 牛群(pets vs. cattle)"的比喻中,宠物是个有名字的、需要精心照料的、不能丢的个体;牛群是可互换的。在我们的场景里,服务器变成了那只宠物——容器一挂,会话就丢了;容器一卡,我们就得想办法救它。

救容器意味着要调试卡死的会话。但我们唯一的观察窗是 WebSocket 事件流,它无法告诉我们故障发生在哪里——这意味着 Harness 里的 bug、事件流里的丢包、容器掉线,看起来都是同一回事。要弄清楚问题,工程师必须打开容器内的 shell;但因为容器同时持有用户数据,这种方式实际上等于失去了调试能力。

第二个问题是:Harness 假设 Claude 操作的所有东西都和它住在同一个容器里。当客户希望把 Claude 接到他们自己的私有云(VPC)时,他们要么得把网络与我们的对等连接,要么得在自己的环境里运行我们的 Harness。Harness 中的隐含假设,在我们想把它接到不同基础设施时变成了障碍。


解耦"大脑"与"双手"

我们最终的解法是把"大脑"(Claude 和它的 Harness)从"双手"(执行操作的 sandbox 和工具)以及"会话"(事件日志)中解耦出来。每一部分都成为对其他部分做最少假设的接口,可以独立失败或被替换。

Harness 离开容器

把大脑从双手中解耦,意味着 Harness 不再住在容器里。它像调用任何其他工具一样调用容器:execute(name, input) → string。容器变成了"牛群"。如果容器死了,Harness 把这个失败当作一次工具调用错误抓住,传回给 Claude。如果 Claude 决定重试,新容器可以用标准菜谱 provision({resources}) 重新初始化。我们再也不用救濒死的容器了。

从 Harness 故障中恢复

Harness 也变成了"牛群"。因为会话日志在 Harness 之外,Harness 内部没有任何东西需要在崩溃中存活。一个 Harness 挂了,可以用 wake(sessionId) 重启一个新的,用 getSession(id) 拿回事件日志,从最后一个事件继续。Agent 循环过程中,Harness 用 emitEvent(id, event) 写入会话,保留事件的持久记录。

安全边界

在耦合设计里,Claude 生成的任何不可信代码都和凭证(credentials)跑在同一个容器里——Prompt Injection 只需说服 Claude 读取自己的环境就够了。一旦攻击者拿到这些 token,就能 spawn 全新的、不受限的会话并把工作委托过去。狭窄授权范围(narrow scoping)是显而易见的缓解措施,但这种做法编码了一个关于"Claude 用受限 token 不能做什么"的假设——而 Claude 越来越聪明。结构性的修复方式是确保凭证从 Claude 生成代码运行的 sandbox 里完全不可达。

我们用了两种模式来保证这点:

  • 凭证可以与资源捆绑,或者保存在 sandbox 之外的 vault 里
  • 对于 Git,我们用每个仓库的访问 token 在 sandbox 初始化时克隆仓库,并把它接到本地 git remote。git pushgit pull 在 sandbox 内部工作,Agent 自己从未接触过 token
  • 对于自定义工具,我们支持 MCP,并把 OAuth token 存在安全 vault 里。Claude 通过专用代理调用 MCP 工具——代理拿到与会话关联的 token,从 vault 取出对应凭证,然后调用外部服务。Harness 永远不知道任何凭证。

Session ≠ Claude 的上下文窗口

长任务往往超出 Claude 的上下文窗口长度,处理这个问题的标准方法都涉及不可逆的"保留什么"决策。我们之前的上下文工程文章探讨过这些技术:

  • 压缩(compaction):让 Claude 保存上下文窗口的摘要
  • memory tool:让 Claude 把上下文写入文件,跨会话学习
  • 上下文修剪(context trimming):选择性移除 token,例如旧的工具结果或思考块

选择性保留或丢弃上下文的不可逆决策可能导致失败。很难知道未来的轮次会需要哪些 token。如果消息被压缩步骤变换过,Harness 会从 Claude 的上下文窗口移除被压缩的消息,而这些消息只有在被存储下来时才可恢复。先前研究探索了把上下文存为上下文窗口之外的对象的方法。例如,上下文可以是 REPL 中的一个对象,LLM 通过写代码来过滤或切片访问它。

在 Managed Agents 中,session 提供了同样的好处——它充当一个住在 Claude 上下文窗口之外的上下文对象。但上下文不是存在 sandbox 或 REPL 中,而是持久化存在 session log 里。接口 getEvents() 让大脑通过选择事件流的位置切片来"询问"上下文。这个接口可以灵活使用:从上次停下读的位置接着读、回到某个时刻前几个事件去看前因、或重读某个动作前的上下文。

任何获取的事件,在传给 Claude 上下文窗口前还可以被 Harness 变换。这些变换可以是 Harness 编码的任意逻辑,包括为提高 prompt cache 命中率做的上下文组织、上下文工程等等。我们把"会话中的可恢复上下文存储"和"Harness 中的任意上下文管理"分开关注,因为我们无法预测未来模型需要什么具体的上下文工程。接口把上下文管理推给 Harness,只保证 session 是持久且可询问的。


多大脑、多双手

多大脑

把大脑从双手中解耦解决了我们最早的客户抱怨之一。当团队希望 Claude 操作他们 VPC 里的资源时,唯一的路径是把网络对等到我们这边,因为持有 Harness 的容器假设每个资源都在它隔壁。一旦 Harness 不在容器里了,这个假设就消失了。

同一个改动还带来了性能收益。最初把大脑放在容器里,意味着多个大脑就需要多个容器。每个大脑都得等容器配置完才能开始推理;每个会话都要付出完整的容器启动成本——即使有些会话根本不会碰 sandbox,也得克隆仓库、启动进程、从我们的服务器拉取待处理事件。

这段死时间体现在 TTFT(time-to-first-token) 上——会话从接受工作到产出第一个响应 token 之间等了多久。TTFT 是用户最直接感受到的延迟。

把大脑从双手中解耦后,容器只在大脑通过工具调用 execute(name, input) → string 真正需要时才被配置。一个不立即需要容器的会话不必等它。一旦编排层从 session log 拉到待处理事件,推理就能开始。在这个架构下,我们的 p50 TTFT 下降约 60%,p95 下降超过 90%。扩展到多个大脑就是启动多个无状态的 Harness,仅在需要时再连接到双手。

多双手

我们也希望每个大脑能连接到多双手。实践中,这意味着 Claude 必须推理多个执行环境,并决定把工作发送到哪里——这比在单个 shell 里操作的认知任务难。我们最初把大脑放在单容器是因为早期模型还做不到这点。随着智能扩展,单容器反而成了限制:那个容器一挂,所有大脑伸进去的双手的状态都丢了。

把大脑从双手中解耦后,每只手都成了一个工具——execute(name, input) → string:传入 name 和 input,返回 string。这个接口支持任何自定义工具、任何 MCP 服务器、以及我们自己的工具。Harness 不知道 sandbox 是容器、手机、还是 Pokémon 模拟器。而且因为没有手与任何大脑耦合,大脑之间可以互相传递双手。


总结

我们面对的是个老问题:如何为"还未被构想出来的程序"设计系统?操作系统能存活几十年,是因为它把硬件虚拟化为足够通用的抽象,能容纳还不存在的程序。Managed Agents 也是按这种精神设计——一个能容纳未来 Harness、sandbox 或其他 Claude 周边组件的系统。

Managed Agents 是一种 meta-harness:对 Claude 未来需要的具体 Harness 不持立场,但提供允许多种不同 Harness 共存的通用接口。例如 Claude Code 是我们在很多任务中广泛使用的优秀 Harness;任务专用 Agent Harness 也在窄领域表现出色。Managed Agents 可以容纳任何这些选择,并随时间匹配 Claude 的智能水平。

设计 meta-harness 意味着对 Claude 周围的接口要有自己的主张:

  • 我们预期 Claude 需要操纵状态(session)和执行计算(sandbox)
  • 我们预期 Claude 需要扩展到多大脑、多双手

我们设计这些接口让它们在长时间维度上可靠且安全地运行。但对于 Claude 需要多少个或位于何处的大脑或双手,我们不做任何假设。