开篇:存什么 vs 放哪里,是两个问题

设计 Agent Memory 时,最容易把两个问题混成一个:

“记忆可以存内容、存硬盘、存关系型数据库、存内存数据库、也可以存向量数据库、还可以做知识图谱。”

这句话听起来什么都对,但什么也没说清。因为它把两个正交维度混在一起了:

维度 1:存什么形态(数据结构)
  - 原始消息(ChatMessage,日志形态)
  - 结构化条目(MemoryEntry,有 subject / type / provenance / status)
  - 向量嵌入(embedding,一串数字)
  - 图谱节点边(entity + relation,有向图)

维度 2:放哪里(存储介质)
  - 内存(Map,进程内)
  - 文件(JSONL / SQLite,本地持久)
  - 关系型数据库(MySQL / PostgreSQL)
  - 内存型数据库(Redis,快但易失)
  - 向量数据库(Milvus / pgvector / FAISS / Pinecone)
  - 图数据库(Neo4j / Graphiti)

两个维度正交:同一份记忆,介质可以换,形态也可以混。结构化条目可以存内存、可以存 MySQL;图谱节点边可以存 Neo4j、也可以用 JSONL 手搓。

本文聚焦维度 2:六种存储介质各自是什么、数据怎么落、怎么查、优缺点、什么时候选。

读完你能:拿到任何 Agent 的记忆后端,立刻判断它在哪一档;给自己设计 Memory 时,按规模和能力选介质,不盲目上向量数据库。


一、六种存储介质逐个拆

1. 内存 Map(进程内)

数据怎么落

ConcurrentHashMap<String, MemoryEntry>
  key   = entry.id()
  value = MemoryEntry record

没有序列化,没有 I/O,对象直接活在 JVM 堆里。

怎么查:Stream filter 链

entries.values().stream()
  .filter(e -> query.scopes().contains(e.scope()))    scope 隔离
  .filter(e -> e.status() == ACTIVE)                  只回活跃
  .filter(e -> !e.isExpired(now))                     TTL 惰性过滤
  .filter(e -> e.content().contains(keyword))         keyword 包含
  .sorted(comparing(MemoryEntry::createdAt).reversed())
  .limit(limit)

优点

  • 零依赖,不需要任何外部组件
  • 查询延迟纳秒级
  • deterministic,单元测试好写(不抖动)
  • 数据结构灵活,record 随便改

缺点

  • 进程崩了数据全丢
  • 容量受 JVM 堆限制(几十万条到头)
  • 无法多进程共享(每个 Agent 实例各存各的)
  • 没有索引,大数据量时 filter 全表扫

适合场景:教学型框架、单进程 Demo、单元测试、原型验证

代表实现:LangChain 的 ConversationBufferMemory、教学型框架的内存实现


2. 文件 JSONL / SQLite(本地持久)

数据怎么落

JSONL 方式–每行一个 JSON 序列化的 MemoryEntry:

{"id":"a1","scope":"user:u1","type":"PREFERENCE","subject":"diet","content":"对花生过敏","status":"ACTIVE",...}
{"id":"a2","scope":"user:u1","type":"FACT","subject":"tz","content":"UTC+8","status":"ACTIVE",...}

SQLite 方式–一张本地文件数据库表:

CREATE TABLE memory_entry (
  id          TEXT PRIMARY KEY,
  scope       TEXT,
  type        TEXT,
  subject     TEXT,
  content     TEXT,
  status      TEXT,
  created_at  INTEGER,
  expire_at   INTEGER
);
CREATE INDEX idx_scope ON memory_entry(scope);
CREATE INDEX idx_subject ON memory_entry(scope, subject);

怎么查

  • JSONL:启动时全量加载到内存 Map,之后同内存方案;或按行扫描(大数据量时慢)
  • SQLite:标准 SQL 查询,带索引

