跳到主要内容

为什么你的智能体控制架构应该是无状态的:在生产环境中实现大脑与双手的解耦

· 阅读需 10 分钟
Tian Pan
Software Engineer

大多数构建 AI agent 的团队都将 harness —— 处理工具路由、上下文管理和推理循环的支架 —— 视为绑定到单个容器的长寿命、有状态进程。当容器出现故障时,会话就会终止。当你需要更换更好的模型时,必须重新启动所有内容。当你想要水平扩展时,会遇到瓶颈:每个 harness 实例对其自身状态了解过多,导致无法互换。

解决方案不是更智能的 harness,而是一个无状态的 harness。

在生产环境中稳定运行数月的 Agent 都有一个共同的结构模式:它们将“思考”与“行动”分离,并将所有持久状态放在两者之外。harness 应该像无状态的 HTTP 处理器一样可替换。运行代码的沙箱应该对其服务的 Agent 会话一无所知。而会话日志 —— 发生的每个事件的记录 —— 应该比这两者的寿命都长。

这听起来像是显而易见的工程卫生习惯。但在实践中,起初几乎没有人这样做。

使 Agent 变得脆弱的紧耦合

当你构建第一个 Agent 时,自然的结构是单体式的:推理循环、工具执行和会话状态都存在于同一个进程中。这对于短寿命任务来说效果很好,但对于任何长期运行的任务,它都会严重崩溃。

故障表现如下:一个 Agent 在执行一项复杂的编码任务,进行了 40 分钟。运行 harness 的容器由于常规部署而重新启动。会话消失了。Agent 无法恢复,因为会话状态仅存在于内存中。唯一的选择是重新开始 —— 这意味着要重新运行昂贵的早期步骤,并希望 Agent 能收敛到相同的中间状态。

更深层的问题是 harness 编码了关于模型能力的假设。随着模型的改进,团队发现自己需要更新 harness 的行为 —— 包含多少上下文、哪些工具调用需要批处理、如何处理错误 —— 但 harness 被固定在运行中的系统中。更改它可能会破坏活跃的会话。

这就是应用到 AI 基础设施上的“宠物 vs 耕牛 (pets vs. cattle)”问题。宠物是命名的、需要单独照料的服务器,你无法承受失去它的代价。而耕牛是可以互换和替换的。传统的 Agent harness 是宠物,而生产系统需要的是耕牛。

虚拟化模式

解决方案遵循从操作系统借鉴的设计原则:将组件虚拟化以允许独立替换。

需要分离的三件事:

会话 (The session) 是一个只增 (append-only) 的事件日志,它存在于 harness 之外,也存在于沙箱之外。每条用户消息、模型响应、工具调用、工具结果和错误都会被记录为一个结构化事件。会话是唯一的真理来源。如果 harness 重新启动,它会获取会话日志并从上次中断的地方继续。如果你想用不同的模型回放会话,你也可以做到 —— 事件日志与模型无关。

Harness 是推理循环。它从会话中获取待处理事件,构建一个工作上下文窗口(选择哪些事件相关以及如何呈现它们),调用模型,并将工具调用路由回会话。至关重要的一点是,harness 自身不持有持久状态。任何 harness 实例都可以处理任何会话。这使得水平扩展变得微不足道:添加更多 harness 实例,它们就可以通过轮询方式为会话提供服务。

沙箱 (The sandbox) 是 Agent 的工具调用实际运行的执行环境 —— 包括文件操作、代码执行、shell 命令。沙箱与 harness 完全隔离。它不知道是哪个模型在调用它。它接收工具调用 (execute(name, input) → string) 并返回结果。这就是整个接口。

这种分离产生了一个有用的特性:组件独立失败。如果沙箱崩溃,harness 可以启动一个新的沙箱并从最后记录的事件恢复。如果你想升级 harness 以处理新的工具类型,活跃会话不会受到影响。如果你需要切换模型,你只需更换 harness,会话历史记录依然存在。

这对延迟有何影响

这种解耦对性能的影响比看起来更大,它源于一种只有在 harness 和沙箱分离时才可能实现的特定优化。

在耦合架构中,harness 在沙箱(容器)配置 (provisioned) 之后才开始推理。即使有预热池,容器配置也可能需要几秒钟。这增加了每个会话启动的延迟 —— 并且在长尾分布(p95、p99)中,当配置缓慢且队列很长时,影响会非常严重。

当 harness 和沙箱解耦时,只要 harness 从会话日志中拉取到待处理事件,推理就可以开始。沙箱配置是按需发生的,只有当模型实际调用了需要沙箱的工具时才会进行。一个只需要从内存读取或查询快速 API 的会话根本不需要配置沙箱。

结果是首 token 延迟 (time-to-first-token) 的显著降低 —— 中位数降低了约 60%,而在 95 分位值则降低了 90% 以上。对于用户正在等待首次响应的交互式 Agent 场景,这种差异是显而易见的。

