
1. Context-Mode是什么先搞清楚它在AI应用里的位置最近好几个朋友把项目里“context-mode”这个词摆到我面前问这到底是什么为什么别人的架构设计里总强调它自己该选哪一种。与其一个个解释不如把我实际做LLM应用时对上下文模式的拆解完整写出来。我先把结论放在前面context-mode不是某个框架里的固定开关而是应用如何组织、取舍、注入“对话上下文”的一套策略。你选择哪一个模式直接决定了产品的记忆能力、响应速度、token成本和回答质量它还决定了一个更隐蔽的东西——当用户聊到第五十轮的时候产品到底是越聊越懂用户还是越聊越糊涂。1.1 从一次真实对话看上下文模式为什么存在举一个我最近在做的知识库问答助手。第一版我图省事把所有历史消息全部塞给模型。结果前几次测试还好第三次开始回答就前言不搭后语——模型“记得”用户十分钟前问过采购流程当用户说“这个价格呢”模型以为是问采购价格实际用户是想继续聊报销标准。这个串台事故本质上就是上下文模式没设计好。你以为是模型变笨了其实是你把不该混在一起的信息混在一起喂给了它。再反过来看另一个场景一个翻译工具每次都独立处理当前这一句不需要任何历史。如果你强行给它上下文它反而会把前一句的翻译结果带进下一句导致输出来回漂移甚至出现“把上一句的术语原样抄进下一句”这种诡异行为。同一个模型为什么有的场景要上下文有的场景越给越糟原因在于上下文不是免费的它占token、增加延迟、引入噪声还会让模型在多个意图之间做不必要的猜测。context-mode存在的意义就是回答三个问题哪些历史信息该带上、用哪种结构带上、带多少。1.2 上下文的三块拼图系统约束、历史记录、外部知识想把上下文管理好先要把“上下文”这三个字拆开。我习惯把它分成三块完全不同的信息这三块的来源、生命周期和处理方式都不同混在一起只会让问题越来越难排查。第一块是系统约束包括system prompt、角色设定、打分标准、安全规则。它的特点是稳定一次会话里从头到尾基本不变但它必须始终占据最高优先级。一旦上下文过长把系统约束挤掉了模型就会出现“性格漂移”比如客服机器人突然用英语回答或者审核规则悄然失效。我的经验是系统约束在任何模式下都要保留固定的token配额并且拼装时永远放在最前面任何裁剪逻辑都不能碰它。第二块是历史记录也就是用户和模型之间的对话消息、工具调用结果。它的特点是持续增长而且越早的消息通常越不重要但某些早期信息又可能是全局约束比如用户一开始就说“我是免费用户”后面所有回答都要基于这个身份。历史记录正是context-mode主要管理的对象也是大多数调参工作集中发生的地方。第三块是外部知识也就是从知识库检索出来的文档片段、API返回的实时数据、数据库查询结果。它的特点是每次都要现取与当前问题强相关但它很容易超长而且经常包含大量与问题无关的冗余信息。三块信息凑到一起就出现了一个本质矛盾模型单次能处理的token有限而你可能需要的信息永远比塞得下的多。context-mode就是在这个矛盾里做取舍它不是对所有信息一刀切而是对不同类型的信息采取不同策略。想通这一点上下文管理就从一个玄学问题变成了一个标准工程问题。2. 主流上下文模式盘点五种方案的取舍与适用场景模式没有绝对好坏只有场景匹配不匹配。我把实际工程里见到的五种主流模式全部拆开讲每种都会说清楚它的工作原理、优点、硬伤和适合的场景。看完这一章你至少不会再拿着滑动窗口的方案去硬扛一个需要长期记忆的产品。2.1 零上下文模式单次任务最稳妥的起点零上下文模式也叫无状态模式核心思路是每次请求只带系统约束和当次输入不带任何历史消息。这是最节省成本也最不容易出错的设计输入token最小、响应最快、不存在“我把上一轮的东西带坏了这一轮”的风险而且服务端不需要保存任何会话状态水平扩展非常容易。我早年做发票信息抽取工具就用的这一套用户上传OCR文本模型负责提取金额、税号、日期这类任务一旦带历史反而可能把上一张发票的税号混进当前结果里那会是灾难级的错误。它的适用场景非常清晰一次性问题、格式转换、信息抽取、分类打标、翻译都适合零上下文。但零上下文的核心代价也很明显——它做不了任何需要多轮协作的任务。用户问“第二步怎么操作”模型不知道“第一步”是什么只能答非所问。所以我的建议是任何有“多轮协作”“连续对话”“状态依赖”需求的产品都不要从零上下文起步但任何保留历史会引入风险的任务一定要优先考虑零上下文。一个产品里完全可以同时存在多个模块、多个上下文模式客服模块用滑动窗口抽取模块用零上下文这很正常。2.2 滑动窗口模式最简单可控的多轮方案滑动窗口是目前生产环境里最常见的方案。它的思路一句话就能讲完维护一个固定大小的消息队列新消息进来旧消息被挤出去。需要注意的是窗口大小按token算不按轮数算。比如窗口上限是8000token每轮对话平均消耗400token那么大约能保留最近20轮。实现也很简单在消息列表尾部追加从头统计token超出就丢弃一个循环就搞定。它的最大优点是可控性强。输入长度基本恒定响应延迟稳定不会出现“聊到一半突然超出模型限制”这种事。但问题也藏在“一刀切”里窗口内保留的是最近N轮可有些关键信息在很早之前出现过。比如用户在第3轮说“我是企业版用户”第50轮问“我的权益有哪些”如果窗口只保留最近30轮这条信息早就被丢掉了。所以滑动窗口的本质是放弃长期记忆用最粗暴的策略换取稳定。在预算有限、第一阶段验证产品的时候滑动窗口往往是性价比最高的选择。我个人的习惯是把滑动窗口当默认方案先跑通业务逻辑之后再根据用户真实对话长度决定要不要加更复杂的记忆方案。你现在直接抄一套混合记忆系统进一个还没验证需求的产品风险远大于收益。2.3 摘要记忆模式用总结换取长对话摘要记忆模式解决的正是滑动窗口丢早期信息的问题。核心思路是当历史消息超过某个阈值时调用模型把最旧的一段消息总结成摘要之后用摘要替代原始消息参与后续对话。举个例子窗口上限8000token当消息总量超过6000token时触发摘要把前50轮对话压缩成一个400token的要点列表然后只把摘要和最近20轮完整消息一起送给模型。这样模型既能看到近期完整细节又能知道早期发生了什么。摘要的成本看起来高因为每次触发都要额外调一次LLM但实际上压缩比通常在5到10倍之间长期跑下来反而比无限保留原文更省钱。摘要模式最容易翻车的点是“模型认为重要的内容不等于用户需要的内容”。我之前做个点餐助手用户在第10轮说“我不吃香菜”第40轮问“这个沙拉里有什么”如果摘要漏掉“香菜”这个词回答就可能把香菜写进去。要缓解这个问题一是摘要指令里明确要求保留实体、数字、否定短语、偏好等关键信息二是用固定模板让模型填“人名/数字/偏好/待办事项”这类结构化字段而不是自由发挥写一段话。摘要模式适合中长会话、客服工单、陪伴类产品。2.4 向量检索记忆模式按需求取长期记忆向量检索记忆模式是把记忆从线性时间线改成按需召回。每条消息或者每条消息切成的chunk经过embedding模型转成向量存进向量数据库。每次构建prompt之前把当前用户问题也转成向量检索最相似的历史片段再把这些片段注入上下文。这个模式理论上最适合长对话和复杂Agent场景。比如一个数据分析Agent第5轮配置过数据源第30轮用户说“还是用之前那个表”向量检索能把第5轮的操作记录找回来滑动窗口完全做不到。但它不是银弹。第一它引入了新的故障面embedding模型的语义相似度不一定等于你的业务相关性定义第二检索质量非常依赖chunk切分消息切太碎语义就断了切太大一条消息混入多个主题召回噪声很高第三它不能替代滑动窗口因为用户最近说的几句话往往最相关如果只依赖向量召回反而容易把近期上下文丢掉。所以我的建议是向量检索永远不会单独使用它要和滑动窗口、摘要组合起来。它更适合记忆总量巨大、无法组织成摘要的场景比如跨多天的客服会话、知识库问答中的历史行为回忆。2.5 混合模式工业级的默认答案讲到这儿你会发现每种单一模式都有硬伤。零上下文没记忆滑动窗口丢早期信息摘要会失真向量检索有召回偏差。所以工业项目里真正被验证的方案基本都是混合模式。我推荐一套默认组合系统约束固定保留最近20到30轮消息用滑动窗口完整保留超出窗口的部分由摘要和向量检索共同兜底知识库检索结果按需注入。这套组合的逻辑是近期信息用完整消息保证准确度中长期信息用摘要保证概要连续关键细节用向量检索保证可回溯三者各有分工。混合模式的主要代价是复杂度。管线上每一环都可能出问题日志、监控、数据回放一样都不能少。但这个复杂度换来的东西也很值你可以在不同业务模块上灵活调参。比如客服模块把摘要权重调高因为工单场景需要保持完整案情脉络数据分析模块把向量召回调高因为用户经常引用很久之前的操作。混合模式不是我发明的东西它是所有绕了一圈之后的团队最终都会收敛到的状态。2.6 模式选型对照表为了方便对照我把五种模式的核心差别整理成一个表。选型时不要只看“哪个更聪明”先问业务需求到底落在哪一列。模式记忆范围token成本响应延迟主要风险最适用场景零上下文无最低最低无法多轮协作单次抽取、翻译、分类滑动窗口最近N轮低稳定早期信息丢失通用客服、早期验证摘要记忆全对话概要中中摘要信息失真中长对话、工单记录向量检索全量按需中高中高召回不准、噪声大长记忆、复杂Agent混合模式近全量高高复杂度高、难排查生产系统默认选择一个简单的判断方法如果用户聊20轮以内就结束滑动窗口足够如果用户聊200轮以上还经常翻旧账才需要考虑混合模式。先定业务再选模式顺序反了后面一定返工。3. 从设计到落地上下文管理器怎么一步步搭出来理论讲完该讲落地。这一章我拿一个Python版的ContextManager当例子把消息数据结构、四层管线、关键参数计算和核心代码骨架全部过一遍。就算你用的是别的语言思路也能无缝迁移。3.1 消息数据结构的底子管理上下文的第一步是定义消息的数据结构。不要图省事只存一个字符串我当时这么干过后面排查问题差点崩溃。一条消息至少需要这几个字段消息ID、会话ID、角色、内容、token数、时间戳、来源和扩展信息。其中message_id和conversation_id是必须的没有它们你没法做会话隔离也没法做日志追踪。token_size建议在消息写入时就算好并缓存下来不要在构建prompt时再重新编码计算否则每次请求会白白多一次tokenizer调用。这里有个容易被忽略的点conversation_id是业务层生成的会话标识不是模型返回里的某个参数。上下文的隔离边界完全由它决定。一个用户可能同时开多个会话每个会话有独立的conversation_id千万不要用user_id做上下文隔离键否则用户多个会话的内容会混在一起酿成严重的串台事故。我见过真实项目因此把用户A的订单信息回答给了用户B这已经不只是技术问题了。{ message_id: msg_88214, conversation_id: conv_90213, role: human, content: 请帮我查一下上个月的报销单, token_size: 18, timestamp: 1710000000, source: user_input, extras: {intent: query_expense, priority: 2} }source字段也很关键它区分消息来自user_input、assistant_reply、tool_result还是knowledge_snippet因为后面各种来源的优先级和过期策略不同。比如tool_result往往时效性极强过期几分钟就没意义而系统约束是常青的永远不该被淘汰。3.2 四层管线采集、整理、召回、注入消息进来之后不能直接拼prompt中间要过四层管线。我每次做新项目都会按这个分层来组织代码好处是每层职责独立可以单独测试出了问题也知道去哪查。第一层是采集层。它在业务代码里拦截所有入站和出站消息统一包装成上面定义的消息结构。不管请求来自Web端、小程序还是开放API都走同一套采集入口。采集层还要负责记录工具调用结果。我见过很多项目在采集层就漏掉了tool_result导致模型不知道工具执行成功还是失败回答全靠猜最后生成一堆胡编的结论。第二层是整理层。它的工作是清洗和标记。清洗包括去重、去空白、过滤敏感词、把过长的工具结果截断。标记则是给每条消息打来源标签、计算token数、按业务规则决定优先级。这一层不需要调用模型纯规则处理就够了目的是让下游拿到的数据尽量干净。第三层是召回层这才是真正做上下文选择的地方。它从短期缓冲里取最近N轮从长期记忆里取需要补充的摘要或向量召回结果把系统约束放在最前面最后输出一个候选列表。第四层是注入层把候选列表拼装成模型实际看到的messages数组。这里要处理很多细节比如把历史消息的role改成human和assistant把工具结果转成对应类型的消息在消息之间插入分隔标识。注入层是最容易出低级bug的地方role序列乱了、空消息混进去、最后一条不是user消息都会导致模型行为异常。四层划分的核心意义在于采集层只管收整理层只管过滤召回层只管选注入层只管拼。出问题时沿线排查比在一个几百行的函数里找线索高效得多。如果你现在的上下文逻辑全写在一个函数里我的建议是尽快按这个方式拆开。3.3 关键参数怎么算窗口大小、摘要阈值、召回数量参数不能拍脑袋我给你一套可复用的估算方法。先算窗口大小。假设产品要求用户连续对话20轮不丢失细节每轮平均400token那么窗口至少是20乘以400等于8000token。但8000只是给历史消息用的还要加上系统约束的500token和给模型留出的输出空间2000token。如果你的核心模型上下文是16K实际窗口最多设为16K减500减2000也就是13500token。留输出空间是最容易被忽视的一步把上下文塞满模型没有token生成回答线上就会出现大量空回复。摘要触发阈值一般按窗口的70%来设。窗口8000token累积消息超过5600就触发摘要把最旧的那部分压缩成400token的摘要。阈值设太低会频繁触发摘要token成本上升阈值设太高摘要还没生成窗口就快爆了。这个70%是我在多个项目里试出来的经验值你可以根据用户对话长度微调。向量召回数量要看单条消息的长度。如果每条历史消息平均100token想补1000token的长期记忆topK取10左右如果消息本身很长平均300tokentopK就降到3到5。不要固定写成topK5然后不管了应根据“计划分配给长期记忆的token预算”动态计算。照搬别人项目的参数是最常见的问题别人的数据分布和你完全不一样。3.4 一版能跑的ContextManager骨架代码下面这个版本把所有核心逻辑都包含了可以直接抄回去改。我没有加锁、缓存、持久化这些生产依赖是为了让你先看清主流程。class ContextManager: def __init__(self, max_window_tokens8000, summary_threshold5600, top_k5): self.messages [] self.summary self.system_prompt self.max_window_tokens max_window_tokens self.summary_threshold summary_threshold self.top_k top_k def add_message(self, role, content, sourceuser_input): token_size estimate_tokens(content) self.messages.append({ role: role, content: content, token_size: token_size, source: source, }) self._maybe_summarize() def _maybe_summarize(self): total sum(m[token_size] for m in self.messages) if total self.summary_threshold: return oldest_tokens 0 keep_from 0 for i, m in enumerate(self.messages): oldest_tokens m[token_size] if oldest_tokens self.summary_threshold - 800: keep_from i break old self.messages[:keep_from] self.messages self.messages[keep_from:] text \n.join(m[content] for m in old) self.summary summarize_text(text) def _recall_long_term(self, question): return retrieve_similar(question, top_kself.top_k) def build_prompt(self, question): long_term self._recall_long_term(question) window [{role: m[role], content: m[content]} for m in self.messages] prompt [{role: system, content: self.system_prompt}] if self.summary: prompt.append({role: system, content: f[长期摘要] {self.summary}}) for item in long_term: prompt.append({role: system, content: f[历史记录] {item}}) prompt.extend(window) prompt.append({role: user, content: question}) return prompt几个细节说明一下。estimate_tokens和summarize_text需要你接具体实现前者用tiktoken或模型的tokenizer都行后者直接调一次LLM。_maybe_summarize里留了800token余量是防止摘要本身还没生成新消息又把窗口填满。build_prompt里把摘要和历史记录放在system角色里是为了减少human和assistant的role序列混乱实测对很多模型的稳定性有帮助。生产环境还需要给这些方法加锁、加缓存、加持久化但不影响理解主流程。4. 实战踩坑记录这些问题我全遇到过方案和代码都有了接下来是实际项目里踩过的坑。这些坑不踩一遍很难从文档里看出来我按“现象、原因、解决”的结构写出来你对照着排查就好。4.1 上下文串台低优先级内容挤占了系统指令现象是客服机器人聊到第10轮开始“变傻”角色设定里的规则好像失效了比如系统指令明明写了“最终退款与否以人工审核为准”模型却直接回答“我们不支持退款”。真正原因是我当时在滑动窗口实现里只按时间顺序保留最近N条导致系统指令被最新的一堆闲聊内容挤出了窗口。模型只看到最近的对话自然就把规则忘了。这是最典型的上下文优先级错误——时间不应该成为唯一排序标准。解决方法是给每条消息加优先级把系统约束、身份信息、合规规则设为最高优先级窗口裁剪时永远先丢弃低优先级内容。拼装prompt时把system_prompt固定在数组第一位任何情况下都不能被裁剪。这个改动看起来很小但解决了很大一类“模型突然失忆”的问题。如果你现在也在用纯时间顺序裁剪窗口建议立刻检查系统指令是否始终保留。4.2 摘要导致关键事实丢失用户反问“你忘了”现象是用户在第3轮说“我是企业版发票抬头写我们公司全称”第20轮要求“开票”模型生成的发票抬头还是错的。原因就是摘要模型把这句话压缩成了“用户是企业版用户”而“发票抬头写全称”这个操作性细节被当成次要信息丢了。摘要本质是有损压缩模型判断“重要”的方式是语义显著性和频率不是业务价值。解决方法是把关键事实单独标记出来。在摘要指令里强制要求填写结构化字段比如“用户身份”“价格限制”“偏好禁用词”“必须遵守的格式”并把字段列表固化在摘要模板里。另一个补充手段是凡是含数字、否定词、专有名词的消息单独打上“关键事实”标签摘要时要求原样保留。这个方法同样适用于向量检索前的关键信息抽取。4.3 Token预算失控成本翻了三倍现象是上线后的账单比预估高了很多查日志发现一次请求的平均token量从预估的3000涨到了9000。往下查罪魁祸首是工具结果。数据分析Agent每次查数据库返回几百行原始记录我把这些记录原封不动塞进上下文而模型其实只需要其中几行结论。很多人把token预算只当成“模型上限”考虑忘了它直接决定成本。一次低质量的工具返回可能吃掉4000tokenAgent里如果有三次工具调用上下文就逼近20000token。解决方法是工具结果进入上下文前必须经过独立截断逻辑只保留前N行、提取关键字段甚至让模型先对结果做一次摘要再注入。同时限制单次对话里的工具调用轮数比如最多三轮超出后要求模型基于已有信息作答。我在日志里还会记录每类来源的token占比包括系统提示词、历史、工具、检索哪一类超了立刻能发现。4.4 检索不到“明明说过”的内容现象是用户说“我之前提过关于加密的要求”向量检索引擎没召回到那段对话模型回答“您之前没有提过相关要求”。这个坑排查了很久最后发现三个叠加因素。第一embedding模型对语义相近但用词差异大的句子检索效果不稳定“用户密码要加盐存储”和“敏感信息要加密”可能完全不匹配。第二我当时把一条长消息切成多个chunk那段关键信息恰好被切在边界上两个半截句子的向量都不完整。第三没有加关键词辅助召回只依赖向量。解决方法是“向量加关键词”双路召回。向量走语义关键词走精确匹配两路结果合并去重再打分。chunk切分尽量按语义段落切而不是按固定字符数硬切并且给相邻chunk设置重叠区域。最后对高频业务词汇维护一个同义词表检索前先做一次查询词扩展。这些改动加完之后类似问题的命中率明显提升。4.5 常见问题定位速查表下面这张表是我日常排查的依据。上下文问题最大的特点是不稳定同一个prompt有时候好有时候坏不用数据回放根本找不到规律。日志里把每次请求的完整prompt结构和token分布记录下来是所有排查手段的前提。症状可能原因快速定位方法解决方案模型突然忘记系统规则系统指令被窗口裁剪查看最终prompt检查system是否在首位固定优先级永不裁剪回答前后矛盾早期关键信息被挤出窗口回放历史找关键事实是否仍在摘要和向量检索兜底空回复或截断上下文塞满没有输出空间看请求token数是否逼近模型上限预留输出token成本超预期工具结果或检索结果过大日志里按来源统计token占比工具结果截断、摘要用户说“你忘了”摘要失真或召回失败对比摘要和原文检查召回结果结构化摘要、双路召回多会话内容互相干扰用user_id做隔离键检查会话ID是否唯一改用conversation_id隔离5. 我在实际项目中坚持的三个关于上下文的判断写了这么多最后分享三个我个人的判断。不一定适合所有项目但至少能帮你少走弯路。5.1 先想清楚产品形态再谈上下文策略很多团队一上来就纠结“该用摘要还是向量库”其实方向反了。应该先回答几个产品层面的问题用户的对话长度分布在哪个区间用户会不会频繁翻旧账连续对话被打断后再回来产品需要保留多少记忆比如一个客服工单系统用户可能隔几天才回来说“上次那个问题怎么样了”那上下文就必须做持久化至少按天保留。而一个临时工具类小程序用户用完即走压根不需要长期记忆。产品形态决定了模式模式再决定参数。跳过产品形态直接选技术方案后面一定会返工。5.2 越复杂的模式不等于越好的体验我见过不止一个团队看到向量检索很流行就在一个只有30轮对话的产品里硬上向量记忆结果引入了一个新的bug源embedding服务抖动、向量库时区不一致、召回噪声让模型无所适从。最终效果还不如简单的滑动窗口加摘要。复杂的模式意味着更多故障面和更高维护成本它应该由真实需求驱动而不是由技术兴奋驱动。项目初期能用滑动窗口解决的就不要上向量库能用摘要解决的就不要上混合管线。每一层复杂度都要有对应的可度量收益。5.3 没有日志的上下文管理都是盲人摸象上下文管理是一个非常黑盒的环节。你很难直接看到模型到底看到了什么只能通过回答质量反推。所以我坚持在每次请求里记录三样东西最终发给模型的完整prompt、各来源内容的token占比、以及摘要和召回的命中来源。有了这些记录当用户投诉“模型变笨”时我可以直接打开那条请求的prompt看到底是系统指令被挤掉还是工具结果太大挤掉了历史还是摘要把关键事实弄丢了。没有这套日志所有排查只能靠猜。我还会在每个会话里存一份debug线随时导出某个会话的完整上下文快照这个习惯帮我在很多疑难问题里快速定位了根因。上下文模式这件事说复杂可以很复杂说简单其实也简单——它就是在有限的token里做取舍取舍的标准来自业务。只要你能回答清楚“哪些信息绝不能丢”“哪些信息丢了没关系”“新信息从哪里来”方案框架就已经出来了。剩下的就是参数调优和耐心排查。做应用的人手里最值钱的工具永远不是更长的模型窗口而是对每一个token去向的掌控力。