ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

大模型Context Mode实战:滑动窗口与摘要压缩的上下文管理

大模型Context Mode实战:滑动窗口与摘要压缩的上下文管理 1. 项目概述Context Mode是什么解决什么问题在做大模型应用落地的时候最容易被忽略、但直接决定用户体验上限的往往不是提示词写得好不好而是 context-mode——上下文模式。简单说它就是“每次请求到底带多少历史信息给模型”的控制开关。很多人把大模型接到项目里第一版跑通了就觉得万事大吉结果用了一段时间发现模型越聊越笨、越聊越“失忆”甚至上一句说过的事下一句就忘了。问题大概率就出在上下文模式没设计好。1.1 一个真实的“翻车”现场也是这个项目的起点先说一个我自己的经历。去年做一个客服问答机器人最初用的是最省事的方案每次调用模型只把用户当前问题发过去不携带任何历史消息。上线后客服反馈说“这个机器人是个金鱼脑子用户说‘我刚才那个订单怎么还没发货’它一脸懵”。后来改成无脑拼接历史消息把所有对话记录一股脑塞给模型。结果是能记住之前的订单了但用户只要多聊几轮系统就报错提示超出最大 token 限制即便没报错模型也经常被很早之前的无关对话带偏。比如用户中途聊了几句天气模型回答就突然开始聊天气完全忘记正事。踩了这个坑之后我才意识到真正的核心不是“带不带历史”而是“怎么带、带多少、带哪些”。这个需求抽象出来就是 context-mode 要解决的问题在有限的上下文窗口里用一套可控的策略让模型在每一轮都能看到最关键的、最相关的那部分信息同时把历史信息的存储成本和控制复杂度都降下来。1.2 Context Mode的核心价值让模型“记得住”又不“记太多”大模型的上下文窗口是硬约束。哪怕是支持超长窗口的模型也不是真的让你把所有内容都堆进去——内容一长注意力会被稀释模型对早期信息的召回率会明显下降而且每次请求的 token 消耗直接和成本挂钩。Context Mode 的价值就是在“记得住”和“记太多”之间找到一个工程上可落地的平衡点。具体来说一个好的上下文模式应该做到三件事控制范围明确每次请求携带哪些内容是最近几轮对话、是系统设定、还是从知识库里检索出来的片段。控制长度在不超过模型上下文窗口上限的前提下把最重要的内容保留下来优先级低的内容该丢就丢。控制成本因为 token 即成本同样的功能1000 token 能解决的事就不要花 4000 token。这篇文章我会从设计思路、实现细节、代码示例到问题排查完整讲一遍我在实际项目中落地 context-mode 的过程。无论你是刚接触大模型开发还是已经被上下文问题折磨过一阵子下面这段内容都值得看完。2. 核心细节解析三种Context Mode的实现思路Context Mode 并不是一个固定的代码模板它是一套策略。不同场景适合不同的策略选错了策略后面怎么调优都别扭。我把它拆成三种基础模式来讲理解清楚这三种基本就能覆盖绝大多数业务需求。2.1 单轮模式最稳但最傻单轮模式很好理解每次请求只传当前输入不传任何历史。这是最不容易出 bug 的模式因为模型每次面对的都是一段独立文本不会被之前的错误信息带偏。适合的工具场景包括一次性文本分类、单条内容审核、独立翻译、模板化生成等。但它的缺点同样明显。只要业务涉及多轮对话单轮模式基本不可用。你让它“结合我们刚才讨论的方案写一份总结”它根本不知道“刚才”发生了什么。我第一次做客服机器人时用的就是这种模式结果被喷得很惨。如果项目初期只求快速验证某个 prompt 效果用单轮模式是可以的。它能把变量控制到最少让你先确认模型对单条输入的输出质量达不达标。但正式做产品时几乎不会只用它。2.2 滑动窗口模式最常用的默认选择滑动窗口模式是目前多轮对话场景里最常用、也最稳妥的一种 context-mode。思路很简单始终携带最近 N 轮对话再早的内容直接丢弃。这个 N 不是拍脑袋定的而是根据模型上下文上限、平均每轮消耗 token 数、以及业务需要的“记忆深度”一起算出来的。举个例子假设你用的是一个窗口上限 8000 token 的模型业务上希望用户能连续聊 20 轮不“失忆”。如果平均每轮用户输入加模型回复消耗 400 token那 20 轮就是 8000 token正好卡满。但实际不能真的卡满因为每轮还要附带 system prompt、工具返回结果、当前问题等信息这些都会额外占 token。所以你需要留出至少 20% 的余量把实际可用的历史空间控制在 6000 token 左右再反推出到底保留最近多少轮。这种模式的优点是实现成本低、行为可预测、不会因为历史太长导致模型注意力涣散。缺点是它天然“记不住太久远的事”。用户聊了 50 轮它只能看到最近 20 轮早期信息彻底消失。如果你的业务中用户有跨多天、多轮次的长期记忆需求这种模式就不够了。2.3 摘要模式突破窗口上限的“记忆压缩”摘要模式是我个人认为最有技术含量的一种。它的核心思想是不要直接丢旧消息而是把旧消息做一次“压缩”生成一段摘要存进 memory 里。以后每次请求把这段摘要放在历史消息最前面再拼接最近的对话这样既保留了早期信息又不会撑爆窗口。具体做法有两种。一种是在对话过程中定期触发摘要生成比如每 10 轮就让模型把之前所有对话总结成 200 字以内的要点另一种是采用固定的“长期记忆 短期记忆”双层结构长期记忆存摘要短期记忆存最近几轮原始消息。摘要模式最大的坑在于摘要本身可能失真。模型在压缩时可能会丢掉关键细节比如用户提到过的具体订单号、时间、金额。如果这些信息是业务关键字段用摘要模式就要非常小心。我的做法是摘要里只保留“事实性要点”比如用户诉求、已确认的结论、待办事项而把原始消息归档到外部存储需要查细节时再单独检索。这三种模式不是互斥的实际项目里经常是混着用。比如先用滑动窗口处理短期对话同时用摘要机制维护一个跨会话的长期记忆检索增强再负责从知识库中补充相关信息。这相当于给模型做了分工哪些信息靠窗口记住哪些靠摘要记住哪些靠外部检索临时拉取。3. 实操过程与核心环节实现写一个可落地的Context Mode组件有了设计思路接下来就是动手实现。我这次分享的版本会尽量保持轻量方便你直接抄进自己的项目里改。我选择了 Python 作为示例语言因为大模型生态里 Python 的库最全也最容易调试。3.1 前置设计与工具选型在写代码之前先明确几个边界模型接口选型我以 OpenAI 兼容接口为例因为国内主流模型服务大多提供兼容接口换 base_url 就能直接切换。你用别的平台也没关系核心逻辑是通用的。Token 计算方法建议用 tiktoken 库做精确计算而不是自己按字数瞎猜。不同模型的 tokenizer 不一样同一个字符串在 gpt-4 和 Claude 上算出来的 token 数可能差很多。存储方案历史消息先放内存里便于理解逻辑。生产环境建议换成 Redis 或者数据库按 session_id 存储。3.2 ContextMode核心代码与注释下面这段代码是我实现的一个小型 context-mode 管理器支持滑动窗口和摘要模式。import json from typing import List, Dict, Optional, Literal from dataclasses import dataclass, field try: import tiktoken except ImportError: tiktoken None dataclass class ContextMode: 一个轻量的上下文模式管理器。 mode: single | sliding | summary mode: Literal[single, sliding, summary] sliding max_context_tokens: int 6000 max_turns: int 20 system_prompt: str 你是一个乐于助人的助手。 summary_prompt: str 请用不超过200字总结以上对话的核心信息包括用户诉求、已确认结论和待办事项。 history: List[Dict[str, str]] field(default_factorylist) summary: str _token_buffer: int 0 def __post_init__(self): if tiktoken: self.enc tiktoken.encoding_for_model(gpt-4) else: self.enc None # 初始化时把 system prompt 放进 history self.history [ {role: system, content: self.system_prompt} ] def _count_tokens(self, text: str) - int: if self.enc: return len(self.enc.encode(text)) # 备用估算中文约0.6 token/字英文约0.25 token/字符 return int(len(text) * 0.6) 5 def _current_total_tokens(self) - int: return sum(self._count_tokens(msg[content]) for msg in self.history) def add_message(self, role: str, content: str) - None: 添加用户/助手消息并触发上下文管理策略 self.history.append({role: role, content: content}) if self.mode sliding: self._apply_sliding_window() elif self.mode summary: self._maybe_apply_summary() def _apply_sliding_window(self) - None: 滑动窗口保证在 max_context_tokens 内且保留最近 max_turns 轮消息 # 先按轮数限制裁剪 # history[0] 是 system prompt所以消息从 index1 开始 while len(self.history) - 1 self.max_turns * 2: # 丢最老的一条 user/assistant 消息 self.history.pop(1) # 再按 token 数限制裁剪 while self._current_total_tokens() self.max_context_tokens and len(self.history) 1: self.history.pop(1) def _maybe_apply_summary(self) - None: 摘要模式当 token 超过阈值时触发摘要压缩 if self._current_total_tokens() self.max_context_tokens: return # 取出所有历史消息不包括 system prompt messages_to_compress self.history[1:] compressed_summary self._generate_summary(messages_to_compress) self.summary compressed_summary # 用 system prompt 摘要 替代全部历史 self.history [ {role: system, content: self.system_prompt}, {role: system, content: f【历史对话摘要】\n{compressed_summary}} ] def _generate_summary(self, messages: List[Dict[str, str]]) - str: 这里应该调用 LLM 生成摘要。 为了演示我只把消息拼起来当摘要实际项目中请替换。 content \n.join(f{m[role]}: {m[content]} for m in messages) # TODO: 接入 LLM 生成真正摘要 return content[:200] def build_messages(self, current_input: str) - List[Dict[str, str]]: 构造最终送入模型的 messages 列表 msgs self.history.copy() msgs.append({role: user, content: current_input}) return msgs def clear(self) - None: 清空历史保留 system prompt self.history [ {role: system, content: self.system_prompt} ] self.summary 这里有一点必须说明_generate_summary方法是简化的实际项目中必须调用一次 LLM 接口来生成摘要。比如可以复用同一个模型也可以用小一点的模型专门做摘要成本更低。3.3 Token计算方法与窗口预算很多新手在这里会踩坑凭感觉设置max_context_tokens。比如模型支持 32K他就设成 32000结果调用时报错或者费用高得吓人。原因在于模型接口限制的是单次请求总 token 数你塞进去的history、system prompt、当前输入、还有模型输出的 token全部算在一起。所以预留输出空间是必须的。我一般按下面这个公式来预算是比较稳的可用历史上下文窗口 模型上限 × 0.8 - 系统提示词 - 当前用户输入 - 预计输出长度以模型上限 8000 为例留 20% 余量即上限按 6400 计算系统提示词占 200当前用户输入平均 500预计输出最长 1000。那么可用历史窗口就是 6400 - 200 - 500 - 1000 4700 token。用这个数字作为max_context_tokens传给 ContextMode滑动窗口就会自动在这 4700 token 内保留尽量多的最近对话。别贪心把窗口设满否则一旦遇到超长输入或者模型一次性输出太多就会报错而且报错后重试的代价更高。如果要精确计算 token建议用 tiktoken。第一次使用前先安装pip install tiktoken然后在上面的代码里_count_tokens方法会自动用 tiktoken 精确计算。需要注意tiktoken 的encoding_for_model只支持 OpenAI 模型列表如果用国产模型或者开源模型可能需要用对应模型的 tokenizer或者用粗略估算方式。4. 常见问题与排查技巧实录代码写完了跑起来不难难的是调好。我把自己实际调试 context-mode 时踩过的坑整理成一份问题速查表按出现频率排序你遇到类似问题时可以直接对着排查。4.1 上下文被“污染”导致回答串台现象用户问订单进度模型却开始聊他上次提到的旅游计划用户说“那个事情算了”模型以为用户要把订单取消。原因滑动窗口保留了太多无关早期信息或者摘要模式生成的摘要没有区分“事实”和“闲谈”。模型注意力被无关内容稀释了。解法加大系统提示词的权重强制模型优先关注最近一轮的意图。摘要模式里只保留事实性信息过滤掉寒暄、情绪化内容。如果用户明确切换了话题考虑清空早期上下文重新开始一轮新记忆。这也是 context-mode 里一个很实用的扩展话题重置开关。4.2 上下文一长就“失忆”与截断策略现象对话超过 50 轮之后模型突然忘记用户最初提出的核心需求尽管这些信息还在窗口里。原因超过一定长度后模型对早期 token 的注意力会自然衰减。这不完全是 mode 的问题而是模型本身的局限性。窗口越大不代表越“记得住”。解法不要只看“有没有”超过窗口要看“最关键的几个 token”是否被挤到太靠前的位置。把核心约束放进 system prompt 里让它始终出现在最前面。在摘要模式中把“用户初始目标”“最终要交付的东西”单独摘出来跟着摘要一起放在窗口头部。对关键信息做外部持久化需要时检索回来而不是完全依赖窗口。4.3 费用账单吓人怎么降本现象引入上下文模式后每次请求 token 数明显变大账单跟着上涨。尤其是摘要模式因为额外多了一次摘要生成的调用。解法使用滑动窗口时把max_turns调小一点。很多场景只需要最近 6 到 8 轮再早的信息用处不大。摘要生成可以选择小模型。比如主对话用大模型摘要用一个小型模型成本能降一个数量级。给摘要触发加阈值比如至少 15 轮才触发一次避免频繁压缩。在build_messages里定期清理超过 24 小时的会话从存储层删除会话时一并清除。4.4 调试Context Mode时必看的几个关键日志context-mode 出问题时最难受的就是“不知道模型到底看到了什么信息”。这条是我自己的经验如果只盯着最终输出很难判断是提示词问题、上下文问题还是模型本身的问题。我的做法是在每次请求前把build_messages的结果落一条日志至少包含当前模式mode当前总 token 数history 消息条数是否触发了 summary 压缩最终 messages 的前 200 个字符。有了这些日志再看到模型回答异常时就能迅速回溯到“当时它到底看到了什么”。这个习惯帮我解决了很多看起来像玄学的问题。我个人更推荐在开发阶段把这个日志输出到本地控制台在测试环境保留最近 100 条记录。生产环境如果担心敏感信息泄漏只记录 token 数和消息条数不记录完整内容也能满足大部分排查需求。最后分享一个我后来一直在用的小技巧把所有 context-mode 的配置参数比如 mode、max_turns、max_context_tokens都做成可配置项放进配置中心或环境变量里。因为线上出问题的时候你大概率需要在不改代码的情况下临时调整窗口大小。提前把参数暴露出来能少一次紧急发布也能让后续调优快很多。
返回列表