Agent Memory · Retrieval Architecture

GauzMem: 我为什么没有把 Agent 记忆 做成一个向量数据库

向量检索负责找到语义种子,图谱补关系,MySQL 补时间。真正交给 Agent 的不是一组相似文本,而是一份带证据、路径和顺序的 Recall Bundle。

做 Agent 记忆系统时,最容易得到的答案是:把历史对话切块,做 Embedding,存进向量数据库,查询时取 Top K。这个方案能跑,但它只解决了“哪些内容和问题看起来相似”,没有解决“这件事为什么发生”“它前后还有什么”“这条结论来自哪段原话”。

GauzMem 的出发点因此不是再封装一次 Vector Search,而是把记忆做成一条可以回到原始证据的链路,再让语义、关系和时间分别承担自己擅长的召回职责。

GauzMem 三路召回概览:语义召回、图谱扩展和时序扩展合并为可追溯的 Recall Bundle。
图 1:GauzMem 的核心不是单一检索器,而是三条职责不同的召回路径。点击查看原图。

向量检索只能回答“像不像”

假设用户曾经说:“发布推迟到周五,因为支付集成还需要一次审查。”几天后,他问:“为什么发布日期变了?”向量检索大概率能找到这条 Fact,但仅靠相似度,系统仍然不知道它和哪段原始对话对应,也不知道上一轮讨论了什么、下一轮最终确认了什么。

这类缺口通常分成三种:

  • 证据缺口:Fact 是模型提炼的,必须能回到原始 Chunk,避免把派生内容当作原话。
  • 关系缺口:相似不等于因果、支持、矛盾或条件,很多“为什么”问题需要沿关系继续找。
  • 时间缺口:对话的前后轮次往往比另一条高相似文本更重要,顺序本身就是信息。

所以我把向量检索放在召回入口,但没有让它成为整个记忆系统。

先把记忆拆成一条证据链

Source → Chunk → Fact → Entity / Relation → Topic
           ↑        ↑
        原始证据   可检索的原子记忆

Source 表示一段对话、一个文档或一次上传;Chunk 保存原始文本、轮次和创建时间;Fact 是从 Chunk 中提取的原子结论。Entity、Relation 和 Topic 都是基于 Fact 继续构建的检索投影。

对象 保存什么 为什么需要
Source 来源与导入边界 区分对话、文档和文件
Chunk 原文、turn、created_at 承担证据与时序真相
Fact 原子化结论与 chunk_id 提供稳定的检索和扩展种子
Entity / Relation 事实之间的结构化联系 支持多跳关系召回
Topic 一组相关事实的高层摘要 补充跨片段的长期主题

这里最关键的字段不是某个漂亮的摘要,而是 Fact.chunk_id。它让任何派生事实都能回到生成它的 Chunk。没有这条边,系统只能告诉 Agent“我记得有这件事”;有了它,系统才能继续回答“我为什么记得、原话在哪里、当时前后发生了什么”。

三路召回不是三个数据库并排查询

查询先在 Qdrant 中找到带 Fact ID 的语义种子,再分别进入语义、Neo4j 图谱和 MySQL 时序路径,最后合并去重并保留来源、路径和时间。
图 2:三条路径共享同一批 seed Fact IDs,但扩展依据不同。手机端可左右滑动,或点击查看原图。

第一路:Qdrant 找到语义种子

查询先被转换成 Embedding,再从 conversation、Fact 和 Topic 三类向量中召回候选。Qdrant 在这里解决的是候选发现:把搜索空间快速收敛到一批相关内容,并且保留 seed Fact IDs,供后续两条路径继续工作。

语义结果本身也会进入最终合并,但它更重要的作用,是给关系扩展和时序扩展提供可靠起点。三路召回因此不是三次互不相干的搜索,而是先找到种子,再从不同维度恢复上下文

第二路:Neo4j 补回关系

GauzMem 把 Fact 之间的 CAUSE、TEMPORAL、SUPPORT、ELABORATE、CONTRADICT、ANALOGY、CONDITIONAL 和 PARALLEL 等关系投影到 Neo4j。拿到 seed Fact IDs 后,系统可以沿这些边做一到三跳扩展,记录完整 reasoning path,并优先保留更短的路径。

这条路径回答的是结构问题:什么导致了这件事,哪条事实支持它,后面又发生了什么。向量相似度可以发现“支付审查”和“发布日期变化”相关;图关系才能明确它们之间是因果还是仅仅同时出现。

第三路:MySQL 补回时间

时序扩展最容易被误解成“读取 Fact metadata 里的时间”。GauzMem 实际上不依赖把时间复制进 Fact metadata,而是从 Fact 回到 Chunk:

seed Fact IDs
        ↓ JOIN facts.chunk_id = mem_chunks.chunk_id
