
前两天有同事问我context-mode 到底是什么——这个词最近在开发者群里反复出现有人说它是 AI 助手的记忆开关有人说它是上下文窗口的分配器还有人觉得它只是产品经理造出来的新名词。我自己的看法比较直接它就是解决“AI 聊得越久越蠢”这个问题的工程化方案。我最近正在维护一个帮团队把会议纪要整理成待办事项的内部小工具因为早期实现太粗暴把整个对话历史不加选择地全都塞给模型结果一长就跑偏。后来我专门开发了一个名叫 context-mode 的模块负责管理“每次请求到底携带哪些上下文”。这篇文章会把它的背景、原理、实现和翻车记录完整拆一遍适合那些正在做 AI 产品、或者被自家 AI 助手的长期对话质量困扰的朋友参考。1. 为什么我需要一个专门的 context-mode从一次翻车说起1.1 “周六取快递”被理解成登录校验的现场先说那次让我下决心动手的翻车。团队内部用的会议纪要工具核心功能是把语音转写稿变成待办事项本身不复杂。某天有同事在对话里说“周六上午取快递记得加进待办事项清单”工具却回复了一堆“请先完成登录校验”的操作指引。那位同事当场发了个截图到群里配上两个字离谱。我第一反应是“模型又抽风了”但翻请求日志时发现根本不是模型的问题。那会儿我的工具相当“朴实”——所有历史消息按时间顺序原封不动拼进 prompt整个对话上下文里还躺着三天前排障时贴进去的一大段报错堆栈以及一条写着“正在调试登录模块”的旧指令。模型看到“取快递”这条新消息前后全是“登录模块”相关内容自然把“取快递”当成了登录会话里的新指令串线串得理直气壮。再往深翻更难看。那段报错堆栈占了上下文总 token 的 31%而真正跟本次任务相关的只有最近两条消息加起来不到 10%。这就是“上下文失控”的典型现场——模型没有变笨是我给的原材料烂掉了。1.2 context-mode 到底管哪三件事那次之后我意识到对话系统需要一个专门模块来管理上下文不能指望模型自己“挑重点读”。我给这个模块取名 context-mode核心职责拆成三条刚开始很简单后来随着踩坑慢慢变全。范围scope决定一次请求里可以携带哪几类信息包括当前对话轮次、系统指令、外部文档、长期记忆卡、工具返回结果。范围定得越清楚模型收到的东西就越克制。保留retention决定对话历史如何老化、压缩和淘汰。不是所有旧消息都值得留更不是所有新消息都足够重要必须有一套打分和降级规则。重置reset决定上下文什么时候清空、什么时候在摘要基础上重建。会话不可能无限延续主题切换、任务完成、用户显式指令都应该触发重置。打个生活化的比方你开会时只需要带着本次议题、最新几轮发言和上一份会议纪要不需要把三个月前每次闲聊的原文全文背诵出来。context-mode 就是做这件事的会议助理——它决定带什么进会议室、什么东西该被压缩成一页纸、什么时候这场会该散场重开。1.3 我给它划定的边界模块边界一开始就得画清楚否则很容易写成一个什么都管的“大泥潭”。我的定位是context-mode 是“进模型之前的数据编排层”它只负责筛选、排序、压缩信息不干预模型推理过程也不负责生成回复。输入是原始消息流、外部文档、系统配置和长期记忆库输出是一段长度受控、优先级明确、保留关键指令的 prompt 片段。它本质上是个“安检员”不是“翻译官”。所有关于“这句该怎么接”的事都留给模型自己决定。边界画清楚的最大好处是出问题时排查范围非常明确。如果模型回复跑偏我先查 context-mode 构造出来的 prompt 有没有脏数据再考虑模型本身的问题。后面几次线上事故基本都在这一层就定位到了。2. 上下文膨胀的六类噪声先搞明白自己是怎么死的2.1 一张真实的 token 账单动手写 context-mode 之前我先做了一次“上下文审计”把线上工具跑了一周统计每次请求里各类消息的 token 占比。结果让我很吃惊一张典型账单长这样消息分组条数token 占比我对有效性的判断系统提示词18%高最近 3 轮的真正任务消息622%高三天前的排障日志和报错堆栈931%低用户粘贴的原始大表格217%中中间过程的思考草稿59%低寒暄、感谢、重复确认1213%很低看到这组数据最简单的结论就是上万 token 的上下文中真正对当前任务有用的可能只有两三成。剩下的大部分是历史包袱、临时粘贴内容和无效寒暄。模型不是被“信息太多”撑死的而是被一堆低质量信息挤占了注意力真正重要的话反而被淹没。2.2 六类高频噪声的通用清单把一周日志里所有低质量内容归类后我总结了六个最常见的问题类型其他团队做同类模块时应该也会遇到陈旧指令残留。用户上一轮说“改用中文回答”这一轮已经切换场景但旧指令还死死留在上下文里持续影响模型行为。过期消息和调试堆栈。排障时粘贴的报错、临时打印的日志、中间变量输出这类内容对当前任务几乎没用却极其占地方。往返寒暄。“好的”“谢谢”“我明白了”这种消息数量一多会腐蚀模型对关键信息的敏感度。复制粘贴的大段原始内容。用户把一份几十行表格或几百行日志直接粘进来实际需要用到的往往只有其中几列。模板化重复文本。有些上游系统每轮都会自动追加一大段标准说明内容每次都一样属于“看似有用实际无用”的僵尸信息。工具返回的原始结构。调用搜索或数据库后把完整 JSON、完整列表直接塞进上下文没做字段裁剪和摘要。2.3 为什么“多塞点”反而会更蠢很多人觉得上下文窗口那么大全塞进去不就行了我踩完坑后的回答是窗口变大了但模型对长文本的注意力是有限的信息一多、相关性一弱精度会肉眼可见地下降。这不是玄学而是我在多次对比里看到的真实差异——同一道问题上下文里只有 3 条相关消息时答得又快又准塞进 30 条无关历史后它开始把注意力分配给“三天前的报错堆栈”然后一本正经地编答案。更麻烦的是指令冲突。如果上下文里同时存在“请用中文回答”和“Please always reply in English”两条旧指令哪怕用户这轮没提任何要求模型也可能在两个指令之间摇摆。上下文管理本质上是在帮模型做“注意力节食”——宁可少吃也不能乱吃。另外 token 成本也会线性上涨深度模式下单次成本能翻四倍效果却不一定变好。3. 核心实现优先级排序、摘要降级与上下文重置3.1 优先级排序先淘汰再按原顺序拼装context-mode 的第一版实现很简单就是对每条消息打一个优先级分按分数从高到低筛选保住预算内的高分消息。打分规则我用 Python 写了个大概核心代码如下def score_message(msg): score 0 if msg.kind system: score 100 if msg.kind latest_user: score 80 if msg.kind instruction: score 70 if msg.kind memory_card: score 40 if msg.is_recent(within_rounds3): score 30 if msg.kind chitchat: score - 20 if msg.has_debug_stack: score - 40 return score def build_context(messages, budget8000): scored sorted( ((score_message(m), i, m) for i, m in enumerate(messages)), reverseTrue, ) kept [] used 0 for score, index, msg in scored: if used msg.token_count budget: continue kept.append((index, msg)) used msg.token_count # 排序只是淘汰的依据拼装时仍然按原始顺序恢复 kept.sort(keylambda x: x[0]) return [msg for _, msg in kept]这里有个很容易踩的坑排序只是为了决定“谁被淘汰”最终拼进 prompt 的消息一定要按原始对话顺序排列。我第一版没注意直接把排序后的列表拼进去了模型看到的对话是倒着来的逻辑完全乱套。记住这个原则筛选时可以打分排序呈现时必须时间有序。打分规则也不需要一开始就做得很完美重要的是把“系统指令”“最新用户消息”“新增指令”这几类权重抬高把“寒暄”“调试堆栈”压下去。后面遇到具体问题再微调即可。3.2 摘要降级长对话自动压成记忆卡光靠淘汰解决不了所有问题。某些早期消息虽然旧但信息价值很高比如“用户确认了报销规则改为线上审批”直接删掉可惜。这个场景我用的是“摘要降级”当候选内容超过预算阈值就把较早但有保留价值的消息合并成一张结构化记忆卡而不是保留原文。记忆卡我设计成了 JSON 格式字段精简方便未来做召回{ memory_card: { id: mc_20250618_001, time_range: 2025-06-15 10:00 ~ 2025-06-18 09:30, topic: 报销流程修改, key_conclusions: [ 报销单统一走BPM系统, 附件上限调整为10个 ], unfinished: [ 审批节点待IT配置 ] } }触发摘要的条件是“上下文总长度超过 summarize_threshold 且 5 条以上早期消息”一旦触发最早的一批有价消息被替换成一张记忆卡。这样既保住了关键结论又把 token 占用压缩到一个很小的比例。实际跑下来一张记忆卡平均只占 200 到 300 token却能让模型在 50 轮之后仍然记得“报销规则改过”。3.3 上下文重置什么时候该“散会重开”不是所有对话都需要无限延续有些会话就是聊完一个任务就该结束。context-mode 的重置机制设计了三个触发条件显式指令用户输入“我们重新开始”“换个话题”“清空上下文”或使用我预留的/context reset命令。主题漂移检测对最近 10 条消息做向量化计算与之前主题的相似度。如果连续 3 轮相似度都低于阈值自动进入重置流程。任务完成信号工具检测到“待办已全部生成”“报销单已提交”这类任务闭环标志主动询问是否重置。重置不是把上下文直接清成空白而是把当前主题的简要摘要写回长期记忆库然后新开一个干净窗口。这样下次聊到相关主题时记忆卡还能被召回用户也不会有“你完全失忆了”的撕裂感。自动重置存在误伤风险所以我在主题漂移检测上加了一层“延后确认”检测到漂移后先记录连续 3 轮才真正执行给用户留出“我还没聊完”的缓冲空间。4. 上线后踩过的三个坑从截断、指令丢失到越答越慢4.1 截断撕碎了最新用户消息模型开始胡编context-mode 上线后的第一起事故是有用户问“上周三的报销单到哪一步了”工具居然回答“找不到相关单号”。用户很确定自己是第一次问这个问题我也很确定历史消息里存在报销单的元数据。排查链路是这样的先看请求日志里 prompt 的最后一条用户消息发现只剩半句——“上周三的报销单到”后面内容被截断了。再查 build_context 的逻辑发现我在执行预算截断时直接把超长部分一刀切连“最新用户消息”也被切了。模型拿到一条不完整的指令自然只能“合理”地猜测用户意图猜错了。修复方案有两个都比较简单但效果立竿见影增加reserve_last_user_message配置项保证最新一条用户消息永远完整进入上下文。预算里预留 20% 的“硬保护区”这部分 token 不允许被截断专门保底关键内容。这个坑的教训是所有的排序、压缩、截断策略都必须把“当前用户请求”放在不可动摇的位置。上下文管理再精细也不能为了节省 token 而牺牲当前指令的完整性。4.2 用户纠错指令被当成普通消息淘汰了第二个坑更隐蔽。用户跟工具说“不对你要用表格输出”工具依然固执地用列表回答。初看像是模型不听话但查完上下文拼接结果后我发现那条纠错消息根本没进模型的眼睛——它被打分成了普通用户消息在预算超限时被淘汰掉了。为什么会淘汰因为纠错指令通常是短句token 数少而且我的打分规则里没有“指令识别”这一项。它既不是 system也不是最新消息只靠“is_recent”加了 30 分在几千 token 的竞争里根本排不上号。修复方法是把“指令提取”单独做一层用正则和简单意图规则从最新几轮用户消息里找出祈使句包含“应该”“必须”“不要”“请用”“换成”等词截取后单独放入一个高优先级缓存区。每次构造 prompt 时这个缓存区的内容固定放在系统提示之后不参与普通淘汰。这件事让我重新理解了 context-mode 的职责它不只是流量控制更是“指令优先级管理”。用户临时给出的修正意见本质上比大部分历史内容重要得多它的优先级应该跟系统提示同级。4.3 “永远不忘”深度模式把延迟从 2 秒拖到了 11 秒第三个坑是我自己搞出来的。为了测试长期记忆效果我把配置调成了“无限记忆模式”——长期记忆库里的相关文档全部召回全部塞进上下文。结果第二天测试时单次请求的首字延迟从 2 秒暴涨到 11 秒。排查链路比较清晰先看耗时拆分发现时间主要花在模型首字生成阶段再看 token 统计上下文从 3k 涨到了 22k。原因是我实现的长期记忆召回没有上限向量库检索到了 40 段相关文档全被并进了 prompt。窗口越大首字延迟越高这是典型的“上下文过载”问题。修复不是取消记忆而是给召回加三道锁retrieval_limit限制召回文档数量、similarity_threshold过滤低相关度结果、retrieval_budget_ratio控制召回内容占用总预算的比例我最终定为 0.3也就是召回最多占三成上下文。后来我干脆把模式分成了三档方便用户自己权衡模式上下文预算(token)召回文档上限平均首字延迟单次相对成本轻量模式400001.2s1x标准模式800052.4s2.1x深度模式16000206.8s4.3x三档模式上线后“永远不忘”的用户可以选深度模式但代价是延迟和成本看得清清楚楚。这个设计也印证了一个观点上下文管理本质上是把“信息量、延迟、成本”三者放在同一个滑杆上用户得自己决定拖到哪一端。5. 落地配置与选型经验5.1 一份可以直接抄的公开配置如果你也想在项目里做一个类似的 context-mode我这份 JSON 配置可以作为起点{ context_mode: { enabled: true, budget: 8000, reserve_last_user_message: true, hard_reserve_ratio: 0.2, summarize_threshold: 6000, max_memory_cards: 3, retrieval_limit: 5, retrieval_budget_ratio: 0.3, similarity_threshold: 0.6, reset_on_topic_shift: true, topic_shift_rounds: 3, manual_reset_command: /context reset } }每个字段按我的理解解释一下配置项含义推荐值budget上下文硬预算超过就触发筛选和截断8000reserve_last_user_message是否完整保留最新用户消息truehard_reserve_ratio预算中不可被排序淘汰的比例0.2summarize_threshold超过多少长度触发摘要降级6000max_memory_cards单次请求最多放几张记忆卡3retrieval_limit长期记忆最多召回几条5retrieval_budget_ratio召回内容占用预算比例上限0.3similarity_threshold记忆召回的最低相关度0.6reset_on_topic_shift是否启用主题漂移自动重置truetopic_shift_rounds连续几轮漂移才触发重置3manual_reset_command手动重置命令/context reset建议不要一上来就把 budget 调到 16000 以上先用标准配置跑一周观察用户反馈和 token 账单再按需调整。配置项多不代表效果好关键是每个参数都对应一个可观测的指标。5.2 什么场景该开、该关、该用深度模式不同产品对上下文管理的需求差异很大。简单问答场景翻译、代码片段解释、算数直接用轻量模式甚至关掉 context-mode 反而更好上下文越短首字延迟越低答案也越干净。多轮任务型场景比如客服、售后、行程规划标准模式最合适——保留最近几轮关键对话配合少量记忆卡既不断线也不会被陈年历史干扰。涉及文档分析、知识库问答、代码仓库检索的场景深度模式确实有用但必须把召回上限控制住否则就会重蹈我 11 秒延迟的覆辙。我自己的原则是先用尽量小的上下文验证问题能不能解决再逐步扩大而不是一上来就追求“全知视角”。对多数产品来说八千到一万二的预算已经能覆盖绝大部分需求。5.3 同类工具的“上下文模式”到底怎么选现在很多工具都提供了类似功能只是名字五花八门——有人叫记忆有人叫上下文有人叫会话策略。本质上都是 scope、retention、reset 这三件事的不同组合。选型时我习惯只看三点默认截断规则它到底怎么决定哪些内容进、哪些内容出如果连文档都不写清大概率是拍脑袋写的。召回上限长期记忆能召回最多几段有没有相关度阈值没有上限的记忆功能都是延迟炸弹。token 透明度能不能在日志里看到每次请求实际消耗了多少 token、哪些内容被截断看不到明细的工具我基本不会在生产环境用。在这三个指标面前功能名反而没那么重要。配置项做得再多如果拿不出可观测的指标出了问题就只能靠猜。这个模块我一共维护了一个多月最大的体会是好的上下文管理模式不是“尽量多记”而是知道什么时候该主动忘。系统提示和用户当前指令永远最高优先价值高的旧信息压缩成记忆卡无关噪声果断丢弃并且给用户一个显式的重置入口——这套框架帮我解决掉了之前大半的“越聊越蠢”问题。最后再补一个实用建议上线后一定要盯住“上下文截断率”和“用户重问率”两个指标前者反映你的预算设置合不合理后者反映模型有没有因为上下文缺料而答错。这两个数字比任何评测样本都诚实我的 context-mode 后续每一次调参基本都靠它们做判断依据。