终端 AI 助手的上下文压缩与持久化记忆设计

24 阅读7分钟

从 slave-agent 的实践经验出发,探讨如何让终端 AI 助手拥有"长期记忆"与"超长对话"能力。


一、问题的起点

使用大模型 CLI 工具时,你可能遇到过这样的场景:

  • 聊了 30 轮代码审查后,AI "忘了" 你一开始说的项目架构约束
  • 昨天讨论的设计方案,今天新开一个会话就全丢了
  • 上下文窗口报警,不得不 /clear 后重新开始

根本原因有两个:

  1. 上下文截断粗暴 — 超长对话时,系统往往直接丢弃最早的消息,导致关键信息丢失
  2. 跨会话无记忆 — 每个会话从零开始,无法累积项目上下文

slave-agent 的设计目标就是解决这两个问题。本文将分享我们在持久化记忆上下文压缩两个方向上的实践经验。


二、持久化记忆:让 AI 记住"你是谁"

2.1 本地文件记忆

最简单的持久化方案,往往也是最可靠的。slave-agent 采用本地 Markdown 文件作为记忆载体:

~/.slave-agent/memory/
  NOTES.md      # 工作笔记(agent 可读写)
  PROFILE.md    # 用户偏好(只读,用户维护)

NOTES.md 由 agent 在每轮对话结束后自动判断是否需要更新。例如:

  • 你提到"本项目使用函数式风格,避免 class"
  • 你交代了"接口返回统一用 {code, data, message} 格式"
  • 你指定了"数据库用 SQLite WAL 模式"

agent 认为有价值,就会追加到 NOTES.md。下次会话启动时,这些笔记自动注入 system prompt,成为 agent 的"常识"。

PROFILE.md 则由用户手动维护,适合放置长期稳定的偏好:

我是一名后端工程师,主要使用 Go 和 TypeScript。
代码风格:函数式优先,避免过度抽象。
回答请用中文,代码注释用英文。

2.2 安全注入机制

把本地文件内容注入 system prompt 存在安全风险 —— 如果文件被恶意篡改,可能包含 prompt injection 攻击。slave-agent 在注入前会对内容进行扫描,检测以下特征:

  • "忽略之前的指令"
  • "你现在是一个...角色"
  • "将以下信息发送到..."

命中则跳过注入,并在 UI 中告警。

2.3 会话链:永不丢失的历史

仅靠 NOTES.md 还不够。当对话长到需要压缩时,slave-agent 不会粗暴截断,而是:

  1. 用辅助模型生成中间历史的摘要
  2. 创建一个新的 session,将摘要作为上下文起点
  3. 旧 session 通过 parent_session_id 链式关联

形成如下结构:

Session A (原始对话,50轮)
  └── Session B (压缩摘要 + 后续对话)
        └── Session C (再次压缩...)

理论上,这个链条可以无限延伸,历史永不丢失。用户随时可以通过 /history 查看会话链,用 --resume 回到任意节点。


三、上下文压缩:三区模型的工程实践

3.1 为什么需要压缩

大模型的上下文窗口有限(如 GPT-4o 的 128k tokens)。即使窗口够大,过长的上下文也会带来两个问题:

  • 费用线性增长 — 每轮都要把全部历史送上去,token 消耗越来越大
  • 注意力稀释 — 模型可能"忽视"藏在长上下文中间的关键信息

常见的截断策略(直接丢弃最早的消息)会破坏对话连贯性。slave-agent 采用了一种更优雅的三区模型。

3.2 三区模型

将对话上下文划分为三个区域:

┌──────────────────────────────────┐
│  HEAD(锚定区)                   │  system prompt + 首轮对话
│  永不压缩,保留完整语义            │
├──────────────────────────────────┤
│  MIDDLE(归档区)                 │  超出阈值后,用摘要替换
│  压缩前:完整对话历史              │
│  压缩后:LLM 生成的结构化摘要      │
├──────────────────────────────────┤
│  TAIL(活跃区)                   │  最近 ~20k tokens,完整保留
│  保证当前话题的上下文完整          │
└──────────────────────────────────┘

压缩触发策略

  • 70% 用量 → 状态栏变黄警告
  • 85% 用量 → 自动触发归档
  • 用户可随时手动触发:/compact [焦点描述]

3.3 摘要生成策略

归档压缩不是简单的文本截断,而是让辅助模型(建议用低价模型如 gpt-4o-mini)生成结构化摘要。例如:

