☰
为Agent构建“海马体”:记忆分层、向量检索与断点续聊实战
2026/10/19 4:12:52 网站建设 项目流程

1. 为什么 Agent 需要"海马体":从无状态到有状态

1.1 无状态 Agent 的尴尬处境

先聊个大家都经历过的场景:你上午让 Agent 帮你整理了一份项目排期表,中午修改了需求文档里的两个截止日期,下午回来继续对话时,它完全不记得上午聊了什么,又开始从头问你项目目标是什么、团队有几个人、里程碑怎么划分。这种体验就像和一个失忆的同事开会,每句话都要重新交代背景,沟通成本直接拉满。

做过 Agent 开发的人都清楚,底层大模型本身是无状态的。每次调用 API,模型只看到你这次丢进去的 messages,它没有"前世今生"。传统做法是把历史对话全部拼进上下文窗口里,可窗口就那么大,对话一长就爆。而且更麻烦的是,很多场景不是单轮问答,而是多步骤任务——Agent 要分步执行工具调用、中间推理、结果确认,任何一个环节中断,整个工作流就断了。

"第二阶段"这个定位很准确。第一阶段能跑通单轮对话、能调工具,那只是 Agent 的"条件反射";真正让它像个有经验的助手,必须解决两个问题:记住该记住的,以及断了能接上。前者是持久化记忆,后者是断点续聊,合在一起,就是给它装一块"海马体"——负责把短期经验固化成长时记忆,在需要的时候再取出来用。

这篇文章不是教科书式科普,而是把我自己在模拟项目X里从零实现这套机制的过程、踩过的坑、最后留下的方案,完整拆一遍。适合已经跑通基础 Agent 调用、正在做记忆增强的开发者参考。看完你至少能搞清楚:记忆该存什么、用什么存、怎么取,以及断点续聊在工程上到底要怎么落地。

1.2 从生物记忆到工程映射

"海马体"这个比喻不是营销话术,它对应着非常具体的设计思路。人的记忆分感觉记忆、短期记忆和长期记忆:海马体负责把短期记忆转化为长期记忆,这个过程叫记忆巩固;需要的时候,通过线索提取相关记忆。Agent 的记忆系统完全可以照这个逻辑映射:

生物概念Agent 对应物技术实现
感觉记忆单次请求的原始输入API 调用参数
短期记忆当前会话上下文内存中的 messages 列表
长期记忆持久化的历史事实与经验数据库 + 向量索引
记忆巩固对话摘要与关键信息抽取LLM 自动摘要 + 结构化存储
记忆提取根据当前问题召回相关内容向量相似度检索 + 关键词过滤

这个映射一旦建立,整个系统的架构边界就清晰了:短期记忆负责"当前正在处理什么",长期记忆负责"这个用户/这个项目曾经发生过什么",两者之间通过写入管道和检索管道连接。对话中断恢复,本质上是把长期记忆里的会话快照重新加载回短期记忆。

我见过不少项目把记忆做成了"把所有历史对话原样塞回上下文",那不叫记忆,那叫缓存。真正的记忆系统必须解决三个问题:存什么(结构化抽取而非全文堆砌)、怎么存(满足高效检索而非线性扫描)、何时取(按需召回而非全量加载)。下面逐个拆。

2. 整体架构设计:记忆分层的三个维度

2.1 工作记忆与长期记忆的物理隔离

第一版设计里,我犯过一个典型错误:把所有历史消息丢进同一个存储表,读取时按时间倒序全部拉出来拼到 prompt 里。结果对话到第 20 轮的时候,prompt 里塞了一万多 token 的历史记录,模型开始"遗忘"最新的指令——因为旧的上下文把注意力窗口挤满了。后来我强制拆分了两套存储:

