ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent长短期记忆架构设计:向量数据库选型与实战经验

AI Agent长短期记忆架构设计:向量数据库选型与实战经验

接手某区域总部的智能问答项目时,我原以为最棘手的是RAG链路里的向量数据库选型。真正跑起来才发现,业务方对Agent的要求早就不是“能检索”,而是“记住”。同一个用户昨天问过什么、上周说过什么偏好,agent如果全忘了,那体验和搜索引擎没什么两样。所以这一个月我大部分时间都在折腾一件事:给AI agent设计一套长短期记忆架构,让它既能应对会话内的连续追问,也能跨会话沉淀用户画像。这篇就来复盘我在这套架构里怎么选向量数据库、怎么设计记忆分层,以及实测下来踩过的坑。

先说结论:如果你的agent还停留在“每次对话都重新开始”的阶段,赶紧把记忆架构补上。这篇内容适合正在做RAG应用、想把智能体推进到生产环境的团队,也适合一个人捣鼓agent demo、想知道怎么设计记忆层的新手。我尽量少讲虚的,把选型逻辑、架构分层、关键代码和排查过程都摊开讲。

1. 为什么Agent需要长短期记忆:先搞清楚要解决什么问题

1.1 场景还原:从一次“答非所问”说起

刚接到这个需求时,业务方给我举了一个例子:用户上周在助手里面说过“我比较喜欢摩托车路线,不喜欢高速”,这周用户问“推荐一条周末短途路线”。如果agent没有记忆,它只能根据当前这句问题给出一堆通用推荐,甚至推荐全程高速的方案。用户觉得“这不是智能,这是复读机”。

这种场景不是个例。决策类、偏好类、身份类信息,用户往往只会在某次对话里透露一次,如果agent不把这些信息沉淀下来,后续所有对话都是在“盲聊”。我当时给这个项目定的目标很朴素:让agent拥有“记住关键信息”的能力,而不是把所有信息都塞进上下文窗口。

1.2 长短期记忆的本质是分层存储

很多团队一上来就想着往向量数据库里塞东西,但连“什么该记、记多久”都没想清楚。我的理解很简单:短期记忆和长期记忆,本质上是数据生命周期不同的两类存储。

  • 短期记忆(工作记忆):当前会话内的信息,比如用户刚说“我只有半天时间”,这种信息有效期只有几分钟,用完就丢或者做摘要压缩。
  • 长期记忆(持久记忆):跨会话的稳定事实,比如用户身份、偏好、历史决策结论,这种信息需要长期保存,且要能被随时检索。

这个分层看起来简单,实际设计时要考虑的问题不少:短期记忆放在哪里、窗口多大、什么时候把短期记忆“固化”成长期记忆、长期记忆怎么检索、新旧记忆冲突了怎么办。后面几节我逐个拆解。

2. 向量数据库选型:我锁定的五个维度和两个反直觉结论

2.1 选型前先列需求:不能只看官网测评分数

市面上的向量数据库测评文章一搜一大把,召回率、QPS、内存占用这些指标确实重要,但在我这个项目里,真正决定选型的是另外五个维度:

第一是写入和更新的稳定性。记忆系统不是离线导入一次就完事,而是随时都在增量写入。用户每产生一条值得记的信息,就要往向量库里面写一条。如果向量库在频繁写入时检索性能急剧下降,或者upsert操作经常超时,这个系统就没法上线。

第二是过滤能力。我们的长期记忆必须按用户ID隔离,检索时只能检索当前用户的记忆,不能A用户搜到B用户的偏好。这意味着向量库必须支持“向量相似度检索 + 标量字段过滤”的组合查询,而且过滤条件要生效在向量检索之前,否则数据量一大就会出现严重的精度问题。

第三是索引参数的可调整性。不同业务场景对召回率和延迟的要求不一样,我希望索引类型(比如HNSW、IVF)和参数(邻居数、构建候选数)是可以在线调整或者至少能快速重建的。买了一个黑盒服务,结果索引参数全被定死,后面优化会很被动。

