LLM上下文模式全解析:从滑动窗口、摘要压缩到缓存与Agent选型

发布时间:2026/10/28 5:47:49
LLM上下文模式全解析:从滑动窗口、摘要压缩到缓存与Agent选型

1. 为什么 context-mode 突然成了开发者绕不开的词

1.1 一个典型的“模型失忆”事故现场

几个月前我在迭代一个客服问答机器人,效果一直不错,直到有一天用户连续发了十二三条消息之后,机器人开始答非所问,甚至把之前已经确认过的订单编号改成了一个根本不存在的号码。我第一反应是模型抽风,查了日志才发现,问题出在上下文管理上——第十二轮对话时,最早的几轮内容已经被悄悄丢弃,模型在只剩半边信息的状态下硬着头皮做了推理。

这个场景应该不陌生。如果你用过任何一个稍微复杂点的 LLM 应用,多半遇到过类似情况:对话一长,模型就像金鱼一样只有七秒记忆。很多人把锅甩给模型本身,但实际上模型没变,变的是你喂给它的上下文是什么、怎么组织的。这背后就是 context-mode——上下文模式——在做决定。过去这个词只在模型推理引擎的内部文档里出现,现在它已经成了 AI 应用开发者在做技术选型时绕不开的考量点。

1.2 context-mode 到底指什么:三层含义别搞混

我刚开始接触这个概念时踩过一个大坑,就是把“上下文模式”和“上下文窗口大小”当成一回事。窗口大小是模型的硬件指标,比如 128K、200K,它决定的是“最多能装多少 token”;而上下文模式是软件层的组织策略,它决定的是“在有限的窗口里,谁优先被装进来、谁可以被丢掉、怎么处理装不下的部分”。

围绕 context-mode 展开,目前业界至少存在三个层面的含义,需要区分清楚:

第一层是推理策略层面,指模型服务端如何调度上下文,比如某些推理引擎会把超长输入按“重要度”粗粒度截断,只保留头尾、压缩中间,这就是一种上下文模式。第二层是框架配置层面,LangChain、LlamaIndex 等框架提供了不同内存类型,比如对话缓冲、滑动窗口、摘要记忆,本质上就是不同上下文模式的封装。第三层是应用协议层面,OpenAI 的 prompt caching、Anthropic 的 prompt caching、各家 SDK 暴露的 cache_control 参数,决定了同一个前缀是否在多个请求之间共享复用,这也被称作上下文模式的一部分。

这三个层面经常被混在一起讨论,但它们的优化目标和注意点完全不同。你调整框架里的 memory 类型,可能并不能改变服务端的截断策略;而你开了 prompt caching,又不代表你的业务逻辑就不用管历史消息的管理。这一章的结论很简单:在开始优化 context-mode 之前,先搞清楚你要优化的是哪一层。

1.3 谁最需要关心 context-mode

如果你的应用只是“问一句答一句”的单轮场景,比如写个翻译工具、做个标题生成器,那 context-mode 基本和你无关,窗口随便够用。但凡是多轮对话、长文档处理、Agent 多步工具调用这三类场景,context-mode 就是核心架构决策,因为它直接决定你的成本、响应速度和回答一致性。

我见过不少团队把大把预算花在购买更大上下文的模型上,结果发现 200K 的窗口也扛不住一天累积的对话量。也有团队为了省 token 把所有历史都截断,结果用户的每一次新提问都变成一场“重生”。这些问题的根源不是窗口不够大、模型不够强,而是上下文模式没有设计好。下面几章我会按“底层策略 — 工具形态 — 选型实战 — 典型坑点 — 调优经验”的顺序,把我自己反复验证过的思路和踩过的坑完整摊开讲一遍。

2. 底层模式拆解:全量、滑动窗口与摘要压缩

2.1 Full Context:最直觉但最昂贵的模式

全量上下文模式是所有方案里最直觉的:把用户与助手之间的每一轮对话、每一份检索到的文档、每一条工具调用结果,原封不动全部塞进 prompt。它的优势一眼就能看出来——信息完整、无截断损失,模型在推理时能够参考到最原始的表述,而不是经过二次加工的内容。