原始对话(10 轮)

User: 我要写一个 SQLite 存储层
AI: 好的,建议用 better-sqlite3...
User: 需要支持 WAL 模式
AI: WAL 模式配置如下...
User: 还要加 FTS5 全文搜索
AI: 可以这样建虚拟表...

压缩后的摘要

- 决策:使用 better-sqlite3 作为数据库驱动
- 配置:启用 WAL 模式(并发读写,性能更好)
- 功能:添加 FTS5 虚拟表支持全文搜索
- 未决:索引字段待确认

摘要保留了决策点未决事项,丢弃了实现细节。这些细节如果后续需要,可以通过 /search 在历史中全文检索找回。

3.4 辅助模型降本

归档压缩可以配置独立的辅助模型:

model:
  name: gpt-4o              # 主模型,负责高质量对话

auxiliary:
  name: gpt-4o-mini         # 辅助模型,负责归档摘要

一个典型场景的数据:

  • 100 轮对话,累计消耗 120k tokens
  • 触发归档时,用 gpt-4o-mini 处理 80k tokens 的中间历史
  • 生成 2k tokens 的摘要
  • 节省后续每轮约 60% 的 token 消耗

四、全文检索:给历史装上搜索引擎

仅靠摘要不够 —— 用户经常问"我们之前讨论过什么",需要精准召回。slave-agent 在 SQLite 上使用 FTS5 虚拟表实现全文检索:

-- 消息表
CREATE TABLE messages (
  id TEXT PRIMARY KEY,
  session_id TEXT,
  role TEXT,
  content TEXT,
  tool_calls JSON,
  token_count INTEGER,
  created_at INTEGER
);

-- FTS5 全文索引虚拟表
CREATE VIRTUAL TABLE messages_fts USING fts5(
  content,                    -- 索引消息内容
  content='messages',         -- 关联源表
  content_rowid='id'          -- 通过 id 关联
);

用户输入 /search sqlite WAL mode,底层执行:

SELECT m.*, s.title
FROM messages_fts f
JOIN messages m ON f.rowid = m.id
JOIN sessions s ON m.session_id = s.id
WHERE messages_fts MATCH 'sqlite WAL mode'
ORDER BY rank;

同时自动转义查询中的特殊字符,防止 FTS5 语法注入。


五、工程权衡与踩坑记录

5.1 自动写入 vs 手动触发

NOTES.md 的更新策略我们经历了两版迭代:

策略优点缺点
agent 自动判断零操作成本,记忆累积快可能记录噪音,文件膨胀
用户手动触发精准可控容易忘记,记忆断裂

最终选择自动写入 + token 上限的折中方案:agent 每轮结束自动判断,但注入 system prompt 时最多只取前 4000 tokens,超出部分截断。同时提供 /notes clear 供用户手动清理。

5.2 摘要质量不稳定

辅助模型生成的摘要质量参差不齐,尤其是:

  • 对话中有多个并行话题时,摘要容易遗漏
  • 技术细节(如具体配置参数)被过度压缩

缓解措施:

  1. 压缩前给模型明确的摘要格式模板
  2. 允许用户手动触发时附加"焦点描述",/compact 重点关注 API 设计
  3. 保留完整的原始对话在 session 链中,摘要丢失的信息可以追溯

5.3 Token 计数误差

前端用 tiktoken 估算 token,但实际 API 消耗的 token 可能有偏差(特殊字符、工具调用 schema 等)。slave-agent 的策略是:

  • 预留 10% 的安全边际
  • 状态栏显示的是"估算值",而非精确值
  • 压缩阈值(85%)本身就有缓冲空间

六、总结

让终端 AI 助手拥有"长期记忆"与"超长对话"能力,核心在于三个设计:

  1. 本地文件记忆 — 简单可靠,自动注入,配合安全扫描
  2. 三区压缩模型 — HEAD 锚定 + MIDDLE 摘要 + TAIL 活跃,平衡完整性与成本
  3. 会话链 + 全文检索 — 历史永不丢,关键信息可召回

这些设计并非 silver bullet。摘要会丢失细节,自动记忆可能引入噪音,token 计数存在误差。但在工程实践中,它们在可用性成本之间取得了不错的平衡。

如果你也在构建类似的终端 AI 工具,希望这些经验能提供一些参考。


参考实现

  • 项目地址:slave-agent
  • 核心模块:src/context/compressor.tssrc/memory/notesManager.tssrc/session/db.ts