跳到正文

AGENT PROJECT INTERVIEW · Q18 / 25

Agent 记忆数据怎么存?关系库、向量库、对象存储与 Schema 怎样设计

讲清 Agent Memory 的关系事实源、向量语义索引、对象工件和缓存分层,以及 Schema、双写一致性、权限与删除。

作者与复核:吴师兄内容依据:公开 Agent 教程、项目题资料与工程推导查看关联教程 →查看吴师兄公开作品 →

25 道题目Q18
本页内容21 节

30 秒先说结论

Agent 记忆存储可分为关系事实源、向量语义索引、对象工件层和缓存层。主记录保存主体、租户、来源、版本、状态、权限、有效期与替代关系;向量命中后回表复查。写入先提交主记录和事件,再异步建索引,更新与删除按版本传播到索引和缓存。

面试官可能会问:“做 Agent Memory,是不是把所有对话切块后存进向量库就够了?”

不够。向量库适合根据语义找相似内容,却不擅长单独承担用户归属、租户隔离、事务更新、唯一约束、版本冲突、精确状态和彻底删除。完整对话、大文件和工具工件全部塞进一张关系表也不合理,读取成本和生命周期会混在一起。

这道题考的是你能否先区分“事实源”和“索引”。关系型存储保存主体、记忆条目、来源、权限、版本、状态和审计;向量索引帮助从已授权范围里找到语义候选;对象存储承载长文档、完整轨迹和大工件;缓存只加速热点读取,不成为真相。

真正的工程难点还包括双写一致性:主记录已经更新,向量索引还是旧版本;用户删除后主表看不到,旧向量却仍可召回;两个会话并发修改同一偏好,后写者无声覆盖前者。Schema 必须为这些生命周期预留字段和恢复路径。

先给出 30 秒回答

我会把 Agent 记忆存储设计成“关系事实源、向量语义索引、对象工件层、缓存加速层”。关系库保存 memory_id、主体与租户、类型、规范化内容、来源、时间、版本、有效状态、权限、替代关系和索引状态;向量库只保存可检索文本、embedding、主记录 ID、版本和必要过滤元数据,命中后必须回关系库复查权限与最新状态;长对话、文档和工具产物放对象存储,State 或记忆只保留引用。写入先提交主记录,再异步构建索引;更新和删除通过版本与事件传播,失败可重试。检索、冲突、遗忘和用户删除都以主记录为准,不能让索引副本成为事实源。

这套回答用数据责任、查询方式和生命周期划分存储职责,比按“关系库、向量库”几个技术名词罗列组件更接近真实工程。

Agent 记忆存储的四层分工

事实、语义索引、大工件和缓存各自承担不同责任。

先分清要存的不是一种数据

Agent 系统里至少会遇到四类持久数据。

第一类是结构化主体和关系,例如用户、租户、会话、任务、消息、工具调用、记忆条目、权限、版本和反馈。它们需要精确查询、约束、事务和审计,适合关系型存储。

第二类是需要语义相似检索的文本表示,例如经过筛选的长期记忆、历史任务摘要、知识文档片段和工具描述。它们适合建立向量索引,但原始归属和有效状态仍应回到事实源判断。

第三类是大对象,例如完整文档、长报告、附件、网页快照、音视频转写、代码压缩包和长工具输出。它们不应反复塞进 Agent State 或向量元数据,适合放对象存储并保存稳定 URI、内容哈希和权限引用。

第四类是短期热点数据,例如最近会话窗口、任务锁、幂等结果和常用记忆缓存。键值或内存缓存可以加速,但必须有过期与失效机制。

如果没有先分数据责任,团队很容易选一个熟悉的数据库包打天下,最后让向量库承担权限,让缓存承担真相,让主表塞满重复大文本。

关系库为什么应该是事实源

记忆系统的关键问题多数是精确的:这条记忆属于哪个用户和租户;是否仍然有效;当前 Agent 有无权限;哪个版本最新;它替代了哪条旧记录;索引是否已更新;用户是否要求删除。