但它的成本增长是线性的,甚至接近指数。假设一次对话平均来回 5 轮、每轮包含用户消息、助手回复、可能的工具调用结果,总 token 数轻松就能上千。如果用户一天进行 50 轮交互,第二天的第 1 次请求就要携带全部历史,几千 token 的开销瞬间变成几万。更麻烦的是,超过模型窗口上限之后,服务端只能暴力丢弃,具体丢哪段不由你决定——我见过不少模型把中间的关键约定丢了,还继续一本正经地回复,这种“不知道自己在丢信息却表现得很自信”的状态,比明确报错难处理得多。

所以全量上下文模式适合的场景很窄:单次会话轮数有限、或者每个请求本身就是独立任务。比如一次性的长文档总结、一次性代码审查,这种场景压根不存在历史累积,全量注入反而是唯一正确解。

2.2 Sliding Window:省 token 但伤连贯性的模式

滑动窗口模式很好理解:维护一个固定大小的窗口,比如最近 20 条消息,旧消息进入窗口后,最老的消息就被挤出去。这种模式在工程上非常轻量,用一个 deque 或者环形队列就能实现,token 开销恒定,响应时间稳定,是目前很多聊天机器人默认采用的策略。

但它有很明显的副作用:当关键信息被挤出窗口后,模型无从知晓。一个具体案例:用户在第 2 轮说过“我的预算是 3 万以内”,第 25 轮问“那我们按刚才说的方案来”,此时第 2 轮的信息如果已经不在窗口里,模型完全不知道“刚才说的”是什么。这就是我在上一章提到的客服机器人事故的根本原因——它并非“模型不行”,而是“记忆结构”不支持长期引用。

滑动窗口的改进版本是“窗口 + 关键信息锚定”:把用户画像、需求约束、决定事项等结构化信息单独抽出来放在窗口之外常驻,让普通对话在窗口中滑动。这样既控制了 token 总量,又保住了核心约束。但这个方案的前提是你得有一个能准确抽取关键信息的前置模块,这本身又是一个不小的工程。

2.3 Summarization 与结构化裁剪:折中路的代价

摘要压缩模式是另一种主流选择:当对话历史超过阈值,用模型把旧历史浓缩成一两段摘要,然后继续以“摘要 + 最近对话”的方式推进。它比滑动窗口保留的信息面广,比全量上下文节省 token,看起来是完美的折中,但实际上暗藏着一个很大的隐患——摘要过程本身是信息有损压缩。

我在实际项目中测过一轮:让模型把 30 轮对话压缩成 150 字摘要,再拿摘要与另一组使用完整上下文的对照组对比,在“用户明确说过不要 X”这类否定信息上,摘要模式经常把“不要”二字丢掉。你的否定指令一旦被摘要吞掉,模型就会沿着错误方向一直执行,而且因为摘要看起来简洁自信,诊断时会非常难发现。

结构化裁剪则是把历史归成槽位:用户偏好、操作结果、待办事项、输出格式要求等等,每个槽位只保留最新状态。它的优点是可解释、可查询,问题在于状态冲突难以处理。举个例子,用户先说“用代码 123”,后来又说“算了换个方式”,如果状态槽只保留最新值,模型无法理解这次变更的原因,后续推理容易僵硬。总结一下:摘要模式适合快速原型、对细节不敏感的场景;结构化裁剪适合有明确状态机的业务系统,两者都不是万能解。

3. 工具链中的 context-mode:从框架配置到 API 参数

3.1 LangChain 的 memory 家族与隐藏语义

大多数人第一次接触 context-mode,都是从 LangChain 的 memory 模块开始的。这个框架提供了一整套记忆类型,名字看着简单,实际语义差异很大:

  • ConversationBufferMemory:全量保存所有对话,对应 Full Context 模式。
  • ConversationBufferWindowMemory:只保存最近 k 轮对话,对应 Sliding Window。
  • ConversationSummaryMemory:周期性对旧对话生成摘要,对应 Summarization。
  • ConversationSummaryBufferMemory:token 数超过阈值才触发摘要,是摘要和缓冲的混合体。
  • VectorStoreRetrieverMemory:把历史消息向量化,按相关性检索返回,对应 RAG 式记忆。

我在用ConversationSummaryBufferMemory时发现一个容易忽略的细节:它的摘要触发逻辑依赖 max_token_limit 参数,但这个参数的参照物是“所有消息的总 token 数”,不是窗口上限。如果业务上设置窗口上限 4000 token、max_token_limit 却设成 8000,摘要永远不会触发,内存消耗直接超预期。这种“框架帮你做了但没完全做”的中间状态,恰恰是最容易出问题的地方。用框架的 memory 时,建议先打开 debug 日志看实际的 prompt 拼装结果,别只看参数声明。

