大模型对话上下文模式全解析:从滑动窗口到摘要压缩

发布时间:2026/10/20 16:40:41

大模型对话上下文模式全解析:从滑动窗口到摘要压缩 刚做对话类应用那阵子我遇到过一件特别窝火的事用户上一轮清楚说了“我住上海喜欢口味偏辣”到第五轮我再问“用户住哪儿”模型一本正经回答“用户可能住在北京”。我当时第一反应是模型智商不够后来翻调用记录才发现是我的代码压根没把前几轮内容传给模型。不是模型笨是它压根没“看见”之前的话。这个坑就是 context-mode 要解决的核心问题。白话讲context-mode 就是把“让模型带着上下文回答问题”这件事做成一套可配置、可管理的工程方案。不管你做的是客服机器人、AI 写作助手还是带记忆的 Agent只要涉及多轮对话就绕不开它。这篇内容我会从原理、Token 成本、代码实现到排错经验完整拆一遍我实际走过的路线适合正在做对话应用但被“失忆”折磨的开发者参考。1. 上下文模式的核心逻辑模型为什么记不住事1.1 一次让我深夜改代码的对话翻车现场先还原一下当时的真实对话流程。用户说“帮我找一家静安区的川菜馆”我这边调用了地图接口、筛选了店铺返回了三条结果。用户接着说“要评分4.5以上的”这时候我的代码只把当前这轮请求发给了模型模型压根不知道前面聊的是“川菜馆”于是它反问“请问您想找什么类型的餐厅”从用户视角看这就是产品智障从技术视角看这是典型的无状态调用。大模型接口本身不保存任何历史你每次调用它都是一张白纸它只知道你这次给它的内容。所谓的“多轮对话能力”全部依赖你自己维护历史消息再拼到请求里。这也是 context-mode 最底层的原理上下文不是模型自己记的而是你在调用前手动拼回去的。那次之后我学乖了开始维护一个 messages 列表把用户每次说的话、我每次的回复都追加进去下次请求把整个列表一起发给模型。效果立竿见影用户再问“评分4.5以上”模型马上知道要在川菜馆里过滤评分。但新的问题又来了对话长了以后这个列表越来越大请求越来越慢费用也跟着往上蹿。1.2 无状态模型的记忆补丁上下文窗口的三种玩法要给模型补记忆核心是管理好消息列表。我自己用下来主流做法就三种对应三个档位的 context-mode。第一种是单轮模式每次只发当前输入不做任何历史拼接。好处是省成本、响应快坏处是模型完全失忆只适合翻译、单次改写、意图识别这类一次性任务。第二种是滑动窗口模式固定保留最近 N 轮对话超过的部分直接丢弃。这个最常用实现也最简单。比如窗口设 10 轮第 11 轮进来时第 1 轮就被挤出去。窗口里始终是最新的内容而最近几轮恰恰是对话中最相关的部分所以性价比很高。第三种是全量加压缩模式窗口保留所有历史但超过阈值时把较早的对话用摘要压缩成一段概述再把摘要和历史一起拼进消息列表。这种方式能最大程度保留长期信息但实现复杂度明显上升需要额外一次模型调用来做压缩。三种模式没有绝对的好坏。我做客服机器人时用滑动窗口就够用户一般几轮内解决诉求做角色扮演或私人助理时用全量加压缩因为用户可能三天后回来继续聊不能真的忘掉三天前聊了什么。1.3 什么时候该用哪种模式一张选型表模式记忆能力单次请求成本实现难度适合场景单轮模式无最低极低翻译、改写、单轮分类滑动窗口保留最近 N 轮中低客服、问答、大部分任务型对话全量加压缩长期记忆 摘要高中高私人助理、角色扮演、跨天对话这里有个容易忽略的点滑动窗口虽然只保留 N 轮但你仍然可以把跨越窗口的关键信息单独抽出来塞进 system prompt。比如用户说自己住上海、爱吃辣这种长期偏好根本不用跟着窗口滚掉而是单独存放每次都随请求带上。我把这个叫“用户画像注入”它比纯窗口模式聪明得多成本却几乎没有增加。2. 上下文窗口与 Token 的算账逻辑成本和精度怎么权衡2.1 上下文窗口不等于有效记忆长度模型文档上写着支持 32K、128K 甚至更长上下文很多人的第一反应是“那就把所有历史全塞进去”。真这么做会翻车。我实测下来大部分开源和商业模型的有效注意力都偏向开头和结尾中间一大段很容易被“忽略”。哪怕窗口够大塞得太满反而会稀释关键信息模型抓不住重点。翻译成人话你给模型一篇两万字的对话记录它表面上都“看过”但注意力有限回答时可能只用最后几句话和最初几句中间那些细节基本等于没看。所以 context-mode 不是简单做“加法”而是要刻意控制送进去的内容总量和质量。我自己设过一条经验阈值普通对话场景历史消息总 Token 数控制在窗口上限的 60% 以内留出 20% 给模型输出再留 20% 作余量。比如模型窗口是 32K那历史消息我最多攒 19K 左右超过就触发截断或压缩。这样做是为了防止碰上限时请求直接报错也为了保住响应速度和稳定性。2.2 Token 到底怎么数一个下单场景的账本Token 不是按“字数”计的而是模型分词器切出来的最小单位。中文场景下大约一个汉字对应 1 到 1.5 个 Token英文一个单词平均约 1.3 个 Token。加标点、空格、代码缩进都会增加 Token。举个我看过的实际例子。用户下了一单咖啡消息列表大概是system prompt120 Token用户第一轮“我要一杯冰美式大杯少冰”15 Token助手回复“好的大杯冰美式一杯请问还需要其他吗”20 Token用户第二轮“不要了送到家地址是...”18 Token助手回复“好的预计三十分钟送达”12 Token这五条加起来不到 200 Token看起来不多。但真实对话往往包含搜索结果拼接、商品列表、订单详情有时候光一条工具返回就上千 Token。几十轮下来上万 Token 很正常。而按主流 API 计费输入 Token 同样是按量收钱的上下文越长单次请求成本越高。所以我在项目里加了计数逻辑每次请求前先估算消息列表的 Token 总数超过阈值就走压缩或截断。不要用大模型接口去数直接用本地分词器速度快且不额外花钱。2.3 记忆管理三件套滑动窗口、摘要压缩与向量召回真要在生产环境里做好 context-mode光靠一个方案不够我目前用的是三件套组合。第一件滑动窗口保底。用一个固定大小的队列维护最近对话保证请求体不会无限膨胀。这是一切的基础哪怕后面接摘要和向量也先得有这个边界。第二件关键信息抽到 system prompt。用户说过的重要信息比如偏好、地址、约束条件不在窗口里滚而是抽出来放进系统提示每次请求都带上。这一步成本极低却是解决“失忆”最有效的手段。我用一个简单的规则凡是用户用了“我喜欢”“我住在”“不要”“必须”这些词的句子都拉出来存进用户画像。第三件超长历史做摘要或向量召回。当窗口快满时触发一次摘要调用把旧对话压缩成三到五条精炼信息。再进一步可以把历史消息向量化存进向量库当前问题进来时做相似度召回只把最相关的几条历史拼进请求。摘要适合中等长度向量适合跨海量历史两者也可以配合。这套组合拳打完上下文模式才算真正“能打”。只做滑动窗口用户隔夜回来聊就接不上只做摘要高频非关键信息反而被压缩没了只做向量冷启动时又没有足够历史可用。3. 从零实现一个带上下文模式的对话助手完整实操3.1 技术选型为什么用消息列表加一个记忆管理器实际动手前先解释下选型。我不用重框架核心就是两份东西一份是标准的 messages 消息结构一份是自定义的记忆管理器。消息结构长这样system放系统指令user放用户输入assistant放模型回复。这种格式几乎所有模型接口都认兼容性最好。记忆管理器负责决定每次请求时从全部历史里挑哪些消息拼进 messages。这么设计有三个理由。第一是调试方便请求慢就打印 messages 看内容第二是与模型解耦以后换模型接口不用动记忆逻辑第三是方便做 A/B 测试开窗口和关窗口的效果一目了然。3.2 核心代码滑动窗口版的 context-mode先实现最简单的滑动窗口版代码用 Python主要靠一个 deque 来滚动窗口。下面是核心类我加了详细注释。from collections import deque import time def call_model(messages, max_tokens512, temperature0.7): 调用大模型接口的封装函数。 实际项目里把它替换成对应 SDK 的 chat 方法即可。 messages 是标准消息列表例如 [{role: system, content: ...}, {role: user, content: ...}] pass class SlidingWindowMemory: def __init__(self, max_rounds10, system_prompt): # max_rounds 表示保留多少轮一”轮“指一问一答 self.max_rounds max_rounds self.system_prompt system_prompt self.history deque(maxlenmax_rounds) def add_turn(self, user_content, assistant_content): # 把用户输入和助手回复追加进历史 self.history.append({role: user, content: user_content}) self.history.append({role: assistant, content: assistant_content}) def build_messages(self): # 组装本次请求的消息列表 messages [] if self.system_prompt: messages.append({role: system, content: self.system_prompt}) messages.extend(self.history) return messages memory SlidingWindowMemory(max_rounds5, system_prompt你是咖啡店客服回答简短友好。) user_input 我要一杯冰美式大杯少冰 assistant_reply 好的大杯冰美式一杯少冰还需要别的吗 memory.add_turn(user_input, assistant_reply) # 第二次用户输入带上了第一轮历史 user_input_2 不要了送到家里地址是幸福路88号 memory.add_turn(user_input_2, 好的已记录地址预计三十分钟送达。) # 请求时模型能看到完整上下文 messages memory.build_messages() reply call_model(messages)这里的关键参数是max_rounds。我做过对比测试5 轮窗口比较省成本但用户说得比较零散时容易丢关键信息10 轮是一个比较稳的起点超过 20 轮以后成本上去了响应变慢但体验提升边际效应已经很小。所以别上来就设一个很大的数先看业务平均对话轮数再定。3.3 加上摘要压缩让窗口装下更长的历史滑动窗口有硬伤用户聊到第 11 轮第 1 轮的“地址是幸福路88号”就被挤出去了。这时候要上摘要压缩。基本思路是窗口快满时把最老的一批对话独立拎出来发给模型让它压缩成三条以内的要点然后把这批旧对话替换成一条伪 assistant 消息继续滚动。class SummaryMemory: def __init__(self, max_rounds10, max_summary_len200, system_prompt): self.max_rounds max_rounds self.max_summary_len max_summary_len self.history deque(maxlenmax_rounds) self.summary # 存放旧对话压缩结果 def _summarize(self, old_messages): # 调用模型压缩旧对话这里为示意实际调用需要填充 messages [ {role: system, content: 把下面的对话压缩成要点保留人名、地点、偏好、决定等信息不超过 str(self.max_summary_len) 字。}, {role: user, content: 对话记录 str(old_messages)} ] return call_model(messages) def add_turn(self, user_content, assistant_content): # 当历史接近上限时先把最旧的几条抽出来压缩 if len(self.history) self.max_rounds * 0.8: old list(self.history)[:4] compressed self._summarize(old) self.summary self.summary \n compressed # 从队列头部移除已被压缩的旧消息 for _ in range(4): self.history.popleft() self.history.append({role: user, content: user_content}) self.history.append({role: assistant, content: assistant_content}) def build_messages(self): messages [{role: system, content: self.system_prompt}] if self.summary: messages.append({role: system, content: 早期对话摘要 self.summary}) messages.extend(self.history) return messages这段代码里有个关键决策我把摘要放到了 system 这一条里而不是塞进 user 或 assistant。原因是摘要本质上是“全局信息”它对后面所有问题都适用放 system 里优先级最高模型会优先参考。实际效果比拼在对话中间好很多。阈值怎么设我用的触发条件不是轮数而是 Token 数。窗口内历史消息总 Token 超过阈值比如窗口最大 Token 的 50%就触发压缩这样更精准。上面代码为了直观用了轮数触发生产环境建议改成按 Token 触发。3.4 关键参数配置阈值、温度与输出长度参数配置是我踩坑最多的环节。先列一张我常用的参考表参数推荐值说明max_tokens512~1024模型回答的最大长度。不是越大越好太大会挤占历史窗口空间temperature0.3~0.7客服类用 0.3创作类用 0.7低一点更稳定top_p0.8~0.9和 temperature 二选一调别同时调高历史 Token 占比窗口的 60% 以内超出就触发压缩或截断摘要触发阈值窗口的 50%到了就触发一次旧对话压缩关于 temperature 多说一句context-mode 场景下我建议偏保守。因为上下文管理本身就要模型“忠实利用历史信息”温度太高模型容易自由发挥甚至篡改用户的偏好。0.3 是任务型对话的甜点位0.7 往上就开始飘了。还有 max_tokens 的隐藏坑它不只是限制输出长度如果你把消息列表塞得很满剩余给输出的空间就不足模型可能话说到一半被截断。所以每次请求前务必留出足够的输出余量。4. 常见问题与排查技巧实录四类高频翻车4.1 模型忘了用户刚说的偏好这是 context-mode 最容易出现的问题绝大多数不是模型的问题而是窗口截断了。用户第一轮说“我喜欢偏辣”你们聊了十五轮工具调用和结果确认等到第十六轮模型推荐菜品时“偏辣”已经从窗口里滚出去了。排查方法很简单打印出每次请求的 messages看看说“偏辣”的那条还在不在。解决方案就是我前面说的用户画像注入。规则提取不够可靠我后来改成了模型抽取每隔几轮把全部历史发给模型让它抽取出用户画像字段比如口味偏好、地址、禁忌、身份然后存下来放到 system prompt。这一步单独的调用费用微乎其微但长期效果稳定得多。4.2 明明没超限制却调用失败有段时间我们的服务频繁报错提示请求超出上下文限制但看消息量根本没到窗口上限。最后定位到原因历史消息里夹杂了一条超长的工具返回结果单条消息近 40K Token超过单条消息的长度限制。上下文限制和单条消息长度限制是两回事后者额度往往更小而且容易被忽略。排查时别只算总 Token要逐条检查 message。解决方式也直接工具返回结果在拼进上下文前预先截断比如只保留前 1000 字符或者把工具输出转成精简的结构化摘要。宁可信息少一点也别让单条消息撑爆限制。4.3 对话变长后响应越来越慢上下文越长响应越慢这是线性增长的关系。我做了一个对照2000 Token 的 requests 平均 1.2 秒返回拉到 12000 Token 后平均要 3 秒翻了一倍多。用户感知非常明显。缓解手段首先是给历史设硬上限这是必须做的。其次是摘要压缩把旧对话从几千 Token 压成几百 Token。最后是缓存高频的 system prompt 和用户画像部分可以做缓存处理减少重复计算。我实际优化后平均响应时间从 2.8 秒降到了 1.5 秒。4.4 两个会话互相串台这个 Bug 很隐蔽。当时我们为了省事把所有会话共用一个记忆管理器结果用户 A 的订单地址跑到用户 B 的对话里差点酿成事故。定位过程也很曲折后来打印请求日志才发现 messages 里混了别的会话内容。修复方式很简单以会话 ID 为维度给每个会话单独创建一个记忆实例互不干扰。加一个字典存 session_id 到记忆对象的映射请求进来时按 ID 取用完别共享。这个错误给我最大的教训是上下文隔离是安全底线千万别为了省内存做全局共享。4.5 摘要把关键词压缩丢了最后分享一个摘要的坑。模型压缩对话时为了省字数很容易把“不要香菜”“对花生过敏”这类否定性的关键约束丢掉。我当时排查了很久最后发现摘要里只剩“点了麻辣香锅”用户“不加香菜”的要求被省了。这里加一个保底规则压缩指令里明确要求“必须完整保留否定句、禁忌、过敏信息、地址电话”并在压缩后做一次关键词检查发现危险词才保留原文。成本多一点但安全性和体验都在线。我个人把这条路走通之后最大的体会是context-mode 别一上来就全量加向量检索先做滑动窗口加用户画像稳定跑一段时间再上摘要压缩最后才考虑向量召回。功能复杂度要跟着实际需要走不然多半是给自己挖坑。另外每次请求的 Token 消耗一定要记录我后来加了这个日志才真正看清哪些历史值得留、哪些纯属浪费钱。这个内容后续还可以扩展的方向是让记忆管理器支持按消息维度设置过期时间或者给不同类型的会话配置独立的记忆策略有需要的话我可以把实战代码再整理一份出来。
延伸阅读