第四是运维成本。团队没有专职的数据库运维,我不可能为了一个记忆模块专门养一个集群。所以自建方案必须足够轻,托管方案必须在预算内,关系库扩展方案要评估对现有库的压力。

第五是生态集成。我们的技术栈主要是Python,后续还要做定时任务做记忆清理和遗忘,所以选型时特别关注有没有现成的客户端、有没有批量操作接口、有没有异步写入能力。

2.2 三个候选方案的实测对比

我当时把候选方案分成三类:自建开源向量库、全托管向量服务、关系库内嵌向量能力。为了不涉及具体商业产品,这里分别叫方案A、方案B、方案C。实测跑了一周,把对比结果整理成一张表:

对比维度方案A:自建开源向量库方案B:全托管向量服务方案C:关系库内嵌向量能力
写入延迟(1000条批量写入)约1.2秒,稳定约2.8秒,受网络影响约3.5秒,受关系库负载影响
检索延迟(百万级向量,P99)约20ms约35ms约80ms
标量过滤支持好,可过滤后向量检索好一般,复杂条件容易走全表
索引参数调整完全可调,可重建部分可调依赖关系库版本
运维成本需自己部署和监控零运维,按量付费复用现有库,但需评估压力
数据隔离能力强,可按集合/分区隔离强,但需设计好key中等,依赖表结构

最终我选了方案A,主要原因有两个:一是写入延迟和检索延迟都最稳,二是索引参数完全可调,后面做优化时不会束手束脚。方案B适合不想维护基础设施的团队,但记忆型业务的数据量增长很快,按量付费的账单会越来越难看。方案C在数据量小时完全够用,但一旦超过百万级向量,复杂过滤场景下的检索延迟会上升到不可接受的程度。

2.3 选型背后的两个“反直觉”经验

第一个反直觉经验:召回率不是第一优先级,写入稳定性才是。很多人选型时盯着评测集的Recall@10看,但在记忆系统里,用户记忆的写入是持续性的,如果写入经常失败,用户上一秒刚说过的偏好下一秒就丢了,召回率再高也没有意义。

第二个反直觉经验:不用盲目追求高维向量。当时有人建议用1024维的向量模型,说表达能力强。实测下来,768维的模型在我们的数据上召回效果和1024维几乎一样,但索引占用内存和检索延迟都明显更低。维度不是越高越好,要看你实际的数据分布和模型能力。

提示:选型不要只看峰值性能,要看“持续写入压力下的P99延迟”。拿小数据集测不出真差距,最好用模拟的增量写入脚本压测至少3天再看结果。

3. 长短期记忆架构设计:把Agent当成一个会成长的系统

3.1 短期记忆:会话层的读写策略

短期记忆我直接用缓存中间件保存会话原始文本,没有放到向量库里。原因很简单:短期记忆访问频率极高,但单一记忆的保存时间很短,用向量库去承载这种高频低价值的读写太浪费。

短期记忆的窗口大小我一开始定的是最近10轮对话,后来发现10轮不够。用户经常会先问几个问题、中间穿插别的话题、再回到最初的需求,如果窗口太小,前面的关键信息就丢了。后来调整成“按时间和轮数的双阈值”:超过20轮,或者距离当前时间超过2小时,就触发一次摘要压缩。

压缩策略是这样的:把窗口内的对话用大模型生成一段摘要,摘要里保留用户偏好、待办事项、明确结论这三类信息,然后把摘要写入长期记忆。这样短期记忆像便利贴一样随写随撕,长期记忆像档案柜一样只存放有价值的东西。

3.2 长期记忆:向量化存储与召回规则

长期记忆是系统的核心,主要存三类内容:

  • 用户画像类:身份、职业、地域、明确说过的偏好。
  • 决策结论类:用户在对话里做过的选择,比如“定了,走山路路线”。
  • 业务事实类:和具体业务相关的状态信息,比如“合同还在审批阶段”。

每条长期记忆写入时,我先用嵌入模型把文本转成向量,同时把记忆内容的结构化字段(用户ID、记忆类型、创建时间、更新时间、置信度)以payload形式一起存进去。向量用来做相似度检索,payload用来做过滤和后续的逻辑控制。

