Logo
热心市民王先生

pi-mono 记忆管理机制 - 上下文压缩与跨模型传递

详解 pi-mono 的上下文压缩、跨模型上下文传递、会话恢复与分支、Token 追踪机制

2.1 上下文压缩算法

压缩触发条件

当会话 tokens 接近模型上下文窗口限制时,pi-mono 自动触发压缩:

const CONTEXT_THRESHOLD = 0.8  // 80% 窗口利用率

async function checkAndCompact(context: Context, model: Model) {
  const tokens = countTokens(context.messages)
  const limit = model.contextWindow
  
  if (tokens > limit * CONTEXT_THRESHOLD) {
    await compact(context)
  }
}

默认阈值

  • 触发点:80% 上下文窗口
  • 目标:压缩后降至 50% 以下

压缩流程

[原始会话]                                              [压缩后]
┌─────────────────────────────────────────┐             ┌─────────────────────────────┐
│ UserMessage: "帮我实现 JWT 认证"          │             │ CompactionEntry:            │
│ AssistantMessage: "好的,我先了解需求"    │ ──────┐     │ "讨论了 JWT 认证需求..."       │
│ UserMessage: "需要支持刷新令牌"          │       │     │                             │
│ ... (早期对话 45K tokens) ...           │       │     │ UserMessage: "下一步做什么?" │
│ UserMessage: "下一步做什么?"            │       └────>│ AssistantMessage: "..."       │
│ AssistantMessage: "..."                 │             └─────────────────────────────┘
└─────────────────────────────────────────┘

步骤

  1. 调用 LLM 生成早期消息摘要
  2. 创建 CompactionEntry 存储摘要
  3. 记录 firstKeptEntryId(保留的第一条消息)
  4. 后续 buildSessionContext() 优先输出摘要

摘要生成提示词

const COMPACTION_PROMPT = `
Summarize the conversation history, capturing:
- Key decisions made
- Code changes implemented
- Open questions
- User preferences

Keep it concise but preserve essential context for continuation.
`

压缩策略对比

策略pi-monoClaude CodeCursor
方法LLM 摘要滑动窗口向量摘要
可解释性✅ 高⚠️ 中⚠️ 中
信息保留⚠️ 依赖摘要质量❌ 丢失早期内容✅ 向量检索
计算成本⚠️ 一次 LLM 调用✅ 无⚠️ 向量嵌入

2.2 跨模型上下文传递

设计动机

pi-mono 支持在会话中切换模型(如从 Claude 切换到 GPT),这需要处理:

  1. Thinking traces 格式差异:各提供商返回不同的推理内容格式
  2. 工具调用签名:不同 API 的工具调用结构不同
  3. 元数据兼容性:token 计数、缓存读取等字段不一致

上下文序列化

import { Context } from '@mariozechner/pi-ai'

const context: Context = {
  messages: [
    { 
      role: "assistant", 
      content: [
        { type: "thinking", thinking: "..." },
        { type: "text", text: "..." }
      ]
    }
  ]
}

// 序列化为 JSON(可存储或传输)
const serialized = JSON.stringify(context)

序列化内容

  • 所有消息及内容块
  • 工具调用及结果
  • Thinking traces(如有)
  • 使用量统计

跨提供商转换

Anthropic → OpenAI

// Anthropic thinking traces
{
  role: "assistant",
  content: [
    { type: "thinking", thinking: "Let me analyze..." },
    { type: "text", text: "The answer is..." }
  ]
}

// 转换为 OpenAI 格式
{
  role: "assistant",
  content: [
    { type: "text", text: "<thinking>Let me analyze...</thinking>\nThe answer is..." }
  ]
}

转换规则

  • Thinking traces 包裹在 <thinking> 标签中
  • 工具调用转换为函数调用格式
  • 使用量字段重新映射

OpenAI → Google

// OpenAI tool calls
{
  role: "assistant",
  tool_calls: [
    { id: "call_123", function: { name: "bash", arguments: "..." } }
  ]
}

// 转换为 Google 格式
{
  role: "model",
  functionCalls: [
    { name: "bash", args: { command: "..." } }
  ]
}

上下文反序列化

// 从 JSON 恢复上下文
const restored: Context = JSON.parse(serialized)

