
我最近在调一个客服机器人的时候被“上下文”这个词折磨得不轻。同一套提示词用户多问两句模型就开始答非所问把历史全塞进去又发现对话还没进行几轮token配额就见底了。后来我把这类问题统一归到一个关键词上——context-mode。你可以把它理解成我们到底用哪种方式把“之前说过的话”交给模型。这篇文章我想说说我从 context-mode 的角度重新思考上下文管理的过程。它不是某个工具里一个单一的开关而是一整套关于“哪些历史信息要进入模型视野、以什么形式进入、保留多久”的设计方案。适合正在做聊天机器人、RAG 检索问答、AI 编程助手的开发者也适合那些想搞清楚“为什么模型总是忘记我刚才说了什么”的重度AI用户。1. context-mode 的整体设计思路1.1 一切都要从上下文窗口说起大模型其实没有“记忆”它只有“眼前这一屏”。你发给它的所有文字、它自己生成的所有文字连同系统提示词一起都会被拼接成一个长序列模型在这个序列上做注意力计算然后预测下一个词。这个序列能有多长就是常说的“上下文窗口”。把这个窗口想象成一张工作台。工作台越大你能摊开的东西越多但你不可能把仓库里所有东西都搬上来。context-mode 本质上就是一类工作台整理策略什么时候摊开所有文件什么时候只放最近几张什么时候把旧文件浓缩成一张便签贴在边上。我见过很多团队在攒提示词的时候没有策略一股脑把用户历史、检索结果、业务数据全往窗口里塞。结果模型不是不聪明而是眼睛被一堆无效信息填满了真正关键的那句话反而没被注意力捕捉到。1.2 常见的三种 context-mode全量、滑动窗口、混合我实际在项目里测试过三种模式每种都有它典型的适用场景。第一种是全量保留模式。所有对话历史、所有轮次、所有用户动作全部进上下文。好处是信息永不丢失模型任何时候都能回溯用户当初说过什么。坏处也很明显token 消耗随时间线性增长成本涨得飞快而且当序列超过一定长度后模型注意力会分散早期内容几乎被“遗忘”长对话质量明显下降。第二种是滑动窗口模式。只保留最近 N 轮或者最近 N 个 token 内的内容更早的直接丢掉。这种模式实现最简单成本可控适合那些短平快的场景比如闲聊机器人、单轮FAQ问答。缺点是一旦用户突然提到“我刚才说过的那件事”模型就一脸茫然因为那件事已经滑出窗口了。第三种是混合模式也是我现在最推荐的做法。把上下文分成两块一块是“长期记忆区”存放用户偏好、项目约束、已经达成的共识、关键承诺这些信息量不大但价值极高另一块是“短期工作区”只保留最近几轮的具体对话过程。长期记忆区用固定文本塞入每次请求短期工作区用滑动窗口滚动淘汰。从效果看混合模式既不会让模型把用户最核心的需求弄丢又不会让 token 爆炸。下面这张表是我一次对比测试中记录的三种模式表现模式成本趋势长对话稳定性信息保留能力实现复杂度全量保留线性增长越往后越差早期信息名义在实际被淹没最低滑动窗口基本稳定稳定但失忆只保留最近低混合稳定可控稳定关键信息长期保留中等1.3 为什么不应该“一键全塞”不少人问我既然模型支持 128k 甚至更大的窗口为什么还要费劲管理上下文把窗口做大不就行了我的回答是窗口大不等于效果好更不等于省钱。我做过一次实验同样一个问题在塞入 80k 无效历史的情况下模型回答准确率比只塞 4k 精选上下文时低了差不多两成。上下文越长模型要处理的干扰信息越多推理延迟也越高API 费用也在同步增加。哪怕模型窗口大到你根本填不满也不代表你该把什么都往里扔。还有一个容易被忽视的问题你调用模型不只是输入输出也要占窗口。如果你预留的输出空间不够生成到一半就会被截断长文档场景特别明显。所以 context-mode 设计的第一步就是搞清楚你的预算在哪里。2. 核心细节解析与实操要点2.1 上下文窗口的三个关键参数设计 context-mode 之前先把三个数字算清楚模型最大上下文长度、输出预留长度、系统提示词固定开销。以 8k 窗口的模型举例假设你每次最多允许模型输出 1k 个字系统提示词写了一大堆固定内容占掉 1k那你实际可用的动态上下文只有 8000 - 1000 - 1000 6000 token。很多人的“上下文不够用”其实不是模型不够用而是没算这笔账把输出预留和系统提示词都忽略掉了。实操中我会在代码里维护一个“预算表格”每次请求前快速统计当前 messages 里所有内容的 token 数。一旦发现要超出可用动态预算就触发裁剪逻辑。这个裁剪策略用什么算法可以放在后面说但预算先算清楚是底线。2.2 token 预算分级的优先级如果动态上下文预算只有 6000 token而用户聊了三十轮总计 4 万 token必然要砍。砍谁不砍谁就涉及到 token 预算优先级。我常用的优先级从高到低是这样系统指令及其关键约束必须完整保留。用户当前这一轮输入这是模型要直接回答的对象。近期对话中与当前问题直接相关的反馈、纠错、追问。可以压缩的中间过程比如冗长的推理、无关的寒暄。最早期的一般性历史优先转成摘要或直接丢弃。实际操作中我不会直接只留最后几轮。我会先把中间那些“模型曾经给过的中间解释、用户长段的背景陈述”抽出来用一两句话做摘要。摘要本身作为一个系统内容塞回上下文这样既压缩了体积又把关键信息保留住了。2.3 信息保鲜给上下文里的事实盖时间戳很多人忽略了一点同样的信息出现在昨天和出现在一分钟前可信度和影响力完全不同。一个用户昨天说“我想省钱”今天说“可以接受更高价格”如果你保留的是昨天的旧偏好模型就会答错。我给 context-mode 加了一个很简单的机制每次写入长期记忆区的内容都带一个基础标记包括最后更新时间、来源轮次、事件的确定性等级。更新时如果旧事实和新事实冲突直接用新事实替换旧事实而不是两条并存。这样模型永远看到的是当前最新的用户画像。这个机制写起来不复杂但对结果的影响非常大。我在一个旅游推荐机器人上试过加了信息覆盖逻辑之后用户对推荐结果的满意度明显上升因为模型不再拿三个月前的偏好来推荐当下的路线了。3. 实操过程与核心环节实现3.1 一个最小可用的 context-mode 实现我以最常用的 Python 调用大模型接口为例写一个简单的滑动窗口维护器你可以直接换成任何大模型。核心思路是messages 列表只保留最近 N 轮并预估 token 数。import tiktoken class ContextWindow: def __init__(self, system_prompt: str, model: str gpt-4o-mini): self.model model self.enc tiktoken.encoding_for_model(model) self.system_prompt system_prompt self.history [] # 短期工作区 # 长期记忆区单独维护见后文 self.max_dynamic_tokens 6000 def _estimate(self, messages) - int: # 粗略估算每条消息按内容长度计算实际可用 tiktoken 精确计算 text .join([m[content] for m in messages]) return len(self.enc.encode(text)) def add_turn(self, user_msg: str, assistant_msg: str): self.history.append({role: user, content: user_msg}) self.history.append({role: assistant, content: assistant_msg}) def trim(self): # 从最老的记录开始丢弃直到预算够用 while True: total self._estimate( [{role: system, content: self.system_prompt}] self.history ) if total self.max_dynamic_tokens or len(self.history) 2: break # 丢最前面一对 self.history self.history[2:] return self.history def build_messages(self): self.trim() return [{role: system, content: self.system_prompt}] self.history这个类做的事情很直接add_turn把每轮对话追加进去trim在超预算时从最老的消息开始删直到能装进窗口。注意我保留了一个最基本的底线至少保证最后一轮对话完整留在上下文里否则模型连用户在问什么都不清楚。3.2 用“状态卡”做关键上下文长期保留滑动窗口解决了短期成本问题但解决不了长期失忆问题。我额外维护一张“状态卡”本质上是一小段固定文本每次请求前插到系统提示词前面专门记录跨会话长期信息。状态卡的结构我一般写成下面这样用户关键偏好 - 预算偏向性价比对价格敏感 - 时间周一至周五白天可联系 - 关注点稳定性 功能丰富 进行中的任务 - 正在做项目A的迁移目标是减少手工操作 已达成共识 - 方案采用异步处理每周五做复盘每次拿到新的用户回复后我会调用一次“状态卡更新逻辑”把这个结构化区块重新生成一遍。这一步可以用一个独立的小模型请求来做也可以写在主流程里。生成时的提示词大意是根据这轮对话更新状态卡中的字段保持格式不变。这样一来即使对话历史已经滚动淘汰掉了最早那十轮状态卡里依然留着用户的预算偏好在模型不会突然“失忆”。3.3 把 context-mode 用在 RAG 检索问答里RAG 的核心是把检索到的外部资料拼进上下文。这里的 context-mode 不再是“保留多少轮历史”而是“检索结果怎么塞、塞多少、怎么和对话历史配比”。我常用的做法是把检索结果分档。先取 top 20 候选用一个轻量重排序模型按相关度打分只把最高分的 top 5 放进上下文。每篇检索片段我还会做截断限制在 300 到 500 字之间。如果检索结果太碎可以先用合并规则拼成段落减少上下文碎片化。另一个容易被忽略的点用户提问太模糊时直接拿原query去检索往往效果差。我习惯在检索前加一步 query 改写让模型把用户的“帮我看看上次说的那个方案”改写成一个包含项目名、目标、技术路径的完整问题。改写后的 query 检索准确性会高很多进到上下文里的内容也更有价值。3.4 流程串联一个完整的 context-mode 请求链路把上面这些串起来一次请求的实际流程是这样的收到用户新消息。判断是否需要更新长期记忆区状态卡。用状态卡 短期最近几轮拼出“稳定版上下文”。如果模型返回结果不理想或用户需求模糊才触发 RAG 检索。检索结果经过重排、截断后插到当前用户消息之前。装配最终 messages做 token 预算裁剪再发给模型。这套链路看起来步骤多其实每次请求也就增加几十毫秒的处理时间换来的是可预期的质量和成本。我在实际项目中落地后单次请求 token 量平均下降了四成左右而长对话场景的准确率反而提升了因为模型看到的不再是一堆脏乱差的历史而是精心准备过的关键信息。4. 常见问题与排查技巧实录4.1 上下文越长回答反而越“糊”排查思路先看当前消息里塞了多少无关内容。我经常在调试日志里打印实际进入模型的 messages 结构检查每个部分的来源和大小。有一次我排查一个知识库问答机器人发现每轮请求都带上了全部检索结果一共 12 篇每篇 1000 字。模型光是被动处理这些文字就已经忙不过来了哪还有余力组织答案。我把检索结果降到 5 篇每篇截断到 400 字问题立刻缓解。经验总结很多时候不是模型变笨了而是你把模型的注意力餐桌摆得太满。宁可少而精不要多而杂。4.2 裁剪历史后模型“失忆”了裁剪掉旧历史之后模型想不起关键约定这是 context-mode 最常见的坑。根源在于只做了“丢”没做“存”。解决方式是裁剪前先生成摘要或抽取出状态卡。我建议任何时候触发 trim 都先跑一个摘要调用把即将被丢弃的旧历史浓缩成一段话插入到上下文开头位置。这样新窗口虽然是精简的但关键事实都还在。有一次我做工单处理助手用户第一天提了个特殊要求第二天又问别的。如果简单滑窗模型早就忘了第一天的事情。加上“工单状态卡”之后模型始终记得这个工单的优先级和客户特殊注意点。4.3 不同会话之间“串味”了这种情况多出现在多用户共用一个全局上下文变量的时候。用户A问天气用户B问股票推荐结果B的请求里带着A的聊天内容。模型可能把A的偏好当成B的偏好输出严重跑偏。解决办法很简单每个会话单独实例化一个 ContextWindow 对象不要用全局 messages。如果做了缓存池key 用 session_id 或用户 ID 做隔离。排查时先确认 messages 里是否混入了其他会话的内容再看有没有把不该复用的隐性上下文带进来。4.4 token 统计不一致导致行为怪异有些框架自带 tokenizer 和模型官方统计方式不一致导致你认为只用了 6000 token实际已经爆了窗口请求直接被拒。我的建议是统一使用模型厂商提供的 tokenizer 库做计数比如 tiktoken。不能用len(text.split())粗算不同语言、不同编码的 token 差距很大中文字符一个可能是 1 到 2 个 token代码符号可能拆成多个 token。预算计算前先拿几个典型文本做校准确认估算偏差在可接受范围内。4.5 钱烧得很快上下文管理做不好最直观的代价就是费用暴涨。每次请求全量塞历史调用频率一高账户余额蹭蹭往下掉。我的成本监控方式是在 ContextWindow 里记录每次请求的输入 token 数和输出 token 数按会话维度累计。设定一个每日预算比如 10 万输入 token。超过后自动切到更省的模式降低检索条数、只保留最近两轮历史、系统提示词精简到必需字段。别忽略输出 token 的费用输出单价通常比输入高。预留输出空间不仅是为了避免截断也是在控制成本。结尾的几句实话把 context-mode 这块啃下来之后我最大的体会是上下文管理不是“怎么把更多信息塞进去”而是“怎么让最重要的信息刚好出现在该出现的位置”。那些号称无限上下文的模型到了真实业务里依然需要取舍因为人脑的注意力有上限模型也一样。最后分享一个我到现在还在用的小技巧每次调试完把实际进入模型的 messages 完整 dump 到本地文件肉眼扫一遍。很多你觉得玄学的问题比如“为什么模型不听话”“为什么重复回答”看完那份输入之后就全明白了。上下文这个事情亲自看一次比自己瞎猜十次都好使。