长运行 Agent

为什么 Agent 跑不了长任务

让 Agent 构建一个完整 Web 应用看似简单,但现实中充满了交接失败和上下文断裂的陷阱。

问题背景
给 Agent 的高层提示
"Build a clone of claude.ai"

这不是一个小任务。一个完整的聊天应用需要认证系统、对话管理、流式输出、文件上传、Markdown 渲染、多轮历史…加起来可能有 200+ 个独立功能点。让 Agent 从零到一完成这样的项目,会遇到什么问题?

失败模式
One-shotting:一口气做太多
最常见的失败模式

Agent 试图在一次会话中完成所有功能,结果:

  • 上下文窗口在实现到一半时被用完
  • 下一个 Agent 接手时,面对半成品代码,只能猜测前任做了什么
  • 大量时间浪费在让基本功能重新跑起来,新功能反而没时间做
  • 即使有 Compaction(上下文压缩)也不够,压缩后的指令不够清晰,新 Agent 依然迷路
One-shotting 的典型时间线
Agent 1 开始写
写了 50% 功能
上下文用完
Agent 2 接手
花时间修复半成品
上下文又用完
每次接手都忙于修复,推进陷入停滞
Premature Completion:过早宣布完成
Agent 觉得「差不多了」

Agent 看到已经实现了一些功能,就认为项目已经基本完成:

  • 声明项目完成,实际上只完成了核心功能的 30%
  • 缺乏任务清单,Agent 不知道还差什么没做
  • 没有验证机制,以为做完了但没有端到端测试证明
模拟演示:看 Agent 如何崩溃
上下文窗口
0%
类比:失忆的换班工程师
想象一个软件项目
每个工程师都只上一个班次。换班时完全失忆:不知道前一个人做了什么、为什么做、下一步该做什么。每个人坐到电脑前,打开一堆半成品代码,只能从零开始理解。这就是没有交接机制的长运行 Agent 的真实状态。
核心洞察
长任务的核心挑战是交接,动手做反而不难。Agent 并不缺乏能力,它缺的是在上下文断裂时维持连续性的机制。解决交接问题,才能解决长运行问题。
长运行 Agent

Initializer + Coding Agent

核心思路:把一个 Agent 拆成两个角色,一个负责规划,一个负责执行。每次交接都留下干净的状态。

双角色解决方案
两个 Agent,明确分工
只跑第一轮
Initializer Agent
负责从零到有:搭环境、定计划、做第一次提交
  • 创建 init.sh 脚本搭建开发环境
  • claude-progress.txt 进度文件
  • 把用户的高级提示展开成详细的功能清单(JSON 格式)
  • 做第一次 git commit,确保仓库状态干净
每轮都跑
Coding Agent
负责从有到多:逐个功能实现,持续推进
  • 读 progress 文件,了解当前状态
  • 一次只做一个功能
  • 完成后更新 progress 文件
  • git commit 并写清楚做了什么
功能清单的设计

一种验证有效的做法是用 JSON 格式来记录功能清单,Markdown 不适合这个用途。原因是:模型更不容易错误修改结构化的 JSON,而 Markdown 容易被模型顺手重写。

// claude-progress.txt 中的功能清单 { "features": [ { "category": "authentication", "description": "Email/password login with session management", "steps": [ "Create login form component", "Implement auth API endpoint", "Add session cookie handling", "Write end-to-end test" ], "passes": false }, { "category": "chat", "description": "Real-time streaming chat with Claude API", "steps": ["..."], "passes": false } ] }
Prompt 中使用强措辞:明确告诉 Agent「不允许删除或修改已有的测试内容」。否则 Agent 会为了让测试通过而降低测试标准。
增量进度:一次只做一个功能
为什么"一次一个"是关键
  • 1 每次完成一个功能后,代码处于可合并状态:没有半成品、没有语法错误
  • 2 Git commit 提供回滚点:如果下一轮搞坏了什么,可以回到上一个干净状态
  • 3 Progress 文件提供上下文:新 Agent 不用猜做到哪了,直接读文件就知道
  • 4 上下文窗口不会溢出:每轮只需关注一个功能的上下文,不会积累到爆
测试验证

Agent 容易以为做完了却没有端到端验证。它说「实现了登录功能」,但实际上按钮根本点不动。解决方案:

明确要求 Agent 用浏览器自动化做端到端测试。
要真正打开浏览器、点击按钮、验证结果,光有单元测试还不够。让 Agent 用 Puppeteer / Playwright 写 E2E 测试,作为功能是否真正"passes"的判定标准。
最终效果
200+ 功能的 claude.ai 克隆成功构建
通过 Initializer + Coding Agent 的双角色方案,成功让 Agent 自主构建了一个包含 200+ 功能的完整 Web 应用。每个功能都有对应的 E2E 测试,代码始终保持可合并状态。
好的交接机制 = 好的长运行 Agent。进度文件、功能清单、增量提交,这些是让 Agent 能持续推进的最基本保障,一点也不花哨。
长运行 Agent

Managed Agent:脑手分离

核心洞察:把 Agent 的思考和执行拆成独立组件,让系统在任何部分出错时都能优雅恢复。

类比:操作系统的虚拟化
操作系统虚拟化硬件
当你调用 read() 时,你不关心底层是 SSD、HDD 还是网络磁盘,操作系统帮你抽象了硬件细节。
Managed Agent 做同样的事:虚拟化 Agent 的各个组件,让脑不关心手是哪个容器,让手不关心脑用的是哪个模型。
三个核心组件
Session
会话日志
Append-only 的事件流,持久化存储。记录所有发生过的事情:用户输入、工具调用、模型输出。
Harness
脑 / 大脑
调用 Claude 并路由工具调用的循环。负责思考:决定下一步做什么、如何组织上下文。
Sandbox
手 / 沙箱
执行代码和编辑文件的容器环境。负责执行:运行命令、写文件、与外部系统交互。
为什么要拆开:宠物 vs 牛群
架构演进
旧方案(宠物)
Session + Harness + Sandbox 都在同一个容器里。

