从确定性优先到持续回忆:Agent长期记忆检索的工程新思路
2026/8/29 7:25:22 网站建设 项目流程

做 Agent 的朋友最近应该都遇到过同一个问题:聊到一半,它把前面的结论忘了;你昨天明确告诉它的偏好,今天再问就变成“我理解你的意思”;哪怕是同一个问题、同一个上下文,两次回答还能给出不同的依据。记忆一直是大模型应用从 Demo 走向生产的最大短板,但大多数团队的第一反应还是“上向量数据库”“加 RAG”,结果召回结果仍然玄学。

最近 Hacker News 上出现了一个很有意思的项目:CueMap。它把记忆检索的思路从“语义相似优先”改成“确定性优先”,核心技术点可以拆成三个词:deterministic-first(确定性优先)、memory retrieval(记忆检索)、continuous recall(持续回忆)。这篇文章不打算复述项目说明,而是想把背后的设计逻辑讲透:为什么相似度检索不够用,确定性优先解决了什么,用什么结构落地,以及它适合哪些场景、不适合哪些场景。读完后,你至少能判断自家 Agent 的记忆模块要不要往这个方向改,也能照着文中的最小示例自己跑通一套雏形。

1. 这篇文章真正要解决的问题

先说结论:当前大多数 Agent 的“长期记忆”本质上是一个黑盒相似度搜索,它对“连续回忆”这件事支持得非常差。所谓连续回忆,不只是“记住一条信息”,而是指系统可以在很长的时间跨度里,用不同的线索稳定地把相关记忆找回来。比如用户三个月前说“我不吃香菜”,今天点菜时提到“有什么忌口”,一个好记性应该能自动关联并稳定命中。这里有两个关键词:关联,以及稳定。

传统向量检索擅长关联,但不擅长稳定。原因在于,向量检索是近似搜索,它依赖 Embedding 模型把文本映射到高维空间,而同一句话在不同模型、不同改写形式下余弦相似度都会波动。你存入一条记忆时用的是用户原话,但用户之后提问会换一种说法,两个文本的向量距离很可能不够近,导致这条记忆根本不会被召回。更麻烦的是,向量检索结果缺乏可解释性,你很难回答“为什么这次没找到”。

CueMap 提倡的 deterministic-first 思路,是先把记忆检索拆成两个阶段。第一阶段用完全确定性的规则和索引去匹配,比如精确键、结构化字段、时间戳、标签、显式关联;第一阶段没有命中时,再退回语义检索。这个顺序看起来简单,但它带来几个无法忽视的好处:可复现、可审计、时延低、不需要昂贵模型参与。本文后面会给出这个思路的完整实现雏形。

什么人最应该读这篇文章?如果你在做 RAG 问答、Agent 长期记忆、个人知识库、客服机器人,或者正在为“记忆不稳定”发愁,这篇文章值得从头到尾读完。如果你只是想把 SQLite 里几十条配置塞进 Prompt,那 CueMap 的思路对你也可能有启发,但你需要的是更轻的解决方案。

2. CueMap 是什么:把“提示”变成一张可导航的地图

先拆名字。Cue 是“提示线索”,Map 是“地图”。CueMap 这个名字非常直白:它把记忆的检索过程,从“大海捞针”变成“按图索骥”。传统向量检索就像是让你在漆黑的仓库里靠嗅觉找东西,而 CueMap 的思路是“给仓库装上标签和货架编号,先按编号找,编号找不到再全场扫描”。

从项目描述看,CueMap 是一个偏系统设计的项目,解决的是大模型长期记忆的提取与召回问题。它没有把重点放在“用什么模型生成记忆”,而是放在“怎么样把记忆找回来”。这个切入点值得注意,因为多数团队把精力花在记忆的写入和存储上,却低估了读取的难度。实际工程里,写入是可控的,读取是实时的、面向用户的、不可控的。你无法要求用户按你存储时的措辞来提问,所以读取策略必须足够鲁棒。

如果把 CueMap 和常见方案放在一起对比,边界会更清楚:

方案匹配方式确定性可解释性时延典型成本
关键词搜索(BM25)词项命中等,有排序较高
向量相似度检索语义空间近似中高(Embedding)
混合检索关键词 + 向量分值融合中高
CueMap 风格确定性索引优先,语义兜底低到中低到中

这里不是要否定向量化检索的价值。语义检索非常适合开放式问题、模糊描述和跨语言场景。但它的缺陷在于,当系统需要“用户提出一个线索,立即精确召回对应记忆”时,向量检索的近似性会成为负担。CueMap 的判断是:先给出可信的硬匹配路径,再让语义检索处理剩余不确定部分。

这个设计背后其实是一个很朴素的工程原则:能用确定逻辑解决的,就不要依赖概率模型。这个原则在搜索、数据库、规则引擎里已经存在几十年,只是到了大模型时代,大家反而忘了它。

3. 核心概念:deterministic-first 到底是什么意思

deterministic-first 不是一个严格学术名词,而是一条检索优先级策略:凡是能被显式键、规则、结构化条件命中的查询,永远优先走确定性路径。只有确定性路径失败时,才允许模糊匹配登场。

举个例子。你往记忆库里写入这样一条记录:

{ "id": "mem_000123", "content": "用户不喜欢吃香菜", "user_id": "user_7788", "tags": ["饮食偏好", "忌口"], "created_at": "2025-03-17T10:00:00Z", "source": "conversation_42" }

如果用户问“我的忌口是什么”,系统可以先用tags精确匹配“忌口”,或者用“饮食偏好”作为维度键,直接命中mem_000123。这一步完全不需要 Embedding,不需要向量距离,命中了就是命中了。

但用户很可能问的是“我今天出去吃饭有什么注意事项”,这句话里没有任何词和“忌口”重叠。此时确定性路径失败,系统才调向量检索,去匹配语义相关的记忆。于是组成这样的检索链路:

Query → 规则/键/标签/时间/关联匹配 → 命中则返回 → 未命中 → 语义相似度检索 → 排序返回 TopK → 语义也未命中 → 返回空或通用兜底

这是一个“级联式检索”(Cascade Retrieval)。它的关键价值在于,大多数记忆查询在第一个阶段就可以收口。因为现实中的很多检索意图是高度结构化的,比如“这个用户的偏好是什么”“这个项目的配置参数是多少”“上次部署是什么时间”,这些本来就不该靠向量去猜。

deterministic-first 还有一个容易被忽视的收益:它是可审计的。生产环境里,如果用户投诉“系统记错了”,你可以导出命中链路,精确复现是哪个键命中了哪条记录。而纯向量检索几乎不具备这个能力,你只能复现一个近似分值,说不清为什么。

当然,确定性优先也有代价。它要求你为数据建模,提前设计好字段、标签、键结构。这比“一股脑塞进向量库”要麻烦。CueMap 的价值主张是:这个前期成本换来的稳定性和可解释性,对长期记忆系统来说非常划算。

4. 为什么 continuous recall 是硬骨头

连续回忆(continuous recall)这个概念,字面意思是“长期持续地把记忆找回来”。它和一般问答检索最大的区别在于时间跨度和线索变化。

短期记忆场景里,上下文就在 Prompt 里,检索压力很小。长期记忆则会遇到三个相互叠加的难题。

第一,线索漂移。用户三个月前说“我不喜欢太辣的东西”,今天可能问“上次那家川菜馆后来去了吗”。两条信息语义上有关系,但字面几乎不重叠。靠纯向量检索,很容易因为 Embedding 方向偏差而丢失关联。

第二,记忆衰退。向量检索本质上是“打分排序”,不是“精确取出”。即便某条记忆确实存在,只要 query 的表示靠近另一个不相关的记忆簇,正确记忆的排名就会被挤压出 TopK。这种失败是随机性的,这次能召回,下次就召回不了,很难调试。

第三,上下文碎片化。长期记忆中,一条完整信息经常被拆成多个片段存储,比如时间、地点、人物分别写成了三条记忆。如果检索时只命中其中一条,Agent 组合出的答案就是残缺的。

