☰
大模型上下文管理实战:context-mode五种模式与选型指南
2026/10/8 0:36:26 网站建设 项目流程

很多做 AI 应用的朋友第一次听到“context-mode”(上下文模式)时,第一反应是:这不就是“把聊天记录传给模型”吗?还真不是。把历史记录一股脑塞给大模型,是最粗暴也最容易翻车的做法。context-mode 真正要解决的是:在有限的上下文窗口内,用一套可控的策略决定“哪些历史必须保留、哪些可以压缩、哪些干脆丢弃、哪些需要检索回来”,从而让模型既记得住该记的,又不会被垃圾信息带偏。这篇文章我直接结合自己做客服机器人和智能体项目的经验,把 context-mode 的几种主流方案、选型思路、可复用的实现代码以及踩坑记录一次性讲清楚。适合正在做 LLM 应用开发、聊天机器人、Agent 工作流的工程师参考。

1. 先弄清楚:context-mode 到底在解决什么问题

1.1 大模型天生“记不住”之前的对话

先打一个比方。大模型本身就像一个只有 30 秒记忆的专家,你每一次提问,它都像是第一次见到你。它之所以能聊出“连贯”的感觉,全靠调用方(也就是你)把之前聊过的内容,以文本形式重新喂给它。所以所谓“上下文”,本质上不是模型自己记住的,而是你的程序每次请求时带进去的“小抄”。

这个小抄是有容量上限的,也就是模型的上下文窗口(Context Window)。比如 128K 窗口的模型,最长能处理大约 128K 个 token 的输入加输出。一旦超过,要么直接报错,要么被静默截断。更麻烦的是,就算没超,你也不希望把 10 万 token 的闲聊内容全部塞进去,因为这样又贵又慢,还会让模型抓不住重点。

1.2 上下文管理的三大痛点

我在实际开发里,几乎每个项目都会撞上这三个问题:

第一个是窗口溢出。用户多聊几轮,加上系统提示词、工具返回结果、历史消息,轻轻松松顶到模型上限。这时候接口直接抛错,对话体验瞬间断裂。

第二个是成本失控。大模型 API 按 token 计费,输入 token 数会随着历史消息的增长而线性膨胀。一个 20 轮左右的对话,历史消息可能就吃掉几千甚至上万 token。如果用户量大,这笔账非常吓人。

第三个是相关性稀释。模型对输入中不同位置的注意力并不均匀,而且大量无关内容确实会干扰它的判断。典型情况是,用户在第 3 轮说了一个关键要求,到第 20 轮时那句要求已经被淹没在 80 条消息里,模型就像在垃圾堆里翻找纸条,找不找得到全看运气。

1.3 context-mode 的定义与价值

所以 context-mode 不是某一个固定算法,而是一整套上下文管理策略。它要回答四个问题:保留什么?丢掉什么?压缩成什么?需要时怎么找回?把这四个问题设计好了,才能让模型在有限窗口里始终拿到最高价值的信息。

这个“模式”之所以值得独立出来设计,是因为它直接决定了应用的上限。同样一个模型,上下文策略做得好的应用,能够稳定处理 100 轮以上的复杂对话;策略粗糙的,可能聊到第 10 轮就开始“失忆”或者“发疯”。本文后面聊的模式,就是针对不同场景给出的可落地方案。

2. 主流 context-mode 类型盘点:各有各的生存场景

2.1 固定窗口模式(Fixed Window)

最朴素的方案:只保留最近 N 轮或者最近 N 个 token,其余全部丢。代码写起来最简单,性能也最稳。比如max_turns=10,那就只把最后 10 条消息拼进 prompt,其他什么都不管。

优点非常明显:策略简单、token 开销可控、不会意外触顶。缺点是太“健忘”,如果用户在第 2 轮说了关键背景,聊到第 15 轮时模型已经完全不知道这件事了。

我自己的经验是,固定窗口适合两类场景:一类是轻量级的单轮问答增强(比如把上一句也带上,让追问更自然);另一类是早期原型验证阶段,先跑通流程,后面再优化记忆。如果你做的是严肃的多轮业务系统,固定窗口一般只能当“保底方案”。

2.2 滑动窗口模式(Sliding Window)