工作记忆(短期)放在 Redis 里,TTL 设置为 30 分钟。结构很轻,就是一个会话 ID 对应的上下文缓存,存的是最近 N 轮原始消息,用于"当前任务未完成时快速续上"。这里的关键是 N 不能太大,我实测控制在 10~15 轮以内效果最好,超过这个量级,模型对最新指令的遵循度明显下降。

长期记忆(持久)放在数据库里,分两张表:一张存事实型记忆(用户偏好、项目信息、关键结论),一张存会话摘要(每轮对话的压缩表征)。长期记忆的写入不是实时的——如果每轮对话都写,会产生大量冗余数据,而且摘要质量会很差。我采用的是"触发式写入 + 定期压缩"。

提示:工作记忆和长期记忆一定要分开存储,不能混在一张表里用 type 字段区分就完事。原因有二:一是生命周期完全不同,短期要能随时淘汰,长期要永久保留;二是检索策略不同,短期是"全量加载",长期是"按需召回",混在一起会导致性能问题。

2.2 记忆写入策略:摘要、抽取、触发时机

记忆写入是我踩坑最多的环节。最初我天真地想:"每轮对话都让 LLM 总结一下发生了什么然后存进去。"结果跑了一天,数据库里堆了三百多条高度重复的摘要——"用户询问了项目进度""用户确认了方案A"——全是废话,真正有用的细节被淹没了。

后来我改成三段式写入流程:

第一步,抽取事实。利用结构化输出,让 LLM 从每轮对话里提取"用户明确表达的信息",比如偏好("我习惯用 Python 写脚本")、决策("方案 B 通过了")、任务状态("登录模块已联调完成")。每条事实带一个置信度字段,低置信度的不写库。

第二步,生成摘要。不是逐轮存,而是每累计 5 轮对话或上下文超过一定 token 量时,才对这段时间的对话生成一条摘要。摘要里包含:本次讨论了什么主题、得出了什么结论、还有哪些未决事项。这样数据量立刻降下来了。

第三步,定期压缩。每 20 轮对话,把历史摘要再做一次"摘要的摘要",同时对同一主题下多条事实做去重合并。这个压榨过程会损失一些细节,但换来的是长期记忆的精炼和检索效率的大幅提升。

触发时机的选择也很讲究。我最后的方案是"双条件触发":轮次达到阈值(5 轮),或者检测到明确的任务节点变更(比如工具调用完成、用户确认了某个决策),任一满足就执行写入。后者需要给 Agent 的执行框架加一个事件钩子,在关键节点主动触发记忆固化,而不是被动等轮次攒够。

2.3 记忆读取策略:主动召回与被动加载

读取策略决定了记忆系统的质量上限。全量加载不行,只按时间倒序取最近记录也不行——用户可能三天前聊过的东西才真正有用。我的实现分两路:

被动加载发生在会话启动或断点恢复时:把该会话最近一次的工作记忆快照、最近 N 条会话摘要、以及关联的所有用户偏好一次性拉出来,拼成"记忆概览"注入 system prompt。这个过程是确定的,不需要模型参与,保证会话恢复后至少能接上大方向。

主动召回发生在每轮用户消息进来之后:把当前用户消息做 embedding,去向量数据库里做相似度检索,取 topK 条相关记忆,作为"参考信息"注入到当前轮的 prompt 中。这部分需要模型判断哪些记忆与当前任务相关,但检索本身是确定的。

两个读取通道必须配合使用。只靠被动加载,对话超过几轮后新的短期记忆会覆盖旧的,Agent 又会"失忆";只靠主动召回,第一轮没有历史上下文时就是空中楼阁,不知道这个用户是谁、以前聊过什么。实测下来,被动加载提供"骨架",主动召回补充"血肉",两者结合才能让 Agent 在长对话中保持稳定的身份和上下文感知。

3. 持久化落地:存储选型与数据结构设计

3.1 方案对比:从 SQLite 到向量数据库

存储选型时我对比了三条路线:

纯关系型数据库(SQLite/PostgreSQL)适合存结构化事实和会话摘要,查询灵活,支持事务。缺点是语义检索做不了——你不能"按意思相近"查记忆,只能靠 LIKE 模糊匹配或标签过滤,召回质量很受限。

纯向量数据库适合做语义检索,但金融级的事务支持和复杂条件过滤比较弱,而且如果只存 embedding,原始文本的更新、删除、版本管理都很别扭。

关系库 + 向量库组合是目前的主流做法,也是我最终采用的方案。关系库管事实和摘要的"原文",向量库管它们的"语义索引",两边用统一的记忆 ID 关联。这个方案的好处是各取所长:精确查询走 SQL,模糊召回走向量,互不干扰。

注意:如果你只是做个 Demo 或者内部工具,没必要一上来就上分布式向量库。我最初用 SQLite + 一个轻量级的本地向量索引(比如基于 numpy 实现的暴力检索)就能跑通全流程,数据量到几万条记忆时才迁移到独立的向量库服务。过早引入重组件只会让项目复杂度失控。

3.2 核心表结构设计

直接给出我在模拟项目X里最终落地的表结构,你在自己项目里可以直接抄:

用户表(用户维度):

CREATE TABLE users ( user_id TEXT PRIMARY KEY, -- 用户唯一标识 profile JSONB, -- 用户画像,如偏好、习惯 created_at TIMESTAMP DEFAULT now() );

记忆事实表(事实型长期记忆):

CREATE TABLE memory_facts ( fact_id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(user_id), content TEXT NOT NULL, -- 事实原文 category TEXT, -- 分类:偏好/决策/任务状态/项目信息 confidence FLOAT, -- 置信度 0~1 source_msg TEXT, -- 来源于哪条消息,方便溯源 created_at TIMESTAMP DEFAULT now(), updated_at TIMESTAMP DEFAULT now() ); CREATE INDEX idx_facts_user ON memory_facts(user_id, category);

会话摘要表(对话经验记忆):

CREATE TABLE memory_sessions ( session_id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(user_id), summary TEXT NOT NULL, -- 摘要内容 topic TEXT, -- 主题标签 start_time TIMESTAMP, end_time TIMESTAMP, msg_count INT, -- 覆盖的消息条数 is_compressed BOOLEAN DEFAULT FALSE -- 是否已被二次压缩 );

断点快照表(支持续聊的关键):

CREATE TABLE session_snapshots ( session_id TEXT PRIMARY KEY, user_id TEXT NOT NULL, snapshot JSONB NOT NULL, -- 序列化的工作记忆快照 updated_at TIMESTAMP DEFAULT now() );

向量索引表(语义检索):

CREATE TABLE memory_embeddings ( memory_id TEXT PRIMARY KEY, -- 关联事实或摘要的 ID memory_type TEXT, -- fact 或 session_summary user_id TEXT NOT NULL, embedding VECTOR(1536), -- 向量维度与 embedding 模型对齐 created_at TIMESTAMP DEFAULT now() );

这套结构的关键设计在于:事实和摘要分离,事实可以独立更新、修正(用户改变主意了就把旧的标为低置信度),摘要只增不删靠压缩降冗余;快照表专门负责断点续聊,存的是序列化后的上下文对象,和记忆事实表解耦,恢复时直接反序列化即可。

3.3 embedding 模型的选型与索引策略

向量维度直接决定了检索性能和成本。我用的是 1536 维的通用 embedding 模型,单条文本的 embedding 耗时约 20~50ms,一万条记忆的暴力检索大约 300ms,勉强可用。数据量再往上就得考虑索引——我在本地测试过几种,简单说下结论:

  • 暴力扫描(Flat):精确度高,但十万条以上延迟就不可接受了,适合数据量小的起步阶段。
  • IVF(倒排文件):把向量空间聚类成多个桶,检索时只搜最近的几个桶,召回速度能提升一个数量级,代价是少量精度损失。适合中等数据量。
  • HNSW(分层小世界图):图索引结构,召回速度和精度平衡得最好,百万级数据也能稳定在几十毫秒级。生产环境我推荐直接上这个。

