1. 为什么AI Agent必须拥有记忆系统1.1 大模型的“无状态”本质聊完就忘才是常态如果你亲手搭过一个Agent大概率经历过这个画面用户第一次对话时Agent精准地回答了问题还记住了用户说自己养了一只叫“煤球”的猫。等过了半小时用户回头说了一句“煤球今天又不吃饭了”Agent一脸茫然反问“煤球是谁”。这时候用户不会觉得是技术限制只会觉得你的产品很蠢。这个问题的根子不在Agent框架而在底层LLM本身。大模型本质上是一个“无状态”的函数输入一段文本输出一段文本。它没有内部寄存器去保存“刚才谁跟我说过什么”每一次调用都是新的开始。你传入的Prompt里没有“煤球”这个信息它就不可能知道煤球是谁。我见过不少刚入门的人在这个地方绕了很久老是想着“换一个大模型是不是就记住了”。换个参数量更大的模型也一样除非你主动把聊天历史、用户画像、业务上下文塞进Prompt里否则它对上一轮对话的记忆为零。这不是某个模型品牌的短板而是当前这代Transformer架构的普遍特性。所以“给Agent装记忆”这件事本质上就是我们在LLM外部搭建一套存储和检索机制把需要记住的信息保存下来在下一次调用时重新拼装进上下文里。理解了这一点后面所有的设计才有了方向Memory不是模型自带的能力而是工程上补出来的外挂。1.2 Memory在Agent架构中的真实定位决策依据不只是存储很多人一听到“记忆系统”第一反应就是“搞个数据库存聊天记录”。这么理解没有错但只对了一半。我在实际项目里更愿意把Memory看作Agent的“决策依据的一部分”而不是一个被动的仓库。举个例子Agent接到了一个任务“帮我写一封邮件给供应商催一下发货”。如果它没有任何记忆它只能根据当前这一句话瞎猜供应商是谁、发了什么货、之前聊到哪一步。但如果它有记忆它能把近期订单信息、沟通记录、甚至用户对供应商的态度偏好都翻出来这封邮件写出来才像“这个人”写的。这里要顺带把Agent和LLM的关系说清楚因为热词里很多人问“agent和llm和AI模型有什么区别”。我习惯用一个比喻LLM是大脑皮层负责思考、推理、生成语言Agent是整个人除了大脑皮层还有眼睛、耳朵、手和记忆。AI模型是广义的能力底座LLM只是其中一类的模型而Agent是使用这些模型去完成完整任务的系统。DeepSeek这类产品你看着是个聊天机器人但给它接上工具和记忆之后它就是Agent的载体。Memory处于这个系统的什么位置它接近人类“过往经验”的位置。Agent每次接到新任务先做的事就是检索记忆把相关的历史信息拉出来作为当前决策的上下文。没有这一环Agent只能活在当下永远无法积累也无法形成“越用越懂你”的效果。1.3 没有记忆的Agent会踩哪些坑我梳理了一下至少这几类问题是必踩的第一多轮对话断裂。用户问了A问题追问B问题时里面用了“它”“那个方案”这类指代词Agent没有前文根本解不开。第二用户画像无法沉淀。用户的偏好、身份、历史诉求每次都靠重复输入产品体验非常生硬。第三任务上下文丢失。Agent执行一个多步骤任务时如果中途需要等外部接口返回等回来发现已经忘了自己刚才执行到哪一步。第四知识无法积累。每次回答都从零开始思考不会借鉴过去已经确认过的答案效率低而且容易前后矛盾。这些问题不是偶发性的只要有真实用户在用对话轮数一上去必然会暴露。所以构建Memory系统不是一个“锦上添花”的优化项而是Agent产品从Demo走向可用之间的必经之路。2. 拆解Agent记忆的分类与数据流转2.1 工作记忆、长期记忆、语义记忆边界怎么划不同的资料里对Agent记忆的分类表述不太一样我用了一套在实践中比较顺手的划分方式工作记忆、长期记忆、语义记忆。工作记忆对应的是当前会话内的信息相当于你在跟人聊天时脑子里临时记着的“刚才他说了什么”。在系统里它通常表现为最近的几轮对话、当前任务的状态变量。这部分数据生命周期短会话结束或者任务完成就可以丢弃或压缩但它的特点是读取频繁、对延迟敏感。长期记忆对应的是跨会话、跨任务需要保留的信息比如用户的偏好、历史订单、过往决策记录。这部分数据需要落盘存储通常会被持久化到数据库或者文件里按用户、按项目、按主题做索引。语义记忆则是那些抽象出来的“知识”不是具体的某次对话而是从多次交互里提炼出的规则和结论。比如用户在三次对话中都提到喜欢极简风格Agent就可以在语义记忆里存一条“该用户偏好极简设计风格”。它比原始聊天记录更精炼也更适合直接作为后续回答的依据。这三者边界不是绝对的同一个信息可能在不同阶段流动。比如刚才的对话内容对话期间属于工作记忆会话结束后经过筛选和总结重要信息转入长期记忆再经过多轮验证模式稳定的部分沉淀为语义记忆。Memory系统的设计很大程度就是设计这套流转机制。2.2 记忆从写入到读取的完整路径我做记忆系统时习惯先把数据流画清楚再谈具体的编码实现。路径大概是这样的写入侧每一轮对话结束后系统截取当前的用户输入和Agent输出先做一次粗提取把明显值得记住的内容比如用户主动提供的个人信息、明确表达过的偏好抽取出来进入待处理队列。队列里的内容经过去重、清洗、格式化之后写入对应的存储区域短期内容留在工作记忆缓冲区长期内容定期批量写入长期存储。读取侧当Agent收到新消息准备生成回复时会先去记忆系统做一次“召回”根据当前问题抽取关键词从长期存储里检索相关度最高的记忆片段再结合工作记忆里最近的对话上下文拼装成完整的Prompt最终交给LLM生成。这条链路里最容易出问题的节点是“写入策略”和“召回策略”。写入策略太激进存储里全是噪音太保守有用的信息漏掉。召回策略太宽Prompt里塞满无关内容浪费Token还干扰生成质量太窄该想起来的事情又想不起来。这两对矛盾几乎贯穿记忆系统设计的全部。2.3 存储选型别一上来就上重量级武器关于存储用什么我的建议是分阶段演进不要一开始就上完整的向量数据库方案。第一阶段的Demo或者内部工具直接用文件或者关系型数据库就够了。把记忆按用户ID和会话ID组织成记录存JSON字段读取时按ID拉出来。这个阶段的核心目标是验证记忆的流程和策略不在于检索性能。第二阶段当记忆条数变多、需要做语义召回时再引入向量数据库。这个阶段才有必要对记忆内容做Embedding把文本向量化再通过相似度检索找到相关记忆。此时记忆内容本身要有相对规范的结构否则向量化之后召回的质量会很差。第三阶段如果业务复杂到记忆需要跨系统共享或者多个Agent实例需要访问同一份记忆再考虑独立部署记忆服务做读写接口、缓存、权限控制。我见过不少团队一上来就搭了一套“MemGPT方案向量库消息队列”的豪华架构结果业务根本没到那个量级反而被基础设施的复杂度拖住了。工具是服务于业务的先把流程跑通再逐步升级这条路走起来舒服得多。3. 上下文管理短期记忆的核心战场3.1 Token天窗与信息衰减为什么要管上下文前面说了工作记忆的核心是“当前会话的信息”而承载这些信息的物理通道就是上下文窗口。大模型每次能接收的输入Token是有限的即使像一些商业模型把窗口开到很大也架不住对话无限增长。你不可能把全部聊天历史永远塞进去。除了窗口上限还有一个更隐蔽的问题信息衰减。模型对Prompt中间部分内容的注意力通常弱于开头和结尾这是目前很多模型普遍存在的倾向。当对话历史特别长的时候哪怕Token没超限模型也容易“忘记”中间讨论过的关键细节。这个现象在长对话场景尤其明显。所以上下文管理的本质是在有限的Token预算里把最有价值的信息放到最合适的位置。它不是一句“把聊天记录截断”就能解决的而是需要一套取舍策略。我建议团队在开始写代码之前先做一个简单的Token估算工具。中文场景下粗略估算可以按“一个汉字约等于1到1.5个Token1个英文单词约等于1.3个Token”来算更精确的办法是直接调用模型服务商提供的Tokenizer库。没有这个估算工具后面所有上下文策略都是盲调。3.2 四种主流上下文管理策略及对比我实践下来上下文的裁剪策略大概可以归成四种每种都有它合适的场景。第一种是简单截断保留最近N轮对话更早的一律丢弃。实现成本最低但缺点也很明显如果关键信息出现在前面被截掉的部分Agent就彻底丢失了那个上下文。只适合对话简短、信息独立的应用。第二种是滑动窗口窗口大小固定随着对话推进依次滑动。它比简单截断好在能持续保留最近的信息但仍然处理不了“需要回顾很久以前某个信息”的场景。适合客服对话、聊天机器人这类对近期信息依赖度高的场景。第三种是摘要压缩当对话超过一定阈值时把较早的对话内容用LLM生成一份摘要用摘要替代原始记录参与后续上下文。代价是每次压缩要消耗一些Token和时间而且摘要会丢失细节。但对于长对话来说这是性价比最高的办法。第四种是关键信息抽取从历史对话中抽取结构化的关键信息用户姓名、偏好、决策点、任务状态持续维护一份状态清单每次只把状态清单和最近几轮原文一起送入上下文。它保真度高适合任务型Agent但需要设计好信息抽取的Prompt。我的做法通常是组合使用滑动窗口保底保证最近对话的完整摘要压缩兜底在窗口过长时启动关键信息抽取贯穿始终维护一份持久的结构化状态。这样三种手段互补覆盖率比单一策略高不少。3.3 上下文构建顺序把什么放在离LLM最近的位置上下文管理不只是“决定保留什么”还包括“决定按什么顺序组织”。同样一批内容顺序不同模型输出效果会有肉眼可见的差异。我当前使用的构建顺序是系统提示词System Prompt在最前声明Agent的角色、能力边界和回复风格然后是结构化记忆区包括用户画像、关键偏好、历史决策点这里放的是从长期记忆和关键信息抽取中得到的结论性内容接下来是最近对话记录按时间顺序排列这部分是实时的语境最后是当前用户输入和任务指令放在最末尾紧邻生成位置。这样安排的原因很直接系统提示词是最稳定的指令必须固化在最前面结构化记忆为Agent提供了“你是谁、你在跟谁说话”的背景对话记录提供了连续的语境当前输入放在最末端是因为生成质量对末尾内容的注意力最高要确保模型清楚此刻需要回应的具体问题。有个细节我需要提醒不要把结构化记忆和对话原文混在一个区块里。分开组织更好否则Agent区分不了“这是已知结论”还是“这是某次说过的话”容易导致事实混淆。这就像你跟同事配合工作他脑子里有一段“项目背景资料”和一句“昨天会上说的原话”这是两种信息不该混为一谈。4. 从零搭建一个可用的Memory系统上篇实现4.1 明确功能边界这一版我们做到什么程度在动手写代码之前我习惯先把这一版的边界划清楚。这篇是上篇重点验证“记忆系统的核心流程”所以我不打算引入向量数据库也不做复杂的异步任务队列。目标是实现一个具备以下能力的最小版本第一能够保存会话内的工作记忆即最近N轮对话。第二能够从对话中抽取关键信息形成结构化的用户画像字段。第三当对话超过阈值时能触发摘要压缩把早期对话压缩成摘要。第四对外提供统一的读写接口便于后续替换底层存储。这个版本的数据库我用SQLite配合JSON字段存储结构化记忆。选择SQLite的原因很朴素零部署成本单文件Python自带支持完全够支撑基础场景。后期迁移到PostgreSQL或者MongoDB只是换一层存储实现的问题。另外一个重要的设计决策是把记忆系统封装成一个独立的类不跟具体的LLM调用代码耦合。这样后续无论是替换模型还是给Agent添加工具调用记忆模块都能独立演进。4.2 数据结构与核心代码实现先设计记忆的数据结构。我采用三层组织顶层是Agent实例一个Agent实例对应一套独立的记忆空间中间层是会话每个会话有独立的ID底层是记忆条目分为系统记忆、对话记录、画像数据和摘要记录。下面这个类是记忆核心的操作接口import json import sqlite3 from datetime import datetime from typing import Any, Dict, List, Optional class MemoryStore: 记忆系统的存储层基于SQLite实现最简版本 def __init__(self, db_path: str agent_memory.db): self.conn sqlite3.connect(db_path) self._init_tables() def _init_tables(self): cursor self.conn.cursor() cursor.execute( CREATE TABLE IF NOT EXISTS sessions ( session_id TEXT PRIMARY KEY, agent_id TEXT NOT NULL, profile_json TEXT DEFAULT {}, summary TEXT DEFAULT , created_at TEXT NOT NULL ) ) cursor.execute( CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, role TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL ) ) self.conn.commit() def create_session(self, session_id: str, agent_id: str) - None: cursor self.conn.cursor() cursor.execute( INSERT OR REPLACE INTO sessions (session_id, agent_id, created_at) VALUES (?, ?, ?), (session_id, agent_id, datetime.now().isoformat()), ) self.conn.commit() def add_message(self, session_id: str, role: str, content: str) - None: cursor self.conn.cursor() cursor.execute( INSERT INTO messages (session_id, role, content, created_at) VALUES (?, ?, ?, ?), (session_id, role, content, datetime.now().isoformat()), ) self.conn.commit() def get_recent_messages( self, session_id: str, limit: int 10 ) - List[Dict[str, str]]: cursor self.conn.cursor() cursor.execute( SELECT role, content FROM messages WHERE session_id ? ORDER BY id DESC LIMIT ? , (session_id, limit), ) rows cursor.fetchall() return [{role: row[0], content: row[1]} for row in reversed(rows)] def get_all_messages(self, session_id: str) - List[Dict[str, str]]: cursor self.conn.cursor() cursor.execute( SELECT role, content FROM messages WHERE session_id ? ORDER BY id ASC , (session_id,), ) rows cursor.fetchall() return [{role: row[0], content: row[1]} for row in rows] def save_profile(self, session_id: str, profile: Dict[str, Any]) - None: cursor self.conn.cursor() cursor.execute( UPDATE sessions SET profile_json ? WHERE session_id ?, (json.dumps(profile, ensure_asciiFalse), session_id), ) self.conn.commit() def get_profile(self, session_id: str) - Dict[str, Any]: cursor self.conn.cursor() cursor.execute( SELECT profile_json FROM sessions WHERE session_id ?, (session_id,) ) row cursor.fetchone() if row and row[0]: return json.loads(row[0]) return {} def save_summary(self, session_id: str, summary: str) - None: cursor self.conn.cursor() cursor.execute( UPDATE sessions SET summary ? WHERE session_id ?, (summary, session_id), ) self.conn.commit() def get_summary(self, session_id: str) - str: cursor self.conn.cursor() cursor.execute( SELECT summary FROM sessions WHERE session_id ?, (session_id,) ) row cursor.fetchone() return row[0] if row and row[0] else 这个存储层的设计要点有几个。第一画像数据和摘要存放在sessions表中跟消息分离这样读取时不需要每次都做全表扫描。第二消息表增加了自增ID排序稳定不会因为时间戳精度问题导致乱序。第三JSON字段存储画像结构灵活后面加字段不用改表结构。4.3 摘要压缩与阈值触发机制摘要压缩是上下文管理的核心动作。我设计的触发条件是“当未压缩的原始消息数超过预设阈值时”说明最近的消息已接近上下文窗口可承载的容量需要启动压缩。为了方便复现我给大模型的调用封装了一个最简的LLM接口from typing import List, Dict class SimpleLLM: 一个极简的LLM调用封装方便演示生产环境请替换为真实SDK调用 def __init__( self, api_key: str , base_url: str , model: str qwen-plus, temperature: float 0.7, ): self.api_key api_key self.base_url base_url self.model model self.temperature temperature def chat(self, messages: List[Dict[str, str]]) - str: 这里的逻辑需要根据你使用的模型服务商调整。 可以把它替换成对OpenAI SDK、DashScope SDK、或任何兼容接口的调用。 返回的是模型生成的文本。 # 示例这里省略真实HTTP调用替换为本地直接返回示意 # 实际开发中通过 requests 或 SDK 发起调用 return 这是一段模型回复的占位文本然后是压缩逻辑的核心函数from typing import Dict, List, Optional # 触发压缩的最大消息条数未压缩状态的原始消息 MAX_MESSAGES_BEFORE_COMPRESS 16 # 压缩后保留原文的最近消息条数 RECENT_KEEP 6 class ContextManager: 短期记忆的管理器负责构建上下文并触发摘要压缩 def __init__(self, memory: MemoryStore, llm: SimpleLLM): self.memory memory self.llm llm def align_messages(self, messages: List[Dict[str, str]]) - int: 估算一段消息的Token数量这里用粗略方式方便演示 total 0 for msg in messages: text msg[content] # 中文字符约1个Token英文单词约1.3个Token粗略估算 cjk_count sum(1 for ch in text if \u4e00 ch \u9fff) ascii_count len(text) - cjk_count total int(cjk_count * 1 ascii_count / 4) return total def maybe_compress(self, session_id: str) - None: 检查是否需要压缩摘要。如果当前未压缩消息数超过阈值 就把最早的若干轮对话拿去生成摘要并合并到已有摘要中。 all_messages self.memory.get_all_messages(session_id) # 提取出不属于压缩摘要内容的原始消息 # 简单方案全都当作原文处理因为摘要单独存字段 if len(all_messages) MAX_MESSAGES_BEFORE_COMPRESS * 2: return # 需要压缩的部分除最近 RECENT_KEEP 条消息之外的内容 compress_part all_messages[: -RECENT_KEEP] compress_text \n.join( [f{m[role]}: {m[content]} for m in compress_part] ) old_summary self.memory.get_summary(session_id) prompt [ { role: system, content: 你是一个对话记忆压缩引擎。请用简洁的要点式中文总结对话中最重要的信息 包括用户偏好、决策、明确的个人信息、待办事项。不要输出无关内容。, }, {role: user, content: f已有摘要{old_summary}\n\n新的对话内容\n{compress_text}}, ] new_summary self.llm.chat(prompt) self.memory.save_summary(session_id, new_summary) # 删除已压缩的原始消息只保留最近RECENT_KEEP条。 # 注意这里演示用生产环境建议用事务和更严格的ID定位删除。 cursor self.memory.conn.cursor() keep_ids [] for msg in self.memory.get_all_messages(session_id): # 获取消息ID需要额外查询这里简化为按时间顺序删除前 N 条 pass # 更稳妥的实现是给messages表增加一个compressed标记而不是物理删除。 实际生产实现建议 1. 给消息表增加 compressed INTEGER DEFAULT 0 字段 2. 压缩完成后把已压缩消息的 compressed 置为 1 3. 读取时 WHERE compressed 0。 这样可以保留审计痕迹避免误删。 def build_context(self, session_id: str, current_input: str) - List[Dict[str, str]]: 构建发送给LLM的完整消息列表 self.maybe_compress(session_id) system_prompt self._build_system_prompt(session_id) recent_messages self.memory.get_recent_messages(session_id, limitRECENT_KEEP) context [{role: system, content: system_prompt}] context.extend(recent_messages) context.append({role: user, content: current_input}) return context def _build_system_prompt(self, session_id: str) - str: profile self.memory.get_profile(session_id) summary self.memory.get_summary(session_id) parts [你是一个AI助手请基于对话记忆和当前输入给出合适回复。] if profile: parts.append(用户画像 json.dumps(profile, ensure_asciiFalse)) if summary: parts.append(历史对话摘要 summary) return \n.join(parts)这段代码里有个容易被忽视的坑压缩之后如果直接物理删除旧消息一旦摘要生成质量不好原始信息就找不回来了。所以我在注释里强调生产环境更稳妥的做法是加一个compressed标记字段软删除而不是物理删除。很多团队上了生产之后才发现删除不可逆的代价有多高。4.4 最小检索实现基于关键词的召回在不引入向量库的情况下为了做一些基础的长期记忆检索我实现了一个最小可行的关键词召回模块。它的思路是把历史消息拆成句子按关键词匹配度打分返回得分最高的若干条。import json import re from typing import List, Dict from memory import MemoryStore def tokenize(text: str) - List[str]: 极简分词中文按字、英文按词实战可按需要替换为jieba等库 # 中文按单个字拆同时提取英文单词 tokens [] for token in re.findall(r[a-zA-Z0-9_]|[\u4e00-\u9fff], text.lower()): if re.match(r[\u4e00-\u9fff], token): # 中文单个字作为一个token可能太细可按二元组做简单增强 tokens.append(token) else: tokens.append(token.lower()) return tokens class KeywordMemoryRetriever: 基于关键词打分的简易记忆召回器 def __init__(self, memory: MemoryStore, top_k: int 3): self.memory memory self.top_k top_k def retrieve(self, session_id: str, query: str) - List[Dict[str, str]]: all_messages self.memory.get_all_messages(session_id) query_tokens set(tokenize(query)) scored [] for msg in all_messages: msg_tokens tokenize(msg[content]) score 0 for qt in query_tokens: if qt in msg_tokens: score 1 if score 0: scored.append({role: msg[role], content: msg[content], score: score}) scored.sort(keylambda x: x[score], reverseTrue) return scored[: self.top_k]这个召回器非常简陋真正生产环境肯定要换向量检索但它的价值在于把“记忆召回”这个环节从流程上打通了。后续接向量库只需要把retrieve方法内部换成Embedding相似度计算对外接口不用变。这就是前期做好接口抽象的好处。5. 运行效果与边界验证5.1 模拟一段长对话看记忆如何生效代码写完后我习惯先跑一段模拟对话验证记忆系统的行为是否符合预期。下面这个流程是一个典型验证脚本# 初始化 store MemoryStore(demo_memory.db) store.create_session(session_001, agent_demo) # 模拟用户连续提供信息 def simulate_user_input(store, user_text): store.add_message(session_001, user, user_text) store.add_message(session_001, assistant, 收到) simulate_user_input(store, 我的名字叫李小明做软件开发的) simulate_user_input(store, 我比较喜欢简洁风格的回答不要太啰嗦) simulate_user_input(store, 我最近在做一个电商项目前端用Vue后端用Python) # 模拟更多对话触发压缩 for i in range(20): simulate_user_input(store, f第{i}次闲聊讨论了一些无关紧要的小事) # 查看画像信息 print(用户画像, store.get_profile(session_001)) # 查看摘要 print(摘要, store.get_summary(session_001)) # 构建上下文 cm ContextManager(store, SimpleLLM()) context cm.build_context(session_001, 我叫什么名字我的技术栈是什么) for msg in context: print(f[{msg[role]}] {msg[content][:50]})跑完这个脚本你会看到早期用户提供的姓名和偏好信息因为已经转成了摘要仍然保留在上下文中近期的无关闲聊则被压缩掉不会占用过多上下文空间。这就是摘要压缩和长期画像配合起来的效果。细心的读者可能会发现上面代码里我并没有抽取画像的步骤。实际项目里画像抽取也是通过LLM做的可以单独设计一个Prompt每一轮对话后判断是否有值得写入画像的信息增量更新profile字段。这个环节我用一个简化的抽取函数示意def extract_profile_with_llm(llm, user_message: str, old_profile: Dict) - Dict: updated dict(old_profile) prompt [ {role: system, content: 你是一个用户画像抽取器。从用户输入中提取关于用户的稳定信息 包括姓名、职业、偏好、目标。只输出JSON没有新信息就返回原对象。}, {role: user, content: f已有画像{json.dumps(old_profile, ensure_asciiFalse)}\n用户输入{user_message}}, ] try: resp llm.chat(prompt) # 实际生产需要对resp做JSON解析这里省略 parsed json.loads(resp) updated.update(parsed) except Exception: pass return updated注意解析LLM输出的JSON一定要做容错我踩过太多次“输出里带了markdown代码块导致json.loads失败”的坑。稳妥做法是先用正则把JSON部分提取出来再交给json.loads。5.2 设计上预留向量检索的扩展点上篇虽然不落地向量库但接口上我直接考虑了后续的演进。前面KeywordMemoryRetriever的retrieve方法就是预留点它返回的结构是List[Dict]元素包含role、content、score向量版本只要保持同样的返回结构上层调用方完全不用改。存储层同样预留了迁移空间。SQLite里的sessions和messages两张表如果后续要换PostgreSQL改动集中在MemoryStore内部如果要把记忆共享给多个Agent实例只需要在会话维度加一层agent_id的过滤或者把存储独立部署为服务。Embedding向量本身可以存在SQLite的BLOB字段里也可以存在独立的向量数据库中。我倾向于后者因为向量检索的性能和算法如HNSW、IVF索引是专门优化的关系型数据库在这块效率差很多。但前期如果数据量小几千条量级用SQLite存向量再暴力计算余弦相似度也不是不能跑只是查询延迟会随着数据量线性上升。设计上还有一种做法双写。同一份文本同时存在关系表里方便按条件过滤也生成向量放向量库做语义召回。两套存储各有分工查询时先走向量召回再回到关系表里捞原始完整内容。这种模式我建议在第二阶段就采用它能明显提升召回准确性。6. 常见问题与避坑实录6.1 高频报错速查表这个表格是我在实际项目中反复遇到的典型问题整理出来给读者参考。问题现象常见原因解决办法Prompt越来越长响应越来越慢、费用越来越高没有做上下文裁剪全量历史堆入引入滑动窗口和摘要压缩摘要里的信息跟原始对话矛盾摘要Prompt没有要求忠实原文在摘要Prompt中增加“不要推断用户未明确表达的信息”约束Token估算与实际超出上限估算公式太粗糙使用模型服务商提供的Tokenizer精确计算用户画像被错误更新比如把玩笑话当真实信息信息抽取策略过于激进设置画像更新置信度门槛多轮验证后再写入摘要生成后原始对话被删想追溯却无从查起物理删除了旧消息改为软删除增加compressed标记多条记忆内容冲突Agent回答自相矛盾没有冲突消解策略为记忆条目增加时间戳以最近一次更新为准冲突时优先采用结构化状态同一记忆被重复写入存储膨胀写入前没有判断去重对新增记忆做哈希或者基于内容相似度判断是否重复召回结果不相关Prompt被噪音污染召回策略过宽设置相关度阈值低于阈值不召回限制最大召回条数除了这些工程问题我还要专门提一类看起来和Agent无关、但实际原理相通的报错“The memory could not be read”这类错误。程序访问了非法内存地址和Agent检索了一条不存在的记忆ID本质上是同一个问题——访问了无效的位置。排查思路也一样先确认目标地址/ID是否存在再确认有没有权限最后检查生命周期管理。Agent记忆系统里如果不断出现记忆键对应不上内容的情况十有八九是上层代码在保存和删除时没做好一致性管理。6.2 记忆污染比没有记忆更可怕很多团队做完第一版记忆系统兴冲冲上线结果发现Agent开始“胡言乱语”。原因往往不是模型变笨了而是记忆被污染了。一个非常典型的场景用户在某次对话中说了一句“我真想把项目整个推倒重来”这明显是情绪化吐槽但如果画像抽取模块把这句话理解成“用户想要重新做项目”写进长期记忆后续Agent每次回复都会带着这个错误前提。这就是坏记忆的连锁反应。规避污染我有几条经验。第一条写入长期记忆的信息必须经过多轮验证一次对话中出现的信息默认只放到短期记忆里连续多次出现或者用户明确确认后才晋升到长期记忆。第二条画像字段必须有来源追踪最好记录每条画像对应的原始对话摘录出现问题时能追溯。第三条提供记忆管理能力给用户或管理员一个查看和删除记忆的入口这在B端场景尤其重要。第四条定期对长期记忆做质量扫描用规则或者模型判断是否存在互相矛盾的条目。6.3 上篇实现里我故意“留白”的部分写到这里我想给读者提个醒上篇这个实现是有意做了一些取舍的有几块内容我没有展开但它们在完整方案里必不可少。第一异步化。示例代码里摘要压缩是同步调用的会阻塞主流程。真实场景下摘要压缩应该在对话响应结束后异步执行不能让用户等摘要生成完才看到回复。第二并发控制。多个请求同时读写同一个session的Memory时要做好锁或者版本控制否则容易出现“最后写入覆盖先前写入”的问题。这跟硬件设计里shared memory的竞争问题是一个思路访问同一块共享资源时冲突处理是绕不开的。第三可观测性。生产环境一定要记录记忆的写入和召回日志否则出了问题根本无从排查只能靠感觉。第四多模态度量。这篇只处理了文本记忆如果Agent要处理图片、音频记忆条目就要支持多模态内容的存储和检索这跟普通软件架构里“给文件增加二进制存储能力”是同一个演进方向。这些内容我计划在“下篇”里系统展开重点会放在向量检索、长期记忆的持久化方案、多Agent之间记忆共享这几个方向上。就我个人体感来说做记忆系统最忌讳的是“急于堆技术”。向量数据库、知识图谱、记忆压缩模型这些东西都很诱人但如果连“什么信息该记、什么信息该忘”这个基本问题都没想清楚叠加再多新技术只会让系统更难维护。先把上篇这套最朴素的流程跑通体会一下记忆在Agent里的完整生命周期再逐步升级这条路我验证过走得稳也走得快。