滑动窗口可以理解为固定窗口的升级版。窗口大小不变,但随着对话推进,窗口整体向前移动。比如窗口大小是 20 条消息,第 25 条到来时,保留的就是第 6 到第 25 条。

它和固定窗口的核心区别在于:固定窗口通常以“最近 N 轮”为单位硬切,滑动窗口则更强调连续的片段保留,在实现上往往按 token 数或消息数动态计算,并且可以结合消息的“角色”做取舍,比如系统消息永远不被滑出窗口。

我建议在需要多轮工具调用或者分步推理的场景优先考虑滑动窗口。例如 Agent 在执行任务时,中间的过程记录(调用了什么工具、返回了什么结果)往往比用户开头的那句“帮我查一下”更有价值。滑动窗口能保证最近的过程信息始终在场,同时控制总量。

2.3 摘要压缩模式(Summarize & Compress)

当对话长到滑动窗口也兜不住的时候,就该请摘要出场了。核心思路:对较早的历史对话生成一段摘要,然后用“摘要 + 最近原文”替代“全部历史原文”。

这里有个关键点:摘要本身也是 token,而且每次对话后摘要可能还要更新。所以更稳妥的是“分层摘要”方案。第一层,把每 5 到 10 轮对话浓缩成一小段;第二层,当这些摘要足够多时,再对摘要做更高层的浓缩。最终送入模型的,是最顶层摘要加最近一层的摘要,再加上最近的原始消息。

我在实践中的配置是:摘要触发阈值设为“原始消息总 token 超过上下文窗口的 40%”,一旦超过就触发一次压缩。压缩后,“旧历史摘要”与“新近原文”的 token 占比大约控制在 1:2,给模型留足输出空间。摘要模型可以直接复用主模型,但如果你对成本敏感,可以考虑用更小更便宜的模型来承担摘要任务,质量会有一定下降,但我实测多数场景影响不大。

2.4 记忆增强模式(Memory & Context)

摘要是对信息的“广撒网浓缩”,记忆增强则是“定向提取”。它从历史对话里抽取结构化的关键事实,比如用户的偏好、明确提出的要求、已确认的事实数据,存入一个记忆库。之后每次构建上下文时,从记忆库里取相关条目,拼到系统提示词中。

举个例子,用户说“我平时都喝无糖拿铁,大杯”,你抽出的记忆条目就是“口味偏好:无糖拿铁(大杯)”。下次对话时,模型直接就知道该推荐什么,而不用从头翻记录。

记忆增强和摘要最大的不同在于:摘要保留“过程信息”,记忆保留“事实信息”。两者可以叠加使用,这也是我比较推荐的一种组合。长期陪伴型应用(虚拟角色、私人助手)、需要记住用户画像的推荐系统,都特别适合引入记忆增强。实现时,抽取可以用模型加结构化输出,也可以用规则加正则,如果需求简单,先用后者跑通,成本更低。

2.5 检索增强模式(RAG Context)

如果历史信息量大到摘要也撑不住,那就不要“全部带上”,而是“按需检索”。RAG Context 的思路是:把所有历史消息切块、做向量化,建立索引;用户每次提问时,把当前问题作为查询,检索出最相关的历史片段,再组装进上下文。

其实这也是 RAG(检索增强生成)在对话上下文管理中的应用。它特别适合知识库问答场景:你有一个几百万字的产品手册,用户问了一个具体问题,你不需要也不可能把整个手册塞进上下文,只需要检索出相关的几段就够了。

这里有个实践教训:检索片段之间的“衔接”很关键。如果只简单返回几段不连续的内容,模型可能看不懂它们之间的关系。我通常会在检索结果前后加上说明性文字,比如“以下内容来自知识库,可能与用户问题相关”,并保留每一段的来源和标题,这样模型能理解这些片段是独立的知识条目,而不是一段完整对话的碎片。

2.6 五种模式对比

我把五种模式放在一起对比,方便你做初步选型:

模式核心策略优势劣势典型场景
固定窗口只留最近 N 轮实现简单,成本低早期关键信息易丢失轻量问答、快速原型
滑动窗口窗口移动,保留连续片段保留最近过程,信息连续仍会丢弃早期信息多轮工具调用、Agent 任务
摘要压缩旧历史摘要化支持长对话,信息覆盖广摘要丢失细节,额外开销长时间深度对话
记忆增强结构化抽取关键事实精准保留用户偏好,个性化强需要抽取策略,无法覆盖全过程陪伴型、画像型应用
检索增强按需检索历史/知识库可处理海量信息,相关性高引入向量检索的复杂度知识库问答、超长资料

3. 从四个维度判断你该选哪种模式

3.1 对话轮次与时长

如果你的应用平均对话只有 3 到 5 轮,固定窗口完全够用,没必要为了“以后可能变长”提前上复杂度。如果你的应用动辄就是 50 轮以上的连续交互,那滑动窗口加摘要就是标配。这里有一个经验值:平均轮次超过 15 轮,就要考虑摘要或记忆了;超过 50 轮,建议上记忆增强加分层摘要。

3.2 信息依赖强度

你需要判断:对话早期产生的信息,对后面的回答到底有多重要?如果是售后咨询,用户聊到后面可能不会再提“我买的是哪个型号”,但你后台其实可以主动注入订单信息,那就不要依赖上下文保留;如果是项目协作助手,用户在开头描述的需求直接决定后续所有建议方向,那早期信息就必须以某种形式留存,这时候摘要和记忆就很重要。

3.3 成本敏感度

token 成本是硬约束。固定窗口和滑动窗口成本最可控;摘要压缩模式每一次压缩都是一次额外的模型调用,虽然能降低后续主请求 token 量,但压缩本身要花钱;记忆增强的抽取也是额外调用;检索增强则需要承担向量化费用和向量库运维成本。我的建议是:先算一笔账——如果每天 1 万次请求,单次主请求节省 2000 token,按当前主流模型价格,一天能省下多少;如果省下的钱远大于压缩/检索的额外成本,那升级就有明确价值。否则别为了“技术先进”而做,做产品要算账。

3.4 实时性要求

对话类应用对延迟很敏感。检索增强模式如果命中不好,可能还需要重试,延迟会明显升高。摘要压缩模式在长对话中出现“卡一下”的感觉,也多半是压缩触发了额外模型调用。我的做法是:把压缩和抽取放到后台异步执行,用户端对话继续用旧的上下文,等压缩完成后再切换,这样用户无感知。

3.5 组合使用的参考方案

应用类型推荐组合
客服机器人(3~10 轮)滑动窗口 + 订单/用户信息固定注入
客服机器人(长会话)滑动窗口 + 摘要压缩 + 订单信息注入
个人助理 / 陪伴角色记忆增强 + 摘要压缩
知识库问答检索增强 + 固定窗口,两者各司其职
复杂 Agent 任务滑动窗口 + 关键信息记忆增强

4. 实操落地:一个可复用的 context-mode 管理模块

4.1 整体设计思路

我建议把上下文管理独立成一个模块,不要和业务代码揉在一起。模块内部可以分四层:

  • 原始消息队列:保存完整的对话原始消息,带时间戳。
  • 压缩层:负责触发摘要压缩,生成和更新摘要块。
  • 记忆层:负责抽取结构化记忆条目,存入记忆库。
  • 组装层:负责按当前模式把原始消息、摘要、记忆、检索结果组装成最终发送给模型的 prompt。

每一层都可以独立开启或关闭,这样线上通过配置就能切换模式,不用改代码。

4.2 核心代码实现(Python 伪代码风格)

下面这段代码是单机内存版的核心逻辑,一行一行看,不用拷到生产环境,重点是理解数据流和判定条件。