continuous recall 真正需要的不是“更聪明的相似度计算”,而是“一套让记忆可定位的结构”。CueMap 的对应解法是:把记忆组织成有线索的地图。每条记忆不是一个孤立的向量,而是一个携带确定性关联的节点,可以通过多个入口稳定进入。比如“用户 A 不喜欢香菜”这个节点,同时挂 tags(饮食偏好)、participants(user_7788)、event(聚餐记录)等多个键。任何一个键被请求命中,都能把这条记忆拉出来。

这种设计很像数据库里的多列索引,而不是全表扫描。它没法保证智能,但能保证“有路可走”。在长期记忆场景里,“有路可走”比“碰运气命中”可靠得多。

5. 从概念到结构:一个 CueMap 风格系统怎么设计

理解了 deterministic-first 之后,可以自己设计一套最小系统。本文不提供 CueMap 官方 SDK 的调用代码,因为项目本身可能仍在迭代,更值得学习的是它的设计模式。下面给出一个兼容 CueMap 思路的最小架构。

核心组件有三个:MemoryStore(记忆存储)、CueIndex(确定性线索索引)、HybridRetriever(混合检索器)。

MemoryStore 负责保存记忆主记录,可以用关系型数据库、KV 存储甚至 JSON 文件承载。每条记忆必须有稳定 ID、内容、创建时间和至少一个可索引字段。

CueIndex 负责维护确定性入口。它由若干“线索”组成,每个线索是一个键值对或多字段组合。常见的线索包括:用户 ID、标签、记忆类型、时间范围、业务实体 ID。CueIndex 可以是内存哈希表,也可以是数据库索引。

HybridRetriever 负责执行级联检索,先走 CueIndex,miss 再走向量检索。为了提高质量,建议给每条记忆增加一个semantic_embedding字段,但不要让向量成为主索引。

来看一个数据表设计的例子:

CREATE TABLE memories ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, content TEXT NOT NULL, memory_type TEXT NOT NULL, tags TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, metadata TEXT, embedding BLOB ); CREATE INDEX idx_memories_user ON memories(user_id); CREATE INDEX idx_memories_type ON memories(memory_type); CREATE INDEX idx_memories_tags ON memories(tags); CREATE INDEX idx_memories_created ON memories(created_at);

这里的 user_id、memory_type、tags 就是确定性线索。在实际业务里,你可以根据领域扩展字段,比如project_iddevice_idcategory。字段越贴近业务,确定性检索的命中率越高。

检索流程可以用下面这段 Python 伪代码表达:

def retrieve(query, user_id, k=5): # 第一阶段:确定性线索匹配 candidates = cue_index.match( query=query, user_id=user_id, memory_type_hint=extract_type(query), tag_hint=extract_tags(query) ) if candidates: return candidates[:k] # 第二阶段:语义向量兜底 query_vec = embed(query) semantic_hits = vector_store.search( query_vec, top_k=k, filter={"user_id": user_id} ) return semantic_hits

这个流程只是一个骨架,但已经能看出 deterministic-first 的完整意图。第一阶段命中率高、时延低、结果稳定;第二阶段负责处理开放式表达。两者不是互斥关系,而是互补关系。

6. 完整示例:最小可用的 deterministic-first 记忆系统

为了让思路可落地,这里给出一个可以直接跑起来的最小实现。不需要重型框架,只要 Python 3.9 以上版本,再加一个 SQLite 数据库即可。该示例不模拟向量检索,而是用文件路径形式演示级联逻辑,方便你在没有 Embedding 服务时也能启动。

项目目录结构如下:

cue_map_demo/ ├── memory_store.py ├── retriever.py ├── config.yaml └── main.py

6.1 记忆存储层

新建memory_store.py

