AI 编程工具的数据格式为什么不能统一

本文面向:需要从多个 AI 编程工具导入对话、被数据格式差异困扰的开发者。

预计阅读时间:10 分钟

最终效果:理解 5 种工具数据格式差异的根源,以及 ChatCrystal 如何用 SourceAdapter 适配器模式把差异隔离在边界内。

五种格式的现实

ChatCrystal 支持从 5 个 AI 编程工具导入对话:Claude Code、Codex CLI、Cursor、Trae、GitHub Copilot。如果你打开它们的数据目录,会看到:

  • Claude Code:JSONL 文件,每行一个 JSON 对象
  • Codex CLI:JSONL 文件,事件流格式
  • Cursor:SQLite 数据库(state.vscdb)
  • Trae:SQLite 数据库(state.vscdb)
  • Copilot:JSONL 文件,会话快照格式

同样是"AI 编程对话",为什么格式差这么多?

历史包袱:每个工具有自己的起点

这 5 个工具不是从零开始设计的,它们各自有不同的技术起点:

Claude Code:终端原生

Claude Code 是 Anthropic 的 CLI 工具,从终端启动。它的对话记录自然地选择了 JSONL(JSON Lines)格式------每行一个 JSON 对象,追加写入,零解析开销。

json 复制代码
{"uuid":"abc-123","type":"summary","timestamp":"2025-01-15T10:30:00Z","message":{"role":"user","content":"帮我修复这个 bug"}}
{"uuid":"def-456","parentUuid":"abc-123","type":"assistant","timestamp":"2025-01-15T10:30:05Z","message":{"role":"assistant","content":[{"type":"text","text":"让我看看代码..."}]}}

JSONL 的优势是流式友好:进程崩溃不会损坏已有数据,最后几行可能不完整但前面的都安全。缺点是查询不方便------要找到特定对话,得扫描文件。

Codex CLI:事件溯源

Codex CLI(OpenAI 的编码代理)采用事件流格式。它不是存储"对话",而是存储"事件"------每个用户输入、模型回复、工具调用都是一个独立事件:

json 复制代码
{"timestamp":"2025-01-15T10:30:00Z","type":"session_meta","payload":{"id":"session-xyz","cwd":"/home/user/project"}}
{"timestamp":"2025-01-15T10:30:01Z","type":"event_msg","payload":{"role":"user","content":"帮我修复这个 bug"}}
{"timestamp":"2025-01-15T10:30:06Z","type":"response_item","payload":{"role":"assistant","content":[{"type":"output_text","text":"让我看看..."}]}}

这种设计适合 Codex 的代理模式------一个任务可能涉及多轮工具调用、代码执行、文件修改,事件流能完整记录这些非线性交互。但对于 ChatCrystal 来说,需要从事件流中重建对话线程,这比直接读取对话列表复杂得多。

Cursor / Trae:VS Code 生态

Cursor 和 Trae 都是基于 VS Code 的 fork。它们继承了 VS Code 的数据存储方式------使用 SQLite 数据库的键值存储(state.vscdb)。

css 复制代码
state.vscdb 结构:
┌──────────────────────────────────────────────┐
│ TableItemTable                               │
├──────────────┬───────────────────────────────┤
│ key          │ value                         │
├──────────────┼───────────────────────────────┤
│ composer.1   │ {"allComposers":[...]}        │
│ bubbleId:abc123:def456 │ {"role":"user","content":...} │
│ bubbleId:abc123:ghi789 │ {"role":"assistant",...}      │
└──────────────┴───────────────────────────────┘

Cursor 把对话元数据(composer heads)和消息内容(bubbles)分开存储在不同的键下。Trae 类似,但用 memento/icube-ai-agent-storage 一个键存所有会话数据,内部是嵌套的 JSON 结构。

为什么用 SQLite?因为 VS Code 生态本身就用 SQLite 做本地状态管理------扩展设置、搜索索引、工作区状态都在 state.vscdb 里。Cursor 和 Trae 作为 fork,自然沿用了这个基础设施。

Copilot:会话快照

GitHub Copilot Chat 在 VS Code 中运行,但它的对话数据存储在 workspaceStorage 的 JSONL 文件中:

json 复制代码
{"kind":0,"v":{"version":2,"sessionId":"abc","creationDate":1705312200000,"requests":[{"requestId":"r1","timestamp":1705312201000,"message":{"text":"帮我写个函数"},"response":[{"kind":"text","text":"好的,这是..."}]}]}}

