提到 context-mode很多人第一反应是“这不就是给模型多喂点上下文嘛”。听起来简单但真把一个对话式应用推到生产环境你会发现这玩意儿根本不是一个概念而是一套需要精细化设计的状态机。我在做某内部知识库问答系统时最开始就是一股脑把历史记录全塞进提示词结果模型还没回答问题先把一大段 Token 烧掉了响应慢到用户直接关页面。后来我把“上下文”拆成四种模式——全局模式、窗口模式、压缩模式、检索填充模式整个系统的稳定性和成本才真正可控。这篇文章不聊虚的直接从工程落地角度讲 clear context-mode 是什么、四种模式怎么选、每一步怎么实现、踩过哪些坑。适合正在做对话机器人、AI Agent、文档问答系统或者想优化模型调用成本的人参考。1. context-mode 到底解决什么问题1.1 从一次线上事故说起当时我们做一个内部知识库问答系统功能很简单用户提问系统从文档里找相关内容喂给大模型生成回答。最初版本没有任何上下文管理直接把用户最近几轮对话全部拼进 prompt再把检索到的片段也一起塞进去。第一个线上问题用户聊到第 20 轮时历史记录占了 90% 的输入长度模型开始随机“失忆”。第二个问题单次请求输入 Token 平均涨到 8000 以上费用每天肉眼可见地飙升。第三个问题最头疼——用户问“刚才那个方案的第二点是什么”系统根本不知道“刚才”指的是哪一轮。这三个问题本质上是同一个上下文的使用方式没有分层所有信息一股脑堆在一起既没有重点也没有边界更没有过期机制。context-mode 就是为解决“如何让模型在正确的时间看到正确的信息”而设计的一套工程方法。1.2 为什么传统“对话轮次”不够用很多团队的初始做法是按“轮次”管理上下文保留最近 5 轮超过就丢弃。这个简单粗暴的方案有两个缺陷第一轮次不代表信息量。用户说“好的”和用户说“请把上一轮方案里的技术架构部分展开讲讲”信息密度完全不同。按轮截断前者浪费空间后者又丢关键信息。第二轮次不是唯一的信息维度。知识库问答里系统自己检索到的文档片段、用户画像、当前任务状态都属于上下文但这些和对话轮次没有直接关系。所以我把 context-mode 定义为根据信息的生命周期和获取方式将上下文划分为不同模式分别采取不同的存储、更新、注入策略。它不只是一个缓存工具而是一种结构化的上下文治理方案。1.3 四种模式的划分逻辑全局模式Static Context全程不变的信息如系统提示词、用户偏好、固定规则每次请求都原样带上。窗口模式Rolling Context最近 N 轮对话按时间滚动保留模拟人类的短期记忆。压缩模式Squeeze Context历史超过窗口后由模型把旧内容提炼成摘要作为长期记忆的压缩形态。检索填充模式Retrieval Context不预先存内容而是根据当前问题从外部知识库实时检索相关片段动态填充到上下文中。这个划分方式和人的记忆模型很像全局模式是“人生信条”窗口模式是“正在进行的工作记忆”压缩模式是“上个月的总结”检索填充模式是“需要时翻笔记本”。一旦用这种视角去设计系统代码结构会清晰很多。2. 全局上下文模式把常量焊死在系统里2.1 全局上下文的适用范围全局上下文负责承载“无论用户在哪个对话分支模型都必须知道”的信息。我总结了四类必放全局的内容身份设定你是谁、你叫什么、你的语气是正式还是亲切。业务边界你能做什么、不能做什么遇到越权问题如何拒绝。输出规范回答格式、长度限制、是否使用列表、是否带来源引用。用户画像如果是实名系统用户的部门、权限等级、常用术语偏好。这些信息的共同特点是生命周期等于整个会话甚至跨会话存在。它们不像对话历史那样变化所以不需要复杂的更新逻辑每次请求直接拼接到 prompt 最前面即可。2.2 实现全局模式的三个关键参数第一全局区固定放在 prompt 首部。大模型对开头内容的注意力权重相对较高把最重要的身份和规则放在这里比塞在末尾更稳。第二全局区要控制体积。我见过有人把一份 3000 字的操作手册全塞进系统提示词结果模型在每个回答里都试图复述手册。全局上下文是“常量”但不是“字典”。理想的体积控制在 500 到 800 字以内只保留不可妥协的规则剩下的进入检索模式。第三全局区支持模板变量。比如用户姓名、当前日期、项目编号这类动态但低频的信息可以在请求时通过模板替换注入。不要把这些动态内容写死在系统提示词里否则每次都要生成新模板不好维护。2.3 全局模式的性能陷阱全局上下文每次请求都会消耗 Token。如果你用 800 字全局区大约相当于 600 到 800 Token乘以每天的请求量是一笔不小的固定成本。实践中我常用一个“双全局”策略对于最高频的 3 到 5 个核心规则直接写在系统提示词里不参与任何裁剪对于外围规则放到一个用分隔符标记的“规则区”如果单次请求的上下文预算紧张就优先裁掉这部分。注意全局上下文不是越多越好。模型在长上下文下对中间部分的注意力会弱全局区过长反而会稀释后面真正关键的信息。控制在“核心规则 极简画像”的粒度就够了。3. 窗口上下文模式只保留最近的 N 轮3.1 窗口大小怎么选从估算 Token 开始这是最常见、也最好理解的一种模式。实现方式就是维护一个最近对话列表超出窗口的丢弃。但窗口大小不能拍脑袋。我总结了一个三步估算流程第一步确定你使用的模型上下文上限是多少。假设是 32K Token。第二步留出 30% 的余量。实际可用约 20K Token。这 30% 是给模型输出和突发长回答留的如果不留单次请求很容易撞到上限。第三步从这 20K 里扣除全局上下文和检索填充区的预算。假设全局占 1K检索区预留 6K那么窗口区域约 13K Token。然后根据实际对话的平均 Token 消耗来定窗口轮数。我们当时实测单轮用户问题加模型回答平均约 800 Token13K 除以 800大约能保留 16 轮。提示Token 估算用模型自带的分词器最准。中文环境下粗略估算可以按“1 个汉字约 1.5 Token”但英文缩写、代码块会严重偏离此估算最好写个小函数在代码里直接统计。3.2 滑动窗口实现示例与细节用 Python 实现滑动窗口其实很容易用一个有最大长度的列表就行。我贴一段简化版from collections import deque class RollingContext: def __init__(self, max_rounds: int): self.history deque(maxlenmax_rounds) def add_round(self, user_msg: str, assistant_msg: str): self.history.append({ role: user, content: user_msg }) self.history.append({ role: assistant, content: assistant_msg }) def build_messages(self, system_prompt: str, current_query: str): messages [{role: system, content: system_prompt}] messages.extend(self.history) messages.append({role: user, content: current_query}) return messages细节主要在deque(maxlenN)它超过 N 时会自动弹出最老的元素天然适合滚动窗口。但实际生产环境要注意两个问题一是“轮”与“条”的关系。每一轮包含 user 消息和 assistant 消息两条窗口的容量单位是消息条数不是轮数。上面的例子中max_rounds其实限制了轮数但内部塞了 2 条消息需要自己换算。二是工具调用、函数调用的消息类型。现在的模型支持tool角色的消息这些也要计入窗口。我建议在设计时把所有角色统一用一个count_token(messages)函数做总量校验而不是单纯数轮数否则一旦遇到单轮特别长的情况窗口轮数没超Token 却爆了。3.3 窗口模式的边界问题窗口模式最大的问题是“突然失忆”。用户在第 10 轮提到“用方案 A”在第 15 轮说“按刚才那个来”此时第 10 轮已经滚出窗口模型根本不知道“刚才那个”是什么。这个问题的根源是窗口只按时间截断不考虑语义重要性。我常用的缓解方案有两个第一窗口内做重要性加权。不是简单按顺序保留而是把包含“方案、决定、结论、编号、日期”等关键实体的消息标记为重要即使它老一点只要窗口空间允许就保留淘汰时优先淘汰“嗯、好的、继续”这类低信息量消息。第二窗口与压缩模式联动。当窗口溢出时不要把老消息直接丢弃而是将其送入压缩模块生成摘要后放到全局或长期记忆区。这是下一部分要讲的内容。4. 压缩上下文模式把 10 轮对话压成一段摘要4.1 压缩策略摘要、关键点、实体关系压缩模式本质上是“用更少的 Token 表达更多的信息”。我试过三种策略各有适用场景策略一自然语言摘要。最简单直接调模型“请把以下对话压缩成 200 字以内的摘要保留决定、方案、待办、用户偏好。”优点是信息完整度高缺点是每次压缩要花一次模型调用且摘要有“再表述”的失真风险。策略二关键点列表。把对话转换成结构化的“要点”列表比如“用户选择了方案A”“预算上限3万元”“下一步是周五前交付原型”。这种格式对后续检索更友好压缩后可以直接塞进全局区或检索索引。缺点是需要做信息抽取实现成本略高。策略三实体状态记录。只保留“用户提到的项目名、数字、日期、结论、否定项”等实体及其状态。适合任务型对话比如客服系统。因为实体是确定性的不容易被摘要模型自由发挥。我们在知识库问答系统里最终采用的是“策略一 策略二”的组合先让模型生成一个 100 字以内的自然摘要然后从摘要里提取出 3 到 5 个关键实体。摘要用于延续语境实体用于后续检索触发。4.2 触发压缩的时机与条件什么时候触发压缩我见过有人每 5 轮压缩一次也有人在 Token 快满时才压缩。这两种都不理想。按我的实践压缩触发条件应该满足以下任意一条对话轮数超过窗口上限的 80%。当前上下文 Token 总量达到预算上限的 70%。检测到“话题切换”信号比如用户从聊需求突然转到聊技术实现。第 3 条比较隐蔽但很关键。用户切换话题后旧话题的上下文不仅没用还会干扰模型对新话题的理解。此时即使 Token 还很充裕也应该把旧话题压缩归档给新话题腾出干净的上下文空间。具体实现上我维护了一个“话题分段”数据结构每次用户消息进来先做一次轻量的领域分类如果当前消息与上一条消息的领域相似度低于某个阈值就认为话题切换触发一次压缩同时把旧话题段存入长期记忆库。4.3 压缩丢失信息的兜底方案压缩必然丢信息最怕的是丢掉用户明确说过的决定。一旦用户问“我之前说的那个预算限制是多少”而压缩摘要里恰好没写这条系统就答不上来。兜底方案是在每次压缩前把完整的原始消息存储到外部日志系统并加上一个可检索的标签。这样压缩摘要负责“让模型聊下去”原始日志负责“让系统可以精确回答‘刚才我说过什么’”。当用户问到类似“我是不是提过……”的问题时系统先尝试从摘要里找找不到就触发检索填充模式去日志系统里按关键词搜原文再把原文片段注入当前上下文。这个组合我在第 5 部分详细说。踩坑提醒压缩后用摘要替换历史时要记得把摘要的消息角色设为“system”或“user”不要用原对话的“user”角色防止模型误认为这是新的一轮正在进行的对话导致时间线混乱。5. 检索填充模式海量资料下依然精准5.1 从“全量塞给模型”到“按需检索填充”知识库类系统最忌讳的就是把整个文档库塞进上下文。假设你有 10 万字的资料即使全部塞进去模型也可能因为中间注意力衰减而漏掉关键内容而且费用高得离谱。检索填充模式的核心思路是先让用户问题去检索一个外部索引找出最相关的 3 到 5 个片段然后把片段转成文本注入 prompt。这个模式的关键在于“检索什么”。我们初期用关键词匹配效果很差因为同义词和近义表述太多。后来改用向量检索把问题转成语义向量再计算和文档片段的相似度准确率提升非常明显。5.2 检索式上下文的工程链路一个完整的检索填充链路包括四步第一步文档切片。原始文档按段落或语义边界切成长度均匀的片段一般每片 200 到 500 字。太短则上下文碎片化太长则向量表示模糊。切分时要注意保留标题信息可以在每个片段前拼接“所在章节XXX”这样检索时能带上位置语义。第二步向量化入库。把每个片段用嵌入模型转成向量存入向量数据库。我习惯同时存储原文和向量以及文档来源、更新时间等元数据。第三步查询触发。不是每一个用户问题都需要走检索。像“你好”“谢谢”这类问候语触发检索纯属浪费费用。用一个简单的意图分类器做预判只有判断为知识查询时才触发检索。第四步结果注入与格式约束。将检索到的 Top-K 个片段拼接成一个“参考资料区”放在用户问题前面并用明显的分隔符标识【参考资料】 片段1…… 片段2…… 【用户问题】 ……给模型明确的格式提示能有效减少“模型用自己的想象补全文档内容”的情况。5.3 检索模式与压缩模式组合使用的玩法检索填充和压缩模式不是互斥的它们可以配合得非常妙。我最终采用的方案是压缩模式负责“节省 Token”检索模式负责“补全细节”。举个例子用户第一轮问“有哪些日志采集方案”系统检索了 4 个文档片段并生成了回答。这时把检索到的片段全部丢弃只保留回答内容进入窗口。第二轮用户说“详细讲讲方案 B”此时如果第一轮的片段还在就能直接引用如果已经滚出窗口就触发一次检索“方案 B”这个关键词会重新命中原始文档片段。这个组合的好处是窗口区永远只保留“模型的输出”检索区只负责“当下这轮所需的外部资料”两者互不干扰上下文总量被控制在很稳定的水平。提示检索填充模式要特别注意“检索结果质量兜底”。如果 Top-K 片段的相似度都很低比如最高分只有 0.5此时最好不要硬塞给模型而是返回一句“当前知识库中未找到相关内容”否则模型会一本正经地编答案。6. 常见问题与实战排查实录6.1 上下文模式切换时的“记忆断层”症状系统在窗口模式下聊得好好的切到压缩模式后用户发现模型忘了之前讨论的细节。原因多半是压缩摘要质量太低或者压缩时机太晚——上下文已经爆了才想起来压缩摘要模型面对的输入太长提炼效果很拉。排查步骤查看压缩触发时原始对话的 Token 量确认是否在预算的 70% 范围内。检查压缩 prompt 里是否明确要求保留“决定、日期、数字、用户偏好”等关键信息不明确的压缩 prompt 会让模型抓不住重点。在压缩前后各保存一份上下文快照人工对比摘要丢了什么。我后来在压缩 prompt 里加了“如果信息不确定宁可多保留也不要省略”的指令效果很直接。6.2 Token 暴涨、响应变慢的原因排查这通常是三种情况叠加窗口轮数太多、检索片段太长、全局区过载。可以用一个简单的日志字段定位在每次请求时记录system_token_count、window_token_count、retrieval_token_count、output_token_count四个值。如果 token 暴涨看是哪个区域涨了。system 区涨检查是否有动态内容错误拼接进了系统提示词。window 区涨检查是否关闭了压缩触发条件或窗口轮数设置过大。retrieval 区涨检查检索 Top-K 是不是被调大了或者文档切片过大。定位到区域后按第 3 部分的预算估算方法重新分配即可。6.3 上下文污染问题上下文污染指模型收到来自不同来源但互相矛盾的信息。典型场景全局模式说“你是物流客服”检索到的片段却是一份关于售后服务的话术模型就会左右横跳。我的处理原则是“分区隔离 优先级声明”。在 prompt 中明确标记每个区块的权威级别[系统指令]最高优先级回答风格和边界遵循此区。 [参考资料]优先于用户历史但低于系统指令。 [对话历史]最低优先级仅作语境参考。这样模型在面对矛盾时有一个明确的裁决规则。实践中确实能显著减少精神分裂式的回答。6.4 四种模式常见问题速查表问题现象可能原因排查手段模型忘记用户偏好全局区缺少用户画像检查 system prompt 是否包含画像模板聊了 10 轮后回答变差窗口覆盖不够统计近 10 轮 Token重算窗口容量转话题后模型还在聊旧话题未触发压缩归档对话前做领域分类检测话题切换用户问“我说过什么”答不上压缩摘要丢细节压缩前存原始日志接检索兜底回答内容与资料不符检索片段不相关检查向量相似度阈值低于 0.5 时拒绝回答费用居高不下全局区或检索区过大分别统计各区块 Token按预算压缩7. 从模式到体系一套完整的上下文分层设计7.1 四种模式如何协同工作单独的某一种模式都有短板组合起来才能形成体系。我的最终架构可以用一句话描述全局固定打底窗口承载最近压缩沉淀长期检索按需填充。请求进来时系统按顺序组装全局上下文区固定在最前压缩摘要区放最近一次话题切换时生成的旧话题摘要窗口上下文区放最近几轮对话检索填充区放本轮检索到的外部资料当前用户问题。这个顺序不是拍脑袋的而是按“信息优先级从高到低、时效性从旧到新”排列的。模型读取时先看到的是不可违背的规则然后是长期记忆再到短期对话最后才轮到需要临时理解的外部资料。7.2 模式动态切换的策略复杂的系统不会从头到尾只用一种模式而是根据当前状态动态切换。我实现了一个简单的状态机状态 A轻聊对话轮数小于 3不触发检索只使用全局 窗口。状态 B知识问答检测到知识类问题触发检索填充此时优先保留检索区 Token。状态 C深度长聊总 Token 达到预算 70%触发压缩将旧话题转为摘要。状态 D跨会话会话结束后把全局画像和压缩摘要持久化到存储供下个会话加载。切换逻辑不要写得太复杂。我最初试图做一个用模型来决策“何时检索、何时压缩”的智能路由器结果模型决策本身的 Token 开销比省下来的还多而且不可控。后来改成基于明确规则的信号触发稳定得多。7.3 可观测性与调试方法上下文模式最怕“黑盒”——你看不见模型到底看到了什么。调试时必须有能力把每次请求的完整上下文导出成文本方便人工检查。我的做法是在每个上下文区块之间加入醒目的标记分隔符比如[GLOBAL_START]、[GLOBAL_END]然后每次请求前后各打印一份完整 prompt 到日志。导出后可以直接用文本对比工具查看丢失了什么、混入了什么。另外建议给每个会话维护一个“上下文操作事件流”记录每次增删改的时机和原因比如“14:02 触发压缩因为窗口达到上限”“14:05 检索到 3 个片段因为用户提到‘预算’”。这样即使上线出错也能逆向还原整个上下文变化过程。一些后话说实话context-mode 不是一个能一键安装的开源库也不是某篇论文里的专用术语。它更像是我在做了多个对话式系统之后沉淀下来的关于“上下文治理”的工程经验。每次在新项目里重新梳理这四个模式都能发现自己上一版设计的疏漏。踩过几次坑之后我的体会是不要一上来就去微调模型也不要把精力花在找更便宜的 API 上。把上下文的使用模式理清楚往往能从根上砍掉一半 Token 开销同时让回答质量上一个台阶。如果你正在做一个对话式产品建议先把现有的 prompt 按这四种模式分个区统计每个区的 Token 占比再重新分配预算——你大概率会发现之前很多钱都花在了模型根本不该看的内容上。这套东西后续还可以往“多模态上下文”和“多智能体共享记忆”方向扩展但先把单会话的四种模式跑稳才是地基。