资讯详情 商用Agent记忆架构设计:从会话记忆到长期记忆的完整落地指南
📅 2026/10/6 17:57:25
说实话这几年我接手过不少商用 Agent 项目最让人头疼的不是模型选型不是 Prompt 调优而是 Agent 的记忆。一个看起来很聪明的 Agent只要会话一长、任务一多就会开始前言不搭后语甚至把用户之前明确说过的话给忘得干干净净。团队里管这叫Agent 健忘症但问题根源其实很清晰你压根没给 Agent 设计一套像样的记忆架构只是把它当成了一个无状态的 API 在调。这篇文章我想把商用 Agent 记忆架构这件事从头到尾讲透。你会在里面看到会话记忆、短期记忆、长期记忆、遗忘机制这四种核心模块的完整设计与代码实现以及我在企业级场景里踩过的坑。适合正在做 Agent 开发、被上下文窗口和记忆问题折磨、想把一个 demo 级 Agent 升级成能稳定商用系统的开发者。内容会尽量贴近实战能直接抄作业也会把每个方案选择背后的理由说清楚。1. 先搞清楚 Agent 为什么会健忘记忆分层设计与架构选型1.1 健忘的本质LLM 本身就没有记忆很多人一开始觉得 Agent 健忘是模型能力不行换个大参数模型就好了。但真相是所有 LLM 本质上都是无状态的函数你输入一段文本它输出一段文本整个过程不产生任何记忆。所谓多轮对话能力不过是你把历史消息拼接到 Prompt 里一起喂进去而已。这里有三道硬约束上下文窗口有限。即便现在很多模型支持 128K 甚至 200K token但商用场景下根本不可能把全部历史塞进去。一个代码检视 Agent 可能要处理整个仓库的变更、几天甚至几周的评审记录10 万 token 都不够看。长上下文会稀释注意力。学术圈老早就发现lost in the middle现象模型对上下文中间部分的信息利用率极低。你把 200 轮历史对话全塞进去模型真正记住的可能只有最后两轮和开头两句。token 成本线性增长。输入 token 是按量计费的每次请求都携带全量历史成本会随会话长度暴涨。我见过一个团队因为这个原因单个会话跑到 80 轮时单次请求成本超过了 10 块钱根本没法商用。所以Agent 健忘不是 bug而是你在用一个有状态的使用方式去调一个无状态的服务架构上就错了。1.2 记忆分层的设计逻辑不学人脑就做不好记忆解决这个问题行业内不约而同地走向了同一个方向分层记忆架构。这个思路其实是在模仿人脑的工作方式。记忆层次存储形态生命周期容量查询方式Agent 对应模块感官记忆原始输入缓存毫秒~秒级极小直接读取请求上下文工作记忆窗口摘要分钟~小时中等滑动窗口短期记忆长期记忆向量库结构化天~月极大检索召回长期记忆遗忘机制淘汰合并更新持续执行控制膨胀异步清理记忆治理在商用 Agent 系统里我通常把这套分三层来落地会话记忆层保存当前会话的原始消息但这层不是无限增长的需要配合窗口策略。短期记忆层基于近期会话生成的结构化摘要、关键事实、临时工作状态目的是让 Agent 在当前任务上下文里记得住正在发生什么。长期记忆层跨会话存续的用户画像、业务偏好、历史决策、领域知识让 Agent 在新会话里仍然认识这个用户。每一层解决不同问题也遵循不同的淘汰策略。很多团队只做了一层——把所有消息都塞进列表里——然后出了各种奇奇怪怪的问题就是因为没有分层。1.3 商用场景下的真实架构参考企业级 Agent 是怎么做的我去年参与的一个企业级代码检视修复 Agent 项目类似华为云码道检视修复智能体那种形态给了我很大启发。这类系统的核心工作流是拉取代码变更 → 扫描缺陷 → 生成修复建议 → 人工确认 → 修复验证。如果 Agent 每个步骤都把整个仓库的代码、历史提交、评审记录一股脑塞进上下文光是 token 成本就撑不住。华为云那个智能体公开的评测数据里缺陷检视修复召回率做到了 91.3%。能做到这个水平单靠 Prompt 和大模型是远远不够的背后一定有一套高效的记忆与检索底座代码文件索引、历史缺陷特征库、修复方案模板库全部通过长期记忆层管理运行时按需检索注入。这也印证了我一直以来的选型观点商用 Agent 的记忆架构核心指标不是你能记住多少而是在几毫秒内找到最相关的那部分信息并把它以最小的 token 成本注入给模型。选型时我通常盯住五个指标检索召回率、准确率、P95 延迟、存储成本、可解释性。后面所有代码实现都是围绕这几个指标来取舍的。2. 会话记忆与短期记忆的代码落地先用一个类撑住 80% 场景2.1 基础会话记忆实现所有记忆架构的起点先说最基础的事怎么管理多轮对话消息。很多初学者上来就写一个 list 往里面 append 消息从不做裁剪然后某天突然收到context length exceeded的报错才开始着急。一个可复用的会话记忆类至少要做三件事追加消息、计算 token、按窗口裁剪。我贴一段我在生产环境里还在用的基础实现import tiktoken from typing import Dict, List, Optional class ConversationMemory: 基础会话记忆原始消息列表 token 感知裁剪 def __init__(self, max_tokens: int 4000, model: str gpt-4o): self.messages: List[Dict[str, str]] [] self.max_tokens max_tokens self.encoder tiktoken.encoding_for_model(model) def add_message(self, role: str, content: str) - None: self.messages.append({role: role, content: content}) def _count_tokens(self, messages: List[Dict[str, str]]) - int: return sum(len(self.encoder.encode(m[content])) for m in messages) def get_context(self) - List[Dict[str, str]]: 返回给 LLM 的消息列表自动裁剪到 max_tokens 以内 messages list(self.messages) while self._count_tokens(messages) self.max_tokens and len(messages) 1: # 从头开始丢消息保留最近的对话 messages.pop(0) return messages def reset(self) - None: self.messages []这段代码有几个细节值得讲。token 计算必须用模型对应的编码器千万别用len(content)估字符数中文场景下字符数和 token 数差距非常大。裁剪策略是丢头部保尾部因为最近的对话往往最关键但代价是早期信息永久丢失。这其实就是最短的捷径就是绕远路——基础版能顶住 demo但撑不起商用需求。2.2 短期记忆的三种策略窗口、摘要、关键信息提取当基础裁剪不够用时就得引入短期记忆的三板斧了。我会根据业务场景选其中一种或组合使用。滑动窗口策略最无脑只保留最近 N 轮对话。优点是零成本、零延迟缺点是一旦超出窗口早期信息直接蒸发。适合角色单一、任务独立性强的客服机器人。摘要压缩策略当消息即将被挤出窗口时调用一次 LLM 把窗口内的内容浓缩成摘要后续用摘要较新的原始消息一起作为上下文。优点是基于早期重要信息不会丢缺点是多了一次 LLM 调用有延迟和成本。关键信息提取策略每一轮对话结束后让 LLM 从对话中抽取结构化的事实、偏好、待办等存成 JSON。这条路径最接近记忆的本质——不记流水账只记真正重要的东西。我在真实项目里最常用的组合是滑动窗口 摘要压缩。下面是代码层面的实现。2.3 短期记忆实现窗口裁剪 自动摘要的完整类class ShortTermMemory: 短期记忆层窗口裁剪 LLM 摘要压缩 def __init__(self, llm_client, max_raw_tokens: int 2000, max_summary_tokens: int 800): self.llm_client llm_client # 需要实现 chat(messages) - str self.max_raw_tokens max_raw_tokens self.max_summary_tokens max_summary_tokens self.conv ConversationMemory(max_tokensmax_raw_tokens) self.summary: Optional[str] None # 已压缩的历史摘要 async def add_message(self, role: str, content: str) - None: self.conv.add_message(role, content) # 超窗就触发压缩 if len(self.conv.messages) 1 and self.conv._count_tokens(self.conv.get_context()) self.max_raw_tokens: await self._compress() async def _compress(self) - None: old_messages self.conv.messages[:-1] # 保留最后一条 if not old_messages: return history_text \n.join(f{m[role]}: {m[content]} for m in old_messages) summary_prompt ( 请把下面这段对话压缩成摘要保留用户的核心诉求、明确表达过的偏好、 已经确认的结论、尚未完成的事项。用中文输出控制在200字以内。\n\n f{history_text} ) new_summary_text await self.llm_client.chat([{role: user, content: summary_prompt}]) # 和旧摘要合并防止信息丢失 if self.summary: merged_prompt ( f这是之前的摘要{self.summary}\n f这是新一轮对话{new_summary_text}\n 请合并成一份新的摘要去除重复内容保留所有关键信息。 ) self.summary await self.llm_client.chat([{role: user, content: merged_prompt}]) else: self.summary new_summary_text # 重置原始消息只留最后一条作为当前轮 last_msg self.conv.messages[-1] self.conv.reset() self.conv.add_message(last_msg[role], last_msg[content]) def get_context(self) - List[Dict[str, str]]: context [] if self.summary: context.append({role: system, content: f[历史摘要] {self.summary}}) context.extend(self.conv.get_context()) return context这里有个细节同一时刻只保留最后一轮原始消息其他全部压缩进摘要。我这样做是为了防止摘要和原始消息重复占用 token。实际效果是一段 50 轮的对话被压缩到 800 token 左右的摘要Agent 对该用户的主要诉求依然一清二楚。2.4 短期记忆踩坑实录摘要丢失信息的教训我第一次做压缩策略时犯过一个错误每次都独立生成摘要不做新旧合并。结果就是 20 轮以后摘要里只剩最近几轮的结论用户 5 轮前明确说的预算控制在三万以内这种关键约束丢了。后来改成上面代码里的旧摘要新对话合并模式才彻底解决。另一个坑是不要在每条消息都触发压缩。我一开始在add_message里判断超窗就压缩结果用户连续发三条短消息就触发三次压缩白白烧了三次 LLM 调用。后来加了阈值缓冲只有当窗口 token 超过max_raw_tokens的 90% 时才触发并且压缩后窗口从 0 开始累计天然就留了冗余。3. 长期记忆的持久化与检索实战向量库结构化双轨方案3.1 长期记忆到底该存什么别什么都往向量库里塞如果说短期记忆管的是正在发生的事长期记忆管的就是已经确定的事。在设计长期记忆层之前你得先想清楚业务里哪些信息值得跨会话保留。以典型的商用 Agent 为例我把长期记忆分成两个轨道类型典型内容存储方案查询方式结构化事实用户ID、行业、预算、审批偏好、禁用词、上次结论SQLite/Redis/MySQL精确条件查询非结构化经验历史对话片段、历史缺陷描述、修复方案、用户评论向量库语义相似度检索有一个常见的误区把用户ID、预算这类结构化字段也塞进向量库。向量检索是近似匹配同一用户的两次查询向量距离可能不是最近的那个结果就是召回结果不稳定。结构化的东西就该用结构化存储搞精确匹配只有无法用离散字段描述的内容才应该走向量检索。3.2 向量检索实现从写入到召回的完整链路向量库负责的是语义记忆。我演示一下用 Qdrant 作为向量库的实现这套代码同样适用于 Milvus、Chroma 这类产品只是客户端 API 略有差别。from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams, PointStruct import uuid class VectorMemoryStore: 基于 Qdrant 的长期记忆向量存储 def __init__(self, embed_func, collection: str agent_memory, top_k: int 5): self.client QdrantClient(hostlocalhost, port6333) self.collection collection self.embed_func embed_func # 文本 - 向量 的函数 self.top_k top_k # 确保 collection 存在 try: self.client.get_collection(collection) except Exception: self.client.create_collection( collection_namecollection, vectors_configVectorParams(size1024, distanceDistance.COSINE) ) def add_memory(self, user_id: str, session_id: str, content: str, importance: float 1.0, ttl_seconds: int | None None) - str: 写入一条长期记忆 memory_id str(uuid.uuid4()) vector self.embed_func(content) payload { user_id: user_id, session_id: session_id, content: content, importance: importance, created_at: datetime.now().isoformat(), ttl_seconds: ttl_seconds, } self.client.upsert( collection_nameself.collection, points[PointStruct(idmemory_id, vectorvector, payloadpayload)] ) return memory_id def query_memory(self, user_id: str, query_text: str) - List[dict]: 召回最相关的记忆先按 user_id 过滤再向量检索 query_vector self.embed_func(query_text) hits self.client.search( collection_nameself.collection, query_vectorquery_vector, query_filtermodels.Filter( must[models.FieldCondition(keyuser_id, matchmodels.MatchValue(valueuser_id))] ), limitself.top_k ) return [hit.payload for hit in hits]关键点在于query_memory里的query_filter。先按 user_id 精确过滤再做向量检索这一步能把召回准确率提升到很可观的水平。为什么因为向量库在百万级数据上做全量检索容易召回到其他用户高度相似但语义不同的记忆业务上这就是张冠李戴。3.3 结构化记忆实现几个人类的记忆存进 SQLite结构化记忆用 SQLite 就够了商用数据量大了可以平滑迁移到 PostgreSQL。我常用的表结构长这样import sqlite3 from datetime import datetime, timedelta class StructuredMemoryStore: 长期记忆中的结构化部分 def __init__(self, db_path: str memory.db): self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.execute( CREATE TABLE IF NOT EXISTS user_facts ( user_id TEXT, fact_key TEXT, -- 例如 budget_limit, preferred_language fact_value TEXT, updated_at TEXT, source_session TEXT, PRIMARY KEY (user_id, fact_key) ) ) self.conn.execute( CREATE TABLE IF NOT EXISTS event_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id TEXT, event_type TEXT, raw_content TEXT, created_at TEXT ) ) self.conn.commit() def set_fact(self, user_id: str, key: str, value: str, source_session: str) - None: 写入/更新一个用户事实 self.conn.execute( INSERT OR REPLACE INTO user_facts (user_id, fact_key, fact_value, updated_at, source_session) VALUES (?, ?, ?, ?, ?), (user_id, key, value, datetime.now().isoformat(), source_session) ) self.conn.commit() def get_fact(self, user_id: str, key: str) - str | None: 精确读取用户事实 row self.conn.execute( SELECT fact_value FROM user_facts WHERE user_id? AND fact_key?, (user_id, key) ).fetchone() return row[0] if row else None这里有个设计心得user_facts 表用INSERT OR REPLACE天然实现了最新值覆盖旧值。很多 Agent 系统短期记忆和长期记忆联动的核心逻辑其实就是从这个表开始的。3.4 长短期记忆联动从短期记忆固化到长期记忆长期记忆不会凭空产生它的来源是短期记忆但并不是每一条短期记忆都值得长期保留。我设计了一个记忆固化环节触发条件通常是这三条之一用户显式表达了长期偏好以后都用简洁风格回复我LLM 判定信息重要度超过阈值importance ≥ 0.8同一信息在多个会话中重复出现至少 2 次async def consolidate_to_long_term(session_context: dict, short_mem: ShortTermMemory, vector_store: VectorMemoryStore, struct_store: StructuredMemoryStore, llm_client) - None: 把短期摘要里的关键信息固化到长期记忆 user_id session_context[user_id] session_id session_context[session_id] # 让 LLM 从摘要中抽取候选记忆 summary_text short_mem.summary or if not summary_text: return extraction_prompt f 从下面的对话摘要中抽取值得长期记住的信息用 JSON 数组输出每项包含 - field: 字段名budget/user_style/deadline/decision 等 - value: 字段值 - importance: 0~1 的重要度 - reason: 为什么值得记住 对话摘要 {summary_text} resp await llm_client.chat([{role: user, content: extraction_prompt}]) candidates parse_json(resp) for item in candidates: if item[importance] 0.7: # 结构化字段进 SQLite长文本进向量库 if len(item[value]) 100: struct_store.set_fact(user_id, item[field], item[value], session_id) else: vector_store.add_memory(user_id, session_id, f{item[field]}: {item[value]}, importanceitem[importance])这套联动的本质是把短期记忆的摘要当作长期记忆的候选池只让高价值信息沉淀下来。否则长期记忆库会迅速被垃圾信息填满检索时召回质量越来越差。这也是我下一节要讲遗忘机制的前置条件。4. 遗忘机制与记忆淘汰策略让 Agent 学会有选择地记住4.1 为什么遗忘是记忆系统里的必备机制很多开发者对遗忘有误解觉得只要存储空间够大就不需要遗忘。实际完全相反遗忘机制不是为了省空间而是为了维护记忆质量。长期记忆库里的数据会随时间贬值。用户的短期偏好可能一个月后就变了代码评审的标准可能随团队规范调整而失效一个历史缺陷的修复方案也可能在下个版本里被全新方案取代。如果不做遗忘过时记忆和当前事实产生冲突Agent 就会被旧记忆幻觉带偏——它明明检索到了东西但不是用户现在要的结果回复质量比没有记忆还差。另外遗忘机制还直接影响检索性能。向量库不是不设上限就能无限跑的数据量从 10 万涨到 100 万单次检索延迟可能从 20ms 涨到 150ms。商用 Agent 对 P95 延迟很敏感主动淘汰掉低质量记忆是控制延迟的必要手段。4.2 遗忘策略设计TTL、LRU、重要性加权工程上我常用的遗忘策略是下面几种组合各有适用场景策略核心思路适用场景实现成本TTL 过期每条记忆带有效期到期自动删除临时事实、限时任务、活动信息低LRU 淘汰长期未访问的记忆优先删除通用兜底策略低重要性加权按 importance 排序低重要度先删垃圾信息过滤中业务失效因业务规则变化主动作废代码规范、策略版本切换中记忆合并多条相似短期记忆合并为一条长期摘要降低重复冗余高我一般按双机制来设计TTL 负责过期信息LRU重要性负责容量上限。这两者存在一个平衡重要度高的记忆 TTL 长、淘汰优先级低重要度低的则相反。4.3 遗忘机制的代码实现定时清理 记忆合并来看一个具体的遗忘模块实现。核心是定期扫描向量库和 SQLite删除过期记忆并对相似记忆做合并。下面是向量库的清理逻辑from qdrant_client import models from datetime import datetime, timedelta class ForgettingScheduler: 记忆遗忘管理器TTL 过期清理 LRU 淘汰 记忆合并 def __init__(self, vector_store: VectorMemoryStore, struct_store: StructuredMemoryStore, max_vectors: int 50_000): self.vector_store vector_store self.struct_store struct_store self.max_vectors max_vectors def run_cleanup(self) - dict: 执行一轮遗忘清理返回清理统计 stats {ttl_deleted: 0, lru_deleted: 0, merged: 0} # 1. TTL 过期清理 cutoff datetime.now().isoformat() # 这里只做演示实际请用 Qdrant 的 scroll filter 实现 all_points self.vector_store.client.scroll( collection_nameself.vector_store.collection, limit10000 )[0] for point in all_points: ttl point.payload.get(ttl_seconds) created datetime.fromisoformat(point.payload.get(created_at)) if ttl is not None and datetime.now() - created timedelta(secondsttl): self.vector_store.client.delete( collection_nameself.vector_store.collection, points_selectormodels.PointIdsList(points[point.id]) ) stats[ttl_deleted] 1 # 2. LRU 淘汰基于 last_access 字段 current_count self.vector_store.client.count(self.vector_store.collection).count if current_count self.max_vectors: # 按 last_access 倒序删除最旧的一批 # 演示代码从简实际用 Qdrant scroll sort 实现 to_delete [] points self.vector_store.client.scroll( collection_nameself.vector_store.collection, limitcurrent_count - self.max_vectors, order_bymodels.OrderBy(keylast_access, directionmodels.Direction.ASC) )[0] for p in points: to_delete.append(p.id) self.vector_store.client.delete( collection_nameself.vector_store.collection, points_selectormodels.PointIdsList(pointsto_delete) ) stats[lru_deleted] len(to_delete) return stats注意一个工程细节遗忘清理绝对不能和用户请求在同一条链路里同步执行。我见过有人把清理逻辑写在检索的前置链路里结果线上一跑P95 延迟从 30ms 飙到 800ms。正确的做法是异步定时任务比如每 5 分钟跑一次或者每天凌晨做一次深度清理。4.4 记忆合并与更新遗忘不只是删也是改除了直接删除遗忘机制里还有一种更优雅的形态——记忆合并和更新。举个例子用户第一次说我用 Python第二次说我最近切到 Go 了。如果只是新增一条记忆库里就会出现两条冲突的记录如果做合并就应该把Python那条标记为过期保留Go这一条。为这我专门做了一个冲突检测模块在处理新记忆写入时def resolve_conflicting_memory(user_fact_key, new_value, old_value, new_importance, old_importance): 新旧记忆冲突解决规则 # 如果新信息重要度更高或者明确是更新操作以新信息为准 if new_importance old_importance: return new_value, replace # 如果新旧信息不冲突只是补充则保留两条可以走合并 return new_value, merge这个逻辑看着简单但它解决了一个很实际的问题Agent 的长期记忆必须跟着用户的变化而变化否则用户会明显感觉这个 Agent 还在用我三个月前的旧信息这种体验比没记忆还糟糕。4.5 隐私与合规红线遗忘是用户的权利最后强调一个容易在工程文档之外被忽略的硬性要求商用 Agent 必须提供记忆删除接口。现在隐私合规要求越来越严用户有权利要求系统删除自己的历史数据。你可以在长期记忆库里实现一个按user_id硬删除的方法并且在所有会话、事件日志表里同样支持级联清理。这不是可选项而是商用系统的底线。5. 全链路串联与工程化避坑从 Demo 到商用的关键细节5.1 全链路数据流一次请求是如何被记忆架构托住的到这里四个核心模块都有了我可以把完整链路串起来了。一次带记忆的 Agent 请求处理顺序大致是这样的用户输入到达先读取短期记忆中的摘要和最近消息恢复当前会话上下文。并行触发长期记忆检索结构化部分用精确查询拿到用户画像向量部分做语义召回。把三类信息按优先级组装进 Prompt系统提示词 长期记忆 短期摘要 最近对话。调用 LLM 生成回复同时异步把新消息追加到短期记忆。短期记忆超窗时触发摘要压缩压缩结果进入记忆固化候选池。定时清理任务把过期记忆、低重要度记忆、冲突记忆处理掉。这个链路的关键在于第 2 步的并行化。检索绝对不能串行等——如果先查 SQLite 再查向量库每个请求硬生生多出几十毫秒的延迟。我在项目里用asyncio.gather把两类检索并发跑总延迟基本稳定在新增 40ms 以内用户无感。5.2 并发与一致性多个会话同时写同一份记忆怎么办一个商用 Agent 不可能只有一个用户。当几百个会话同时往长期记忆库写入时你会遇到几个坑第一个是重复写入。同一条信息被多个会话重复固化库里出现大量冗余向量。解决办法是写入前先做一次精确查重比如按user_id field value组合判断。第二个是并发覆盖。两个会话同时更新同一个user_fact后写覆盖先写但语义上可能先写的信息才是正确的。工程上我给user_facts表加了一列updated_at在写入前检查版本如果本地读到的版本旧于库里的最新版本就触发冲突解决逻辑而不是直接覆盖。第三个是检索超时。当向量库数据量超过千万级时单机 Qdrant 的检索延迟开始不稳。暴力解法是分片但更实际的方案是业务层限流把同一个user_id并发检索限制在 5 个以内优先保证在线请求的稳定。这个思路对Agent 怎么扛并发这个问题很有用——别指望一个组件扛所有并发用业务层限流把高并发请求消化到记忆系统能承受的范围内。5.3 冷启动与记忆迁移没有历史记忆的 Agent 怎么干活新用户进来时长期记忆库是空的这时 Agent 就要靠冷启动策略撑住。我的做法是在用户首次会话时注入一套默认画像模板把常见参数设成中性默认值比如默认代码风格、默认回复长度、默认安全边界。然后随着对话推进通过短期记忆固化逐步替换为真实数据。记忆系统上线前还要准备记忆迁移方案。我在项目里做过一次从测试环境迁移到生产环境踩了大坑测试库里的user_id和生产库对不上向量库里的 payload 全部失效。后来我把所有记忆记录统一加了environment字段迁移时先做字段映射再按 environment 过滤复制。这个经验强烈建议你提前做——等数据攒了几十万条再补会非常痛苦。5.4 性能监控与评测怎么证明你的记忆架构真的有用最后聊评测。商用系统不能被感觉 Agent 记忆力变强了这种话术带过去得有量化指标。我落地时盯四个数指标定义怎么测记忆召回率用户问我上个月说过的预算约束Agent 能不能答对构造 50 条历史记忆模拟对话检查回答是否包含关键信息记忆准确率召回的记忆里有多少确实是当前有效的人工标注一批检索结果计算 precisionk任务成功率提升有记忆 vs 无记忆任务成功率的差值A/B 测试同一组 prompt 只切换记忆模块开关遗忘健康度废旧记忆占用比例、检索延迟变化定期对记忆库抽样统计过时时间与活跃度一个简单的自动化评测脚本思路是这样的准备一个test_queries.json每条里包含背景记忆片段和测试问题然后分别跑无记忆 Agent和有记忆 Agent对比回答中是否出现了预期关键事实。这个脚本虽然粗糙但能拦住 90% 的退化问题。我在每个记忆模块改完之后都会跑一遍这个回归测试效果很稳。最后说几句真实的工程体会我在这个项目里踩过的最深的坑是想一口气把整套记忆架构做完美——结果做了两周上线后到处出 bug最后还是推倒重来。后来我学乖了记忆架构是长出来的不是设计出来的。先把会话记忆和基础窗口做好等业务数据和真实对话量积累到一定程度再逐步引入摘要压缩、长期记忆检索、遗忘机制。每一步都跟着实际痛点走方案才能扎得住。最后一个分享的小技巧无论你最终选了什么方案一定要给每条记忆记录带上 trace_id 字段把记忆的写入来源、写入时间、关联会话全埋进来。线上排查问题的时候这个字段能让你少熬好几个夜。毕竟 Agent 记忆这件事出了问题最怕的就是它到底为什么记得这个完全查不到。