容器挂了 = 会话丢失 = 无法调试 = 任务彻底失败。

就像养了一只宠物,挂了就完了,无法替代。
新方案(牛群)
每个组件独立部署,互不依赖。

容器挂了 = 工具调用失败 = Claude 决定重试 = 新建容器继续。

就像牧场的牛,挂了重来一个,系统照常运行。
动手试试:故障恢复模拟
Harness
Brain(脑)
Sandbox
Hand(手)
Event Log
Session(会话)
点击下方按钮,观察不同故障场景下系统的恢复行为。
脑离开容器后的好处
关键改进
  • 容器变成工具调用execute(name, input) -> string,对 Harness 来说就是一个普通函数
  • 容器挂了 = 工具调用返回错误 = Claude 自行决定是否重试 = 自动新建容器继续工作
  • Harness 可以在容器启动前就开始处理,不需要等容器 ready
-60%
TTFT p50 降低
-90%+
TTFT p95 降低

TTFT = Time to First Token(首 Token 响应时间)

安全的结构性解决
安全问题的架构级解决
  • 旧方案:Agent 生成的代码和 API 密钥在同一个容器里,Prompt Injection 可以直接偷密钥
  • Git Token:在克隆仓库时注入容器环境变量,沙箱内可以用,但 Agent 看不到 token 值
  • MCP OAuth Token:存在外部 vault 里,通过代理转发 MCP 调用,沙箱内无法直接访问 token
多脑多手
组件可以自由组合
Brain A
Brain B
Brain C
多个 Brain:各自只是无状态 Harness,按需启动
Sandbox 1
Sandbox 2
Sandbox 3
Sandbox 4
多个 Hand:各自是独立工具,可以传递给不同 Brain

这意味着你可以让一个 Brain 同时操控多个 Sandbox(并行执行),也可以让同一个 Sandbox 在不同的 Brain 之间传递(接力执行)。组件之间完全解耦

好的架构让组件可以独立失败和独立替换。脑手分离不只是性能优化,它从根本上改变了系统的可靠性模型:从「一个宠物挂了全完」到「任何部分都能重建」。
长运行 Agent

Session 不等于 Context Window

两个最容易混淆的概念:Claude 当前能看到的,和所有发生过的事。它们必须分离。

两个容易混淆的概念
临时 / 有限
Context Window
Claude 当前能看到的 Token。就像人的工作记忆:容量有限,过了就忘。每次对话开始时构建,用完就丢。
当前推理所需的精选内容
永久 / 可回溯
Session
所有发生过的事件的持久日志。就像完整的录像:从头到尾什么都记着,永远可以倒回去看。Append-only,只增不减。
所有原始事件的完整记录
为什么必须分离
上下文管理的不可逆性
  • 1 Compaction(压缩)和 Trimming(裁剪)都是不可逆操作,一旦压缩,原始细节就丢了
  • 2 压缩时很难知道未来哪些 Token 重要,今天看似无关的信息,可能是明天关键决策的依据
  • 3 如果压缩后原始信息丢了,就永远回不来了,这是信息论的基本约束

所以正确的做法是:原始事件永久保存在 Session 里,Context Window 只是从 Session 中临时取景的一个视角。丢了 Context 没关系,因为 Session 还在,随时可以重建。

Session 作为持久上下文对象
getEvents() 接口
Session 提供一个类似数据库的查询接口:Brain 可以按需读取任意范围的事件,摆脱固定上下文窗口的限制。
// Brain 可以灵活查询 Session const recentEvents = session.getEvents({ from: position - 100, // 从某个位置开始读 to: position // 读到当前位置 }); // 可以倒回到某个时间点 const beforeDecision = session.getEvents({ from: decisionPoint - 20, to: decisionPoint + 5 }); // 可以过滤特定类型的事件 const toolCalls = session.getEvents({ filter: "tool_use" });

这就像 REPL 中的对象一样,LLM 可以写代码来查询和过滤 Session 中的事件。Brain 从任意位置开始读、倒回到某个时间点、重读某个决策前后的上下文。

Harness 的灵活性
从 Session 取出的事件可以做任意变换
  • 优化 Prompt Cache 命中率:保持前缀稳定,减少重复计算的 Token 开销
  • 做上下文工程:根据当前任务类型,选择性地放入最相关的历史事件
  • Harness 是可换的:不同模型可能需要不同的上下文策略,换 Harness 不影响 Session
核心对比
维度 Context Window Session
持久性 临时,用完即丢 永久,持久化存储
内容 精选后的 Token 所有原始事件
操作 只读(对 Claude 来说) 可追加(append-only)
大小 有限(模型上限) 无限制
用途 当前推理 历史回溯、状态恢复
硬件类比
内存 (RAM)
Context Window
速度快、容量小、断电即失。CPU 正在用的数据必须在内存里,但内存不是用来长期存储的。
硬盘 (Disk)
Session
速度慢、容量大、断电不失。所有数据最终都存在硬盘上,需要时再加载到内存。

你不会把所有文件同时加载到内存里,那样内存会爆。同样的道理,你不应该把所有历史事件都塞进 Context Window,那样 Token 会爆。正确的做法是按需加载:Session 存全量,Context 取子集。

Session 是 Agent 的硬盘,Context Window 是内存,不要把内存当硬盘用。把所有原始事件存进 Session,让 Harness 按需组装 Context。这样即使上下文压缩了、模型换了,历史永远不会丢。
1 / 4