import json import time from typing import List, Dict, Optional class ContextManager: def __init__( self, max_context_tokens: int = 8000, summarize_threshold: int = 6000, window_size: int = 20, mode: str = "sliding+summary" ): # 原始消息队列,全量保存 self.messages: List[Dict] = [] self.summaries: List[str] = [] self.memories: Dict[str, str] = {} # 关键参数 self.max_context_tokens = max_context_tokens self.summarize_threshold = summarize_threshold self.window_size = window_size self.mode = mode def add_message(self, role: str, content: str): self.messages.append({ "role": role, "content": content, "ts": time.time() }) def estimate_tokens(self, text: str) -> int: # 生产环境建议用 tokenizer 精确计算 # 这里给一个粗略估算:中文约 1.5 字符/token return max(1, int(len(text) / 1.5)) def should_summarize(self) -> bool: total = sum( self.estimate_tokens(m["content"]) for m in self.messages ) return total > self.summarize_threshold def summarize_old_messages(self) -> str: # 取最老的一半消息做摘要 mid = len(self.messages) // 2 old_part = self.messages[:mid] old_text = "\n".join( f"{m['role']}: {m['content']}" for m in old_part ) # 实际场景这里调用 LLM 生成摘要 # 这里用简单截断代替,只是为了数据结构演示 summary = f"[摘要占位] 共{len(old_part)}条消息,核心要点:{old_text[:100]}..." self.summaries.append(summary) self.messages = self.messages[mid:] return summary def extract_memories(self): # 从最新消息中抽取结构化记忆 for m in self.messages[-5:]: content = m["content"] # 实际用 LLM 抽取:{"偏好": "...", "要求": "..."} # 这里用规则示例:包含"我"和"喜欢"或"要"的行 if "喜" in content or "要" in content: key = content[:8] self.memories[key] = content[:50] def build_context(self, query: str) -> List[Dict]: if self.mode.startswith("sliding"): selected = self.messages[-self.window_size:] elif self.mode.startswith("fixed"): selected = self.messages[-10:] else: selected = self.messages # 组装 system prompt system_parts = [] if self.summaries: system_parts.append( "以下是较早对话的摘要:" + " | ".join(self.summaries[-2:]) ) if self.memories: mem_text = ";".join( f"{k}: {v}" for k, v in list(self.memories.items())[-5:] ) system_parts.append("已知用户偏好与要求:" + mem_text) messages_for_llm = [] if system_parts: messages_for_llm.append({ "role": "system", "content": "\n".join(system_parts) }) messages_for_llm.extend(selected) messages_for_llm.append({"role": "user", "content": query}) return messages_for_llm # 使用示例 cm = ContextManager(max_context_tokens=8000, summarize_threshold=6000) cm.add_message("user", "我平时都喝无糖拿铁,大杯") cm.add_message("assistant", "好的,记住了,您要无糖拿铁,大杯") cm.add_message("user", "今天天气怎么样") cm.extract_memories() result = cm.build_context("推荐一款适合我的咖啡") for msg in result: print(f"{msg['role']}: {msg['content'][:80]}")

这段代码非常简化,但数据流是完整的:消息先入队,量够大触发摘要压缩,记忆抽取独立执行,组装时把摘要、记忆、窗口消息拼在一起。你真正落地的时候,需要把占位部分替换成真实的 LLM 调用和向量检索。

4.3 关键参数怎么调

max_context_tokens这个参数我建议设置成“模型最大上下文窗口的 60% 到 70%”,需要给模型的输出预留空间。比如模型是 128K 窗口,但你要输出长报告,那输入上下文不要超过 80K;如果只是普通对话,可以放宽到 90K 左右。

summarize_threshold建议设置为max_context_tokens的 70% 到 80%。意思就是,原始消息量快接近上限时,提前触发压缩,而不是等已经顶到窗口上限才处理。太早触发,浪费 token,压缩频率也高;太晚触发,容易在压缩完成前出现一次超限请求。

window_size设置与业务强相关。工具调用密集的 Agent,建议取 30 到 50 条消息,因为每条工具调用消息都很短,但信息密度高;普通客服对话,20 条消息以内就够了。经验法则是:窗口内消息的 token 数不超过max_context_tokens的 60%。

4.4 监控与调试建议

上线后一定要记录三个指标:每次请求的实际输入 token 数、触发压缩的次数、用户侧感受到的回复延迟。你很快会看到,token 数呈现锯齿状——涨到阈值后被压缩压下去,然后又慢慢涨上来。如果压缩过于频繁(比如每 3 轮对话就压缩一次),说明摘要阈值设得太低,或者单条消息太长,需要调大阈值或对超长消息单独处理。