# 文件路径:cue_map_demo/memory_store.py import sqlite3 import json from datetime import datetime, timezone class MemoryStore: def __init__(self, db_path: str = "memories.db"): self.conn = sqlite3.connect(db_path) self.conn.row_factory = sqlite3.Row self._init_schema() def _init_schema(self): self.conn.execute( """ CREATE TABLE IF NOT EXISTS memories ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, content TEXT NOT NULL, memory_type TEXT NOT NULL, tags TEXT, created_at TEXT NOT NULL, metadata TEXT ) """ ) self.conn.execute( "CREATE INDEX IF NOT EXISTS idx_user ON memories(user_id)" ) self.conn.execute( "CREATE INDEX IF NOT EXISTS idx_type ON memories(memory_type)" ) self.conn.commit() def add_memory( self, memory_id: str, user_id: str, content: str, memory_type: str, tags: list, metadata: dict = None, ): now = datetime.now(timezone.utc).isoformat() self.conn.execute( """ INSERT INTO memories (id, user_id, content, memory_type, tags, created_at, metadata) VALUES (?, ?, ?, ?, ?, ?, ?) """, ( memory_id, user_id, content, memory_type, json.dumps(tags, ensure_ascii=False), now, json.dumps(metadata or {}, ensure_ascii=False), ), ) self.conn.commit() def query_by_user_and_type(self, user_id: str, memory_type: str): cursor = self.conn.execute( "SELECT * FROM memories WHERE user_id = ? AND memory_type = ?", (user_id, memory_type), ) return [dict(row) for row in cursor.fetchall()] def query_by_tag(self, user_id: str, tag: str): rows = [] for row in self.conn.execute( "SELECT * FROM memories WHERE user_id = ?", (user_id,), ): record = dict(row) tags = json.loads(record["tags"]) if tag in tags: rows.append(record) return rows

这个文件的核心是 SQLite 查询。query_by_user_and_typequery_by_tag都是确定性检索,不需要任何模型参与。

6.2 检索器

新建retriever.py

# 文件路径:cue_map_demo/retriever.py from memory_store import MemoryStore class CueMapRetriever: def __init__(self, store: MemoryStore): self.store = store def retrieve(self, query: str, user_id: str): # 第一层:关键词规则,识别是否存在明确类型线索 type_hint = self._extract_type_hint(query) if type_hint: hits = self.store.query_by_user_and_type(user_id, type_hint) if hits: return {"stage": "deterministic", "items": hits} # 第二层:标签匹配 tag_hint = self._extract_tag_hint(query) if tag_hint: hits = self.store.query_by_tag(user_id, tag_hint) if hits: return {"stage": "deterministic", "items": hits} # 第三层:模拟语义兜底 return {"stage": "semantic_fallback", "items": []} def _extract_type_hint(self, query: str): type_mapping = { "偏好": "preference", "忌口": "preference", "配置": "configuration", "部署": "deployment", } for keyword, memory_type in type_mapping.items(): if keyword in query: return memory_type return None def _extract_tag_hint(self, query: str): tag_mapping = { "香菜": "饮食", "辣": "饮食", "环境": "部署", "端口": "部署", } for keyword, tag in tag_mapping.items(): if keyword in query: return tag return None

这里的关键是retrieve方法的三层级联。它先用规则抽取类型线索,再用标签抽取,两者都失败才进入兜底。实际生产中,规则抽取可以替换成更精确的实体识别,但优先级顺序不会变。

6.3 配置文件

新建config.yaml

memory: db_path: "memories.db" deterministic_first: true fallback_strategy: "semantic_vector" retriever: type_hint_keywords: 偏好: "preference" 忌口: "preference" 配置: "configuration" 部署: "deployment" tag_hint_keywords: 香菜: "饮食" 辣: "饮食" 环境: "部署" 端口: "部署"

这个配置文件的价值在于把规则和代码解耦。后面扩充线索词时,不需要改 Python 代码。

6.4 入口程序

新建main.py