这些条件适合结构化字段和事务约束。关系库可以对稳定 ID、主体、版本和状态建立索引,使用外键或业务约束维护引用,通过条件更新处理并发版本。

事实源不是说所有正文都必须留在关系表。短文本可以直接保存,长内容可以保存对象引用。但决定一条记忆能否被读取的状态、权限、版本和来源,应该在一个可精确验证的位置。

向量检索返回 memory_id 后,服务回到关系库读取当前版本并复查 tenant_idsubject_idvisibilitystatus 和有效期。即使向量索引延迟,这个复查也能挡住已经撤回、删除或越权的旧候选。

一张记忆主表至少要表达什么

下面是用于面试讨论的最小字段示意:

memory_entry (
  memory_id,
  tenant_id,
  subject_id,
  memory_type,
  canonical_key,
  content,
  source_type,
  source_ref,
  status,
  visibility,
  version,
  valid_from,
  expires_at,
  supersedes_id,
  content_hash,
  index_status,
  created_at,
  updated_at
)

这不是可直接复制的生产建表语句,也没有给出数据库方言和字段类型。它只是说明 Schema 要回答哪些工程问题。

tenant_idsubject_id 确定归属;memory_type 决定检索与保留策略;canonical_key 帮助结构化偏好去重;来源字段让系统知道这是用户明确声明、人工确认、工具事实还是模型候选;status 区分候选、有效、过期、被替代、撤回和删除;visibility 限制谁可读;version 支持并发更新;有效时间控制时效;supersedes_id 保留冲突替代关系;content_hash 用于幂等和内容变化识别;index_status 跟踪向量副本。

敏感等级、用途授权、保留策略、加密键和删除任务状态也可能需要独立字段或关联表,取决于具体产品与合规要求。不要把所有权限塞进一段自然语言标签。

消息、任务状态和长期记忆要不要放一张表

通常不建议。

消息记录描述谁在什么时候说了什么,强调会话顺序与原始来源;任务状态描述当前执行到哪一步,强调版本、Checkpoint、工件和恢复;长期记忆描述未来任务可复用的稳定信息,强调筛选、权限、有效期、冲突和遗忘。

三者可以共享用户、租户和来源引用,却有不同读写模式与保留周期。把所有内容塞进 messages,会让“最后一条消息”同时承担状态和记忆;把长期记忆直接等同于消息向量,会把临时假设和工具错误一起召回。

更合理的是保留消息表作为原始对话,任务表和 Checkpoint 保存执行状态,记忆表保存经过生命周期审核的条目。记忆的 source_ref 可以指向消息、任务结果或人工确认记录,必要时回查原始证据。

审计日志也应独立。审计记录谁在何时读取、修改或删除了什么,不等于可用于个性化的长期记忆。即使审计因规则需要保留,也不能自动注入模型上下文。

向量索引应该存什么

向量索引的最小责任是为语义候选生成服务。

它可以保存向量、用于匹配的文本、主记录 ID、租户或作用域、记忆类型、状态快照、版本、Embedding 模型版本和更新时间。支持服务端元数据过滤时,可以在搜索前缩小租户、类型和有效状态范围。

但不要只依赖索引里的状态快照。更新传播有延迟,索引可能仍显示 active。结果返回后必须回主记录复查。向量命中是候选,不是权限授权,也不是事实正确证明。

可检索文本也不一定等于完整正文。结构化偏好可以生成规范化检索表示,长历史摘要可以只索引摘要并引用对象工件。敏感内容需要最小化,不能为了提高召回把完整秘密复制到更多索引副本。

Embedding 模型升级要保存版本。新旧向量空间不可直接假定兼容。迁移可以新建索引或双版本构建,用固定评测集比较,再逐步切换。没有实际测试时不能声称“无损迁移”。

对象存储解决什么问题