优点

  • 进程崩了数据还在(文件持久)
  • 人可读(JSONL 可以 cat / diff / 手动编辑)
  • 零部署依赖(SQLite 就是单文件)
  • 调试方便(出问题直接看文件内容)

缺点

  • JSONL 全量加载到内存(或按行扫),不适合大数据量
  • 并发写要加锁(JSONL)或依赖 SQLite 的文件锁
  • 无向量检索能力
  • 多进程共享要小心(SQLite 多写者性能差)

适合场景:单机生产、单租户 Agent、开发调试、需要数据可审计可回溯但规模不大

代表实现:dsh 的 session log(JSONL)、LangChain 的 ConversationBufferFileMemory


3. 关系型数据库(MySQL / PostgreSQL)

数据怎么落:标准关系表

CREATE TABLE memory_entry (
  id          VARCHAR(64) PRIMARY KEY,
  scope       VARCHAR(128) NOT NULL,
  type        VARCHAR(32) NOT NULL,
  subject     VARCHAR(256),
  content     TEXT NOT NULL,
  importance  FLOAT DEFAULT 0.5,
  provenance  JSON,                           -- 来源溯源存 JSON
  status      VARCHAR(32) NOT NULL DEFAULT 'ACTIVE',
  created_at  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expire_at   TIMESTAMP NULL,
  tenant_id   VARCHAR(64) NOT NULL            -- 多租户隔离
);

CREATE INDEX idx_scope ON memory_entry(tenant_id, scope);
CREATE INDEX idx_scope_subject ON memory_entry(tenant_id, scope, subject);
CREATE INDEX idx_status ON memory_entry(tenant_id, status);

怎么查:标准 SQL

SELECT * FROM memory_entry
WHERE tenant_id = ?
  AND scope IN (?, ?)                          -- scope 隔离
  AND status = 'ACTIVE'                        -- 只回活跃
  AND (expire_at IS NULL OR expire_at > NOW()) -- TTL
  AND content LIKE CONCAT('%', ?, '%')         -- keyword
ORDER BY created_at DESC
LIMIT ?;

优点

  • 事务保证(ACID,写入要么全成要么全败)
  • 并发支持(多 Agent 实例同时读写)
  • 多租户天然隔离(tenant_id + scope 双重过滤)
  • 索引能力强(B+Tree,按 scope/subject/status 查询毫秒级)
  • 审计友好(完整的变更日志可以单独建表)
  • 生态成熟(运维、监控、备份方案多)

缺点

  • 部署依赖(要跑一个数据库进程)
  • keyword 用 LIKE 走全表扫(除非加全文索引)
  • 没有语义检索能力(“过敏” 匹配不到 “禁忌”)
  • schema 变更要迁移

适合场景:多租户企业 Agent、需要事务和审计的生产环境、ChatGPT memory 的 saved memories 落地

代表实现:企业 Agent 生产环境、ChatGPT memory 后端

进阶:PostgreSQL + pgvector 扩展

PostgreSQL 装上 pgvector 扩展后,可以加一列向量字段:

CREATE EXTENSION vector;

ALTER TABLE memory_entry ADD COLUMN embedding vector(384);

CREATE INDEX idx_embedding ON memory_entry
  USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100);

这样同一张表既能 SQL 查 keyword,又能向量查语义–关系型 DB 的能力扩展到第 2 档检索,不用单独引一个向量数据库。


4. 内存型数据库(Redis)

数据怎么落:用 Redis 的 Hash 或 Sorted Set

Hash:
  HSET memory:a1 scope "user:u1" type "PREFERENCE" content "对花生过敏" status "ACTIVE" ...

Sorted Set(按时间排序,方便取最近):
  ZADD memory:user:u1:timeline 1724054400 "a1"
  ZADD memory:user:u1:timeline 1724140800 "a2"

怎么查

  • 按哈希键精确查:HGET memory:a1
  • 按 scope 查所有:HGETALL memory:user:u1:*(要 SCAN)
  • 按时间范围:ZRANGE memory:user:u1:timeline 0 10 REV
  • 无原生 keyword 包含匹配(要自己实现或用 RedisSearch 模块)

