返回文章
NO.
034
DATE
READ
~5 min
KIND
笔记
STATUS
原文

TAGS: AI 协作

我不再让一个 Agent 直接接管另一个 Agent 的会话

一次让 Pi 接管 Claude Code 会话的经历:4 条消息烧掉近 2000 万 Token。我据此把 Coding Agent 的交接从恢复会话,改成 Git + HANDOFF + 新会话。

最近用 Pi 做一个开发任务时,我踩到一个成本问题:一次看起来很便宜、缓存命中率超过 90% 的会话,仍然烧掉了近两千万 Token。根子不在模型贵,也不在缓存没生效,而在于我让一个 Agent 去接管了另一个 Agent 没跑完的会话。这篇记下这个结构错在哪,以及我最后把交接流程改成了什么。它是我的成本控制经历,不是什么模型限制或通用结论。

这个任务之前在 Claude Code 里做了一部分,所以我很自然地给 Pi 下了一条指令:接着这个未完成的会话做完,claude --resume <session-id>。从人的角度看这很合理——Claude Code 已经知道前因后果,恢复会话让 Pi 接着监督它完成,不必重新理解项目。

跑了一段时间后我看了一眼 API Usage,发现不太对。这次 Pi 会话里只有 4 条用户消息,却产生了:

Assistant: 53
Tool calls: 52

Input: 19.86M tokens
Cached: 18.02M
Cache hit: 90.7%

缓存命中率其实很健康。问题不在缓存有没有工作,而在于 Agent 在一个接近 1M Token 的上下文上连续跑了很多轮——绝大部分内容命中缓存,但命中的缓存依然不是免费的。这让我重新想一个问题:一个 Coding Agent 没做完的任务,到底应该怎么交给另一个 Agent?

问题不是 Resume,而是 Agent 套 Agent

claude --resume 本身没问题。如果 Claude Code 因为网络或终端中断,我重新 claude --resume <session> 继续工作完全合理。问题出在我实际搭出来的结构:

Pi
 └── shell
      └── Claude Code
           └── Model API

Pi 是一个 Agent,Claude Code 又是另一个完整的 Agent。于是我做的其实不是让 Pi「继承 Claude Code 的任务」,而是让一个 Agent 启动了另一个完整的 Agent Runtime。而 Claude Code 的终端输出、工具结果、历史信息,又源源不断地进入 Pi 的上下文。

这次的上下文规模变化很直白,从几十 K 很快涨上去,之后大量模型调用都维持在接近 1M 的规模:

~35K  →  ~490K  →  ~1.02M

真正贵的不是某一次特别长的回答,而是把接近 1M 的上下文乘以很多次 Agent Loop。 模型调一次工具、回来继续推理,就要重新过一遍当前上下文;几十次工具调用很容易累积成几千万 Token 的处理量。

我原来的理解错在哪

我以前潜意识里把「会话」约等于「项目状态」,觉得只要把会话恢复回来,Agent 就能无缝接着干。但对代码项目,这不是个好的状态模型。真正可靠的项目状态已经存在于 Git、源码、配置、测试和构建结果里;聊天记录额外保存的,只有「为什么这么做」和「接下来准备做什么」,而这部分根本不需要几十万 Token 的历史对话。

所以 Coding Agent 的会话应该被当成一个临时计算环境,而不是项目数据库。 真正该持久化的是 Repo。

我现在使用的方案

原则很简单:一个任务只保留一个主 Agent,不同 Agent 之间用文件交接,而不是用会话交接。 假设 Claude Code 做到一半要换 Pi 接管,我不再直接让 Pi 去 claude --resume,而是分两步。

第一步,如果 Claude Code 还能恢复,我先进去,但这次不让它继续开发,只给它一个收尾任务:

停止继续开发。把当前任务继续执行所必需的信息写入 .ai/HANDOFF.md:

