ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

跨CLI编程Agent会话接力:中间格式实现上下文无缝迁移

跨CLI编程Agent会话接力:中间格式实现上下文无缝迁移 1. 为什么需要跨 CLI 编程 Agent 的会话接力1.1 一个真实到让人头疼的场景我平时的工作流里终端窗口基本是常驻的。左边一个跑 Claude Code 做代码审查和重构右边一个开 Codex CLI 处理批量脚本生成中间还夹着一个跑测试的 shell。问题来了当我在 Claude Code 里聊了半小时把项目背景、约束条件、命名规范、踩过的坑全都喂给了它结果切到 Codex CLI 想让它接着干同一件事的时候一切归零。我得重新把上下文再讲一遍而且讲得还不一定比第一次全。这不是个别现象。只要你同时用两个以上的 CLI 编程 Agent就一定会遇到这个断层。每个 Agent 都有自己的会话状态存在自己的目录里格式互不兼容谁也不认识谁。Claude Code 把会话存在~/.claude/projects/下面Codex CLI 存在~/.codex/sessions/里两边都是 JSONL 或者 JSON 结构但字段名、消息格式、角色定义全都不一样。所谓会话接力就是让一个 Agent 的对话历史能够被另一个 Agent 读取、理解并继续。听起来简单做起来要解决三个层面的问题会话文件在哪、格式怎么转、接上之后怎么保证不串味。1.2 会话接力到底解决什么问题先说清楚价值不然没必要折腾。第一省掉重复交代上下文的成本。一个成熟项目的背景信息认真讲一遍至少五到十分钟涉及技术栈、目录结构、代码风格、禁用库、历史决策。这些信息在 Agent A 里已经存在了接力之后 Agent B 直接继承不用重讲。第二发挥不同 Agent 的差异化能力。Claude Code 在长上下文理解和复杂重构上更稳Codex CLI 在生成独立函数、写测试、跑批处理上响应更快。理想状态是用 Claude Code 做架构设计和方案评审把结论接力给 Codex CLI 去落地实现。两边各干各擅长的事中间靠会话接力打通。第三保留决策链路。项目做到一半换工具最怕的是丢失为什么这么设计的记录。会话历史里藏着大量决策依据接力过来等于把决策链路一起带过去了。1.3 适合谁来参考这套方案这套东西不是给纯新手准备的。你需要满足几个前提本地已经装好了至少两个 CLI 编程 Agent 并且能正常跑起来对终端操作、文件路径、JSON 格式不陌生最好懂一点 Python 或者 Node因为格式转换那部分要写脚本。如果你只是偶尔用一个 Agent那没必要折腾。但如果你像我一样日常在多个 Agent 之间来回切或者团队里不同人用不同工具需要交接那这套方案能省下大量重复沟通的时间。提示会话接力涉及读取和改写 Agent 的本地会话文件操作前务必备份原始目录。改坏了顶多是丢会话但丢的是你花时间聊出来的上下文心疼。2. 核心思路与方案选型拆解2.1 三种可行路线对比实现跨 CLI 会话接力我实际试过三条路线各有取舍。路线实现方式优点缺点适用场景文件直转解析 A 的会话文件转成 B 的格式写回去无需额外进程纯离线格式耦合强Agent 升级易失效一次性接力、低频使用中间格式定义统一的中间 JSON双向转换解耦扩展新 Agent 成本低需要维护转换层多 Agent 长期混用上下文注入把 A 的摘要作为首条消息喂给 B实现最简单不碰文件丢失细节只有摘要快速接力、只要结论我最后选的是中间格式路线。原因很直接文件直转在 Agent 版本升级后经常崩字段一改脚本就废上下文注入又太糙摘要会丢掉关键的代码片段和约束细节。中间格式虽然多写一层但一次投入长期受益而且中间格式本身就是一份可读的会话存档出问题好排查。2.2 中间格式怎么设计中间格式的核心是只保留语义不保留平台特性。我定义的 schema 大概长这样{ session_id: relay-20240101-001, source_agent: claude-code, created_at: 2024-01-01T10:00:00Z, project_path: /Users/me/project, messages: [ { role: user, content: 把 utils 里的日期处理统一成 dayjs, timestamp: 2024-01-01T10:00:05Z }, { role: assistant, content: 好的我先扫描 utils 目录..., timestamp: 2024-01-01T10:00:08Z } ], metadata: { model: claude-sonnet, total_turns: 12 } }关键设计决策有三个。role 只保留 user 和 assistant 两种因为不同 Agent 对 system、tool、function 这些角色的定义差异太大强行映射会出错工具调用记录我选择在转换时丢弃只保留对话主干。content 统一成纯文本多模态内容图片、附件在接力场景下价值有限直接跳过。metadata 只放非关键的辅助信息接力时目标 Agent 用不上但排查问题时有用。2.3 为什么不做全量转换有人会问为什么不把工具调用、文件 diff、执行结果全都转过去我的经验是转得越多错得越多。Claude Code 的一次工具调用记录里包含工具名、参数、返回结果、耗时Codex CLI 那边的工具调用结构完全不同硬转过去要么报错要么被目标 Agent 当成无效历史忽略。更麻烦的是某些 Agent 看到历史里有工具调用记录会尝试续上那个调用结果执行了不该执行的命令。所以我的原则是只接力对话不接力动作。目标 Agent 拿到的是我们聊了什么、结论是什么至于中间执行过哪些命令让它自己重新判断。这样既安全又避免了格式地狱。2.4 会话定位怎么找到要接力的那个会话这是实操里第一个卡点。Claude Code 的会话文件按项目路径哈希分目录文件名是 UUID光看文件名根本不知道哪个是哪个。Codex CLI 类似按日期分目录。我的做法是写一个会话索引脚本扫描会话目录提取每个会话的首条用户消息和最后修改时间生成一张清单python3 session_index.py --agent claude-code --project /Users/me/project输出大概是这样[2024-01-01 10:00] 3f2a... 把 utils 里的日期处理统一成 dayjs [2024-01-01 09:30] 8b1c... 重构 auth 模块拆出 token 校验 [2023-12-31 18:20] 5d9e... 写一个批量重命名脚本有了这张清单接力的时候直接按时间或者关键词选不用去猜 UUID。这个索引脚本本身也是中间格式的副产品扫描的时候顺手就把会话解析成中间格式了。3. 核心细节解析与实操要点3.1 Claude Code 会话文件结构解析Claude Code 的会话存在~/.claude/projects/项目路径哈希/下面每个会话一个.jsonl文件每行一条消息。单行结构简化后是这样{ type: user, message: { role: user, content: [{type: text, text: 实际内容}] }, timestamp: 2024-01-01T10:00:05Z, uuid: ... }几个坑点要注意。content 是数组不是字符串里面可能有 text、tool_use、tool_result 多种类型解析的时候要按 type 过滤只取 text。type 字段和 message.role 可能不一致有些系统消息 type 是 user 但 role 是别的判断角色要以 message.role 为准。时间戳是 ISO 格式带时区转换时统一成 UTC。3.2 Codex CLI 会话文件结构解析Codex CLI 的会话在~/.codex/sessions/下按年/月/日分目录文件名是rollout-时间戳-uuid.jsonl。单行结构{ timestamp: 2024-01-01T10:00:05.000Z, type: message, payload: { role: user, content: [{type: input_text, text: 实际内容}] } }和 Claude Code 的差异很明显外层字段名不同type vs type但取值语义不同内容类型名不同input_text vs text嵌套层级不同payload 包一层。这些差异就是转换层要抹平的地方。3.3 转换层的三个关键处理角色归一化。两个 Agent 都有 user 和 assistant但 Codex CLI 还有 developer、system 等角色。我的处理是user 和 assistant 原样保留其他角色统一映射成 user并在内容前加标记[系统上下文]让目标 Agent 知道这不是用户直接说的。内容提取。写一个递归函数遍历 content 数组把所有 text 类型的片段拼起来。遇到 tool_use 或 tool_result 直接跳过。这里有个细节如果一条消息里全是工具调用没有文本这条消息就丢弃否则会产生空消息。时间戳统一。全部转成 ISO 8601 UTC 格式秒级精度就够。时间戳在接力时其实用不上但保留着方便排序和排查。3.4 写回目标 Agent 的注意事项把中间格式转成目标 Agent 格式写回去比读取更危险因为写错了可能让目标 Agent 启动就崩。第一不要覆盖已有会话。永远新建一个会话文件让目标 Agent 以新会话的方式加载。覆盖已有会话一旦出错原会话就没了。第二会话 ID 要新生成。用目标 Agent 期望的 UUID 格式别用源会话的 ID否则可能冲突。第三首条消息加接力标记。我会在第一条 user 消息前插入一段说明[会话接力] 以下内容来自另一个编程助手的会话记录请基于这些上下文继续工作。这样目标 Agent 知道自己在接手不会对历史消息里的指令产生困惑。注意不同版本的 Agent 对会话文件的校验严格程度不同。有的版本会校验 uuid 格式、时间戳格式、字段完整性写回前最好先用一个空会话文件对照字段结构。4. 完整实操流程与关键环节实现4.1 环境准备与目录确认先确认两个 Agent 的会话目录位置。Claude Code 默认在~/.claude/projects/Codex CLI 默认在~/.codex/sessions/。如果你改过配置去配置文件里找。ls -la ~/.claude/projects/ ls -la ~/.codex/sessions/确认能看到会话文件后先做一次全量备份cp -r ~/.claude/projects ~/.claude/projects.bak cp -r ~/.codex/sessions ~/.codex/sessions.bak备份这一步别省。我踩过一次坑转换脚本有个 bug 把源会话文件写坏了幸好有备份。4.2 会话索引脚本实现这个脚本负责扫描会话目录输出可读清单。核心逻辑是遍历目录、解析每个会话文件、提取首条用户消息。import json import os from pathlib import Path from datetime import datetime def index_claude_sessions(project_hash_dir): sessions [] for f in Path(project_hash_dir).glob(*.jsonl): first_user_msg None last_mtime f.stat().st_mtime with open(f, r, encodingutf-8) as fh: for line in fh: try: obj json.loads(line) except json.JSONDecodeError: continue msg obj.get(message, {}) if msg.get(role) user: content msg.get(content, []) for c in content: if c.get(type) text: first_user_msg c[text][:50] break if first_user_msg: break if first_user_msg: sessions.append({ file: str(f), preview: first_user_msg, mtime: datetime.fromtimestamp(last_mtime).isoformat() }) return sorted(sessions, keylambda x: x[mtime], reverseTrue)跑一下就能看到清单。这个脚本我建议存成session_index.py后面转换脚本会复用里面的解析逻辑。4.3 中间格式转换实现读取源会话转成中间格式。以 Claude Code 为例def claude_to_intermediate(session_file, project_path): messages [] with open(session_file, r, encodingutf-8) as fh: for line in fh: try: obj json.loads(line) except json.JSONDecodeError: continue msg obj.get(message, {}) role msg.get(role) if role not in (user, assistant): continue texts [] for c in msg.get(content, []): if c.get(type) text: texts.append(c[text]) if not texts: continue messages.append({ role: role, content: \n.join(texts), timestamp: obj.get(timestamp, ) }) return { session_id: frelay-{datetime.now().strftime(%Y%m%d%H%M%S)}, source_agent: claude-code, created_at: datetime.utcnow().isoformat() Z, project_path: project_path, messages: messages, metadata: {total_turns: len(messages)} }Codex CLI 的解析逻辑类似只是字段路径不同把msg.get(content)换成obj.get(payload, {}).get(content)把text类型换成input_text。4.4 写回目标 Agent 实现把中间格式转成 Codex CLI 的格式写回import uuid def intermediate_to_codex(intermediate, output_dir): session_uuid str(uuid.uuid4()) now datetime.utcnow() date_dir output_dir / now.strftime(%Y/%m/%d) date_dir.mkdir(parentsTrue, exist_okTrue) out_file date_dir / frollout-{now.strftime(%Y%m%dT%H%M%S)}-{session_uuid}.jsonl with open(out_file, w, encodingutf-8) as fh: # 首条接力标记 marker { timestamp: now.isoformat() Z, type: message, payload: { role: user, content: [{type: input_text, text: [会话接力] 以下内容来自另一个编程助手的会话记录请基于这些上下文继续工作。}] } } fh.write(json.dumps(marker, ensure_asciiFalse) \n) for m in intermediate[messages]: line { timestamp: m[timestamp] or now.isoformat() Z, type: message, payload: { role: m[role], content: [{type: input_text, text: m[content]}] } } fh.write(json.dumps(line, ensure_asciiFalse) \n) return str(out_file)写完启动 Codex CLI用--resume或者对应的会话恢复参数加载这个新会话就能看到历史对话了。4.5 一次完整的接力演示假设我在 Claude Code 里聊了一个重构任务会话文件是~/.claude/projects/abc123/3f2a.jsonl现在要接力给 Codex CLI。第一步索引找到会话python3 session_index.py --agent claude-code --project /Users/me/project第二步转成中间格式python3 relay.py export --agent claude-code \ --session ~/.claude/projects/abc123/3f2a.jsonl \ --project /Users/me/project \ --out /tmp/relay.json第三步写回 Codex CLIpython3 relay.py import --agent codex \ --input /tmp/relay.json \ --out ~/.codex/sessions第四步启动 Codex CLI 恢复新会话验证历史是否完整。整个过程不到一分钟比重新讲一遍上下文快得多。5. 常见问题与排查技巧实录5.1 接力后目标 Agent 不认历史最常见的现象是会话文件写进去了但启动 Agent 后它像没看见一样还是从空白开始。排查顺序是这样的。先确认文件路径对不对不同版本 Agent 的会话目录可能变过去配置文件里核对。再确认文件格式拿一个 Agent 自己生成的正常会话文件和你的输出文件逐字段对比看有没有缺字段或者字段类型不对。最后看时间戳有些 Agent 会按时间戳排序如果时间戳格式不对或者顺序乱了可能加载失败。我遇到过一次是时间戳精度问题Agent 期望毫秒级我写的是秒级结果加载时解析失败但没报错静默跳过了。5.2 中文内容乱码写文件时一定要指定encodingutf-8读的时候也一样。Python 在部分系统上默认编码不是 UTF-8不指定就会乱码。另外json.dumps要加ensure_asciiFalse否则中文会被转成\uXXXX转义虽然不影响解析但可读性差。5.3 会话太长导致加载慢如果源会话有几百轮对话全量接力过去目标 Agent 加载会变慢而且可能超出上下文窗口。我的处理是做截断只保留最近 N 轮或者按 token 数估算超过阈值就从最早的消息开始丢。丢的时候注意保持 user/assistant 成对别丢出个孤立的 assistant 消息。def truncate_messages(messages, max_turns50): if len(messages) max_turns: return messages return messages[-max_turns:]5.4 工具调用记录导致的异常前面说过要丢弃工具调用但有时候源会话里工具调用和文本混在一条消息里。我的处理是只提取 text 片段整条消息里如果没有 text 就丢弃。这样虽然会丢一些上下文但避免了目标 Agent 误执行历史命令。5.5 常见问题速查表现象可能原因排查方向目标 Agent 不认历史路径错、格式错、时间戳格式错对照正常会话文件逐字段比对中文乱码编码未指定 UTF-8读写都加 encodingutf-8加载慢或超窗口会话过长截断到最近 N 轮启动报错字段缺失或类型错用空会话文件对照 schema历史串味接力标记缺失首条消息加接力说明源文件被改坏脚本写回源目录永远新建文件不覆盖5.6 几个我踩过的坑坑一以为会话文件是纯 JSON。其实是 JSONL每行一个独立 JSON用json.load整体加载会报错必须逐行json.loads。坑二忽略了项目路径哈希。Claude Code 按项目路径哈希分目录同一个项目在不同机器上哈希可能不同接力时如果跨机器要确认目标机器上项目路径一致。坑三直接改了源会话文件。有次图省事直接在源文件上改结果脚本中途出错源会话损坏。从此坚持只读源、只写新。坑四没考虑 Agent 版本差异。升级 Agent 后会话格式变了老脚本直接失效。我的应对是把格式解析逻辑做成可配置的字段路径写在配置里升级时改配置不改代码。6. 进阶玩法与扩展方向6.1 双向接力与循环接力单向接力跑通后可以做成双向的Claude Code 聊完接力给 Codex CLICodex CLI 干完再接力回 Claude Code。这样两个 Agent 形成一个工作闭环各自发挥所长。循环接力要注意避免上下文膨胀。每接力一次就多一层历史几轮下来会话会变得很长。我的做法是每次接力时做一次摘要压缩把早期对话用一段总结代替只保留最近几轮原文。6.2 接入更多 Agent中间格式的好处在这里体现。要接入第三个 Agent只需要写一个xxx_to_intermediate和一个intermediate_to_xxx不用动其他代码。我目前接了 Claude Code、Codex CLI还在试一个本地的开源 Agent接入成本大概半小时。6.3 团队协作场景如果团队里有人用 Claude Code 有人用 Codex CLI可以把中间格式的会话文件当成交接文档。A 做完设计导出中间格式提交到仓库的.relay/目录B 拉下来导入自己的 Agent直接接着干。这比写交接文档靠谱因为上下文是完整的、可执行的。6.4 自动化触发我现在把接力做成了半自动在 Claude Code 里输入特定指令触发一个 hook 脚本自动导出中间格式并提示已准备好接力到 Codex CLI。省掉了手动跑命令的步骤。这个 hook 的实现依赖 Agent 的扩展机制不同 Agent 支持程度不同Claude Code 支持得比较好Codex CLI 目前还得手动跑。6.5 会话存档与检索中间格式的会话文件本身就是一份干净的存档。我把所有接力过的会话存在~/.relay/archive/下按项目和时间组织。需要找上次那个重构是怎么决策的直接 grep 存档目录比翻 Agent 自己的会话文件方便得多。grep -r 日期处理 ~/.relay/archive/ --include*.json这个检索能力是意外收获但用起来很顺手现在已经成为我工作流的一部分了。最后分享一个小心得接力脚本写完先拿一个无关紧要的测试会话跑通确认目标 Agent 能正常加载、历史完整、没有报错再去接力真正重要的会话。我一开始图快直接拿生产会话试结果格式没调对白折腾了半小时。测试会话花五分钟造一个能省下后面一堆麻烦。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进