一次研究任务可能生成搜索结果快照、网页正文、报告草稿、数据文件和 Trace。把这些大内容直接放进每个 Checkpoint,会导致状态膨胀和重复复制。

对象层保存原始或派生工件,关系表只保存对象 ID、URI、内容哈希、版本、媒体类型、大小、创建者、权限和保留状态。Agent 需要时通过受控工具读取相关片段,而不是把整个对象塞进 Prompt。

对象引用必须可验证。链接指向的版本发生变化,旧任务恢复会读到不同内容。内容哈希、不可变版本或版本化 URI 能帮助复现。权限检查不能只在生成链接时做,读取时仍要根据当前主体验证。

删除长期记忆时,如果正文或来源工件也属于删除范围,要沿引用清理。若对象被多个合法记录共享,不能因为一条记忆删除就误删公共工件,需要引用关系与保留策略裁决。

主记录、向量索引与对象工件的引用关系

向量命中只返回候选 ID,正文、权限、版本和工件都回到事实层验证。

写入为什么不能要求所有系统一次成功

关系库、向量服务和对象存储通常不共享一个本地事务。直接在请求里依次写三处,任何一步超时都会留下不确定状态。

更稳的流程是先完成授权、去重和冲突检查,在关系事务中写主记录与待索引事件。事务提交后,由异步消费者生成向量并更新 index_status。对象工件如果先上传,可以使用暂存状态,主记录提交后再确认归属;流程失败则按期限清理孤儿对象。

写入请求需要稳定幂等键。调用方超时后重试,系统先检查同一候选或内容版本是否已经提交,避免重复条目。索引消费者也按 memory_id + version 幂等处理,旧版本事件不能覆盖新版本索引。

如果索引暂时失败,主记录仍是事实,但该条记忆不能假装已支持语义召回。监控可见待处理、失败与重试状态。对于可以精确查询的结构化偏好,系统仍可能通过关系查询读取;具体降级要按记忆类型设计。

这类模式可以用 Outbox、任务队列或其他可靠事件方案实现。面试回答不需要把某个中间件说成唯一答案,重点是主记录提交、事件不丢、消费者幂等和状态可观察。

更新与冲突如何落到数据结构

用户修改偏好时,不建议原地改文本且丢掉旧来源。可以新增版本,使用条件更新保证当前版本仍是预期值,再把旧条目标记为被替代。

如果两个会话同时更新同一 canonical_key,版本条件会让其中一个发现冲突。系统重新读取最新状态,判断两条是替代、条件并存还是需要用户确认。不能让数据库的最后写入顺序代替业务裁决。

向量索引按版本更新。新版本建立后,旧版本从普通候选删除或失效。搜索结果回表时再检查 version,即使旧向量暂时残留也不会进入上下文。

条件并存时,可以把场景、项目、设备或任务类型存成结构化作用域,而不是把全部条件埋在正文里。检索先按当前上下文过滤,减少模型猜测。

来源也能多对一。一条稳定记忆可能由多次明确声明支持。可以用关联表保存多个 source_ref,避免为每个来源复制一条完整记忆,也保留审计证据。

检索路径怎样避免绕过权限

客户端不应直接连接向量库并拿回正文。统一的 Memory 服务接收当前身份、租户、Agent 角色和任务作用域,构造过滤,执行候选查询,再回主记录做权限和有效状态复查。

如果使用缓存,缓存键必须包含租户、用户、权限版本和查询作用域。权限变化、记忆更新、撤回和删除时主动失效。只按查询文本缓存结果,可能把一个用户的候选返回给另一个用户。

不同 Agent 的权限也要最小化。写作 Agent 可能读取表达偏好,支付 Agent 才能接触交易状态;研究子 Agent 只拿当前项目公开材料。共享 Memory 不代表全部 Agent 共用一个无边界向量集合。

Trace 记录候选 ID 和判定理由,避免把敏感正文复制进普通日志。调试工具本身也必须鉴权和脱敏。

