3327 字
约 11 分钟
9
长运行 Agent:为什么跑不了长任务,以及三种交接方案——双角色、脑手分离与 Session 不等于 Context

开场:长运行 Agent 的困境

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

一句话速览:一口气做太多、做完就收工,两种典型失败模式。 核心洞察是:长任务的核心挑战是交接,动手做反而不难。

一、问题背景与两种失败模式

问题背景:给 Agent 的高层提示

给 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 看到已经实现了一些功能,就认为项目已经基本完成:

  • 声明项目完成,实际上只完成了核心功能的 30%
  • 缺乏任务清单,Agent 不知道还差什么没做
  • 没有验证机制,以为做完了但没有端到端测试证明

类比:失忆的换班工程师

想象一个软件项目,每个工程师都只上一个班次。换班时完全失忆:不知道前一个人做了什么、为什么做、下一步该做什么。每个人坐到电脑前,打开一堆半成品代码,只能从零开始理解。这就是没有交接机制的长运行 Agent 的真实状态。

核心洞察:Agent 并不缺乏能力,它缺的是在上下文断裂时维持连续性的机制。解决交接问题,才能解决长运行问题。

二、方案一:Initializer + Coding 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 能持续推进的最基本保障,一点也不花哨。

三、方案二:Managed Agent(脑手分离)

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

类比:操作系统的虚拟化

操作系统虚拟化硬件:当你调用 read() 时,你不关心底层是 SSD、HDD 还是网络磁盘,操作系统帮你抽象了硬件细节。Managed Agent 做同样的事:虚拟化 Agent 的各个组件,让脑不关心手是哪个容器,让手不关心脑用的是哪个模型。

三个核心组件

  • Session(会话日志): Append-only 的事件流,持久化存储。记录所有发生过的事情:用户输入、工具调用、模型输出。
  • Harness(脑 / 大脑): 调用 Claude 并路由工具调用的循环。负责思考:决定下一步做什么、如何组织上下文。
  • Sandbox(手 / 沙箱): 执行代码和编辑文件的容器环境。负责执行:运行命令、写文件、与外部系统交互。

为什么要拆开:宠物 vs 牛群

旧方案(宠物): Session + Harness + Sandbox 都在同一个容器里。容器挂了 = 会话丢失 = 无法调试 = 任务彻底失败。就像养了一只宠物,挂了就完了,无法替代。

新方案(牛群): 每个组件独立部署,互不依赖。容器挂了 = 工具调用失败 = Claude 决定重试 = 新建容器继续。就像牧场的牛,挂了重来一个,系统照常运行。

脑离开容器后的好处

关键改进: 容器变成工具调用: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(各自只是无状态 Harness,按需启动)+ 多个 Sandbox(各自是独立工具,可以传递给不同 Brain)。这意味着你可以让一个 Brain 同时操控多个 Sandbox(并行执行),也可以让同一个 Sandbox 在不同的 Brain 之间传递(接力执行)。组件之间完全解耦。

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

四、方案三:Session 不等于 Context Window

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

两个容易混淆的概念

Context Window(临时 / 有限): Claude 当前能看到的 Token。就像人的工作记忆:容量有限,过了就忘。每次对话开始时构建,用完就丢。当前推理所需的精选内容。

Session(永久 / 可回溯): 所有发生过的事件的持久日志。就像完整的录像:从头到尾什么都记着,永远可以倒回去看。Append-only,只增不减。所有原始事件的完整记录。

为什么必须分离

上下文管理的不可逆性:

  1. Compaction(压缩)和 Trimming(裁剪)都是不可逆操作,一旦压缩,原始细节就丢了
  2. 压缩时很难知道未来哪些 Token 重要,今天看似无关的信息,可能是明天关键决策的依据
  3. 如果压缩后原始信息丢了,就永远回不来了,这是信息论的基本约束

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

Session 作为持久上下文对象

Session 提供一个类似数据库的查询接口(getEvents()):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 的灵活性

  • 优化 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。这样即使上下文压缩了、模型换了,历史永远不会丢。

结语

三种方案从不同层面解决长运行问题:双角色(Initializer + Coding Agent) 解决交接问题,一次一个功能、增量提交、进度文件是最基本的保障;脑手分离(Managed Agent) 解决可靠性问题,让组件可以独立失败和独立替换;Session 与 Context Window 分离 解决记忆问题,把所有原始事件存进 Session,让 Harness 按需组装 Context。

长任务的核心挑战是交接,动手做反而不难。 好的交接机制 = 好的长运行 Agent。

长运行 Agent:为什么跑不了长任务,以及三种交接方案——双角色、脑手分离与 Session 不等于 Context
https://clxhxhhr.top/posts/4631/
作者
clxstart
发布于
2026-10-11
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。