03. 会话状态和上下文应该怎么管理?多轮对话的 Context 怎么维护?
整理会话状态、多轮上下文维护与 Context 管理策略。
简单回答
会话状态的核心挑战是把"有限的 context window"用好。多轮对话的 context 管理通常包括:存储完整对话历史(Redis/数据库)、每次请求时截取最近 N 轮或按 token 上限裁剪、维护 system prompt + 关键信息的 "固定区" + 动态历史的 "滑动区"。高级方案还会做摘要压缩、重要信息提取和 RAG 检索。
详细解释
问题的本质
LLM 是无状态的——每次调用都是独立的,模型本身不"记得"之前说过什么。要实现多轮对话,必须在每次调用时把历史消息拼接到 Prompt 中发给模型。但 context window 是有限的(4K~128K 不等),而对话历史会不断增长,所以需要一套策略来管理这个 context。
存储层
对话历史的完整记录通常存在两层。热数据(最近的活跃会话)存 Redis,结构是 session_id → [messages],TTL 按业务需求设置(比如 30 分钟无交互自动过期)。冷数据(历史会话)落盘到数据库(PostgreSQL、MongoDB 等),按需加载。
每条消息至少包含:role(system/user/assistant/tool)、content、timestamp、token_count。存储时预计算 token 数,这样截取时不需要实时 tokenize。
Context 构建策略
每次调用 LLM 时,需要从对话历史中构建一个不超过模型 context window 的 Prompt。一个常见的架构是把 context 分成三个区域:
固定区:system prompt + 关键指令 + 用户画像等。这部分不会被裁剪,每次都带上。占用 token 量要尽量控制住。
RAG/工具结果区:本轮检索到的相关文档、工具调用结果等。这部分和当前问题相关,优先级高。
历史对话区:从最近的消息开始往回填充,直到接近 token 上限。最简单的策略是滑动窗口——保留最近 N 轮,超出的直接丢弃。更精细的做法是按 token 数裁剪,预留足够的空间给模型生成(通常预留 max_tokens 的输出空间)。
高级 Context 管理策略
第一种是对话摘要压缩。当对话历史超过一定长度后,对早期的对话做摘要,用摘要替代原始消息。比如前 20 轮的对话压缩成一段 200 token 的摘要放在 context 开头,后面只保留最近 5 轮的完整消息。摘要可以用同一个 LLM 生成(代价是额外一次 API 调用),也可以用更轻量的模型。
第二种是关键信息提取。在对话过程中提取关键实体和事实(用户名、项目名、需求要点),存成结构化数据,每次作为 system prompt 的一部分注入,不需要完整回放所有历史。
第三种是用 RAG 检索历史。把所有历史消息做 embedding 存进向量数据库,每轮对话时根据当前问题检索最相关的历史片段,而不是简单地按时间顺序截取。这样即使对话很长,也能找到很久之前提到的关键信息。
工程上的坑
token 计算要精确。不同模型的 tokenizer 不同,同样的文本在 GPT-4 和 LLaMA 中 token 数不一样。context 构建时必须用对应模型的 tokenizer 做精确计算,不能拿字符数或词数估算。
多模态消息的 token 计算。如果对话中包含图片,图片也占 context window。不同模型对图片的 token 消耗算法不同,需要单独处理。
并发安全。同一个用户可能同时发多条消息(快速连点),需要保证对话历史的读写是原子的,不能出现消息乱序或丢失。
面试时可以这样答
LLM 本身是无状态的,多轮对话的关键是每次调用时把历史消息拼到 Prompt 里。核心挑战是 context window 有限但对话历史不断增长。 存储上一般用 Redis 存活跃会话、数据库存历史会话。每条消息预计算好 token 数,构建 context 时不用实时 tokenize。
Context 构建我一般分三个区:固定区放 system prompt 和关键指令,不裁剪;RAG 区放本轮检索结果;历史区从最近的消息往回填,直到 token 接近上限。最简单的策略是滑动窗口截最近 N 轮。高级方案可以做对话摘要压缩——早期对话压成摘要,只保留最近几轮完整消息。或者用向量检索从历史中找最相关的片段。
工程上要注意 token 计算必须用对应模型的 tokenizer 精确算,不能估算。还有并发安全——同一用户快速连点时要保证消息顺序不乱。
常见追问
- 对话摘要压缩用什么模型做?延迟和成本怎么控制?
- 滑动窗口截断丢了早期重要信息怎么办?
- 如果用户引用了很久之前说的话,模型没有 context 该怎么处理?