删除为什么是存储设计的验收题

用户删除一条记忆,最先做的是让主记录立即不可读。随后触发向量索引、关键词索引、缓存、派生摘要和相关对象的清理任务。

删除任务记录每个副本的状态。某个索引清理失败时持续重试并告警,不能因为主表已标记就报告全部完成。检索结果回表复查能在异步窗口中阻挡旧向量。

物理删除、软删除和审计保留要按用途区分。软删除不能成为永久隐藏;若按请求需要彻底移除,就必须执行后续清理。审计如需保留,只保留必要字段并与模型可读记忆隔离。

备份恢复也要纳入。可以保存删除水位或删除事件,在恢复后重新应用,避免已删除内容复活。对象存储的版本与生命周期策略同样需要验证。

遗忘不一定等于用户删除。系统按保留策略让低价值或过期条目失效,也要同步索引;用户撤回则优先级更高。无论哪种情况,状态来源都应是主记录。

写入、更新与删除的一致性矩阵

主记录先改变可读状态,再让索引、缓存与对象副本逐步收敛。

Schema 怎样支持可观察性

存储层至少要能回答:一条记忆为什么存在,谁写的,基于哪个来源,当前版本是什么,是否已经建立索引,最近一次被哪个任务使用,何时过期,以及删除传播到了哪里。

写入 Trace 记录候选、规则与授权结果;检索 Trace 记录过滤、候选和最终注入;更新 Trace 记录旧版本、新版本和冲突裁决;删除 Trace 记录每个副本状态。日志中使用 ID 与必要摘要,避免再次复制敏感正文。

反馈可以连接到记忆与任务。用户纠正一条记忆时,系统知道是哪次召回影响了哪个答案,从而修正内容、条件或排序。只记录最终答案评分,无法定位是存错、索引旧还是模型没用对。

可观察性字段不是为了堆表,而是为了让失败能够归因。没有 source_ref,无法核对事实;没有版本,无法解释冲突;没有 index_status,无法知道为什么写入后搜不到;没有删除任务状态,无法证明清理完成。

一个完整例子:保存并更新输出语言偏好

用户明确要求“这个工作区的技术文档默认使用中文”。系统从当前会话产生候选,保存用户、租户、工作区、偏好类型、规范化键、内容、来源消息和授权状态。

冲突检查发现没有同作用域偏好,于是在关系事务中创建 version 1,并写出待索引事件。向量消费者建立检索表示,但检索时仍要求工作区匹配。对象存储不参与,因为正文很短。

后来用户说“对外 API 文档改用英文,内部文档仍用中文”。系统不是把中文偏好改成英文,而是新增更具体的 API 文档条件记录。检索某次写作任务时先看工作区和文档类型,外部 API 命中英文,内部文档命中中文。

若用户删除工作区全部个性化偏好,主记录立即变成不可读状态,事件清理向量和缓存。恢复备份后,删除事件再次应用。整个例子没有依赖模型在两段自然语言之间临场猜优先级。

怎样验收存储设计

写入测试覆盖主记录成功、索引失败、重复请求、对象上传失败和消费者重复事件。预期是主记录不重复,索引状态可见,失败可重试,孤儿对象可清理。

更新测试覆盖两个会话并发修改、旧事件晚到、条件并存和新版本索引完成前的读取。预期是旧版本不能覆盖新版本,冲突不会静默丢失。

检索测试覆盖跨租户、跨用户、无权限 Agent、已过期、被替代和已删除条目。即使手工把旧向量留在索引中,回表复查仍要挡住。

删除测试覆盖关系记录、向量、关键词、缓存、摘要、对象和备份恢复。权限测试覆盖角色变化后旧缓存失效。迁移测试覆盖 Embedding 版本切换和回滚。

性能要在真实规模与查询模式下测。没有运行数据时,只说明索引、分区、缓存、批处理和观测方案,不虚构吞吐、延迟、存储成本或节省比例。

