摘要
大语言模型本身并不会永久记住用户。每次调用模型时,应用都需要主动把必要的信息放入本次请求的上下文中,模型才能根据这些信息生成连贯回答。
当对话变长后,简单地把全部历史消息拼接到 Prompt 中,会遇到上下文窗口不足、调用成本增加、响应变慢、旧信息干扰新问题以及隐私泄露等问题。因此,真正的"记忆"不是无限保存所有内容,而是围绕当前任务选择、压缩、检索和管理上下文。
本文从对话应用的实际需求出发,介绍上下文窗口、消息历史、短期记忆、长期记忆、摘要压缩和相关信息检索等核心概念,并通过 Python 示例实现一个简化的上下文管理器。读完本文后,你应该能够:
- 区分上下文、历史消息和长期记忆;
- 设计适合多轮对话的上下文拼装策略;
- 使用摘要和检索控制 Token 消耗;
- 处理记忆写入、更新、删除和隐私保护;
- 为 AI 应用建立可观测、可评估的记忆机制。
一、背景与问题
1. 模型为什么会"忘记"之前的对话
一次大模型调用通常是独立的。应用发送本轮消息,模型根据请求中的内容生成响应;下一次调用时,如果应用不再发送上一轮内容,模型就无法知道之前发生了什么。
例如,用户先说:
text
我叫林舟,是一名 Java 后端工程师。
然后又问:
text
我适合学习哪门语言?
如果第二次调用没有携带"林舟"和"Java 后端工程师"这些信息,模型只能根据当前问题回答,而不会真正记得用户身份。
因此,所谓"记住用户",通常是应用层完成的:
text
用户输入
-> 读取已有会话历史和用户记忆
-> 选择与当前问题相关的信息
-> 组装本次模型请求
-> 生成回答
-> 判断哪些新信息需要保存
模型负责理解和生成,应用负责保存、选择和注入。
2. 把所有历史消息都传给模型可行吗
在对话刚开始时,可以直接发送全部历史:
text
系统指令
+ 第一轮用户消息
+ 第一轮助手消息
+ 第二轮用户消息
+ 第二轮助手消息
+ 当前用户消息
但这种做法会随着对话变长逐渐失效:
- 上下文窗口有最大容量;
- 每次请求都重复发送旧消息,成本持续增加;
- 输入越长,模型处理时间通常越长;
- 很久以前的无关内容会干扰当前任务;
- 历史中的敏感信息可能被不必要地带入请求;
- 用户纠正过的旧信息可能与新信息冲突。
所以,记忆系统的目标不是"保存得越多越好",而是"在正确的时机提供正确的信息"。
3. 上下文管理要解决的四个问题
一个可用的上下文系统至少需要回答:
- 当前任务必须保留哪些信息?
- 哪些历史内容可以压缩或丢弃?
- 哪些用户偏好需要跨会话保存?
- 如何避免错误、过时或敏感信息被长期使用?
这四个问题分别对应上下文选择、历史压缩、长期记忆和记忆治理。
二、核心概念
1. Context Window:上下文窗口
上下文窗口是模型一次能够处理的输入和输出容量。它通常以 Token 作为计量单位,而不是简单按字符数计算。
一次调用的 Token 预算大致包括:
text
系统指令 Token
+ 工具定义 Token
+ 历史消息 Token
+ 长期记忆 Token
+ 检索结果 Token
+ 当前用户消息 Token
+ 预留输出 Token
<= 模型上下文上限
如果输入内容已经占用了大部分容量,即使模型支持较长上下文,也可能没有足够空间生成完整回答。
因此,应用在组装上下文时应预留输出空间,而不是把上下文窗口全部用于历史消息。
2. Message History:消息历史
对话历史通常由不同角色的消息组成:
| 角色 | 含义 |
|---|---|
| system | 应用的行为规则、角色设定和安全约束 |
| user | 用户输入 |
| assistant | 模型生成的回答 |
| tool | 工具调用结果或外部系统返回值 |
消息历史主要用于保持当前会话的连贯性。例如,上一轮用户问了一个错误原因,下一轮用户说"那应该怎么改",应用需要保留上一轮问答才能正确理解"那"指什么。
3. 短期记忆和长期记忆
可以按照有效范围区分记忆:
| 类型 | 有效范围 | 典型内容 |
|---|---|---|
| 当前消息 | 当前请求 | 用户本次输入 |
| 短期记忆 | 当前会话 | 最近几轮对话、当前任务状态 |
| 会话摘要 | 当前会话或一段时间 | 对话目标、已确认结论、待办事项 |
| 用户画像 | 跨会话 | 用户偏好、语言习惯、职业背景 |
| 业务记忆 | 特定业务对象 | 订单状态、项目配置、知识库资料 |
| 外部事实 | 由系统维护 | 文档、数据库记录、实时业务数据 |
短期记忆解决"当前对话是否连贯",长期记忆解决"跨会话是否个性化"。两者不应混为一谈。
4. 语义记忆、情节记忆和工作记忆
借鉴认知系统的划分方式,可以把长期信息进一步分为:
- 语义记忆:相对稳定的事实,例如用户是 Java 开发者;
- 情节记忆:某次具体事件,例如用户上周讨论过订单退款问题;
- 工作记忆:当前任务中暂时有效的状态,例如正在修改第 3 个接口;
- 偏好记忆:用户明确表达的偏好,例如回答希望使用中文并提供代码。
不同记忆的保存策略不同。用户明确说"以后都用中文回答",可能适合保存为长期偏好;用户说"这次先用 Python 写",通常只对当前任务有效。
5. 摘要不是简单截断
历史过长时,可以使用摘要替代旧消息。好的摘要应保留:
- 当前任务和目标;
- 用户已经确认的事实;
- 已经采取的方案;
- 未解决的问题;
- 重要约束和偏好;
- 后续行动。
低质量摘要常见的问题是只保留主题,丢失关键细节。例如"用户讨论了数据库问题"无法替代"用户使用 MySQL 8.0,订单表按 user_id 查询慢,已确认需要检查联合索引"。
摘要可以分为:
- 滚动摘要:每次达到阈值就更新;
- 阶段摘要:一个任务或阶段结束后生成;
- 事实摘要:只提取可验证的用户事实;
- 决策摘要:记录已经确认的方案和约束。
6. 记忆检索和 RAG 的关系
长期记忆通常不能全部放进 Prompt,需要先检索与当前问题相关的内容:
text
当前问题
-> 生成查询表示
-> 从记忆库召回候选记忆
-> 按相关性、时效性和可信度排序
-> 过滤冲突与敏感信息
-> 注入有限条目
这与 RAG 的流程相似,但数据来源不同:
| 对比项 | 知识库 RAG | 用户记忆检索 |
|---|---|---|
| 数据来源 | 文档、网页、企业资料 | 用户对话、用户偏好、任务记录 |
| 主要目标 | 提供外部事实 | 保持个性化和任务连续性 |
| 关键风险 | 文档过时、召回错误 | 隐私、误记、过度推断 |
| 更新方式 | 文档更新和重新索引 | 对话中提取、确认和修正 |
用户记忆不能因为"语义相似"就被默认当作事实。相似度只是召回信号,不等于真实性和当前有效性。
三、工作原理
1. 一次带记忆的对话请求
完整流程可以表示为:

这里有两个时间点:
- 生成前:决定本次请求携带什么;
- 生成后:决定本轮内容是否值得保存。
如果只做前者,系统只能"使用记忆"但不会持续学习;如果只做后者,系统会积累大量未经筛选的历史噪声。
2. 上下文拼装的优先级
一个实用的优先级可以是:
text
最高:安全规则和系统约束
其次:当前用户问题
其次:当前任务状态和最近几轮对话
其次:与问题直接相关的长期记忆
较低:较早且不确定的历史内容
最低:与当前任务无关的闲聊
当 Token 不够时,应优先保留高优先级内容。不能为了保留一段很久以前的闲聊,删除当前任务的关键约束。
3. 消息历史的压缩策略
常见的压缩策略包括:
| 策略 | 优点 | 缺点 |
|---|---|---|
| 固定保留最近 N 轮 | 简单、可预测 | 可能丢失重要旧事实 |
| 按 Token 裁剪 | 能控制预算 | 可能截断语义完整的对话 |
| 滚动摘要 | 能保留整体脉络 | 摘要可能丢细节或引入错误 |
| 重要性筛选 | 更节省上下文 | 需要设计评分规则 |
| 摘要加最近消息 | 兼顾全局和局部 | 实现复杂度适中 |
| 相关性检索 | 只带相关信息 | 可能召回错误内容 |
实际应用通常采用组合策略:保留当前任务摘要、最近几轮原文,再补充少量相关长期记忆。
4. 记忆写入不应完全自动
用户在聊天中说出的每句话,都不应该自动变成长期记忆。可以先将内容分为三类:
- 明确记忆:用户明确要求保存;
- 稳定事实:经过规则或确认后认为长期有效;
- 临时信息:只保留在当前会话中。
例如:
text
"以后请都用中文回答" -> 候选长期偏好
"我今天要参加一个会议" -> 当前会话信息
"我的密码是......" -> 禁止保存
"我可能下个月换工作" -> 不确定信息,暂不固化
对高风险信息,应当默认不保存,或要求用户明确确认。
5. 记忆更新和冲突处理
用户信息可能变化。例如历史记忆中保存"用户住在杭州",后来用户说"我已经搬到上海"。系统需要更新旧记忆,而不是同时保留两条互相矛盾的事实。
记忆条目可以包含:
text
memory_id
user_id
content
memory_type
source_message_id
confidence
created_at
updated_at
expires_at
status
当新记忆与旧记忆冲突时,可以采用:
- 识别相同主题的旧记忆;
- 比较新信息的明确程度和来源;
- 将旧记忆标记为失效,而不是物理删除;
- 写入新记忆;
- 在需要时向用户确认。
保留变更记录有助于审计和问题排查,但不代表所有历史内容都应该继续注入模型。
四、实战示例
下面实现一个简化的上下文管理器。示例使用抽象的 LLMClient 和 MemoryStore,不绑定具体模型供应商,重点展示上下文管理逻辑。
1. 定义消息和记忆结构
python
from dataclasses import dataclass
from datetime import datetime
from typing import Literal
Role = Literal["system", "user", "assistant", "tool"]
@dataclass
class Message:
role: Role
content: str
created_at: datetime
@dataclass
class Memory:
memory_id: str
user_id: str
content: str
memory_type: str
confidence: float
updated_at: datetime
expires_at: datetime | None = None
active: bool = True
真实项目中,这些对象通常会映射到数据库表。会话消息、会话摘要和长期记忆应尽量分开存储,因为它们的查询方式、保留周期和权限要求不同。
2. 设计存储接口
先定义接口,再决定使用 PostgreSQL、Redis、向量数据库还是其他存储:
python
class ConversationStore:
def list_recent_messages(
self,
conversation_id: str,
limit: int
) -> list[Message]:
raise NotImplementedError
def get_summary(self, conversation_id: str) -> str | None:
raise NotImplementedError
def save_message(
self,
conversation_id: str,
message: Message
) -> None:
raise NotImplementedError
def save_summary(
self,
conversation_id: str,
summary: str
) -> None:
raise NotImplementedError
class MemoryStore:
def search(
self,
user_id: str,
query: str,
limit: int
) -> list[Memory]:
raise NotImplementedError
def upsert(self, memory: Memory) -> None:
raise NotImplementedError
接口抽象的价值在于,上下文管理器不需要知道数据具体存在哪里。后续从内存实现切换到数据库,或者增加向量检索,不必重写请求编排逻辑。
3. 估算 Token 并控制预算
不同模型的 Token 计算规则可能不同,工程中应优先使用目标模型或 SDK 提供的计数方法。这里用一个粗略估算演示预算控制:
python
def estimate_tokens(text: str) -> int:
# 仅用于演示,生产环境应替换为实际 tokenizer
return max(1, len(text) // 3)
def messages_tokens(messages: list[dict]) -> int:
return sum(
estimate_tokens(message.get("content", ""))
+ 4
for message in messages
)
def fit_memories(
memories: list[Memory],
available_tokens: int
) -> list[Memory]:
selected = []
used = 0
for memory in memories:
cost = estimate_tokens(memory.content) + 8
if used + cost > available_tokens:
break
selected.append(memory)
used += cost
return selected
生产系统不能把这个估算当成精确结果。除了消息内容,还可能有工具定义、图片、结构化输出约束和模型输出预留空间。
4. 拼装本次请求上下文
python
class ContextManager:
def __init__(
self,
conversations: ConversationStore,
memories: MemoryStore,
llm,
max_input_tokens: int = 6000,
output_reserved_tokens: int = 1500
):
self.conversations = conversations
self.memories = memories
self.llm = llm
self.max_input_tokens = max_input_tokens
self.output_reserved_tokens = output_reserved_tokens
def build_messages(
self,
user_id: str,
conversation_id: str,
current_query: str
) -> list[dict]:
summary = self.conversations.get_summary(conversation_id)
recent = self.conversations.list_recent_messages(
conversation_id,
limit=12
)
memories = self.memories.search(
user_id,
query=current_query,
limit=8
)
messages = [
{
"role": "system",
"content": (
"你是一个可靠的中文助手。"
"只使用上下文中明确提供的信息,不要把不确定内容当作事实。"
)
}
]
if summary:
messages.append({
"role": "system",
"content": "当前会话摘要:\n" + summary
})
if memories:
memory_text = "\n".join(
f"- {memory.content}"
for memory in memories
if memory.active
)
messages.append({
"role": "system",
"content": "与当前问题可能相关的用户记忆:\n" + memory_text
})
for message in recent:
messages.append({
"role": message.role,
"content": message.content
})
messages.append({
"role": "user",
"content": current_query
})
return self._trim_to_budget(messages)
def _trim_to_budget(
self,
messages: list[dict]
) -> list[dict]:
budget = self.max_input_tokens - self.output_reserved_tokens
if messages_tokens(messages) <= budget:
return messages
system_messages = messages[:1]
current_message = messages[-1]
middle = messages[1:-1]
while middle and messages_tokens(
system_messages + middle + [current_message]
) > budget:
middle.pop(0)
return system_messages + middle + [current_message]
这个版本展示了最基本的裁剪逻辑。生产实现不应简单地从头部逐条删除,还应该优先保留摘要、当前任务状态和成对的 user/assistant 消息,避免只留下半截对话。
5. 加入摘要压缩
当历史消息超过阈值时,可以让模型生成摘要:
python
class SummaryService:
def __init__(self, llm):
self.llm = llm
def summarize(
self,
old_summary: str | None,
messages: list[Message]
) -> str:
history = "\n".join(
f"{message.role}: {message.content}"
for message in messages
)
prompt = f"""
请将下面的对话整理成后续回答可以使用的事实摘要。
保留:
1. 用户当前目标;
2. 已确认的事实;
3. 已做出的决定;
4. 未解决的问题;
5. 明确的偏好和约束。
不要:
1. 编造对话中没有出现的信息;
2. 保存密码、Token 或其他敏感凭据;
3. 把猜测写成确定事实;
4. 添加与任务无关的闲聊。
已有摘要:
{old_summary or "无"}
新增对话:
{history}
"""
return self.llm.generate_text(prompt)
摘要生成结果也应该经过长度限制和质量检查。对高价值业务,可以保留原始消息并支持重新生成摘要,避免一次错误摘要永久影响后续回答。
6. 处理一次完整的对话请求
python
class ChatService:
def __init__(
self,
context_manager: ContextManager,
conversations: ConversationStore,
summary_service: SummaryService
):
self.context_manager = context_manager
self.conversations = conversations
self.summary_service = summary_service
def chat(
self,
user_id: str,
conversation_id: str,
query: str
) -> str:
messages = self.context_manager.build_messages(
user_id=user_id,
conversation_id=conversation_id,
current_query=query
)
answer = self.context_manager.llm.generate(
messages=messages
)
now = datetime.utcnow()
self.conversations.save_message(
conversation_id,
Message("user", query, now)
)
self.conversations.save_message(
conversation_id,
Message("assistant", answer, now)
)
return answer
真实系统还需要处理模型调用失败、流式输出、重复请求和并发写入。保存消息的时机要根据业务定义:有些系统只保存完整成功的回答,有些系统会保存中断回答并标记为 partial。
7. 设计记忆提取规则
可以让模型从本轮对话中提取候选记忆,但模型输出只能作为候选,不能直接写入长期记忆:
python
def extract_memory_candidates(
user_message: str,
assistant_message: str
) -> list[dict]:
"""
实际项目中可调用一个结构化输出模型。
返回内容示例:
[
{
"type": "preference",
"content": "用户偏好使用中文回答",
"confidence": 0.96,
"should_persist": True
}
]
"""
candidates = []
if "以后请用中文回答" in user_message:
candidates.append({
"type": "preference",
"content": "用户偏好使用中文回答",
"confidence": 0.98,
"should_persist": True
})
return candidates
更稳妥的做法是:
- 通过规则拦截密码、Token、身份证号等敏感信息;
- 只有明确表达长期意图的句子才进入候选;
- 对低置信度候选要求用户确认;
- 给每条记忆设置来源、时间和过期策略;
- 允许用户查看、修改和删除。
8. 使用 FastAPI 暴露聊天接口
python
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
class ChatRequest(BaseModel):
user_id: str = Field(min_length=1)
conversation_id: str = Field(min_length=1)
message: str = Field(min_length=1, max_length=8000)
@app.post("/api/chat")
def chat(request: ChatRequest):
answer = chat_service.chat(
user_id=request.user_id,
conversation_id=request.conversation_id,
query=request.message
)
return {
"conversation_id": request.conversation_id,
"answer": answer
}
用户身份不能直接信任请求体中的 user_id。生产环境应从登录态或经过验证的 Token 中取得用户身份,并检查当前用户是否有权访问指定会话。
五、常见问题与实践建议
1. 历史消息越多,回答一定越好吗
不一定。过多历史可能带来:
- 无关信息干扰;
- 旧结论与新结论冲突;
- Prompt 注入内容重复出现;
- Token 成本上升;
- 关键内容在长上下文中被忽略。
建议将"完整历史保存"和"每次请求注入"分开。数据库可以保留完整审计记录,但模型只接收当前任务所需的信息。
2. 摘要丢失了重要细节怎么办
摘要压缩本身是有损操作。可以采用分层保存:
text
原始消息:完整保存,可回查
阶段摘要:保留任务脉络
事实记忆:保存稳定且可复用的事实
当前状态:保留尚未完成的操作
在生成摘要时,可以明确要求保留数字、名称、时间、约束和决策。对关键业务还可以让系统从摘要中提取结构化字段,而不是完全依赖自然语言摘要。
3. 模型把记忆中的错误信息当成事实
解决方法包括:
- 在记忆注入文本中标记来源和可信度;
- 明确告诉模型记忆可能过期,只能作为参考;
- 对高风险事实要求实时查询业务系统;
- 让用户可以纠正和删除记忆;
- 新旧信息冲突时优先询问,而不是自行猜测。
例如订单状态、库存和账户余额不应从历史记忆中读取,应始终查询权威业务数据源。
4. 会话摘要和长期记忆应该放在哪里
可以按数据特征选择存储:
| 数据 | 常见存储 |
|---|---|
| 最近消息 | 关系型数据库或 Redis |
| 会话摘要 | 关系型数据库 |
| 结构化用户偏好 | 关系型数据库 |
| 大量语义记忆 | 向量数据库或带向量能力的数据库 |
| 原始审计记录 | 对象存储或关系型数据库 |
| 临时工作状态 | Redis 或任务状态存储 |
不要一开始就把所有内容都向量化。结构化、可过滤、可更新的用户偏好,通常优先使用普通数据库字段;只有需要语义检索的内容才适合向量索引。
5. 多用户和多租户场景如何避免串记忆
每条会话和记忆都必须绑定明确的作用域:
text
tenant_id
-> user_id
-> conversation_id
-> message_id
查询时必须带上租户和用户条件,不能只根据自然语言相似度检索。缓存 Key 也要包含作用域,避免不同用户因为 Key 冲突读到彼此的历史。
6. 流式输出时什么时候保存消息
流式响应通常需要区分几个状态:
- generating:模型正在生成;
- completed:完整生成并正常结束;
- cancelled:用户主动取消;
- failed:模型调用失败;
- partial:只收到部分内容。
可以先创建一条 generating 记录,持续追加或临时缓存内容,结束后再标记最终状态。不要把未完成内容直接当作可信长期记忆。
7. 记忆会不会被 Prompt Injection 污染
会。用户输入、网页内容和工具返回值都可能包含"请记住某条指令"或伪装成系统消息的内容。
防护建议:
- 将记忆作为不可信数据,而不是系统指令;
- 使用明确的分隔符标注记忆来源;
- 不允许记忆覆盖系统安全规则;
- 对记忆写入做规则过滤和人工确认;
- 不把工具返回的任意文本直接升级为用户事实;
- 对记忆命中和实际使用建立审计日志。
8. 用户是否应该能够管理自己的记忆
应该。至少提供:
- 查看当前保存了哪些记忆;
- 删除某条记忆;
- 清空全部记忆;
- 禁用长期记忆;
- 纠正错误信息;
- 查看记忆来源和更新时间。
记忆功能涉及个性化,也涉及隐私和控制权。系统不能只提供"记住",却不提供"忘记"。
六、进阶思考
1. 从"记忆"升级为"状态管理"
简单聊天只需要消息历史,但复杂 Agent 任务需要保存结构化状态:
text
任务目标:生成一份项目方案
当前阶段:需求整理完成
已确认约束:使用 Java、部署到容器平台
已调用工具:文档检索、数据库查询
待办事项:补充监控方案
失败次数:1
这类信息不应全部依赖自然语言消息传递,而应放入状态对象中。模型负责提出下一步行动,应用负责校验和持久化状态。
2. 记忆写入可以采用事件驱动
对话完成后,可以发布一个事件:
text
ConversationCompleted
-> 保存消息
-> 更新会话摘要
-> 提取候选记忆
-> 敏感信息检测
-> 记忆去重与冲突处理
-> 更新检索索引
-> 记录审计日志
这样可以把用户等待的聊天请求和较慢的记忆处理解耦。但要注意最终一致性:下一次请求可能发生在记忆索引更新之前,系统应能接受短暂延迟。
3. 记忆检索不应只使用相似度
一个更完整的排序函数可以综合:
text
最终得分
= 语义相关性
+ 任务相关性
+ 时间新鲜度
+ 用户明确程度
+ 来源可信度
- 敏感性风险
- 与当前事实的冲突程度
不同业务的权重应该通过评估集调优,而不是一开始固定一个看似合理的数值。高相关但低可信的记忆,可能比低相关但权威的业务数据更危险。
4. 如何评估上下文管理质量
可以建立专门的评估问题:
- 模型是否记住了用户明确要求保存的偏好;
- 是否在无关问题中错误注入记忆;
- 记忆冲突时是否能主动询问;
- 摘要后是否保留关键约束;
- 用户删除记忆后是否真正停止使用;
- 多用户之间是否完全隔离;
- 长对话下 Token 成本和延迟是否可接受。
指标不应只有回答是否正确,还应包括记忆准确率、记忆召回率、错误记忆率、过度记忆率、删除生效时间和跨用户泄露率。
5. 上下文管理的工程原则
可以将核心原则总结为:
text
保存完整历史,注入有限上下文
事实和指令分开管理
临时状态和长期记忆分开管理
相关性不等于真实性
模型提取结果必须经过治理
高风险信息默认不保存
所有记忆都应该可追溯、可修正、可删除
这些原则适用于普通聊天应用,也适用于知识库问答、代码助手和 Agent 工作流。
结论
大语言模型不会自动永久记住用户。AI 应用中的"记忆"本质上是应用对历史消息、会话摘要、用户偏好和业务状态进行保存、筛选、压缩和注入。
本文的核心内容可以归纳为:
- 上下文窗口决定一次请求能够携带多少信息;
- 短期记忆主要依赖最近消息和会话摘要;
- 长期记忆需要独立存储、检索和治理;
- 历史压缩应优先保留目标、事实、决策和约束;
- 记忆写入必须考虑准确性、时效性、安全和用户控制;
- 结构化业务状态不应完全依赖自然语言历史;
- 评估记忆系统时,需要同时关注记得住、用得准和忘得掉。
下一篇可以继续学习 RAG,了解如何让大模型基于企业知识库回答问题,并进一步区分"对话记忆"和"外部知识检索"在系统设计中的边界。