// 可在任意模型上继续
const model = getModel('google', 'gemini-2.5-flash')
const response = await complete(model, restored)

限制

  • Thinking traces 可能丢失语义(变为纯文本)
  • 工具调用参数需重新验证
  • 缓存读取统计不跨提供商

2.3 会话恢复与分支

会话恢复模式

Continue(继续最近)

pi --continue

行为

  1. 扫描 ~/.pi/agent/sessions/<path>/ 目录
  2. 按时间戳排序找到最近会话
  3. 加载到最后一条 leaf 节点
  4. 继续对话

Resume(恢复指定)

pi --resume 20260307_140000_abc12345

行为

  1. 解析会话文件路径
  2. 完整加载 JSONL 文件
  3. 重建消息树
  4. 定位到 leaf 节点

Fork(分叉新会话)

pi --fork <session-id> --from <entry-id>

行为

  1. 创建新会话文件
  2. 复制源会话到指定条目
  3. 添加 parentSession 元数据
  4. 新消息形成独立分支

分支导航

# 查看会话树
pi --tree

# 切换到历史节点
pi --goto <entry-id>

# 从当前节点创建分支
pi --branch

树状显示

* a1b2c3d4 - User: "帮我实现 JWT 认证"
  b2c3d4e5 - Assistant: "好的,我先了解需求"
    c3d4e5f6 - User: "需要支持刷新令牌"
    * d4e5f6g7 - User: "使用 Redis 存储"  ← 当前分支
    e5f6g7h8 - User: "使用数据库存储"
      f6g7h8i9 - Assistant: "数据库方案如下..."

分支摘要

切换分支时自动创建摘要:

{
  "type": "branch_summary",
  "id": "g7h8i9j0",
  "parentId": "a1b2c3d4",
  "fromId": "d4e5f6g7",
  "summary": "在该分支上,用户选择了 Redis 存储方案,讨论了过期时间设置..."
}

作用

  • 保留离开分支的上下文
  • 新分支可理解之前的探索
  • 避免重复讨论

2.4 Token 追踪机制

使用量统计

interface Usage {
  input: number       // 输入 tokens
  output: number      // 输出 tokens
  cacheRead: number   // 缓存读取 tokens
  cacheWrite: number  // 缓存写入 tokens
  totalTokens: number
  cost: {
    input: number     // 输入成本(美元)
    output: number    // 输出成本
    cacheRead: number // 缓存读取成本
    cacheWrite: number// 缓存写入成本
    total: number     // 总成本
  }
}

成本计算

const PRICING = {
  'claude-sonnet-4-5': {
    input: 3e-6,      // $3 / 1M tokens
    output: 15e-6,    // $15 / 1M tokens
    cacheRead: 0.3e-6,
    cacheWrite: 3.75e-6
  }
}

function calculateCost(usage: Usage, model: string): Cost {
  const rates = PRICING[model]
  return {
    input: usage.input * rates.input,
    output: usage.output * rates.output,
    // ...
  }
}

会话级追踪

每条 AssistantMessage 包含使用量:

{
  "type": "message",
  "message": {
    "role": "assistant",
    "usage": {
      "input": 15000,
      "output": 2000,
      "totalTokens": 17000,
      "cost": { "total": 0.075 }
    }
  }
}

累计统计

# 查看会话总成本
pi --stats

# 输出示例
Session: 20260307_140000_abc12345
Total tokens: 125,000
Total cost: $0.52
Messages: 45

跨会话统计

# 所有会话统计
pi --stats --all

# 按项目分组
pi --stats --group-by project

本章小结

pi-mono 的记忆管理机制特点:

  1. 自动压缩:LLM 生成摘要,保留关键上下文
  2. 跨模型兼容:序列化/反序列化支持任意模型切换
  3. 树状分支:原生支持多路径探索
  4. 成本透明:精确追踪 tokens 和成本

下一章将介绍如何通过扩展 API 增强记忆功能。


参考资料

  1. pi-mono Session Documentation. https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/session.md
  2. pi-mono SDK Documentation. https://github.com/badlogic/pi-mono/blob/main/packages/coding-agent/docs/sdk.md
  3. Context Handoff Example. packages/ai/src/context.ts