kind:0 标记这是一个快照。每次对话更新,Copilot 会追加一个新的快照行,包含完整的对话历史。这意味着同一个会话文件可能有多个快照,需要取最新的一条。

设计优先级的差异

格式差异的根源是每个工具的设计优先级不同

工具 优先级 格式选择
Claude Code 追加写入、崩溃安全 JSONL 流式追加
Codex CLI 事件溯源、非线性交互 事件流
Cursor VS Code 生态集成 SQLite KV
Trae VS Code 生态集成 SQLite KV
Copilot 快照式持久化 JSONL 快照

没有"最好的"格式,只有"最适合该工具场景的"格式。Claude Code 不会为了方便第三方解析而改用 SQLite;Cursor 不会为了流式写入而放弃 VS Code 的状态管理架构。

对知识管理的影响

格式不统一对知识管理工具有三个直接影响:

1. 路径发现

每个工具把数据存在不同位置:

javascript 复制代码
Claude Code:  ~/.claude/projects/**/*.jsonl
Codex CLI:    ~/.codex/sessions/**/*.jsonl
Cursor:       %APPDATA%/Cursor/User/workspaceStorage/*/state.vscdb
Trae:         %APPDATA%/Trae/User/workspaceStorage/*/state.vscdb
Copilot:      %APPDATA%/Code/User/workspaceStorage/*/chatSessions/*.jsonl
              %APPDATA%/Code/User/globalStorage/emptyWindowChatSessions/*.jsonl

路径模式不同,文件扩展名不同,甚至同一个工具在不同操作系统上的路径都不同(Windows 用 %APPDATA%,macOS 用 ~/Library/Application Support,Linux 用 ~/.config)。

2. 消息结构

即使成功找到了数据文件,消息结构也完全不同:

  • Claude Code 的 message.content 可以是字符串或 ContentBlock 数组
  • Codex 的消息嵌套在 event_msg / response_item 的 payload 中
  • Cursor 的消息存储为 bubble 对象,需要根据 composer ID 关联
  • Trae 的 assistant 消息可能在 agentTaskContent.proposal
  • Copilot 的回复在 response 数组中,每项有 kind 字段区分类型

3. 噪声过滤

每个工具有不同的"噪声"------不是对话内容的系统消息、进度指示、流式增量:

  • Claude Code 有 system-remindercommand-name 等 XML 标签需要剥离
  • Claude Code 有 file-history-snapshotprogressmessage(流式增量)等类型需要跳过
  • Codex 有 turn_context 等辅助事件需要过滤
  • Cursor 的 bubble 可能有空内容需要跳过

ChatCrystal 的适配器模式

ChatCrystal 用适配器模式(Adapter Pattern) 解决了格式统一问题。核心是一个 SourceAdapter 接口:

typescript 复制代码
interface SourceAdapter {
  readonly name: string;           // 'claude-code'
  readonly displayName: string;    // 'Claude Code'

  // 检测当前机器上是否有该数据源
  detect(): Promise<SourceInfo | null>;

  // 扫描所有对话文件,返回元数据(不解析内容)
  scan(): Promise<ConversationMeta[]>;

  // 解析单个对话,返回统一格式
  parse(meta: ConversationMeta): Promise<ParsedConversation>;
}

三个方法覆盖了完整的导入流程:

  1. detect() :检查数据目录是否存在、是否有数据文件
  2. scan() :列出所有对话文件,返回 id、路径、大小、修改时间等元数据
  3. parse() :读取单个文件,过滤噪声,重建对话线程,输出统一的 ParsedConversation

ParsedConversation 是统一的输出格式:

yaml 复制代码
interface ParsedConversation {
  id: string;
  slug: string | null;
  source: string;
  projectDir: string;
  projectName: string;
  cwd: string | null;
  gitBranch: string | null;
  messages: ParsedMessage[];
  firstMessageAt: string;
  lastMessageAt: string;
}

interface ParsedMessage {
  id: string;
  parentUuid: string | null;
  type: 'user' | 'assistant' | 'system';
  role: string | null;
  content: string;
  hasToolUse: boolean;
  hasCode: boolean;
  thinking: string | null;
  timestamp: string;
}

不管底层是 JSONL 还是 SQLite,是事件流还是快照,parse() 的输出永远是这个结构。下游的摘要生成、Embedding 计算、搜索索引都不需要知道数据来自哪个工具。

实现细节:每个适配器的挑战

Claude Code 适配器

最大的挑战是噪声过滤 。Claude Code 的 JSONL 包含大量非对话内容:文件历史快照、流式增量、工具进度。适配器维护了一个 SKIP_TYPES 集合,跳过这些类型的消息。

另一个挑战是内容块类型 。Claude Code 的 message.content 可能是纯文本字符串,也可能是 ContentBlock 数组(包含 text、thinking、tool_use 等类型)。适配器需要递归处理这些块,提取有意义的文本内容。

Codex 适配器

Codex 的事件流需要重建对话线程 。事件按时间顺序排列,但对话可能是非线性的------用户可能在工具调用中途插入新指令。适配器按事件类型分组,将 event_msgresponse_item 配对为完整的对话轮次。

还需要加载会话索引 :Codex 的 session_index.jsonl 文件记录了会话 ID 到线程名称的映射,用于生成有意义的对话标题。

Cursor / Trae 适配器

最大的挑战是SQLite 键值存储的解析。对话数据不是一个结构化的表,而是分散在多个键下的 JSON 字符串。适配器需要:

  1. 扫描所有 workspaceStorage 目录,找到 state.vscdb 文件
  2. 打开 SQLite 数据库,查询 composer 相关的键
  3. 解析 JSON,提取 composer heads(元数据)和 bubbles(消息内容)
  4. 按 composer ID 关联消息,重建对话顺序

Cursor 和 Trae 共享类似的 SQLite 结构,但键名和内部 JSON 格式不同,所以分别有独立的适配器实现。

Copilot 适配器

Copilot 的 JSONL 格式相对规整,但需要处理多快照问题。同一个会话文件可能有多行(多个快照),每行都包含完整的对话历史。适配器只取最后一行(最新快照)来解析。

还需要处理双路径问题:工作区对话在 workspaceStorage 下,全局对话在 globalStorage/emptyWindowChatSessions 下。适配器需要扫描两个位置。

新增数据源:插件化

适配器模式让新增数据源变得简单。要支持一个新的 AI 编程工具,只需要:

  1. 实现 SourceAdapter 接口(detect、scan、parse 三个方法)
  2. 在适配器注册表中添加一行
  3. 在 import service 中调用

不需要修改摘要生成、Embedding 计算、搜索索引等任何下游代码。这就是适配器模式的价值------把"数据格式差异"隔离在适配器内部,让核心流程保持统一。

小结

5 个工具、5 种格式,这不是任何人"设计失败"的结果。每个工具根据自己的技术起点和设计优先级,选择了最适合的数据格式。JSONL 适合流式追加,SQLite 适合键值查询,事件流适合非线性记录。

ChatCrystal 不能要求这些工具统一格式------这不是 ChatCrystal 能影响的事情。但它可以通过适配器模式,在自己的边界内实现统一。SourceAdapter 接口就像一个翻译层:对外,它理解 5 种方言;对内,它只说一种语言。

如果你正在开发需要处理多个数据源的工具,适配器模式是值得优先考虑的架构选择。把差异隔离在边界处,让核心逻辑保持简洁。


项目地址:github.com/ZengLiangYi...

如有疑问欢迎在 GitHub Issues 或私信交流,很乐意解答。

相关推荐
Master_Azur1 小时前
JavaEE之网络编程(TomCat介绍)
后端·网络协议
陈_杨1 小时前
鸿蒙APP开发-带你走进旧物集的时间线与收藏管理
前端·javascript
预知同行1 小时前
订单超时自动取消,你真的做对了吗?从定时任务到千万级高并发方案的完整演进
后端
尼斯湖皮皮怪1 小时前
iceCoder双模详解
javascript
小雨下雨的雨1 小时前
月相分析工具鸿蒙PC Electron框架技术实现详解
前端·javascript·华为·electron
杉氧2 小时前
100% Kotlin:基于 KMP + Compose Multiplatform 的全栈架构实战(Clean Architecture + MVI)
android·架构
布依前端2 小时前
基于 Vue 3 的 Tiptap 富文本编辑器实践:tiptap-editor-vue3 项目介绍
前端·javascript·vue.js
ServBay2 小时前
2026年重新定义 Python 开发工作流的8个现代化工具
后端·python
奥利奥夹心脆芙2 小时前
OTel / Logstash / Fluentd 全维对比,及统一日志与指标管道的 AWS ECS 落地
javascript