现在大家做 AI Agent,都能搭个基础的任务循环:接收用户输入 → 调用工具 → 返回结果。但做出来的 Agent 特别“健忘”:用户聊了半小时,前面说过的信息转头就忘,每次对话都要重新说一遍背景,体验特别差。
核心问题就是缺了长记忆模块。我们在做长记忆架构的时候,踩了不少坑,今天把整体的实现思路拆解出来,给做 Agent 开发的同学做个参考。
为什么简单的对话历史拼接没用
很多人做长记忆,就是把之前所有的对话历史全部拼到提示词里,觉得这样 Agent 不就记住了?
实际根本不行:首先上下文长度有限,聊个十几轮就把窗口占满了;其次就是前面说过的长上下文注意力衰减问题,对话轮次一多,前面的信息 Agent 根本注意不到。
而且不是所有历史对话都需要记住——用户随口说的废话、临时的情绪吐槽,根本不需要占记忆空间,全塞进去反而会干扰 Agent 的判断。
长记忆的三层存储架构
下面是三层存储之间的数据流转关系,以及写入判断、召回拼接、遗忘机制分别在哪个环节触发:
#mermaid-svg-Ce14kdeUw0v23U8s{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Ce14kdeUw0v23U8s .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Ce14kdeUw0v23U8s .error-icon{fill:#552222;}#mermaid-svg-Ce14kdeUw0v23U8s .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Ce14kdeUw0v23U8s .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Ce14kdeUw0v23U8s .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Ce14kdeUw0v23U8s .marker.cross{stroke:#333333;}#mermaid-svg-Ce14kdeUw0v23U8s svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Ce14kdeUw0v23U8s p{margin:0;}#mermaid-svg-Ce14kdeUw0v23U8s .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-Ce14kdeUw0v23U8s .cluster-label text{fill:#333;}#mermaid-svg-Ce14kdeUw0v23U8s .cluster-label span{color:#333;}#mermaid-svg-Ce14kdeUw0v23U8s .cluster-label span p{background-color:transparent;}#mermaid-svg-Ce14kdeUw0v23U8s .label text,#mermaid-svg-Ce14kdeUw0v23U8s span{fill:#333;color:#333;}#mermaid-svg-Ce14kdeUw0v23U8s .node rect,#mermaid-svg-Ce14kdeUw0v23U8s .node circle,#mermaid-svg-Ce14kdeUw0v23U8s .node ellipse,#mermaid-svg-Ce14kdeUw0v23U8s .node polygon,#mermaid-svg-Ce14kdeUw0v23U8s .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Ce14kdeUw0v23U8s .rough-node .label text,#mermaid-svg-Ce14kdeUw0v23U8s .node .label text,#mermaid-svg-Ce14kdeUw0v23U8s .image-shape .label,#mermaid-svg-Ce14kdeUw0v23U8s .icon-shape .label{text-anchor:middle;}#mermaid-svg-Ce14kdeUw0v23U8s .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Ce14kdeUw0v23U8s .rough-node .label,#mermaid-svg-Ce14kdeUw0v23U8s .node .label,#mermaid-svg-Ce14kdeUw0v23U8s .image-shape .label,#mermaid-svg-Ce14kdeUw0v23U8s .icon-shape .label{text-align:center;}#mermaid-svg-Ce14kdeUw0v23U8s .node.clickable{cursor:pointer;}#mermaid-svg-Ce14kdeUw0v23U8s .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Ce14kdeUw0v23U8s .arrowheadPath{fill:#333333;}#mermaid-svg-Ce14kdeUw0v23U8s .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Ce14kdeUw0v23U8s .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Ce14kdeUw0v23U8s .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ce14kdeUw0v23U8s .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Ce14kdeUw0v23U8s .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ce14kdeUw0v23U8s .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Ce14kdeUw0v23U8s .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Ce14kdeUw0v23U8s .cluster text{fill:#333;}#mermaid-svg-Ce14kdeUw0v23U8s .cluster span{color:#333;}#mermaid-svg-Ce14kdeUw0v23U8s div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Ce14kdeUw0v23U8s .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Ce14kdeUw0v23U8s rect.text{fill:none;stroke-width:0;}#mermaid-svg-Ce14kdeUw0v23U8s .icon-shape,#mermaid-svg-Ce14kdeUw0v23U8s .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Ce14kdeUw0v23U8s .icon-shape p,#mermaid-svg-Ce14kdeUw0v23U8s .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Ce14kdeUw0v23U8s .icon-shape .label rect,#mermaid-svg-Ce14kdeUw0v23U8s .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Ce14kdeUw0v23U8s .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Ce14kdeUw0v23U8s .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Ce14kdeUw0v23U8s :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
用户输入
短期工作记忆(提示词窗口,最近 5-10 轮)
写入判断是否有值得长期记住的事实?
丢弃,不写入记忆
提取结构化事实条目打上时间、标签
中期事实记忆(向量数据库)
长期偏好记忆(用户画像)
召回拼接按当前问题召回 3-5 条相关记忆
拼入提示词,生成回复
遗忘机制过期自动删除 / 用户明确要求删除
我们把长记忆分成了三层,不同的记忆存在不同的地方,需要的时候再调出来:
第一层:短期工作记忆
就是当前对话轮次内的上下文,放在提示词窗口里,存最近5-10轮的对话,用来做当前任务的上下文参考,这部分和我们平时做对话机器人没区别。
第二层:中期事实记忆
用户在对话中说的关键事实,比如用户的身份、偏好、之前提过的需求,用结构化的方式存到向量数据库里。
比如用户说“我是做装修公司的,在长沙,之前找过三家服务商都不满意”,系统自动把这些事实提取出来,打上标签存起来,下次用户再对话的时候,自动检索相关的事实,拼到提示词里,Agent 就知道“哦,这个用户是长沙开装修公司的”,不用用户反复说。
第三层:长期偏好记忆
跨对话的用户偏好,比如用户喜欢简洁的回复、不喜欢被推销、之前对什么问题特别反感,这些信息存到用户画像里,是不会随对话删除的,每次对话都加载,用来调整 Agent 的回复风格。
记忆的写入和召回逻辑
不是所有对话内容都要写进记忆的,我们做了一个简单的写入判断:
这样既不会把记忆撑爆,又能保证 Agent 需要的时候,能拿到之前用户说过的关键信息。
下面用一个 Python 示例,把「写入判断」和「召回拼接」这两步串起来看:
from typing import List, Dict
import json
# 假设这是你的向量数据库客户端,封装了相似度检索
class VectorDB:
def search(self, query: str, top_k: int = 5) –> List[Dict]:
# 实际项目中这里会调用 FAISS / Milvus / pgvector 等
# 返回形如 [{"id": "fact_001", "text": "用户是长沙的装修公司", "score": 0.92}, …]
...
def judge_should_write(conversation: str, llm) –> bool:
"""
第 1 步:让大模型判断这轮对话里有没有值得长期记住的事实。
返回 True 表示需要写入记忆,False 表示只是闲聊/临时内容。
"""
prompt = f"""
判断下面这轮对话中是否包含需要长期记住的用户事实
(如身份、偏好、需求、重要承诺等)。
只回答 yes 或 no,不要输出其它内容。
对话:
{conversation}
"""
answer = llm.chat(prompt).strip().lower()
return answer == "yes"
def extract_facts(conversation: str, llm) –> List[Dict]:
"""
第 2 步:把对话里的关键事实提取成结构化条目,
打上时间、标签,方便后续按语义召回。
"""
prompt = f"""
从下面的对话中提取需要长期记住的事实,输出 JSON 数组。
每个元素包含字段:text(事实内容)、tag(分类标签,如 identity/preference/requirement)。
没有可提取的事实就返回 []。
对话:
{conversation}
"""
raw = llm.chat(prompt)
return json.loads(raw)
def recall_and_build_prompt(user_question: str, db: VectorDB, llm) –> str:
"""
第 3 步:根据当前问题召回最相关的 3-5 条记忆,
拼接到提示词里,让 Agent 在回答时能带上历史背景。
"""
memories = db.search(user_question, top_k=5)
memory_block = "\\n".join(f"- {m['text']}" for m in memories)
prompt = f"""
以下是该用户之前提到过的相关背景信息:
{memory_block}
请结合以上背景,回答用户当前的问题:
{user_question}
"""
return prompt
# 使用示意
# if judge_should_write(last_round, llm):
# facts = extract_facts(last_round, llm)
# for f in facts:
# db.upsert(f) # 写入向量数据库
# prompt = recall_and_build_prompt(current_question, db, llm)
核心逻辑就三步:先让大模型判断这轮对话值不值得记,值得就把事实结构化后写入向量库;下次用户提问时,再用当前问题去向量库召回最相关的几条记忆,拼进提示词。这样既不会把记忆库撑爆,又能保证 Agent 在需要的时候拿到之前的关键信息。
下面是「写入判断 → 事实提取 → 召回拼接 → 生成回复」的完整流程图:
#mermaid-svg-NhYE1xE45IqzUcdp{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NhYE1xE45IqzUcdp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NhYE1xE45IqzUcdp .error-icon{fill:#552222;}#mermaid-svg-NhYE1xE45IqzUcdp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NhYE1xE45IqzUcdp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NhYE1xE45IqzUcdp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NhYE1xE45IqzUcdp .marker.cross{stroke:#333333;}#mermaid-svg-NhYE1xE45IqzUcdp svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NhYE1xE45IqzUcdp p{margin:0;}#mermaid-svg-NhYE1xE45IqzUcdp .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-NhYE1xE45IqzUcdp .cluster-label text{fill:#333;}#mermaid-svg-NhYE1xE45IqzUcdp .cluster-label span{color:#333;}#mermaid-svg-NhYE1xE45IqzUcdp .cluster-label span p{background-color:transparent;}#mermaid-svg-NhYE1xE45IqzUcdp .label text,#mermaid-svg-NhYE1xE45IqzUcdp span{fill:#333;color:#333;}#mermaid-svg-NhYE1xE45IqzUcdp .node rect,#mermaid-svg-NhYE1xE45IqzUcdp .node circle,#mermaid-svg-NhYE1xE45IqzUcdp .node ellipse,#mermaid-svg-NhYE1xE45IqzUcdp .node polygon,#mermaid-svg-NhYE1xE45IqzUcdp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NhYE1xE45IqzUcdp .rough-node .label text,#mermaid-svg-NhYE1xE45IqzUcdp .node .label text,#mermaid-svg-NhYE1xE45IqzUcdp .image-shape .label,#mermaid-svg-NhYE1xE45IqzUcdp .icon-shape .label{text-anchor:middle;}#mermaid-svg-NhYE1xE45IqzUcdp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NhYE1xE45IqzUcdp .rough-node .label,#mermaid-svg-NhYE1xE45IqzUcdp .node .label,#mermaid-svg-NhYE1xE45IqzUcdp .image-shape .label,#mermaid-svg-NhYE1xE45IqzUcdp .icon-shape .label{text-align:center;}#mermaid-svg-NhYE1xE45IqzUcdp .node.clickable{cursor:pointer;}#mermaid-svg-NhYE1xE45IqzUcdp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NhYE1xE45IqzUcdp .arrowheadPath{fill:#333333;}#mermaid-svg-NhYE1xE45IqzUcdp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NhYE1xE45IqzUcdp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NhYE1xE45IqzUcdp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NhYE1xE45IqzUcdp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NhYE1xE45IqzUcdp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NhYE1xE45IqzUcdp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NhYE1xE45IqzUcdp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NhYE1xE45IqzUcdp .cluster text{fill:#333;}#mermaid-svg-NhYE1xE45IqzUcdp .cluster span{color:#333;}#mermaid-svg-NhYE1xE45IqzUcdp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NhYE1xE45IqzUcdp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NhYE1xE45IqzUcdp rect.text{fill:none;stroke-width:0;}#mermaid-svg-NhYE1xE45IqzUcdp .icon-shape,#mermaid-svg-NhYE1xE45IqzUcdp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NhYE1xE45IqzUcdp .icon-shape p,#mermaid-svg-NhYE1xE45IqzUcdp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NhYE1xE45IqzUcdp .icon-shape .label rect,#mermaid-svg-NhYE1xE45IqzUcdp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NhYE1xE45IqzUcdp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NhYE1xE45IqzUcdp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NhYE1xE45IqzUcdp :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
否
是
每轮对话结束
大模型判断是否有值得长期记住的事实?
丢弃,不写入记忆
提取结构化事实条目打上时间、标签
写入对应记忆层(中期事实 / 长期偏好)
下次对话开始
按当前问题召回最相关的 3-5 条记忆
拼入提示词
生成回复
必须做的记忆遗忘机制
人的记忆是会忘的,Agent 也不能把所有信息都存着,不然时间长了记忆库全是垃圾信息。
我们做了两个遗忘规则:
加上遗忘机制之后,记忆库不会越来越臃肿,召回的内容也都是有用的信息,准确率比全量存储高很多。
下面用一个 Python 示例,把「过期自动删除」和「用户明确删除」这两种遗忘操作串起来看:
from typing import List, Dict
from datetime import datetime, timedelta
# 假设这是你的向量数据库客户端,封装了按条件删除的能力
class VectorDB:
def delete_by_ids(self, ids: List[str]) –> None:
# 实际项目中这里会调用 FAISS / Milvus / pgvector 的 delete 接口
# 例如:collection.delete(ids=ids)
...
def delete_by_filter(self, filter_expr: str) –> None:
# 按元数据过滤删除,例如过期时间 < 当前时间
# 例如:collection.delete(filter="expire_at < '2026-10-08 00:00:00'")
...
def auto_delete_expired(db: VectorDB, now: datetime = None) –> int:
"""
规则 1:过期临时事实自动删除。
每条记忆在写入时都带一个 expire_at 时间戳,
定期扫描(比如每小时跑一次)把已过期的条目删掉。
"""
now = now or datetime.now()
# 构造过滤表达式:删除所有 expire_at 早于当前时间的记忆
filter_expr = f"expire_at < '{now.isoformat()}'"
db.delete_by_filter(filter_expr)
# 实际项目中这里会返回删除条数,便于记录日志
return 0
def delete_by_user_request(db: VectorDB, memory_id: str) –> None:
"""
规则 2:用户明确说“忘掉这个事”,立刻删除对应条目。
这里 memory_id 是向量库里该条记忆的唯一 id,
通常由召回时返回,或由用户指认的某条记忆解析得到。
"""
db.delete_by_ids([memory_id])
# 使用示意
# 1. 定时任务里调用:auto_delete_expired(db)
# 2. 用户说“忘掉我上次说的出差安排”时:
# memory_id = resolve_memory_id_from_user_intent(user_msg, db)
# delete_by_user_request(db, memory_id)
核心逻辑就两条:一是给每条临时记忆打上 expire_at 时间戳,定时把过期的批量删掉;二是用户明确要求遗忘时,直接按记忆 id 精确删除。删除操作都通过向量数据库的 delete 接口完成,和写入、召回共用同一套存储,保证记忆库始终干净、召回结果始终有效。
下面是遗忘机制的两种触发路径流程图:
#mermaid-svg-wjKw6go6sCYUBqLO{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wjKw6go6sCYUBqLO .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wjKw6go6sCYUBqLO .error-icon{fill:#552222;}#mermaid-svg-wjKw6go6sCYUBqLO .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wjKw6go6sCYUBqLO .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wjKw6go6sCYUBqLO .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wjKw6go6sCYUBqLO .marker.cross{stroke:#333333;}#mermaid-svg-wjKw6go6sCYUBqLO svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wjKw6go6sCYUBqLO p{margin:0;}#mermaid-svg-wjKw6go6sCYUBqLO .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-wjKw6go6sCYUBqLO .cluster-label text{fill:#333;}#mermaid-svg-wjKw6go6sCYUBqLO .cluster-label span{color:#333;}#mermaid-svg-wjKw6go6sCYUBqLO .cluster-label span p{background-color:transparent;}#mermaid-svg-wjKw6go6sCYUBqLO .label text,#mermaid-svg-wjKw6go6sCYUBqLO span{fill:#333;color:#333;}#mermaid-svg-wjKw6go6sCYUBqLO .node rect,#mermaid-svg-wjKw6go6sCYUBqLO .node circle,#mermaid-svg-wjKw6go6sCYUBqLO .node ellipse,#mermaid-svg-wjKw6go6sCYUBqLO .node polygon,#mermaid-svg-wjKw6go6sCYUBqLO .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wjKw6go6sCYUBqLO .rough-node .label text,#mermaid-svg-wjKw6go6sCYUBqLO .node .label text,#mermaid-svg-wjKw6go6sCYUBqLO .image-shape .label,#mermaid-svg-wjKw6go6sCYUBqLO .icon-shape .label{text-anchor:middle;}#mermaid-svg-wjKw6go6sCYUBqLO .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wjKw6go6sCYUBqLO .rough-node .label,#mermaid-svg-wjKw6go6sCYUBqLO .node .label,#mermaid-svg-wjKw6go6sCYUBqLO .image-shape .label,#mermaid-svg-wjKw6go6sCYUBqLO .icon-shape .label{text-align:center;}#mermaid-svg-wjKw6go6sCYUBqLO .node.clickable{cursor:pointer;}#mermaid-svg-wjKw6go6sCYUBqLO .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wjKw6go6sCYUBqLO .arrowheadPath{fill:#333333;}#mermaid-svg-wjKw6go6sCYUBqLO .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wjKw6go6sCYUBqLO .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wjKw6go6sCYUBqLO .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wjKw6go6sCYUBqLO .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wjKw6go6sCYUBqLO .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wjKw6go6sCYUBqLO .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wjKw6go6sCYUBqLO .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wjKw6go6sCYUBqLO .cluster text{fill:#333;}#mermaid-svg-wjKw6go6sCYUBqLO .cluster span{color:#333;}#mermaid-svg-wjKw6go6sCYUBqLO div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wjKw6go6sCYUBqLO .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wjKw6go6sCYUBqLO rect.text{fill:none;stroke-width:0;}#mermaid-svg-wjKw6go6sCYUBqLO .icon-shape,#mermaid-svg-wjKw6go6sCYUBqLO .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wjKw6go6sCYUBqLO .icon-shape p,#mermaid-svg-wjKw6go6sCYUBqLO .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wjKw6go6sCYUBqLO .icon-shape .label rect,#mermaid-svg-wjKw6go6sCYUBqLO .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wjKw6go6sCYUBqLO .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wjKw6go6sCYUBqLO .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wjKw6go6sCYUBqLO :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
是
否
是
否
记忆库中的条目
是否过期?expire_at < 当前时间
自动批量删除delete_by_filter
保留
用户是否明确要求遗忘?
按记忆 id 精确删除delete_by_ids
继续保留,参与后续召回
记忆库保持干净召回结果始终有效
性能与边界条件
三层存储架构在真实场景下会遇到几个典型的性能瓶颈,下面结合具体数值分析一下:
1. 单用户对话超过 100 轮时的召回延迟变化
短期工作记忆只保留最近 5-10 轮,所以对话轮次增加并不会线性拖慢提示词拼接——真正影响延迟的是中期事实记忆的召回。当单用户对话超过 100 轮时,向量库中该用户的事实条目可能累积到 200-500 条。此时单次召回(top_k=5)的延迟通常在 20-50ms 量级(以 FAISS 的 IVF 索引、单机 CPU 为例),相比 10 轮以内的 5-15ms 有明显上升,但仍在可接受范围。真正的风险在于:如果每次对话都做一次「写入判断」的大模型调用,100 轮下来累计的 LLM 调用成本会显著增加。
2. 向量数据库条目超过 10 万条时的检索性能瓶颈
当全局条目超过 10 万条时,暴力检索(brute-force)的延迟会飙升到 200ms 以上,必须切换索引策略。建议:
- 使用 IVF-PQ 或 HNSW 索引,10 万条规模下召回延迟可控制在 10-30ms;
- 按用户 ID 做分区(partition),把检索范围从「全库」缩小到「该用户的子集」,通常能再降一个数量级;
- 给条目打上 user_id 和 tag 元数据,召回时先用过滤条件缩小候选集,再做向量相似度计算。
3. 短期工作记忆窗口大小对回复质量的影响
窗口太小(比如只保留 3 轮),Agent 容易丢失当前任务的上下文,回复显得「断片」;窗口太大(比如 20 轮以上),一方面提示词变长、单次推理延迟和成本上升,另一方面会触发长上下文注意力衰减,前面的信息反而被稀释。经验值:5-10 轮是性价比最高的区间,既能覆盖大多数任务的上下文,又不会明显拖慢推理。如果任务确实需要更长的上下文,建议把关键信息提前沉淀到中期事实记忆,而不是无限扩大窗口。
4. 遗忘机制定时扫描频率与系统开销的平衡
过期扫描太频繁(比如每分钟一次),会反复触发向量库的过滤删除,占用数据库连接和 IO;太稀疏(比如每天一次),临时事实会残留较久,召回时可能拼入已失效的信息。建议:
- 临时事实的 expire_at 粒度按小时设置,扫描频率设为每小时一次,与过期粒度对齐;
- 扫描时用「批量过滤删除」而非逐条删除,单次删除 1000 条以内的开销通常在几十毫秒;
- 把扫描放到低峰时段(如凌晨),避免与用户请求高峰争抢数据库资源。
整体来看,三层架构在百万级条目、千级并发下仍能保持可用,关键在于:索引策略选对、按用户分区、窗口大小克制、遗忘扫描错峰。把这些边界条件处理好,长记忆模块才能从「能跑」变成「扛得住」。
写在最后
长记忆是 Agent 从“玩具”变成真能用的工具的核心模块,不是把对话历史全塞到提示词里那么简单。把三层存储、写入判断、召回逻辑、遗忘机制这几块搭好,你的 Agent 才能真的像个记得住事的助手,而不是聊完就忘的鱼。
本文为技术科普,不构成任何商业建议。
网硕互联帮助中心





评论前必须登录!
注册