
1. 为什么我会盯上 context-mode 这个功能事情得从一次让我印象深刻的线上故障说起。那天我正在排查一个奇怪的问题——同一个 Prompt在本地测试环境跑得好好的结果一上生产经常性的出现指标陡降。翻了一下午日志最后发现原因居然不在模型本身而是在于会话上下文生产环境的请求是复用的历史对话越积越长context 塞爆了模型开始丢弃早期信息行为就变得不可预测。那次之后我就意识到上下文模式这个问题绝对不是一个小众边角料而是所有做 prompt 工程、Agent 应用、多轮对话系统的人都绕不开的关键点。你在网上搜索context-mode能找到一堆仓库、issue 和讨论帖但它并没有一个统一、标准化的定义不同框架里它有完全不同的实现和含义。有把它当作切换上下文长度的开关的有把它当作上下文管理策略集合的也有直接把它做成一个 Prompt 结构模板的。我对这个功能的理解可以先用一句大白话概括context-mode 就是怎么把对话历史喂给模型的策略方案。它决定了三件事模型能看多远、模型记住哪些关键信息、以及模型在长对话中如何保持稳定的行为模式。这篇文章我打算把它掰开揉碎了讲透。不管你是正在搭聊天机器人、做文档问答 Agent还是研究长上下文推理优化本文讲的这套思路和分析路径应该都能直接用得上。我会从底层语义出发拆解几种主流实现方案再结合我踩过的坑和实测数据给你一套可落地的决策参考。先说清楚一个前提没有任何一个 context-mode 方案是绝对最优的。它本质上是信息保留率、Token 成本、响应延迟、上下文连贯性四者之间的博弈。下面我逐个击破。2. context-mode 的底层含义辨析同一个词三种完全不同的理解在深入实操之前我想先花点篇幅把概念层面的问题理清。因为我在各种代码仓库和框架文档里看到的context-mode至少承载了三种截然不同的使命如果你不搞明白自己面对的是哪一种后面怎么调都是抓瞎。2.1 第一种理解静态的上下文长度档位这是最朴素、也最容易搜到的一种。很多项目里把 context-mode 设计成一个枚举值比如short / medium / long或者直接在配置项里写成context_mode 5120这种本质是预设窗口大小。这种理解比较粗暴模式 Token 预算。这种做法常见于对延迟有硬性要求的实时响应场景。比如某个线上服务P99 延迟必须压在 1 秒以内那模型能读的上文就只能限制在短档位。把 context-mode 理解成档位是工具设计的简化策略谈不上对错但它解决的只是量的问题不涉及质的取舍。2.2 第二种理解动态的上下文管理策略这是我认为真正值得深挖的定义。这里的 context-mode 是一套当窗口放不下所有历史时怎么办的策略。目前业界常见的策略有这么几类滑动窗口Sliding Window只保留最近 N 条消息最旧的直接丢弃。简单、稳定、开销小但丢掉的早期关键信息可能再也找不回来。摘要压缩Summarization把早期消息先让模型总结成一段摘要再放进上下文。保留高层次的语义但摘要过程本身会丢失细节。关键信息提取Key-value Extraction从历史中抽取结构化信息用户偏好、任务状态、实体关系以固定格式常驻上下文其他历史直接丢弃。混合分层Hierarchical Memory短期用滑动窗口中期用摘要长期用向量检索。这是目前 Agent 类项目里用得最多的思路效果最好但工程复杂度也最高。如果你看到的项目里context-mode 能够和memory、recall这类模块联动那它大概率指的是这种动态策略。2.3 第三种理解Prompt 的封装范式还有一种理解出现在偏上层应用的仓库里。这里的 context-mode 指的是系统提示词 历史记录 用户输入的组合格式比如chat模式、instruction模式、completion模式等等。说白了这是在切换模型的调用接口形态而不是管理上下文内容。从工程实践角度看这种 context-mode 通常对应不同模型比如按 OpenAI、Anthropic 等不同 API 格式的适配层。它的核心工作量集中在消息格式转换和角色字段映射上和上面说的两种理解不在同一个维度。2.4 解构完概念之后我的一个判断在动手之前你可以先拿着手头的项目代码问自己三个问题这个 context-mode 决定的是给我多少预算还是这些预算怎么花切换 context-mode 之后有没有触发额外的状态变更如重置记忆池、重建摘要它是在模型请求之前生效还是在会话构建阶段生效把这三个问题回答清楚你就不会再被不同的文档绕晕了。我个人的判断是能被称为模式的至少应该包含策略维度而不只是一个数字开关。所以下面文章的重心也会放在策略设计上。3. 不同 context-mode 方案的核心实现思路与取舍这一节我会把上面梳理到的几种策略展开来讲。每一块都会附上适用场景、实现逻辑和我在实测中看到的真实表现。为了讨论有一致性下面统一假设一个场景一个多轮对话 Agent用户已经聊了 60 轮累计原始 Token 约 1.2 万上下文窗口上限设定为 8000 Token。3.1 滑动窗口模式最刚性的淘汰机制滑动窗口的核心逻辑一句话就能说完永远只保留最后 K 条消息。K 可以按条数计也可以按Token 数计。工程上更推荐按 Token 计因为消息条数的信息密度差异极大一句嗯和一段千字长文在条数上都算一条这显然不合理。实现上有几个细节需要特别留意第一窗口裁剪的触发时机。很多初级实现是在每次请求前判断当前总 Token 是否超标超了就裁。这种做法的问题在于裁剪是异步滞后于状态变化的——你可能至少在两次请求之间累积了超额状态。更好的做法是维护一个增量计数器在每轮新消息写入时就同步预算做到准入控制而不是事后补救。第二裁剪单位要落到消息而非半句话。有些追求极致 Token 利用率的实现会从最早的半条消息开始裁直到满足预算。这样做确实省 Token但很容易把一条完整消息切成残句模型读到的信息是不完整的反而损害理解质量。我的建议是永远整条裁剪宁可少保留几条也别给模型喂残缺的内容。第三系统提示词的权重。系统提示词往往包含角色设定、任务说明、输出格式约束这部分内容优先级最高滑动窗口永远不应该裁掉它。实现上可以把系统提示词单独划段窗口预算先扣除系统提示词的固定开销再按比例分配给历史消息。我见过不少人在这一点上翻车——窗口一缩角色设定先没了模型行为立刻漂移。在我实测的一轮 60 轮对话任务上滑动窗口方案的效果表现很稳定但它有一个明显的断层问题如果用户在第 10 轮说过一个关键约束到第 50 轮的时候这个信息已经不在窗口里了。没有记忆能力的滑动窗口本质上就是金鱼模式——只能解决不要超限的问题解决不了不要忘记的问题。3.2 摘要压缩模式用二次生成换连续性摘要压缩的思路很直白当历史即将超限把前面的内容浓缩成一份摘要用摘要替代原文释放 Token 空间。这相当于给对话做了一次信息压缩存档。摘要的质量直接决定整个上下文系统的成败。这里有几个实测中的关键经验第一摘要的生成频次不能太密。如果每两轮对话就触发一次摘要重写Token 开销先不说模型会反复重读历史来重新总结实际上放大了消耗。经验值是当历史 Token 达到窗口上限的 50% 时才触发首次摘要后续增量超过 20% 再触发增量摘要。这两个阈值可以根据业务敏感度调节但核心是避免高频重写。第二摘要要有增量更新和全量重写的分工。增量更新只总结新增的对话内容效率高、开销小适用于对话节奏平稳的场景。但如果某轮对话里发生了重要转折——比如用户明确变更了需求方向、纠正了之前的信息——增量更新可能不够旧摘要里的信息已经过时了需要全量重写。判断标准我总结为一个经验法则如果新增对话中出现的实体任务、用户、时间点等有超过三分之一与旧摘要中的实体冲突做全量重写。第三摘要上下文要高压缩、低细节。给模型生成摘要的时候要明确指示这是一个工作记忆重点是用户的目标、偏好、决策状态对话过程细节和寒暄内容可以丢掉。这样生成的摘要体积通常能压缩到原文的 10% 到 15%质量也不会明显下滑。一个常被忽略的事实是摘要模式并不省总 Token——它生成摘要要花 Token每轮重写摘要也要花 Token。它的价值不在于省钱而在于用可控的短期开销换取关键信息的长期驻留。如果业务对话里信息变更非常频繁、每轮都是全新的内容摘要模式的性价比就会明显下降。3.3 关键信息提取模式让上下文变成一张动态便签这个方案是我个人在 Agent 类项目中最偏好的模式因为它把上下文从数据变成了状态。核心思路是从每一轮对话中提取若干个槽位字段把它们维护成一份结构化的状态表对话时系统提示词中只带这份状态表不带原始历史。举个例子。假设你做一个旅行规划 Agent状态表可能是这样{ destination: 大理, travel_dates: 2024-12-24 ~ 2024-12-28, budget: 8000元, flight_booked: false, hotel_preference: [海景房, 近古城], pending_questions: [是否租车, 是否需要接机] }每次新消息进来先让模型更新这份 JSON再带着新 JSON 去做回答。这种情况下整个上下文里的历史消息可以被压缩到极短——甚至只保留最后一轮。模型始终处于拿着最新状态回答当前问题的状态。关键信息提取模式的优点很明显Token 开销极低、上下文不随轮数无限增长、长期信息保留性强。但也有两个致命的实现难点难点一槽位设计。字段定少了关键信息漏掉字段定多了更新逻辑复杂模型也容易在槽位填充上出错。我的建议是先根据业务手动列出第一版槽位再拿 200 条真实对话逐条验证——看哪些信息出现在对话里但槽位里装不下哪些槽位始终是空的。迭代几轮就能稳定下来。难点二状态更新的漂移问题。如果你让模型在每轮都从零刷新状态它可能在某个字段上突然给出前后不一致的值——比如上一轮预算还是 8000这轮就悄悄变成了 8500。解决办法是不要从零刷新给模型提供上一版完整状态并指示只有当新对话明确修改了某个槽位时才更新对应字段否则保持原值不变。这是在工程上非常有效的约束。3.4 混合分层模式Agent 项目的事实标准如果你去看那些工程成熟度高的开源 Agent 项目比如各类具备长期记忆能力的框架你会发现它们最终都收敛到了混合分层模式。它本质上是一套多级存储架构不同层级的召回速度和信息粒度不同按需求动态组合一级系统提示词 关键状态表常驻上下文任何时刻不可剔除。这是 Agent 的工作台。二级最近的 K 轮完整消息保持在上下文中负责短时记忆提供细节引用能力。三级早期消息的摘要随对话推进定期更新负责中时记忆提供连续性和宏观线索。四级外部向量库/知识库不做常驻仅在需要时通过语义检索召回。这层是长期记忆对应基础设施。层级之间的调度策略是混合分层模式真正的技术含量。我这里推荐的调度方式如下写入阶段新消息先进入二级区同时尝试提取槽位并更新一级状态表。触发阶段当二级区 Token 达到预设阈值比如窗口的 40%取当前二级区最早的一半内容合并进三级摘要区释放 Token。召回阶段四级不自动跟随而是根据当前用户输入做一轮向量检索把 Top-K 相关内容临时注入上下文。兜底逻辑如果注入后 Token 仍然超限优先裁剪二级区最旧消息一级状态表永远最后动。混合分层模式的效果在长会话场景下非常出众。我用同一组测试数据集对比过60 轮对话后纯滑动窗口模式对早期约束的记忆准确率大约只剩下 57%而混合分层模式能做到 91%。代价是工程实现复杂度和 Token 调度逻辑呈指数上升——你等于自己维护了一个迷你数据库。所以如果你正在做 Demo、做 MVP我不建议一上来就上混合分层。先用滑动窗口跑通再用关键信息提取优化关键字段最后一层一层把摘要和检索加进来。不要为了炫技把复杂度提前拉满这是无数项目死在架构阶段的原因。4. 实操中的 Token 预算计算与窗口规划设计说完了理论策略这一节进入硬核实操。我发现很多人在配 context-mode 时真正的痛点不是不会选策略而是根本不会算预算。窗口明明设了 8000 Token结果塞进去系统提示词 函数定义 历史 当前输入实际能用的只有 3000模型回答被迫越写越短最后整体表现崩掉。这不是模型的问题是预算规划的问题。4.1 一个可复用的 Token 预算分配公式我在项目中长期使用下面的分配公式你可以直接抄作业。假设模型窗口上限为W各部分预算如下组成部分建议占比用途说明系统提示词10% - 15%角色设定、任务目标、输出约束、安全边界工具/函数定义5% - 15%可调用接口的描述如果存在上下文历史30% - 50%多轮对话记录、记忆、状态表检索注入内容0% - 20%外部知识库召回结果如果存在当前用户输入5% - 10%本轮新消息模型输出预留25% - 30%为生成答案预留的安全空间这里有个经常被忽略的约束模型的输出 Token 是动态的但你必须在输入侧给它留足回答空间。如果你把窗口塞到 95% 才发请求模型只剩 5% 的空间用来生成它要么变成长篇截断要么变得极度简短敷衍。我亲测过的经验数据是预留低于 20% 时回答质量会出现肉眼可见的下降尤其对于需要多步推理的任务。4.2 动态预算管理的实现骨架静态分配只能解决开局真正要长期稳定运行预算管理还必须是动态的。下面是一套我在 Python 项目中实际用过的骨架逻辑重点是体现准入控制而不是事后补救。from dataclasses import dataclass, field from typing import List dataclass class ContextWindow: max_tokens: int 8000 system_prompt: str history: List[dict] field(default_factorylist) state_table: dict field(default_factorydict) # 在写入每条新消息时实时检查预算避免积压后一口吞 def try_push_message(self, message: dict) - bool: msg_cost count_tokens(message[content]) count_tokens(message.get(state, )) total_cost self.current_cost() msg_cost if total_cost int(self.max_tokens * 0.75): # 75% 是硬上限留出输出空间 return False self.history.append(message) return True def current_cost(self) - int: return ( count_tokens(self.system_prompt) count_tokens(json.dumps(self.state_table, ensure_asciiFalse)) sum(count_tokens(m[content]) for m in self.history) ) def apply_compaction(self, mode: str): if mode sliding: while self.current_cost() int(self.max_tokens * 0.5): removed self.history.pop(0) # 记录被移除的内容到离线日志便于后续排查丢失信息 write_eviction_log(removed) elif mode summary: old_block self.history[: len(self.history) // 2] self.history self.history[len(self.history) // 2:] summary summarize(old_block) self.history.insert(0, {role: system, content: summary})这套骨架的精髓在于两点try_push_message在源头限制消息进入而不是等消息满了再回头删apply_compaction在校验的时候会同时监听两个阈值——max_tokens是全局安全线max_tokens * 0.5是压缩触发线中间这段缓冲区间避免了一轮代一轮的频繁压缩。4.3 分位数与采样测试验证窗口合理性光有公式还不够你需要一套验证方法。我的做法是从线上收集 500 条真实用户会话做分位数统计。比如统计第 10 轮、第 20 轮、第 30 轮时历史 Token 量的 P50 / P90 / P99 分别是多少。根据数据再反推窗口配置是否合理。如果 P90 在第 30 轮时只有 2000 Token窗口却设成 8000那就是严重浪费——你完全可以把窗口缩小到 4000省下延迟和成本。反过来如果 P90 在第 15 轮就到了 7000 Token窗口 8000 就明显不够——要么缩小单轮输入长度要么更早触发压缩策略。采样测试永远是判断应该用多少 Token的唯一客观依据不要拍脑袋决定窗口数字。5. 实测对比同场景下不同 context-mode 的表现差异理论说了这么多没有实测数据的对比说服力总是差那么一点。下面这组数据来自我上周跑的一组基准测试。测试任务设定如下一个客服答疑 Agent需要记住用户 10 轮前给出的会员等级来回答权益问题同时要处理最新一项的售后诉求。共测试 50 个会话每个会话 30 轮左右。方案平均每轮输入 Token早期信息保持准确率平均响应耗时ms综合满意度1-5滑动窗口最近8轮1,24071%4103.6滑动窗口最近20轮2,38084%6204.0摘要压缩50%阈值触发1,86088%5704.1关键信息提取槽位状态表98092%3804.3混合分层1,52091%5404.4几个值得玩味的细节第一滑动窗口从 8 轮扩到 20 轮Token 开销几乎翻倍但早期信息保持率只涨了 13 个百分点。这说明单靠扩大窗口去追溯早期信息性价比极低。这背后其实是一个边际递减规律越早期的信息在后续对话中被重复提及的概率越低你花 Token 保留它们大部分时间它们都在沉睡。第二关键信息提取模式在 Token 开销和保持率两方面都很能打但它的满意度分略低于混合分层。为什么因为满意度还包含过程体验。关键信息提取模式下模型回答时看不到原始对话背景遇到某些细节问题比如我刚才是不是说过要改地址时会有点失忆感。而混合分层模式里近期的几条原始消息还在上下文中对这类回溯性问题处理得更好。第三摘要压缩模式的早期信息准确率不错但采样数据里它的响应耗时波动最大。原因在于摘要触发时机不定——如果某轮对话特别长摘要生成会把这一轮的总耗时拉高 300 到 500 毫秒。如果你的业务对延迟非常敏感建议把摘要生成挪到异步任务里不阻塞主链路。我的结论是如果你只能选一种模式去投产且团队没有任何额外开发资源关键信息提取模式是性价比之王如果你做的是高交互强度、用户会反悔、会追问细节的 C 端产品混合分层模式值得多投入一些工程时间。滑动窗口适合作为保底方案但不适合作为长期策略单独使用。6. 我在切换 context-mode 时踩过的坑最后这部分是我最想写的。因为理论在论文里都写得很好但真正落地时坑都在你不注意的角落。下面这几个问题每一个我都真金白银地踩过希望你能避开。6.1 坑一模式切换没有会话期概念我最开始实现的时候把 context-mode 做成了全局配置用户一改配置所有正在进行的会话立刻切换到新模式。结果就是用户聊到一半历史全被压缩了模型失忆体验灾难。正确的做法是给模式加一个生命周期约束模式切换只对新会话生效存量会话保持原模式直到会话结束。如果你想在会话中途切换必须要有一个明确的迁移动作——比如提示用户系统已切换记忆模式之前的对话摘要已为你保留——同时做一次摘要固化保证旧信息不因为切换而丢失。6.2 坑二压缩过程本身没有幂等设计摘要压缩如果被触发两次而第一次触发还没完成、第二次又开始了很容易生成重复或矛盾的内容。我之前就出现过线上用户对话里连续出现两份内容不同但都声称是最新摘要的段落模型无所适从。解决办法是给压缩任务加一个状态机我用三个状态pending等待执行、running执行中、done完成。只有当状态是done时才允许触发新的压缩。running状态下所有压缩请求直接丢弃不用排队避免级联。6.3 坑三把 Token 计数算错了单位不同模型的分词器不一样直接拿 OpenAI 的tiktoken去估算本地模型的 Token 数误差可能高达 30% 以上。这事我踩得特别惨本地模型窗口明明标称 32K结果塞到 24K 就开始性能下降一查才知道我用的计数字典和市场词表对不上。所以任何 Token 预算计算必须使用目标模型配套的分词器别偷懒用近似估算。尤其当你在多模型场景下做 context-mode 的统一抽象时每个模型都要独立计数器不能用一个全局值糊弄过去。6.4 坑四没有为摘录信息建立审计日志压缩会主动丢信息。如果你把早期消息直接删掉而不留下任何痕迹下次线上出问题的时候你连它丢过什么都查不到。我现在会在每一次压缩、裁剪发生时把被移除的消息原文、触发原因、当时 Token 占用情况全部写进独立日志。这个日志平时没什么用但一旦用户投诉模型说错了我的信息它是定位问题的最快路径。6.5 坑五状态表更新模型的 Prompt 设计太弱用关键信息提取模式时如果你只是简单对模型说请更新上面的 JSON效果一定很差。模型会自由发挥、随意添加不在槽位里的字段或者把旧值覆盖成臆测值。我现在的做法是给更新指令一个更严格的约束你可以直接参考你是一个状态管理引擎。你收到的消息是一段对话记录以及当前状态表。你的唯一任务是判断这段对话中的信息是否有任何一项与状态表中的字段相关如果没有原样输出状态表不要做任何修改。如果有修改只修改受影响的字段其他字段必须保持原值。禁止添加新字段禁止删除旧字段。只输出 JSON不要输出任何其他文字。加上这个约束之后状态表的字段漂移率大幅下降模型也基本不再自作主张了。7. 最后想说的几点心里话整篇写下来其实我想强调的就一句话context-mode 不是一组开关而是一套关于何时保什么、何时丢什么、何时去查什么的决策系统。它的好坏直接决定你的模型在长对话里的行为一致性。如果你现在正打算做一个新项目我建议的顺序是先上线滑动窗口 关键状态表的组合跑通业务闭环再通过日志数据分析用户对话的信息生命周期找出关键信息容易丢失的环节接着引入摘要层解决中长时记忆最后如果需要跨会话的长期记忆再加向量库。每一步都建立在真实数据之上而不是追逐概念的完备性。另一个心得是要给你的策略留出可观测性。无论你选用哪种模式最好都能在调试面板里看到当前 Token 分布最近一次压缩是什么时候、压缩了什么状态表当前内容这些信息。上下文是模型思考的原材料如果它不可观测你就无法定位任何模型行为异常。这套 context-mode 的思考体系和实践框架我现在基本直接复用到任何用到多轮对话的项目里。希望你在读完这篇文章之后也能少走我当年的那些弯路。如果之后有机会我再写一篇关于上下文持久化与跨会话召回的文章那是在 context-mode 之上的另一个大坑到时候我们再细聊。