如何解析 5 种完全不同格式的 AI 对话

发布时间:2026/6/1 21:04:18

如何解析 5 种完全不同格式的 AI 对话 本文面向想了解 5 种 AI 对话格式各自解析策略的开发者。预计阅读时间12 分钟最终效果理解 JSONL 流式解析、SQLite KV 查询、Event Stream 重建、快照读取的完整策略以及各格式的特殊处理技巧。五种格式五种世界AI 编程工具没有统一的对话存储标准。每家都按自己的理解设计数据格式有的追加写日志有的用数据库有的存快照。ChatCrystal 需要同时支持 5 种数据源每种的解析逻辑都截然不同。本文逐个拆解每种格式的特点和对应的解析策略。一、Claude CodeJSONL 流式日志 噪音过滤Claude Code 的对话存储在~/.claude/projects/下按项目目录组织。每个会话是一个.jsonl文件每行一个 JSON 对象。数据特点这是流式写入的日志。每一行代表一个事件——包括流式 delta文本片段、工具调用片段、思考片段、完整消息、系统事件等。一个用户提问可能产生几十行日志。核心难点噪音过滤。解析器定义了一个SKIP_TYPES集合constSKIP_TYPESnewSet([file-history-snapshot,last-prompt,progress,agent_progress,hook_progress,queue-operation,message,// 流式 deltatool_use,// 流式工具 deltatool_result,// 流式工具结果 deltathinking,// 流式思考 deltatext,// 流式文本 deltatool_reference,]);过滤逻辑有两层第一层检查uuid是否存在流式 delta 没有 uuid第二层检查type是否在黑名单中。所有system类型也被跳过包括turn_duration、api_error等。最终只保留user和assistant类型的完整消息。内容提取消息的content字段可能是纯字符串也可能是 ContentBlock 数组。解析器遍历数组按 block type 分别处理text提取文本thinking提取思考过程tool_use标记工具调用tool_result跳过。内容清洗sanitizeContent()函数用正则移除 Claude Code 注入的 XML 标签resultresult.replace(/system-reminder[\s\S]*?\/system-reminder/g,);resultresult.replace(/command-name[^]*\/command-name/g,);resultresult.replace(/command-message[^]*\/command-message/g,);resultresult.replace(/command-args[^]*\/command-args/g,);resultresult.replace(/local-command-stdout[^]*\/local-command-stdout/g,);resultresult.replace(/local-command-caveat[\s\S]*?\/local-command-caveat/g,);这些标签是 Claude Code 的运行时注入对知识提取没有价值。二、Codex CLI事件流 多源去重Codex CLI 的会话记录在~/.codex/sessions/下文件名格式为rollout-{ISO时间}-{sessionId}.jsonl。与 Claude Code 不同Codex 的 JSONL 不是简单的消息追加而是一个结构化的事件流。事件类型路由每行 JSON 有type和payload两个顶层字段。解析器按type分支处理session_meta— 会话元数据ID、工作目录、Git 分支。只提取一次。event_msgpayload.type user_message— 用户消息。需要调用extractUserPrompt()去除 IDE 上下文前缀Codex VS Code 会注入 “## My request for Codex:” 标记。event_msgpayload.type agent_message— 助手文本回复。response_itempayload.type message— 完整消息对象含output_textcontent blocks。response_itempayload.type function_call或custom_tool_call— 工具调用标记到上一条助手消息的hasToolUse。去重问题Codex 会同时通过event_msg/agent_message和response_item/message两种路径输出助手回复。解析器用lastAssistantMsg指针检测重复如果response_item的文本与上一条agent_message完全相同就跳过。Slug 来源Codex 有一个~/.codex/session_index.jsonl文件存储会话 ID 到线程名称的映射。解析器在parse()结束时加载这个索引来填充slug字段。三、Cursor跨两个 SQLite 数据库Cursor 基于 VS Code对话数据存在 SQLite 的 KV 表里。但它的数据分布在两个位置Workspace DB(workspaceStorage/{hash}/state.vscdb) — 存储composer.composerData包含该工作区所有 composer 会话的元数据列表。Global DB(globalStorage/state.vscdb) — 存储cursorDiskKV表包含所有 bubble消息气泡的实际内容。扫描流程先遍历workspaceStorage/下所有子目录读取workspace.json获取项目路径打开state.vscdb查询composer.composerData。这是一个 JSON 字符串解析后得到allComposers数组每个元素有composerId、createdAt、name等字段。孤儿发现有些 composer 的 bubble 数据存在于 global DB 中但在任何 workspace DB 的composerData里都找不到比如工作区被删除了。findOrphanBubbleComposers()用 SQL 的SUBSTRINSTR从cursorDiskKV的 key 中提取 composerId过滤掉已知的再验证是否有实际文本内容。Bubble 解析每个 bubble 的 key 格式为bubbleId:{composerId}:{bubbleId}value 是 JSON。BubbleData 用type字段区分用户1和助手2还有toolResults、codeBlocks、allThinkingBlocks等结构化字段。Cursor 的 bubble schema 有版本号_v解析器会对未知版本发出警告。四、Trae单 key 存储 agentTaskContent 嵌套Trae 同样基于 VS Code数据也存在state.vscdb里但存储方式与 Cursor 完全不同。单一 KV 结构Trae 把所有会话数据存在一个 key 下memento/icube-ai-agent-storage。这个 value 是一个 JSON 字符串解析后是{ list: TraeSession[], currentSessionId: string }。每个TraeSession包含sessionId、createdAt、updatedAt和一个messages数组。消息结构TraeMessage 有role、content、turnIndex、timestamp等字段。排序时用turnIndex而不是 timestamp因为助手消息的时间戳有时不可靠。agentTaskContent 提取Trae 的 SOLO Builder agent 不直接把回复写在content里而是存在agentTaskContent中。这个结构包含proposal— 提案文本proposalReasoningContent— 顶层推理过程guideline.planItems[]— 计划步骤列表每个步骤有toolName、thought、reasoningContent解析策略是优先用content直接内容如果为空则回退到agentTaskContent。在 agentTaskContent 中toolName finish的步骤的thought通常是完整回复。工具使用检测则看是否存在toolName不是finish的 planItem。时间戳合成Trae 的 assistant 消息时间戳可能早于 user 消息。解析器用一个单调递增的orderMs计数器如果真实时间戳大于当前计数器就用真实的否则用计数器值并递增 1ms。五、CopilotJSONL 快照 只读首行GitHub Copilot 的对话存在 VS Code 的workspaceStorage/{hash}/chatSessions/和globalStorage/emptyWindowChatSessions/目录下文件格式为.jsonl或.json。快照机制.jsonl文件的第一行是完整会话快照kind:0后续行是 UI 状态 patchkind:1。ChatCrystal 只读第一行——用readFirstLine()函数打开流读一行就关闭。.json文件则是单一 JSON 对象旧格式直接用readFile()读取整个文件内容asyncfunctionreadFirstLine(filePath:string):Promisestring|null{conststreamcreateReadStream(filePath,{encoding:utf-8});constrlcreateInterface({input:stream,crlfDelay:Infinity});forawait(constlineofrl){rl.close();stream.destroy();returnline;}returnnull;}格式归一化.jsonl的快照包裹在{kind:0, v:{...}}中.json的数据直接在顶层。解析器用snapshot.v ?? snapshot统一处理。Request-Response 结构会话数据的核心是requests数组。每个 request 有message用户消息和response助手回复数组。response 数组的每个元素有kind字段text是文本、thinking是思考过程、toolInvocationSerialized是工具调用、mcpServersStarting和inlineReference是噪音。时间戳处理助手回复的时间戳设为request.timestamp 1加 1 毫秒保证在同一 request 内助手消息排在用户消息之后。总结对比维度Claude CodeCodexCursorTraeCopilot文件格式JSONLJSONLSQLite KVSQLite KVJSONL/JSON存储粒度每会话一文件每会话一文件跨两个 DB单 key 全量每会话一文件噪音程度高流式 delta中事件类型多低结构化低但嵌套深低快照模式工具调用标记content block typefunction_call eventtoolResults isAgenticplanItemstoolInvocationSerialized思考过程thinking block加密不可用allThinkingBlocksproposalReasoningContentthinking kind特殊处理XML 标签清洗IDE 上下文剥离孤儿发现时间戳合成只读首行五种格式五种解析策略但最终都归一化成同一个ParsedConversation。这就是 SourceAdapter 插件架构的价值——格式差异被隔离在适配器内部下游的导入、存储、搜索逻辑完全不需要关心数据从哪来。项目地址github.com/ZengLiangYi/ChatCrystal如有疑问欢迎在 GitHub Issues 或私信交流很乐意解答。

相关新闻