用了半年大模型,终于总结出这套 context-mode 上下文管理实战方案
你有没有遇到过这种场景同一个大模型接口别人调出来像聊天机器人你调出来像金鱼——聊到第七句就把你第一句说过的话忘得一干二净。或者反过来你给了模型一堆资料结果它答非所问从背景资料里挑了一条最不该当答案的内容给你。问题不一定出在模型本身多半是你没用对context-mode。我最早听到context-mode这个词是在做一个基于大模型的文档问答系统时。当时我们接了一个企业内部的知识库文档量不大但每一篇都特别长技术规范动辄上万字。模型用的是通用大模型每次请求只能带有限的上下文窗口。一开始我们把整篇文档直接塞进提示词里结果很快撞上 token 上限后来又改成只带用户问题对应的片段结果模型回答得前言不搭后语因为没有前因后果。最后折腾下来才发现真正要解决的不是该塞多少字而是怎么管理模型看到的上下文。这就是 context-mode 的用武之地它不是一个具体的功能按钮而是一整套管理对话上下文的设计模式。这篇文章我就把我在实际项目中趟过的那套 context-mode 设计思路、实现方案和踩坑记录完整梳理一遍。内容面向正在做 AI 应用开发、接大模型接口的工程师也适合产品经理和独立开发者参考——哪怕你只是用 Coze、Dify 这类平台搭过几个 Bot里面很多概念也是通用的。读完你会知道context-mode 到底管什么、有哪几种主流实现方式、各自的适用场景是什么、参数怎么调才不翻车。1. 内容整体设计与思路拆解1.1 先搞明白 context-mode 管的是哪三件事我用了一年多的大模型应用开发最深的体会是上下文管理不是简单的截断字符串。它本质上是三件事的集合记忆范围、信息优先级、模式切换。记忆范围决定模型能记住多长的对话历史或资料片段。比如用户和机器人聊了二十轮前面说过我住在杭州到第十五轮问我家附近有什么好吃的模型如果看不到第一轮的信息就回答不了。信息优先级解决的是在有限的 token 预算里哪些内容必须保留、哪些可以被压缩。这个跟做 PPT 一样——页面就那么多你不能什么都往上放得砍掉次要信息保住核心信息。模式切换则是说不同的任务场景需要不同的上下文策略比如闲聊场景需要完整的对话脉络文档问答场景需要精准的资料片段总结场景需要全局视野。在我拆过的项目里很多团队第一步就走错了。他们把 context-mode 等同于调大 max_tokens 参数或者把历史消息全部传过去。前者治标不治本后者直接撑爆 token 上限。context-mode 的核心思路应该是在有限的上下文窗口里通过合理的取舍和编排让模型在每一轮请求中都能看到它最关键的信息。1.2 为什么不能把所有内容都塞给模型这得先说清楚大模型的一个短板它的上下文窗口是固定且有限的。GPT 类的模型有 4K、8K、16K、128K 等不同档位但不管多大只要你传的内容超过窗口上限调用就会直接报错。就算没超限塞得太多也会带来三个实际问题。第一个问题是注意力稀释。模型在生成回答时理论上会看完你给的所有内容但研究表明它对中间部分的注意力明显低于开头和结尾。你塞了五千字背景资料模型很可能把重点放在了你最后写的那句帮我总结一下前面的详情反而没看全。第二个问题是成本暴涨。大模型 API 是按 token 计费的input 和 output 都要花钱。每轮对话把全部历史都带上意味着用户多聊一句你就要多付几千甚至上万 token 的钱。第三个问题是响应变慢。上下文越长模型处理时间越长用户的等待体验直线下降。所以 context-mode 的底层逻辑很直白上下文不是越多越好而是越准越好。管理上下文的本质是用更少的 token 表达更多的有效信息。1.3 从传什么到怎么传模式化的好处理解了上面两点就能明白为什么要模式化而不仅仅是写死一套逻辑。context-mode 的模式二字强调的是可切换、可配置、可复用。我举个实际例子。我在做一个客服机器人时最初只实现了一种策略永远携带最近十轮对话。结果发现两个极端场景都不好用。用户问售后政策时最近的对话里根本没有政策原文模型只能凭记忆瞎编用户闲聊时又根本用不上那种每轮都塞全量历史的做法纯属浪费。后来我改成按意图路由上下文模式检测到用户问政策类问题时自动切换到资料检索模式从知识库里检索相关段落闲聊时切换到轻量记忆模式只保留最近三轮对话。效果立竿见影回答准确率上去了成本还降了三分之一。这就是模式化的价值不同任务用不同策略互不干扰还能自由组合。2. 核心细节解析与实操要点2.1 模式一滑动窗口最简单的对话记忆滑动窗口是最基础也最常用的 context-mode 实现方式。它的逻辑非常简单只保留最近 N 轮对话更早的内容直接丢弃。每次新对话进来就像一列火车进站最新的车厢挂上去最旧的车厢被甩掉。具体实现时你需要做两件事。第一设定窗口大小也就是保留多少轮对话。第二设定轮次的单位一般有按轮和按token两种方式。按轮好理解按token更精确——比如设定窗口为 4000 token每次组装请求前从最新消息往前累加直到接近 4000 再截断。实操中我建议按 token 而不是按轮次来切。为什么因为每轮对话的长度差异极大。用户问一句你好算上系统提示词可能才几十 token但用户粘贴一段报错日志就有几百上千 token。你按十轮保留有时候保留的内容不到几百 token浪费窗口有时候又超过上限直接报错。按 token 切就可以精准控制每次请求的体量。def build_sliding_window(messages: list[dict], max_tokens: int 4000) - list[dict]: 按 token 数量从后往前构建滑动窗口。 messages 为完整的对话历史按时间正序排列。 # 简单估算每个 message 的 token 数中文字符约 0.6 token/字英文约 1 token/词 def count_tokens(text: str) - int: # 生产环境建议用模型配套的 tokenizer这里用近似估算 return int(len(text) * 0.7) 2 selected [] total 0 # 从后往前遍历最近的优先保留 for msg in reversed(messages): tokens count_tokens(msg.get(content, )) if total tokens max_tokens: # 如果当前消息放不下就截断这条消息保留前半部分 # 注意这里需要按 token 边界截断避免切断一个完整词 if total max_tokens: remain max_tokens - total # 简化截断直接按字符比例截断 char_ratio remain / tokens selected.append({ role: msg[role], content: msg[content][: int(len(msg[content]) * char_ratio)] }) break selected.append(msg) total tokens # 转回正序 return list(reversed(selected))滑动窗口的优点是实现简单、开销低、不用额外调模型特别适合算力预算紧张的团队。但它有个致命短板一旦信息被窗口滑出去就永久丢失了。就跟开会做笔记只记最近几页一样翻篇的内容全忘了。所以在需要长期记忆的场景里单独用滑动窗口基本不够。2.2 模式二摘要压缩用更少的字记住更多的事为了解决滑动窗口记不住的问题第二种模式出现了摘要压缩。思路是每隔几轮对话让模型把前面的内容总结成一段摘要之后请求就带摘要最近的详细对话。这样做的好处很明显摘要占用的 token 少但保留了核心事实。比如用户说了二十轮关于项目需求的话摘要可以压缩成用户需要做一个人事管理系统包含考勤、薪资、请假三个模块偏好用 Vue 技术栈预算 20 万以内。这段摘要可能只要 80 token却包含了二十轮对话的全部关键信息。实现上有两个关键点。第一是触发时机。一般有两种做法达到固定轮数就触发比如每五轮摘要一次或者达到固定 token 阈值再触发比如历史消息超过 3000 token 就先把最旧的部分摘要掉。我建议用 token 阈值触发理由跟滑动窗口一样——用户说话密度不一样固定轮数往往不够精准。第二个关键点是摘要的层级结构。对话一长单层摘要也不够用。我自己常用的做法是分层摘要先按两三百轮一组做底层摘要再在底层摘要之上做顶层摘要。请求时根据当前对话的长度决定带底层摘要还是顶层摘要。def compress_history(messages: list[dict], summarizer, token_limit: int 3000) - list[dict]: 当历史消息超过 token_limit 时将最旧的一部分压缩为摘要。 summarizer 是一个可调用对象输入文本输出摘要文本。 # 先算当前总 token 数 total_tokens sum(estimate_tokens(m[content]) for m in messages) if total_tokens token_limit: return messages # 没超限就不压缩 # 从最旧开始切分切出需要压缩的部分 compress_candidates [] compress_token 0 for m in messages[:-2]: # 保留最近两轮完整对话 t estimate_tokens(m[content]) if compress_token t token_limit * 0.6: # 压缩部分不要超过 60% 预算 break compress_candidates.append(m) compress_token t # 调用模型生成摘要 source_text \n.join(f{m[role]}: {m[content]} for m in compress_candidates) summary summarizer(source_text) # 返回摘要 未被压缩的最近对话 return [{role: system, content: f前情摘要{summary}}] messages[len(compress_candidates):]这里有个坑要提醒一下摘要本身也会出错或丢失细节。有时候模型会把关键数据漏掉比如用户说过合同金额 320 万摘要里可能只写合同金额较大。所以在摘要时如果源文本里有具体的数字、日期、人名、订单号我建议在摘要提示词里强制要求保留所有数字和专有名词。细节丢了比没钱更致命。2.3 模式三向量检索让模型只看到最相关的片段当对话历史不长、但有大量外部资料要参考时前两种模式都不够用。滑动窗口不涉及外部内容摘要压缩需要模型通读全文才能概括——资料多了根本没法全量塞进摘要。这时候要上第三种模式向量检索。向量检索的思路是先把知识库的文档切片每片生成一个向量embedding存入向量数据库。当用户提问时把问题也转成向量然后做相似度检索找出最相关的几个片段拼进提示词里交给模型。这样模型每次回答都能看到只跟当前问题有关的那一小段资料而不用面对整座知识库。我在代码里用的逻辑大概是这样的def retrieve_context(question: str, vector_db, top_k: int 3) - str: 从向量数据库检索与问题最相关的 top_k 个片段。 vector_db 需要实现 search() 方法返回 [(text, score), ...] results vector_db.search(question, top_ktop_k) if not results: return # 将检索到的片段拼接成上下文 blocks [] for i, (text, score) in enumerate(results): # score 是相似度数值越高越相关 blocks.append(f[片段{i1}]相关度{score:.2f}\n{text}) return \n\n.join(blocks)向量检索模式的成败很大程度上取决于切片策略。我见过很多人直接把长文档按固定字符数切比如每 500 字一片。这种做法简单但很容易把逻辑完整的段落切碎。比如一篇介绍产品功能的文档前半段讲功能背景后半段讲具体参数硬切成两片后模型只检索到参数那一片就会答得莫名其妙。我推荐的切片方式是按语义边界切也就是以段落、小节为基本单位一个标题下的内容尽量放同一片。长度可以不同优先保语义完整。还有一个参数很关键top_k也就是取几个片段。取少了信息可能不全取多了无关内容会干扰模型判断。我一般从 3 开始调然后根据回答质量微调。如果发现模型总是漏细节就加如果发现模型答非所问就减。2.4 模式四混合路由生产环境里真正能打的方案讲完三种基础模式你在实际项目中大概率会发现只依赖任何一种都会露怯。滑动窗口丢记忆摘要压缩费 token向量检索管不了对话脉络。所以生产环境里真正常用的是混合路由模式——先判断当前请求属于什么场景再决定走哪种上下文策略。我现在做的系统就是这套逻辑。进来的每一轮请求先经过一个意图分类器可以用大模型本身做也可以用轻量分类模型。分类结果分四类闲聊寒暄走滑动窗口只保留最近三轮对话历史提问走摘要压缩带上前情摘要最近详细对话知识查询走向量检索从资料库取最相关片段对话历史只保留当轮上下文综合分析走多重策略既带摘要也带检索片段还保留最近完整对话这样每次请求用的 token 预算差异很大但每个任务都能看到自己真正需要的信息。不像单一策略那样一刀切效果自然好很多。混合路由的实现并不复杂本质就是一个if-elif的分支判断加上对应的组装函数。真正的难点在于判断条件的准确率——如果意图分类错了后续所有上下文策略都跟着错。所以如果预算允许我建议给意图分类加上一个置信度输出低于阈值时走默认的滑动窗口模式宁可用基础方案也别走错路。3. 实操过程与核心环节实现3.1 从零配置一套双模式 context-mode很多读者可能觉得上面的概念有点空我直接分享我在一个智能客服项目里从零配置 context-mode 的完整过程你可以照着抄。项目背景客服机器人需要回答产品功能介绍、价格查询、售后政策三类问题同时还要能延续用户的多轮对话比如用户先问这款耳机多少钱再问支持无线充电吗。知识库约 200 篇文档每篇 2000 到 8000 字不等。我的方案选型是滑动窗口 向量检索 摘要压缩三模式混合。为什么没有只用一种因为客服场景天然混合了闲聊、知识问答和连续追问三种节奏单一模式肯定漏。第一步搭建向量检索链路。我用开源的 embedding 模型把文档切片后转成向量存入本地的向量数据库切片策略按 Markdown 标题分块——每个二级标题下的内容作为一片这样语义完整度最高。检索的top_k先设为 3。第二步配置滑动窗口。由于客服对话比较短我把滑动窗口设定为 2000 token也就是大概能容纳最近的 8 到 12 轮问答。超过这个额度后最早的消息被弹出。第三步加摘要压缩层。每 4 轮对话或累计超过 1500 token 时就把最早的一段对话压缩成摘要放进 system prompt 的前情摘要字段。最终组装提示词时我按固定的顺序拼接[系统提示词]你是一个客服机器人请根据参考信息回答用户问题。 [前情摘要]来自摘要压缩 [参考片段]来自向量检索 [最近对话]滑动窗口保留的原始消息这个顺序是有讲究的。系统提示词在最前保证模型明确自己的角色前情摘要其次提供对话背景参考片段是知识来源放在中间位置最近的详细对话放在最后因为模型对最后内容的注意力最强回答时自然会优先参考。3.2 参数调优实录从 token 预算到阈值选择配置完成后参数调优才是真正磨人的部分。我记录一次典型的调优过程。第一次跑通时我把滑动窗口的 token 上限设成 6000因为模型支持 8K 上下文我觉得留 2K 给输出就够了。结果连续对话超过十轮后经常出现超出最大上下文长度的报错。排查后发现原因系统提示词本身就有 800 多 token向量检索的片段平均每片 500 token三片就是 1500 token摘要又有 300 token这些都要算进 6000 的预算里。实际留给滑动窗口的只有 6000 - 800 - 1500 - 300 3400 token大概只能装七轮对话。而我的代码里滑动窗口参数写的是 6000等于窗口和总预算打架了。这就是 context-mode 参数设计里最容易翻车的地方预算不是单一维度的。你需要先算出固定开销再从总上下文窗口里减去固定开销剩下的才是滑动窗口的可用额度。我整理了一个简单的预算表供你参考模型上下文上限固定开销系统提示格式token检索片段开销摘要开销滑动窗口可用额度80008001500300540016000800300050011700320001000600080024200调完预算后第二个要调的是top_k。我最初设 3发现部分答案不够精确——模型经常参考到相似但不完全相关的片段。改成 2 之后准确率反而上升了。原因是客服文档里有大量相似的产品型号介绍top_k太大时检索到的第二、第三个片段可能是同一产品的不同版本反而干扰判断。所以top_k不是越大越好得根据知识库的区分度来定。如果文档主题差异大可以取 4 到 5如果文档主题相近建议 2 到 3。3.3 摘要触发条件的微调技巧摘要压缩的触发条件我一开始用的是固定轮数每 4 轮触发一次。效果不理想——如果用户连续发长消息4 轮就够累积 3000 token 了摘要生成时源文本过长不仅慢摘要质量也会下降。后来我改成轮数 token 双条件只要消息数超过 6 轮或者当前未摘要的消息总 token 超过 2500就触发压缩。这里还有一个细节摘要任务本身也要消耗 token 和调用延迟。如果用户在深夜想快速得到答案你让模型先做一次摘要再回答整体耗时可能翻倍。我的处理办法是在摘要逻辑里加一个闸门只有对话空闲超过 3 秒或者用户明确表示继续之前的话题时才执行摘要压缩普通轮次直接走滑动窗口。这个方案省了不少开销但也带来一个新问题摘要没有及时更新跨多轮的长对话中间可能会断片。解决办法是在触发摘要前先检查当前是否有旧摘要如果有就把旧摘要和新的未压缩消息合并让模型更新摘要而不是从头生成。这样生成的摘要一致性更好token 开销也更低。def update_summary(old_summary: str, new_messages: list[dict], summarizer) - str: prompt f 你负责维护一段对话摘要。 以下是已有的摘要 {old_summary} 以下是最新的对话内容 {format_messages(new_messages)} 请输出更新后的摘要保留原有信息并补充新出现的核心事实。 注意不要遗漏数字、日期、姓名和关键事件。 return summarizer(prompt)这段代码的核心思想是摘要不是重新生成而是增量更新。实际效果比每次重新总结全部对话不仅快还更连贯。3.4 完整请求流水线把三种模式串起来到这里我把完整的请求流水线串一遍。每次用户发来消息系统依次执行以下步骤意图分类判断本轮属于闲聊、知识问答还是长对话延续。上下文组装配分计算可用 token 预算按预算和意图选择策略组合。拉取候选内容根据策略从三个仓库取内容——滑动窗口保留的最近对话、摘要存储中的前情摘要、向量数据库中的检索片段。组装最终提示词按照系统提示 → 前情摘要 → 检索片段 → 最近对话的顺序拼装。调用模型把组装好的提示词发给大模型获取回答。更新存储把本轮对话写入滑动窗口如果触发压缩条件执行摘要更新。这里每一步都可能出岔子但最隐蔽的问题往往不在代码逻辑里而在存储与请求之间的数据一致性。比如滑动窗口更新了、摘要没更新模型看到的最近对话和前情摘要之间出现了重叠或断档。你问用户之前说过的某个数字摘要里有但最近对话里没有模型可能回复根据之前的讨论然后给一个错误数据。解决办法是在组装提示词时加一层查重——摘要里已覆盖的信息滑动窗口里对应轮次可以跳过不传避免信息重复。4. 常见问题与排查技巧实录4.1 模型回答记不住或答非所问怎么办这是 context-mode 最典型的症状。我排查的思路分三步。第一步确认是不是窗口太小。如果用户提到很久之前说过的内容模型答不上来大概率是滑动窗口根本没包含那部分历史摘要也没有覆盖。解决办法调大窗口额度或缩短摘要触发周期。第二步确认是不是检索片段不相关。如果用户的提问含有多层意图比如那个支持无线充电的耳机多少钱它既要检索无线充电相关的产品资料又要检索价格相关的商务信息。而我们的向量检索通常是按整句相似度匹配的很可能只检索到其中一层信息。解决办法对提问做子问题拆分分别检索再合并。第三步确认是不是提示词顺序问题。如果你把检索片段放在最后模型对检索片段的注意力太强可能会忽略前情摘要和最近的对话脉络导致答非所问。解决办法按我上面说的固定顺序组装并在系统提示词里写明信息权重——优先参考参考信息如果参考信息不足以回答再结合对话历史给出合理解释。4.2 token 超限和成本失控怎么处理token 超限的报错几乎是每个新手的必修课。除了在配置端把预算算清楚代码端也要加一层兜底截断逻辑组装完提示词后计算总 token 数如果超限先从滑动窗口里丢弃最旧的消息再超就减少检索片段数量最后才考虑截断摘要内容。成本控制方面我的经验是别在每轮请求里无脑带全量历史。混合路由最大的省钱点就在于闲聊场景不检索知识库知识问答场景不带长对话历史。我上线后把每轮平均 token 消耗从 4500 压到了 1800 左右接口费用降了一半多。省下来的是实打实的成本。4.3 检索质量差模型总是引用错误资料向量检索有个常见误区embedding 模型和场景不匹配。通用 embedding 模型在技术术语密集的领域效果往往一般因为它在训练时见过更多通用语义对专业术语的区分度不够。如果你的知识库里有大量相似术语建议专门用领域语料微调一个 embedding 模型或者直接选用领域型 embedding API。但微调 embedding 的成本不低很多小团队扛不住。我这里有一个折中的土办法关键词过滤 向量排序。先把知识库索引里的文档按关键词分类比如产品 A 的资料和产品 B 的资料各建一个索引检索时先根据用户提问里的关键词锁定索引范围再做向量相似度排序。这样即便向量排序排得不完美至少不会检索到另一个产品线的内容。4.4 长对话中摘要失真怎么办摘要失真基本上逃不掉。我做过一次对比实验让模型把一段 3000 token 的对话压成 150 token 的摘要再拿摘要反推对话细节结论是 30% 以上的具体信息会丟失尤其是数字和否定表达。比如不支持蓝牙可能被压成支持无线连接语义完全反转了。对策主要有两个。一是关键信息提取而不是摘要在摘要的同时让模型额外输出一个关键事实列表专门记录数字、日期、人名、产品型号、需求偏好。列表形式比自然语言摘要更容易保留细节。二是对否定表达做双重标记在摘要提示词里强调所有否定、限制、例外条件必须原样保留。这个我吃了不少亏才总结出来。5. 从单一模式到模式路由一点进阶心得如果你已经跑通了基础的 context-mode我建议下一步做模式自动路由。前面说的混合路由里我用的是意图分类器来决定走哪个模式。但分类器也会有错更稳的做法是让模型自己判断该用哪种模式。具体做法是在系统提示词里定义 several context modes对话延续、资料查询、混合分析让模型在每次回答前先输出一个mode tag系统根据这个 tag 来决定下一轮请求组装上下文时用哪些模块。我用下来效果不错模型的自我判断比外部分类器更贴合真实语义。当然这种方式要求模型本身有较强的指令遵循能力如果你的模型太小或太便宜它可能只会乱标记。还有一个小技巧关于上下文清理的时机。用户如果切换话题旧话题的上下文就变成了干扰。比如用户上一轮在问退货政策这一轮突然问你们公司叫什么你还带着一大段退货政策模型可能会答非所问。我一般会在意图分类里加一个话题切换类型一旦检测到切换清空前情摘要和检索片段只保留当前轮次的完整内容。代价是用户如果之后又想起退货的问题得重新问一遍但在实际客服场景里话题切换后回跳的概率非常低清理的收益远大于损失。最后再分享一个我在生产环境中验证过的心得context-mode 的配置不是一次调好、永久生效。随着用户量上涨、知识库更新、模型版本升级原来合适的窗口大小、top_k值、摘要触发阈值都可能需要重新调。我建议每两周做一次回放测试——拿真实用户的对话记录喂给新配置跑一遍看看回答质量和成本有没有变化。这个测试花的时间不长能避免很多线上才暴露的隐性故障。