召回规则是:用户发起新对话时,先按用户ID过滤出该用户的全部长期记忆,再用当前对话内容做向量检索,取top5记忆拼接到系统提示词里。这个规则看着简单,但实际写的时候有几个细节很关键:记忆召回结果必须带上时间戳,这样模型能判断哪些是旧偏好、哪些是新偏好;检索分数低于阈值的记忆宁可不要,也不能硬塞给模型。

3.3 记忆控制器:决定“什么时候记”“记什么”

标题里提到的“指挥官”,我落地成了一个叫记忆控制器的模块。它本质上是一个规则引擎加调度器,负责三件事:

记忆写入决策。并不是每轮对话都值得写长期记忆。我的做法是先让大模型对每轮对话打一个标签:是否包含值得长期保存的信息,并给出记忆类型和置信度。置信度高于0.8才写入长期记忆,0.6到0.8之间的进入待确认队列,低于0.6直接丢弃。

记忆冲突处理。典型场景是用户先说“我喜欢摩托车路线”,三天后又说“摩托车太累了,还是开车吧”。这两条记忆是冲突的。控制器会在写入新记忆时检查是否存在类型相同且内容方向相反的历史记忆,如果存在,就把旧记忆标记为“已过期”,降权处理,让新记忆在召回时排在前面。

主动遗忘。长期记忆不代表永远保存。一些临时性信息,比如“这个月月底前要提交材料”,过期后就该被清理。控制器每天跑一个定时任务,检查记忆里的过期时间字段,自动归档或删除过期记忆,避免向量库被垃圾数据塞满。

注意:记忆控制器是整个架构设计的核心,也是和纯RAG系统的最大区别。RAG只知道“从库里检索”,控制器知道“该不该记、记什么、什么时候忘”。

4. 实操落地:从一个向量集合到完整记忆系统

4.1 数据准备与Embedding管道

进入实操环节之前,先说数据。很多团队犯的错是直接把对话原文切片后灌入向量库,结果垃圾进垃圾出。我的做法是先做一层清洗和过滤。

清洗规则按优先级排列:去掉纯寒暄对话和“嗯”“好的”这类无意义回复;合并同一用户在同一时间窗口内的连续相似表述;对包含用户隐私信息的原始文本做脱敏处理,只保留业务需要的字段。清洗完的数据再按“用户偏好、决策结论、业务事实”分别打标签,方便后面按类型过滤。

Embedding部分我选了一个中文效果稳定的向量模型,输出维度768。写入前用了一个小技巧:把记忆文本结构化后再嵌入。比如用户的原始说法是“我不喜欢走高速,上次堵车堵了三个小时”,我会让大模型先改写成结构化记忆:“偏好:出行避开高速公路,原因是堵车体验不佳,时间约在2025年某月”,再把这条结构化文本送入嵌入模型。实测下来,结构化后的记忆召回精度比直接存原始对话高了不少。

4.2 向量库写入与检索代码骨架

下面这段是核心记忆写入的伪代码,基于我实际项目里的结构简化而来,可以当作设计参考:

# 记忆写入模块(伪代码) from vector_store import VectorCollection, SearchParams, DistanceMetric class LongTermMemory: def __init__(self, collection_name="agent_memory_v1", dim=768): # 按用户ID分partition,保证数据隔离和清理 self.collection = VectorCollection( name=collection_name, dimension=dim, distance_metric=DistanceMetric.COSINE ) # 索引参数:HNSW,M控制邻居数量,ef_construction控制构建召回 self.collection.create_index( index_type="HNSW", params={"M": 32, "ef_construction": 200} ) def write_memory(self, user_id, memory_text, memory_type, confidence, expire_at=None): # 1. 文本结构化(实际项目里这里会调用大模型做改写) structured_text = self._normalize_memory(memory_text) # 2. 向量化 embedding = self._embed(structured_text) # 3. 写入向量库,payload保存结构化字段 self.collection.upsert( id=get_memory_id(user_id, memory_text), vector=embedding, payload={ "user_id": user_id, "text": structured_text, "type": memory_type, # preference / decision / fact "confidence": confidence, "created_at": now(), "expire_at": expire_at } ) def recall_memory(self, user_id, query_text, top_k=5, min_score=0.55): # 检索时先按user_id过滤,再做向量搜索 query_embedding = self._embed(query_text) results = self.collection.search( vector=query_embedding, filter={"user_id": user_id}, limit=top_k, min_score=min_score ) return [r.payload for r in results if not r.payload.get("expired")]

