很多做 LLM 应用的人第一次被线上事故教育往往不是模型能力不够而是上下文先崩了。要么 token 超限直接报错要么多轮对话跑到后面模型把最开始的关键约束忘得一干二净要么同样的知识库换一种问法就答非所问。这些问题的根源都指向同一个东西——context-mode也就是上下文模式。这个概念听起来像是某个框架里的开关参数但实际落地时它是你应用里最值钱、最容易炸、也最值得精心设计的一层。今天这篇不聊空泛的架构图就聊聊我怎么从一个被 400 报错和胡言乱语逼到加班的菜鸟到把这层东西做成一条稳定管线的完整过程。1. 先搞懂 Context Mode 是什么再说怎么用1.1 我那次事故400 报错和一本正经的胡说八道先说一个真事。前年我做一个企业内部的报告问答机器人最开始很天真把所有历史对话和文档内容一股脑塞进 prompt让模型自己找答案。上线第一天就出事用户问了三轮之后请求体里的对话历史超过 8k token 窗口直接 400 报错。我当时第一反应是换更大的窗口不就行了于是把模型换成支持 128k 上下文的版本。结果更惨窗口是够了但模型开始在 40 轮之前的旧对话里翻出一句无关紧要的吐槽当成用户最新指令来执行回答方向完全跑偏。这个经历让我意识到把上下文简单地塞给模型不是使用上下文而是靠运气让模型从垃圾堆里挑重点。真正的问题是缺少一个对上下文进行统一管理、裁剪、组织和更新的机制。这个机制就是 context-mode 要解决的事情。1.2 Context Mode 的三个核心职责我后来把 context-mode 拆成三件事每次设计都按照这三个维度去检查基本不会再犯低级错误。第一是上下文的构建也就是说我们最终给模型的那一堆 token到底由哪些内容组成系统提示词、历史对话、外部检索结果、当前用户输入、工具返回结果这些信息应该按照什么优先级和顺序拼在一起。这个问题不解决后面所有优化都是空中楼阁。第二是上下文的压缩即如何让有限长度的上下文表达尽可能多的有效信息。这里不只是粗暴地截断而是包括滑动窗口、关键事实提取、阶段性摘要等策略。核心目标是保证模型始终能看到最近最重要的信息而不是最新的所有信息。第三是上下文的持久化尤其针对多轮对话场景。用户上一轮提到的偏好、约束、未完成任务这些信息在下一轮仍可能重要但我们不能把所有原始文本都保留。需要在会话存储层面建立结构化的状态管理把原文转化为可检索、可回溯、可覆盖的上下文记录。如果你正在做一个需要多轮交互的 AI 应用无论是智能客服、Copilot 还是 Agent都应该先想清楚这三个职责。很多项目失败的原因不是模型选得不好而是根本没有一个上下文模式的设计所有东西都堆在 prompt 里碰运气。2. 方案选型全量、压缩、检索还是混合2.1 全量塞入最直接但最烧钱先把最省事的方案说清楚。对于单轮问答、对话轮次极少、输入内容本身很短的场景全量塞入到 prompt 里并不是错误。比如一个简单的翻译工具用户输入一句话我们加上系统提示词直接调用模型这就是最简单的 context mode。但这种方式有三个明显的边界。第一是费用现在大模型 API 按 token 计费prompt 部分通常还比 output 贵只要上下文里堆了几万字每一次请求都在烧钱。第二是窗口即便模型支持 128k也扛不住无限增长的对话历史。第三是迷失在中间的问题有研究显示模型对长上下文中间的注意力显著弱于开头和结尾如果你把最关键的指令藏在第 60k token 的位置它很可能就是看不见。所以我的建议是全量塞入只应该作为基线方案存在用来验证功能是否跑通。一旦需要上线、需要稳定、需要考虑成本就必须进入下面三种方案。2.2 结构化压缩让上下文按逻辑组织压缩不是简单地砍掉一半字符而是把对话内容按照信息价值重新组织。我在实践中最常用的方法是把上下文分成几个固定槽位系统指令、事实列表、任务状态、近期对话摘要、最近几轮原始记录。举个例子用户在第 1 轮说我是财务部的预算审批流程我要重点关注审批权限第 20 轮又说给我推荐一个项目管理软件。如果保留所有原文模型要自己从 20 轮对话里判断财务部这个信息是否依然重要。而结构化压缩的做法是在系统维护一个用户画像-关键事实槽位每一轮对话结束时将提取到的事实更新进去。最终模型看到的上下文可能长这样关键事实用户属于财务部关注审批权限上次讨论过预算系统重构。当前任务推荐项目管理软件需要支持自定义审批流。最近对话第 19、20 轮的原始内容。模型看到的是一个高度组织化的工作台而不是一坨历史记录。这个方案不需要额外的向量检索服务适合对话轮次不是特别多、但需要维持长期记忆的场景。缺点是信息提取依赖模型本身提取质量会直接影响后续表现所以需要在提取时给足上下文并用稳定的输出格式约束模型。2.3 检索增强把大海变成一瓢水如果你的知识库很大比如几百篇文档、几万行代码那结构化压缩顶不住。这时候就需要走 RAG 路线也就是检索增强。context-mode 在这里的任务变成根据当前用户输入从知识库中检索出最相关的几个片段拼装进有限的上下文窗口。这里的关键不是用向量数据库这么简单而是设计检索策略。我踩过的坑是只做 top-k 向量检索结果用户问一个综合性的问题检索回来的片段全是字面相似的废话真正关键的表格数据一个都没进来。后来我改成向量召回 关键词召回 重排序的组合并在检索前做查询改写比如把代词的指代对象还原成实体效果才明显改善。RAG 的优点是可以支撑无限大的知识库token 成本可控。缺点是引入了检索链路链路里每个环节都可能失败而且检索结果的质量上限决定了回答质量上限。我始终认为RAG 不是替代上下文管理而是上下文管理中的一个信息源组件。2.4 混合模式与选型建议实际项目里我几乎不会只用一种纯方案而是混着来。一套标准的混合策略是这样的用户问题进来后先对问题进行意图判断和查询改写。同时从三个渠道取信息结构化槽位用户画像、状态、最近对话摘要、外部检索结果。按照优先级组装上下文系统指令 关键事实 当前任务状态 检索结果 近期对话原文 当前用户输入。组装前做预算判断超了就优先砍掉检索结果中相关度最低的片段。这个模式能同时兼顾长期记忆和新鲜信息也是我后面三个生产级项目能稳定跑起来的基础。3. Token 预算管理让上下文在窗口内精确运行3.1 预算公式和计算过程很多人以为只要把内容拼起来不超过窗口上限就行但实际上模型输出的 token 也要从窗口里扣。如果你把 window_size 全部塞满 prompt模型可能因为输出没有空间而报错或者被迫提前截断。我一般用一个保守的预算公式上下文预算 窗口上限 - 预留输出 - 系统提示词 - 安全边际举例模型窗口是 8192预留输出 1024系统提示词固定 600安全边际 200那么上下文预算就是 8192 - 1024 - 600 - 200 6368 token。这 6368 个 token 才是我真正可以自由分配给历史对话、检索结果、额外信息的空间。别小看那 200 的安全边际因为不同模型版本对 token 的计数方式会有轻微差异尤其是中文场景差几十个 token 就可能导致整条请求失败。计算 token 数时不能靠字符串长度猜测要用模型对应的 tokenizer 来精确计算。OpenAI 系可以用 tiktoken 库其他模型也有各自的 tokenizer。我测试过一份 6000 字的中文文档不同模型的 token 数差异能到 20% 以上靠估算很容易翻车。3.2 动态截断与滑动窗口预算定好之后动态截断和滑动窗口就派上用场了。对于多轮对话我最常用的是分层滑动窗口最新的两到三轮对话保留完整原文更早的对话只保留摘要。如果空间仍然不够就继续把摘要变短。这里有一个很容易犯的错误所有历史对话都只保留摘要会丢失细节全都保留原文预算必然爆掉。所以我用权重判断——包含具体信息数字、金额、日期、承诺事项的句子优先保留寒暄和无关内容优先丢弃。这个过程如果交给规则做用正则就能筛掉大半如果想让效果更好就让模型来打分但不要每轮都打分代价太高。实现上我会做一个重要片段标记的步骤。每一轮对话结束之后对这一轮内容做一次轻量解析把包含关键实体姓名、部门、时间、数字的句子单独存储。等需要压缩时这些被标记过的片段会最先被保留。实测下来这种方式比单纯按轮次截断要好得多丢失关键信息的概率明显降低。3.3 压缩触发时机的判定压缩的时机也很重要不是说每次请求都压缩那样既慢又浪费。我设置了两级阈值当接近 token 预算的 70% 时只对最旧的历史做一次轻量截断丢掉无关寒暄保留原句。当接近 token 预算的 90% 时触发一次完整压缩把旧对话整合成摘要并清理已经失效的任务状态。这个设计能避免频繁调用压缩导致延迟升高。在真实项目中如果把完整压缩触发点设得太低比如 50%你会发现用户每说几句话就触发一次摘要整个对话体验变得很卡顿而且摘要有损重构的问题会被放大。还有一种情况需要警惕用户的问题特别长或者检索结果特别多即使压缩完了还是超预算。这时候就应该放弃强制性拼装转为告诉用户你的问题涉及的内容太多我分成几步回答或者让用户精简问题。强行压缩只会得到一段语义破碎的上下文模型给出垃圾答案的概率急剧上升。4. 实战一条可落地的 Context Mode 管线4.1 整体框架与模块划分前面讲了很多概念现在落地。我做过的其中一版 context-mode 管线整体分为五个环节输入解析、状态更新、预算计算、信息组装、提交生成。它们像流水线一样依次执行每一步的输出都是下一步的输入。第一步输入解析把用户最新一句话清洗一下识别有没有指代词需要还原有没有明确的意图切换。第二步状态更新把用户新表达的关键事实写入结构化槽位同时更新任务状态。第三步预算计算用窗口上限减去输出预留等固定项得到本次可用的上下文空间。第四步信息组装按照优先级依次放入系统指令、状态槽位、检索结果、摘要、最近原文每放一项就累计 token 数超了就丢弃下一优先级的内容。第五步把组装好的上下文提交给模型并把模型输出追加到会话存储中。这五个环节不需要全部靠模型很多可以用规则完成。我倾向于让状态更新里的事实提取用模型信息组装用纯代码逻辑预算计算用精确 tokenizer。这样的好处是模型只参与它擅长的语义理解其他环节确定性高、可测试、可观测。4.2 关键模块的实现细节与代码示例动手环节。我拿 Python 写过一个简化版本核心是两个类TokenBudget 负责预算计算ContextAssembler 负责按优先级组装。import tiktoken class TokenBudget: def __init__(self, window_size8192, max_output1024, system_prompt_tokens600, margin200): self.window_size window_size self.max_output max_output self.system_prompt_tokens system_prompt_tokens self.margin margin property def available(self): return self.window_size - self.max_output - self.system_prompt_tokens - self.margin def fits(self, tokens: int) - bool: return tokens self.available class ContextAssembler: def __init__(self, tokenizer): self.tokenizer tokenizer self.budget TokenBudget() self.sections [] def add_section(self, name, content, priority): token_count len(self.tokenizer.encode(content)) self.sections.append({ name: name, content: content, priority: priority, tokens: token_count }) def assemble(self): self.sections.sort(keylambda x: x[priority], reverseTrue) total_tokens 0 final_blocks [] for section in self.sections: if total_tokens section[tokens] self.budget.available: continue final_blocks.append(section[content]) total_tokens section[tokens] return \n\n.join(final_blocks), total_tokens这段代码很简单但有几个细节值得注意。首先是 tokenizer 一定要跟模型配套。我见过有人用 gpt-3.5-turbo 的 tokenizer 去估算 gpt-4 的 token虽然结果大差不差但一旦内容里出现特殊符号或代码误差会变大最好还是严格匹配。其次是 add_section 里的 content 最好先经过预处理而不是直接把原始 JSON 丢进去。我习惯把所有内容先转成干净的文本格式比如把系统提示词中的变量填充好把检索结果里的 metadata来源、日期、页码和正文拼成一段文本这样组装时不会出现格式混乱。再来一个状态更新的例子。我用一个简单的 prompt 让模型从用户输入中抽取关键事实和任务状态变更state_update_prompt 你是会话状态管理器。根据用户最新输入和当前状态输出 JSON { facts_to_add: [新增或变动的事实保留实体和关键限定词], facts_to_delete: [如果用户明确否定了旧事实填入旧事实的原子描述], task_status: 当前最高优先级的任务描述 } 当前状态 {state} 用户输入 {user_input} 把模型返回的 JSON 解析后写入结构化槽位。注意这里的 JSON 解析一定要做异常兜底因为模型偶尔会输出多余的前后文字我用一个正则把第一个 { 到最后一个 } 之间的内容截取出来再 json.loads几乎不会再翻车。4.3 实测效果对比数据讲完实现上一组实测数据。我在一个内部知识问答项目里对比过三种模式全量塞入、纯滑动窗口截断、完整 context-mode 管线。测试集是 200 道从运营人员真实问题中整理出来的题目标准是人工判断回答是否符合预期。全量塞入那一版在文档小于 20 页时可以支撑超过 50 页以后几乎每 5 道题就有一道因为超限失败改大窗口后 token 成本上升 40%准确率反而下降 6%典型的高投入低回报。纯滑动窗口截断那一版把每轮历史保留最近 5 轮超出的直接丢弃。它的优点是代码简单但问题在于第 3 轮提到的某个约束到了第 30 轮还在影响用户预期却被无脑丢掉了。测试里出现了 11% 的答非所问原因就是关键约束被滑动窗口淘汰。完整 context-mode 管线配合状态槽位和摘要压缩同样的 200 道题回答准确率从 72% 提升到 89%token 成本相比全量塞入降低了 33%。延迟上由于信息组装是纯计算单次请求额外增加只有 20-50 毫秒压缩触发时才需要额外的一次模型调用。整体来看这个投入产出比是值得的。当然这些数据不能直接迁移到所有场景但它反映了一个趋势context-mode 做得好不好决定了同样一个模型在你的项目里是 70 分的水平还是 90 分的水平。5. 常见问题与踩坑记录5.1 上下文漂移与旧信息丢失多轮对话里最常见的翻车场景就是上下文漂移。用户在第 2 轮说我是用 Mac 开发的请用 macOS 命令到了第 15 轮你推荐了一条 Linux 命令模型还振振有词地解释。问题出在哪出在压缩过程中系统把 13 轮之前那条macOS约束当成过时信息给丢掉了或者摘要生成时把它概括成了用户提到开发环境丢失了具体限定词。我的解决方案是在状态槽位中把跨轮次硬约束单独分一类包括用户声明的身份、环境、偏好、禁止项。这类信息在每次压缩时都强制保留并且放在高优先级区域。用一个简单的常量列表来管理这些硬约束的类型比让模型自由判断要可靠得多。另外当用户明确改变之前的一个设定时一定要触发状态更新里的facts_to_delete把旧约束显式移除。否则模型在上下文中同时看到用 macOS和用 Windows两个互相矛盾的约束它大概率会晕。5.2 中文与代码场景的 token 计算偏差中文场景下一个中文字符大约对应 1 到 2 个 token但这只是一个粗略估计。我之前在预算计算中只按字符数除以 2 估算结果有一次实际 token 数比估算值多了 18%直接顶到窗口上限请求失败。后来我严格按照 tokenizer 计算而且每次组装完上下文后再做一次最终校验如果超过预算就触发降级路径。代码场景更需要注意缩进、特殊符号、长标识符都会让 token 数变得不可预测。不要嫌麻烦每次请求拿去编码转换成 token 再判断成本可以忽略不计但能规避大量线上问题。5.3 摘要丢失细节的补救方案摘要压缩一定会丢细节这是物理规律。问题是丢了之后怎么补。我最开始的实现是只做摘要结果用户问你刚才说的那个报价单 5% 折扣还有效吗模型一脸茫然因为它只见到了摘要里的讨论了报价未完全确认完全不知道 5% 这个数字。补救办法是在摘要槽位旁边加一个关键数字与事实索引。摘要用自然语言概括而索引里以结构化方式保存最近 20 轮内出现过的金额、日期、百分比、编号等硬数据。一旦用户后续问到这些内容直接从索引里查而不是依赖摘要。这个机制我非常推荐它成本低、效果好是上下文压缩里的一个高杠杆设计。5.4 多轮场景下的累积延迟上下文模式如果设计得不好多轮对话越往后越慢。我见过一个实现每轮都对全部历史对话做一次完整摘要用户讲到第 10 轮时单次请求处理时间已经到了 6 秒用户体验肉眼可见地变差。要控制延迟核心原则是分级处理。轻量截断和规则过滤每轮都做但代价很低重量级摘要只在接近预算阈值时才触发。另外摘要生成可以做成异步更新先不阻塞主流程把旧对话的原始文本异步传给摘要模型生成完毕后再写回状态存储。这样主链路上的延迟不会被摘要拖累唯一的代价是有短暂的时间窗口内摘要可能略微滞后但对大多数场景无感。我把这个异步更新用在实际项目后多轮对话 30 轮以内的 p95 延迟始终稳定在 1.2 秒左右完全没有越聊越卡的毛病。最后再分享一个小技巧把 context-mode 做出来之后一定要加日志每一次组装完成后的 sections 列表、每部分的 token 占比、最终哪些内容被截掉了全部记录下来。线上问题分析时这些日志比什么调试工具都管用。你可能一开始觉得这是小事但等你真的遇到用户说模型失忆了而你又不知道模型到底看到了什么的时候就知道这步有多重要了。根据我个人经验上下文问题有八成靠日志就能当场定位。