在实际 Agent 项目中,“AI Agent 总是失忆”不是一句玩笑话。对话刚结束,新建一个会话,它就不记得用户的偏好、项目背景和刚刚做过的决定;任务执行到一半,上下文一超限,前面的关键信息就被截断。根本原因在于,多数 Agent 本质上是无状态服务,每次请求只依赖当前上下文窗口里的内容,会话一结束、窗口一超限,模型对历史就没有感知。
要解决这个问题,不能只靠“把对话历史全部塞进 Prompt”,更合理的做法是搭建一套独立的长期记忆系统:把需要长期保留的信息从对话中提取出来、结构化存储、按需检索,再在生成回复前把记忆注入上下文。这篇文章会先从原理出发解释 Agent 为什么会失忆,然后带你从零搭建一个最小可运行的记忆系统,再讨论企业级落地时涉及的会话管理、检索链路、时效与遗忘、权限隔离和常见问题排查。适合正在做 Agent 应用、想解决多轮对话与跨会话记忆问题的开发者。
1. 先理解 Agent 为什么“失忆”:无状态调用与上下文窗口是根因
1.1 把历史对话全部塞进上下文,为什么行不通
很多人的第一反应是:把历史消息全部塞进请求里,模型不就记住了吗?这种方式在演示阶段有效,一旦进入真实场景就会暴露出三个问题。
第一是上下文窗口有限。当前模型的上下文窗口从几千 token 到几十万 token 不等,但真实业务对话中一次会话可以产生远超窗口上限的文本。窗口超限后只能截断,而截断往往丢掉的是最早的关键约定。
第二是成本和时延失控。每轮请求携带的 token 越多,推理耗时越长,费用也越高。把三个月前的寒暄、试错过程、重复问题全部带进每一轮请求,对结果没有帮助,纯属浪费。
第三是注意力被稀释。即使窗口放得下全部历史,模型也难以在一大堆冗余内容中准确找到当前任务真正需要的事实。历史越长,关键信息反而越容易被噪声覆盖。
更关键的是,这种方案根本无法处理跨会话记忆。用户关闭页面、重新打开一个新会话,旧的请求上下文完全消失,模型对上次会话一无所知。所以“塞历史”本质上是缓存,不是记忆。
1.2 把记忆分成三层:短期、工作与长期
业界在讨论 Agent 记忆时,通常会把记忆拆成三层来设计,而不是笼统地说“保存历史”。
短期记忆对应当前会话内的消息列表,生命周期就是一次会话,通常放在 Redis 或内存里,用滑动窗口控制长度。工作记忆对应当前任务的状态,比如执行计划、中间变量、已经调用过的工具和返回结果,任务结束即失效。长期记忆是跨会话保留的信息,比如用户偏好、重要事实、历史决策、领域知识,必须持久化存储,并且按需检索。
| 记忆类型 | 典型内容 | 存储位置 | 生命周期 | 访问方式 |
|---|---|---|---|---|
| 短期记忆 | 当前会话消息列表 | Redis、内存 | 会话结束或 TTL 到期 | 全量或窗口读取 |
| 工作记忆 | 任务计划、中间变量、工具调用结果 | 内存、分布式状态存储 | 任务结束 | 按 key 读取或更新 |
| 长期记忆 | 用户偏好、关键事实、历史决策、领域知识 | 数据库、向量库 | 长期保存,按策略遗忘 | 按需检索召回 |
三者的关系可以这样理解:短期记忆负责“当前对话上下文的连续性”,工作记忆负责“当前任务的执行连续性”,长期记忆负责“跨会话的身份和知识连续性”。Agent 记忆系统的核心工作,就是在这三层之间建立写入、读取和更新链路。
1.3 企业级记忆系统的四个核心能力
一个小玩具可以在代码里硬编码几条消息,但企业级 Agent 记忆系统必须具备四个能力。
提取能力:从非结构化的对话文本中抽取结构化记忆,包括用户画像、事件记录、业务结论,并给每条记忆打重要度分。存储能力:记忆要支持结构化字段、JSON 元数据和高维向量,能够按用户、Agent、记忆类型快速过滤。检索能力:根据当前问题召回最相关的记忆,而不是把全部记忆都塞给模型,检索要支持向量召回、关键词召回和重排序。管理能力:包括记忆的合并、更新、过期、遗忘、权限隔离和审计日志。
下文的技术选型和代码实现,都会围绕这四个能力展开。
2. 技术选型:记忆系统由哪几层组件构成,各层怎么选
2.1 存储层:向量库、关系库、缓存各司其职
长期记忆存储最常见的选择是“关系库 + 向量扩展”,典型组合是 PostgreSQL 配合 pgvector。这样做的好处是业务字段、JSON 元数据、向量和 SQL 过滤可以放在同一个数据库里,减少组件数量,事务也容易保证。中小规模项目用这一套就足够。
如果向量规模很大,比如千万级以上,并且有很高的并发检索要求,可以单独引入 Milvus、Qdrant 等专用向量数据库。短期记忆通常放在 Redis 里,利用它的 TTL 和列表结构保存最近消息。
| 组件 | 负责内容 | 适用场景 |
|---|---|---|
| PostgreSQL + pgvector | 长期记忆主存储、元数据过滤、向量检索 | 中小规模项目、团队已使用 PostgreSQL |
| Milvus / Qdrant | 大规模向量检索 | 千万级向量、高并发检索 |
| Redis | 短期记忆、会话消息、缓存 | 会话级上下文、热点记忆缓存 |
| MySQL / 其他关系库 | 只存结构化字段,不做向量检索 | 已有 MySQL 且数据量不大时 |
这里要注意一点:不要把短期记忆和长期记忆放在同一个地方管理。短期记忆是高频写入、短生命周期,长期记忆是低频写入、长生命周期,两者对存储和清理策略的要求完全不同。
2.2 提取层:让 LLM 把非结构化对话转换成结构化记忆
对话文本是自然语言,不能直接作为长期记忆存储。提取层要调用 LLM,把原始对话转换成结构化记忆条目。
一条典型的记忆条目应该包含记忆类型、内容、重要度、来源会话和时间。常见记忆类型可以设计为:user_profile 用户偏好与画像,episodic 重要事件和任务决策,knowledge 领域知识和业务规则。
提取逻辑通常放在对话的异步链路上。对话结束后,或者每积累若干轮消息后,把最近几轮消息交给 LLM 提取,而不是每轮都提取。这样既能控制成本,也能避免把中间过程的临时内容写入长期记忆。
2.3 检索层:按需召回而不是全量加载
长期记忆只能按需召回。检索层要做三件事。
第一,向量召回。把当前用户问题转成向量,与记忆表中的 embedding 做相似度检索。第二,元数据过滤。检索 SQL 必须强制带上 agent_id、user_id、记忆类型、过期时间等条件,保证只召回当前用户在当前 Agent 场景下的记忆。第三,重排序。向量相似度不能作为唯一排序依据,还要综合考虑记忆的重要度、访问次数和最近访问时间。
重排公式可以简化成:最终分数 = 向量相似度 × 权重 + 重要度 × 权重 + 时效因子 × 权重。具体权重需要在业务上调试,但“相似度 + 重要度 + 时效”这三个维度必须同时存在,否则会出现“检索到一堆相关但没价值的记忆”的情况。
2.4 对话集成层:记忆如何安全进入 Prompt
检索回来的记忆不能直接拼在用户消息后面。更稳妥的做法是组装出独立的结构化记忆块,放在系统提示词里,并告诉模型:“以下是该用户的长期记忆,回答时优先参考,但不要虚构记忆中没有的信息。”
组装顺序一般是:系统提示词 → 长期记忆块 → 短期记忆消息列表 → 当前用户输入。这里有两个关键控制点:记忆块必须带来源说明,比如“记忆类型:user_profile,来源:2025-06-01 会话”;记忆块必须受 token 预算限制,超出的部分要截断或减少召回数量。
3. 从零搭建最小可运行的 Agent 记忆系统
3.1 环境准备:依赖与基础设施版本
本节的项目结构使用 Python 3.10、FastAPI、PostgreSQL 15 + pgvector、Redis 7,并使用兼容 OpenAI 接口的 LLM 和 Embedding 服务。生产环境落地前,需要先确认项目实际依赖的版本和模型接口是否一致。
# docker-compose.yml version: "3.8" services: postgres: image: pgvector/pgvector:pg16 environment: POSTGRES_USER: agent POSTGRES_PASSWORD: agent123 POSTGRES_DB: agent_memory ports: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data redis: image: redis:7-alpine ports: - "6379:6379" volumes: pg_data:依赖清单如下。
# requirements.txt fastapi uvicorn psycopg2-binary pgvector redis openai pydantic-settings启动基础设施后,检查两个端口是否可用:5432 对应 PostgreSQL,6379 对应 Redis。如果本地端口冲突,需要修改 docker-compose 中的映射关系。
3.2 项目目录与数据模型设计
最小系统建议采用如下目录结构。
agent-memory/ ├── docker-compose.yml ├── requirements.txt ├── schema.sql └── app/ ├── main.py # FastAPI 入口,提供记忆读写接口 ├── memory_store.py # 长期记忆持久化与向量检索 ├── extractor.py # 调用 LLM 提取记忆、调用 Embedding 生成向量 └── memory_service.py # 业务编排:召回、重排、写入长期记忆表的设计是这套系统的核心。下面是基础 DDL。
-- schema.sql CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE IF NOT EXISTS agent_memory ( id BIGSERIAL PRIMARY KEY, agent_id VARCHAR(64) NOT NULL, user_id VARCHAR(64) NOT NULL, session_id VARCHAR(64), memory_type VARCHAR(32) NOT NULL, content TEXT NOT NULL, metadata JSONB NOT NULL DEFAULT '{}'::jsonb, importance REAL NOT NULL DEFAULT 0.5, embedding VECTOR(1536), access_count INT NOT NULL DEFAULT 0, last_accessed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), expired_at TIMESTAMPTZ ); CREATE INDEX idx_memory_owner ON agent_memory (agent_id, user_id, memory_type); CREATE INDEX idx_memory_expire ON agent_memory (expired_at);embedding 字段的维度要与 Embedding 模型输出维度保持一致。使用 text-embedding-3-small 时是 1536 维,如果换模型,表定义要同步改。生产环境中,数据量上来之后还需要创建专门的向量索引,比如 ivfflat 或 hnsw,开发阶段可以先用顺序扫描。
3.3 核心代码:记忆写入、提取与检索
先实现持久化层,负责写入和向量检索。
# app/memory_store.py import json import os import psycopg2 from pgvector.psycopg2 import register_vector class MemoryStore: def __init__(self): self.conn = psycopg2.connect(os.getenv("DATABASE_URL")) register_vector(self.conn) def save(self, memory: dict, embedding: list[float]): cur = self.conn.cursor() cur.execute( """ INSERT INTO agent_memory (agent_id, user_id, session_id, memory_type, content, metadata, importance, embedding, expired_at) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) """, ( memory["agent_id"], memory["user_id"], memory.get("session_id"), memory["memory_type"], memory["content"], json.dumps(memory.get("metadata", {}), ensure_ascii=False), memory.get("importance", 0.5), embedding, memory.get("expired_at"), ), ) self.conn.commit() def search(self, agent_id, user_id, query_embedding, memory_type=None, top_k=5): cur = self.conn.cursor() sql = """ SELECT id, memory_type, content, metadata, 1 - (embedding <=> %s) AS score FROM agent_memory WHERE agent_id = %s AND user_id = %s AND (expired_at IS NULL OR expired_at > now()) """ params = [query_embedding, agent_id, user_id] if memory_type: sql += " AND memory_type = %s" params.append(memory_type) sql += " ORDER BY score DESC LIMIT %s" params.append(top_k) cur.execute(sql, params) rows = cur.fetchall() return [ { "id": r[0], "memory_type": r[1], "content": r[2], "metadata": r[3], "score": r[4], } for r in rows ]这里使用<=>计算余弦距离,1 - 距离就是相似度分数。SQL 中强制过滤了 agent_id 和 user_id,这是多租户隔离的最基本要求。
接下来是提取层,负责调用 LLM 提取记忆,并调用 Embedding 服务生成向量。
# app/extractor.py import json import os from openai import OpenAI llm = OpenAI( base_url=os.getenv("LLM_BASE_URL"), api_key=os.getenv("LLM_API_KEY"), ) embedder = OpenAI( base_url=os.getenv("EMBEDDING_BASE_URL"), api_key=os.getenv("EMBEDDING_API_KEY"), ) EXTRACT_PROMPT = """ 你是记忆提取器。从对话中抽取值得长期保留的信息,包括: 1. user_profile:用户的偏好、身份、习惯 2. episodic:重要任务事件和决策过程 3. knowledge:领域结论和业务规则 只输出 JSON 数组,每项包含 memory_type、content、importance。 importance 取值范围 0 到 1,1 表示非常重要。 如果没有值得保留的信息,输出 []。 """ def extract_memories(conversation: list[dict]) -> list[dict]: resp = llm.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=[ {"role": "system", "content": EXTRACT_PROMPT}, {"role": "user", "content": json.dumps(conversation, ensure_ascii=False)}, ], temperature=0, ) text = resp.choices[0].message.content.strip() text = text.removeprefix("```json").removeprefix("```").removeprefix("```").removesuffix("```").strip() return json.loads(text) def embed_text(text: str) -> list[float]: resp = embedder.embeddings.create( model=os.getenv("EMBEDDING_MODEL"), input=text, ) return resp.data[0].embedding提取 Prompt 是记忆质量的关键。如果模型经常输出格式错误,可以在提取层增加重试和格式校验逻辑,而不是让它进入主链路报错。
最后是业务编排层,把提取、存储、召回和重排串起来。
# app/memory_service.py from .extractor import embed_text, extract_memories from .memory_store import MemoryStore class MemoryService: def __init__(self): self.store = MemoryStore() def recall(self, agent_id, user_id, query, top_k=5): query_embedding = embed_text(query) candidates = self.store.search( agent_id, user_id, query_embedding, top_k=top_k * 3 ) # 重排:相似度 + 重要度 + 时效 scored = [] for c in candidates: meta = c["metadata"] or {} importance = meta.get("importance", 0.5) rerank_score = c["score"] * 0.6 + importance * 0.4 scored.append((rerank_score, c)) scored.sort(key=lambda x: x[0], reverse=True) return [item[1] for item in scored[:top_k]] def remember(self, agent_id, user_id, session_id, conversation): extracted = extract_memories(conversation) saved = 0 for mem in extracted: mem["agent_id"] = agent_id mem["user_id"] = user_id mem["session_id"] = session_id mem["metadata"] = mem.get("metadata", {}) mem["metadata"]["importance"] = mem.get("importance", 0.5) embedding = embed_text(mem["content"]) self.store.save(mem, embedding) saved += 1 return saved这里需要注意的是:重排时如果没有考虑访问时间和访问次数,长期不用的重要记忆和刚写入的相关记忆会混在一起。生产环境可以在 SQL 中维护 last_accessed_at 和 access_count,再把它纳入重排分数。
3.4 与 Agent 对话循环集成:一次完整调用链
记忆系统只有接入对话循环才有意义。下面是一个简化的集成示例,包含短期记忆读写和长期记忆召回。
# app/agent.py import json import os from redis import Redis from extractor import embed_text from memory_service import MemoryService redis_client = Redis.from_url(os.getenv("REDIS_URL")) memory_service = MemoryService() def build_system_prompt(long_memories): if not long_memories: return "你是企业知识助手,回答要准确、简洁。" memory_text = "\n".join( f"- [{m['memory_type']}] {m['content']}" for m in long_memories ) return ( "你是企业知识助手,回答要准确、简洁。\n" "以下是该用户的长期记忆,回答时优先参考," "但不要虚构记忆中没有的信息。\n" + memory_text ) def run_turn(agent_id, user_id, session_id, user_input): # 1. 读取短期记忆 session_key = f"session:{session_id}:msgs" short_memory = [] for raw in redis_client.lrange(session_key, 0, -1): short_memory.append(json.loads(raw)) # 2. 召回长期记忆 long_memories = memory_service.recall(agent_id, user_id, user_input) # 3. 组装 Prompt messages = [{"role": "system", "content": build_system_prompt(long_memories)}] messages.extend(short_memory) messages.append({"role": "user", "content": user_input}) # 4. 调用 LLM reply = llm.chat.completions.create( model=os.getenv("LLM_MODEL"), messages=messages, ).choices[0].message.content # 5. 把本轮消息写入短期记忆,并触发异步记忆提取 redis_client.rpush( session_key, json.dumps({"role": "user", "content": user_input}, ensure_ascii=False), ) redis_client.rpush( session_key, json.dumps({"role": "assistant", "content": reply}, ensure_ascii=False), ) redis_client.ltrim(session_key, -20, -1) redis_client.expire(session_key, 86400) # 提取放到后台任务,避免阻塞主链路 recent = short_memory[-6:] + [ {"role": "user", "content": user_input}, {"role": "assistant", "content": reply}, ] memory_service.remember(agent_id, user_id, session_id, recent) return reply这只是一个最小闭环。真实项目中,提取操作应该放入消息队列或异步任务框架中执行,并记录失败次数。如果提取链路依赖外部 LLM 服务,还需要超时控制和失败降级,不能让记忆提取失败反过来影响用户请求。
4. 企业级落地:从“能跑”升级到“能上线”的关键机制
4.1 短期记忆会话管理:Redis Key 设计与滑动窗口
短期记忆的 Redis Key 设计必须带上租户、Agent、用户和会话维度。
{tenant_id}:{agent_id}:{user_id}:session:{session_id}:msgs带 tenant_id 是为了多租户隔离。如果只用人维度,不同企业租户的用户 ID 可能冲突,会发生会话串号。Key 的 TTL 应根据业务会话周期设置,比如企业客服场景可以设置 24 小时,长周期项目场景可以设置 7 天。
滑动窗口的实现方式是 ltrim 只保留最近 N 条,N 的取值要根据模型上下文窗口和单条消息平均 token 数计算。更准确的做法是按 token 数裁剪,而不是按消息条数裁剪,因为一条消息可能包含很长的工具返回结果。
4.2 长期记忆的多租户隔离与权限控制
长期记忆是最敏感的数据,隔离必须同时发生在存储层和应用层。
存储层的隔离就是检索 SQL 中强制带 agent_id 和 user_id。应用层的隔离是统一的鉴权入口。不要信任客户端传入的 user_id,必须从当前登录态中解析用户身份,再由后端注入查询条件。企业内部还要区分普通用户和管理员:用户可以查看和删除自己的记忆,管理员可以查看统计和审计日志,但不能直接修改业务记忆内容。
如果 Agent 系统支持多企业租户,建议在记忆表增加 tenant_id 字段,并把所有索引都带上 tenant_id,避免不同租户的数据互相干扰检索结果。
4.3 遗忘机制与时效管理
长期记忆如果没有遗忘机制,数据库会越来越大,检索结果会被过期信息污染。遗忘机制可以设计成三个层次。
第一是硬删除。对明确失效的记忆,比如用户主动要求删除的历史数据,直接物理删除。第二是软过期。每条记忆可以设置 expired_at,过期后不再参与检索,由定时任务统一清理。第三是分数衰减。记忆的最终得分要引入时间衰减因子,比如“最后访问时间越久,得分越低”,这样不常用的旧记忆会自然沉底。
最终分数 = 向量相似度 * 0.5 + 重要度 * 0.3 + 时效因子 * 0.2时效因子的具体函数可以根据业务调整,比如用最近访问天数进行指数衰减。衰减的目的不是删除记忆,而是把用户当下最可能需要的内容排到前面。
4.4 一致性、审计与生产保障
记忆写入失败不能影响主对话链路,这是生产环境的红线。常用做法是写入操作走独立异步任务,失败进入重试队列,超过重试次数后写入失败日志,由值班人员处理。
审计方面,建议增加一张记忆审计表,记录每次写入的来源会话、提取前的原始对话摘要、写入时间和操作人。用户对记忆内容提出异议时,可以快速定位是哪轮对话导致了错误记忆。
生产环境还需要关注三个点:数据库备份与恢复演练、记忆服务的接口限流与超时、以及记忆检索的监控指标,包括召回耗时、写入成功率、单次召回 token 数。没有监控的记忆系统,出问题时只能靠用户反馈来发现。
5. 运行验证:怎样确认 Agent 真的“记住了”
5.1 设计跨会话验证场景
验证记忆系统不能只在同一会话里连续聊天。最有效的验证方式是设计一组跨会话场景。
| 验证场景 | 操作步骤 | 预期结果 |
|---|---|---|
| 记住用户偏好 | 会话 1:告诉 Agent 你是数据团队,输出要带 SQL 方言 | 会话 2:提出分析问题,回答自动带 SQL,且风格匹配 |
| 记住历史决策 | 会话 1:讨论后确定主库用 PostgreSQL | 会话 2:询问上次技术选型结果,回答包含 PostgreSQL 及原因 |
| 提取业务结论 | 会话 1:约定每周五发布版本 | 会话 2:询问发布节奏,回答提到周五 |
| 记忆不串号 | 用户 A 写一条私密偏好 | 用户 B 询问相同问题,不应包含 A 的记忆 |
如果场景 1 能通过,说明“写入 + 提取 + 检索 + Prompt 注入”整条链路是通的。
5.2 验证检索召回质量
检索层需要单独验证,不能只看最终对话效果。可以写一个调试脚本,直接调用 recall 方法查询。
python -c " from memory_service import MemoryService svc = MemoryService() for m in svc.recall('agent_demo', 'user_001', '上次我们决定用什么数据库?'): print(m['memory_type'], round(m['score'], 3), m['content'][:40]) "预期输出应该类似:
knowledge 0.862 项目最终选择 PostgreSQL 作为主库,原因是团队熟悉 episodic 0.731 2025-06-01 评审会上否决了 MongoDB 方案如果召回结果与问题无关,优先检查 Embedding 模型是否一致、记忆内容是否足够语义完整。注意,单条记忆内容不能太短,比如“好的”“同意”这种碎片不适合作为长期记忆。
5.3 验证 Prompt 组装结果
检索正确不代表模型会用上。需要把最终发给模型的消息打印出来检查。可以在 run_turn 中临时增加日志输出,确认系统提示词里确实包含记忆块,且记忆块顺序在用户消息之前。
System: 你是企业知识助手。以下是该用户的长期记忆... - [knowledge] 项目最终选择 PostgreSQL 作为主库 - [user_profile] 用户来自数据团队,输出需要带 SQL 方言 User: 请帮我写一个查询最近订单量的 SQL如果记忆块出现在正确位置,但回答仍然没有参考它,问题往往出在提示词措辞上。可以改为更明确的指令:“你必须在回答中优先使用上述记忆中的事实,记忆冲突时说明不同来源。”
5.4 性能与资源验证
记忆系统上线前至少要关注四组指标。
| 指标 | 开发环境参考 | 生产环境建议 |
|---|---|---|
| 单次记忆召回耗时 | 100ms 以内 | 控制在 200ms 以内 |
| 记忆写入成功率 | 调试阶段可失败重试 | 99.9% 以上,失败走告警 |
| 单次召回 token 占用 | 不超过上下文 20% | 设置硬上限,超限截断 |
| 重复记忆比例 | 人工抽查 | 写入链路做相似度去重 |
写入链路要去重,否则同一事实反复被提取,数据库中会出现大量重复条目。去重的简单做法是写入前先按用户和内容做一次精确匹配,更合理的做法是用向量相似度做近似去重。
6. 常见问题排查:记忆丢失、召回为空、答非所问
6.1 先看这条排查链路
记忆系统出问题时,不要直接改 Prompt。要按照“写入 → 存储 → 检索 → 注入 → 生成”这条链路逐层排查。
现象 └─ 1. 日志里是否触发记忆提取 └─ 2. 数据库里是否落库成功 └─ 3. 检索 SQL 是否能召回 └─ 4. 组装后的 Prompt 是否包含记忆 └─ 5. 模型回答是否参考记忆任何一层断了,最终表现都是“Agent 没记住”或“记住但没用上”。
6.2 记忆没有写入
现象是数据库里查不到新的记忆记录。优先检查三处:是否到达了触发条件,比如对话轮数不足、后台任务没有执行;提取 LLM 返回的内容是否是合法 JSON,格式错误会导致解析异常;写入事务是否因为字段错误回滚,比如 embedding 维度不匹配。
排查时看应用日志里提取链路的输出。建议在提取服务中记录原始返回文本,格式错误时能够直接看到。不要把异常吞掉,否则问题非常难追。
6.3 写入成功但检索不到
数据库有记录,但召回结果为空。常见原因有三个。
第一,检索时使用的问题向量与写入时的内容向量由不同模型生成,导致向量空间不一致,相似度普遍很低。第二,SQL 中 agent_id 或 user_id 过滤条件不匹配,比如写入时 user_id 带了前缀,查询时没有带。第三,expired_at 已经过期,或者顺序扫描在大表上超时被截断。
检查方法是把查询 SQL 中的过滤条件逐个手工执行一遍。先在库里确认记录存在,再确认 embedding 字段有值,再执行相似度查询,逐步缩小范围。
6.4 检索到了但注入 Prompt 无用
召回正常,但回答没有利用记忆。先看组装后完整 Prompt。如果记忆块被放在很靠后的位置,可能已经被上下文截断;如果记忆块内容过短,模型无法判断它的用途;如果系统提示词没有明确说明记忆块的使用方式,模型倾向于忽略。
解决方式:把记忆块放到系统提示词中,并增加使用指令;在生成阶段对记忆块单独设置 token 上限,避免被长任务内容挤掉。
6.5 记忆冲突、过期与上下文污染
同一事实产生多个矛盾版本,是长期记忆系统必然遇到的现象。比如用户在会话 1 说“周报用 Excel”,在会话 5 又说“周报改用在线表格”。如果不处理,检索会把两个版本同时捞出来,模型就会给出矛盾答案。
处理策略有两种。一种是覆盖策略:对同一用户和同一主题,新记忆写入时标记旧记忆为失效。另一种是版本策略:保留多个版本,并在记忆块中标注时间和来源,让模型自行判断哪个更新。生产环境建议先做覆盖策略,配合来源时间和会话 ID 的审计字段,必要时再升级到版本管理。
| 问题现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 新会话完全不记得 | 未触发写入或写入失败 | 查提取日志和 agent_memory 表 | 补全触发条件,增加失败重试 |
| 写入成功但召回为空 | Embedding 模型不一致 | 对比写入和查询时的模型名 | 统一模型,重建失效向量 |
| 召回结果相关但回答没用 | 记忆块被截断或指令不清 | 打印组装后的完整 Prompt | 调整 Prompt 结构,设置记忆 token 上限 |
| 回答出现矛盾信息 | 记忆冲突未处理 | 按用户查同主题多条记录 | 实现覆盖策略或版本标注 |
| 不同用户记忆串号 | 过滤条件缺失 | 检查检索 SQL 的 user_id | 强制租户条件,禁止客户端传身份 |
7. 最佳实践与可复用清单
7.1 记忆系统设计原则
第一,写入异步、读取同步。记忆提取和 Embedding 生成依赖外部 LLM,延迟高且可能失败,不能放在用户请求的主链路上。用户请求只依赖检索读取,写入全部异步化。第二,检索必须走“窄召回 + 精排”。先按租户和类型过滤,再向量召回,再重排,最后限制返回条数。不要在大表上做全量相似度扫描。第三,记忆块必须有来源标注。没有来源的记忆在审计和纠错时无法追溯。
不要在高频请求里重复生成同一段文本的向量。对 Embedding 结果做本地缓存可以显著降低成本,但缓存 Key 必须包含用户维度和模型版本,否则模型升级后新旧向量空间不一致,召回质量会突然下降。
7.2 学习环境与生产环境的差异对照
本地跑通最小系统只是起点,生产环境还需要补齐很多环节。
| 维度 | 学习环境 | 生产环境 |
|---|---|---|
| 数据库 | 单节点 pgvector | 主从复制、定期备份、慢查询监控 |
| 向量索引 | 默认无索引 | 按数据量建立 ivfflat 或 hnsw |
| 记忆提取 | 同步调用 | 消息队列异步执行、失败重试 |
| 鉴权 | Testing 环境直接传 user_id | 从登录态解析身份、服务间鉴权 |
| 监控 | 无 | 调用链日志、召回指标、写入成功率告警 |
| 清理 | 手动清表 | 定时任务做过期清理和分数衰减 |
如果项目早期就并行输出两套方案,改造成本会小很多。把表结构调整为带 tenant_id 的通用模型,把提取操作放进异步任务,这是从开发走向上线前最值得先做的两件事。
7.3 上线前检查清单
记忆写入检查 [ ] 提取任务是否在用户请求主链路之外 [ ] 提取失败是否会重试并记录日志 [ ] 写入是否做了重复记忆去重 [ ] 每条记忆是否包含来源会话和时间字段 记忆检索检查 [ ] 检索 SQL 是否强制过滤 tenant_id / agent_id / user_id [ ] 写入和检索是否使用同一 Embedding 模型 [ ] 是否设置了召回条数和 token 上限 [ ] 重排是否考虑重要度和时效 数据与安全检查 [ ] 是否支持用户查询和删除自己的记忆 [ ] 是否有审计日志 [ ] 数据库是否有备份和恢复演练 [ ] 是否有过期记忆清理任务 验证检查 [ ] 是否跑通过至少 4 组跨会话验证场景 [ ] 是否打印并检查过组装后的完整 Prompt [ ] 是否验证过多租户隔离不串号7.4 扩展方向:知识库、工作流编排与多语言生态
长期记忆系统稳定之后,可以沿着三个方向继续扩展。
第一个方向是接入外部知识库。很多团队会把企业文档、个人笔记导入知识库,再通过 RAG 提供给 Agent 查询。这与长期记忆系统天然互补:记忆保存的是个性化、动态变化的信息,知识库保存的是相对稳定的领域知识。像 Obsidian 这类本地知识库与 Agent 结合时,就可以把笔记内容同步到同一个向量库,实现“个人知识 + 对话记忆”的统一检索。
第二个方向是与工作流编排平台集成。企业级部署往往不是只有一个 Agent,而是多个 Agent 配合工作流执行。n8n 这类支持企业级部署的编排工具,可以把“会话消息 → 记忆提取 → 记忆存储 → 下游任务”串成可视化流水线,方便非后端团队维护触发条件。
第三个方向是多语言生态。当前示例是 Python 技术栈,但很多企业在 Java 体系中。Java 技术栈可以参考 Spring AI 的客户端能力,把长期记忆检索封装成一个组件,供不同的 Agent 客户端复用。无论是哪种语言,记忆系统本身的接口设计思想是一致的:写入异步、读取同步、租户隔离、检索可控。
如果正在准备与 AI Agent 相关的技术面试,记忆系统是一个高频考察点。面试官通常会问:上下文窗口有限时如何控制成本与质量、跨会话记忆如何设计、向量检索与关键词检索如何结合、记忆与知识库有什么区别。能徒手画出记忆分层架构,并能解释写入、检索、遗忘和隔离这四个链路,通常就能把这一类问题讲清楚。
真正值得优先投入精力的,不是无限扩大上下文窗口,而是建立一套“知道该记住什么、该忘掉什么、该在什么时候回忆起什么”的管理机制。先把最小闭环跑通,再补齐异步化、多租户隔离、遗忘机制和监控告警,这套系统就能从“能跑”走向“能上线”。