# 文件路径:cue_map_demo/main.py from memory_store import MemoryStore from retriever import CueMapRetriever def main(): store = MemoryStore() store.add_memory( memory_id="mem_0001", user_id="user_7788", content="用户不喜欢吃香菜,点餐时不要加香菜", memory_type="preference", tags=["饮食", "忌口"], metadata={"channel": "menu_agent"}, ) retriever = CueMapRetriever(store) test_queries = [ "我的忌口是什么?", "今天聚餐有什么注意事项?", "帮我看看生产环境的部署配置", ] for query in test_queries: result = retriever.retrieve(query, user_id="user_7788") print(f"Query: {query}") print(f"Stage: {result['stage']}") print(f"Items: {result['items']}") print("---") if __name__ == "__main__": main()

运行方式:

cd cue_map_demo pip install pyyaml python main.py

预期输出大致如下:

Query: 我的忌口是什么? Stage: deterministic Items: [{'id': 'mem_0001', 'content': '用户不喜欢吃香菜,点餐时不要加香菜', ...}] --- Query: 今天聚餐有什么注意事项? Stage: semantic_fallback Items: [] --- Query: 帮我看看生产环境的部署配置 Stage: deterministic Items: [] ---

注意第三条查询没有命中,因为记忆库中没有配置类型的记录,这是符合预期的。如果之后添加相关记录,确定性路径就会命中。

7. 实际验证与效果评估

一个记忆系统不能只看“能不能跑通”,还要看检索质量和稳定性。建议从四个维度评估。

7.1 命中率

在测试集上统计“正确记忆是否出现在 TopK 结果中”。对 deterministic-first 系统来说,命中率要拆开统计:确定性阶段命中率、语义兜底命中率、整体命中率。如果整体命中率低,先看确定性阶段的线索词覆盖率。

评估命令可以这样写:

python main.py > output.txt grep "Stage: deterministic" output.txt | wc -l

7.2 可复现性

同一个查询,连续运行 10 次,结果是否一致。纯向量检索受 Embedding 模型和距离计算影响,结果可能出现微小波动;确定性检索则应该 100% 一致。如果出现不一致,说明代码里存在随机逻辑或查询排序不稳定。

7.3 时延

分别统计确定性阶段和语义兜底阶段的耗时。确定性阶段一般应在毫秒级,语义兜底则依赖模型推理。如果检索接口被频繁调用,建议对确定性阶段做缓存。

7.4 失败模式

当返回空结果时,系统应该能区分“记忆确实不存在”和“记忆存在但没找到”。理想设计是:确定性阶段 miss 后,在日志里记录 query、候选键、兜底结果;如果语义兜底也 miss,再记录一次。这样你可以分析是线索设计问题还是语义模型问题。

排查顺序建议如下:

  • 先确认查询是否经过正确入口。
  • 查看日志中Stage字段,判断命中了哪一个阶段。
  • 如果停在 deterministic 且未命中,检查关键词映射表里有没有覆盖此查询。
  • 如果进入 semantic_fallback 但结果为空,再排查 Embedding 服务和向量库。

8. CueMap 思路的适用场景与边界

从项目定位来看,CueMap 的思路适合很多场景,但并不是万能钥匙。下面按“推荐”“谨慎”“不推荐”三类来划边界。

推荐场景有三类。第一类是 Agent 长期记忆,用户偏好、历史结论、项目状态这些结构化程度较高的信息,天然适合确定性线索索引。第二类是个人知识库或个人助理,用户经常用碎片化问题触发记忆,稳定召回比模糊丰富更重要。第三类是运维和客服领域,很多问题本质上是“查配置”“查上次结论”,确定性策略能显著降低幻觉风险。

谨慎使用的场景是开放域问答。比如“给我推荐一部电影”这类问题,用户预期的是发散、多元的结果,确定性优先反而会限制探索性。此时更适合把 CueMap 作为过滤层,而不是主召回层。

不推荐的场景是首轮冷启动和极短对话。如果用户只说过一句话,记忆量不足,建立索引的意义不大。另一个不推荐的场景是高度自由的知识组织,比如用户希望系统自动抽取任意主题的知识点,且不提供任何结构化入口,此时强行设计确定性索引会非常累。

