从 slave-agent 的实践经验出发,探讨如何让终端 AI 助手拥有"长期记忆"与"超长对话"能力。
一、问题的起点
使用大模型 CLI 工具时,你可能遇到过这样的场景:
- 聊了 30 轮代码审查后,AI "忘了" 你一开始说的项目架构约束
- 昨天讨论的设计方案,今天新开一个会话就全丢了
- 上下文窗口报警,不得不
/clear后重新开始
根本原因有两个:
- 上下文截断粗暴 — 超长对话时,系统往往直接丢弃最早的消息,导致关键信息丢失
- 跨会话无记忆 — 每个会话从零开始,无法累积项目上下文
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 不会粗暴截断,而是:
- 用辅助模型生成中间历史的摘要
- 创建一个新的 session,将摘要作为上下文起点
- 旧 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 摘要质量不稳定
辅助模型生成的摘要质量参差不齐,尤其是:
- 对话中有多个并行话题时,摘要容易遗漏
- 技术细节(如具体配置参数)被过度压缩
缓解措施:
- 压缩前给模型明确的摘要格式模板
- 允许用户手动触发时附加"焦点描述",
/compact 重点关注 API 设计 - 保留完整的原始对话在 session 链中,摘要丢失的信息可以追溯
5.3 Token 计数误差
前端用 tiktoken 估算 token,但实际 API 消耗的 token 可能有偏差(特殊字符、工具调用 schema 等)。slave-agent 的策略是:
- 预留 10% 的安全边际
- 状态栏显示的是"估算值",而非精确值
- 压缩阈值(85%)本身就有缓冲空间
六、总结
让终端 AI 助手拥有"长期记忆"与"超长对话"能力,核心在于三个设计:
- 本地文件记忆 — 简单可靠,自动注入,配合安全扫描
- 三区压缩模型 — HEAD 锚定 + MIDDLE 摘要 + TAIL 活跃,平衡完整性与成本
- 会话链 + 全文检索 — 历史永不丢,关键信息可召回
这些设计并非 silver bullet。摘要会丢失细节,自动记忆可能引入噪音,token 计数存在误差。但在工程实践中,它们在可用性和成本之间取得了不错的平衡。
如果你也在构建类似的终端 AI 工具,希望这些经验能提供一些参考。
参考实现
- 项目地址:slave-agent
- 核心模块:
src/context/compressor.ts、src/memory/notesManager.ts、src/session/db.ts