对话式AI中的context-mode:滑窗、摘要压缩与长期记忆的工程实践
语言模型产品做久了你会发现“context-mode”这个词被提得越来越频繁。圈子里聊它、项目里写它、发布会上也讲它但落到实际开发里每个人对它的理解其实不太一样。有人拿它当“多轮对话记忆开关”有人把它理解成“上下文窗口的调度策略”还有人干脆把它当成一种产品交互模式来设计。我的理解更朴素一些context-mode 是对话式 AI 应用里围绕“怎么用、用好、用不完上下文”的一整套工程方案。它解决的是同一个矛盾——模型再强的上下文窗口也有限而真实产品的对话永远比窗口长。这篇就围绕这个核心展开聊聊我在实际项目里对 context-mode 的拆解、落地和避坑经验给正在做 AI 应用开发的同行一个可复用的参考框架。先说个共识。做 LLM 应用的人都清楚把全部历史对话一股脑塞给模型是不现实的。你塞得下但响应速度和成本会先把你压垮就算你真塞下了模型对不同位置信息的注意力也不是均匀分布的中间那段历史经常被“选择性遗忘”。所以 context-mode 不是什么高深理论而是每个 AI 应用都要做的一道必答题当前这轮请求到底该带哪些上文给模型不带哪些。1. context-mode 是什么一个概念三个真问题context-mode 不是一个标准术语更像是工程界对“上下文管理方式”的一个统称。我在 GitHub 上见过有人把它封装成库在论文里见过它指代“上下文压缩策略”在几个开源框架里则是“记忆模式”的配置项。结合自己做过的几个 AI 产品我倾向于把它拆成三个问题去看模型侧的上下文窗口限制、产品侧的记忆策略、以及用户侧的感知模式。模型侧的上下文窗口是硬约束。不同模型的窗口大小差异很大GPT-4o 的 128K、部分开源模型的 32K/64K、以及一些支持百万级上下文的实验性模型。但窗口大不等于能随便用。上下文越长推理延迟越高、单位请求成本越高模型在中段信息的召回能力也会下降。你给模型塞 100K 的对话历史它可能只“认真看”了前面和最后的部分中间的关键信息反而丢了。这就是业内常说的“迷失在中间”现象。产品侧的记忆策略是真正的设计空间。同样一个客服机器人你可以让它只记住最近 6 轮对话也可以让它记住用户三个月前的投诉记录也可以让它把历史对话摘要成几条要点塞进 system prompt。不同的做法就是不同的 context-mode没有绝对最优只有场景适配。用户侧的感知模式则容易被忽略。用户感知不到 token 窗口但他们感知得到一件事——AI 还记不记得我上句话说了什么、三天前说过什么、甚至去年提过的偏好。这是产品体验的分水岭。context-mode 的终极目标不是“塞得最多”而是“记得最准”。1.1 上下文窗口的物理与注意力限制理解模型上下文窗口不能只把它当成“容量限制”。窗口大小决定的是模型“能看到的材料范围”但真正影响输出质量的是模型对这段材料里有效信息的提取效率。我习惯用一个比喻模型像是一个阅读速度极快但笔记有限的临时工。你给他 100 页资料他能快速翻完但你要他准确说出第 47 页第 3 段的措辞他大概率会记混。你说“第 47 页大致讲了什么”他能答得不错。这决定了 context-mode 的核心策略不能指望模型从完整历史里精确提取每一个细节而应该由产品层替模型做信息筛选与结构化压缩。另一个实际约束是性能。实测中把上下文从 4K 拉到 32K单次请求的延迟通常会翻倍甚至更高。对实时性要求高的产品比如客服、语音助手这是不可接受的。所以 context-mode 在工程上还要回答如何在“信息完整度”和“响应速度”之间取一个业务可接受的值。1.2 产品侧上下文管理的三个维度我在设计 context-mode 时会从三个维度考虑信息分层、时间衰减、内容结构化。信息分层是把上下文分成“恒定层”系统指令、产品规则、用户长期画像、“会话层”当前任务的中间状态、最近几轮对话和“临时层”一次性指令、临时上传的文件内容。恒定层每次必带会话层按窗口预算动态裁剪临时层用完即丢。时间衰减对应的是“多久以前的信息还值得带”。这不是简单的按轮数截断。比如一个购物助手用户 10 分钟前咨询过退换货政策这个信息在后续对话里高度相关而用户 3 天前闲聊过一句“今天天气不错”这个信息基本可以丢弃。好的 context-mode 应该对上下文做相关性排序而不是机械地按时间顺序保留。内容结构化则是把非结构化的历史对话转成更紧凑、更可用的形态。比如把“用户抱怨了三次物流慢、一次包装破损”压成一句“用户对物流时效敏感曾反馈包装破损”模型后续生成回复时就不需要每次都读原始抱怨才能理解用户情绪。这一层做得好不好直接决定了 context-mode 的“智能感”。2. 三种主流 context-mode 实现方式与选型把概念落到代码之前先看现在工程里最常见的三种实现方式。它们不是互斥关系实际产品里通常是组合使用但作为独立方案理解更容易看清各自的边界。2.1 滑动窗口模式滑动窗口是最简单、最直觉的做法只保留最近 N 轮对话超出窗口的信息直接丢弃。实现上就是一个固定长度的队列新消息进来旧消息出去。优点是成本低、实现快、响应稳定。对质量要求不高、对话轮次普遍较短的场景比如简单的 FAQ 机器人、表单填写助手滑动窗口完全够用。缺点也明显——它没有任何“记忆”能力。用户在第 3 轮提供的信息到第 10 轮就查无此人了。这会导致两个常见问题一是模型反复询问用户已给过的信息用户会觉得 AI 是金鱼二是用户需要手动复述旧信息才能继续任务交互效率极低。另外窗口长度怎么定是个玄学。太短信息不够太长延迟和成本飙升。我见过不少项目把窗口定在 10-20 轮理由是“差不多够用”但几乎从没验证过这个数字是否真的匹配业务场景。2.2 摘要压缩模式摘要压缩是为了解决滑动窗口“健忘症”而生的。核心思路是把超出窗口的历史对话交给模型或用更轻量的算法生成一段摘要然后把摘要保留在上下文中替代原始对话。这个方案比滑动窗口聪明得多。它保留了“更久远但更重要”的信息同时大幅压缩 token 占用。举个例子50 轮对话原始内容可能有 20K token压缩成 400 token 的摘要后配合最近 10 轮原始对话总窗口占用可以控制在 3-4K 以内而关键信息几乎没有丢失。但摘要压缩有几个工程痛点。第一摘要有损。模型概括信息时可能会遗漏细节、模糊数字最怕的是把“用户说不要A”概括成“用户偏好A”语义颠倒是致命伤。第二摘要会累积误差。每 20 轮做一次摘要摘要的摘要的摘要……迭代几轮之后原始内容已经面目全非。第三摘要时机不好把握。做太频繁浪费调费做太少又跟不上对话进展。我在项目里通常用一个阈值策略历史对话超过窗口上限的 60% 时触发压缩同时把“压缩前的重要信息核对”当成一个独立校验步骤来做。2.3 长期记忆模式长期记忆模式是现在的热门方向也是把 context-mode 从“工程技巧”升维到“产品能力”的关键。它的核心思路是对话结束后把有价值的信息提取出来写入结构化的记忆库新对话开始时根据当前意图从记忆库检索出相关记忆注入上下文。这套思路和 RAG检索增强生成高度重合。你可以把长期记忆理解成一个“私人 RAG 库”但有几个不同点记忆的写入是隐式发生的模型自己判断哪些信息值得记、记忆的粒度更细一条记忆可能只是“用户养了一只叫豆包的柯基”、记忆的时效性更复杂有些信息一次有效有些需要长期保留有些过段时间需要更新。长期记忆模式的优势是上限极高——它能真正实现“AI 懂你”的体验。但落地难度也最大怎么写记忆、怎么去重、怎么更新、怎么避免记忆冲突每个问题都需要细致的规则设计。目前业界的做法也五花八门有人用纯文本写“用户档案”有人用向量库做相似检索有人上知识图谱。我自己的实践是把三者结合起来结构化的画像用文本/JSON 保存非结构化的事件记录用向量存关系类信息用户A关注商品B、用户C与用户D是家人关系用图来建模。2.4 三种模式对比选型表模式实现成本记忆能力上下文占用信息失真风险适用场景滑动窗口低近程记忆受窗口限制低但有遗漏短对话、FAQ、表单摘要压缩中中程记忆低中摘要有损客服、销售、咨询长期记忆高远程记忆低按需注入低但设计复杂陪伴、教育、个性化助手选型上没有“最好”只有“合适”。我的建议是从业务里最痛的那个点出发如果用户反馈“AI 总忘记我前面说的话”直接上摘要压缩如果用户反馈“AI 一点不了解我”再考虑长期记忆。别一开始就奔着最复杂的方案去系统复杂度上去了bug 和成本也会跟着上去。3. context-mode 的具体工程实现理论说再多最终还是要落到代码。这里我把一个混合型 context-mode滑动窗口 摘要压缩 简单记忆的工程实现拆开讲。这套代码我跑过多个项目逻辑上做了简化但框架可以直接复用。3.1 第一步先管好 token 预算任何 context-mode 的起点都是 token 预算。不估算 token 盲目拼上下文生产环境必炸。OpenAI 系的模型可以用 tiktoken 做精准计数开源的 BGE 家族或者其他模型可以用 AutoTokenizertransformers来算。我的建议是所有上下文相关逻辑统一封装一个估算函数不要在生产环境里用 len(text.split()) 这种粗糙办法中文场景尤其不准。import tiktoken def estimate_tokens(text: str, model: str gpt-4o) - int: 估算一段文本的 token 数用于上下文的预算控制。 注意不同模型的 tokenizer 可能不同切换模型时务必同步切换。 encoding tiktoken.encoding_for_model(model) return len(encoding.encode(text)) def trim_to_budget(text: str, budget: int, model: str gpt-4o) - str: 把文本裁剪到指定 token 预算内。注意裁剪不要切在 token 中间 这里用迭代逼近来保证语义相对完整。 encoding tiktoken.encoding_for_model(model) tokens encoding.encode(text) if len(tokens) budget: return text return encoding.decode(tokens[:budget])token 预算要留余量。假如模型窗口是 8K我不会把上下文填到 8K。原因很简单模型还要在剩余空间里生成回复生成内容本身也要占 token。我一般把上下文预算设为窗口的 75%-80%剩下的空间留给系统提示语和模型输出。具体留多少取决于你期望的回复长度回复越长预留越大。3.2 第二步滑动窗口的落地代码滑动窗口的本质是“如何从完整消息列表里选出一批消息”。这里有几个容易踩坑的细节system 消息永远保留、最新的消息优先保留、单条消息如果超出预算需要单独处理另外一个容易忽略的是——你给模型看的上下文里不能只塞消息正文角色标记还要带上。from typing import List, Dict, Any def build_sliding_window_context( messages: List[Dict[str, str]], max_context_tokens: int 6000, model: str gpt-4o, reserve_tokens: int 1500, ) - List[Dict[str, str]]: 构造滑动窗口上下文。 - 保留所有 system 消息 - 从最新消息开始往前累计直到预算耗尽 - reserver_tokens 为模型回复预留的 token 空间 budget max_context_tokens - reserve_tokens system_messages [m for m in messages if m[role] system] history_messages [m for m in messages if m[role] ! system] selected: List[Dict[str, str]] [] used 0 for msg in reversed(history_messages): cost estimate_tokens(msg[role] msg[content], model) if used cost budget: # 如果单条消息超预算做截断处理这是最后手段 if not selected: trimmed_content trim_to_budget(msg[content], budget, model) selected.insert(0, {role: msg[role], content: trimmed_content}) used budget break selected.insert(0, msg) used cost return system_messages selected几个容易被忽略的实现细节。第一消息里的 role 字段也要计入 token 消耗别小看这一两 token消息多了也是成本。第二当一条历史消息本身就超过剩余预算时不要简单丢弃它——用户刚发的这条消息如果丢了模型就完全不知道用户在说什么了激进的做法是截断处理或者干脆把预算放宽到至少能容纳最后一条消息。第三这个实现里没有处理“消息内容为空”的情况实际项目中要加个过滤避免空消息影响 token 计算。3.3 第三步加摘要与长期记忆的分层方案只有滑动窗口的 context-mode 是“残缺”的因为它的记忆覆盖范围太短。我在生产项目里用的是一套三层结构第一层恒定层system prompt 里的产品规则、人格设定、全局知识。这个基本不变。第二层摘要层之前多轮对话的压缩摘要每 20 轮或每超预算一半时刷新一次。摘要内容不是简单“总结对话”而是按业务维度提取用户说了什么需求、给出过什么关键信息、做过什么决定、还有哪些未完成事项。第三层短期层最近几轮原始对话。和滑动窗口方案一致只是窗口可以调小因为前两层已经承担了记忆职责。def build_hybrid_context( user_profile: Dict[str, Any], conversation_summary: str, recent_messages: List[Dict[str, str]], max_context_tokens: int 6000, model: str gpt-4o, ) - List[Dict[str, str]]: 混合型 context-mode 拼接 1. 用户画像 - 恒定层 2. 历史摘要 - 摘要层 3. 最近对话 - 短期层 顺序不能乱模型对不同位置信息的敏感性不同。 system_parts [] if user_profile: profile_text 用户画像 , .join( f{k}是{v} for k, v in user_profile.items() ) system_parts.append(profile_text) if conversation_summary: system_parts.append(历史对话摘要 conversation_summary) system_prompt \n.join(system_parts) if system_parts else 你是智能助手。 system_msg {role: system, content: system_prompt} # 短期层直接复用滑动窗口逻辑 return build_sliding_window_context( [system_msg] recent_messages, max_context_tokensmax_context_tokens, modelmodel, reserve_tokens1500, )摘要生成我单独写一个函数并且加了两个约束。第一个约束是“摘要必须用事实性语言不准带模型自己的推测”。第二个约束是“保留数字、日期、商品名等精确信息”。当时加这两个约束是因为遇到过模型在摘要里写“用户好像对价格有疑问”但这种含糊表述后续根本没法用——你要么花额外 token 去猜要么就丢失了“价格敏感”这个关键标签。def generate_conversation_summary( messages: List[Dict[str, str]], model: str gpt-4o ) - str: 把一段对话压缩成结构化摘要。 关键明确告诉模型要保留哪些信息宁可长一点不要丢关键事实。 dialog_text \n.join( f{m[role]}: {m[content]} for m in messages ) prompt ( 你是对话记录员。请把下面的对话压缩成结构化摘要要求\n 1. 只记录客观事实不输出主观评价\n 2. 必须保留用户的需求、提供的具体信息订单号/日期/数字/产品名、明确表达过的偏好、未完成事项\n 3. 输出格式按‘事实-要点’分组每行一条不要编造原文没有的信息。\n\n f对话内容如下\n{dialog_text} ) # 实际调用你选择的模型接入层 response call_llm(prompt, modelmodel, temperature0.2) return response.strip()这个函数有个前提摘要生成本身也要消耗 token 和时间。所以触发策略很关键。我在项目里用的是双重条件历史对话轮数超过 N 轮或者历史消息总 token 超过预算的一半就触发摘要生成并且把旧的摘要和新的对话合并后再生成新摘要而不是每次都从零开始。3.4 关于缓存与性能的补充context-mode 做复杂了之后性能问题会暴露出来。尤其是摘要生成和 token 重算每次请求都做一遍延迟会很难看。我的做法是加两层缓存。第一层是 token 计数缓存——同一段文本的 token 数量不会变计算一次后用 content hash 做 key 存起来直接省掉大量重复计算。第二层是摘要缓存——同一批历史消息不做重复摘要。通常在对话流的每一轮结束时异步把“本轮新增消息 上一版摘要”提交给摘要模型生成新摘要并存储。下一轮请求直接读取缓存不用实时计算。这样核心链路的延迟几乎不受摘要影响代价是要多维护一个存储但用 Redis 就够。4. 按业务场景配置 context-mode 的实战参考框架有了但具体项目怎么配这一节按业务场景给配置参考。注意这些配置来自我的项目实践和同行的公开经验不是标准答案。你是要根据自己产品的对话特征做微调的。4.1 客服问答场景客服场景的特点是对话轮次不长、信息精度要求高、用户情绪需要照顾。上下文的配置重点不是“记住多少”而是“不要把关键信息弄丢”。system prompt 里保留产品规则、退换货政策摘要、客服话术红线。滑动窗口保留最近 8-12 轮覆盖一次完整交互基本够用。摘要层只需要做一件事把用户的历史工单记录、历史投诉点、会员等级压成几句话。这个场景尤其要注意“数字精度”。用户报的订单号、金额、日期一旦被摘要模型模糊化后续处理全是坑。我的处理是在摘要生成 prompt 里明确列出“必须原样保留订单号、金额、日期”并在注入上下文时把新鲜对话里的数字信息用正则抽出来单独放进一个“关键信息”区不让它们躺在对话流里随窗滑动丢出去。4.2 编程助手场景编程助手和客服差别很大。用户会粘贴大量代码片段对话本身往往长达几十上百轮而且中途常常切换任务刚从重构函数切到排查 bug又切去写测试。这个场景我反而建议少用摘要压缩。代码语义对精度极其敏感摘要一旦曲解了逻辑比如把变量名替换错了、把函数签名简化错了模型后续生成的代码大概率是错的。宁可窗口滚大一点用原始代码做上下文也不要贪 token 省成本。长期记忆在这里倒是很有价值。可以记录用户项目的技术栈、偏好“用户喜欢用 Python 的 dataclass 而不是 dict”、常见错误模式。这些信息以“编码规范附加说明”的形式注入 system prompt对生成质量的影响立竿见影。窗口配置上代码片段天然占 token 多我会把单条消息的预算放宽但控制消息条数比如最多 15 条避免上下文变成一锅粥。4.3 角色扮演与陪伴场景这类场景对“记忆连续性”的要求是最高的。用户可能上一周说“我最近在准备考研”这周就聊“今天模拟考心态崩了”。如果 AI 忘了考研这回事用户会瞬间出戏。这里的 context-mode 配置是摘要是绝对主力短期窗口只要 4-6 轮保持对话自然感就够了。长期记忆模块要做厚用户表格式的画像职业、家庭、兴趣爱好、最近情绪状态必须有专门的结构化字段不能只在摘要里带一笔。角色设定AI 扮演的人设属于恒定层每一次请求都要完整携带不能省。我用过一个技巧在记忆模块里维护“情感状态时间线”。每次对话结束后模型判断用户当前情绪正/中/负并记录触发原因。下次对话时如果近 3 次对话都是负面情绪模型会在回复中加入安抚性的表达。这种细节的“记忆感”比记住十个事实更有温度。4.4 场景配置速查表场景窗口轮数摘要频度长期记忆关键注意点客服问答8-12每轮异步可选保数字精度防情绪冲突编程助手10-15不使用强推荐保留原始代码不压缩角色陪伴4-6每次重大转折必须画像结构化情感时间线文档分析按文档分块不用可选用 RAG 按块检索不做全局摘要文档分析场景补一句。用户上传一个 200 页 PDF 时你不可能把它整个塞进上下文。正确思路是把它切成块、建索引然后按当前问题检索出相关块再拼入上下文。这也是 context-mode 的一种——“检索式上下文模式”。partial RAG 和 long-term memory 底层是同一套东西差别只在数据源和写入逻辑。5. 我在 context-mode 上踩过的坑与排查实录讲完方案分享一些真实踩过的坑。这些都是线上事故级别的教训写出来希望你能避开。5.1 坑一摘要里的错误信息被模型当成“事实”反复使用这个坑发生在一次客服机器人迭代里。用户先咨询了“A 商品的退货政策”后来改口说“不退了想换 B 商品”。对话摘要第一次生成时是正确的但结合上一条摘要再次压缩时模型把“用户要退 A 商品”和“用户想换 B 商品”混在了一起生成了“用户要求退换商品”——看似没错实际模糊掉了用户的关键意向。后果是后续客服话术一直围绕退货展开用户怎么解释都拉不回来。排查耗时很久最后在日志里对比了多版本摘要才定位。解决办法给每一条摘要加上置信标记和时间戳摘要生成后用正则/关键词检查是否有“歧义句式”比如“退换”这种多义组合词触发二次确认。更重要的是我把摘要模型 temperature 调低了减少它“发挥”的概率。5.2 坑二上下文拼接顺序导致用户指令覆盖系统指令prompt 的顺序问题比很多人以为的更严重。有一次我调整了上下文拼接逻辑把滑动窗口消息放在了 system prompt 之前。结果用户一个略带攻击性的问题居然把系统指令顶掉了——模型开始按用户的口吻说话而不是按设定好的客服人设。原因是模型对文本序列最后位置的注意力权重更高。当用户消息在上下文中“压过” system prompt 时模型的指令遵循优先级会被带偏。解决办法有两个层面一是结构上保证 system prompt 永远在用户消息之前二是用 XML 标签把各段上下文隔开让模型更清楚信息的边界。推荐这个模板system.../systemcontext历史摘要.../contextuser.../user实测对模型理解指令边界有明显帮助。5.3 坑三滑窗太短导致模型“失忆”并自我重复这个坑出现在一个陪伴类产品上。我把聊天窗口从 12 轮调到 6 轮之后用户反馈“AI 突然变得很傻同样的建议给了一遍又一遍”。查日志发现模型在每轮都只看到最近 6 轮的内容它不知道自己已经在 10 轮前推荐过“去看心理咨询师”于是每次听到情绪低落话题都会再次给出同样的建议。这类问题用摘要压缩就能解决——把更早轮次里“模型给出过什么建议”记入摘要后续轮次就不会重复推荐。另外我加了一个内存变量在逻辑层记录“最近 3 次已给出的核心建议”拼接上下文时如果检测到模型即将再次推荐相同内容就手动注入“你之前已经推荐过 X用户未接受请换一种角度回应”的提示词。5.4 我的排查三步法context-mode 出问题特征往往隐蔽不像普通 bug 那么好定位。我总结了三个排查步骤你可以直接拿去用。第一步拉全量上下文日志。每一轮请求发出前把最终拼装好的上下文完整记录下来系统提示语、摘要、消息列表、token 预算。线上问题出现时这组日志就是你唯一的线索。注意给日志加唯一 request_id不然多轮排查会把你绕晕。第二步回放对比。把同样的问题用不同的 context-mode 配置去回放。比如线上是“滑动窗口 8 轮”你离线改成“摘要压缩 最近 4 轮”看输出是否改善。这一步能快速定位问题是出在“信息缺失”还是“信息错误”。我说个捷径准备一个自动化回归集里面放 30-50 个典型用户问题每次改动上下文逻辑就整体跑一遍比人工点半天效率高太多。第三步检查 token 分布。打印每一段上下文的 token 占比。如果某段摘要占了 70% 的预算但输出的关键信息很少那这段摘要就是优化对象。生产上我见过最离谱的情况一份 300 字的用户画像反复拼接了 20 遍占了 8K token——bug 出现在一个 for 循环里把画像加到了每轮消息中。这类问题看一眼 token 分布立刻就能发现。最后聊一点我自己的体会。做 context-mode 越久越发现它的核心不是技术而是产品判断。你要清楚你的 AI 产品到底需要记住什么、忘记什么。记住一切是灾难忘记一切是智障平衡点在哪里取决于你的用户是谁、他们用你的产品做什么。技术只是实现平衡的手段。如果让我给一条最想分享的经验那就是context-mode 的代码写出来不难难的是你为它设计的那套“记忆边界”。每次迭代上下文策略之前先问问自己——这一版是让用户感觉到“AI 更懂我了”还是只是让你自己的 token 成本降下来了。这两者不矛盾但排序不能反。另外无论你最终选了哪种模式务必从第一天就维护好回归测试集。上下文逻辑是 AI 应用里最容易被改坏的模块一个看似微小的调整比如窗口从 8 轮改成 10 轮都可能在某个用户的超长对话里引爆一个从未出现过的问题。有测试集兜底你才能放心大胆地迭代。