大模型没有记忆多轮对话怎么做到不失忆?
摘要:大语言模型(LLM)本质上是一个“无状态”的概率预测函数,每一次 API 调用完成后,模型就会彻底清空上下文,不保留任何记忆。然而在 ChatGPT、Claude 或 DeepSeek 中,我们却能与 AI 进行数十轮流畅的对话,AI 还能清晰记得第一轮提到的个人偏好。
AI 是如何做到“不失忆”的?答案是:大模型本身没有记忆,是外部软件工程架构赋予了它记忆的“幻觉”。
本文将从大模型的“无状态”本质切入,系统拆解多轮对话记忆的五大工程演进范式(全量拼接、滑动窗口、摘要压缩、向量检索记忆、MemGPT/Letta 分层存储架构),结合 Prompt Caching 优化与意图重写机制,并提供一套生产级的 Python 对话记忆管理器代码实战,帮助开发者彻底攻克多轮对话中的上下文管理难题。
前言:一个充满戏剧性的工程“假象”
在刚接触大模型开发时,许多开发者都会被一个现象所“欺骗”:
你在 Web 界面上对 AI 说:“我叫小明,是一名 Python 工程师。”
过了 20 轮对话后,你问 AI:“我叫什么名字?我擅长什么语言?”
AI 毫不犹豫地回答:“你叫小明,擅长 Python。”
这看起来就像 AI 拥有一个能够持续学习和记录的“大脑”。然而,从底层技术事实来看,大语言模型(LLM)本身是绝对“无状态(Stateless)”的。
当请求结束、Token 生成完毕的那一瞬间,部署在 GPU 显存中的模型权重不会发生任何改变,上一次推理生成的中间变量也会被立即清空。下一次调用 API 时,大模型对你依然一无所知。
那么,多轮对话中那无缝衔接的上下文记忆,到底是从哪里来的?
知名 AI 专家 Andrej Karpathy 给过一个非常精准的类比:
-
LLM 模型权重(Weights):相当于计算机的 ROM(只读存储器),在训练完成后就固定不变。
-
上下文窗口(Context Window):相当于计算机的 RAM(内存/工作记忆),决定了模型此刻能同时推理处理的数据上限。
-
外部存储(向量库/数据库/历史日志):相当于计算机的 Disk(硬盘),用于持久化存储所有历史对话。
所谓的“多轮对话记忆”,并不是改变了大模型的权重,而是外部工程系统在每一次发起请求时,将历史对话记录从“硬盘”拉取出来,经过精心清洗、压缩与组装后,重新填满大模型的“内存”(Context Window),并再次喂给大模型。
一、 为什么大模型本质上是没有记忆的?
要搞懂如何构建记忆系统,首先需要理解大模型的推理机制与“工作内存”的物理边界。
1.1 函数映射机制:无状态的概率推理
从数学与代码的角度来看,大模型可以简化为一个纯粹的函数:
Output_Tokens = Model_Inference(Input_Tokens, Model_Weights)
在这个公式中:
-
Model_Weights(模型权重)是固定的数值矩阵。 -
Input_Tokens(输入的提示词)是唯一的变量。 -
Output_Tokens(生成的回答)完全由输入的 Token 序列与权重决定。
每次调用 API(例如执行一次 chat.completions.create),服务器都是在独立的容器或显存块中运行该函数。推理结束后,中间生成的 KV Cache(键值缓存)会被销毁或释放。大模型没有任何原生的全局变量或数据库来记录“上一次谁和我说话”。
[第一轮对话]
用户发送: "我叫小明"
───> [LLM 推理] ───> 产生回答: "你好,小明!"
───> (显存清空,对话状态消失)
[第二轮对话 - 如果直接只发送新问题]
用户发送: "我叫什么?"
───> [LLM 推理] ───> 产生回答: "抱歉,我不知道你的名字。" (因为输入里没有任何关于小明的信息)
1.2 上下文窗口(Context Window)与“中间丢失”困境
既然记忆完全依赖于每次请求时传入的 Input_Tokens,那么只要把所有历史对话记录全塞进 Input_Tokens 不就行了吗?
在早期(如 GPT-3.5 时代,上下文窗口仅 4K Tokens),这种做法很快就会触顶报错。虽然如今模型的上下文窗口已经扩展到 128K、1M 乃至更长,但盲目塞入全量历史文本会带来三大致命问题:
-
费用爆炸:大模型 API 是按照输入 Token 数量计费的。如果在第 50 轮对话时把前 49 轮几万字的文本全部重新发一遍,单次调用的成本将暴涨数十倍。
-
推理延迟(TTFT)激增:首字延迟(Time To First Token)与输入的 Token 长度成正比,过长的上下文会导致用户等待白屏时间过长。
-
“中间丢失”现象(Lost in the Middle):研究与实践表明,基于 Transformer 架构的模型(尤其是受旋转位置编码 RoPE 衰减影响)对上下文头部(开头)和尾部(最新对话)的信息关注度最高,而存放在中间位置的历史细节极容易被模型“忽视”。
因此,多轮对话记忆工程的核心,就是在有限且昂贵的上下文窗口中,用最低的 Token 成本,保留最关键的语义上下文。
二、 多轮对话记忆的五大工程演进范式
在真实的工业级 AI 应用(如 LangChain、LlamaIndex、Dify、Mem0)中,多轮对话的记忆管理经历了从简单粗暴到复杂精细的架构演进。
┌─────────────────────────────────────────┐
│ 多轮对话记忆架构演进路线 │
└────────────────────┬────────────────────┘
│
┌──────────────────────┬───────────────┴──────────────┬──────────────────────┐
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 全量历史拼接 │ │ 滑动窗口 │ │ 摘要压缩 │ │ 向量/图谱检索│
│ Full History │ │Sliding Window│ │ Summarization│ │Vector / Graph│
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────┐
│ 分层虚拟存储 │
│MemGPT / Letta│
└──────────────┘
2.1 范式一:全量历史拼接(Full History Buffer)
这是最基础的记忆实现方式。客户端或后端系统建立一个数组,记录每一次的交互:
messages = [
{"role": "system", "content": "你是一个助手。"},
{"role": "user", "content": "我叫小明。"},
{"role": "assistant", "content": "你好小明!"},
{"role": "user", "content": "我喜欢吃苹果。"},
{"role": "assistant", "content": "记住啦,你喜欢吃苹果。"}
]
在发起第 3 轮提问时,程序将整个 messages 数组打包发送给大模型。
-
优点:代码实现极简,信息零丢失,能完美保留每一轮的细节与语气。
-
缺点:Token 消耗随对话轮数成线性(甚至二次方)增长;一旦超过模型的上下文上限,程序直接抛出异常崩溃。
-
适用场景:交互轮数极少(< 5 轮)的简单任务或测试 Demo。
2.2 范式二:滑动窗口(Sliding Window Buffer)
为了防止 Token 爆炸,工程师引入了固定大小的“滑动窗口”策略——只保留最新的 N 轮对话,丢弃较早的历史记录。
原始对话历史: [Turn 1] [Turn 2] [Turn 3] [Turn 4] [Turn 5] [Turn 6]
│<───────── 设定窗口大小 K = 4 ─────────>│
发送给 LLM: [Turn 3] [Turn 4] [Turn 5] [Turn 6]
-
实现策略:可以基于对话轮数(Message Count),也可以基于Token 阈值(Token Budget)(例如限制历史记录最多占用 2000 Tokens)。
-
优点:成本可控,请求延迟稳定,永远能精准捕捉用户最新的意图。
-
缺点:存在典型的“硬性断崖式遗忘”。如果用户在第 1 轮说了“我对花生过敏”,而第 10 轮系统滑动窗口挤掉了第 1 轮,AI 就会在推荐菜谱时给出包含花生的食品,造成严重事故。
2.3 范式三:摘要递归压缩(Conversation Summary Memory)
为了兼顾“长久记忆”与“低 Token 消耗”,摘要压缩范式应运而生。
工作原理:当历史对话累积到一定长度(如触发阈值)时,后台异步调用一个轻量级大模型(如 GPT-4o-mini 或 Qwen-7B),将较早的对话压缩成一段高度精炼的“历史摘要(Summary)”,再将这份摘要作为背景信息拼接到 Prompt 中。
[系统请求 Prompt 组装结构]
┌─────────────────────────────────────────────────────────┐
│ System Prompt: 你是一个专业助手。 │
├─────────────────────────────────────────────────────────┤
│ Summary (历史摘要): │
│ 用户叫小明,对花生严重过敏。之前讨论了关于 Python 的学习路线│
├─────────────────────────────────────────────────────────┤
│ Recent Messages (最新窗口对话): │
│ User: 那你推荐我晚饭吃什么? │
└─────────────────────────────────────────────────────────┘
更新逻辑:每次触发压缩时,采用增量递归摘要(Incremental Summarization):
新摘要 = LLM_Summarize(旧摘要, 被挤出滑动窗口的新对话)
-
优点:大幅节省 Token,理论上支持无限轮数的长期对话。
-
缺点:
-
信息抽象损耗:摘要过程必然伴随着细节的丢失(例如压缩模型可能记录了“用户讨论了订单问题”,但删掉了具体的“订单号 883921”)。
-
复合误差(Compounding Errors):对摘要进行二次摘要(Summary of Summary),经历 5~10 次迭代后,原始信息可能发生严重的语义漂移。
-
2.4 范式四:语义检索与向量记忆(Semantic / Vector Memory)
在伴侣型 AI、个人数字分身或复杂的长流程 Agent 场景中,仅靠摘要是不够的。我们需要像人类大脑的“情节记忆(Episodic Memory)”一样,实现按需提取(Recall on Demand)。
工作原理:
-
持久化存储:每一轮对话(或按段落提取出的实体和事实)都被转换为向量(Embedding),存储在外部向量数据库(如 Qdrant、Milvus、Mem0)或图数据库中。
-
意图匹配与检索:当用户提出新问题时,系统不直接把全部历史塞给大模型,而是先用当前问题在向量库中查找语义最相关的几条历史记录。
-
动态注入:将检索出来的历史片段嵌入到当前的 Prompt 中。
用户提问: "我上个月提到的那本关于心理学的书叫什么来着?"
│
▼
[向量数据库检索] ──> 命中第 42 轮历史记录: "用户提到最近在读《被讨厌的勇气》"
│
▼
[组装最终 Prompt]:
System: 基于以下拉取到的记忆回答用户。
Memory: [第 42 轮]: 用户正在阅读《被讨厌的勇气》。
User: 我上个月提到的那本关于心理学的书叫什么来着?
│
▼
LLM 回答: "你上个月提到的心理学书籍是《被讨厌的勇气》。"
-
优点:突破上下文窗口限制,精准唤醒很久以前的特定细节,Token 占用极低。
-
缺点:增加了向量检索的开销,且非常依赖检索算法的准确率(如果检索没命中,AI 依然会回答“不记得”)。
2.5 范式五:操作系统级分层记忆架构(MemGPT / Letta 模式)
2023 年底,UC 伯克利团队提出了著名的 MemGPT(现演进为 Letta 框架),将操作系统(OS)的虚拟内存管理(Virtual Memory Management)理念引入到了大模型记忆架构中。
┌─────────────────────────────────────────────────────────┐
│ MemGPT / Letta 分层记忆架构 │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ 工作内存 (Working Context / RAM) - 有限可读写 │ │
│ │ - Core Memory: 存储用户信息、角色设定 (随时修改)│ │
│ │ - FIFO Queue: 最近发生的对话队列 │ │
│ └────────────────────────┬────────────────────────┘ │
│ │ │
│ 通过 Function Calling │
│ 自主决定读写/换入换出 │
│ │ │
│ ┌────────────────────────▼────────────────────────┐ │
│ │ 外部存储 (Archival & Recall Memory / Disk) │ │
│ │ - Recall Memory: 全量历史对话日志 │ │
│ │ - Archival Memory: 大规模海量知识库与事实片段 │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
核心突破:将记忆的管理权彻底交还给 LLM 自己。
在 MemGPT 架构中,大模型不仅仅是被动接收文本,系统为其提供了专门的工具函数(Function Calling):
-
core_memory_append:大模型发现用户提到了关键信息(如“我改名叫张伟了”),自主调用函数更新 Core Memory。 -
archival_memory_insert:将不常用但重要的信息写入硬盘存储。 -
archival_memory_search:当大模型发现当前信息不够时,自己发起搜索查询外部记忆。
这种架构让 AI 具备了像人类一样主动记录、主动修改、主动检索的真正“长期记忆能力”。
三、 现代大模型记忆系统的两大工程利器
除了上述记忆选择与压缩范式外,现代生产级 AI 系统能做到低延迟、低成本的多轮对话,还依赖于以下两大关键工程技术:
3.1 Prompt Caching(提示词缓存):拯救成本与延迟
在 2024~2026 年的生产实践中,各大模型 API 供应商(如 OpenAI、Anthropic、DeepSeek、火山引擎)纷纷推出了 Prompt Caching(提示词缓存) 机制。
为什么 Prompt Caching 是多轮对话的救星?
在多轮对话中,每一次请求发送给大模型的 Prompt 头部(如复杂的 System Prompt、角色设定、前 20 轮相同的对话历史)几乎是完全重合的。
请求 1: [System Prompt] [Turn 1] [Turn 2] -> 生成 Turn 2 Answer
请求 2: [System Prompt] [Turn 1] [Turn 2] [Turn 3] -> 生成 Turn 3 Answer
└───────────────────────────────┘
完全相同的前缀!
-
传统模式:服务端每次都要对前缀文本重新执行昂贵的 Matrix 计算与 Prefill 过程。
-
Prompt Caching 模式:大模型服务端自动识别并缓存重复前缀的 KV Cache(键值缓存)。当请求命中缓存时:
-
价格降低 50% ~ 90%!
-
首字延迟(TTFT)降低 80% 以上!
-
针对 Prompt Caching 的工程设计原则
为了确保多轮对话能够稳定命中服务端缓存,客户端在组装 Prompt 时必须遵循 “静态内容在前,动态内容在后” 的原则:
[最佳 Prompt 结构,保障最高 Cache 命中率]
1. [静态] 复杂的 System Prompt (固定不变)
2. [静态] 业务知识库 / 角色定义 (固定不变)
3. [准静态] 按照时间顺序排列的历史对话 (前缀保持不变,尾部追加)
4. [动态] 用户当前刚刚发送的新问题 (放在最末尾)
3.2 意图重写与指代消解(Query Rewriting)
多轮对话中存在一个极高频的痛点:用户的输入往往包含大量的代词和省略语。
-
第 1 轮:用户:“请问特斯拉 Model Y 多少钱?” -> AI:“Model Y 售价约为 25 万元起。”
-
第 2 轮:用户:“它的续航是多少?”
-
第 3 轮:用户:“那和 Model 3 相比呢?”
如果系统直接将第 2 轮用户的原话“它的续航是多少?”发送给外部向量库或 RAG 检索器,检索系统会完全懵圈——“它”到底是谁?
解决方案:大模型前置 Query 重写(Query Reframing)
在进入记忆检索或知识库前,系统先调用一个极快的轻量级模型,结合历史上下文将用户带指代的问题重写为一个独立的、语义完整的问题:
[原始对话历史] + 用户输入: "它的续航是多少?"
│
▼
[轻量重写模块 (Query Rewriter)]
│
▼
[重写后的独立问题]: "特斯拉 Model Y 的续航里程是多少?"
│
▼
再拿这个独立问题去检索向量库 / 匹配记忆
四、 生产级多轮对话记忆管理器代码实战
下面提供一份基于 Python 的生产级多轮对话记忆管理器(Memory Manager)完整实现。
该模块整合了:
-
Tiktoken 准确 Token 统计。
-
滑动窗口与 Token 预算控制。
-
异步增量摘要压缩。
-
针对 Prompt Caching 优化的消息队列输出。
Python 代码实现
import asyncio
import logging
from typing import List, Dict, Any, Optional
import tiktoken
from openai import AsyncOpenAI
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger("MemoryManager")
class ProductionMemoryManager:
def __init__(
self,
system_prompt: str,
max_total_tokens: int = 4000,
summary_trigger_tokens: int = 2500,
model_name: str = "gpt-4o-mini"
):
self.system_prompt = system_prompt
self.max_total_tokens = max_total_tokens
self.summary_trigger_tokens = summary_trigger_tokens
self.model_name = model_name
# 初始化 Tokenizer
try:
self.encoder = tiktoken.encoding_for_model(model_name)
except KeyError:
self.encoder = tiktoken.get_encoding("cl100k_base")
# 内部状态存储
self.summary: str = "" # 当前历史摘要
self.recent_messages: List[Dict[str, str]] = [] # 最近的对话消息队列
# OpenAI 异步客户端(用于生成摘要)
self.client = AsyncOpenAI()
def _count_tokens(self, text: str) -> int:
"""精确计算文本的 Token 数量"""
if not text:
return 0
return len(self.encoder.encode(text))
def _count_message_tokens(self, message: Dict[str, str]) -> int:
"""计算单条 Message 占用的 Token 数(包含元数据开销)"""
# 每条消息包含 role, content,元数据额外消耗约 4 个 Token
return self._count_tokens(message.get("content", "")) + 4
def add_user_message(self, content: str):
"""添加用户消息"""
self.recent_messages.append({"role": "user", "content": content})
def add_assistant_message(self, content: str):
"""添加 AI 助手消息"""
self.recent_messages.append({"role": "assistant", "content": content})
def get_total_token_usage(self) -> int:
"""计算当前完整上下文的总 Token 消耗"""
tokens = self._count_tokens(self.system_prompt) + 10
if self.summary:
tokens += self._count_tokens(self.summary) + 10
for msg in self.recent_messages:
tokens += self._count_message_tokens(msg)
return tokens
async def _generate_summary_async(self, old_summary: str, messages_to_summarize: List[Dict[str, str]]) -> str:
"""调用轻量级模型异步增量生成摘要"""
conversation_text = ""
for msg in messages_to_summarize:
conversation_text += f"{msg['role']}: {msg['content']}\n"
prompt = f"""你是一个高效的对话上下文压缩助手。请根据【旧摘要】与【新对话记录】,更新并输出一份高度精炼的【新摘要】。
【旧摘要】:
{old_summary if old_summary else "无"}
【新增对话记录】:
{conversation_text}
【要求】:
1. 保持客观,用第三人称记录。
2. 严格保留核心事实信息(如:用户姓名、偏好、提出的具体需求、关键数字/单号)。
3. 剔除无意义的寒暄与客套话。
4. 控制输出在 200 字以内。
输出新摘要:"""
try:
response = await self.client.chat.completions.create(
model=self.model_name,
messages=[{"role": "user", "content": prompt}],
temperature=0.1,
max_tokens=300
)
new_summary = response.choices[0].message.content.strip()
logger.info(f"成功更新对话摘要: {new_summary}")
return new_summary
except Exception as e:
logger.error(f"摘要生成失败: {e}")
return old_summary
async def optimize_memory_if_needed(self):
"""检查并执行动态记忆裁剪与摘要压缩"""
current_tokens = self.get_total_token_usage()
# 如果当前 Token 消耗未达到阈值,无需触发优化
if current_tokens < self.summary_trigger_tokens or len(self.recent_messages) <= 2:
return
logger.info(f"当前 Token 数 ({current_tokens}) 触发摘要阈值 ({self.summary_trigger_tokens}),开始优化记忆...")
# 取出最旧的一半消息进行摘要压缩,留出最新的消息
num_to_summarize = len(self.recent_messages) // 2
messages_to_summarize = self.recent_messages[:num_to_summarize]
self.recent_messages = self.recent_messages[num_to_summarize:]
# 异步更新摘要
self.summary = await self._generate_summary_async(self.summary, messages_to_summarize)
def get_messages_for_llm(self) -> List[Dict[str, str]]:
"""
组装最终发送给大模型的完整消息队列
遵循“静态前缀在前,动态内容在后”原则,最大化缓存命中率
"""
payload = []
# 1. 静态 System Prompt
payload.append({"role": "system", "content": self.system_prompt})
# 2. 动态生成的历史摘要(若存在)
if self.summary:
payload.append({
"role": "system",
"content": f"【此前对话的背景历史摘要】: {self.summary}"
})
# 3. 最新窗口的自然对话列表
payload.extend(self.recent_messages)
return payload
# ==================== 运行测试 ====================
async def main():
# 初始化记忆管理器,设置较小的阈值以便触发摘要测试
memory = ProductionMemoryManager(
system_prompt="你是一位专业、贴心的理财顾问助手。",
max_total_tokens=1000,
summary_trigger_tokens=200, # 降低阈值方便演示
model_name="gpt-4o-mini"
)
# 模拟多轮密集对话
print("--- 第一轮对话 ---")
memory.add_user_message("你好,我叫张伟,今年 32 岁,准备投资 50 万元。")
memory.add_assistant_message("你好张伟!很高兴为你服务。请问你的投资偏好是稳健型还是激进型?")
print("--- 第二轮对话 ---")
memory.add_user_message("我偏向稳健型,希望每年收益率在 5% 左右,千万不能亏损本金。另外我有两个孩子。")
memory.add_assistant_message("明白了,针对有两个孩子的家庭且追求保本的需求,建议建立稳健的债基与定期理财组合。")
print(f"当前总 Token 消耗: {memory.get_total_token_usage()}")
# 触发检查与自动优化
await memory.optimize_memory_if_needed()
print("\n--- 优化后拼装给大模型的最终 Payload ---")
final_payload = memory.get_messages_for_llm()
for idx, msg in enumerate(final_payload):
print(f"[{idx}] {msg['role'].upper()}: {msg['content']}")
if __name__ == "__main__":
# 真实运行前需配置 OPENAI_API_KEY 环境变量
asyncio.run(main())
五、 不同业务场景下的记忆架构选型矩阵
在企业级 AI 产品落地时,绝不能“一刀切”地使用某种单一架构。应该根据业务场景的对话轮数、容错率与预算进行精准选型:
| 业务场景类型 | 典型代表 | 最佳记忆架构组合 | 选型核心理由 |
| 任务型客服 / 简单 FAQ | 快递查询、机票退改签 | 滑动窗口 + 意图重写 (Query Rewrite) | 对话轮数通常少于 10 轮,注重时效与低延迟,无需长期记忆。 |
| 编程助手 / IDE Plugin | GitHub Copilot, Cursor | 系统 Prompt 固定 + 文件 AST 上下文 + 局部滑动窗口 | 代码依赖极为精准,不能接受摘要抽取的细节损耗,必须保留精准代码行。 |
| 伴侣型 AI / 角色扮演 | Character.ai, 虚拟恋人 | 向量语义检索 (Mem0) + 动态知识图谱 + 增量摘要 | 对话可能持续几个月甚至数年,极度依赖对用户习惯、纪念日等细节的“闪回唤醒”。 |
| 自主 Agent 智能体 | AutoGPT, Devin, 自动化运维 | MemGPT / Letta 分层架构 (系统自主 Function 读写) | 任务链条极长,Agent 需要自主规划并清空短期缓存,将中间运行结果持久化存储。 |
六、 总结与未来展望
回到本文开头的那个悖论:大模型本身没有记忆,为何能够聊得热火朝天?
答案是:大模型负责“思考”,而软件工程架构负责“记忆”。
作为一个 AI 开发者,理解大模型的无状态本质是迈向高级架构师的必经之路:
-
不要寄希望于模型权重去记住用户的上下文,每一次对话都是一次全新的函数调用。
-
记忆管理的本质,是在 Token 预算、推理成本、首字延迟与语义完整性之间寻找完美平衡。
-
从简单的滑动窗口,演进到增量摘要与向量检索,再到Prompt Caching 缓存优化以及 MemGPT 分层记忆,外部架构的不断进化正一步步将大模型从一个“无状态的文本计算器”,打造成为真正具备持久记忆能力的“数字智能体”。
在未来,随着原生支持百万级超长上下文且 KV Cache 成本极低的新一代模型普及,记忆管理的形态还将继续进化,但“利用软件工程补充模型物理极限”的核心哲学,将始终贯穿 AI 系统设计的始终。
更多推荐
所有评论(0)