更多相关文章

2026/10/20 16:40:41

静态路由从原理到排错:下一跳、管理距离与回程路由全解析

静态路由这四个字,很多刚入行的网络工程师第一反应是“手动指定一条路嘛,太简单了”。但恰恰是这个“太简单”的认知,让不少人栽了跟头。我见过太多生产故障了——下一跳写错导致整个网段不通、回程路由漏配导致业务有去无回、优先级设成默认…

2026/10/20 16:35:38

AI短剧生成的五大物理合规断崖与工业化落地路径

1. 短剧赛道里没人明说的“AI幻觉陷阱”最近帮三个不同背景的团队做过AI短剧全流程陪跑:一个是刚转型的内容工作室,两个是想用AI降本增效的MCN机构。他们提得最多的一句话是:“模型生成的画面很美,但拍出来根本没法用。”不是技术…

2026/10/20 16:35:38

特征提取、特征选择与聚类分析:机器学习数据挖掘完整链路解析

在模式识别与机器学习这条路上,特征提取、特征选择与聚类分析这三个名字经常一起出现,但真要问它们各自解决什么问题,很多刚开始接触的同学都会卡壳。L10这一讲把三个主题放在一起,我最早以为只是课程章节的安排,直到自…

2026/10/20 18:26:50

大模型程序员转型指南:应用开发、微调训练与推理部署全解析