这段代码里有几个设计点值得展开。

按用户ID分partition而不是直接全局检索:这是数据隔离的关键。如果不做过滤,全局向量检索会频繁召回别人的记忆,哪怕加了用户ID约束,检索效率也会受到影响。分partition之后,每次检索只扫描当前用户的数据,延迟和精度都可控。

cosine距离度量:对文本记忆来说,余弦相似度比欧式距离更符合语义比较的习惯,因为向量的模长和文本长度相关性太强,欧式距离会偏向短文本。实际用下来,cosine的召回结果在业务场景里更稳定。

min_score阈值:这个是防“硬凑”。向量检索只要排序就能返回前5条,但有时候这5条和当前问题根本不相关。设一个0.55的阈值,低于阈值的记忆不返回,宁缺毋滥。后面排查问题时会发现这个阈值很关键。

4.3 关键参数的推导与调试记录

top_k取值:我先从3开始测,发现3条记忆太少,用户上一个会话刚提到的信息经常覆盖不到。调到5之后,覆盖率明显提升。再往上调到8,模型输入变长,响应延迟增加,而且记忆之间互相矛盾的几率也上升。最终定在5。

chunk_size和embedding粒度:我们不是做文档库的RAG,而是记忆库,所以不存在大文档切片的说法。一条记忆就是一个短句或摘要,长度控制在100字以内。短的好处是记忆语义单一,召回时不会出现一条记忆包含多个互相矛盾的偏好。

HNSW参数M和ef_construction:初始配置M=32、ef_construction=200,召回率已经很理想。调大M到48后,召回率只提升了不到1%,但构建索引耗时增加了约40%,果断回退。这里建议大家不要盲目照搬默认值,先用数据集做一组小规模验证,确认提升幅度再决定要不要加参数。

记忆条数估算:假设有10万活跃用户,平均每个用户沉淀50条长期记忆,总量就是500万条向量。每条向量768维,对应原始浮点数据大约3KB,加上payload里的文本和字段,存储总量在10GB~20GB之间。这个量级方案A完全扛得住,不需要做额外的分库分表,只要按用户ID做partition就够了。

5. 优化与排查:我踩过的五个坑

5.1 召回质量差:从“换模型”到“混合检索”

第一版上线后最明显的问题是召回质量不稳定。用户明明说过“不吃辣”,用向量检索就是召回不出来。我先试了更换更强的向量模型,提升有限。真正让效果质变的是改成混合检索。

做法是把向量召回和关键词召回结合起来,再用一个简单的排序融合算法取交集。关键词部分我用的是轻量级的BM25实现,专门对记忆文本里出现过的具体名词做精确匹配。比如“不吃辣”这种关键词,在向量语义空间里可能匹配到“饮食清淡”,但在关键词空间里能精确命中。

融合逻辑用的是经典的RRF公式:给每个文档在向量召回和关键词召回里的排名取倒数,求和后排序。实测下来,混合检索把记忆召回准确率从78%提升到了92%。这一步可以说是整轮优化里性价比最高的一招。

提示:如果你的向量召回经常答非所问,先别急着换模型,看看是不是缺了关键词召回这一路。很多看似“语义理解不够”的问题,其实是精确匹配没有兜住。

5.2 写入瓶颈与批量策略

记忆写入是一个持续不断的过程,但业务方要求记忆写入不能阻塞正常对话。一开始我的实现是同步写入,用户每产生一条记忆就立刻写库,结果高峰期接口超时频发。