优点

  • 延迟极低(微秒级,全内存操作)
  • 支持过期自动清理(TTL 原生支持,不用惰性过滤)
  • 数据结构丰富(Hash / Set / Sorted Set / List 各有场景)

缺点

  • 数据易失(Redis 默认异步刷盘,崩溃可能丢最近几秒)
  • 内存成本高(全量数据驻内存,比磁盘贵得多)
  • 查询能力弱(无 SQL,无向量,复杂过滤要客户端做或加 RedisSearch)
  • 不适合做主存储(适合做缓存层)

适合场景:热数据缓存层(当前会话活跃记忆)、配合关系型 DB 用的加速层、Session 级短期记忆

代表实现:大规模 Agent 系统的热数据层、配合 MySQL 用的缓存

典型混合用法

写入:写关系型 DB(持久) + 写 Redis(加速)
读取:先查 Redis(快),miss 时回退关系型 DB
过期:Redis TTL 自动清理,关系型 DB 靠惰性过滤

5. 向量数据库(Milvus / pgvector / FAISS / Pinecone)

这是和前面四种介质最大的分水岭:前四种介质回答"放哪里",向量数据库额外回答了"怎么查"–从字面匹配升级到语义匹配。

数据怎么落:结构化条目 + 向量字段

原有 MemoryEntry 字段不变:
  id / scope / type / subject / content / status / provenance / ...

新增向量字段:
  embedding  VECTOR(384)  -- 384 维浮点数组

向量怎么来:
  "对花生过敏" -> 过 embedding 模型 -> [0.12, -0.34, 0.56, ..., 0.08]

怎么查:向量相似度检索

步骤 1:把查询"用户问过敏"过 embedding 模型 -> 查询向量 [0.13, -0.32, ...]
步骤 2:在向量库里找余弦相似度最高的 K 条
步骤 3:返回 Top-K 记忆条目

SQL 版本(pgvector):
  SELECT *, 1 - (embedding <=> $query_vec) AS similarity
  FROM memory_entry
  WHERE scope IN (?, ?) AND status = 'ACTIVE'
  ORDER BY embedding <=> $query_vec   -- 余弦距离
  LIMIT 10;

Milvus 版本:
  collection.search(
    data=[query_vec],
    anns_field="embedding",
    param={"metric_type": "COSINE", "params": {"nprobe": 10}},
    limit=10,
    expr='scope in ["user:u1"] and status == "ACTIVE"'
  )

embedding 是什么:把文字过 embedding 模型,变成一串几百到几千维的浮点数。语义相近的文字,向量距离也近。

"我对花生过敏"       -> [0.12, -0.34, 0.56, ...]
"花生是我的饮食禁忌"  -> [0.11, -0.32, 0.55, ...]   <- 很接近!
"今天天气不错"        -> [0.87, 0.21, -0.09, ...]   <- 完全不同

所以向量检索能匹配"过敏"和"禁忌"这种字面不同但语义相同的内容。

优点

  • 语义模糊匹配(核心价值:字面不同的能匹配到)
  • 检索质量高(用户换说法、用同义词都能命中)
  • 工业成熟(Milvus/Pinecone 生态完整)

缺点

  • 依赖 embedding 模型(每次写入都要算 embedding,要么调 API 要么跑本地模型)
  • 向量索引占空间(384 维 float = 1.5KB/条,百万条 = 1.5GB)
  • 每次结果可能微抖动(取决于 embedding 模型,单元测试不好写)
  • 查询不是精确的"匹配/不匹配",而是"相似度 Top-K"

四种向量数据库对比

数据库类型部署方式适合
pgvectorPostgreSQL 扩展跟着 PG 跑已有 PG,不想引新组件,百万级以下
Milvus专用向量 DB独立集群大规模生产,亿级向量,需要高吞吐
FAISS库(不是服务)进程内嵌入单机、库内调用、不想部署服务
Pinecone云托管 SaaSAPI 调用不想运维、快速验证、按量付费