3.2 OpenAI 与 Anthropic 的上下文缓存对比

框架层管的是业务记忆,但服务端的上下文模式同样值得关注,尤其是各家推出的 prompt caching。OpenAI 的 Context Caching 是按前缀匹配的,这意味着你必须在每次请求中把不变的指令、示例、资料放在 prompt 的最前面,并尽量保持完全一致,任何一处字符变动都会导致缓存 miss。Anthropic 的 prompt caching 则需要显式指定cache_control断点,你可以把“历史摘要 + 业务规则”放在一个断点前,把“最新对话”放在断点后,这样每次请求只有断点后的小段内容重新计费,断点前的内容只需支付缓存读取费用。

有一点在技术文档里很少被强调:缓存的“热度”是有时效的。不是说你配了缓存就每笔都命中,各家都有 TTL,比如几分钟内没有相同前缀的请求,缓存就会失效。如果你的应用是低频运维场景,比如每隔半小时才调用一次,缓存命中率会低到不划算。我在实际测试中对比过两家 API 的计费账单,同一个业务量下,高频短间隔调用有缓存和没缓存的成本差可以达到 6 倍以上,而低频调用差异不到 10%。所以缓存策略的收益,强烈依赖你的请求频率分布,光看宣传材料里的数字很容易踩坑。

3.3 自研 context router:一种工程化尝试

框架自带的 memory 类型和服务端的 prompt caching 解决的是“装什么”的问题,但很多业务还需要一个“谁先谁后、哪些必留哪些可丢”的裁决器,我自己习惯把它叫 context router。一个简单的实现思路是给消息打优先级标签:

系统指令优先级最高,永远不裁;任务相关的中间结论(比如用户确认过的订单号、完成状态)次之;寒暄和重复描述优先级最低,最早被淘汰。每次组装 prompt 时按优先级倒序填充,直到达到窗口预算。这样比固定滑动窗口灵活,也比统一摘要省 token。

实际落地时我是这样做的:

messages = [] priority_map = { "system": 0, "constraint": 1, "task_result": 2, "history": 3, "smalltalk": 4 } budget = 6000 # token预算 # 按优先级升序排列,高优先级在前 sorted_items = sorted(raw_messages, key=lambda m: priority_map[m["kind"]]) for item in sorted_items: item_tokens = estimate_tokens(item["content"]) if budget - item_tokens <= 0: break messages.append(item["content"]) budget -= item_tokens

这只是一个骨架,真正要落地还需要考虑:估算 token 用什么方法(我常按 1 个英文单词约 1.3 token、1 个汉字约 0.6 token 粗算,但中英混合时误差会加大)、超过预算时的降级策略(是截断还是开启摘要模式)等。但思路本身是通用且有效的:context-mode 不只是一个选项,更是一个你可以在代码里显式控制的流程。

4. 选型实战:长对话、文档问答、Agent 任务链的配置差异

4.1 长对话助手:窗口+摘要的混合策略

长对话助手是最典型的 context-mode 应用场景。我在实际项目中最终采用的不是单一模式,而是“分层策略”:最近 10 轮用全量原文,往前 20 到 50 轮用摘要,更早的内容只有在被主动查询时才启用检索。这样既保留了近期对话的细节,又让中期历史保持可理解,成本被控制在固定范围内。

需要注意的代价值:摘要的质量直接决定整体体验。我踩过的一个坑是每轮对话结束都重新生成摘要,导致上下文还短的时候就反复触发大模型调用,延迟和费用同时上涨。后来改成“当历史 token 累积到预设阈值才触发一次摘要生成”,请求频率降到了原来的四分之一。另一个值得推荐的做法是,在摘要文本的最前面放一个“重要约束区”,把这些对话中出现的所有用户硬性要求逐条列出,防止摘要叙事化之后把条件性信息磨平。

4.2 文档问答:RAG 模式下 context-mode 的新角色

文档问答场景有个特殊性:用户的问题往往需要从一段很长的文档里找答案,而文档的大部分内容对当前问题毫无帮助。传统做法是 RAG——先检索相关的片段,再把片段拼进 prompt。在这种模式下,context-mode 的重心从“历史对话管理”转移到了“检索片段与对话历史的混合排列”。