另一个很有用的调试手段是保存“上下文快照”,也就是把每次请求最终发送给模型的 messages 完整地打到日志里,并在开发环境可视化查看。很多问题(比如模型突然丢失早期要求)只要看一眼快照就能定位:原来是早期信息被窗口滑出去了,而不是模型变笨了。

5. 常见问题与排查技巧实录

5.1 模型“越来越蠢”,回答开始偏离主线

这是我在多轮对话里最常遇到的情况,根因通常是早期关键信息被挤出窗口。排查思路:先把上下文快照导出来,看系统提示词里有没有用户最初提出的核心要求;没有的话,说明信息确实丢失了。解决办法分三步:一是把核心要求显式抽取进记忆库,每次组装压到 system prompt 里;二是提高摘要的覆盖质量,不要只留“最近内容”;三是如果业务允许,把用户最初提交的表单信息或订单信息通过接口查询重新注入,而不是指望对话历史替你记住。

5.2 突然报context length exceeded

这种报错往往不是渐进式增长导致的,而是中途一条消息异常地大。比如用户粘贴了一整篇文章,或者某个工具接口返回了全量数据。我的处理方式是给单条消息设上限,超过上限就提前截断或要求工具层先做聚合,不能让一条消息就干翻整个上下文窗口。另外,在组装请求之前先做一次 token 预估,超了就降级(去掉摘要、收窄窗口),宁可牺牲一点质量,也不能直接报错。

5.3 摘要压缩后,关键细节反而丢了

摘要天然会丢细节,这个问题无法完全避免,只能减少。我的方法是在摘要 prompt 里明确要求保留几类信息:具体数字、日期、明确的产品名称、用户的硬性要求(比如“不要推荐辣的东西”)、已定方案和待办事项。我甚至会让摘要模型按固定模板输出,分成“用户要求/已确认事实/未解决问题/其他重要细节”几个区块,这样后续检索时也更方便。

5.4 记忆库里的信息重复、矛盾

同一件事用户可能变了几次说法:“我喝冰美式”,过了几天又说“胃不好,改成喝热拿铁”。如果记忆库不做更新,模型就会发现 system prompt 里两条信息打架。解决方式:在抽取记忆时,每次先检索是否已有同类条目,有的话走“更新”而不是“新增”;并在组装时只取最新时间戳的条目。这个逻辑不复杂,但很多人一开始都会漏掉,结果是记忆越攒越多,模型越聊越乱。

5.5 成本没降反升,原因出乎意料

有的团队在引入摘要模式之后发现账单更贵了,就怀疑上下文管理是骗人的。真实原因通常是压缩触发太频繁,摘要模型调用量过高,而且每次摘要虽然缩短了历史,但摘要本身也在重复生成、重复发送。我建议给摘要结果做缓存:只有旧消息增长达到一定比例(比如 20%)才重新生成摘要,否则沿用上一次的结果。这一个改动通常会省掉一半以上的摘要调用开销。

5.6 一个从零起步的实践路线图

如果你现在还在犹豫,我建议按这个路线走:先用固定窗口上线,同时记录消息增长曲线和用户反馈;当平均对话轮次超过 15 轮时,引入滑动窗口;当固定窗口无法承载早期信息价值时,再加摘要压缩;当开始做用户画像和个性化推荐时,再加入记忆增强;当你有海量知识库时,才是检索增强的主场。别一上来就五种模式全上,那样你根本分不清哪个环节出了问题。

结合我自己的经验,做 context-mode 最忌讳两件事:一是“技术洁癖”,看到别人的架构用了记忆增强加向量检索就觉得也得堆一个,忘了自己的业务根本不需要;二是“伪优化”,一上来就上最复杂的策略,结果优化了半天,连基础日志都没埋点,出了问题无从排查。上下文管理本质上是在做信息降维和选择性保留,它不是越高大上越好,而是越匹配业务越好。先把你自己的对话数据拉出来看看增长曲线和丢信息场景,再选择对应的模式,才是最快的路径。

最后留一个小技巧:把上下文模式做成一个可配置项,存到环境变量或配置中心里,线上出了交互质量的问题,你可以在不改代码的情况下,直接把模式从“sliding”切到“sliding+summary”,对比前后效果。这个配置能力看似不起眼,但在线上排障的时候能救你命。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询