检索时还有一个容易忽略的细节:必须按 user_id 过滤。多用户场景下,如果不加过滤条件,A 用户的记忆可能会被 B 用户的对话召回,这不仅是体验问题,还是隐私问题。我的做法是 SQL 里先按 user_id 缩小候选集,再对候选集做向量检索。有的向量库支持预过滤(pre-filter),有的只支持后过滤(post-filter),选型时要注意这个差异,否则可能返回大量无关结果再被程序抛弃,白白浪费计算量。

4. 断点续聊的核心实现:快照、恢复与上下文重建

4.1 会话快照机制:把"进行到哪了"存下来

断点续聊的本质,是把 Agent 的运行时状态完整保存下来,中断后能精确恢复。这个状态不只是消息列表,还包括:

  • 当前任务的执行计划(如果 Agent 正在分步执行多阶段任务)
  • 已调用的工具及其参数、返回结果
  • 中间推理链(Agent 为什么做了某个决定)
  • 尚未完成的下一个动作
  • 用户上下文(当前关注的项目、隐含约束等)

我设计快照数据结构时参考了状态机的思路:

{ "session_id": "sess_8f2a...", "user_id": "user_001", "updated_at": "2026-05-12T09:30:00Z", "context": { "current_goal": "整理Q3项目排期并生成甘特图", "messages": [ {"role": "user", "content": "把登录模块的验收提前到周五"}, {"role": "assistant", "content": "好的,我更新一下排期..."} ], "pending_tools": [ {"tool": "update_schedule", "params": {"task_id": "T-102", "due": "2026-05-15"}} ], "execution_plan": [ {"step": 1, "desc": "定位排期表中登录模块任务", "status": "done"}, {"step": 2, "desc": "更新截止日期", "status": "pending"} ], "scratchpad": "用户强调验收优先级高于联调" } }

快照的保存时机,并不是每轮都存——那样 I/O 太频繁了。我的策略是:关键节点保存。工具调用完成、任务阶段切换、用户明确确认某个结果时,打一个快照点。平时只更新 Redis 里的工作记忆缓存,快照表里的 JSONB 则在这些关键节点落地。这样既能保证中断时最多丢失"最近一轮对话"这种程度的进度,又不会把数据库写爆。

4.2 恢复流程:从快照到可对话状态的三步操作

用户重新发起对话时,恢复流程分三步:

第一步,加载快照与长期记忆。根据 session_id 查 session_snapshots 表拿到最新快照,反序列化后恢复 messages 和工具状态;同时从 memory_facts 和 memory_sessions 里拉取该用户的偏好和最近摘要,组装成一份"记忆概览"。

第二步,校验状态一致性。这一步很多人会忽略。恢复的上下文里如果有尚未执行的工具调用参数,需要校验这些参数引用的资源是否仍然有效——比如要更新的任务是否已被删除。我的做法是让工具函数在挂起前先做一次"预校验",把校验结果也存进快照,恢复时直接拿缓存结果,避免重复调用外部服务。

第三步,注入 system prompt。组装恢复后的首轮请求,system prompt 里明确告诉模型:"这是一次恢复的会话,以下是之前的对话摘要和关键记忆,请继续帮助用户完成未完成的任务。"同时把挂起的工具参数原样放回,让模型决定是继续执行还是先向用户确认。

这套三步流程实测下来,恢复后的对话基本能做到"无缝衔接",用户不会感觉到中间断了一截。

4.3 核心代码:一个可落地的恢复函数

直接上恢复逻辑的核心代码,基于 Python 伪代码风格编写,你可以自由适配到自己的框架:

