不提取不总结,照样96.6%召回率——MemPalace怎么用原文打败LLM记忆系统
一个反直觉的结论
4月5号在GitHub上出现了一个项目叫MemPalace,到今天17天,48000多星。做的事情很简单:给AI Agent加记忆。
但它的做法跟主流方案完全相反。
Mem0用LLM提取事实("用户喜欢PostgreSQL")。Mastra用GPT观察对话再总结。Supermemory搞了个多Agent团队做搜索。这些方案都有一个共同假设:需要AI来决定记住什么。
MemPalace不提取,不总结,不让LLM碰你的对话。它把原文一字不改存进去,然后用ChromaDB的默认embedding搜。
LongMemEval测试500个问题,召回率96.6%。不用任何API key,不用任何LLM,纯本地跑。
这个数字意味着什么?Mem0在LoCoMo基准上的QA准确率大概66.9%。虽然两个指标不完全一样(一个是检索召回率,一个是端到端QA),但差距已经说明问题了。
为什么"不提取"反而更好
先说一个我在用Mem0时碰到的问题。
有一次我跟Agent讨论了为什么从MongoDB迁移到PostgreSQL。讨论了半小时,涉及到迁移成本、团队技术栈、读写比例等好几个维度。Mem0提取出来的记忆是一行字:"用户偏好PostgreSQL"。
一周后我问Agent"当时为什么选了PostgreSQL",它只知道我"偏好"PostgreSQL,但说不出任何理由。因为提取过程把讨论的上下文全丢了。
MemPalace的作者在benchmark文档里写了这段话,说得很准确:
When an LLM extracts "user prefers PostgreSQL" and throws away the original conversation, it loses the context of why, the alternatives considered, the tradeoffs discussed. MemPal keeps all of that, and the search model finds it.
本质上就是信息论的问题。提取是有损压缩,而且你没法提前知道未来会需要哪些被丢掉的细节。
Palace架构:Wing → Room → Drawer → Closet
MemPalace没有把所有内容扔进一个向量库了事。它用了一套建筑比喻来组织数据:
- Wing:按人或项目分区。比如一个Wing放你的工作项目,另一个Wing放个人事务
- Room:Wing下面按话题分区
- Drawer:存原始内容的最小单元,一个Drawer就是一段原始对话或文档片段
- Closet:轻量级的索引层,存的是从Drawer内容里提取的话题指针
Closet不存原文,它存的是这种格式的指针行:
built graphql migration|PostgreSQL;MongoDB;TypeORM|→drawer_001,drawer_002,drawer_003
左边是话题(action verb + context),中间是实体,右边指向实际的Drawer ID。
搜索时先在Closet层做快速匹配,命中后再去Drawer里拉原文。这比直接在全量原文上做向量搜索快很多,尤其是数据量大的时候。
源码拆解:Closet构建管线
看一下palace.py里的build_closet_lines函数,这是构建索引指针的核心逻辑:
CLOSET_CHAR_LIMIT = 1500
CLOSET_EXTRACT_WINDOW = 5000
def build_closet_lines(source_file, drawer_ids, content, wing, room):
drawer_ref = ",".join(drawer_ids[:3])
window = content[:CLOSET_EXTRACT_WINDOW]
# 提取出现2次以上的专有名词作为实体
words = _candidate_entity_words(window)
word_freq = {}
for w in words:
if w in _ENTITY_STOPLIST:
continue
word_freq[w] = word_freq.get(w, 0) + 1
entities = sorted(
[w for w, c in word_freq.items() if c >= 2],
key=lambda w: -word_freq[w],
)[:5]
# 提取动作短语作为话题
topics = []
for pattern in [
r"(?:built|fixed|wrote|added|pushed|tested|created|decided|"
r"migrated|reviewed|deployed|configured|removed|updated)"
r"\s+[\w\s]{3,40}",
]:
topics.extend(re.findall(pattern, window, re.IGNORECASE))
# markdown标题也算话题
for header in re.findall(r"^#{1,3}\s+(.{5,60})$", window, re.MULTILINE):
topics.append(header.strip())
# 组装指针行:topic|entities|→drawer_ref
lines = []
for topic in topics:
lines.append(f"{topic}|{entity_str}|→{drawer_ref}")
return lines
几个设计细节拿出来说说:
-
CLOSET_EXTRACT_WINDOW = 5000。只扫描前5000个字符,不扫全文。实体在文档开头的出现频率最高,扫全文收益不大但成本高。
-
实体阈值是出现2次。只出现一次的词大概率是噪音。
-
_ENTITY_STOPLIST过滤掉常见干扰词。The、This、User、Assistant、Monday这些词频繁出现但没有检索价值,直接排除。
-
话题用动作动词匹配。built、fixed、deployed这些词天然带上下文——"deployed kubernetes cluster"比"kubernetes"语义更精确。
Hybrid检索管线
MemPalace的检索分三层,逐层增加复杂度:
Raw模式(默认):纯ChromaDB语义搜索,96.6% R@5。一行代码的事。
Hybrid V4模式:在语义搜索基础上加了三个boost:
- 关键词匹配提升(query里出现的关键词命中drawer,加分)
- 时间近邻提升(最近的对话优先级更高)
- 偏好模式提取(检测"我喜欢X""我不用Y"这类表达)
Hybrid V4在留出的450个问题上做到了98.4%。
Rerank模式:取Top-20候选,用LLM(Haiku、Sonnet、甚至Ollama上跑的minimax-m2.7都行)做二次排序。满分500/500。
这里有意思的是,Haiku就够了。不需要大模型做rerank,因为rerank的输入已经被Hybrid管线缩到只有20条候选,判断"这条跟问题相关吗"对小模型来说很简单。
Backend抽象层
MemPalace的存储后端是可插拔的。backends/base.py定义了一套ABC接口:
class BaseCollection(ABC):
@abstractmethod
def add(self, *, documents, ids, metadatas=None, embeddings=None): ...
@abstractmethod
def query(self, *, query_texts=None, query_embeddings=None,
n_results=10, where=None, include=None) -> QueryResult: ...
@abstractmethod
def delete(self, *, ids=None, where=None): ...
@abstractmethod
def count(self) -> int: ...
默认是ChromaDB,但接口设计清晰到可以换成任何向量库。QueryResult和GetResult是带类型的dataclass,还保留了dict兼容(__getitem__和get),方便迁移老代码。
PalaceRef这个value object把palace的id、本地路径、namespace分开了。这意味着同一套接口可以同时服务本地存储和远程租户模式,不用改业务代码。
@dataclass(frozen=True)
class PalaceRef:
id: str
local_path: Optional[str] = None
namespace: Optional[str] = None
实际用起来
装起来很快:
pip install mempalace
mempalace init ~/projects/myapp
把你的Claude Code对话历史挖进去:
mempalace mine ~/.claude/projects/ --mode convos
搜索:
mempalace search "为什么从MongoDB迁到PostgreSQL"
它还提供了29个MCP工具,直接在Claude Code或Gemini CLI里用。包括palace的读写、知识图谱操作、跨Wing导航、Agent日记本。
知识图谱部分也挺有意思:它不光做向量检索,还维护了一个带时间窗口的实体关系图,存在本地SQLite里。你可以查"某个实体在某个时间段的状态变化",这在做项目决策回溯时很有用。
踩坑记录
实际用了两天,碰到几个问题:
1. 中文分词不太行
_candidate_entity_words用的是正则匹配,主要覆盖英文的CamelCase和Title Case。中文人名、项目名很难被识别为实体。这个问题在i18n目录下有locale JSON配置,但中文的pattern还很初步。如果你的对话主要是中文,Closet层的实体提取基本废了,退化成纯语义搜索。
解决办法:在mine之前把中文人名加到entity pattern里,或者等官方的中文支持完善。
2. normalize_version升级会触发全量重建
palace.py里有个NORMALIZE_VERSION = 2。每次这个版本号变,file_already_mined就返回False,下次mine时所有文件重新走一遍。如果你的palace里有上万个文件,重建要跑很久。
NORMALIZE_VERSION = 2 # v2 (2026-04): strip_noise() for Claude Code JSONL
建议在升级前先确认数据量,数据多的话用--wing参数分批处理。
3. mine锁机制是文件锁
多个Agent同时mine同一个文件会冲突。MemPalace用了fcntl文件锁来防这个:
@contextlib.contextmanager
def mine_lock(source_file: str):
lock_path = os.path.join(lock_dir,
hashlib.sha256(source_file.encode()).hexdigest()[:16] + ".lock")
lf = open(lock_path, "w")
fcntl.flock(lf, fcntl.LOCK_EX)
yield
fcntl.flock(lf, fcntl.LOCK_UN)
Windows上用的是msvcrt.locking。跨平台没问题,但如果你在NFS或网络文件系统上跑,fcntl可能不可靠。建议把palace放在本地磁盘。
Benchmark对比数据
放一组LoCoMo基准的数据,这个比LongMemEval更难(1986个多跳问答):
| 方案 | R@10 | 是否需要LLM |
|---|---|---|
| MemPalace Hybrid v5 + Sonnet rerank | 100% | 是 |
| MemPalace bge-large + Haiku rerank | 96.3% | 是 |
| MemPalace Hybrid v5 (无LLM) | 88.9% | 否 |
| MemPalace Session基线 | 60.3% | 否 |
| Mem0 (RAG) | 30-45%* | 是 |
*Mem0数据来自ConvoMem基准,非LoCoMo直接对比。
最有意思的是Wings v3的设计。它给每个说话者分配独立的Closet,说话者自己的发言存原文,对方的发言只标[context]。这个设计直接把"对抗性问题"(故意混淆说话者的问题)的召回率从34%拉到92.8%。
总结
MemPalace给AI记忆领域提了一个好问题:你真的需要LLM来管理记忆吗?
从benchmark结果看,答案是"存储阶段不需要"。原文逐字存储 + 好的embedding + 结构化索引,就能干到96.6%。LLM只在检索阶段的rerank步骤有价值,而且Haiku这种小模型就够了。
装一下,把你最近的Claude Code对话挖进去,搜一搜两周前讨论过的技术决策,感受一下差别。