作为查询问题的上下文管理

会话外部化一个被低估的好处是它对上下文管理的作用。

在耦合架构中,上下文管理是一种破坏性操作。你决定哪些事件包含在上下文窗口中,哪些被丢弃,而这个决定是永久性的 —— 你排除掉的事件不会存在于其他任何地方。这意味着你正在对稍后会话中哪些内容重要进行不可逆的押注。

当会话是一个外部事件日志时,上下文管理就变成了一个查询问题。治理程序(harness)获取会话的一个视图 —— 可以是按位置切片、按事件类型过滤、总结或逐字记录 —— 而无需修改底层日志。如果后面的轮次需要会话早期的信息,治理程序可以获取它。原始事件始终都在。

这将存储模式与提示词格式解耦。你可以尝试不同的上下文压缩策略,而无需触动事件日志。你可以更改事件渲染成提示词的方式,而不会丢失任何历史记录。你可以编写回溯分析会话日志的工具,而无需担心治理程序当时决定包含什么。

实际上,这意味着你可以演进你的上下文工程方法 —— 这通常是生产级 Agent 中迭代最频繁的部分之一 —— 而不受你决定存储什么的约束。

执行边界的安全

沙箱隔离的安全影响超出了可靠性范畴。

当治理程序和沙箱处于同一个进程时,Agent 使用的凭据(OAuth 令牌、API 密钥、git 访问令牌)必然与 Agent 生成并执行的代码处于同一个内存空间。这创造了一个攻击面:嵌入在工具输出或网页内容中的恶意指令 —— 即提示词注入 —— 可能通过指示模型编写读取内存或环境变量并将其发送到某处的代码来窃取这些凭据。

当沙箱被隔离且仅通过狭窄的工具接口通信时,攻击面会显著缩小。凭据永远不会到达执行环境。相反,它们存储在治理程序通过代理访问的安全保险库(vault)中。代理处理身份验证,并仅在网络边界针对特定的出站请求注入凭据。沙箱永远看不到它们。

对于 git 操作,这意味着访问令牌在仓库初始化期间(沙箱存在之前)打包,而不是作为环境变量传递。对于 API 调用,代理拦截来自沙箱的请求,并根据目的地附加适当的凭据,而 Agent 无需知道实际的令牌值。

这种模式并没有消除提示词注入风险 —— 受损的 Agent 仍然可以在其范围内采取有害操作 —— 但它防止了凭据窃取这一攻击后果,而这通常是价值最高的目标。

面向可替代性构建

采用这种架构更深层的原因是应对模型能力变化的韧性。

写于 2023 年的 Agent 治理程序编码了在当时合理的假设:模型需要显式的状态跟踪,因为它们无法在多个轮次中保持连贯的上下文;工具调用必须以特定格式进行,因为模型不能可靠地遵循自由格式的指令;错误恢复必须编写脚本,因为模型无法对失败进行推理。对于现在的模型,许多假设已经过时,并且对于未来的模型,它们会以不同的方式失效。

固化在有状态、长时间运行进程中的治理程序很难在不中断会话的情况下进行更新。无状态治理程序只是一个函数:输入事件,输出模型调用,记录结果。你可以在不触动会话日志的情况下替换该函数。你可以并行运行两个治理程序实现 —— 将一些会话路由到新的实现,将其他会话留在旧的实现上 —— 并直接比较行为。

这是操作系统抽象提供的核心属性:无论读取的是 1970 年代的磁盘组还是现代的 SSD,read() 系统调用都具有相同的接口。抽象比底层的实现更持久。一个设计良好的治理程序接口可以跨越几代底层模型能力。

分离这些组件的架构工作是实实在在的 —— 它需要重新思考状态如何在系统中流动,并投入资源在外部会话存储上。但回报是一个能够随模型改进而演进的系统,而无需你在每次模型能力发生变化时都从头开始构建。

首先构建什么

如果你正在启动一个新的 Agent 系统,这种模式的最小可行实现是:

  • 在处理每个事件之前,将其持久化到外部存储(甚至是数据库表也可以)
  • 使治理程序成为一个纯函数,它接收会话 ID、获取事件、运行推理并追加结果 —— 且在调用之间不保留任何内存状态
  • 在通过狭窄接口通信的独立进程中运行工具执行,并且永远不要将凭据直接传递给该进程

你不需要在第一天就做到完美的沙箱隔离。但尽早将会话外部化可以防止因为重启而丢失一切的这类故障 —— 并且它让架构的其余部分更容易演进。

随着模型能力的提升,硬编码了当前模型假设的治理程序将成为你最大的债务。请为了可替代性而构建它。

References:Let's stay in touch and Follow me for more thoughts and updates