记得去年这个时候,圈子里聊的还是“要不要学大模型”,今年再聊,已经变成“怎么学、从哪儿切入”了。程序员群体对大模型的焦虑,说实话比一般行业更早也更真实——不是因为怕被替代,而是因为我们的工具箱正在被重构&…

2026/10/20 18:26:50

多智能体辩论与红蓝对抗:Agent决策质控的工程实践

我们内部开始认真研究多智能体辩论机制,是因为在模拟项目X里碰到了一次很典型的失败。当时产品负责人提了个需求:把客服售后判责流程交给 Agent 来做。我给一个争议样本跑了一遍,单 Agent 很快给出"不符合换新条件"的结论&#xff…

2026/10/20 18:26:50

C++相场模型模拟金属凝固枝晶生长:方程离散、并行加速与可视化

枝晶这个字,只要搞过材料成型或凝固模拟的工科生都绕不开。焊池冷却、铸锭结晶、增材制造的熔池里,金属从液态到固态那短短一瞬间,微观尺度上会长出一种像蕨类植物一样的树枝状组织——这就是枝晶。很多团队做工艺优化时都想把这玩意算出来&a…

2026/10/20 18:26:50

AI重塑内容生产:从视频号切片到GPT-4商业化的底层逻辑

这两天圈子里最热闹的信息,基本被三件事包了:视频号被曝出在内测一个和内容变现有关的功能,外围讨论直接把它定性为重磅;TikTok那边则悄悄在开发AI头像;微软交出了接入GPT-4之后的首份成绩单。单拎任何一条都能写一篇分…

2026/10/20 18:21:49

Rust供应链攻击的五种隐蔽路径:从名字仿冒到build.rs投毒

先讲一件我最近处理的小事故。某公司一位开发者往内部工具链里加了一个解析JSON的小库,只因为把包名里的连字符写成了下划线,CI里的构建过程就开始向一个从未见过的域名发起外连。幸好该环境配了网络白名单,编译直接失败,才没让那…

2026/10/19 18:06:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/20 1:05:37

单片机C运行时从启动到多任务:中断安全与共享资源冲突

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

GCC编译四步详解:预处理、编译、汇编、链接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:05:37

FIW线是什么?FEP注塑线与普通氟塑料线的区别及应用指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