记忆、压缩与上下文工程
MEMORY & CONTEXT
状态:🔴 历史 · 最后核对 2026-08-06
2026-06-24 · 综合对话压缩审计、酒馆架构分析、漂移防御实践、向量化升级讨论
相关文档:architecture · CHANGELOG · competitive-analysis · character-consistency-research
⚠️ 本文档写于向量化升级之前,"现状"部分已全部过期(复核于 2026-07-28):
文中说 实际 §1.1 / §1.4 嵌入模型是 MiniLM 384d 已迁到 BGE-small-zh-v1.5 512d(中途试过 BGE-M3 1024d 又退回,CPU 推理 4s→9ms) §1.1 L3 检索含 BM25 BM25 已删除(2026-06-25,-197 行),改纯双集合稠密检索。
⚠️ 2026-08-05 差点原路走回去:backlog F20 登记的是「混合检索(关键词 + 向量)」,而我是从零重新推导出「词法帮倒忙」的,没先来看这一行。改成了交叉编码器重排,见 decisions§1.2 深聊每 5/20 轮压缩、核心叙事 ~1000 tokens、上下文 20 轮 改为每 10 轮合并 / 每 50 轮重写,叙事预算 5000 tokens,上下文 200 轮(2026-07-12) §2.3 检索是主动触发的 每轮被动检索已实施 §2.6 context 无硬上限 窗口化上下文已实施,50 轮硬上限 + 状态快照 第五节整章的"向量化升级路线" P0-P3 全部已实施,此章现在是历史记录而非计划 仍然有效的部分:第三节(SillyTavern 机制分析)、第四节(窗口化设计原理)、 第七节(总览图)。系统当前状态以 architecture.md §3.2/§3.10/§3.11 为准。
一、现状:我们有什么
1.1 三层记忆管道
用户消息
│
├─ L1 短期(原始消息)
│ 按语义 chunk 分块,嵌入漂移检测自动切块
│ 硬上限 200 条,锚点权重保护
│ 活跃 chunk ≤ 20 条保留在 context
│
├─ 触发压缩(>20条)→ 后台 LLM
│ ├─ 摘要 → L2 中期(30 条 slice)
│ │ 遗忘曲线 + 显著性 + 主题标签
│ │ Hub 升级:注入 ≥ 3 次 → L3
│ │
│ └─ 提取 → L3 长期(ChromaDB + BM25 RRF)
│ 每干员 500 条事实,去重 + 贝叶斯置信度
│ LLM Wiki:高分互动后自动写洞察
│
└─ 检索(每轮)
协调器 Z-Score 混排:L2/L3 各取 top-30
→ 双时间轴衰减 → 信赖维度调制 → 填充预算
1.2 深聊独立管道
深聊不经过 L1 压缩流水线。独立 SQLite 存储。
核心叙事(~1000 tokens)
├─ [ANCHORS] 星标时刻——告白、承诺、冲突和解——永不过期
├─ [ARC] 最近 20 轮连贯叙事 ~1200 字
└─ [ECHO] 20 轮以前模糊摘要 ~300 字
压缩节奏:每 5 轮增量合并 → 每 20 轮完整重写
辅助索引:细节索引(每轮提取 1-3,星标永不过期)
消息索引(用户消息 + 嵌入,200 条 FIFO)
情绪弧线(60 条记录,含身体锚点)
桥接到 L2/L3:结束 → memory capsule → L2 + L3
1.3 抗退化防御
| 机制 | 触发 | 原理 |
|---|---|---|
| 周期身份重锚定 | 每 15 轮 | 身份信息重新放到注意力 U 型右端点 |
| 元认知重述 | 每 10 轮 | LLM 在 <thinking> 自己重述身份+铁律 |
| 代理信号监控 | 长度递增/句式密度/躯体脱钩 → 2+ 触发 | 注入强制重锚定块 |
| Nautilus Compass | 每轮 embedding 对比行为锚点 | 偏离时注入 reaffirmation |
| 身份守卫 L1-L3 | 攻击/覆写检测 | 状态机防御 |
1.4 嵌入模型
当前:paraphrase-multilingual-MiniLM-L12-v2(384 维,2021 年)
备选:智谱 embedding-2(远端 API)
BM25:自写 bigram 分词 + 标准 BM25 公式(k1=1.2, b=0.75)
二、问题:哪里有缺口
2.1 压缩的结构性不对称
| 普通聊天 L1→L2 | 深聊 core_narrative | |
|---|---|---|
| 格式 | {topic, summary, key_facts} |
[ANCHORS] → [ARC] → [ECHO] |
| 锚定 | 无——摘要不含身份锚点 | 有——ANCHORS 段天然锚定 |
| 重写周期 | 被动(消息超阈值才触发) | 主动(每 5/20 轮定期) |
| 衰减精度 | 扁平——所有 chunk 摘要精度相同 | 三层——全精 / 精 / 模糊 |
普通聊天的压缩没有"锚定格式"。LLM 生成摘要时不包含"她是谁"和"她怎么说话"——这恰是我们在漂移分析里说的"朴素摘要加速漂移"。
2.2 没有压缩后验证
摘要直接写盘,不检查是否保留了关键约束。如果摘要在写入时已有偏差,后续所有检索都基于偏差。
2.3 检索是主动的,不是被动的
跨块回溯只在用户说"你还记得吗"时触发。酒馆的聊天向量化是每轮被动检索——不等用户问,自动拉回最相关的旧内容。
2.4 检索目标的"信息密度"问题
酒馆社区的共识:原始消息向量检索有致命缺陷——"几十条战斗描述淹没一条魔法剑的关键信息"。解决方案是先摘要再向量化。你们 L1→L2→L3 已经在做这个,但检索时 L2/L3 的结构化字段(topic_tags, emotion_valence, category)没有用于检索过滤,仅用于排序加权。
2.5 嵌入模型已旧
MiniLM 384 维是 2021 年模型,中文不是一等公民。BGE-M3(1024 维 + dense + sparse 双输出)是当前 SOTA,可以一个模型统一替代 MiniLM + 手写 BM25。
2.6 Context Window 没有硬上限
靠 L1 压缩延后问题——压缩是被动触发的(消息数超阈值),不是主动的(轮次到上限就切窗口)。深聊有 20 轮重写周期做结构重置,普通聊天没有。
三、酒馆(SillyTavern)的参考
3.1 核心机制:聊天向量化
每轮生成前:
1. 取最后 N 条消息 → embedding → 查询向量
2. 和聊天历史所有消息做余弦相似度
3. 保留最近 M 条不动(Retain# = 5)——保护当前对话流
4. 拉回 top-K 最相关的旧消息(Insert# = 3)
5. 注入到 prompt 开头或底部——"重排序"而非"追加"
本质:利用 LLM 注意力 U 型分布,把可能被遗忘的相关旧消息
搬到注意力高峰位置。和 periodic re-anchoring 同一理论基础。
3.2 社区方案的关键方向
| 模式 | 代表 | 要点 |
|---|---|---|
| 结构化事件 | VectFox | 提取 characters, locations, items, concepts 元数据 → 检索时字段过滤 |
| 记忆图谱 | ST-BME | 实体-关系-事件图谱 + 图扩散 + DPP 多样性采样 + LLM 精排 |
| 时间衰减 | VectHare | 指数/线性时间衰减,可设半衰期 |
| 认知架构 | ST-BME | 区分角色 POV / 用户 POV / 客观世界记忆 |
| 跨会话连续性 | CharMemory | 自动提取结构化记忆 → 新聊天也能唤起 |
3.3 酒馆的共同缺陷
- 没有漂移防御——答案是"用户自己注意,不行就开新聊天"
- 摘要退化——社区承认"1000+ 条消息后滚动摘要变成一团模糊"
- 信息密度不均——原始消息直接向量化会淹没关键信息
3.4 酒馆 vs 罗德岛
| 机制 | 酒馆 | 罗德岛 |
|---|---|---|
| 短期记忆 | 最近 N 条保留在 context | L1 活跃 chunk 保留原始消息(≤20 条触发压缩) |
| 中期记忆 | 滚动摘要(单块,越来越模糊) | L2 结构化 slice + 遗忘曲线 + 显著性 + 主题标签 |
| 长期记忆 | Data Bank 文件 RAG(手动上传) | L3 ChromaDB + BM25 RRF(自动提取) |
| 旧消息拉回 | 每轮被动检索 + 重排序 | 跨块回溯(主动触发——用户说"还记得吗") |
| 深层叙事 | 无 | deep_chat 三层结构 + 情绪弧线 + 详情索引 |
| 漂移防御 | 无 | 五层防御体系 |
| 压缩品质 | 滚动摘要退化 | 结构化 slice + 遗忘曲线 |
酒馆做得更好:每轮被动检索——不等用户触发,自动拉回相关记忆。 罗德岛做得更好:漂移防御、遗忘曲线、结构化记忆、叙事完整性保护。
四、窗口化上下文管理(新方案)
4.1 核心理念
当前:context 无限增长 → 压缩延后 → 重锚定防御 → 终究会退化
改为:context 硬上限 40-50 轮 → 窗口结束时主动采集状态快照
→ 新窗口从快照恢复 → context 永远在安全范围内
这不是替代现有系统——是给它加一层"结构重置"。窗口内的 L1/L2/L3 管道、漂移检测、身份守卫全部不变。窗口切换只是在管道之上做硬性约束,让 context 永远不超出安全长度。
4.2 窗口切换流程
窗口 N(第 1-50 轮)
│
│ 第 45 轮起:后台预采集状态快照
│ (复用现有 summarize_and_extract_task 的后台模式)
│
├─ 第 50 轮结束:窗口关闭
│ 采集:情绪状态 + 关系状态 + 关键事件([ANCHORS])
│ + 身体记忆 + 说话方式偏移
│
▼
窗口 N+1(第 51-100 轮)
│ 初始化:从快照恢复状态
│ system prompt 开头不是"之前发生了什么"
│ 而是"你是翎羽。你现在的状态是..."
│
└─ context window 从 0 开始,永远 ≤ 50 轮
4.3 状态快照格式
{
"window_id": 3,
"round_range": [101, 150],
"emotional_state": {
"tone": "温暖", "intensity": 4, "undertone": "疲惫",
"trajectory": "从警觉→放松→信任"
},
"relationship": {
"trust": {"professional": 62, "emotional": 71, "rapport": 55},
"impression": "嘴硬心软",
"recent_shift": "她开始在他面前不再检查望远镜"
},
"anchors": [
{"round": 123, "type": "promise", "text": "博士说'下次我陪你'——她没回答但耳朵红了"},
{"round": 138, "type": "confession", "text": "她说'不是因为你是博士——是因为你来了'"}
],
"somatic_memory": {
"residues": ["右肩紧张已缓解", "说话时不再看门口"],
"s_shift": {"warmth": +0.12, "tension": -0.08}
},
"speech_pattern": {
"avg_length": "比窗口 1 短了 30%",
"banned_pattern_triggers": 0,
"sentence_variety": "正常"
}
}
4.4 Per-operator 窗口大小
利用漂移信号数据动态调整:
| 干员 | 建议窗口 | 依据 |
|---|---|---|
| 凯尔希 | 60 轮 | 沉稳型,话少而精,退化慢 |
| 翎羽 | 50 轮 | 默认值 |
| 拉普兰德 | 35 轮 | 情绪波动大,对话节奏快 |
| 贝洛内 | 40 轮 | 平均会话最长,漂移风险高 |
五、向量化升级路线
5.1 四个维度总览
| 维度 | 做什么 | 效果 | 迁移代价 |
|---|---|---|---|
| 换模型 | BGE-M3 替代 MiniLM + BM25 | 中文质量 ↑↑,一个模型统一稠密+稀疏 | 中——需重建 ChromaDB |
| 表示方式 | 分段嵌入替代单向量 | 长摘要检索精度 ↑ | 低——不改模型,只改存储格式 |
| 降维量化 | Matryoshka 分层精度 | 去重 1024d / 检索 512d / 话题检测 256d | 极低——BGE-M3 天然支持截断 |
| 时间感知 | 向量尾部拼接时间特征 | 远期记忆天然排名更低 | 低——检索时计算 |
5.2 P0:BGE-M3 迁移
为什么先做:一个模型统一稠密 + 稀疏双路检索,删掉手写 bigram 分词器。中文语义区分度从 384 维提升到 1024 维。
怎么迁移(零停机):
- 新事实写入 BGE-M3 集合
- 旧事实留在 MiniLM 集合,随窗口切换逐步被新快照替代
- 检索时两个集合都查,合并结果
- 过渡期结束后删除 MiniLM 集合
需要注意:所有余弦阈值需要重新标定(话题切换 0.5、L2 相关性 0.35、L3 去重 0.08 都是在 384 维上调的,1024 维空间分布不同)。
5.3 P1:Matryoshka 分层精度
BGE-M3 天然支持截断——存全维,检索时按场景取前 N 维:
| 场景 | 维度 | 理由 |
|---|---|---|
| L3 去重(写入) | 1024 | 最高精度判断是否重复 |
| L2 检索(注入 prompt) | 512 | 中等精度足够,更快 |
| 话题漂移检测(每轮) | 256 | 只需粗粒度语义偏移判断,极快 |
零额外存储——同一批向量,不同场景不同截断长度。
5.4 P1:每轮被动检索(酒馆模式)
当前:用户说"你还记得吗" → 触发跨块回溯
改为:每轮生成前 → 用本轮对话语义做查询向量
→ 检索 L2 摘要 + L3 事实(不是原始消息)
→ 注入 top-3 最相关记忆到 prompt
复用现有 retrieve_zscore(),改触发条件。和酒馆的聊天向量化同原理,但检索目标是信息密度更高的摘要和事实,而非原始消息。
5.5 P2:结构化事件元数据
在 L2 摘要写入时,LLM 额外提取结构化字段:
{
"event_type": "emotional_breakthrough",
"characters_involved": ["翎羽", "博士"],
"location": "舰桥第五瞭望哨",
"concepts": ["哨位", "凌晨", "绒羽变色", "陪伴"],
"emotional_peak": true
}
检索时做元数据过滤 + 向量相似度混合排序——精度从"语义相似"提升到"语义 + 结构 = 精确过滤"。
5.6 P3:时间感知特征拼接
dense_vector (1024d) + [window_age, recency_score, anchor_weight] (3d)
查询向量同样拼接——余弦计算天然包含时间维度。远期记忆在向量空间里"自然地远离"当前查询。
六、实施优先级
近期(1-2 周)—— 低垂果实
| 项 | 改动量 | 说明 |
|---|---|---|
| 每轮被动检索 | 小 | 复用 retrieve_zscore(),改触发条件。不等用户问,每轮自动拉回相关记忆 |
| 压缩后格式验证 | 极小 | 摘要写入后跑 BANNED_PATTERNS 正则 + 长度检查 |
| L1→L2 锚定摘要 | 小 | SUMMARY_PROMPT 加一步:先逐字复制身份+铁律,再写摘要 |
中期(1 月)—— 结构性改进
| 项 | 改动量 | 说明 |
|---|---|---|
| BGE-M3 迁移 | 中 | 双集合过渡,统一稠密+稀疏,删掉手写 BM25 |
| Matryoshka 分层精度 | 小 | BGE-M3 截断参数,各场景独立阈值 |
| 窗口化上下文管理 | 大 | 窗口计数器 + 预采集 + 新窗口初始化 + 状态快照格式 |
长期(季度)—— 深度优化
| 项 | 改动量 | 说明 |
|---|---|---|
| 结构化事件元数据 | 中 | SUMMARY_PROMPT 扩展 + L2 格式升级 + 元数据过滤检索 |
| 时间感知特征拼接 | 小 | 3 维时间特征拼入 L3 向量 |
| Per-operator 窗口大小自适应 | 中 | 基于漂移信号数据动态调整窗口上限 |
| 记忆图谱 | 大 | 评估 ST-BME 模式是否适合罗德岛的规模 |
七、总览
┌──────────────────────────────────────────────────────────────┐
│ Context Window(≤ 50 轮) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────┐ │
│ │ 氛围块 │ │ 状态快照 │ │ 被动检索 │ │ 硬指令垫底 │ │
│ │ (感官世界)│ │ (身份+关系)│ │ (L2+L3) │ │ (铁律+CoT) │ │
│ └──────────┘ └──────────┘ └──────────┘ └─────────────┘ │
│ │
│ 窗口内防御:周期性重锚定 · 元认知重述 · 信号监控 · 身份守卫 │
└──────────────────────────────────────────────────────────────┘
│ 窗口关闭
▼
┌──────────────────────────────────────────────────────────────┐
│ 状态快照(跨窗口桥接) │
│ 情绪 · 关系 · ANCHORS · 身体记忆 · 说话方式偏移 │
└──────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────┐
│ 长期记忆管道(持续运行) │
│ │
│ L1(原始消息)→ L2(结构化摘要)→ L3(向量化事实) │
│ │ │
│ BGE-M3 1024d + Matryoshka 分层 │
│ 稠密+稀疏双路 + 结构化元数据过滤 │
│ 每轮被动检索 + 时间感知 │
└──────────────────────────────────────────────────────────────┘