
1. context-mode 解决的痛点为什么上下文塞得越满模型越容易翻车最近在给团队的自研 Agent 工具加一个上下文管理模块名字就叫 context-mode。起因特别朴素——我们接入了三家不同厂商的大模型接口之后发现同一个任务A 模型我用了一套很长的 system promptB 模型根本吃不下同一个模型在不同的上下文窗口状态下输出质量忽高忽低。于是问题不再是往 prompt 里塞多少上下文而是哪些上下文该在什么时候、以什么形态进入模型。如果你也在做 AI 工具、Agent 编排、RAG 管道或者 prompt 工程这篇文章大概率对你有用。1.1 塞满上下文的模型为什么反而变笨了先说一个反直觉的实测结果。我们当时有个知识库问答场景用户会连续追问好几轮。最开始的想法很简单把所有历史消息全部塞进 prompt让模型记住一切。结果发现当上下文总长度超过某个阈值之后模型的回答质量不是持平而是肉眼可见地下降。具体表现在三个地方模型开始忘掉最前面的 system prompt 指令比如明明要求只输出 JSON它却开始说废话。面对用户最新的问题模型反而优先参考中间某一段无关紧要的历史记录答非所问。推理类任务比如多步计算、代码 Debug的准确率明显下滑模型会从旧上下文里捡错误信息来用。这就是典型的注意力稀释问题。Transformer 架构的注意力机制虽然理论上能处理长序列但实际训练出来的模型对长上下文中不同位置的关注度是非均匀的。中间位置的信息容易被挤掉指令和用户最新意图会发生竞争。我们后来做了一个简单的对照实验用同一道数学题分别塞 1K、4K、8K 的无关历史上下文准确率从 92% 掉到 78%再掉到 61%。当你把上下文当成内存随便塞模型反而变成了一个读过很多书但记不住重点的人。1.2 三类上下文的生命周期完全不同问题的根源在于我们一直在用一个一锅炖的方式处理三类生命周期完全不同的上下文上下文类型典型内容生命周期变化频率会话记忆用户历史提问、助手历史回答随对话轮次增长高频变化业务规则system prompt、工具使用说明、输出格式约束整个会话基本不变低频静态工具结果检索返回、API 响应、代码执行输出单轮或几轮内有效瞬时变化把这三类东西混放在同一个消息列表里丢给模型最大的问题是它们会互相干扰。业务规则是宪法本该始终占据注意力高位会话记忆是会议记录只需要保留与当前议题相关的部分工具结果则是临时便签用完就应该撕掉。context-mode 的核心思想就是给这三类上下文分别设计进入模型的方式和时机。静态规则走常驻通道会话记忆走滑窗摘要通道工具结果走短期缓存通道。这个分类逻辑是所有后续工程实现的地基。2. 注入式与代理式两条实现路线的取舍确定了要按类型管理上下文之后下一个问题是怎么实现。我和团队在方案评审时吵了两轮核心分歧在于context-mode 应该做成 prompt 层面的注入逻辑还是做成模型调用层面之外的代理路由逻辑。2.1 注入式把模式写进 system prompt注入式是最直接的做法。在拼装请求时根据当前对话状态动态生成一段模式指令放进 system prompt告诉模型当前应该怎么处理上下文。举个例子当检测到用户正在连续追问某个技术方案时我们会注入这样的指令system_prompt.append( 当前处于 context-mode: technical-deepdive 上下文处理策略 - 优先参考本会话中最近 3 轮与「技术方案」相关的讨论 - 忽略与当前主题无关的历史闲聊 - 如果历史记录不完整明确回答信息不足不要推测 )这种做法实现简单、调试直观prompt 里写什么模型就直接照着做。我们也确实在不少场景里看到了立竿见影的效果尤其是当模型跑偏是因为不知道要关注哪段上下文时一段明确的模式指令能把准确率拉回来不少。但注入式有一个明显天花板prompt 本身也在消耗 token而且模型对指令的遵循程度是概率性的。模式指令写得太长会挤占业务规则的空间写得太短模型又可能不当回事。更麻烦的是当上下文长度逼近窗口上限时注入的模式指令会反过来加剧注意力稀释。2.2 代理式在模型调用之外做路由代理式的思路完全不同。它不是去说服模型而是在调用模型之前由外部代码决定哪些内容能进 prompt、哪些不能进、按什么顺序进。我们实现了一个轻量级的 context router它拦截每一次模型请求按优先级组装最终发送给模型的上下文def build_prompt(mode, session, tool_results): if mode conversation: return assemble( business_rules, # 业务规则全量保留 sliding_window(session), # 会话记忆滑窗 last_tool_output(tool_results) ) elif mode troubleshooting: return assemble( business_rules, high_signal_history(session, max_turns8), diagnostic_output(tool_results) )代理式的优点是可控性极强放什么进去完全由代码决定不依赖模型对指令的理解。而且它天然适合做多模型适配——同一套路由逻辑可以给不同模型输出不同长度的 prompt。缺点是工程复杂度上来了你需要自己处理滑窗、摘要、缓存失效这些原本交给模型就行的事情。2.3 我们的选择混合策略以及为什么实测下来纯注入式和纯代理式都不是最优解。我们最终采用的是混合策略用代理式做硬性过滤保证任何情况下进入模型的上下文都不会超过预算上限也不会混入已失效的内容。用注入式做软性引导在系统 prompt 里保留一小段模式描述让模型知道当前应该以什么姿态处理这些上下文。这样做的理由很实际。代理式解决的是底线问题——防止上下文爆炸和内容串味注入式解决的是效率问题——让模型更精准地聚焦到关键信息。两条路线各管一层互不干扰。如果你打算自己实现 context-mode我的建议是从纯代理式起步先把上下文的闸门控住再考虑要不要加注入式的微调。3. 滑窗压缩与摘要降级把 token 预算花在刀刃上模式路由解决了放什么内容的问题但还有一个更硬的约束token 预算。无论用哪种模式最终拼出来的 prompt 必须塞进模型的上下文窗口。我们在实践中的经验是与其等 token 超限报错不如提前做好预算分配和压缩策略。3.1 先算清一笔 token 账我们以 4K token 上下文窗口的模型为例做了这样一个预算分配上下文类型预算占比预算量说明业务规则20%800 token系统指令、输出格式、工具说明当前意图15%600 token用户最新一条消息会话记忆45%1800 token滑窗 历史摘要工具结果15%600 token检索结果、函数返回值安全余量5%200 token防止模型输出阶段超限这笔账是为了防止上下文挤占输出空间。很多 Agent 工具出错不是输入太长而是预留的 max_tokens 太小模型生成到一半被截断。把安全余量写进预算是我踩了几次截断坑之后总结出来的硬性规则。3.2 两级压缩先滑窗再摘要预算定了之后真正难的是超了怎么办。我们实现了两级压缩按成本从低到高依次触发。第一级是滑窗压缩。只保留最近 N 轮完整对话更早的内容整段丢弃。N 的选择需要根据场景调我们内部定的默认值是 5。为什么是 5因为实测中大多数用户追问的问题只和最近 3 到 5 轮强相关再往前的内容模型重读一遍也提取不出更多有价值的信息。第二级是摘要降级。当滑窗内的内容仍然超出预算时就把最早的部分做一次摘要替代原始对话。这里有一个关键细节摘要不是把对话缩短而是抽取与当前主题相关的要点。我们用的方法是先提取当前用户问题里的核心实体比如产品名、报错码、版本号再带着这些实体去压缩历史记录只保留包含这些实体的片段。def compress_history(history, budget, focus_entities): if token_count(history) budget: return history recent history[-3:] # 最近3轮完整保留 older summarize(history[:-3], focus_entitiesfocus_entities) return older recent3.3 压缩质量怎么验证用实体召回率压缩之后最大的风险是丢了关键信息。我们监控一个指标叫实体召回率——把每轮对话里用户提到的关键实体订单号、报错信息、具体名词都标记出来压缩后再检查一遍计算有多少实体还在。这个指标很有用。有一次我们把历史压缩摘要上线后发现订单类问题的解决率掉了 8 个点查指标才发现实体召回率只有 63%再查是摘要 prompt 里忘了让模型保留数字型实体。调整之后召回率回到 94%解决率也恢复到了正常水平。所以我强烈建议任何做上下文压缩的工程都要有一个可量化的质量指标而不是看起来差不多。4. 上线前的两个大坑上下文泄漏与模式误触发的完整排查背景铺垫得差不多了来讲讲实际踩坑的经历。这两个问题一个发生在内部测试阶段一个发生在半夜上线后的 20 分钟里都属于不踩一次永远想不到的经典坑。4.1 上下文泄漏上一轮的内部指令闯进了下一轮现象是用户连续对话到第三轮时模型突然开始重复 system prompt 里的内部指令甚至把上一轮工具返回的原始数据片段当成自己的回答内容说出来。更诡异的是这个问题不是稳定复现而是随机出现频率大概在 5% 左右。我们花了两天排查完整链路是这样的第一步从日志里 dump 出出错请求的完整 payload发现请求里的会话消息列表混入了一段不该出现的内容上一条工具结果被重复拼接了一次而且拼接位置在 system prompt 和用户消息之间。第二步回查拼接代码定位到问题出在流式输出的落库环节。模型还在流式生成时我们就把未结束的响应片段写进了会话记录等到工具结果返回时又基于这份半成品记录做了上下文拼接。第三步进一步深挖根因发现是并发写会话导致的。两个请求共享同一个 conversation_id前一个请求还没写完后一个请求已经读到中间数据于是把截断内容当成完整内容拼了进去。修复方案分两层。第一层是流式输出落库前增加完整性校验必须等到 finish_reason 为 stop 才允许写入会话。第二层是会话记录增加 version 字段每次写入都自增拼接上下文时只认最新版本。这个 bug 的教训是上下文管理不只是怎么拼 prompt还包括什么时候把数据写进会话库两边的时序必须一致。4.2 模式误触发把闲聊当成了工具调用请求第二个坑是 context-mode 的模式分类误触发。我们设计了一个自动模式识别模块根据用户消息的关键词判断当前应该走普通对话模式还是检索增强模式。上线后 20 分钟线上收到反馈用户问你能不能帮我想想这个方案怎么写模型没有正常回答而是触发了一次检索并且把检索结果误当成方案内容输出了。排查链路第一步查看模式识别模块的 trace 日志发现分类置信度只有 0.41明显低于我们设定的 0.7 阈值但代码里没有对低置信度做兜底处理于是走了默认分支触发了检索。第二步检查触发规则发现正则规则里看看|查一下|怎么这类词过于宽泛。用户消息里有一个想想和规则里的查一下在 embedding 空间里距离很近被错误匹配到了检索意图。第三步根因确认模式分类只看关键词命中没有做意图与上下文的联合判断。比如用户是在回应上一轮的闲聊话题而不是真的想发起检索。修复方案采用双保险。第一低置信度结果不再直接执行工具调用而是先走普通对话模式把用户意图确认一下。第二工具调用的最终执行前增加白名单校验只有分类结果既满足关键词命中、又满足置信度阈值、还在该模式下允许的工具清单里才真正触发。修复之后误触发率从 1.2% 降到了 0.1% 以下。5. 默认配置、监控指标与多模型档位适配踩过的坑最终都沉淀成了配置文件里的默认参数。这一节我把 context-mode 的关键配置项和监控指标完整列出来方便你直接参考。5.1 配置文件的默认值长什么样context_mode: enable: true default_mode: conversational # 硬性预算单位token budget: business_rules: 800 current_intent: 600 conversation_memory: 1800 tool_results: 600 safety_margin: 200 # 滑窗参数 sliding_window: max_turns: 5 min_turns_for_summary: 8 summary_trigger: auto # 模式分类参数 classifier: confidence_threshold: 0.7 fallback_on_low_confidence: conversational require_whitelist: true这里面的几个参数我逐个说说为什么这么定。max_turns: 5是综合了信息充足度和token 成本之后折中的结果。min_turns_for_summary: 8表示对话轮次超过 8 轮才启用摘要压缩因为 5 到 8 轮这个区间滑窗还能兜住摘要反而容易引入压缩噪声。confidence_threshold: 0.7是我们用历史日志做了分布统计后取的阈值低于这个值的分类结果基本都值得怀疑。5.2 上线后一定要盯的三个指标配置只是起点上线后我更关心的是三个运行指标指标含义我的监控阈值报警条件上下文命中率滑窗内内容覆盖当前问题的比例正常应大于 90%低于 85% 时检查滑窗是否过短摘要压缩触发率触发摘要的请求占全部请求的比例因场景而异单场景超过 70% 时检查是否有上下文膨胀输出质量回归率新版本相对上一版的答案质量评分变化波动范围在上下 5%超过 10% 立即回滚这三个指标配合起来看能快速定位大多数上下文管理问题。命中率低说明滑窗策略太激进摘要触发率高说明会话记忆在快速膨胀质量回归率异常则说明压缩逻辑或者模式切换把关键信息弄丢了。5.3 多模型适配给上下文开档位最后说一个扩展点。我们对接的模型窗口大小差异很大从 4K 到 128K 都有。同一个配置不可能通吃所以我们在配置里加了档位概念small 档4K 窗口严格按上面表的预算分配滑窗只保留 3 轮。medium 档32K 窗口业务规则预算翻倍到 1600滑窗保留 8 轮。large 档128K 窗口业务规则和会话记忆都可以放大但输出预算仍然预留 5%。档位的选择通过一个简单的探测函数实现请求进来时先查模型的最大上下文窗口再决定加载哪套配置。这个设计让我们在切换模型供应商时不用改动任何业务代码只改配置文件的档位映射。我在实际使用中最大的体会是context-mode 这类设计本质上是在给模型减负。模型的能力再强你把一堆没有结构化的信息砸给它它也只能靠概率去猜哪里重要。而工程师真正该做的是在模型开口之前就把信息整理成它最容易理解和发挥的形态。这套思路从 Agent 到 RAG 到普通的 prompt 优化都适用。最后再分享一个小技巧每次调整滑窗长度或摘要策略时记得留一组固定的回归测试题目别只盯着整体指标因为整体指标会被高分项掩盖掉具体场景的劣化。