- 最终目标
- 已完成的工作
- 尚未完成的工作
- 修改过的重要文件
- 已确定的设计决策
- 当前测试状态
- 已知问题
- 推荐的下一步
- 最终验证方式

不要复制完整聊天记录、完整源码或大量日志。
确保当前修改已保存在工作区后退出。

第二步,Pi 开一个全新的会话接管:

接管当前任务。先读取 .ai/HANDOFF.md、git status、git diff --stat,
然后只读取完成剩余任务所必需的文件。

先验证 HANDOFF 是否与当前代码一致;如果冲突,以 Git 和实际代码为准。
确认后继续完成剩余工作。

结构于是从「Pi → Claude Code → 历史会话 → 更多输出 → 回灌 Pi 上下文」变成:

Claude Code  →  HANDOFF.md  →  Git / Workspace  →  新的 Pi 会话

两个 Agent 之间真正传递的只有任务状态,而不是整个思考历史。

HANDOFF 其实不需要很复杂

我现在给项目统一保留一个 .ai/HANDOFF.md,内容保持很短:

# Handoff

## Goal
最终需要实现什么。

## Completed
已经完成什么。

## Remaining
- [ ] 剩余任务 A
- [ ] 剩余任务 B

## Changed Files
- `src/a.ts`
- `src/b.ts`

## Decisions
已经确定的重要设计决策,以及为什么。

## Tests
已经运行哪些测试,目前结果如何。

## Known Problems
当前还存在什么问题。

## Next Step
下一个 Agent 最应该先做什么。

## Verification
任务完成后如何验证。

如果一个 Agent 没法用这么一份文件说清自己做到哪里,那往往意味着任务本身还没被很好地结构化。

会话也需要主动结束

这次还有一个教训:不该等 Context 真塞满了才考虑换会话。Pi 能直接显示当前 Context 使用率,我现在用一条很粗的规则:

< 50%       正常继续
50% ~ 70%   任务快结束就继续;马上进新阶段就准备 HANDOFF
> 70%       不再开大型新任务,先收尾当前步骤并换会话

这不是模型的技术限制,只是我的成本控制线。Coding Agent 场景里一次用户指令可能触发几十次工具调用,在 70% 的 Context 上再起一个「顺便把这个模块重构一下」,接下来几十次模型请求都得带着巨大的历史上下文跑,而新开一个会话的成本通常反而低得多。

模型也尽量在会话边界切换

另一个容易忽略的点是缓存。这次我在长上下文状态下切过模型,其中一次接近百万 Token 的上下文需要重新建立缓存。所以我现在也尽量避免在 900K 上下文里直接把 Opus 换成另一个模型再继续,而是先在当前会话把这一阶段做完,写 HANDOFF,开新会话,再换模型——让模型切换和 Context 重置发生在同一个边界上,逻辑也更干净。

我最后只保留了四条规则

折腾一圈后,我觉得 Coding Agent 工作流没必要设计得特别复杂,目前只准备长期遵守四条:

  1. Repo 才是项目状态,会话只是临时上下文。
  2. 一个任务只保留一个主 Agent,不默认 Agent 套 Agent。
  3. 不同 Agent 用 HANDOFF.md + Git 交接,而不是恢复对方的完整会话。
  4. 长 Context 不硬撑,进入新阶段时主动换会话。

至于缓存命中率、模型价格、Context Window 有多大,这些当然值得关注,但更像第二层优化。第一层真正重要的是:不要让项目的连续性依赖某一个 AI 的聊天记录。

我现在判断一个 Coding Agent 工作流是否健康,会问自己一个问题:如果现在把所有 Pi、Claude Code、Codex 的会话全部删掉,一个新的 Agent 能不能只靠 Git 和项目里少量的状态文件继续工作?如果能,那说明项目状态真正属于项目,Agent 只是暂时来这里工作。

评论 →

CC BY-NC-SA 4.0

评论

评论由 GitHub Discussions 提供。登录 GitHub 后即可评论。 前往对应 Discussion