概述
最近用 Erlang 完整实现了 SillyTavern(俗称”酒馆”)的双记忆系统——即 Vector RAG + 滚动摘要的架构。这个项目让我对 RAG 系统的设计和实现有了更深的理解。本文是对整个实现过程的总结,记录了已实现的功能、踩过的坑,以及还有哪些可以改进的地方。
背景
SillyTavern 是一个流行的 AI 聊天前端,底层对接各种 LLM API。它的记忆系统设计得相当精巧:同时维护两套记忆——向量记忆负责精确检索历史细节,摘要记忆负责维持宏观上下文。
我的目标是用 Erlang 重写这套系统,适配自己的聊天后端。核心需求很明确:
- 聊天记录能自动存入向量数据库
- 检索时能召回最相关的历史消息
- 长时间聊天能自动摘要,保持上下文连贯
核心架构
双记忆系统
系统本质上是一个双通道并行架构:
| 通道 | 技术 | 作用 |
|---|---|---|
| Vector RAG | Embedding + 向量检索 | 精确回忆细节,找到最相关的历史片段 |
| Summarize | LLM 滚动摘要 | 压缩长期记忆,保持宏观上下文 |
两套系统独立运行,最终通过 build_memory_prompt 合并注入到 LLM 的 system prompt 中。
数据流
1 | 用户消息 → sync_chat → 计算哈希去重 |
已实现功能
经过对照 SillyTavern 源码分析,核心功能已基本对齐:
✅ 双记忆架构
build_memory_prompt/2同时处理向量检索和摘要两套并行通道- 最终合并注入 prompt,格式为
## Summary+## Past events
✅ 增量同步
sync_chat/2实现哈希去重- 对比已存消息,仅新增增量,逻辑完整
✅ 向量检索
jiuguan_embedding支持批量 embeddingjiuguan_db:search_similar/5实现余弦相似度检索 + 阈值过滤 (MIN_SIMILARITY = 0.25)query_chat_memory/2使用最近 2 条消息构建查询
✅ 滚动摘要
maybe_update_summary/2每 10 条消息触发摘要do_update_summary/3用 LLM 合并旧摘要 + 新内容- 逻辑与 SillyTavern 的 Summary Extension 对齐
✅ 存储层
- 使用 PostgreSQL + pgvector(比 SillyTavern 原生的 vectra 文件索引更健壮)
- 支持按 collection_id 隔离多聊天会话
- 提供
purge/1清空功能
踩过的坑
1. rearrangeChat 理解错误
最初我把”检索后注入”理解成了简单的”追加到 prompt 末尾”。实际源码逻辑是:
- 从原始 chat 数组中删除已检索的消息
- 用压缩后的格式注入
这一步我没有完全对齐,导致 prompt 可能会包含重复信息。
2. 摘要存储位置
SillyTavern 把摘要存在 chat message 的 extra.memory 字段里,通过 setExtensionPrompt 注入。我则是单独存到数据库的 jiuguan_summaries 表,然后手动拼接到 system prompt。
两种方式效果类似,但数据生命周期管理略有不同。
3. Embedding 来源
SillyTavern 支持 18+ 种 embedding 来源(OpenAI、Ollama、llama.cpp、transformers 等),我的实现暂时只支持 OpenAI 一种。
未实现的功能
| 功能 | 优先级 | 说明 |
|---|---|---|
| Embedding 来源抽象 | 中 | 目前硬编码 OpenAI |
| 消息预摘要 | 低 | 向量化前先用 LLM 压缩 |
| 消息分块 | 低 | 长消息递归分块 |
| 上下文管理 | 中 | 检索后从 chat 数组移除 |
| 跨集合查询 | 低 | 多 collection 合并搜索 |
| 文件/World Info RAG | 低 | 仅实现聊天记录 RAG |
总结
这次实现基本达到了预期目标——聊天记忆的核心流程跑通了。向量检索 + 滚动摘要这套双通道架构,在实际对话中能有效帮助 LLM”记住”之前的上下文。
核心差距主要在可选的增强功能上。如果当前只需要基础的聊天记忆功能,现有实现已经够用。下一步可以考虑:
- 补齐 embedding 来源抽象(支持本地模型)
- 修正 rearrangeChat 的上下文压缩逻辑
- 添加消息分块策略
源码地址略。后续有空会继续迭代。