资讯动态

上下文模式全解析:从滑动窗口到摘要压缩的LLM应用实践

发布时间:2026/10/29 13:45:56 来源:尧图企业网站定制
做LLM应用开发的人应该都有这种经历助手刚开始对话时思路清晰、条理分明可聊着聊着就开始原地打转连自己几分钟前给出的结论都忘得一干二净。你以为是模型不行其实问题大多出在一个东西上——context-mode也就是上下文模式。我最近重构一个客服机器人项目正好把上下文管理这块从头到尾捋了一遍。这篇就专门聊聊我对context-mode的理解、踩过的坑以及一套可以直接拿去复用的实操方案。context-mode这个词在AI应用里其实包含了好几层意思模型层面的上下文窗口有多大、对话层面如何构造上下文、业务层面怎么在有限窗口里塞下最有效的信息。很多团队做AI应用代码写了上千行模型换了好几个效果始终上不去根子往往就是没把上下文模式想明白。这篇文章适合正在做智能客服、知识库问答、Agent类应用的开发者也适合那些“模型调用接得通但产品体验始终差口气”的团队参考。1. 内容整体设计与思路拆解先把上下文模式这件事看明白1.1 context-mode到底指的是什么我见过不少人对上下文模式的理解停留在“把聊天记录拼起来发给模型”这一步。这个理解不能说错但太浅了而且很危险——因为一旦对话变长简单拼接很快就会被窗口耗尽卡死然后各种诡异问题就全冒出来了。真正做AI应用的人需要从三个层面理解context-mode第一层是模型上下文窗口。这是模型的硬性参数决定了它一次最多能看多少token。目前主流模型的上下文窗口从32K到200K不等看起来数字很大但扣除系统提示、知识库检索结果、示例内容之后真正能留给“最近对话历史”的空间并不宽裕。之前我在一个文档问答项目里就遇到过系统提示加检索片段就占了80%窗口的情况历史对话几乎没有空间模型自然“记不住”用户刚才说了什么。第二层是上下文学习。这是提示词工程的核心通过有选择地在上下文中放入示例、背景、约束条件让模型在推理时有更充分的依据。这和传统训练的权重更新是两码事它不改变模型参数只改变模型“看到”的内容。第三层是上下文管理策略。这是应用层该做的事情也是我最想展开讨论的部分如何在有限的窗口里组织信息、压缩历史、控制成本让模型在每个时刻看到的信息都是精炼、有序、足以回答当前问题的。很多项目把三两层混为一谈觉得“反正模型窗口够大全塞进去就行了”结果既要多花钱又没换来更好的回复质量。理解这三层之间的差别是做上下文模式设计的第一步。1.2 为什么上下文管理成了躲不开的刚需一句话因为模型没有无限记忆而业务需要长期记忆。拿客服机器人来说用户可能先问“我上个月订单为什么还没发货”然后聊到“怎么申请退款”再因为心情不好追问“你们平台到底靠不靠谱”。这几句话之间有关联吗有关联。如果机器人完全不记得前面的内容它就无法理解为什么用户的语气越来越差也无法针对“退款”给出真正贴合情境的回答。如果完全不管理上下文最省事的办法是把所有历史消息一股脑全传给模型。但这样干会撞上三个现实问题成本。所有token都会计入计费对话越长费用越高。客服机器人一天几万次会话如果每次会话都拖着完整的长历史账单会非常惊人。曾经有个项目上线半个月模型调用费用比预期高了四倍查下来就是历史消息无脑全量携带。卡顿与超时。窗口越大、token越多模型首字等待时间就越长。用户那边等着机器人回复背后却因为塞了太多历史消息迟迟出不来结果体验直接崩掉。注意力被稀释。模型并不会因为上下文变长就变得更聪明反而容易被无关的历史信息干扰抓不住当前问题的重点。我们实测过当历史会话超过一定轮次后继续增加历史内容回答准确率不升反降。基于这些原因上下文管理不是可选项而是所有正经LLM应用的必选项。问题的关键不再是“要不要管理”而是“用什么模式来管理”。1.3 方案选型背后的核心权衡做上下文管理方案本质上是在三个目标之间找平衡点回复效果、运行成本、实现复杂度。效果要准成本要低代码不能太难维护。这三个目标经常打架没有完美的方案只有适合特定场景的取舍。目前常见的上下文管理策略主要有四种全量携带、滑动窗口、摘要压缩、检索增强。我所在的团队在不同项目里都试过各自的适用面很不一样。全量携带实现最简单效果对短对话来说也最稳但成本随对话长度线性上升根本撑不住长会话。滑动窗口是成本与效果的折中点只保留最近N轮对话实现也简单但缺点是模型会遗忘窗口之外的信息一旦用户回头问“我刚才说过的那件事”就懵了。摘要压缩是在滑窗基础上加了一个“记忆层”定期把旧对话提炼成一段摘要放在上下文里信息保留率高很多代价是实现复杂而且摘要质量直接影响模型后续理解。检索增强则是把长期记忆和维护成本都外包给向量数据库每次只捞跟当前问题相关的片段进来适合知识库场景但需要额外维护检索链路还要处理检不准、检不全的新问题。我的经验是多数客服场景用“滑动窗口摘要”就够用了既不会太贵又不会完全失忆。知识库问答则要用检索增强因为它面对的不是连续对话而是大量文档。内容创作类工具往往需要全量携带加手动摘要的长上下文模式。方案选型时不要盲目追求“最先进”先想清楚你的场景里上下文的核心价值到底是“记住连续对话”还是“找到相关材料”对症下药比跟风堆技术靠谱得多。2. 核心细节解析与实操要点上下文模式里的关键参数与选型2.1 看得见的隐形天花板token窗口要设计上下文模式先得学会用token这个单位来思考。token是模型处理文本的最小单元可以简单理解成“词组碎片”。对于英文一个token大约对应0.7个单词对于中文一个汉字大约对应0.6到1.5个token具体取决于分词器。这不是精确换算但足够我们做预算估算。为什么强调token而不是字数因为模型的计费和窗口限制都以token为单位的而token与字数、字符数之间并不是线性对应。我见过有同事按字数做了上下文上限结果模型不同、分词规则不同实际占用的token比预期多出三成线上频繁报错排查半天才发现是估算口径错了。目前主流模型的上下文窗口大概分三档第一档是32K到64K适合大多数对话场景第二档是128K到200K适合长文档处理的场景第三档是1M左右主要用于超长代码库或整本书级别的分析。这里要泼一盆冷水窗口大不等于好用。模型窗口越长对注意力的管理就更困难并且输入token的计费也同步上升把长文本全塞进去既慢又贵。所以选择模型时不要只看窗口大小应该优先确保业务的核心数据能放进窗口再留出20%到30%的余量。我自己习惯用一个傻瓜式公式来做预算检查一条请求的总token数约等于系统提示token数 历史消息token数 检索片段token数 当前用户输入token数再乘以1.2的安全系数。如果这个值超过了窗口的七成我就要开始考虑截断或者压缩了。记住七成是警戒线不是满负荷才处理因为模型的输出还需要占用窗口响应内容过长会把请求直接顶爆。2.2 少样本上下文学习不只是把示例塞进去那么简单上下文模式里我特别想强调少样本上下文学习这一环。很多产品经理会问同样一个模型为什么你们换一段话术效果就完全不一样答案就在这里。少样本学习的本质是通过在上下文中放入若干“输入输出对”作为示范让模型从示例中推断出当前任务的格式、语气和思考路径。它不更新模型的权重完全靠上下文引导。这种做法在分类、抽取、改写、工具调用这类场景中效果非常明显。举个例子我在做一个意图识别功能时最初只给模型一段描述“帮我把用户意图分类为售后、售前、投诉”。效果很差模型经常把“催发货”归类为售前而实际上这种已下单用户的不满更应该归为投诉。后来我在上下文里加了三条示例明确展示“输入用户问题-输出分类-给出简短理由”的格式模型准确率立刻提高了一大截。这就是少样本上下文学习直接带来的提升。实操上做少样本有哪些细节要注意第一示例质量远比数量重要三五个精心设计、覆盖边界情况的示例好过二十个凑数的。第二示例要贴近真实业务数据不要在上下文里放理想标准话术模型会被带跑偏。第三示例的开头和结尾位置很重要放在中间容易被注意力机制忽略一般放在用户消息之前或对话的开头。最后别忘了示例本身也占token别看每个例子只有几十个token积少成多对成本的影响也不小。2.3 上下文模式的三种主流架构对比在我做过的项目里最常上手的上下文管理架构是三种全量拼接、滑动窗口、摘要压缩。我给它们做个结论性对比方便直接对照选型。全量拼接把整个会话历史从头到尾全带上。优点是实现极简、信息零丢失短会话效果很好缺点是成本随对话线性上涨长会话会撞窗口上限而且冗长历史会稀释模型对最新问题的注意力。只适合每次会话短、轮次不超过几轮的工具型应用比如单轮翻译、测试用例生成。滑动窗口只保留最近N轮对话。比如窗口保留10轮超过10轮就丢弃最旧的消息。优点是成本和token开销可控实现也简单缺点是模型会在边界处“失忆”无法回答“我前天问的那个问题”这类需要长期记忆的query。它适合轮次较短、容忍一定遗忘的对话机器人。但我提醒一下滑动窗口实现时别简单按“最近20条消息”来切最好按“最近N轮完整对话”来切也就是保证user和assistant成对出现避免把assistant的回复截断成半截。摘要压缩在滑动窗口基础上增加“摘要层”每隔几轮或当历史超过阈值时先用一个专用的摘要模型把旧对话压缩成一段概括性文本放在窗口顶部。这样模型既有最近的完整细节又有旧对话的概要记忆跨度可以拉得很长。代价是多了模型调用成本和实现复杂度摘要质量不高时还会引入错乱信息。适合客服系统、陪伴式助手、长流程Agent这类对话跨度大、历史参考需求强的场景。我做客服机器人时一开始用的就是纯滑动窗口上线两天就收到用户投诉“刚才说过的话机器人就忘了”。后来改成摘要压缩才算是把体验拉了起来。没有绝对的好坏只有场景匹配不匹配。2.4 工具链的选择框架够用为什么我还要自研目前处理上下文模式社区里现成的工具并不少。LangChain有Memory模块提供了ConversationBufferWindowMemory、ConversationSummaryMemory等实现LlamaIndex的ChatEngine内置了上下文管理的逻辑一些国产框架也把这部分封装成了配置项。坦白说这些框架适合做原型验证和快速起步。如果项目目标是几天内跑通Demo直接用框架的记忆模块最省事。但框架的问题在于它的通用性决定了它无法完全贴合你的业务。比如LangChain的摘要内存默认基于OpenAI风格的消息结构如果团队接入的是国内模型或者换了不同的消息格式就要做大量适配。而框架的“记忆机制”是黑盒的一旦上下文累积出问题你很难判断是模型的问题还是记忆模块的问题调试成本反而更高。我们团队的决策是初期用LangChain把Demo跑通验证产品方向中期开始自研一个极简的上下文管理模块只保留我们真正需要的几个能力——消息分组、token估算、滑动窗口截断、摘要压缩触发。自研代码量大概两百多行维护成本远低于当初适配框架的成本。我建议只要项目计划上线并且对长对话有要求越早把上下文管理这部分收回到自己手里越好。两百行代码换来的透明度和可控性是框架给不了的。3. 实操过程与核心环节实现手把手搭一套上下文管理模块3.1 模块设计思路先把信息分四层在设计上下文管理模块前我习惯先把一条最终发给模型的请求拆成四层信息。这样动手写代码之前思路就已经清晰了。第一层是系统指令层。包括角色设定、回复风格、业务规则、安全边界。这些内容每轮都必须存在但最占地方也最容易越写越臃肿。我见过一些团队的项目系统提示写了一千多个token里面全是叠床架屋的要求效果反而不如精简版。这一步的要求是定期审视系统提示里每句话是否还有存在价值该删就删。第二层是摘要记忆层。存放旧对话压缩后的摘要比如“用户已确认收货但对商品颜色不满意客服已承诺补发赠品”。摘要会随对话推进不定期更新它是整个上下文模式里最有价值的部分。第三层是近期对话层。比如最近五到十轮完整对话按时间顺序保留原始内容是模型回答具体问题的主要依据。第四层是查询输入层。也就是当前用户的输入以及本次请求触发的检索片段。这一层必须放在消息列表最后因为模型对越靠后的内容注意力越强。如果把当前问题塞在中间模型经常“看到”但不“关注”回答起来就像没听见你最后那句最关键的话。分层想清楚后接下来的代码实现就顺理成章了。3.2 核心代码实现滑动窗口加摘要兜底下面是我在一个客服机器人项目里实际用过的上下文管理模块的核心代码。它做的事情很朴素估算token、维持滑窗、超限触发摘要压缩。使用了tiktoken库来做token估算如果你用的是国内大模型厂商的SDK换算逻辑类似只是tokenizer换一下。import tiktoken from typing import List, Dict class ContextManager: def __init__(self, system_prompt: str, max_tokens: int 8000): self.system_prompt system_prompt self.max_tokens max_tokens # 请求总token预算 self.history: List[Dict[str, str]] [] # 近期对话按轮次存放 self.summary # 全局摘要 self.enc tiktoken.get_encoding(cl100k_base) def _count_messages(self, messages: List[Dict[str, str]]) - int: # 按消息列表估算token总量额外加4是每条消息的角色标记开销 tokens len(self.enc.encode(self.system_prompt)) 4 for msg in messages: tokens len(self.enc.encode(msg[content])) 4 tokens 2 # 角色标记的偏移 return tokens def _build_messages(self, query: str, context: List[str] None) - List[Dict[str, str]]: messages [] # 摘要层优先放在system保证模型先读到全局记忆 if self.summary: messages.append({role: system, content: 对话背景摘要 self.summary}) messages.append({role: system, content: self.system_prompt}) # 检索片段以user身份包裹多段用序号隔开 if context: merged \n\n.join([f[資料{i1}] {c} for i, c in enumerate(context)]) messages.append({role: user, content: 相关知识\n merged}) # 近期对话层 messages.extend(self.history) # 当前查询必须放在最后 messages.append({role: user, content: query}) return messages def add_and_merge(self, user_msg: str, assistant_msg: str): self.history.append({role: user, content: user_msg}) self.history.append({role: assistant, content: assistant_msg}) def _update_summary(self, oldest: List[Dict[str, str]]): # 生产环境这里应该调用摘要模型此处简化为规则拼接仅用于演示 compact ; .join( [f{m[role]}: { .join(m[content].split())[:50]} for m in oldest] ) self.summary ( f用户已咨询过订单发货、退款流程相关问题。{compact} if not self.summary else self.summary 后续追问 compact ) def build_prompt(self, query: str, context: List[str] None, max_history_rounds: int 10) - List[Dict[str, str]]: # 先按历史轮次做一轮裁剪保护最近的对话 if len(self.history) max_history_rounds * 2: overflow self.history[: len(self.history) - max_history_rounds * 2] self._update_summary(overflow) self.history self.history[len(overflow):] messages self._build_messages(query, context) # 再按token预算做精确收缩从最旧的消息开始剔除 while self._count_messages(messages) self.max_tokens and len(self.history) 0: first_msg self.history.pop(0) second_msg self.history.pop(0) if len(self.history) 0 else None self._update_summary([first_msg] ([second_msg] if second_msg else [])) messages self._build_messages(query, context) return messages这个类有三个值得留意的设计点。第一摘要层和系统提示都用了system角色但摘要放在最前面系统提示紧随其后。这样做的原因是摘要代表的是动态更新的对话背景必须先于规则被模型读到静态规则放后一点也不会丢。第二检索片段统一用user角色携带而不是拼接进system提示。不同模型对system内容的感知强度和位置习惯不同而把它作为user知识输入绝大多数模型都会认真对待。第三token超限时剔除历史采用的是“从最旧开始弹”的策略同时把弹出的部分折入摘要避免信息凭空消失。实测这种策略在遗忘边界上的表现比直接丢弃好得多。3.3 参数怎么定那些看起来像玄学的数字写完了代码你会发现自己不得不面对一堆参数比如max_tokens设多少、max_history_rounds设多少、摘要多久触发一次。这些数字回答得好不好直接决定了模块效果。我给一套自己用的经验值。先算max_tokens。假设你选用的是32K窗口的模型我给请求总token预算设为8000到10000。为什么不是16000因为你还需要给模型输出预留空间Agent场景还要考虑中途插入工具调用等额外逻辑而且Windows窗口太满会让模型处理速度和准确度同时下降。用窗口的三到四成做预算是一个偏保守但稳妥的选择。再算max_history_rounds。具体回合数没有标准答案我建议按“token估算”反过来推假设每轮对话约200个token分配给历史层的预算约为3000 token那么保留轮次就是3000除以(200乘以2)大约7到8轮。不同产品的对话长度差异很大但设计思路是一致的先定预算再折算轮数而不是拍脑袋定个“20轮”。最后是摘要触发时机。我的建议是“双条件触发”当历史轮次超过max_history_rounds或者历史消息token总量超过总预算的一半时就把最早若干轮并入摘要。双条件的好处是既照顾了轮次维度又照顾了token维度避免出现“轮次不多但每轮都很长”时的漏网。还有一个小细节容易被忽视摘要不能只有一句“用户之前咨询过相关问题”。我在实践里发现有帮助的摘要必须包含主体、事件、结果三要素比如“用户咨询了订单延迟问题客服解释因物流高峰期导致用户已经接受”。缺少任何一要素后续模型就很难在摘要基础上做推理。3.4 上线前的自测清单代码写完后别急着上线先对着下面这个清单做一轮自测。这些都是我踩过坑后沉淀下来的检查项每一条都是线上事故换来的。第一确认当前用户输入永远在消息列表最后。很多人建消息列表时用到了insert或者复制了历史数组后追加新消息一个不注意就把当前问题插到了中间。第二确认system角色只有你主动定义的那一两条不要有业务代码临时往system里塞内容。第三token估算要与实际线上模型的分词器一致。切换模型后如果忘替换tokenizer预算会完全失真。第四测试“长对话断点续聊”——把会话拖到超过预算两倍然后回过去问最早几轮里明确提到的内容确认摘要能兜住。第五观察一轮请求的token消耗与响应时间在压力的100倍范围内都保持稳定。第六确认摘要模型本身的输出也被计入预算不能只算主模型的调用。这些点看起来不起眼但任何一个出了问题线上表现都会让你措手不及。4. 常见问题与排查技巧实录我在实际项目里踩过的坑4.1 模型突然失忆聊到一半忘了前面的结论排查这类问题我强烈建议先做“改日志”再做“改代码”。第一步把每次发给模型的请求完整打日志包括消息数量、每条的token数、是否触发了摘要。第二步打开日志看模型失忆的那一刻上下文里到底有没有包含之前的信息。我遇到过一次典型情况用户在第25轮问“那我刚才说的那个赠品还能补发吗”模型回答“您说的赠品我不了解”。日志显示上下文里确实还有第3轮提到的赠品信息但它早已经被滑窗截掉了摘要层也没来得及生成模型自然“毫无印象”。问题不是模型傻而是我的上下文管理把关键信息丢在了窗口外。解决方案是调整滑窗触发策略在删除旧消息前把关键要素提前写入摘要而不是等到被删了再补。这里有一个隐藏的技巧摘要更新不能只在“消息被删除那一刻”做最好是在每轮对话结束后都做一次增量更新哪怕只是对摘要做几句补充。因为信息一旦被挤出去再想从原始日志里捞回来就麻烦得多。还有一种是摘要格式问题。摘要层使用System角色且混入过多枯燥的大纲式语句后模型对摘要的信任度会下降。解决办法是把摘要写得像“人话”有前因后果而不是一句干巴巴的“用户说了一些事情”。4.2 token越算越不准成本预算频频超支这个问题我犯过不止一次最典型的原因是“多模型场景下的tokenizer混用”。比如主模型用的A厂商它的tokenizer对中文更友好而我在代码里用的是B厂商的tiktoken等价编码统计出来的token数偏低导致请求长度比预期大账单跟着超标。排查方法不难找一个有代表性的真实对话手动打印每条消息的token数和模型服务返回来的usage字段对比。如果差异超过10%就需要换成模型厂商官方推荐的tokenizer。另外中文标点和英文标点、换行符、代码块这些细节也会影响估算精度统一用官方tokenizer后这类误差基本都会消失。还有一个容易被忽视的成本黑洞摘要模型本身的调用开销。很多团队只监控主模型消费忘了摘要模型每轮也在消费token。如果每轮都触发摘要更新摘要模型的token开销甚至能占到总费用两成以上。解决办法是给摘要更新增加“冷却时间”比如至少隔三轮或摘要内容发生变化才触发而不是每轮都跑一次。4.3 RAG引入的上下文把提示词挤爆了知识库应用里检索增强是必须的但检索出来的内容量往往非常不可控。早期我做过一个企业制度问答系统向量检索Top10返回的内容加起来快六千token加上系统提示和用户问题请求直接撞上输出上限。最糟糕的是检索回来的内容里可能只有一段与问题真正相关其余都是噪音还干扰了模型的判断。这个问题有几个解决思路。第一限制检索条数TopK不要定太高实测下来很多场景Top4与Top10的效果差异不大但成本和噪音区别明显。第二给检索片段打分过滤比如设定相似度阈值低于阈值的片段直接丢弃。第三对检索结果做重排序把最相关的片段放到最前面并确保它们都在用户查询附近的位置。第四做动态长度压缩长文档在入库时就切成小块检索时只捞最相关的块而不是把整篇文档拖出来。这套组合打下来检索内容量基本能压到原方案的三成而且回答质量反而更稳。4.4 多个System消息大战模型到底听谁的这个问题非常隐蔽。很多开发者在构造上下文时习惯把一些临时约束也塞进System消息比如“你现在用客服的口吻回复”“语气要委婉”“不许超过500字”。当我们同时在消息列表里放了多个System消息时就埋下了一个隐患不同模型对多条System消息的处理策略不一样有的取第一条有的取最后一条有的会串着理解。结果就是某天线上运行的用户会话里上下文突然混进了一条诡异的System指令模型行为完全失控。我花过很长时间排查一个“助手风格突然换成翻译腔”的线上问题日志翻来翻去最终发现是有段业务代码在请求中追加了一条覆盖性System消息把原始的客服系统提示给顶替了。这个问题的修法很干脆整个请求里只保留绝对必要的System指令至多一两条。其他动态约束如果确实需要用普通user或者assistant消息放到对应位置更安全。如果有多个指令需要表达就合并成一条系统提示用目录式的编号结构呈现避免模型对多条System消息产生歧义。最后再分享一点个人的实操体会上下文模式这个东西听起来像概念做起来全是细节。我自己的体会是把它当成“一等公民”来设计整个AI应用的质量会上一个台阶反过来如果只是把它当作模型调用的附属品后面线上出问题时排查起来一定会手忙脚乱。一个很管用的压测技巧是故意把上下文预算调小到正常值的一半看系统在极限压缩下还能不能记住关键信息、能不能给出可用答案。只要能撑过这种极限测试放开预算后系统基本就是过量的余量稳得很。如果你想把这个模块再往上走一步还可以试试给不同的历史片段做重要性打分让高价值信息存活更久、低价值信息早点进入摘要——这就是后话了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