1. 为什么需要跨 CLI 编程 Agent 的会话接力
1.1 一个真实到让人抓狂的场景
我平时写代码有个习惯,终端里同时开着好几个 AI 编程助手。Claude Code 用来做架构梳理和长上下文重构,Codex CLI 用来跑一些需要快速迭代的小脚本,偶尔还会切到别的 CLI Agent 处理特定任务。问题就出在"切"这个动作上——每个 CLI Agent 都有自己的会话上下文,你在 Claude Code 里聊了半小时的需求背景,切到 Codex 就得从头再讲一遍。更别提有时候一个 Agent 的额度用完了,想换另一个继续干,上下文直接断档,那种感觉就像写了一半的文档突然断电,还没保存。
这个痛点其实非常普遍。现在做 AI 编程的人,手里基本都不止一个 CLI Agent。Claude Code 的强项是理解复杂代码库、做多文件重构;Codex CLI 在代码生成和快速验证上很顺手;还有一些团队自研的 Agent 或者基于开源框架搭的 CLI 工具,各有各的适用场景。但它们的会话状态是彼此隔离的,没有一个通用的"接力棒"机制。
所谓会话接力,说白了就是:Agent A 干到一半,把当前的对话历史、任务状态、关键上下文打包,交给 Agent B 继续干,B 能无缝接上,不需要人肉复述。这件事听起来简单,做起来要解决三个核心问题——会话怎么导出、格式怎么统一、导入后怎么让新 Agent "理解"之前的进度。
1.2 会话接力的核心价值在哪里
先想清楚为什么要做这件事,不然容易陷入为了折腾而折腾的陷阱。
第一是成本优化。不同 Agent 的计费模型不一样,有的按 token 计费,有的包月,有的有免费额度。一个长任务如果全程用最贵的那个跑,成本会很难看。会话接力让你可以在任务的不同阶段选择性价比最高的 Agent。比如前期需求梳理用便宜的,核心代码生成用贵的,测试用例补充再切回便宜的。
第二是能力互补。没有哪个 Agent 在所有任务上都最强。Claude Code 对大型代码库的理解确实好,但有时候 Codex 生成的代码更符合某些语言的惯用法。会话接力让你能"集各家之所长",而不是被单一工具绑死。
第三是容错和连续性。Agent 服务偶尔会抽风,网络会断,额度会用完。如果会话能接力,这些都不是问题——换个 Agent 接着干就行,任务不会因为工具的问题而中断。
第四是团队协作。团队里不同人可能偏好不同的 Agent,如果会话能标准化导出和导入,那 A 用 Claude Code 做到一半,B 可以用 Codex 接着做,交接成本大幅降低。
1.3 这件事的难度到底在哪
很多人第一反应是"不就是把聊天记录复制过去吗",实际做起来会发现远没那么简单。
格式差异是第一个坎。Claude Code 的会话存储格式和 Codex CLI 完全不同,字段名、嵌套结构、元数据都不一样。你不能简单地把 JSON 从一个格式映射到另一个格式,因为有些信息在源格式里有,在目标格式里根本没有对应的位置。
上下文窗口是第二个坎。一个跑了很久的会话,token 数量可能远超目标 Agent 的上下文窗口。你不可能把全部历史都塞进去,必须做摘要和裁剪,但裁哪些、留哪些,直接决定了接力后 Agent 能不能理解任务。
状态语义是第三个坎。会话里不只有对话,还有工具调用记录、文件修改历史、当前工作目录状态等。这些"隐式状态"如果不传递,新 Agent 接手后会一脸懵。
工具链差异是第四个坎。不同 Agent 支持的工具集不一样,Claude Code 能调用的工具和 Codex CLI 能调用的工具未必重合。接力时如果历史里有目标 Agent 不支持的工具调用,需要做转换或降级处理。
搞清楚了这些难点,后面的方案设计就有了方向。
2. 整体方案设计与技术选型思路
2.1 三种可行路线对比
做会话接力,业界目前大致有三条路线,各有优劣,我逐一分析一下。
路线一:中间格式转换。定义一个通用的会话中间格式(类似"会话交换格式"),每个 Agent 的会话先导出成这个中间格式,再从中间格式导入到目标 Agent。优点是扩展性好,新增一个 Agent 只需要写一对导入导出适配器;缺点是中间格式的设计很难做到既通用又不丢信息,容易变成"最小公倍数"式的信息阉割。
路线二:直接点对点转换。针对每一对 Agent 写专门的转换逻辑。优点是转换质量高,可以针对性地处理两个格式之间的差异;缺点是组合爆炸,N 个 Agent 需要 N×(N-1) 个转换器,维护成本高。
路线三:代理层拦截。在 Agent 和它的后端服务之间加一个代理层,拦截所有请求和响应,在代理层做会话的持久化和转换。优点是透明,Agent 本身不需要改;缺点是实现复杂,而且涉及网络请求的拦截和重放,稳定性和安全性都要仔细考虑。
我最终选择的是路线一为主、路线二为辅的混合方案。核心用中间格式做通用转换,但对于 Claude Code 和 Codex CLI 这两对高频组合,额外做一些针对性的优化处理。这样既保证了扩展性,又在关键路径上不牺牲质量。
2.2 中间格式的设计原则
中间格式是整个方案的地基,设计得好不好直接决定成败。我遵循了四条原则。
原则一:保留原始信息,不做有损压缩。中间格式里要完整保留源会话的所有信息,包括那些目标 Agent 暂时用不上的字段。用扩展字段的方式存放,而不是直接丢弃。这样将来目标 Agent 支持了这些信息,或者需要回溯原始会话时,数据还在。
原则二:区分"必须"和"可选"。每个字段标注是否必须。导入时,必须字段缺失就报错,可选字段缺失就用默认值。这样能快速定位格式问题,而不是让 Agent 带着残缺的上下文跑。
原则三:时间线统一。不同 Agent 的时间戳格式、时区处理可能不一样。中间格式统一用 ISO 8601 带时区的格式,并且保留原始时间戳作为附加字段。
原则四:角色和消息类型标准化。把 user、assistant、system、tool 这些角色统一命名,工具调用和工具结果用统一的结构表示。这是最容易被忽视但最重要的一点,因为角色映射错了,Agent 会把工具结果当成用户输入,直接跑偏。
2.3 技术栈选择
实现语言我选了 Python。原因很直接:Claude Code 和 Codex CLI 的会话文件都是 JSON 或 JSONL 格式,Python 处理这类数据最顺手;而且后续要做摘要和裁剪,Python 的生态里有现成的文本处理库。如果你更熟悉 Node.js,用 TypeScript 实现也完全没问题,逻辑是一样的。
依赖方面尽量精简,核心只用标准库的json、pathlib、datetime,摘要功能可选依赖一个文本处理库。我刻意避免引入重型框架,因为这东西是要长期维护的,依赖越少越不容易烂。
存储上,中间格式的会话文件我建议用 JSONL(每行一个 JSON 对象),而不是单个大 JSON。原因是会话可能很长,JSONL 支持流式读写,追加新消息也方便,不用每次重写整个文件。
3. 核心细节解析与实操要点
3.1 会话文件的定位与解析
不同 Agent 的会话存储位置差别很大,这是实操中第一个要解决的问题。
Claude Code 的会话数据通常存放在用户目录下的配置目录里,按项目路径做哈希分目录,每个会话是一个独立的文件。Codex CLI 的存储结构类似但路径规则不同。我的做法是写一个会话发现模块,针对每个 Agent 定义它的存储根路径和文件匹配规则,扫描出所有会话文件,并提取出会话 ID、创建时间、最后修改时间、消息数量等元信息,形成一个会话索引。
这里有个坑要注意:不要硬编码路径。不同操作系统、不同安装方式,路径都可能不一样。我的做法是通过环境变量和配置文件双重查找,优先读环境变量,读不到再查常见路径列表,最后才报错提示用户手动指定。
解析会话文件时,要处理格式版本差异。Agent 更新后,会话格式可能会变。我的解析器里加了一个版本检测逻辑,遇到不认识的格式版本就给出明确提示,而不是默默解析出一堆空数据。
3.2 消息角色的映射规则
角色映射是转换质量的关键。我整理了一张映射表,覆盖了常见的情况。
| 源角色 | 中间格式角色 | 目标角色(Claude Code) | 目标角色(Codex CLI) |
|---|---|---|---|
| human / user | user | user | user |
| assistant / ai | assistant | assistant | assistant |
| system | system | system | system |
| tool_call | tool_call | tool_use | function_call |
| tool_result | tool_result | tool_result | function_call_output |
映射时最容易出错的是工具调用和工具结果的配对。一次工具调用会有一个调用 ID,工具结果里必须带上对应的 ID,否则目标 Agent 无法把结果和调用关联起来。我在转换时会校验这个配对关系,发现孤儿调用或孤儿结果就记录下来,在导入时做特殊处理。
还有一个细节:连续同角色消息的合并。有些 Agent 会把连续的多条 assistant 消息分开存,有些会合并。中间格式统一按"不合并"处理,导入时再根据目标 Agent 的习惯决定是否合并。这样更灵活。
3.3 上下文裁剪与摘要策略
这是整个方案里技术含量最高的部分。一个长会话可能有几十万 token,直接导入目标 Agent 会超上下文窗口。必须做裁剪。
我的策略是分层保留。
第一层是系统提示和任务定义,这部分永远保留,因为它是任务的"宪法"。
第二层是最近 N 轮对话,N 根据目标 Agent 的上下文窗口动态计算。比如目标窗口是 200K token,我预留 50K 给新生成的内容,剩下 150K 里拿出 100K 放最近对话,50K 放摘要。
第三层是早期对话的摘要。用文本摘要的方式把早期对话压缩成一段话,放在最近对话之前。摘要要保留关键决策、已完成的步骤、待解决的问题。
第四层是工具调用记录的压缩。工具调用往往很占 token,但很多中间过程的调用结果对后续任务没用了。我的做法是只保留最近几次工具调用的完整记录,更早的只保留"调用了什么工具、结果成功还是失败"的简要信息。
裁剪的 token 计算我用的是近似估算:英文按 4 字符 1 token,中文按 1.5 字符 1 token。这个估算不精确,但足够用来做裁剪决策。如果你追求精确,可以接入目标 Agent 的 tokenizer,但会增加复杂度和依赖。
3.4 文件状态与工作目录的传递
这是最容易被忽视但影响很大的一点。Agent 在会话过程中可能修改了文件、创建了新文件、切换了工作目录。这些状态如果不传递,新 Agent 接手后会基于错误的文件状态工作。
我的做法是在会话导出时,额外记录一份文件状态快照:当前工作目录、会话期间被修改过的文件列表、每个文件的修改时间。导入时,把这些信息作为一条特殊的 system 消息注入,告诉新 Agent "当前工作目录是 X,以下文件在之前的会话中被修改过:..."。
注意,这里不传递文件内容本身,只传递文件路径和修改状态。因为文件内容可能很大,而且新 Agent 可以直接读取文件系统。传递状态信息就够了。
如果两个 Agent 运行在不同的机器或容器里,文件系统不共享,那就需要额外做文件同步。这种情况我建议用版本控制工具做中转,把修改提交到一个临时分支,新 Agent 拉取后再继续。
4. 实操过程与核心环节实现
4.1 环境准备与目录结构
先把项目骨架搭起来。我用的目录结构是这样的:
session-relay/ ├── adapters/ │ ├── claude_code.py │ ├── codex_cli.py │ └── base.py ├── core/ │ ├── schema.py │ ├── converter.py │ ├── truncator.py │ └── summarizer.py ├── cli.py └── config.yamladapters目录放各个 Agent 的适配器,每个适配器负责该 Agent 会话格式的读写。core目录放核心逻辑:schema 定义中间格式,converter 做格式转换,truncator 做裁剪,summarizer 做摘要。cli.py是命令行入口。config.yaml放配置,比如各 Agent 的存储路径、上下文窗口大小等。
配置文件的示例:
agents: claude_code: session_root: "~/.claude/projects" context_window: 200000 reserved_tokens: 50000 codex_cli: session_root: "~/.codex/sessions" context_window: 128000 reserved_tokens: 30000 relay: recent_turns: 20 summary_max_tokens: 2000 tool_call_keep_recent: 5这个配置结构的好处是,新增一个 Agent 只需要加一段配置和写一个适配器,核心逻辑不用动。
4.2 中间格式的 schema 定义
中间格式我用 Python 的 dataclass 定义,清晰且类型安全。核心结构如下:
from dataclasses import dataclass, field from typing import List, Optional, Dict, Any from datetime import datetime @dataclass class Message: role: str # user / assistant / system / tool_call / tool_result content: str timestamp: str tool_call_id: Optional[str] = None tool_name: Optional[str] = None metadata: Dict[str, Any] = field(default_factory=dict) @dataclass class Session: session_id: str source_agent: str created_at: str updated_at: str working_directory: str messages: List[Message] file_snapshot: List[Dict[str, Any]] = field(default_factory=list) extra: Dict[str, Any] = field(default_factory=dict)extra字段用来存放源 Agent 特有的、中间格式没有对应位置的字段。这样保证信息不丢失,将来需要时可以回溯。
4.3 Claude Code 会话导出适配器
Claude Code 的会话文件是 JSONL 格式,每行一条消息。适配器的核心逻辑是逐行读取、解析、转换成中间格式的 Message 对象。
import json from pathlib import Path from core.schema import Message, Session class ClaudeCodeAdapter: def __init__(self, session_root: str): self.session_root = Path(session_root).expanduser() def list_sessions(self): sessions = [] for f in self.session_root.rglob("*.jsonl"): stat = f.stat() sessions.append({ "path": str(f), "session_id": f.stem, "updated_at": stat.st_mtime, }) return sorted(sessions, key=lambda x: x["updated_at"], reverse=True) def export(self, session_path: str) -> Session: messages = [] with open(session_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue raw = json.loads(line) msg = self._convert_message(raw) if msg: messages.append(msg) return Session( session_id=Path(session_path).stem, source_agent="claude_code", created_at=messages[0].timestamp if messages else "", updated_at=messages[-1].timestamp if messages else "", working_directory="", messages=messages, ) def _convert_message(self, raw: dict) -> Message: role = raw.get("role") or raw.get("type", "user") role_map = {"human": "user", "ai": "assistant"} role = role_map.get(role, role) content = raw.get("content", "") if isinstance(content, list): content = self._flatten_content(content) return Message( role=role, content=content, timestamp=raw.get("timestamp", ""), metadata={"raw_keys": list(raw.keys())}, ) def _flatten_content(self, blocks: list) -> str: parts = [] for block in blocks: if isinstance(block, str): parts.append(block) elif isinstance(block, dict): if block.get("type") == "text": parts.append(block.get("text", "")) elif block.get("type") == "tool_use": parts.append(f"[tool_use: {block.get('name')}]") elif block.get("type") == "tool_result": parts.append(f"[tool_result: {block.get('tool_use_id')}]") return "\n".join(parts)这里有个实操要点:content 字段可能是字符串也可能是数组。Claude Code 的会话里,简单消息的 content 是字符串,带工具调用的消息 content 是结构化数组。适配器必须两种都处理,否则遇到工具调用就崩。
4.4 Codex CLI 会话导入适配器
导入比导出复杂,因为要处理格式差异和上下文裁剪。核心流程是:读取中间格式会话 → 裁剪 → 转换成 Codex CLI 格式 → 写入会话文件。
class CodexCLIAdapter: def __init__(self, session_root: str, context_window: int, reserved_tokens: int): self.session_root = Path(session_root).expanduser() self.context_window = context_window self.reserved_tokens = reserved_tokens def import_session(self, session: Session, target_project: str) -> str: from core.truncator import truncate_session from core.summarizer import summarize_early_messages budget = self.context_window - self.reserved_tokens truncated = truncate_session(session, budget) summary = summarize_early_messages(session, truncated) output_lines = [] if summary: output_lines.append(json.dumps({ "role": "system", "content": f"[会话接力摘要] {summary}", }, ensure_ascii=False)) for msg in truncated.messages: output_lines.append(json.dumps( self._to_codex_format(msg), ensure_ascii=False )) target_dir = self.session_root / target_project target_dir.mkdir(parents=True, exist_ok=True) target_file = target_dir / f"relay_{session.session_id}.jsonl" with open(target_file, "w", encoding="utf-8") as f: f.write("\n".join(output_lines)) return str(target_file) def _to_codex_format(self, msg: Message) -> dict: role_map = { "user": "user", "assistant": "assistant", "system": "system", "tool_call": "assistant", "tool_result": "tool", } result = { "role": role_map.get(msg.role, "user"), "content": msg.content, } if msg.tool_call_id: result["tool_call_id"] = msg.tool_call_id return result导入时要注意目标目录的创建。Codex CLI 不会自动为接力会话创建目录,需要适配器自己处理。另外文件名我加了relay_前缀,方便区分哪些是接力来的会话,哪些是原生会话。
4.5 裁剪器的实现细节
裁剪器是核心中的核心,我单独说一下实现。
def estimate_tokens(text: str) -> int: chinese = sum(1 for c in text if '\u4e00' <= c <= '\u9fff') other = len(text) - chinese return int(chinese / 1.5 + other / 4) def truncate_session(session, budget): from core.schema import Session system_msgs = [m for m in session.messages if m.role == "system"] other_msgs = [m for m in session.messages if m.role != "system"] system_tokens = sum(estimate_tokens(m.content) for m in system_msgs) remaining = budget - system_tokens kept = [] used = 0 for msg in reversed(other_msgs): t = estimate_tokens(msg.content) if used + t > remaining: break kept.insert(0, msg) used += t return Session( session_id=session.session_id, source_agent=session.source_agent, created_at=session.created_at, updated_at=session.updated_at, working_directory=session.working_directory, messages=system_msgs + kept, file_snapshot=session.file_snapshot, )裁剪逻辑是从后往前保留,因为最近的消息对当前任务最重要。system 消息永远保留,因为它们是任务定义。被裁掉的部分交给摘要器处理。
这里有个经验:裁剪的边界要落在完整的对话轮次上,不要从一轮对话中间切断。一轮对话指的是 user 消息加上它对应的 assistant 回复。如果裁剪边界落在 assistant 消息上,说明这轮对话不完整,应该往前多裁一条。
4.6 摘要器的实现
摘要器负责把被裁掉的早期对话压缩成一段话。我用的是基于规则的抽取式摘要,而不是调用大模型,原因是快、免费、可控。
def summarize_early_messages(full_session, truncated_session): kept_ids = {id(m) for m in truncated_session.messages} dropped = [m for m in full_session.messages if id(m) not in kept_ids] if not dropped: return "" key_points = [] for msg in dropped: if msg.role == "user": text = msg.content.strip() if len(text) > 100: text = text[:100] + "..." key_points.append(f"用户提出:{text}") elif msg.role == "assistant" and len(msg.content) > 200: key_points.append(f"助手给出方案:{msg.content[:150]}...") if len(key_points) > 15: key_points = key_points[:5] + ["...(中间省略)..."] + key_points[-5:] return "早期对话要点:\n" + "\n".join(key_points)抽取式摘要的规则是:用户消息保留前 100 字,助手的长回复保留前 150 字,中间太多就省略。这样能保留关键决策点,又不至于太长。
如果你追求更好的摘要质量,可以接入大模型做生成式摘要。但要注意,生成式摘要会引入额外的 API 调用成本和延迟,而且摘要质量不稳定。我的建议是先用抽取式,觉得不够再升级。
5. 常见问题与排查技巧实录
5.1 导入后 Agent 不认上下文
这是最常见的问题。表现是新 Agent 接手后,完全无视之前的对话,从头开始问需求。
排查思路分三步。第一步查角色映射,确认导入后的会话里,user 和 assistant 的角色没有搞反。如果搞反了,Agent 会把之前的助手回复当成用户输入,自然就乱了。第二步查消息顺序,确认时间线是递增的,没有乱序。有些 Agent 对消息顺序敏感,乱序会导致解析失败。第三步查 system 消息,确认任务定义类的 system 消息被正确保留和放置。system 消息如果被当成普通消息处理,Agent 就接收不到任务约束。
我踩过的一个坑是:Codex CLI 对 system 消息的位置有要求,必须放在最前面。我一开始把摘要 system 消息放在了中间,结果 Codex 直接忽略了它。后来改成所有 system 消息前置,问题解决。
5.2 工具调用配对失败
表现是导入后 Agent 报错,说找不到对应的工具调用或工具结果。
原因是工具调用的 ID 在转换过程中丢失或改变了。排查时重点看tool_call_id 字段是否在转换前后保持一致。我的做法是在转换器里加一个校验步骤,遍历所有消息,检查每个 tool_result 是否都能找到对应的 tool_call,反之亦然。发现不配对就记录警告,并在导入时把孤儿消息转成普通文本消息,避免 Agent 报错。
还有一个隐蔽的坑:不同 Agent 的工具调用 ID 格式不一样。有的用 UUID,有的用递增数字。转换时不要试图"翻译"ID,直接原样保留,只要保证配对关系不断就行。
5.3 上下文还是超限
明明做了裁剪,导入后还是报上下文超限。这种情况通常是token 估算不准导致的。
我的估算公式是英文 4 字符 1 token、中文 1.5 字符 1 token,但实际 tokenizer 的切分更复杂,代码、特殊符号、多语言混合的文本,估算误差可能到 20%。解决办法是留足安全余量,我一般按估算值的 80% 来用,也就是估算出来 100K token,实际只放 80K 的内容。
另一个原因是目标 Agent 的上下文窗口配置不对。有些 Agent 的实际可用窗口比标称的小,因为要预留输出空间。我的配置里reserved_tokens就是干这个的,一般留 30% 到 40% 给输出。
5.4 会话文件写入后 Agent 读不到
导入适配器写入了文件,但 Agent 启动后看不到这个会话。排查方向是文件路径和命名规则。
不同 Agent 对会话文件的命名和存放位置有严格要求。比如有的要求文件名必须是特定格式的 UUID,有的要求放在特定哈希目录下。我的做法是先用 Agent 自己创建一个原生会话,观察它生成的文件路径和命名,然后照着这个规则来写接力会话。
还有一个原因是文件权限。如果适配器以某个用户身份运行,写出的文件权限不对,Agent 进程读不到。检查文件权限,确保和原生会话文件一致。
5.5 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| Agent 无视历史上下文 | 角色映射错误 | 检查导入后消息的 role 字段 | 修正角色映射表 |
| 报错找不到工具调用 | tool_call_id 丢失 | 对比转换前后的 ID | 保留原始 ID,校验配对 |
| 上下文超限 | token 估算不准 | 用目标 tokenizer 复核 | 留 20% 安全余量 |
| Agent 读不到会话 | 路径或命名不符 | 对比原生会话文件 | 照原生规则命名 |
| 摘要丢失关键信息 | 抽取规则太激进 | 检查被裁消息 | 调整抽取阈值 |
| 文件状态不对 | 未传递工作目录 | 检查 file_snapshot | 注入状态 system 消息 |
5.6 几个独家避坑技巧
技巧一:先做单向验证。不要一上来就做双向接力,先把 Claude Code 到 Codex CLI 这一个方向跑通,验证整个链路没问题,再扩展其他方向。这样出问题时排查范围小。
技巧二:保留原始会话备份。转换和导入过程中,永远不要修改或删除原始会话文件。所有操作都在副本上进行。我见过有人转换失败把原始会话搞坏了,欲哭无泪。
技巧三:加一个 dry-run 模式。导入前先跑一遍 dry-run,只做转换和裁剪,输出统计信息(消息数、估算 token 数、被裁消息数),不实际写入。确认没问题再真正导入。
技巧四:会话 ID 加时间戳。接力生成的会话 ID 里带上时间戳,比如relay_20250101_143022_abc123。这样多个接力会话不会互相覆盖,也方便追溯。
技巧五:定期清理接力会话。接力会话会越积越多,占磁盘空间。写个清理脚本,定期删除超过一定天数的接力会话。但注意别误删原生会话。
6. 扩展方向与进阶玩法
6.1 支持更多 Agent
当前方案支持 Claude Code 和 Codex CLI,扩展到其他 Agent 只需要写一个适配器。适配器要实现两个方法:export把该 Agent 的会话转成中间格式,import_session把中间格式转成该 Agent 的会话。核心的转换、裁剪、摘要逻辑完全复用。
新增 Agent 时,重点搞清楚三件事:会话文件存在哪、格式长什么样、有什么特殊要求。前两件通过观察原生会话就能搞清楚,第三件需要查文档或试错。
6.2 自动接力
现在是手动触发接力,进阶玩法是自动接力。监控当前 Agent 的 token 使用量,接近上下文窗口上限时,自动导出会话、裁剪、导入到备用 Agent,并提示用户切换。这样长任务可以一直跑下去,不用人盯着。
自动接力的难点是判断接力的时机。太早接力浪费,太晚接力上下文已经丢了。我的经验是设置在窗口使用率达到 80% 时触发,留 20% 的缓冲。
6.3 会话版本管理
把接力会话纳入版本管理,每次接力生成一个版本,记录接力的时间、源 Agent、目标 Agent、裁剪策略。这样可以回溯整个任务的演进过程,也方便对比不同 Agent 在同一任务上的表现。
实现上可以用 Git 管理会话文件,每次接力提交一次。会话文件是文本格式,Git 能很好地做 diff 和版本追踪。
6.4 团队共享会话
把中间格式的会话文件放到共享存储上,团队成员可以互相接力。A 用 Claude Code 做到一半,导出成中间格式上传,B 下载后导入到自己的 Codex CLI 继续做。这样团队协作时,工具选择不再是个人的事,而是可以灵活切换。
这里要注意敏感信息处理。会话里可能包含 API key、密码、内部路径等信息。共享前要做脱敏,把敏感信息替换成占位符。我写了一个简单的脱敏规则,匹配常见的敏感信息模式,自动替换。
6.5 会话质量分析
接力会话积累多了,可以做质量分析。比如统计不同 Agent 在同类任务上的表现,分析哪些任务适合哪个 Agent,为后续的 Agent 选择提供数据支撑。这个方向偏数据科学,但基础数据就是接力过程中产生的会话记录。
分析维度可以包括:任务完成率、平均对话轮次、工具调用次数、用户干预次数等。这些指标能客观反映 Agent 的能力边界。
7. 我个人的一些实操体会
这套东西我从有想法到跑通,前后折腾了大概两周。中间踩的坑比预想的多,但跑通之后确实省事。现在我的工作流是:复杂重构用 Claude Code 起头,做到上下文快满了就接力到 Codex CLI 继续,Codex 额度用完了再接力回来。整个过程不用重复讲需求,任务连续性很好。
有几个体会比较深。第一,中间格式的设计值得多花时间。我一开始图快,中间格式设计得很简陋,结果扩展新 Agent 时发现信息不够用,又回头重构。如果一开始就设计得完整一些,后面会省很多事。第二,裁剪策略要保守。宁可多留一些上下文,也不要裁得太狠。裁太狠导致 Agent 理解不了任务,比上下文超限更麻烦。第三,测试要覆盖边界情况。空会话、单条消息的会话、全是工具调用的会话,这些边界情况我都遇到过,处理不好就崩。
最后分享一个小技巧:如果你只是想快速验证接力效果,不用完整实现所有功能,先写一个最简单的版本——把源会话的消息原样读出来,按目标格式写进去,不做裁剪和摘要。跑通这个最小闭环,再逐步加功能。这样能快速看到效果,也更容易定位问题。
这套方案不是银弹,它解决的是特定场景下的特定问题。如果你的任务都很短,一个 Agent 就能搞定,那没必要折腾接力。但如果你经常做长任务、经常在多个 Agent 之间切换,那这套东西能实实在在提升效率。工具是为人服务的,选择适合自己的就好。