def resume_session(session_id: str, user_msg: str) -> str: # 1. 拉取快照 snapshot = get_snapshot(session_id) if not snapshot: return start_new_session(user_msg) # 2. 拉取长期记忆概览 memory_overview = build_memory_overview(snapshot["user_id"]) # 3. 组装 system prompt system_prompt = f""" 你是一个具备持久记忆的智能助理。 这是用户之前的会话快照: {json.dumps(snapshot["context"], ensure_ascii=False)} 用户长期记忆概览: {memory_overview} 请基于以上信息继续帮助用户。如果存在未完成的工具调用,请优先处理。 """ # 4. 将历史消息 + 新消息一起发给 LLM messages = [{"role": "system", "content": system_prompt}] messages += snapshot["context"]["messages"] messages.append({"role": "user", "content": user_msg}) response = call_llm(messages) # 5. 收到回复后异步更新工作记忆缓存,并在关键节点落快照 update_work_memory(session_id, user_msg, response) maybe_save_snapshot(session_id, response) return response

这段代码里最容易被忽视的是第 4 步的 messages 组装顺序:先注入记忆概览(system 级),再放历史消息,最后是新用户消息。历史消息要截断到最近 N 轮,防止上下文爆掉。我项目里 N 设为 12,配合前面的摘要机制,既保证了连续性,又控制了 token 成本。

另外,快照里的 pending_tools 不建议直接塞给模型让它"自动继续执行"。工具调用涉及外部副作用,恢复后最好先输出一条中间消息问用户"检测到您之前有一个未完成的操作(更新任务 T-102 的截止日期),是否继续执行?"——让用户确认再动手,避免因为快照状态过期导致误操作。

5. 常见问题与排查实录:踩坑后的经验清单

5.1 记忆污染:模型把无关内容当成事实写进记忆

这是记忆系统上线后最头疼的问题。LLM 抽取事实时,经常把推测、假设甚至用户随口一说的话当成确定事实存进长期记忆。比如用户说"我可能下个月要换数据库",模型就记住了"用户下个月换数据库",再过几轮就变成了"用户计划迁移数据库到新方案",事实就这样慢慢被扭曲了。

我的解决办法是三条:

  • 抽取事实时强制模型输出 confidence 字段,低于 0.7 的不入库。虽然模型自评的置信度不完全可靠,但能过滤掉大多数模棱两可的表达。
  • 给事实加"证据来源"字段(source_msg),后续如果要更新或纠正,可以追溯到原始对话,避免无据可查。
  • 定期对长期记忆做"矛盾检测":把同一主题下的多条事实并排发给模型,让它标出相互矛盾的记录,人工或者自动标记为待确认状态。

5.2 检索召回质量差:为什么相关记忆总是找不回来

另一个高频问题是向量检索召回结果和当前问题关系不大。排查时我建议按这几步走:

  1. 先看 embed模型选择是不是有问题。通用 embedding 对专业术语(比如"联邦学习""时序数据库分片")的表征能力有限,如果领域特征明显,考虑在领域语料上微调一个 embedding 模型,或者切换到领域适配的向量模型。
  2. 再看 chunk 切割。如果一条记忆太长,语义会被稀释,embedding 的方向会偏向片段里的高频信息。我实测把每条记忆压到 100 字以内再入库,召回准确率能提升约 15%。
  3. 最后看检索参数。topK 太小会漏,太大会引入噪声。我项目里 topK=5 时效果最均衡,同时配合一个"最小相似度阈值"(0.75),低于阈值的结果直接丢弃。

如果你发现某类记忆总召不回,另一个实用技巧是混合检索:向量相似度 + 关键词命中双重打分,加权排序后取 topK。SQLite 里用 FTS5 做全文索引,和向量检索结果做融合,效果比纯向量好不少,实现成本也不高。

5.3 快照恢复后上下文错乱:时间线与状态不一致