适合场景:语义检索(RAG)、需要"模糊匹配"的记忆系统、mem0 / ChatGPT memory 的历史引用通道

代表实现:mem0(OpenAI embedding + Qdrant)、RAG 系统

关键认知:向量数据库解决的是"怎么查",不是"放哪里"

很多人以为"上了向量数据库就不用关系型 DB 了"。错。向量数据库解决的是检索方式(从字面到语义),不是存储介质。生产级常见做法是主存还是关系型 DB,向量作为旁路索引

主存:MySQL / PostgreSQL(存结构化条目,支持事务和审计)
旁路:Milvus / pgvector(只存 id + embedding,负责语义检索)

查询时:
  1. 先在向量库找相似 id(Top-K)
  2. 拿 id 去主存捞完整条目
  3. 主存做 scope/status/TTL 过滤
  4. 返回结果

这样事务、审计、多租户隔离由主存保证,向量库只管"找相似"。


6. 图数据库(Neo4j / Graphiti)

这是最复杂也最强的一档。前面的介质都是"存条目",图数据库存的是"条目之间的关系"。

数据怎么落:节点 + 边(有向图)

节点:
  (userA:User {id: "u1"})
  (fact1:Fact {content: "对花生过敏", validFrom: T1, validTo: T3})
  (fact2:Fact {content: "不过敏", validFrom: T3, validTo: null})
  (userB:User {id: "u2"})

边:
  (userA)-[:SAID {at: T1, runId: "r1"}]->(fact1)
  (fact1)-[:SUPERSEDED_BY {at: T3, by: "admin"}]->(fact2)
  (userA)-[:RELATIVE_OF]->(userB)
  (userB)-[:SAID {at: T2}]->(fact3:Fact {content: "家族坚果过敏史"})

每条边可以带属性(什么时候说的、谁说的、从哪次 run 来的)。

怎么查:图遍历 / 路径推理(Cypher 查询语言)

// 查"用户亲属的过敏史" -- 跨实体推理
MATCH (u:User {id: "u1"})-[:RELATIVE_OF]->(rel:User)-[:SAID]->(f:Fact)
WHERE f.content CONTAINS "过敏" AND f.validTo IS NULL
RETURN f;

// 查"这个事实谁说的、什么时候、后来改过没"
MATCH (f:Fact {content: "对花生过敏"})<-[:SAID]-(sayer)
OPTIONAL MATCH (f)-[:SUPERSEDED_BY]->(corrected)
RETURN sayer, f, corrected;

双时间轴(Zep / Graphiti 的核心特色):

validFrom / validTo   事实在现实世界中的有效期
  fact1: "对花生过敏" validFrom=T1, validTo=T3   (T1 到 T3 期间为真)
  fact2: "不过敏"     validFrom=T3, validTo=null (T3 之后为真)

recordedAt            系统什么时候录入这条记忆
  fact1.recordedAt = T1   (T1 时录入)
  fact2.recordedAt = T3   (T3 时录入)

这让你能回答"上周三时,用户对花生过敏吗"–查 validFrom <= 周三 < validTo 的事实。关系型 DB 的记忆只有"录入时间",回答不了这种时序问题。

优点

  • 关系推理能力强(跨实体关联,回答"A 的亲属的病史")
  • 双时间轴(回答时序问题)
  • 审计链天然完整(supersede 就是图的一条边)

缺点

  • 实现最复杂(schema 设计难,查询语法学习曲线陡)
  • 运维重(独立图数据库集群)
  • 不擅长大规模简单查询(按 scope 查所有条目,不如关系型 DB 快)
  • 生态不如关系型 DB 成熟

适合场景:复杂关联推理、时序推理、需要回答"什么时候知道/什么时候过期"的场景