我实测中发现一个实用的交互逻辑:把检索片段和对话历史分块处理,但让对话历史“薄”一些。因为用户的问题通常只依赖最近一两轮,不需要把三天前的闲聊都带进来。有一次我在任务里把 200 轮对话全部塞入 prompt,加上 8 个检索片段,直接把 16K 的预算打满,模型反而被大量不相关内容干扰,回答质量下降。后来我做了两个改动:一是对话历史仅保留与当前问题语义相似的 5 轮,按向量相似度检索;二是检索片段控制在 4 段以内,每段 500 token 左右,多余部分放在 prompt 末尾的“补充资料区”并显式标注“仅供参考,非直接证据”。实测回答准确率提升了约 15%,token 消耗下降 60%。

4.3 Agent 多轮任务:工具调用链的上下文取舍

Agent 场景是我目前接触过的 context-mode 里最复杂的,因为它不只是对话历史的取舍,还叠加了工具调用的输入、输出、状态变化等多层信息。一个工具返回的 JSON 可能就有 2000 token,五个工具轮流调用一轮,单轮上下文就到了 10000 token 以上。

这里的关键取舍原则是:保留结果,放弃过程;保留状态,放弃噪音。工具返回的完整 JSON 里,真正需要保留的往往只有状态字段、关键数据和对后续决策有影响的部分,其余字段是噪音。我在实现工具调用归档时,会让模型对工具返回结果做一个结构化抽取,只保存这个结构进历史,原始输出在必要时才按需回放。比如地图查询工具返回了一长串 POI 列表,我最后只保留“已选择地点:A;备选:前三名清单及评分”,后面模型需要具体地址时再重新查一次。整体 token 开销减少了近一半,任务成功率反而提高了,因为对模型而言,简洁而准确的过程状态比又长又乱的原始输出更容易理解。

5. 上下文模式的坑:我实测中遇到的四个典型问题

5.1 坑一:摘要累积误差让模型“自信地胡说”

摘要压缩模式最大的隐性风险是累积误差。单次摘要的准确率可能是 90%,但摘要之后的历史又参与下一次摘要,错误和遗漏就会像滚雪球一样放大。到第五六次摘要时,原始信息可能已经面目全非,但形式上依然是一段流畅、自信的文本。模型在推理时完全信任这段摘要,就会产生一种“一本正经地胡说八道”的效果,而且错误来源非常隐蔽——从 prompt 里看摘要,怎么看都像是对的。

应对方法有两个方向:一是做“跨版本核对”,不定期把最新摘要与原始完整历史做一致性校验,发现关键信息偏差时主动跳出具条;二是尽量让摘要保留原始短句而不是改写,我实测下来,保留关键原句的摘要比全改写的摘要错误率低了约三分之一,代价是 token 会略高一些。追求意义上的“顺”,不如追求事实上的“准”。

5.2 坑二:缓存热度的黄金时间,很多人根本不知道

前面提到过各家服务商的 prompt caching 有 TTL 机制,这里展开讲一下时限分布:OpenAI 的自动缓存一般是 5 到 10 分钟级别,Anthropic 的 prompt caching 设计为 5 分钟,但实际的 TTL 取决于配置和流量模式,在文档里并不总是明确写清。如果你的请求间隔长于 TTL,那么缓存形同虚设,你为缓存付出的调用管理成本反而是白白浪费的。

我在某个文档问答服务里连续测了三天,把日志按请求时间分组后发现,下午 2 点到 4 点的高峰期缓存命中率最高,上午时段因为请求稀疏、间隔拉长,命中率掉了一半。后来我加了定时预热任务,在重点时段提前发一个“ping 请求”保持缓存常热,才稳定住成本。配置缓存前,先分析你真实的请求时间分布,别让缓存变成摆设。

5.3 坑三:截断位置与指令冲突的隐性 bug

这是我在代码审查场景里遇到的。文档要求模型始终按照 Markdown 表格式输出,我把这个要求放在系统提示词里,结果模型有时候还是输出乱格式。后来排查发现,代码内容太长导致上下文被滑动窗口截断时,窗口尾部的“下一轮指令模板”——恰好包含了“按表格输出”的提示——被切掉了。也就是模型读到的指令是不完整的,它根本不知道自己被要求用表格。