seed Chunk.turn / Chunk.created_at
        ↓ calculate window + direction
neighbor Chunks in MySQL
        ↓ load facts by chunk_id
temporal context

turn 模式下,如果 seed Fact 来自第 12 轮,hop_distance=1direction=both,系统会查询第 11 到 13 轮的 Chunk,再取回这些 Chunk 对应的 Facts。forward 只向后扩,backward 只向前扩。

time 模式使用 mem_chunks.created_at 计算窗口。当前实现把一跳映射为五分钟,再按方向扩展。无论是哪种模式,时间和顺序都属于原始证据 Chunk,而不是某个可能漂移的派生向量 payload。

为什么 MySQL 是真相源,另外两个是投影

三个数据库同时存在,不代表它们拥有同等权威。GauzMem 把 MySQL 当作 Source、Chunk、Fact、Topic、turn、created_at 和 provenance 的持久化真相源;Qdrant 和 Neo4j 是为特定查询优化的投影。

存储 主要职责 丢失后怎样处理
MySQL 原始证据、派生记录、时间、顺序与追溯关系 需要备份与恢复
Qdrant conversation、Fact、Topic 的语义索引 可从真相源重新嵌入构建
Neo4j Fact、Entity、Relation 的图关系投影 可从事实和关系记录重建

这种 authority / projection 分离让系统更容易解释一致性问题:检索索引可以异步更新,也可以重建;真正不能丢的是证据链。它还避免把 Qdrant payload 或 Neo4j 节点误当成所有业务状态的唯一来源。

Recall Bundle 才是面向 Agent 的产品边界

如果 API 直接返回三份列表,调用方仍然需要理解 Qdrant、Neo4j 和 MySQL 的内部结构。GauzMem 在最后一步把三路结果合并、去重,并保留 source、path、time、chunk_id、hop level 等追溯信息,形成 Recall Bundle。

对 Agent 来说,一条可用记忆至少应该回答四个问题:

  • 内容是什么:Fact 或原始 Chunk 表达了什么。
  • 为什么被召回:来自语义命中、图关系还是时序邻接。
  • 从哪里来:对应哪个 Source 和 Chunk。
  • 处在什么上下文:它的关系路径、轮次和时间窗口是什么。

这也是我把系统定位成 memory service,而不是 vector database wrapper 的原因。数据库是实现细节,Recall Bundle 才是 Agent 真正消费的上下文合同。

短期记忆与长期记忆不应该互相等待

一段对话进入 GauzMem 后,原始 Chunk 会先成为可用的短期记忆,后台再继续抽取 Fact、Entity、Relation 和 Topic,并更新向量与图谱投影。这个顺序的重点不是“用了异步任务”,而是先保住证据,再生产解释:即使后面的编译暂时失败,刚刚发生的对话仍然存在,也仍然可以重新加工。

这让写入延迟和记忆质量可以分开优化:短期路径优先保证“刚说过的话不会忘”,长期路径再负责把原始对话编译成更稳定的知识结构。项目已经实测的结果包括短期记忆可用时间小于 1 秒、向量召回小于 100 ms、图召回小于 500 ms,以及 SOTA 长期记忆表现。

三路召回不是从零并行,而是先找到记忆锚点

把三路召回理解成“三个数据库各查一次”仍然不够准确。图谱不知道应该从哪里开始遍历,时间也不知道应该围绕哪一刻展开。语义召回的真正价值不是直接给出最终答案,而是先把查询收敛成一小组 seed Facts,为后两条路径确定记忆锚点。

有了锚点,图谱扩展才能回答“这条事实沿什么关系连到另一条事实”,时序扩展才能回答“这条事实前后发生了什么”。因此,先语义、再扩展不是数据库编排技巧,而是一条认识论约束:每一段新增上下文,都必须能够说明自己从哪条记忆出发,以及为什么被带回来。

这套设计仍然有明确边界

  • 图扩展限制在一到三跳,防止关系数量失控;更多跳数不天然等于更好的上下文。
  • turn 不是全局时钟,只在会话域内有意义;当一个项目承载多条独立会话时,时序查询必须同时限定 session 边界。
  • Qdrant 和 Neo4j 可以重建,不代表可以忽略同步失败;投影状态仍然需要健康检查与可观测性。
  • Recall Bundle 能提供证据,不保证上层模型一定做出正确推理;生成答案仍然需要自己的评测。

这些边界反而让系统更可靠。一个记忆服务不需要假装自己解决了所有智能问题,它只需要清楚地说明:什么是事实,什么是投影,召回为什么发生,证据能否回去。

GauzMem 已经开源。对我而言,它首先是一份可运行的架构判断:Agent 的记忆不应止于保存和命中,而应该能够沿证据、关系和时间重新组织上下文。

View GauzMem on GitHub →