快照恢复不是简单的"读出来塞回去",真实场景里会遇到很多状态不一致的问题。我整理过一张排查表,写在这里供你直接参考:

症状可能原因排查方案
恢复后模型态度突变system prompt 里的记忆概览与当前话题冲突检查记忆概览里是否混入了过期事实,给事实加时间过滤
工具重复执行快照里 pending_tools 没有被清除恢复流程里先清空旧的 tool 状态,由模型重新发起
消息顺序混乱快照 messages 与新增消息时间戳冲突所有消息按单调递增的 sequence 排序,不依赖时间戳
上下文被截断快照里的 messages 太长了恢复时先做一轮摘要压缩,再注入最近的 5~10 轮原消息
用户信息张冠李戴多用户共用了同一份记忆空间全链路检查 user_id 过滤,从 SQL 查询到向量检索都不能漏

其中"工具重复执行"这个问题最值得展开。我们项目里出过一次事故:恢复会话后,Agent 把用户已经审批通过的"发送周报"操作又执行了一遍,原因是快照里保留了"等待执行"状态的工具调用,恢复时模型发现了它就直接执行了,没有向用户确认。从那以后我定了一条硬规则:恢复后的第一个动作永远不是执行工具,而是先向用户确认上下文。宁可多一轮交互,也不能让 Agent 自作主张操作有副作用的动作。

5.4 存储膨胀与成本控制:记忆不是越多越好

还有一个很多人在前期意识不到的问题:记忆数据量增长极快。每轮对话都抽取事实、每 5 轮生成摘要、每 20 轮压缩一次,单用户单日对话 100 轮,一周下来事实表能攒出上千条记录。如果上线后没有清理策略,检索性能和存储成本都会失控。

我的建议是双管齐下:一是分层淘汰,低置信度、超过 90 天未访问的事实定期归档或删除;二是记忆合并,把同主题的多个细小事实合并成一个完整条目,保留合并时间。这个过程可以放在凌晨的定时任务里做,不占用在线资源。我自己实测,每月跑一次合并清理,事实表规模能缩减 40% 左右,语义检索的精度还会因为噪声减少而上升。

6. 从海马体延伸:记忆系统的下一步演进

做到这里,Agent 的"海马体"算是有雏形了:能记住事实,能回忆对话,能断点续聊。但如果你想让记忆系统从"能用的工具"变成"真正的核心竞争力",我个人认为还有两个值得探索的方向。

第一个是记忆的主动遗忘。生物大脑不是所有记忆都永久保存,它会在睡眠时修剪掉无关的突触连接。当前这套系统只进不出,长期跑下来一定会积累大量过期甚至错误的记忆。我在想能否设计一个"记忆质量评估器",定期给每条记忆打分,低于阈值的自动弱化,被后续对话反复纠正的记忆降权,让系统像一个真人一样"越聊越清楚",而不是"越聊越混乱"。

第二个是跨会话的经验迁移。现在的事实和摘要都是围绕单用户组织的,但很多经验是可以复用的——比如针对某个特定工具的使用技巧、某些常见任务的执行模板。如果把这类"经验型记忆"从用户维度抽离,存成全局共享的知识库,再通过检索注入到新用户的会话里,新用户的第一轮体验会好非常多。这个方向实现起来比想象中复杂(涉及隐私边界、全局与个人的优先级冲突),但价值也大得多。

最后分享一个我做这个项目最深的体会:记忆系统没有一劳永逸的完美方案,它是一个需要持续调优的系统,评估标准就一句话——用户是否感觉到这个 Agent"记得我"。所有的存储选型、检索策略、快照机制,最终都是为了这个目标服务。先把能跑通的版本上线,然后在真实对话数据里去发现问题、调整参数,比憋大招一次性设计一个"完美系统"要务实得多。希望这篇文章能帮你少走我走过的弯路,给你的 Agent 也装上一块靠谱的"海马体"。

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

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

立即咨询