代表实现:Zep / Graphiti


二、六种介质速查表

介质数据结构查询方式持久多租户延迟适合规模
内存 Map对象Stream filter进程级纳秒万级
JSONL 文件JSON 行全量加载/扫描加载后纳秒万级
SQLite关系表SQL + 索引毫秒十万级
MySQL/PG关系表SQL + 索引毫秒亿级
RedisKV/Hash/Set键查/SCAN半持久微秒千万级
向量 DB条目+向量向量相似毫秒亿级向量
图 DB节点+边图遍历毫秒~秒千万级节点

三、检索能力三档进化

存储介质决定了能查多"准"–这是选型时最容易被忽略的维度:

第 1 档:关键词匹配(字面包含)
  "过敏" 只匹配含"过敏"两字的条目
  "禁忌" 查不到 -- 字面不一样就不命中
  -> 介质:内存 Map、关系型 DB 的 LIKE

第 2 档:向量相似(语义接近)
  "过敏" 能匹配到"禁忌""花生不耐受"
  -> 需要:embedding 模型 + 向量索引
  -> 介质:向量 DB / pgvector

第 3 档:图谱推理(关系推理)
  问"用户亲属的过敏史" 能顺着"用户-亲属-病史"边推理出来
  "这个 PR 沉寂 3 天了" 能关联 PR->CI->失败->用户
  -> 需要:图结构 + 路径查询
  -> 介质:图 DB / Zep

检索能力越强,存储结构越复杂、运维越重。 教学型用第 1 档够,工业级主用第 2 档,复杂场景才上第 3 档。

这也是为什么很多团队"用了向量数据库但效果不好"–向量只解决"语义相似",不解决"关系推理"。如果你的核心需求是"用户上周说过 X,这周问 Y,X 和 Y 有关联",向量帮不了你,要图谱。


四、选型决策树

你的 Agent 要记忆吗?
  └── 是 -> 规模多大?
        ├── 教学 / Demo / 单进程
        │     -> 内存 Map
        │     -> 要持久?加 JSONL 文件
        ├── 单租户 / 单机生产
        │     -> SQLite / JSONL + 内存缓存
        │     -> 检索要语义?加 pgvector
        ├── 多租户企业 Agent
        │     -> MySQL / PostgreSQL
        │     -> 检索要语义?pgvector 扩展
        │     -> 审计严格?加审计日志表
        │     -> 热数据要快?加 Redis 缓存层
        ├── RAG / 语义检索
        │     -> 向量 DB(Milvus / Pinecone)
        │     -> 主存还是关系 DB,向量旁路
        └── 复杂关联推理
              -> 图 DB(Neo4j / Graphiti)
              -> 通常作为旁路,主存还是别的

五、混合存储:生产级做法

大规模 Agent 系统不是"选一个数据库",是按访问模式分层,每种数据放最适合的介质

┌──────────────────────────────────────────────────┐
│ 热数据(当前会话活跃记忆)                          │
│   介质:Redis                                      │
│   原因:微秒延迟,TTL 自动清理                      │
│   内容:当前用户最近 N 轮的记忆 + 会话状态           │
├──────────────────────────────────────────────────┤
│ 温数据(近期记忆,跨会话)                          │
│   介质:MySQL / PostgreSQL                         │
│   原因:事务、索引、审计、多租户隔离                │
│   内容:用户偏好、事实、历史会话摘要                 │
├──────────────────────────────────────────────────┤
│ 冷数据(历史归档)                                  │
│   介质:对象存储 / JSONL 文件                       │
│   原因:便宜、容量大                                │
│   内容:超过 N 天的记忆条目归档                     │
├──────────────────────────────────────────────────┤
│ 语义检索索引(旁路)                                │
│   介质:Milvus / pgvector                          │
│   原因:向量相似检索                                │
│   内容:每条温/冷记忆的 embedding(只存 id+向量)   │
├──────────────────────────────────────────────────┤
│ 关系推理索引(旁路,可选)                          │
│   介质:Neo4j / Graphiti                           │
│   原因:跨实体关联推理                              │
│   内容:实体节点 + 关系边                           │
└──────────────────────────────────────────────────┘