Agent 记忆存储验收清单

存得进去只是起点,更新不乱、读取不越权、删除不复活才算完整。

面试官会怎样连续追问

追问一:为什么不直接把向量库当主库

向量库擅长相似检索,但权限、事务、唯一约束、版本冲突和精确状态通常需要结构化事实源。向量命中只能作为候选,不能直接证明可读与有效。

追问二:关系库里还要不要存 Embedding

取决于数据库能力与规模。可以使用支持向量的关系数据库,也可以独立向量服务。无论物理部署怎样,逻辑上仍要区分事实字段和语义索引责任。

追问三:怎样保证主表和向量索引一致

先事务提交主记录与事件,消费者按 memory_id 和 version 幂等构建索引,记录 index_status。搜索命中后回表复查最新状态,处理异步延迟。

追问四:两个会话同时改偏好怎么办

用版本条件检测并发冲突,再按来源和适用条件裁决。数据库最后写入顺序不能替代业务冲突处理。

追问五:用户删除后为什么还可能被搜到

向量、缓存或派生摘要可能没有同步。先让主记录不可读,检索回表拦截,再异步清理全部副本并跟踪状态。

追问六:长对话和大工具结果放哪里

完整原文和大工件放对象存储,关系层保存引用、哈希、版本和权限;记忆层只存筛选后的摘要或事实,不把所有轨迹复制进向量库。

用数据责任讲清 Memory Schema

Agent Memory 我会按责任拆成四层。关系库是事实源,保存记忆主体、租户、类型、内容或对象引用、来源、时间、版本、有效状态、权限、替代关系和索引状态;向量库只做语义候选索引,命中后回主记录复查权限和最新版本;完整文档、长轨迹和工具产物放对象存储;缓存只加速热点并随权限和版本失效。写入先在关系事务中提交主记录和待索引事件,消费者按 memory_id 加 version 幂等建索引。更新用版本条件处理并发,删除先让主记录不可读,再清理向量、缓存、摘要和对象。这样写入、检索、冲突、遗忘和权限都有明确事实源,也能在索引延迟时保持安全。

你的 Memory Schema 能否回答这些追问

  1. 是否区分关系事实源、语义索引、对象工件和缓存。
  2. 主记录是否包含主体、来源、版本、状态、权限、有效期和替代关系。
  3. 消息、任务状态、长期记忆和审计是否承担不同生命周期。
  4. 向量命中后是否回主记录复查权限与最新状态。
  5. 写入和索引是否有事件、幂等、重试与可观察状态。
  6. 并发更新是否用版本检测,而非最后写入者无声覆盖。
  7. 删除是否覆盖向量、缓存、摘要、对象与备份恢复路径。
  8. 缓存键是否包含租户、用户、权限版本和查询作用域。
  9. Embedding 版本是否可识别、迁移和回滚。
  10. 没有真实运行数据时,是否避免虚构吞吐、延迟、规模和成本。

好的 Memory Schema 不以“用了多少种数据库”证明专业。它应该让每条记忆有明确归属、来源、版本和退出机制,让语义检索保持灵活,同时又不牺牲权限、一致性和可删除性。

INTERVIEW FOLLOW-UPS

面试官会怎样继续追问?

这些追问会继续检查方案取舍、验证方法和项目事实。
  1. Q01

    为什么不能直接把向量数据库当作 Memory 主库?

  2. Q02

    关系主记录和向量索引怎样保持一致?

  3. Q03

    两个会话并发修改同一条记忆时怎么处理?

  4. Q04

    用户删除后怎样保证向量、缓存和备份不再返回?

PROJECT CHECK

换成你的项目,这几件事能否说清楚?

  • 区分关系事实源、向量索引、对象工件和缓存
  • 保存主体、来源、版本、状态、权限和索引状态
  • 用事件、幂等和版本处理写入与并发更新
  • 验证跨租户隔离和删除副本不再召回
用自己的项目经历练一次