跳转到内容

会话树状管理 — Tree-based Session Architecture

会话树状管理 — Tree-based Session Architecture

问题定义

线性对话历史在以下场景中不够用:

  1. 分支探索:用户希望从对话的某个中间点”分叉”,尝试不同的指令路径,而不丢弃原始对话
  2. 上下文压缩:长对话超出模型上下文窗口时,需要将早期历史替换为摘要,同时保留关键信息
  3. 状态追溯:需要能够重建任意时刻的对话快照,用于调试、回放或审计

树状会话管理(Tree-based Session)将对话历史建模为一棵有根树,每个节点是一条消息或一个元事件(模型切换、思考级别变化、压缩、分支摘要),通过 parentId 建立父子关系,通过 leaf 指针追踪活跃分支末端。

核心概念

会话树条目(SessionTreeEntry)

树中的每个节点是一个 SessionTreeEntry,包含通用字段和类型特有字段:

interface SessionTreeEntryBase {
type: string; // 条目类型标识
id: string; // 全局唯一 ID
parentId: string | null; // 父节点 ID(根节点为 null)
timestamp: string; // 创建时间戳
}

条目类型分类

会话树条目按语义可分为四类:

分类条目类型作用
消息messageLLM 对话消息(user/assistant/toolResult)
消息custom_message应用自定义消息(如通知、状态更新)
元事件model_change记录模型切换
元事件thinking_level_change记录思考级别变化
元事件active_tools_change记录可用工具集变化
元事件session_info会话元信息(名称等)
元事件label给特定节点打标签
压缩compaction压缩标记,含摘要和保留起始点
压缩branch_summary分支摘要,记录从某节点分叉的总结
结构custom自定义数据节点
结构leaf活跃分支末端指针

树的结构

root (parentId: null)
├── message: user "hello"
├── message: assistant "hi there"
├── message: user "write a function"
├── message: assistant [tool calls]
├── message: toolResult
├── message: assistant [final answer]
├── compaction ← 压缩标记
│ ├── summary: "用户请求编写函数,助手完成实现"
│ └── firstKeptEntryId: "msg-5" ← 从此节点开始保留原始消息
│ ├── branch_summary ← 分支摘要
│ │ ├── fromId: "msg-3"
│ │ └── summary: "分叉路径的交互总结"
│ └── leaf ← 当前活跃分支末端
│ └── targetId: "msg-8"

关键操作

1. 路径重建(Path to Root)

给定一个 leafId,从叶子沿 parentId 链回溯到根,得到从根到叶的完整路径。这是构建模型上下文消息列表的基础。

getPathToRoot(leafId) → [root, ..., leaf] 的反转

2. 分支(Fork)

在树的任意节点处分叉,创建新的子分支。分叉操作需要指定:

  • 分叉点:目标节点 ID
  • 位置before(在目标之前)或 at(在目标之后)

分叉后,新的 leaf 指针指向新分支的末端,旧分支保留在树中不被删除。

3. 上下文构建(Build Session Context)

从根到活跃 leaf 的路径中,提取所有可转换为 LLM 消息的条目,构建模型上下文:

路径条目 → 过滤/转换 → AgentMessage[] → LLM Message[]

压缩标记的处理特殊:遇到 compaction 节点时,将摘要作为一条系统消息插入,然后从 firstKeptEntryId 开始继续保留原始消息。

4. 上下文压缩(Compaction)

当对话 token 数接近模型上下文窗口时,触发压缩:

  1. 计算当前 token 数:基于 LLM 返回的 usage 统计
  2. 判定阈值contextTokens > contextWindow - reserveTokens
  3. 选择切割点:从旧到新扫描,找到保留 keepRecentTokens 个最近 token 的切割位置
  4. 生成摘要:调用 LLM 对切割点之前的历史生成总结
  5. 插入压缩节点:将摘要和 firstKeptEntryId 写入树中

压缩后的上下文 = [压缩摘要] + [firstKeptEntryId 之后的原始消息]

5. 分支摘要(Branch Summary)

当从某节点分叉时,生成分支摘要:

  • 总结该节点之前对话的关键信息
  • 保留在树中作为分支点的上下文锚
  • 新分支的上下文构建时会包含此摘要

持久化策略

JSONL 存储

每个会话对应一个 .jsonl 文件,每行是一个 JSON 序列化的 SessionTreeEntry。追加写入,不修改历史行。

优点

  • 追加写入天然支持崩溃恢复
  • 每行独立,可逐行读取而不加载整个文件
  • 兼容标准文本工具(grep、awk、head)

文件命名{timestamp}_{sessionId}.jsonl,按工作目录分目录组织。

内存存储

用于测试和短期会话,条目存储在内存数组中。接口与 JSONL 存储一致,通过 SessionStorage 接口统一抽象。

设计权衡

维度选择理由
数据结构树而非图简化路径重建逻辑,避免环检测
持久化格式JSONL 而非数据库轻量、可文本处理、易调试
压缩策略摘要替换而非滑动窗口保留语义上下文,而非简单的截断
分支处理保留所有分支支持事后比较不同路径的结果
ID 生成UUID v7时间有序、全局唯一、可排序

陷阱与对策

陷阱对策
压缩后 firstKeptEntryId 指向不存在的节点压缩时验证 ID 有效性,写入前校验
多压缩节点链导致上下文重建错误按时间顺序遍历,只使用最新的压缩节点
分叉后 leaf 指针未更新导致读取旧分支fork 操作原子性地更新 leaf 指针
JSONL 文件损坏导致部分条目丢失逐行解析,单行失败不影响其他行
压缩摘要丢失关键文件操作信息压缩时记录 readFiles/modifiedFiles 列表到 details 字段