
先交代一个背景我做过的项目里返工率最高的不是模型能力不够而是上下文没有管好。最近半年圈子里越来越多人讨论 context-mode 这个词说白了就是不再把“聊天记录”当成唯一记忆而是把灌给大模型的那堆文本按职能拆分、按场景组装、按预算分配。如果你正在做 RAG、智能客服、Agent或者任何需要和模型持续对话的应用这一套做法几乎绕不开。这篇文章把我踩过的坑、写过的代码、调参的思路都整理出来希望能帮你少走几个月的弯路。先从一个真实场景说起。我接手过一个智能客服项目demo 阶段一切正常一问一答很流畅老板很满意。结果一上线用户连续问三轮以上模型就开始答非所问甚至把上一单的收货地址带到这一单里有时候还自己编出一个从没出现过的折扣。我一开始怀疑是模型问题换了更强的模型问题照样存在。后来我把传给模型的 messages 拉出来看了一遍发现整个上下文就是一条平铺直叙的流水账系统提示词、历史对话、检索到的资料、用户实时输入全混在一起。模型不是变笨了它是真的分不清哪些是规则、哪些是历史、哪些是参考资料。这就是我理解 context-mode 的起点它不是一个新模型也不是某种精确到小数点的算法而是一套上下文治理框架。把原本混为一谈的上下文拆成几个职责互不重叠的模式系统说明、对话历史、检索资料、用户画像、临时输入。每一种模式有独立的生命周期、独立的组装规则、独立的 token 预算。最后再按优先级把它们拼装成一个结构化的上下文块交给模型。这个方法解决的不只是“记不记得住”的问题也解决了“token 花在哪里”和“回答依据是什么”的问题。我见过很多团队把注意力全放在提升模型推理能力上结果同一段 prompt 换个用户、换个会话就崩。原因几乎都一样你没有给上下文设计边界让它随着对话无限膨胀。context-mode 本质上就是给 LLM 应用加一道调度层类似操作系统里“进程”和“线程”的区分——不再是所有数据挤在一个内存池里而是按生命周期和访问频率分成热数据、冷数据、常驻数据。1. 先弄清 context-mode 到底在解决什么问题在动手拆解之前我们必须把它的核心问题定位清楚。否则很容易把它当成一个花哨的代码框架学完就忘。context-mode 解决的核心矛盾很简单大模型的上下文窗口是有限的资源而真实业务中值得被“记住”的信息远远超出窗口容量。1.1 把“对话历史”当唯一记忆坑在哪里最常见的做法是把用户和模型的历史消息全部塞进 messages 列表然后一路累积下去。我见过最夸张的一个项目会话还没到期messages 已经有几十万 token调用一次接口光上下文的成本就已经超过了回答本身。这还只是钱的问题更严重的是质量劣化。当历史消息里混入了大量低信息密度的寒暄、重复、纠错内容时模型需要在噪声中找重点。注意力机制虽然强大但并不意味着它能从一万个 token 里精准提取那个决定性的约束条件。我实测下来当上下文超过一定阈值关键信息反而更容易被忽略。比如用户在第 5 轮说过“不要推送短信”到第 23 轮模型又主动建议开通短信通知。它不是忘记而是那条指令被几百条闲聊淹没了。token 成本失控和质量劣化只是表象。更底层的问题是所有信息被当成同一种类型来处理。事实上“系统的角色设定”和“用户上一条消息”的生命周期完全不一样前者是稳定不变的后者是瞬间有效的。“上个月用户浏览过的商品清单”和“当前问题的检索答案”在时效性上也完全不同。把它们一股脑扔进同一个 messages 数组等于强迫模型自己去做信息类型判断。它当然能做但效果远不如你在上游把它明确分好。1.2 context-mode 是“显式上下文管理”的工程方案我给 context-mode 一个比较实用的定义根据具体业务场景把上下文划分成多个职责独立的模式各自采用不同的写入、更新和过期策略再按固定规则组合成最终输入。生活里有个很好的类比。你不可能把十年里做过的所有文件都摊在办公桌上。桌面只放当前任务需要的文件抽屉里放经常用的工具档案柜里放不常用但需要时能找到的资料墙上贴最常提醒自己的规则。桌面、抽屉、档案柜、便利贴就是不同的上下文模式各有各的存取策略。如果你的工位真的把所有文件都堆在桌面上工作效率必然崩溃——大模型也是一样的。那么一套完整的 context-mode 设计通常包含四层核心模式。第一层是系统上下文定义模型的身份、能力边界、输出格式属于常驻模式基本不变。第二层是检索上下文根据当前用户问题从知识库、数据库、文档里拉取动态内容按需注入。第三层是会话上下文只保留当前对话中最新的若干轮次或者经过摘要压缩后的历史要点。第四层是长期记忆上下文沉淀跨会话的用户偏好、事实信息和业务约束独立存储、按需加载。把上下文拆成这四层之后每个层都可以独立优化。检索层命中率低就调切片和重排不会影响会话层。会话层过长就做摘要压缩不会影响系统层的稳定性。这就像把单体应用拆成微服务每个服务可以独立扩容和修复而不是每次改动都牵一发动全身。2. 四种上下文模式的拆解与适用场景接下来我把四种最常用的上下文模式逐一拆开把它们的定位、写入方式、过期策略以及实操中应该注意的细节都讲清楚。理解这四种模式基本就能覆盖绝大多数对话型应用。2.1 会话上下文模式管好短期记忆别贪多会话上下文是最容易被理解也最容易被滥用的一种模式。它的职责是保存当前会话中“还需要用到”的信息。但注意大量历史信息其实是瞬时性的比如用户说了一句“好的谢谢”这句话在上下文窗口里没有任何保留价值。我用过的比较有效的方案是“轮次窗口 摘要压缩”的组合。轮次窗口是指只保留最近 N 轮我常用 8 到 12 轮或者根据 token 预算上限截断。超过窗口的早期内容交给一个小模型或规则脚本生成一段摘要把关键约束抽出来作为摘要块放在会话上下文的头部。例如用户在第 3 轮提到“预算 5000 元”第 23 轮说“预算提高到 8000”摘要里就要明确记录“当前预算 8000”。这时候截断掉原始轮次也不会有信息损失反而降低了噪声。这套方案的关键是窗口轮次的截断策略不是拍脑袋定的。我一般先用日志统计真实会话的平均轮数和关键信息出现的位置找出“80% 的关键约束出现在前几轮”再倒推窗口大小。如果盲目扩大窗口token 预算被历史对话占满检索上下文和系统上下文就被挤压最后模型看到的历史很多但参考依据很少质量下降。还有一点容易被漏掉会话上下文需要打上时间或序号标签。如果你在一次请求里把历史和当前输入平铺开模型对“哪句话是新说的”没有显式感知。我的做法是在会话块前面加一行“以下是本次对话历史最后一条是用户最新输入”用分隔符把历史与当前请求隔开给模型一个定位锚点。2.2 检索上下文模式让答案有据可依检索上下文本质上是把外部知识库的结果动态注入到 prompt 里最常见的实现就是 RAG。它的核心问题是当你拿到 top-k 检索结果之后怎么把它们组织成一个模型愿意读、且不会被误导的上下文块。先说检索结果的组织格式。直接把向量库返回的原始 chunk 拼在一起丢进去效果很差因为 chunk 往往带着切分痕迹上下文不完整。我的做法是检索到候选片段后先用一个重排模型cross-encoder精排把与问题最相关的内容提到最前然后对每条结果做一句轻量改写补全起止信息合并成“参考材料 1/2/3”的格式。再在整块前面加一句固定前缀“以下是从企业知识库中检索到的参考资料回答请优先基于这些内容并在无法回答时明确说明。”这个前缀不是可有可无的。它相当于给模型一个指令锚点告诉它“这部分是外部证据不是用户的话”。有一次我忘了加前缀结果模型把参考材料里的某个产品型号当成了用户在下单差点在客服场景里闹出事故。检索上下文还有一个常被忽略的参数查询改写。用户问“它支持蓝牙吗”其中“它”指代上一轮的商品如果直接用原文去检索召回效果会很差。我通常在组装检索模式之前先用一个小模型或规则模型做查询改写把指代词替换成具体实体。这一步对检索命中率的提升甚至比调 embedding 模型还明显。检索上下文并不是每轮都要注入。如果当前用户问题跟知识库无关比如“你好”“你是谁”强行注入只会浪费 token 和引入干扰。我在路由层加了意图判断只有命中知识库查询意图时才触发检索模式。这样既节省成本也避免无关资料干扰模型判断。2.3 长期记忆模式把用户偏好沉淀成稳定画像长期记忆模式解决的是跨会话的信息沉淀问题。最简单的应用场景是它会记住用户所在的地区、偏好、历史购买记录、沟通风格等。这些信息通常存储在数据库或向量库里每次会话开始时加载。长期记忆的写入机制比读取机制更值得花心思。因为在一次对话过程中用户表达出的信息不一定都是稳定事实。用户说“我住在北京”是稳定事实但用户说“最近搬家好累”不构成事实。如果每句话都写入长期记忆画像会被污染而且后续检索时会产生错误的上下文。我加了一道置信度机制只有满足“实体明确 陈述语气 上下文语境一致性”三重条件的信息才允许写入画像存储。实测中还有个教训长期记忆的读取时机也很关键。默认在 session 开始时读取所有画像信息如果画像积累很多可能超过 2000 token这会挤占其他模式的空间。我改成按业务维度拉取用户当前是来咨询订单的就只加载订单相关的画像字段不加载兴趣偏好字段。相当于给长期记忆加了业务维度的索引而不是一篮子全端出来。长期记忆和会话上下文之间还经常发生冲突。比如画像里记录“用户偏好简洁回复”但用户当前明确要求“请详细列出对比表格”。这种情况下必须以当前会话的最新指示为准。所以我在组装顺序上做了硬性规定临时指令的优先级高于长期画像。2.4 系统上下文模式边界与安全都靠它系统上下文模式是所有模式里最稳定的一种但也最容易被忽略。它的职责是定义模型的身份、任务目标、输出规范、可用工具和禁止事项。在 prompt 工程里它对应的是 system prompt 部分但在 context-mode 框架下它应当被当成一段独立的、版本化的代码资产来维护。我在团队里推行过 system prompt 的版本管理就像管理业务代码一样。每次修改系统上下文都要记录变更原因、影响范围并在回归测试集上跑一遍。有人觉得这是小题大做——不就是几行提示词吗但实际情况是你改一句“请用中文回答”可能在某类问题里引发连锁风格变化没有回归测试很难发现。系统上下文里还应该包含工具调用的“使用说明”。如果模型可以调用查订单、查物流、退货等工具那么工具的使用边界需要写清楚。比如“只有用户询问退货时才能调用退货接口并且要同时告知退款时效”。这能防止模型在模糊语境下自作主张调用不该调用的工具。工具上下文的边界感很重要。有一次我让模型在工具调用返回之后把原始 JSON 原样拼到对话历史里结果下一轮模型把 JSON 里某个数字当成了商品价格输出给用户。后来我把工具返回数据单独放在一个 mode 里标记为内部数据不在最终回答中直接复述。这类问题不拆分上下文模式的话排查周期会非常长。3. 实操手写一个可落地的 context-mode 管理器前面讲的是模式认知现在进入工程落地。我会带你从零设计一个轻量级 context-mode 管理器包含模式定义、组装顺序、token 预算控制以及一段可以直接改来用的 Python 核心代码。这套东西我已经在多个项目里复用结构不复杂但很稳定。3.1 组装顺序和预算分配是第一步不同模式在最终 prompt 里的排列顺序会直接影响模型对信息的重视程度。我的经验排序是系统上下文在最前长期记忆紧随其后检索上下文居中会话上下文在较后位置最后是用户当前输入。为什么这么排模型对 prompt 头尾关注度更高。系统上下文需要被严格遵守放最前面最合适。用户当前输入是本次请求的核心放最后恰好贴近模型输出位置。长期记忆和检索内容是背景材料放中间即可。顺序定了之后接着是 token 预算。我给每个模式分配固定比例用总预算的上限约束每个模式的用量。用一个典型业务举例总上下文预算按 8000 token 算系统上下文固定 1500长期记忆最多 1500检索上下文最多 2500会话历史最多 2000用户当前输入最多 500。各模式超出阈值时做截断或摘要而不是任其膨胀。这里有一个非常容易踩的坑只设总额不设分量。如果只控制总 token 不超过 8000检索模块可能撑到 7000系统上下文被挤得只剩几百模型的指令遵循能力会锐减。所以我在管理器里引入了“动态水位算法”当检测到用户当前输入变长时优先压缩会话历史摘要而不是压缩检索内容。3.2 核心数据结构与代码骨架我实现这个管理器时把模式定义成了一个枚举类每个模式对应独立的构建函数。这样扩展新模式的时候不需要改拼接逻辑。下面是一段简化但可直接运行的骨架示例。from enum import Enum from dataclasses import dataclass, field from typing import List, Dict, Any class ContextModeType(Enum): SYSTEM system PROFILE profile RETRIEVAL retrieval EPISODE episode TRANSIENT transient dataclass class ContextBlock: mode: ContextModeType priority: int content: str token_estimate: int 0 def render(self) - str: tag_map { ContextModeType.SYSTEM: 系统指令, ContextModeType.PROFILE: 用户画像, ContextModeType.RETRIEVAL: 参考资料, ContextModeType.EPISODE: 历史对话, ContextModeType.TRANSIENT: 当前输入, } title tag_map[self.mode] return f[{title}]\n{self.content}\n[/{title}] class ContextManager: def __init__(self, system_prompt: str, total_budget: int 8000): self.blocks: Dict[ContextModeType, ContextBlock] {} self.total_budget total_budget self.budget_map { ContextModeType.SYSTEM: 0.20, ContextModeType.PROFILE: 0.18, ContextModeType.RETRIEVAL: 0.30, ContextModeType.EPISODE: 0.22, ContextModeType.TRANSIENT: 0.10, } self.add_block( ContextModeType.SYSTEM, system_prompt, priority0, ) def add_block(self, mode: ContextModeType, content: str, priority: int): block ContextBlock(modemode, prioritypriority, contentcontent) self.blocks[mode] block def remove_block(self, mode: ContextModeType): if mode in self.blocks: del self.blocks[mode] def build_prompt(self, user_input: str) - str: self.add_block(ContextModeType.TRANSIENT, user_input, priority5) ordered sorted( [b for b in self.blocks.values() if b.content.strip()], keylambda b: (b.priority, -self._mode_order(b.mode)) ) parts [block.render() for block in ordered] return \n\n.join(parts) staticmethod def _mode_order(m: ContextModeType) - int: order { ContextModeType.SYSTEM: 5, ContextModeType.PROFILE: 4, ContextModeType.RETRIEVAL: 3, ContextModeType.EPISODE: 2, ContextModeType.TRANSIENT: 1, } return order[m]这段代码的核心不是界面而是两个思路一是用标签把不同模式的内容包裹起来让模型能感知到信息类型二是用优先级字段控制排序而不是写死 if-else。实际项目中我会把每个 block 的 token_estimate 用 tokenizer 实时计算一旦超出预算就调用对应的压缩器。压缩器和模式绑定比如 episode 走摘要模型retrieval 走重排截断profile 走字段裁剪。3.3 长会话场景下的窗口滚动策略对话应用最头疼的是长会话。如果一直把所有历史轮次都塞进 episode 块token 迟早爆掉。我用的策略是分阶段滚动前 6 轮保留完整原文之后的旧轮次每隔 5 轮做一次摘要合并。摘要合并时不是简单总结全文而是按“实体、约束、未完成任务”三个维度抽取要点。举个例子用户在第 10 轮说“我要把订单地址改成上海市浦东新区”之后又聊了 20 轮别的内容到第 30 轮模型应该仍然知道地址已经改成了浦东。如果在摘要阶段遗漏了这个实体后面回答物流问题时就会给旧地址。所以我的摘要 prompt 里固定要求保留所有地点、时间、金额、联系方式、明确承诺过的事项。这种“关键信息强制保留”策略比让模型自由总结可靠得多。我在日志里会记录每一轮滚动前后的 token 数量和命中率变化。如果发现某个窗口段压缩后后续涉及该信息的问答准确率下降就说明摘要模板抽取得不够完整需要补充抽取维度。这是一个持续迭代的过程不是写一次摘要函数就一劳永逸。3.4 模式和场景的解耦配置驱动而不是硬编码把模式和场景绑定死是灾难。比如同一个底层对话引擎可能要服务售前、售后、闲聊三个场景。售前场景需要大量检索商品参数售后场景需要更多订单信息闲聊场景几乎不需要知识检索。如果管理器里写死“每轮必须检索”三个场景都会出问题。我后来加了一层场景配置一个场景就是一个 context-mode 模板的集合。售前场景注册 SYSTEM RETRIEVAL EPISODE TRANSIENT售后场景注册 SYSTEM PROFILE RETRIEVAL EPISODE TRANSIENT闲聊场景只注册 SYSTEM EPISODE TRANSIENT。配置用 JSON 描述切换场景时管理器读取相应的配置文件动态调整模式集合和预算比例。这样做的好处很明显。新场景上线时不用改代码只加一份配置。而且每个场景的 prompt 回归测试可以独立跑某场景调了检索参数不会影响其他场景的会话表现。工程上线后维护成本低了很多。4. 排查实录context-mode 最容易踩的四个坑框架搭好了不等于高枕无忧。我把自己在真实项目里反复踩过的几个坑整理成排查速查表每个都对应一条可执行的处理办法。这些问题如果不提前关注上线后被用户投诉再去查成本就高了。4.1 上下文漂移模型开始“答非所问”现象是对话中段开始模型对早期用户指令执行得越来越不准确甚至违背明显约束。我用日志打印每次请求的完整 prompt对比早期和当前的系统上下文发现系统上下文没问题问题出在 episode 块里积累了大量与约束无关的闲聊。排查思路是给每一条上下文打上 mode 标签。如果发现 episode 块占比过大而系统约束被挤到低权重位置那基本就是漂移。解决办法有两个一是提高系统上下文的重复频率二是把关键约束提炼到 TRANSIENT 块前面。我在实际项目中会选择后者因为重复系统提示词会白白消耗 token而把约束显式放在用户输入附近模型更容易遵守。比如用户最早说过“不要推荐超过 5000 元的手机”我就在每次请求的 TRANSIENT 块里自动追加一句提醒“注意用户约束为价格不超过 5000 元。”这个提醒不是简单的原话重复而是从画像里取出来的实时摘要准确率明显提升。4.2 检索命中但模型就是不引用有一种诡异情况日志显示向量检索返回了正确的材料top1 的分数也不低但模型回答完全没用到它。我把 prompt 拉出来看发现检索材料被放在一个很长的系统提示词后面模型读到那里时已经出现首因疲劳注意力掉得厉害。解决办法是调整检索块的摆放位置同时给每条材料增加“编号引用”机制。比如每个参考资料开头标注“参考资料 1”回答要求里写明“引用时标注对应编号”。模型在生成时为了满足输出规范会更主动地参考编号蓝牙内容。实测这个机制对“是否回答有依据”这一项效果提升非常显著。还有一个隐蔽问题检索材料的文本长度如果超过上下文窗口的一半模型容易后续信息丢失。我统一把每个切片控制在 500 token 以内超过的直接丢弃或者用快速摘要压缩。宁可少给两条不要给一条超长材料。4.3 长期记忆和当前会话“打架”用户画像里写着“喜欢通过邮件接收文档”但当前会话明确说“这次直接发微信就行”。模型有可能按照大脑的低频习惯优先执行画像内容导致回答和用户最新要求冲突。我的解决方法是给长期记忆块加“覆盖标记”。当对话中出现与画像字段直接冲突的陈述时管理器会在这个 session 内临时屏蔽对应画像字段只在内存中保留一个待更新标记。会话结束后再根据这段冲突信息决定是否回写画像。这个机制不能只靠 prompt 写“以当前为准”而是要在数据层面把冲突字段摘除否则模型还是会在不同轮次间动摇。我自己实现过一个简化版就是在组装 profile 块时做一次字段级过滤。需要联网或外部系统时可以用一个轻量的规则匹配提取用户当前输入里的关键字段和画像 key 做比对遇到同一 key 就临时用新值覆盖旧值。三四十行代码就能完成收益很可观。4.4 token 预算失调导致延迟和成本飙升有些项目上线一段时间后接口延迟从 800ms 飙到 2.5s账单也跟着涨。一看日志空闲模式下会话上下文还在无脑累加检索模块每次请求都拉 10 条材料长期记忆加载了全部画像字段。三个模式各涨一点整体就爆了。我做的调整是三件事。第一为每个模式设置“空闲过期时间”超过 3 分钟没有新消息episode 块自动降级为摘要模式。第二检索材料数量从 top10 降到 top5并加上重排分数阈值 0.35低于阈值的不进入上下文。第三长期记忆的召回从“加载全部字段”改成“加载与当前意图匹配的字段”这一步直接让每次请求的 profile 块从 1800 token 降到了 600 token 左右。优化完之后延迟恢复到了 1s 以内成本降了大约四成。这三件事都不需要改模型纯粹是 context-mode 管理器的调优。提示排查任何上下文类问题时第一步先打开每次请求的完整 prompt 日志确认每个模式块的实际 token 和排序而不是凭感觉猜。我在项目里专门做了一个 prompt 对比工具把线上某次问题的输入和标准模板做 diff几秒钟就能定位是哪个模式块出了问题。5. 关于调优我最后想说的几句实在话做了这么多个对话项目我最大的体会是很多人一上来就在 prompt 里使劲加“请记住”“一定要考虑之前的内容”这些空话对后者模型基本没用。真正有效的做法是把上下文当成系统资源来设计明确哪些信息必须出现在哪些位置哪些信息应该被丢弃哪些信息需要被摘要。context-mode 给了我一个很好的思维框架它迫使你把模糊的“记忆”拆成可管理、可测试、可控成本的具体模块。如果你想从这篇文章开始落地建议按这个顺序行动。第一找出你当前项目里 messages 的唯一拼接点把所有历史记录替换成带标签的上下文块。第二给每个块加上 token 预算和截断策略先在日志里跑一周看每个模式块的实际占比。第三引入长期记忆和检索模式之前先把基础的系统模式与会话模式调稳再逐步叠加复杂度。一次别贪多稳定优先。还有一个很实用的小技巧我为每个模式块设计了独立的开关线上遇到质量问题时可以通过配置中心实时切换比如临时关闭 profile 块或者调高检索阈值不需要重新发版。这个能力在灰度验证新模式时特别有用。每做一个变更就在测试集上跑一遍主要场景的回归防止“修一个坑、掉一个坑”。context-mode 不是什么高深理论它就是把工程上已经验证过的模块化思想用在了 prompt 组织上。希望这篇文章能帮你把自己的对话应用从“凭感觉跑通”推进到“可排查、可优化、可复用”的阶段。