这个问题的本质是:上下文模式的截断逻辑并不会考虑指令的重要性,它只按位置切分。解决方案很直接:把不可截断的指令(系统提示词、固定输出格式、安全约束)挪到 prompt 的最前方,并确保前端 token 计入预算时预留足够空间;同时给窗口滑动留一个“保护区”,让关键指令永远不被挤出界。听起来简单,但很多框架默认的实现并没有考虑这一点,还是按先进先出处理。

5.4 坑四:把系统提示词当作豁免区

不少人以为系统提示词是“永远不会被截断”的,实际上在严格的全量上下文模式下确实如此,但一旦你启用了滑动窗口或摘要压缩,某些框架会把系统提示词和普通历史消息一起封装,超过窗口后同样会被裁掉。即使服务端不会裁,如果你的提示词前缀缓存没有把系统提示词放在固定的最前方,任何动态插入的内容都可能破坏缓存前缀,导致整个缓存失效。

更隐蔽的情况是:系统提示词本身非常长,占掉了大量预算,挤压了真正有用的历史信息。我见过一个团队把整个业务说明手册塞进系统提示词,结果单次请求 token 直接翻了一倍,而模型真正用到的只是其中三段。把系统提示词分成“核心约束”和“参考资料”两块,核心约束常驻,参考资料按需注入,才是合理的做法。

6. 调优经验:把 context-mode 从“够用”拉到“好用”

6.1 分层记忆结构:短期、中期、长期各干各的活

如果让我给一个最小可用的 context-mode 架构打样,我会选择三层结构:短期层保存最近 N 轮原始对话,中期层保存经过裁剪的关键事实表和摘要,长期层用向量库存储可检索的完整历史。

短期层解决“即时连贯”,模型能在最近几轮里找回上下文;中期层解决“跨轮约束”,用户的核心诉求和已确认状态不会因为对话滑出窗口而消失;长期层解决“跨会话记忆”,用户再次回来也能中断后继续。这三层各司其职,不要用一个全量 buffer 试图解决所有问题。实际部署时可以先用最简单的两层(短期 + 长期检索)跑起来,等出现具体短板时再增加中间层,避免一开始就架构过度。

6.2 token 审计:让每次请求的上下文构成可视化

很多时候 context-mode 的“锅”不在策略本身,而在我们根本不知道每次请求里到底放了什么。我在项目里集成过一个简单的 token 审计脚本:每一个请求打日志,记录 prompt 各部分的 token 占比、各段的来源标签、以及是否触发了截断策略。跑了一个星期后,我清楚地看到哪些历史消息占了 40% 的 token 却对回答质量毫无贡献,哪些关键指令占比虽小但位置始终稳定。这个数据帮助我调整了窗口大小和摘要阈值,最终把平均 token 消耗降了 35%,同时把长对话的准确率稳定在 90% 以上。

审计脚本的核心其实就是给消息打标签,然后用现有的 tokenizer(tiktoken、transformers 等都行)统计。有了准确的数据,你做的任何调整就不再是“拍脑袋”,而是可以用 A/B 测试来验证收益。

6.3 一个值得尝试的混合判据

最后分享一个我在多个项目里反复验证过的经验:当你在不同 context-mode 之间犹豫时,不要期待找到“唯一正确模式”,而是设置触发条件来实现动态切换。我的默认策略是这样的:

  • 上下文 token 总量低于窗口预算的 60%:使用全量模式。
  • 超过 60% 但未超过 90%:转为“关键指令 + 最近 N 轮原文 + 摘要历史”的混合模式。
  • 超过 90%:启用长期检索 + 最短摘要模式,强制压缩。

这个 60%/90% 阈值可以根据业务调整,关键是让上下文结构从静态设置变成动态适配。你可以把它理解成“水位线”——低水位时什么都不用做,高水位时才启动压缩机制。这样既避免高频摘要造成的质量和延迟损失,又防止窗口溢出时被服务端粗暴截断。

我在实际使用中的体会是,context-mode 没有银弹,它是一个需要持续观察、持续调整的系统设计维度。与其迷信某个框架的宣传,不如亲手把每一层打开看一眼:prompt 里最终拼了什么、token 消耗在哪里、模型在哪一轮开始犯错。等你掌握了这三个问题的答案,你对上下文模式的理解就已经超过了大多数只调框架参数的开发者。如果你正准备改造自己的 AI 应用,建议先按我上面说的加一个 token 审计脚本,跑一周数据,再回头决定用全量、滑动、还是摘要——那时候你做出的选择,是会经得起线上考验的选择。