ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

大模型记忆系统构建指南:从上下文工程到RAG与本地部署

2026/8/30 12:57:57 拓冰建站 浏览量
大模型记忆系统构建指南:从上下文工程到RAG与本地部署 给大模型做记忆郝建业团队半年内完成三轮融资这条消息让“大模型记忆”这个技术方向从实验室议题变成了应用开发者不得不关注的工程问题。对一线开发者来说融资节奏只是背景真正要回答的是几个很具体的问题大模型为什么记不住事情、记忆应该放在哪里、检索不到时怎么排查、本地部署时怎么接入。下面会先讲清楚大模型记忆缺失的根源再把上下文工程、RAG、微调和记忆框架四条路径对比清楚然后用一个最小可运行的对话记忆系统串联完整流程最后给出本地部署、常见问题排查和生产环境落地时最容易踩的坑。1. 为什么说上下文窗口不是真正的记忆1.1 大模型的无状态特性是记忆问题的根源大模型本质上是无状态函数。无论是调用云端 API 还是本地加载模型每一次请求都是独立计算输入一批 token经过多层 Transformer 解码输出下一个 token。模型不会因为你上一次问过“我喜欢喝美式咖啡”就在下一次回答里自动记住这个偏好。常见的对话产品看起来能连续聊天是因为前端把历史消息全部带上并不是模型自己记住了。这在架构上有个直接后果所谓“记忆”在大模型场景里本质是“对输入的重组”。模型能看到的记忆必须出现在它的上下文窗口中。上下文窗口是模型可以同时处理的 token 上限例如 8K、32K部分模型支持 128K 甚至更大。窗口越大能携带的历史内容越多但窗口不是硬盘更像一块临时工作台东西放得下但工作台一清空就什么都没有。这也是为什么很多人把大模型对话比作“金鱼记忆”。更准确的说法是模型根本不具备长期存储能力开发者必须替它在外部找地方放记忆。1.2 KV Cache 解决的是性能问题不是持久化问题大模型推理时有一个经常被混淆的概念KV Cache。Transformer 解码是逐 token 生成的生成第 N 个 token 时如果重新计算前面 N-1 个 token 的 Key 和 Value 矩阵计算量会随序列长度平方级增长。KV Cache 的做法是把已经算好的 K、V 缓存下来后面生成时直接复用。KV Cache 带来两个工程问题。第一它随上下文变长而占用大量显存长对话推理时KV Cache 可能占据比模型权重还多的显存。第二它是一次请求内部的缓存随请求结束而释放不会跨请求保留。换句话说KV Cache 是“工作记忆”层面的加速手段跟“记住用户上个月的需求”完全是两回事。在大模型部署里KV Cache 的精度和显存分配是绕不开的话题。常见做法是使用 fp16 或 bf16 存 KV进一步还可以做 KV Cache 量化牺牲少量精度换显存和吞吐。fp32 精度更高但显存翻倍适合追求精确结果或调试异常时使用bf16 相比 fp16动态范围更大在训练和推理中更不容易溢出很多新卡的软硬件栈对 bf16 支持更好。选型时不要只盯模型权重精度KV Cache 的精度和显存预留同样要提前计算。1.3 把记忆分成三层再设计工程上可以借用计算机体系结构里“存储分级”的思路把大模型记忆分成三层短期记忆、工作记忆和长期记忆。记忆层次存放位置典型容量读取方式生命周期典型用途短期记忆上下文窗口8K 到 128K token直接拼接单次请求当前对话轮次、临时指令工作记忆KV Cache / 请求内部缓存与上下文长度相关注意力机制自动访问单次生成过程当前回复的连贯性长期记忆向量库、关系库、文件几乎无限检索后拼接跨会话持久化用户偏好、历史事实、领域知识这三层不是互斥的而是配合关系。长期记忆负责“记住”短期记忆负责“当下能用”工作记忆负责“这一句话怎么组”。一个合格的记忆系统设计目标是在长期记忆里快速找到最相关的片段转成短期记忆塞进上下文让模型在生成时像“想起来”一样使用这些信息。三层里短期记忆的问题最简单也最容易踩坑盲目把所有历史都塞进去很快会撞到上下文上限。长期记忆则是工程重点因为要解决存什么、怎么存、怎么找、找错了怎么办。后面几节会逐步深入。2. 主流记忆方案四条技术路线的取舍2.1 上下文工程先用最便宜的手段最简单的一种“给大模型做记忆”是把历史对话或业务信息直接拼到 system prompt 或 messages 里。实现成本最低效果也最直观模型生成时能看到这些内容。但直接拼接会很快遇到瓶颈。假设每轮对话平均 300 token10 轮就是 3000 token100 轮就是 30000 token。长会话场景下把所有内容都塞进去既不经济也不可行。于是有了两种常见优化滑动窗口只保留最近 N 轮对话更早的内容丢弃。适合闲聊类产品用户通常只关心最近的上下文。摘要压缩每过几轮调用模型把历史对话压缩成一段摘要后续用摘要代替原始记录。这样能保留更长时间的信息但摘要过程有信息损失反复压缩还会产生“记忆漂移”。上下文工程适合作为第一版方案因为不引入额外组件调试直观。它的问题在于当历史内容超过窗口时要么丢、要么压缩没有真正的“选择性记忆”。模型无法在需要时精确找到三个月前的某个细节。2.2 RAG长期记忆的主流形态RAG检索增强生成是目前给大模型做长期记忆最主流的工程方案。基本流程是先把记忆文本切块用 Embedding 模型转成向量存入向量数据库需要时把当前问题也转成向量在向量库里做相似度检索取回 top-k 个片段再和问题一起交给大模型生成回答。这个方案的核心价值是“选择性读取”。上下文窗口有限但向量库可以无限增长每次只取与当前问题最相关的几个片段进入上下文既控制了 token 成本又扩大了记忆范围。RAG 的局限也很明显。相似度不等于真正相关两个都聊咖啡的段落可能在向量空间里很近但一个是“用户喜欢美式”一个是“咖啡因代谢速度”。RAG 只负责召回不负责判断“这段记忆是否正确、是否过时、是否该用”。所以生产系统通常要在检索后加重排序rerank、时间过滤、权限过滤等环节。2.3 微调把记忆写进参数但不适合频繁更新微调是另一种思路把知识或行为模式通过训练写进模型权重。相比 RAG参数化记忆的好处是响应时不需要外部检索逻辑更简单缺点是更新成本高、周期长、容易遗忘旧知识而且每次用户偏好变化都要重新训练不现实。所以在记忆场景里微调通常用于两类内容一类是稳定的领域知识比如企业内部的文档风格、固定术语另一类是固定的行为偏好比如客服机器人统一使用温和语气。个人化、频繁变化的记忆不应该放进参数应该放在外部存储里通过检索读取。LoRA 这类参数高效微调技术显著降低了微调成本但本质仍然是“低频更新 相对稳定”的内容适合微调。判断标准可以很简单这个信息多久变一次如果每周都在变不要微调如果半年才变一次可以考虑。2.4 记忆代理与记忆框架的通用思路近两年出现了一批专门做大模型记忆的开源和商业框架例如 MemGPT后改名 Letta、Mem0、Zep 等。它们各自提出了不同的记忆组织方式。MemGPT 的核心思路是把操作系统里的“分层存储 分页”概念引入大模型模型自己决定什么内容放进上下文什么内容转储到外部存储上下文不够时再把需要的内容调回来。这相当于让模型具备“主动管理记忆”的能力。Mem0 的做法更偏记忆生命周期管理从对话中提取记忆支持增加、更新、删除和冲突解决。比如用户先说“我喜欢靠窗的位置”后来又说“靠窗太晒了”框架会识别这是冲突并更新旧记忆而不是同时保留两条矛盾记录。Zep 则把“时间”作为记忆的重要维度融合了图结构不仅记录事实还记录实体之间的关系和最近活跃程度让“上周经常提到的人”比“一年前提过一次的人”更容易被召回。这些框架提醒开发者给大模型做记忆不是单一技术而是一套“提取、存储、检索、更新、遗忘”的完整流程。直接引入框架可以快速起步但落到生产环境还是要理解底层机制才能排查为什么某个记忆没有被召回。四条路径的取舍可以用下面这张表对比方案适用场景优点主要风险更新频率上下文工程短会话、原型验证实现简单、无额外依赖上下文超限、信息丢失每次请求动态拼接RAG长期知识、历史对话可扩展、控制 token召回不准、需要治理可实时写入微调稳定领域知识、固定风格响应快、无需检索更新慢、可能遗忘旧知识低频记忆框架复杂多轮个性化场景管理完整、冲突处理依赖外部服务、黑盒风险持续自动更新3. 从零实现一个最小可用的对话记忆系统3.1 案例目标与流程拆解这一节用代码跑通一个最小闭环一个没有内置记忆的对话模型通过外部记忆系统记住用户偏好并在后续对话中自动使用。流程拆解为四步记忆写入每轮对话结束后把“用户偏好、关键事实”提取出来转成向量存入存储。记忆检索新问题进来时先检索与问题最相关的记忆片段。提示词组装把检索到的记忆拼进 system prompt。对话生成调用大模型完成回答。需要说明的是这里不是唯一实现方式。有些系统直接把整段历史记录向量化不做提取有些系统用摘要代替原始文本。下面的代码用于说明核心链路实际项目要结合自己的数据字段、模型和向量库调整。3.2 环境准备依赖项Python 3.10 或更高版本一个 OpenAI 兼容的模型接口可以是云端服务也可以是本地部署的模型Embedding 模型用于把文本转成向量向量存储可以用 ChromaDB、FAISS、Milvus 等安装示例pip install openai chromadb如果使用 FAISSpip install faiss-cpu安装版本以当前稳定版为准。这里使用 ChromaDB 做演示因为它简单、支持持久化适合学习环境生产环境再根据数据量、高可用要求替换为 Milvus、Qdrant 等。注意本地或云端的大模型接口必须兼容 OpenAI 协议代码才能直接复用。如果使用非兼容协议需要先封装一层适配器。3.3 记忆写入把对话转成可检索的记录首先封装 embedding 调用。用函数抽象好处是后面可以随时替换 embedding 服务import os from openai import OpenAI client OpenAI( api_keyos.getenv(OPENAI_API_KEY, local-key), base_urlos.getenv(OPENAI_BASE_URL, http://localhost:8000/v1), ) def get_embedding(text: str) - list[float]: resp client.embeddings.create( modelos.getenv(EMBEDDING_MODEL, bge-m3), inputtext, ) return resp.data[0].embedding这段代码的关键点是base_url 可以指向任意 OpenAI 兼容服务包括本地 vLLM、Ollama 或云厂商的兼容端点。embedding 模型也可以替换成本地模型只要返回固定维度的向量。然后定义记忆存储这里用 ChromaDB 的持久化客户端import chromadb class MemoryStore: def __init__(self, collection_name: str user_memory): self.client chromadb.PersistentClient(path./memory_db) self.collection self.client.get_or_create_collection(namecollection_name) def add_memory(self, memory_id: str, text: str, user_id: str, metadata: dict | None None): meta {user_id: user_id, text: text} if metadata: meta.update(metadata) self.collection.upsert( ids[memory_id], documents[text], metadatas[meta], )这里有个细节代码里传了 documents 让 Chroma 自动做 embedding又把原始 text 保留到 metadata。为什么要保留原文因为 embedding 向量本身不可读检索命中的是一条向量最终真正要拼接给大模型的是原始文本。建议在 metadata 里带上 user_id 和 timestamp方便做权限过滤和时间衰减。写入记忆时还需要考虑“什么时候写”。常见做法有两种每轮对话结束后把整个会话记录入库。先让大模型做一次抽取只把“值得记住的事实”入库。第二种更接近生产设计因为不是每句话都值得长期记住。下面是一个简单抽取示例def extract_memory(conversation: str) - list[str]: prompt ( 从下面的对话中提取值得长期记住的用户事实 例如偏好、身份、习惯、明确要求。 只输出事实列表每条一行。\n\n f对话\n{conversation} ) resp client.chat.completions.create( modelos.getenv(LLM_MODEL, qwen2.5:7b), messages[{role: user, content: prompt}], temperature0, ) content resp.choices[0].message.content return [line.strip(- ).strip() for line in content.splitlines() if line.strip()]注意这个函数返回的是字符串列表还需要去重、过滤空行。抽取不是银弹抽取模型本身可能漏掉重要信息所以生产系统往往同时保留“原始对话”和“抽取事实”两类记忆检索时按场景选择。3.4 记忆检索召回 top-k 并组装上下文检索的核心是把当前问题向量化然后在向量库里查最近邻def search_memory(store: MemoryStore, query: str, user_id: str, top_k: int 5): results store.collection.query( query_texts[query], n_resultstop_k, where{user_id: user_id}, ) docs results[documents][0] metas results[metadatas][0] return [{text: doc, meta: meta} for doc, meta in zip(docs, metas)]这里的 where 条件是权限隔离的关键只查当前用户自己的记忆。实际项目中user_id 字段可以换成租户 ID、部门 ID 等组合使用。检索之后不能直接全塞进提示词。建议按相关性分数、时间衰减做一次重排并控制最终拼接长度。组装提示词的示例def build_memory_prompt(memories: list[dict]) - str: if not memories: return lines [] for i, m in enumerate(memories, 1): lines.append(f{i}. {m[text]}) return 以下是该用户的长期记忆回答时请参考\n \n.join(lines)3.5 组装完整对话链路把写入、检索和生成串起来def chat_with_memory(user_id: str, user_input: str, store: MemoryStore): memories search_memory(store, user_input, user_id, top_k5) memory_prompt build_memory_prompt(memories) messages [] if memory_prompt: messages.append({role: system, content: memory_prompt}) messages.append({role: user, content: user_input}) resp client.chat.completions.create( modelos.getenv(LLM_MODEL, qwen2.5:7b), messagesmessages, temperature0.7, ) return resp.choices[0].message.content调用示例store MemoryStore() reply1 chat_with_memory(u_001, 我平时喜欢喝美式咖啡不太吃辣。, store) print(reply1) store.add_memory(mem_001, 用户喜欢美式咖啡不吃辣。, u_001, {ts: 1690000000}) reply2 chat_with_memory(u_001, 帮我推荐一杯饮品。, store) print(reply2)运行后第二轮提问因为检索到了“用户喜欢美式咖啡”这条记忆模型更有可能给出符合偏好的推荐。如果没有检索到说明需要检查 embedding 模型、切块粒度或权限过滤条件。真实项目里记忆写入应该自动发生而不是手动调用 add_memory。可以在每轮对话后用 extract_memory 抽取并写入形成闭环。4. 记忆系统里最容易被忽略的五个设计点4.1 存原始对话还是结构化摘要存什么决定了检索质量。原始对话信息完整但噪声大、切块困难一段 2000 字的对话里可能只有一个关键偏好。抽取事实信息密度高、检索命中率好但可能漏记。生产实践常见组合是“原始库 摘要库”。原始库用于追溯摘要库用于召回检索优先命中摘要必要时回看原文。写入时用一层“记忆提取器”把对话转成结构化事实同时保留原始对话 ID 关联。4.2 检索排序相似度之外再加时间衰减和重要性向量相似度只是排序的一个维度。用户三个月前说过“我准备去杭州”和昨天说过“我已经在杭州了”两条都跟“杭州”相关但后者的时效性明显更强。检索排序可以在相似度基础上叠加时间衰减因子例如score sim * exp(-lambda * days)其中 days 是记忆距今的天数lambda 是衰减系数。重要性权重则可以通过记忆被访问频率来调整经常被召回的片段说明确实有用可以略微提高权重。这种做法的代价是引入超参数需要用小规模标注数据或用户反馈调。不要一开始就追求复杂排序先把 top-k 过滤和权限隔离做对。4.3 遗忘与更新记忆不是只增不删很多初版记忆系统只做写入和检索不做更新和删除。时间一长向量库里堆积大量过时或矛盾记录。用户先说喜欢 A后来改成 B系统应该把 A 标记为过时或直接删除否则模型每次都会同时看到两条矛盾记忆。更新策略可以设计为新写入与已有记忆冲突时优先保留新记忆旧记忆标记为 archived。超过一定时间未访问的记忆降权而不是直接删除。用户明确删除某条记忆时必须同步清理向量索引和相关副本。4.4 多用户隔离记忆必须带命名空间这个问题在 Demo 里最容易翻车。如果向量库里的记忆没有 user_id 字段检索时也没有 where 条件用户 A 的偏好会被用户 B 的请求检索出来造成严重的信息泄漏。生产环境的隔离要同时做三层数据层写入时强制带上 user_id、tenant_id 等字段。检索层query 时强制加等值过滤。模型层个性化 prompt 里不输出其他用户信息并通过测试用例验证。4.5 上下文预算长对话如何控制 token记忆召回后可能一个片段有几百 tokentop_k 取 10 条就会超过几千 token。建议在组装提示词前做 token 预算控制def truncate_memories(memories: list[dict], max_tokens: int 1200) - str: parts [] used 0 for m in memories: text m[text] approx len(text) / 2 # 中文场景粗略估算生产环境用真实 tokenizer if used approx max_tokens: break parts.append(text) used approx return \n.join(parts)这里的 len(text)/2 只是中文场景的粗略估算生产环境最好用真实 tokenizer 计算。上下文预算的意义在于检索到的记忆再好也不能挤占当前对话的位置否则模型会把记忆和当前指令混淆。5. 本地部署场景下怎么给大模型接记忆5.1 Ollama 部署本地模型时的记忆接入Ollama 让本地跑模型变得很简单。通过 OpenAI 兼容接口前面写的代码几乎不用改ollama pull qwen2.5:7b ollama serve然后设置环境变量export OPENAI_BASE_URLhttp://localhost:11434/v1 export OPENAI_API_KEYollama export LLM_MODELqwen2.5:7b这样 client 就会请求 Ollama。embedding 也可以使用 Ollama 提供的 embedding 接口或者换成本地 sentence-transformers。本地部署记忆系统的注意力要放在“检索质量”上。本地小模型的 embedding 能力通常比云端大模型弱同一句话和同一段记忆的相似度可能偏低top_k 需要调大或者用更强的本地 rerank 模型做第二遍排序。5.2 vLLM 部署时的显存与批处理考虑vLLM 是大模型推理服务里常用的框架支持 PagedAttention、连续批处理吞吐表现好。给 vLLM 接记忆主要看两个参数max-model-len决定模型能接收的最大序列长度。记忆拼接后的整体 prompt 不能超过这个值。gpu-memory-utilizationKV Cache 的显存占比