排查后发现两个问题:一是每条写入都要重新建连接,网络开销大;二是写入和检索共用一套资源,写入压力上来后检索也跟着慢。

解决方案:引入批量写入和异步队列。具体做法是先写入一个内存队列,后台每2秒或者攒够200条批量提交一次。检索走只读副本,写入走主库。上线后P99写入延迟从2.5秒降到800毫秒,检索完全不受影响。这里还有个小细节:向量库的upsert操作对大量小请求不友好,批量提交能显著提高吞吐,但要注意batch大小不是越大越好,我实测200条一批是最稳的。

5.3 新旧记忆冲突与遗忘机制

记忆冲突处理是控制器里最难的一块。刚开始我用的策略是“新记忆直接覆盖旧记忆”,结果出了麻烦:用户可能只是临时改主意,第二天又改回来,旧记忆被覆盖后就再也找不回来了。

后来改成版本化存储。同一类型的记忆保留多个版本,每条都带起始时间和结束时间。召回时默认取“当前时间仍在有效期内”的版本,如果存在多条,按时间倒序返回。用户改回原来的偏好时,再写一条新版本,旧版本自动归档但不清除。这个设计让冲突记忆的恢复成本大幅降低,也方便后面做记忆审计时回溯。

5.4 冷启动与脏数据清洗

系统上线前需要从历史对话里回填一批记忆,这部分坑最多。历史对话质量参差不齐,很多对话没有明确的用户ID标识,有些对话里还夹杂着客服的回复片段。

我定的清洗规则是:只有标记为“用户发言”且包含明确意图的文本才进入记忆库;客服回复一律不写入。同时做一个“记忆置信度”校验,对每条回填记忆计算一个来源可靠度分数,分数低于0.7的不写入。跑完清洗后,初始记忆库比预想的少了一半数据,但质量干净了很多。这里给个建议:冷启动阶段宁可少记,也不要让垃圾数据污染向量库,因为清洗脏数据的成本远高于重新生成。

5.5 监控指标与灰度发布

排查问题不能只靠用户反馈,我把关键指标整理成了一张监控表,挂在运维看板上:

指标预警阈值处理动作
记忆写入成功率低于99.5%检查批量提交队列是否堆积、向量库连接是否异常
记忆召回命中率低于85%检查min_score阈值是否过高、embedding模型是否有数据漂移
检索P99延迟超过150ms检查索引是否需要重建、数据量是否突破了partition阈值
记忆冲突率超过5%检查控制器版本化逻辑是否生效
过期记忆清理完成率低于90%检查定时任务是否被阻塞

灰度发布也很关键。我们按用户ID的hash值把流量切成10个桶,每天放量1~2个桶,先让小部分真实用户验证效果,没有明显问题再继续放量。这个阶段抓到了几个在测试环境里根本复现不了的问题,比如某类用户ID在过滤时因为编码不一致导致检索为空,这类问题只能靠灰度流量来暴露。

6. 后续扩展:几点个人预期

这套长短期记忆架构上线跑了两周,效果比预想的好。业务方反馈最明显的是用户画像召回变准了,同一个用户的连续会话里,agent不再像第一次见面一样说话。我最深的一个体会是:记忆架构的设计难点不在向量库本身,而在“该记什么、什么时候忘”这套控制逻辑,向量库只是最后的落地载体。

后面我想把记忆做成“可视化记忆卡片”的形式,用户可以在界面上手动确认或删除某条记忆,这样agent的长期记忆就不再是黑盒,用户对agent的信任感也会更强。另一个方向是把记忆召回从“单轮召回”升级成“多步规划召回”,让控制器根据当前对话目标推测未来可能需要的记忆,提前载入上下文。这个改动会复杂很多,但值得尝试。

最后说一个实操建议:做记忆系统不要一上来就追求大而全的架构。先跑通“短期记忆存会话、长期记忆进向量库、控制器做调度”的最小闭环,再按用户反馈迭代。记忆架构的优化空间很大,但前提是先有一个能在生产环境稳定运行的地基。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进