
前阵子在帮一个项目接入大模型对话能力时遇到一个特别典型的翻车场景用户连续追问了几个问题之后AI突然开始失忆把前面聊过的内容全忘了甚至开始重复提问。排查到最后发现问题不在模型本身而是我们压根没有把上下文当成一个需要专门处理的问题来对待。后来我仔细研究了一圈才发现context-mode这个词最近在技术社区里被频繁提及本质上讲的就是一套系统化处理上下文的方法论。这篇东西就是想把这套方法论里最核心的东西讲清楚包括上下文窗口的底层逻辑、主流管理策略的取舍以及我自己实现过程中踩过的坑和沉淀下来的经验。不管你是刚开始接触大模型应用开发还是已经在生产环境里跟上下文搏斗过几轮应该都能从这里找到点有用的东西。1. 先搞清楚模型的记忆到底存在哪很多人第一次接触大模型时都有个错觉觉得模型像人一样天生自带记忆。你问它昨天聊过什么它翻翻脑子就能答上来。但实际上大模型本质上是无状态的——每次调用它就像一个考试时临时拿到考卷的考生你的问题就是这张考卷它只根据你当前给的这张考卷来作答完全不记得昨天、上个小时甚至上一秒钟发生过什么。这句话值得反复咀嚼模型自身不保留任何对话历史你每一次请求对它来说都是全新的开始。那为什么你用 ChatGPT、Claude 这些产品时感觉它记得很清楚因为产品层替你做了上下文管理——把之前的对话记录整理后和当前的问题一起打包发给模型。也就是说所谓记忆其实是你主动喂给模型的。1.1 上下文切换的那一瞬间发生了什么我们项目最初接入模型的方式非常粗暴把用户每次的输入直接拼进 API 请求发出去就完事。第一次测试一切正常能回答问题。但用户紧接着追问刚才你说那个方案的缺点是什么的时候模型一脸茫然。因为第二次请求里压根就不包含第一次的对话内容模型根本没看过那段话自然回答不了。这就是 context-mode 要解决的第一个核心问题你需要在每次请求中显式地携带已经发生的对话历史。没有这个携带动作任何基于大模型的应用都只是在做一次性问答而不是真正的对话。1.2 从无状态到有状态的工程改造让应用拥有记忆是一个典型的工程问题。最常见的做法是在服务端维护一个会话对象把每次的用户输入和模型输出都追加进去下次请求时把整个会话记录塞进 messages 数组。from openai import OpenAI client OpenAI() conversation [] def chat(user_input: str) - str: conversation.append({role: user, content: user_input}) response client.chat.completions.create( modelgpt-4o, messagesconversation ) assistant_reply response.choices[0].message.content conversation.append({role: assistant, content: assistant_reply}) return assistant_reply这种最简单的实现就是 context-mode 的雏形用用户消息 助手回复的交替序列来模拟记忆。但当你真的这么跑起来很快就会撞上那个所有大模型开发者都绕不开的墙——上下文窗口是有上限的。2. 上下文窗口的底层逻辑Token、窗口与成本先普及一个概念Token。模型处理文本不是按字来的而是把文本切成一个个 Token中文大概一个字对应一个到两个 Token英文一个单词通常一个多 Token。上下文窗口说的就是模型单次能处理的 Token 总量。2.1 窗口大小不是越大越好现在主流的模型窗口从 8k、32k 到 128k 甚至 200k 都有。乍一看窗口越大越好毕竟能塞进去的内容更多。但这里有几个被忽视的问题窗口越大单次推理的延迟和显存占用越高部分模型的性能在超长上下文中会衰减成本不是线性的超长输入的 token 费用让很多人肉疼我见过一个团队把 128k 窗口当成无限用允许对话历史无限累积。结果是每轮请求的 token 数量越来越大单次调用成本从几分钱飙升到几块钱响应时间也从 2 秒拖到 20 秒体验全面崩盘。2.2 成本模型输入便宜输出昂贵各家 API 的计费逻辑基本一致输入 Token 的价格远低于输出 Token但对话历史全部算在输入里。你每多带 1000 个历史 token就多付一份输入费用。想象一下一个用户聊了 50 轮每轮平均 500 token那你下一次请求光是历史记录就要携带 50000 token。这还没算系统提示词、检索到的参考资料。长此以往单个重度用户的单日成本可能比一个普通用户高出两个数量级。2.3 窗口溢出时的崩溃现场更麻烦的是窗口硬上限。当 messages 数组的总 token 数超出模型窗口后API 直接报错你的服务瞬间从能聊变成不可用。我们第一次遇到这个问题时是在一个周末用户聊着聊着突然收到错误提示排查半天才发现是会话历史撑爆了窗口。那一刻我意识到不做上下文管理应用连基本可用都保证不了更别提体验优化。3. 主流 Context Management 策略三大流派怎么选围绕如何在有限的窗口里保留最有价值的信息业界逐渐形成了三套主流方案。我在不同项目里都试过各自有明确的适用边界。3.1 滑动窗口截断最简单粗暴的方案思路非常直白只保留最近 N 轮对话更早的记录直接丢掉。MAX_TURNS 10 def build_messages(conversation: list[dict]) - list[dict]: return conversation[-MAX_TURNS * 2:]这个方案的好处是零成本、零延迟适合客服问答这类当前问题与远古信息关联不大的场景。但代价同样明显——用户如果在第 5 轮提过一个关键约束第 30 轮想让你基于那个约束做判断模型根本看不见。早期我们就是用这个方案结果用户经常抱怨我说了不要葱你为什么不记得那真不是模型笨是我们把人家的话丢了。3.2 摘要压缩给历史做脱水处理既然完整保留会爆窗口直接丢弃又会丢关键信息那就折中一下把早期对话浓缩成一段摘要保留语义精华丢掉冗余细节。我实现过一个简单的版本每累计 5 轮对话后调用一次大模型把之前的对话总结成 200 字以内的要点然后替换掉原始记录。def summarize(history: list[dict]) - str: prompt 请将以下对话压缩成一段200字以内的摘要保留关键信息\n\n format_history(history) resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: system, content: prompt}] ) return resp.choices[0].message.content这里有一个实操细节摘要压缩本身也是一次 API 调用会产生额外成本和延迟。所以压缩频率不能太高一般建议在对话轮次翻倍时触发一次比如满 10 轮压缩一轮压缩后清掉原始记录再等下一个 10 轮。这种指数级压缩策略在长会话场景里效果很稳单次对话撑到几百轮都没问题。3.3 检索增强RAG需要时再去翻历史这个方案最优雅所有历史记录完整存到向量数据库里每次请求前根据当前问题做一次语义检索只把最相关的那部分历史拽回来拼进上下文。def search_relevant(memory, query: str, top_k: int 5): query_embedding embed(query) return memory.similarity_search(query_embedding, ktop_k)RAG 的优势是理论上无限记忆——只要向量库存得下用户怎么聊都不怕。缺点是工程复杂度直线上升要维护向量化服务、向量库、检索调参还有语义检索本身漏召回的风险。适合那种用户会随时翻旧账的场景比如知识库问答、项目协作助手。3.4 三个方案怎么选一张表说清楚策略实现成本记忆保真度适合场景滑动窗口极低低旧信息全丢一次性问答、客服摘要压缩中等较高有损但保核心长对话、角色扮演检索增强高高按需精确召回知识库、复杂项目我个人建议起步阶段先用滑动窗口跑通业务等用户量上来了、真实对话数据够多了再根据用户反馈决定要不要上摘要或 RAG。步子迈得太大很容易在基础设施上耗死。4. 一个真正能用的 Context Mode 实现分模块设计光说不练假把式。接下来把我目前在一套生产环境里稳定跑了一个季度的实现方案完整拆开。它结合了滑动窗口和摘要压缩是一个混合上下文管理的典型设计。4.1 整体架构设计整个模块分四个层次会话存储层负责持久化所有原始消息存到数据库历史整理层定期对早期消息做摘要压缩上下文构建层每次请求前拼装 messages决定哪些原文保留、哪些用摘要替代交互层面向业务方暴露简单的 send_message 接口class ContextManager: def __init__(self, session_id: str, storage, summarizer): self.session_id session_id self.storage storage self.summarizer summarizer self.summary None # 历史摘要 self.recent_messages [] # 近期原文 def add_message(self, role: str, content: str): self.storage.append(self.session_id, role, content) self.recent_messages.append({role: role, content: content}) self._maybe_compress() def _maybe_compress(self): # 近 10 轮对话触发一次摘要整合 if len(self.recent_messages) 20: old self.recent_messages[:-10] summary_text self.summarizer.summarize(old) self.summary (self.summary \n summary_text).strip() self.recent_messages self.recent_messages[-10:] def build_request_messages(self): messages [] if self.summary: messages.append({ role: system, content: f以下是更早对话的摘要请将其视为已发生的背景信息\n{self.summary} }) messages.extend(self.recent_messages) return messages这个设计里的关键点是把摘要放在 system 角色里而不是 user 角色。因为摘要本质上是背景设定不是用户说的话。如果混在 user 消息里模型会把它当作用户当前输入的一部分可能导致指令优先级混乱。放在 system 里则明确告诉模型这段内容是既成事实。4.2 Token 预算管理算清楚再动手高可靠的上下文管理必须有预算控制。我的做法是给不同内容类型分配固定配额系统提示词恒定占用 500 token历史摘要上限 1000 token近期对话原文上限 3000 token当前输入 最近一轮回复预留 1000 token输出 token 预算保留 2000 token总量控制在 7500 token 以内。这样做的好处是请求的 token 消耗基本稳定不会出现某次突然冲高到好几万的状况成本可预期响应延迟也可控。如果近期对话原文超了预算就把它转给摘要器处理腾出空间。这里有个容易忽略的细节摘要器自身也有输入限制不能把超长文本一次性丢给它要先截断到它能处理的范围内再分段压缩、合并。4.3 摘要的二次压缩解决摘要膨胀问题摘要方案跑了一段时间后你会发现一个新问题摘要本身也在不断变长。第一轮 200 字压了 10 次之后可能变成 2000 字。所以要定期对摘要做二次压缩把多段摘要合并成更高层的总摘要。我的节奏是对话满 50 轮时触发一次总摘要重写。把已有的摘要和最近几轮原文一起喂给模型让它输出一份全新的 300 字以内的总摘要替换旧摘要。这样摘要不会无限膨胀始终压在预算内。4.4 一个必须处理的边界用户明确引用了很久远的信息摘要压缩是有损的用户完全可能问一句你记得我第三次说的那个需求吗。如果那个需求在摘要里恰好没被保留下来就会翻车。我的应对方式是在摘要中刻意突出用户明确表达过的偏好、决定、承诺在压缩提示词里强化这类信息的权重。你在摘要提示词里可以这么写在压缩对话时特别注意保留 1. 用户明确表达过的偏好、限制、否决 2. 双方做出的决定与结论 3. 用户提到过的人名、项目名、关键数字 4. 任何带情感色彩的表述不满、强烈期待等实测下来这样调整之后用户翻旧账的命中率提升明显虽然不能保证 100%但至少在关键信息上不会出错。5. 生产环境里的坑一个比一个实在5.1 上下文污染历史里的无效指令还在发挥作用积累了几百轮会话之后我发现一个隐蔽问题早期对话里用户随口说的一句先别管性能会在后续所有请求中长期存在于上下文中导致模型一直忽略性能优化相关的指令。这就是上下文污染——历史中的噪声信息持续影响当下的判断。解决办法是在摘要阶段做一次指令清洗明确告诉压缩模型历史中的临时性指令、已过期约束、情绪化表达都要丢弃只保留仍然成立的长效约束。这类清洗要写进摘要提示词否则模型默认会照着字面全保留。5.2 角色混用带来的格式灾难另一个坑来自 messages 角色的误用。我见过有人把来自检索的百科资料放进 assistant 消息期待模型记住这些内容。结果模型把这段资料当成自己之前说过的话回答问题时频繁以我前面提到过开头但实际根本没说过。正确做法是用户输入 → roleuser模型回复 → roleassistant外部知识/检索结果 → rolesystem 加括号标注来源压缩后的历史摘要 → rolesystem 加背景信息标注角色语义一旦混乱模型的指令遵循质量直线下降。这个属于那种不遇到一次根本想不到的坑排查起来极为费劲因为表面上什么都不报错就是输出变飘了。5.3 摘要里的幻觉大模型压缩时会自己编内容大模型做摘要时有一个倾向会在信息缺失处用常识脑补。比如对话里提到某个模块叫支付服务摘要器可能自动补成支付服务已完成支付流程优化但原文根本没这个结论。这种幻觉一旦进入长期上下文会像谣言一样反复污染后续所有回答。应对方案有两个一是压缩提示词里强制要求不得输出原文中未出现的信息二是在代码层面对摘要做校验比如做一次幻觉检测比较摘要与原对话的事实一致性。考虑到幻觉检测本身也烧钱我目前采用的是折中方案——在摘要前用规则层把关键实体数字、人名、专有名词抽取出来摘要完成后检查这些实体是否仍在其中丢失的强制补回。5.4 调试上下文问题的最笨但也最有效的方法上下文问题的邪门之处在于同样的输入今天正常明天抽风换个模型版本就变样。这里分享一个我在实践中总结出的调试链路把请求的完整 messages 落盘到日志带上 token 计数和时间戳用户反馈异常时直接在日志里重放这条请求链逐个削减 messages 里的内容段定位删掉哪一段后模型恢复正常——这一段就是污染源根据污染源类型决定处理策略是压缩、清洗还是彻底移除这套链路虽然原始但胜在稳定。AI 应用的 bug 不像传统软件那样有清晰的堆栈可以跟日志重放就是最靠近堆栈的东西。5.5 成本监控不能只看总额我强烈建议给每个会话单独打点统计每轮请求的 token 输入量、输出量、累计消耗。不这样做的话你很难发现异常会话——有的用户可能会连续对话数百轮单个会话的成本顶得上几百个普通用户。有了会话级监控你就能设置预警阈值对极端会话做主动降级处理比如强制触发摘要、限制单轮携带的历史量或者干脆走更便宜的轻量模型。6. 一些进阶方向与个人体会如果你的应用已经跑通了基础的上下文管理后面有几个值得尝试的进阶方向。第一是分层记忆。把记忆拆成长期记忆和短期记忆短期记忆存近期对话原文长期记忆存用户画像、偏好、关键事实。长期记忆用结构化方式存储比如 JSON 或 KV 对而不是全部堆在自然语言摘要里这样按需取用时会更精准。我最近在做的项目就是这个方案效果比纯文本摘要稳定不少。第二是按需更新摘要。现在的策略是定时触发摘要但其实很多对话段落是高度重复的。可以考虑做语义相似度检测发现话题重复时直接合并旧摘要省一次压缩调用。第三是给关键信息打标签。在消息进入存储前先用一个轻量分类器判断消息是否包含需要长期记住的信息是的话直接进长期记忆库不参与滑动窗口的淘汰。这相当于给上下文管理加了一层优先级的维度。最后说一点个人体会。之前踩坑的时候我总在想这些上下文管理的问题是不是等模型窗口变大后就不存在了。后来想明白了窗口再大成本也在涨用户对话的总量也永远在涨。只要模型还是无状态的、按 token 计费的上下文管理就永远是应用层的必答题。与其期待模型厂商解决一切不如尽早把自己这套基础设施建好。哪怕只是最朴素的摘要压缩也比裸奔强得多。上下文这件事本质上是在有限的窗口里做取舍的艺术——你得想清楚什么值得留什么该舍弃然后用工程手段把这个取舍自动化、可预期。想清楚这一点之后后面很多方案设计就顺了。