所以准确地说,CueMap 不是一个“记忆生成器”,它是一套“记忆检索的工程秩序”。它要求你在写入阶段就考虑“以后怎么找到这条记忆”。如果你不想投入这个建模成本,它就不适合你;如果你愿意,它能给你大量回报。

9. 常见问题与排查思路

问题现象可能原因排查方式解决方案
确定性阶段总是不命中关键词映射表覆盖不足查看日志中的关键词解析结果扩充 type_hint / tag_hint 映射
同一查询结果两次不同兜底阶段使用了向量检索且无确定性缓存增加缓存并对比两阶段结果优先确认是否存在确定性路径,避免过早进入语义阶段
检索结果包含无关记忆多用户数据未按 user_id 过滤检查 SQL 是否拼接了 user_id 条件统一在 store 层强制携带 user_id 过滤
记忆新增后查不到写入未提交事务或索引未更新查询数据库记录是否存在在写入后检查 commit,并刷新 CueIndex
语义兜底时延过高每次请求都调用 Embedding 模型检查请求日志、模型推理耗时增加查询缓存,或降低兜底触发频率
日志里看不到命中链路没有记录 Stage 字段补充检索阶段日志在 retrieve 返回结构中加入 stage 和 matched_key 字段

10. 最佳实践与工程建议

从 CueMap 的设计思想中,可以提炼出一套适用于大多数记忆系统的工程实践。

第一,写入时就要设计可检索的线索。不要只存“内容”,还要存user_idmemory_typetagsentitiescreated_at。后续每次检索都会依赖这些字段。这套字段体系应该和产品功能对齐,比如“用户偏好”“项目配置”“部署历史”,每个类型对应一个明确业务含义。

第二,检索顺序保持稳定。第一层精确键,第二层结构化条件,第三层语义兜底。不要随意调换顺序。稳定的顺序也意味着可观测的日志,每次查询都能记录落在了哪一层。

第三,把关键词映射做成配置,而不是硬编码。上一节示例里已经演示了config.yaml的做法。业务变化后,运营或开发人员可以直接改配置,不需要发布代码。

第四,语义检索永远作为兜底,而不是主路径。这样做能减少 Embedding 服务的调用量,降低系统成本和不可控性。如果数据量超过十万条,还可以考虑在语义检索前增加基于标签的预过滤。

第五,关注记忆的更新与删除。长期记忆系统的可靠性不仅取决于检索,还取决于一致性。用户更正偏好后,旧记忆必须立即失效或标记为废弃。建议给每条记忆增加statussuperseded_by字段,避免新旧版本同时命中。

第六,设定安全边界。记忆数据往往包含个人偏好、业务敏感信息,检索接口必须做好权限校验。任何跨用户查询都不应该出现。生产环境建议最小权限原则,数据库账号只开放所需表的读写权限,并在 API 层做用户维度隔离。

11. 总结与后续学习方向

CueMap 这个项目最值得关注的,不是它的具体代码,而是它提出的一个判断:在长期记忆检索里,确定性应该优先于概率性。向量检索可以作为兜底,但不应成为唯一路径。这个判断在工程上非常扎实,因为你一旦把确定性路径建立起来,系统的可维护性会明显改善,用户对“记忆是否可靠”的信任也会增强。

下一步可以从三个方向深入。第一,把最小示例扩展成支持向量检索的完整服务,用真实 Embedding 模型替换模拟兜底逻辑。第二,设计一套记忆生命周期管理机制,包含写入、索引、更新、失效和归档,让记忆不是一个只增不减的仓库。第三,尝试把检索结果反馈到 Prompt 组装层,观察不同的记忆召回策略对最终生成质量的影响。

最后想提醒一点:记忆系统的核心指标不是“记住了多少”,而是“需要时是否稳定地想得起来”。CueMap 的 deterministic-first 思路,给这个目标提供了一个非常务实的路线。如果你的 Agent 也正被“记忆不稳定”困扰,不妨先画出自己的线索地图,再考虑向量库选型。建议收藏本文,方便后续落地时对照实践。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询