典型查询流程

用户问"帮我订午餐"
  -> 1. Redis 查当前会话有没有相关记忆(热)-> 命中返回
  -> 2. miss -> 向量库找语义相似条目(语义)-> 拿到 id 列表
  -> 3. 拿 id 去主存 MySQL 捞完整条目(温)-> scope/status/TTL 过滤
  -> 4. 返回"用户对花生过敏"
  -> 5. 写回 Redis 缓存(下次快)

六、起步选择 + 扩展路径

起步推荐

存什么:结构化条目(有元数据,可治理,不存原始消息)
放哪:内存 Map(教学/原型)/ SQLite(单机持久)/ MySQL(多租户生产)
怎么查:keyword 包含匹配
↑ 第 1 档最简版,先用最简单的跑通

扩展路径(关键:接口抽象藏住"怎么查")

设计 MemoryStore 接口时,按 query(MemoryQuery) 抽象,把"怎么查"藏在实现里:

路径 A:换存储介质(同一形态,换实现)
  内存 Map -> JSONL 文件(持久)
         -> JDBC(MySQL/PG,多租户)
  检索方式不变,还是 keyword
  调用方零改动

路径 B:换检索能力(数据加字段,查法升级)
  记忆条目加 vector 字段
  query 方法换向量相似
  -> 向量 DB(Milvus/pgvector)
  调用方(retriever / contextBuilder)零改动

起步用 keyword、中期换向量、后期换图谱,调用方都不用改一行。 这是接口抽象的价值:把"存储怎么实现"和"记忆怎么用"解耦。


七、防错清单

设计时容易踩的坑:

1. 盲目上向量数据库
   -> "听说向量数据库好"就上,但你的规模根本不需要
   -> 教学/Demo 用内存 Map 够;单机生产用 SQLite;多租户用 MySQL
   -> 向量数据库解决"怎么查",不是"放哪里"

2. 用了向量但效果不好
   -> 向量只解决"语义相似",不解决"关系推理"
   -> 核心需求是跨实体关联?要图谱不是向量

3. Redis 当主存
   -> Redis 快但易失,崩溃可能丢最近几秒数据
   -> Redis 适合缓存层,主存还是要持久介质

4. 存储介质和检索方式焊死
   -> 换个数据库要改一堆代码
   -> 接口按 query(MemoryQuery) 抽象,藏住查法

5. 只看介质不看检索能力
   -> "用 MySQL 存记忆" 听起来没问题,但 LIKE 全表扫
   -> 同样是关系型 DB,PostgreSQL + pgvector 能做向量检索,MySQL 不能
   -> 选介质时同时考虑检索能力

6. 冷热不分离
   -> 所有记忆都堆在同一个介质
   -> 热数据要快(Redis),冷数据要便宜(文件),混在一起性能和成本都不好
   -> 按访问模式分层

八、总结

设计 Agent Memory 的存储层,三个层次想清楚:

1. 存什么形态(数据结构)
   原始消息 / 结构化条目 / 向量嵌入 / 图谱节点边
   主流选结构化条目(有元数据,可治理)

2. 放哪里(存储介质)
   内存 Map(教学)/ 文件(单机持久)/ 关系 DB(多租户生产)
   / Redis(热缓存)/ 向量 DB(语义检索)/ 图 DB(关系推理)
   按规模和能力选,见决策树

3. 怎么查(检索能力)
   keyword(字面)-> 向量(语义)-> 图谱(推理)
   能力越强,结构越复杂,运维越重

一句话收束:

Agent Memory 的存储层不是"选哪个数据库",是"按规模选介质、按能力选检索、按访问模式分层"。存储介质是可换的,接口抽象是不变的。


Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