大模型上下文模式实战:从滑动窗口到混合结构化选型指南

发布时间:2026/10/7 1:20:11
大模型上下文模式实战:从滑动窗口到混合结构化选型指南 最近在给一个内部知识库系统接大模型最让我头疼的环节不是选哪个模型而是它的 context-mode上下文模式该怎么定。模型推理能力再强你喂给它的上下文如果又乱又长输出照样一塌糊涂上下文切得太碎它又容易丢失关键信息回答起来像是失忆了一样。这个问题在单轮对话里还不明显一旦涉及多轮、长文档、工具调用就直接决定了一个应用是从能用到好用的分水岭。这篇东西我不打算讲抽象的理论就围绕 context-mode 这个话题把我自己从选型到落地、再到踩坑修复的完整过程写出来。适合正在做 LLM 应用开发、智能客服、知识库问答、Agent 编排的开发者参考。我把几种主流模式掰开揉碎结合真实项目里的决策思路和代码实现尽量让看完的人能直接照着调整自己的方案。1. 为什么 context-mode 突然成了绕不开的问题过去我们写聊天机器人所谓多轮对话不过是把用户最近几轮的消息拼起来丢给模型。数据量小的时候这么干没问题但现在的应用早就不是这个玩法了系统提示词动辄几千字知识库检索结果几十段工具返回的 JSON 一个比一个长再加上历史对话——全部塞进一次请求里token 轻松过万甚至几万。Token 一多问题就来了。第一个问题是成本。按目前主流 API 的定价输入 token 和输出 token 分开计价输入虽然便宜一些但在高频场景下每次请求都携带上万 token一个月下来账单会非常可观。我见过一个团队做的 AI 客服UV 不大但因为每次请求都带上完整历史对话和 20 条检索片段一个月的 token 成本翻了四倍。这还没算上长上下文带来的延迟增加。第二个问题是效果。上下文越长模型越聪明是一个流传很广的误解。实际上主流模型在输入超过一定长度之后对中间部分的注意力会明显衰减这就是所谓的lost in the middle现象。我实测下来把关键信息放在对话开头或者结尾模型召回率明显高于放在中间。这意味着你只是无脑堆上下文反而会稀释掉最关键的指令和证据。第三个问题是稳定性和可调试性。上下文是动态拼装的如果拼装逻辑混乱同样的用户问题这次带着这堆历史、那次带着那堆检索结果模型的输出风格和准确率就会像过山车。context-mode 解决的就是这件事它规定了你以什么策略去组织、截断、压缩和检索上下文。它是一套规则而不是一个简单参数。换句话说context-mode 的本质是在有限的上下文窗口内做信息资源的调度。你的窗口是稀缺资源系统提示、用户意图、历史记忆、外部知识、工具结果都在抢占这块地盘谁能进、谁该走、以什么形态存在就是上下文管理模式要做出的决策。2. 四种主流 context-mode 的取舍与适用边界我梳理了实际项目中遇到过的四种主流模式每种都有自己的脾气。给它们做一个横向对比大家先建立整体印象。模式核心思路优点短板典型场景滑动窗口模式只保留最近 N 轮对话实现简单token 可控遗忘早前关键信息闲聊、轻量客服摘要压缩模式定期将历史对话压缩成摘要长对话记忆持久摘要丢失细节、有延迟长时程任务、角色扮演检索增强模式从外部知识库召回相关内容拼入知识面广、实时性强依赖检索质量增加复杂度知识库问答、企业内部助手混合结构化模式按角色/来源分区块动态组装灵活可控、效果稳定工程复杂度最高Agent 编排、复杂工作流1.1 滑动窗口最省事但容易失忆滑动窗口的思路就是维护一个固定长度的队列新对话进来最旧的消息被移出去。代码写起来非常简单from collections import deque class SlidingWindowContext: def __init__(self, max_messages: int 10): self.messages deque(maxlenmax_messages) def add_user_message(self, content: str) - None: self.messages.append({role: user, content: content}) def add_assistant_message(self, content: str) - None: self.messages.append({role: assistant, content: content}) def build_context(self, system_prompt: str) - list[dict]: return [{role: system, content: system_prompt}, *self.messages]这种模式适合的场景是每一步的决策主要依赖最近一两轮的信息而不需要追溯很久以前的状态。比如一个天气查询机器人、一个点餐助手用户每一步的需求都比较独立。它的致命伤也很明显如果用户在第 3 轮提到我之前说过的那个项目而项目名称是在第 1 轮出现的此时第 1 轮已经被挤出队列模型就会一脸茫然。这也是很多初期产品被吐槽人工智障的原因之一。1.2 摘要压缩长对话的救星但要选对压缩时机摘要模式的核心是在对话积累到一定长度后用一个总结请求把此前的对话变成一个更短的摘要保留全局信息释放上下文空间。这里有个关键问题什么时候触发压缩压缩太频繁每两轮就做一次摘要多余的 token 开销反而大于省下来的压缩太晚上下文已经超限了模型可能直接报错。我实践的触发策略是双阈值当消息序列的预估 token 数超过窗口上限的 60% 时触发对最旧一半消息的压缩压缩后的摘要如果仍然超过窗口上限的 30%则递归地对摘要再压缩实现片段class SummarizeMode: def __init__(self, summarize_fn, max_token_ratio: float 0.6): self.summarize_fn summarize_fn self.max_token_ratio max_token_ratio self.summary self.recent_messages [] def add_message(self, message: dict, estimate_tokens) - None: self.recent_messages.append(message) total_tokens estimate_tokens(self.summary) sum( estimate_tokens(m) for m in self.recent_messages ) if total_tokens self.max_token_ratio * 8000: # 假设窗口 8000 self._rollup() def _rollup(self) - None: old_messages self.recent_messages[: len(self.recent_messages) // 2] self.recent_messages self.recent_messages[len(self.recent_messages) // 2 :] new_summary self.summarize_fn(self.summary, old_messages) self.summary new_summary注意这个 summarize_fn 一般是独立的一次模型调用所以压缩本身是有成本的。我一般建议在用户没有主动提问的间歇做异步压缩而不是在用户发起请求的时候同步去做——否则用户会明显感到首字延迟变长。摘要模式的另一个坑是事实漂移摘要压缩次数多了之后模型为了摘要的连贯性可能会自行补充缺失信息导致脑补出来的内容和原始对话不一致。所以做摘要的时候我会在 prompt 里强制要求只能压缩已有内容禁止补充和推断并且保留原始对话的关键实体列表比如人名、项目名、时间点作为摘要的附加字段。1.3 检索增强知识库问答的基石但质量全看入口检索增强模式也就是 RAG 的上下文部分不再依赖历史对话而是根据用户当前问题从外部知识库召回 Top-K 个最相关的文本片段和问题一起拼接后发给模型。这个模式我用的最多也是 context-mode 讨论里最常被提及的一种。它的核心拆成两段入库阶段把文档切片、向量化存进向量数据库召回阶段用户问题向量化相似度检索 Top-K再拼进上下文很多人以为这就完事了其实不然。召回拼入上下文的组织方式对答案质量有非常直接的影响。我见过一个失败案例把 8 条检索结果没有做去重和排序就一股脑塞进去模型被两条互相矛盾的片段搞糊涂了回答质量惨不忍睹。我的做法是给召回结果增加一个后处理管道先按来源文档分组同一文档内按照原始章节顺序排列再跨文档按相似度得分排序同时过滤掉相似度低于阈值比如 0.5的结果宁可少给不要给垃圾。片段与片段之间用明确的块首标识分隔方便模型区分这是不同来源的信息。def build_rag_context(query: str, retriever, top_k: int 5, min_score: float 0.5) - tuple[str, list[dict]]: hits retriever.search(query, top_ktop_k) hits [h for h in hits if h.score min_score] hits.sort(keylambda h: (h.doc_id, h.seq_in_doc, -h.score)) blocks [] for h in hits: blocks.append(fsource doc{h.doc_id} seq{h.seq_in_doc}\n{h.text}\n/source) return \n\n.join(blocks), hits检索增强模式最怕的是检索到了但不会用模型在生成的回答里引用了检索片段之外的信息或者分不清哪些来自检索、哪些来自自己的预训练知识。所以我通常会在 system prompt 里明确要求优先使用检索片段中的信息回答如果片段中没有相关内容明确告知用户你不知道不要编造。这一条指令能显著降低幻觉率。1.4 混合结构化面向 Agent 编排的重型方案最后是混合结构化模式这是我现在做得最复杂的方案。它不采用单一的上下文策略而是把上下文切成若干逻辑区块每个区块有独立的生命周期和组装规则。典型的分块包括系统指令块始终保留定义全局行为用户目标块来自用户的第一条或最新一条明确指令始终保留记忆摘要块历史对话压缩后的摘要周期性更新检索证据块当前问题相关的知识库片段每次动态替换工具结果块最近一次工具调用的输出用后即焚临时思考块中间推理过程不进入最终上下文每个区块都有独立的 token 预算。比如系统指令块固定 1500 token记忆摘要块上限 2000 token检索证据块上限 3000 token超出部分按优先级丢弃。这种模式的工程成本明显更高但换来的是可解释性和稳定性。调试时你可以只看某一个区块的内容快速定位是哪一块污染了模型输出。3. 实战选型我给知识库助手定 context-mode 的决策流程前面讲完了理论地图现在说说我在实际项目里怎么做的。那个项目是一个企业内部知识库问答系统知识库里沉淀了数百份技术文档和售后工单用户的问题跨部门、跨时期有的需要参考一年前的决策记录有的只需要最近一周的更新说明。我一开始直接上了最复杂的混合结构化模式结果发现步子太大团队只有两个人维护成本吃不消。后来我重新梳理了需求把决策流程固定成三步。3.1 先量化遗忘容忍度我做的第一件事是问自己一个很笨的问题如果用户第 3 轮提到的信息在第 9 轮要用到系统不知道后果多严重对这个知识库系统来说后果很严重——用户可能重复提问或者干脆认为系统无能。所以我淘汰了纯滑动窗口方案。但如果是自动补全代码的编辑器插件第 1 轮和第 5 轮的上下文关联度极低滑动窗口反而够用。遗忘容忍度低就不要用滑动窗口做上层方案。3.2 再测外部知识依赖度这个知识库系统的问题高度依赖文档内容比如售后服务流程在去年 Q4 做过什么调整这种问题靠模型预训练知识是答不了的。所以检索增强模式必须上。反过来说如果是一个写小红书文案的助手依赖的主要是用户的风格偏好和历史爆款数据检索外部文档的意义就不大反而更需要记忆摘要模式。3.3 最后看运营和维护成本预算混合结构化模式虽然效果上限最高但需要维护分块逻辑、缓存策略、降级预案一期开发至少多花两到三倍时间。我们当时的排期只给了四周所以我选择了检索增强为主、摘要压缩为辅的折中方案长期记忆交给摘要单次问答的知识来源交给检索。这样既覆盖了用户的知识依赖需求又不至于一开始就把工程复杂度拉满。后来事实证明这个决策是对的。上线两周最核心的问题类型——某个流程在特定时间后的变化——准确率从纯检索方案的 58% 提升到了 79%。这个决策过程我总结成了一张自查表大家可以拿去对照决策问题偏否偏是多轮之间是否强依赖早期信息滑动窗口够用需要摘要或混合答案是否依赖外部资料摘要/滑动窗口即可必须上检索增强对输出一致性要求是否极高简单模式更稳混合结构化值得投入是否有专人维护上下文逻辑尽量用成熟方案可以自研混合框架4. 核心实现一个可运行的上下文管理模块说了这么多还是得上点能跑的东西。我给大家展示一个精简版的上下文管理器它实现了滑动窗口、摘要压缩、检索增强三种模式的可插拔切换。4.1 统一接口设计class BaseContextMode: def __init__(self, system_prompt: str): self.system_prompt system_prompt def add_user_message(self, text: str) - None: raise NotImplementedError def add_assistant_message(self, text: str) - None: raise NotImplementedError def add_tool_result(self, name: str, content: str) - None: raise NotImplementedError def build_messages(self) - list[dict]: raise NotImplementedError统一接口的意义在于业务代码只依赖 BaseContextMode不关心背后是哪种模式。切换模式只需要改一处实例化代码。这是一个特别值得坚持的工程约束。4.2 检索增强模式的具体实现class RAGContextMode(BaseContextMode): def __init__(self, system_prompt: str, retriever, top_k: int 5, min_score: float 0.5): super().__init__(system_prompt) self.retriever retriever self.top_k top_k self.min_score min_score self.current_question None def add_user_message(self, text: str) - None: self.current_question text def build_messages(self) - list[dict]: messages [{role: system, content: self.system_prompt}] if self.current_question: rag_chunks, _ build_rag_context( self.current_question, self.retriever, top_kself.top_k, min_scoreself.min_score, ) if rag_chunks: messages.append({ role: system, content: f以下是可能与用户问题相关的资料片段请优先依据它们回答\n{rag_chunks}, }) if self.current_question: messages.append({role: user, content: self.current_question}) return messages注意我把检索片段放在了一个独立的 system 消息里而不是拼进 user 消息。这个细节有两个好处第一在 OpenAI 的 chat 接口里多条 system 消息会被同等对待模型能区分指令和资料第二这段资料内容和用户问题分离后日志审计更容易——你可以直接看到这次请求到底给了模型什么证据。4.3 摘要与检索的组合模式在知识库项目里我最终落地的是摘要检索组合。用户消息到达时先判断是否有历史对话存在有的话把历史摘要、检索片段、当前问题组装成上下文。class SummaryRAGContextMode(BaseContextMode): def __init__(self, system_prompt: str, retriever, summarizer, max_history_tokens: int 2000, top_k: int 5): super().__init__(system_prompt) self.retriever retriever self.summarizer summarizer self.max_history_tokens max_history_tokens self.history [] self.history_summary self.current_question None def _maybe_rollup_history(self) - None: # 这里用简化 token 预估1 token 约等于 2 个汉字 history_text .join(m[content] for m in self.history) if len(history_text) / 2 self.max_history_tokens: return self.history_summary self.summarizer(self.history_summary, self.history) self.history [] def add_user_message(self, text: str) - None: self._maybe_rollup_history() self.current_question text def add_assistant_message(self, text: str) - None: self.history.append({role: assistant, content: text}) def build_messages(self) - list[dict]: messages [{role: system, content: self.system_prompt}] if self.history_summary: messages.append({ role: system, content: f以下是历史对话摘要\n{self.history_summary}, }) if self.current_question: rag_chunks, _ build_rag_context(self.current_question, self.retriever) if rag_chunks: messages.append({ role: system, content: f参考资料\n{rag_chunks}, }) messages.append({role: user, content: self.current_question}) for m in self.history: messages.append(m) return messages if messages else [{role: system, content: self.system_prompt}]这套实现的关键点是把历史摘要和检索证据分开放在不同 system 消息里而不是混成一大段。实测这么做有两个好处第一模型更容易区分背景记忆和待引用事实回答时会倾向于用检索证据做事实锚点第二出问题的时候日志里可以直接看到是哪一块缺失或过长排障效率显著提升。4.4 接入主流程mode SummaryRAGContextMode( system_prompt你是一个严谨的企业知识库助手回答需注明信息来源。, retrievervector_store.as_retriever(), summarizerllm_summarize, ) mode.add_user_message(售后流程在去年Q4的调整内容有哪些) messages mode.build_messages() response llm.chat.completions.create(modelgpt-4o, messagesmessages)整个调用链非常清爽业务层完全不需要关心上下文是怎么组织的。5. 实测下来最疼的三类问题与排查链路代码写好了并不代表就高枕无忧了。我在知识库项目上线后的一周内连续踩了好几个坑每一个都值得拿出来讲讲排查过程。5.1 召回片段互相矛盾模型骑墙现象用户问某功能是否支持批量操作文档 A 说支持文档 B 说部分支持模型回答模棱两可用户体验很差。排查链路先抓请求日志把模型实际收到的上下文打印出来。果然检索片段里有两条来自不同版本的文档。进一步查向量库发现两条文档都没标版本号入库时也没有建立版本覆盖逻辑。根因不是 context-mode 本身而是上游数据管道没有做好文档版本管理。修复方案在检索后处理阶段增加同主题多片段投票逻辑当多条片段对同一问题给出冲突说法时优先采纳更新时间最近的文档同时把版本信息写入每个片段的元数据检索排序时加权。这个坑给到我的教训是context-mode 再优化也救不了脏数据。先管好数据入口再谈上下文策略。5.2 摘要压缩引发事实漂移现象多轮对话超过 20 轮后用户问我之前说我们用的是哪个供应商模型给出的回答和用户最初说的事实完全不符。排查链路我把摘要请求的输入和输出都打了出来对比后发现第二次压缩时摘要里出现了一个原文中根本不存在的供应商名称。原因是我用来做摘要的 prompt 没有加约束模型在信息缺失时自行做了合理的补全。修复方案在摘要 prompt 里明确加上约束如果原始对话中没有明确信息请用未知代替不要推测或补全保留所有命名实体原样。同时调整压缩时机保证摘要输入中始终包含原始对话的前几条信息避免信息在逐级压缩中丢失。5.3 长上下文导致首字延迟飙升现象一次请求的上下文组装到 12000 token 后接口首字返回时间从 1.2 秒涨到 3.5 秒用户明显觉得卡。排查链路先看是不是网络问题排除了。再测不同 token 量级的延迟发现确实随 token 数线性增长。这时候有两个优化方向一是减少上下文体积二是换用支持 prefix caching 的服务端。修复方案我同时做了两件事。第一把历史摘要的上限从 2000 token 压到 1200 token牺牲一部分长时记忆细节第二检索片段从 5 条减到 4 条并提高相似度阈值。结果表明首字延迟降到了 1.6 秒左右而核心问题的回答准确率没有明显下降。这说明我在原方案里其实给上下文分配了多余的空间属于典型的过程冗余。5.4 一个关于伪相关性的隐藏问题还有一些问题一开始根本不会注意到。比如检索增强模式下用户问的问题比较口语化比如那个啥流程现在还能用不向量检索召回的结果往往和流程作废、流程变更这类文档相关性很高但模型不知道用户具体在问哪个流程。我当时的排查思路是看用户问题的向量化结果——口语化表达和标准文档的表述差异太大Top-K 里混入不少弱相关片段。后来我在检索前加了一个 query 改写步骤用一个轻量模型把用户口语化的问题转换成多个候选检索词再分别检索、合并结果。这个改动让召回精准度提升了大约 9 个点。提示如果你发现模型答案总是对不上题先怀疑检索入口而不是模型能力。检索召回的是相似还是相关有时候并不一致。6. 我的经验不同场景下 context-mode 的默认配置建议经过这几个项目的折腾我形成了一个比较稳定的选择逻辑。虽然每个场景都需要个性化但有一个默认配置作为起点会大大减少试错时间。6.1 轻量客服机器人默认配置滑动窗口 固定知识 FAQ 检索。窗口大小10 条消息系统提示词包含 FAQ 关键词映射表触发条件当用户输入中命中 FAQ 关键词时把对应 FAQ 答案拼入上下文这种场景的用户问题比较固定历史依赖不强优先保证响应速度和成本。FAQ 答案本身就是权威内容不需要太多外部检索。6.2 企业知识库问答默认配置检索增强 历史摘要。检索 Top-K4-6 条相似度阈值0.45-0.5需要根据实际向量模型调摘要上限1200 token文档版本管理必须有这也是我在知识库项目最终用的配置。如果文档量特别大超十万级建议在检索前加一层基于关键词的粗筛减少向量检索的候选集规模。6.3 复杂 Agent 工作流默认配置混合结构化模式。区块划分系统指令 / 用户目标 / 工具结果 / 记忆摘要每次工具调用后工具结果块只保留最近一次每次规划前插入一个当前状态块汇总已完成步骤每完成一个子任务同步更新记忆摘要这种配置的调试成本高但提供最大的可控性。实际使用中我建议把上下文管理的逻辑独立成服务让各个 Agent 通过 API 获取上下文而不是各自维护一套。6.4 长文档写作辅助默认配置摘要模式 分章节滑动。把长文档按章节切片每章节独立管理上下文系统提示词里保留当前章节标题和全文大纲写作时只加载当前章节和相邻章节的摘要写作助手的问题在于上下文既需要全局信息大纲、风格又需要局部信息当前段落单一模式两头不讨好。分章节滑动是目前工程上比较实用的折中方案。我把这些配置整理成了一个速查表方便大家存下来业务类型推荐模式核心参调优首要风险闲聊/轻型客服滑动窗口窗口大小早期信息遗忘知识库问答检索增强Top-K、阈值台词不匹配、伪相关长时程对话摘要压缩压缩时机、摘要约束事实漂移Agent 编排混合结构化区块预算工程复杂度高长文写作分章节滑动摘要章节粒度全局信息丢失7. 最终的一些提醒在我实际把这个系统跑起来的这几个月里有一个体会很强烈context-mode 不是一个配置项而是一个系统工程。它涉及数据管道、向量检索、摘要策略、缓存机制、成本控制每一环都在互相影响。很多团队一开始把精力都花在模型选型和 prompt 调优上最后却发现瓶颈往往出在怎么喂上下文这件事上。另外不要迷信某个模式是银弹。滑动窗口实现简单但会失忆检索增强上限高但依赖数据质量混合模式灵活但工程量大。我的建议是先用最朴素的方案把流程跑通埋好日志然后再针对你观察到的问题延迟高遗忘频繁事实漂移逐步升级模式。上线前先明确你的核心指标是什么是速度、成本还是准确性上下文模式往往是在三者之间取平衡。最后多说一句上下文日志是你最宝贵的调试材料。每一次请求实际发给模型的完整消息序列都值得保存下来。很多诡异的问题靠猜靠试错都太慢打开日志看一眼答案往往就摆在那儿。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询