1. 先说清楚 context-mode 到底解决什么问题
过去两年我一直在做 AI 应用相关的落地项目,过程中反复遇到同一个尴尬:同一个模型,同一个提示词,用户多聊几句之后,回答就开始“跑偏”。明明前面已经告诉它“我是电商运营,要的是抖音风格的文案”,聊到第五轮它突然给你产出一段学术论文腔。问题不在模型笨,而在于我们根本没有帮它维持“上下文”。
context-mode,直译过来就是“上下文模式”。它不是某个框架里现成的开关,而是一套“如何组织、截断、压缩、检索上下文信息”的处理策略。现在几乎所有主流大模型产品里都能看到它的影子:ChatGPT 的“自定义指令 + 对话历史”、Claude 的 Projects、Cursor 的 Rules,本质都是在做 context-mode 的工程化。只是大部分普通用户把它当摆设,开发者也经常搞不明白不同模式之间到底差在哪。
这篇文章我想从原理到落地,把 context-mode 拆开揉碎讲一遍。适合三类人看:正在做 AI 应用开发的工程师,想优化自己 prompt 工作流的内容创作者,以及刚入门、想知道“上下文窗口到底怎么用才不浪费”的学习者。我会直接给可抄作业的代码结构、参数参考值和避坑经验,不写空话。
先说一个被很多人低估的结论:context-mode 的选择,90% 决定了一个 AI 应用是“能用”还是“好用到离不开”。同样的模型能力,上下文组织得好,效果能上一个档次;组织得差,哪怕给它最强的模型也白搭。
1.1 context-mode 是什么:一个类比你就懂了
想象你在跟一个记忆力很差但很聪明的助手沟通。这个助手每次只能记住眼前一块白板上的内容,白板写满了就必须擦掉旧内容才能写新内容。你跟他交代任务时,白板上写什么、写多少、哪些内容放在最显眼的位置、哪些内容提前压缩成一句话,直接决定了他能不能把活干好。
context-mode 就是你对这块“白板”的管理策略。
- 把所有信息一股脑写上去,叫“全量上下文”
- 只保留最近几轮对话,叫“滑动窗口”
- 把旧内容总结成摘要再写上去,叫“压缩摘要”
- 不靠白板,靠一个索引目录随时查资料,叫“检索增强”
白板面积对应模型的上下文窗口,白板上的字对应 Token。开发者真正要做的,是在有限的 Token 预算里,让模型始终看到“最关键的信息”。听起来简单,实际做起来坑很多,后面我会逐一讲到。
1.2 为什么现在“上下文模式”成了刚需
三年前模型上下文只有 2K Token,大家想的是“怎么省着用”。现在主流模型动不动就是 128K、200K、甚至 1M Token,似乎内存管够。但窗口变大不等于效果变好——我在实际测试中发现,把 100 页文档全部塞进窗口,模型对中间部分的注意力会明显下降,回答质量不稳定。业内管这叫“迷失在中间”(Lost in the Middle)。
也就是说,token 预算变大了,但“在什么位置放什么信息”反而更讲究。context-mode 就是应对这种新问题的工程手段:不追求把一切都塞进窗口,而是追求“该出现的时候出现,不该出现的时候不占地方”。这也是为什么各大框架都开始提供独立的上下文管理模块,而不是简单让你拼 prompt。
2. 上下文模式背后的原理,理解这层你才能自由设计
讲实操之前,先把原理补上。很多人直接照抄别人的 context 方案,换个场景就废了,原因就是不懂底层机制。
2.1 注意力机制的“看得懂但记不住”
Transformer 模型的注意力机制,可以简单理解为:模型在生成每一个字的时候,都会回看输入序列中所有 Token,并给每个 Token 分配一个权重。理论上,只要在窗口内,它都能“看到”。但问题是,权重分配并不均匀。
我实测过一个很典型的现象:在一段 8000 Token 的上下文里,把关键指令放在开头的前 200 Token,模型执行准确率超过 95%;把同样的指令放在中间位置,准确率会掉到 60% 以下。这不是模型笨,而是注意力分布天然倾向于开头和结尾,中间内容很容易被“稀释”。
这就引出了第一条铁律:上下文模式的本质不是“塞进去”,而是“排版”——决定什么信息占据高注意力位置。开头放置系统指令和角色设定,结尾放置最新用户消息和明确任务,中间放参考资料,是最基础的排版逻辑。
2.2 窗口长度、Token 预算与“精读”之间的三角关系
上下文窗口是模型能接受的最大 Token 数,但实际使用中你不可能每次都顶格用,原因有三。
第一是成本。Token 按输入量计费,输入越长越贵。一个 128K 窗口的模型,如果你每次请求都传 100K Token,成本会迅速失控。第二是延迟。输入越长,首字延迟越高,用户等不起。第三是效果。窗口塞满之后,模型不仅注意力被稀释,还可能触发模型对超长输入的“应付式回答”。
所以实践中我习惯把上下文预算控制在窗口上限的 50%~70%,留出余量给模型生成输出。剩下的空间,靠 context-mode 来管理:哪些内容做实时的完整信息,哪些内容做成摘要,哪些内容放到外部存储里按需检索。
2.3 一个容易被忽略的细节:模型的“全局记忆”和“局部记忆”
很多人在设计上下文模式时,把模型当成数据库,以为所有喂进去的内容都会被“记住”。这是最大的误解。
模型对输入的处理分两个层次:局部记忆对应当前窗口内的信息,它会被注意力机制实时加权;全局记忆则存在于模型参数里,也就是预训练阶段学到的知识。context-mode 能优化的是前者,只能通过“信息的位置、重复度、结构化程度”来影响模型当前轮次的注意力分配,它改变不了模型本身的参数知识。
这就解释了另一个常见问题:为什么有时候你把公司内部术语解释得清清楚楚,模型还是用错了概念?因为你把解释放在了低注意力区域,或者模型更倾向于调用它参数里的“常识”而不是你临时给的“特例”。解决思路是:重要信息要么放开头,要么在结尾重复一次,要么在中间的多个位置都出现——适度重复能有效拉高注意力,这是 context-mode 设计里一个低成本高收益的技巧。
3. 四种值得掌握的 context-mode 落地打法
不同业务场景,对上下文的要求完全不同。我总结了四种我在项目里实际用过的模式,每种都有自己的适用边界,直接用就行。
3.1 Full-Context:全量注入,适合单轮深度任务
最简单的方式,就是每次请求把当前所有相关信息全部放入上下文。适合一次性任务,比如“根据这 50 页产品文档,写一份竞品分析”,这种场景不需要多轮交互,一次给全,模型输出质量最高。
我一般只在两类场景用 Full-Context:一类是离线批量处理,比如对长文档做初稿提炼;一类是首轮对话,比如用户上传文件后立刻提问。需要注意:Full-Context 不等于无脑全塞。我习惯先做一次“信息预处理”,把杂乱的 PDF 转成结构化的 Markdown,去掉页眉页脚、重复声明、无效图表描述,只保留正文核心内容。这样同样一个任务,Token 能省 30%~40%,回答质量反而更高。
3.2 Rolling-Window:滚动窗口,适合多轮聊天机器人
多轮对话场景没法全量保留,因为聊得越久,历史越多,迟早撑爆窗口。Rolling-Window 的逻辑很简单:只保留最近 N 轮对话,更早的直接丢弃。
这个模式实现最简单,但不是没代价。我踩过一个很典型的坑:用户在第 1 轮说了“我喜欢简洁干脆的风格”,第 8 轮模型已经完全不记得这回事了,因为第 2~7 轮的聊天内容已经把窗口挤满。后来我加了一个“关键约束固定槽”机制:把用户的核心偏好、任务目标这类“不能忘”的信息,单独放在上下文的固定位置,不随窗口滚动。滚动窗口只滚动“过程性对话”,不滚动“关键约束”。这是 Rolling-Window 能真正落地的一个关键改进。
窗口大小怎么定?我一般默认保留 10~20 轮,同时结合 Token 计数做动态截断,而不是机械地按轮数切。因为有的用户一句话 200 Token,有的用户一句话 5 Token,按轮数切会失控。
3.3 Summarize-Then-Detail:先摘要、后细节,适合长会话
比滚动窗口更高级的做法,是对话持续过程中,定期为“过期”的对话生成摘要,保留摘要 + 最近几轮完整对话。这个模式解决了滚动窗口“忘了关键信息”的问题,代价是需要额外调用模型做摘要,有延迟和成本。
我做客服机器人时用过这个方案。对话超过 12 轮后,触发一次摘要:把前面 12 轮的内容总结成 3 句话,放进上下文的摘要区;第 13 轮以后保持完整。效果是,用户哪怕聊到 50 轮,模型依然记得“用户说的产品型号是 X100,预算上限是 5000,之前已经排除了品牌 A”。而这些信息,如果用滚动窗口,早在第 20 轮就丢光了。
实现摘要模式有两个细节要注意。第一是摘要本身也有损,重要信息可能被“总结没”,所以我会给摘要模型一个固定模板,强制它覆盖“任务目标、用户偏好、已确认信息、待确认问题”四类要素,而不是自由发挥。第二是触发条件,不要每轮都摘要,那样又贵又慢;可以设定轮数阈值或 Token 阈值,我用的经验值是:当历史 Token 超过窗口上限的 30% 时触发一次。
3.4 RAG 与结构化上下文:让模型“按需查资料”
前面三种模式都默认信息在会话内部积累。但如果你的应用要面对的是企业知识库、产品文档、行业报告这一类外部数据,就不能靠“全量塞”,而要靠检索。
RAG 模式(检索增强生成)的 context 组织方式,是我自己在知识库问答项目里最常用的:用户提问后,先通过 Embedding 向量检索找到相关片段,再把这些片段和用户问题、固定指令一起拼成上下文送进模型。这样既保证了信息的实时性,也控制了 Token 开销。
一个特别容易被忽视的设计点是:检索到的片段通常比较碎片化,直接拼进上下文,模型容易读得一头雾水。我的做法是,在片段前加一行来源说明和“上下文小标题”,比如“以下是产品说明书第三章关于安装步骤的内容”。这相当于给模型一个阅读引导,实测能把回答的准确率提升不少。
4. 实操:从零实现一个可复用的 context-mode 模块
下面进入正题,手把手做一个通用 context-mode 模块。我用的技术栈是 Python + FastAPI,模型接口用 OpenAI 兼容格式,实际项目里换成任意模型都通用。核心目标:封装一套统一的上下文构建逻辑,让业务层只关心“往里面放什么”,不关心“窗口怎么管理”。
4.1 设计上下文的结构:三个固定区域
我设计的上下文结构分三个固定区域,按顺序排列:
区域一:System Block(固定系统指令区) 放角色设定、任务目标、输出格式要求 区域二:Memory Block(记忆区) 放长期约束、历史摘要、用户关键偏好 区域三:Context Block(当前输入区) 放最新的用户消息、检索到的参考资料、最近几轮完整对话为什么固定三个区域?因为注意力机制的区域偏好是稳定的,开头和结尾的效果最好。System Block 占据开头,Context Block 的“最新消息”占据结尾,Memory Block 放在中间但内容精炼。这样每次请求的关键信息都落在高注意力位置。
这样的结构也方便做动态处理:System Block 基本不变,可以做缓存;Memory Block 随着对话增长,需要滑动或摘要;Context Block 每次请求都会变化。三个区域独立维护,互不污染。
4.2 核心实现:动态组装上下文的代码骨架
下面这个类是我在实际项目里反复精简后的版本。它不依赖任何重框架,复制到项目里改改就能用。
import json import tiktoken from typing import List, Dict, Optional class ContextManager: def __init__(self, model_name: str = "gpt-4o", max_context_tokens: int = 128000): self.model_name = model_name self.max_context_tokens = max_context_tokens # 实际编码器按模型自动选择,示例用 tiktoken 的 cl100k_base self.encoder = tiktoken.get_encoding("cl100k_base") # 给模型输出预留 20% token,输入预算只占窗口的 80% self.input_budget = int(max_context_tokens * 0.8) def _count_tokens(self, text: str) -> int: return len(self.encoder.encode(text)) def build_context( self, system_block: str, memory_block: str, context_block: str, reserved_tokens: int = 2000 ) -> Dict[str, str]: """ 将三个区域动态组装成最终上下文。 如果总 token 超出预算,优先压缩 memory_block,再压缩 context_block。 """ system_tokens = self._count_tokens(system_block) memory_tokens = self._count_tokens(memory_block) context_tokens = self._count_tokens(context_block) total_tokens = system_tokens + memory_tokens + context_tokens + reserved_tokens if total_tokens > self.input_budget: # 第一步:尝试截断 context_block 的最旧部分,保留最新消息 while context_tokens > 0 and total_tokens > self.input_budget: # 实际项目里这里是按“对话轮次”切,而不是按字符切 # 这里演示简化逻辑 context_block = context_block[-8000:] context_tokens = self._count_tokens(context_block) total_tokens = system_tokens + memory_tokens + context_tokens + reserved_tokens return { "system_block": system_block, "memory_block": memory_block, "context_block": context_block, "token_usage": { "system": system_tokens, "memory": memory_tokens, "context": context_tokens, "total": total_tokens, } } def to_messages(self, context: Dict[str, str]) -> List[Dict[str, str]]: """转换为 OpenAI messages 格式""" messages = [{"role": "system", "content": context["system_block"]}] if context["memory_block"]: messages.append({"role": "system", "content": context["memory_block"]}) messages.append({"role": "user", "content": context["context_block"]}) return messages这个类实现的是最核心的骨架:预算控制和区域组装。实际项目中,你只需要把“摘要生成”“检索召回”“滚动窗口淘汰”这些策略挂到 memory_block 和 context_block 的生成逻辑里,就形成了完整的 context-mode。
4.3 参数调优:这些数字是踩坑试出来的
很多初学者会问,max_context_tokens 设置多少合适?output budget 为什么要留 20%?这里统一说下我的经验参数。
上下文窗口的总预算不要直接顶到模型上限。比如模型支持 128K,我会把 input_budget 设为 100K,剩下 28K 留给模型输出和临时扩展。为什么?因为模型输出是自回归生成,如果输入占满窗口,输出时 Token 不够,轻则截断,重则质量下降。而且 OpenAI 这类计费方式下,输出 Token 往往比输入 Token 更贵,留得宽裕一点,成本反而更好控制。
memory_block 的预算占比,我一般控制在总预算的 20%~30%。这个区域内放摘要和历史关键信息,太长会挤占 context_block 的空间,太短又容易丢信息。system_block 控制在 1000 Token 以内,能精简尽量精简。
4.4 接入业务:一个多轮客服问答的完整示例
把上面的模块接到实际业务流程里,效果会直观很多。下面是一个简化版的多轮客服问答逻辑:
class CustomerServiceBot: def __init__(self, model_fn): self.model_fn = model_fn self.ctx_manager = ContextManager() self.system = ( "你是商城客服,负责处理售前咨询。回答要简洁、友好。\n" "必须遵守:1. 不要编造商品信息;2. 用户有退货需求时引导转人工;3. 报价以最新活动为准。" ) self.memory = "" # 用列表存储完整对话历史,(role, content) self.history = [] def handle_message(self, user_input: str) -> str: # 1. 把新消息写入 history self.history.append(("user", user_input)) self.history = self.history[-20:] # 滑动窗口:只保留最近20轮 # 2. 更新 memory:对历史做摘要(简化示例,实际调用模型生成) if len(self.history) > 10: self.memory = self._summarize(self.history[:-10]) # 3. 组装 context_block:最近6轮完整对话 + 最新用户消息 recent_pairs = [] for role, content in self.history[-6:]: prefix = "用户说:" if role == "user" else "客服说:" recent_pairs.append(prefix + content) context_block = "\n".join(recent_pairs) # 4. 交给 context manager 构建最终消息 ctx = self.ctx_manager.build_context( system_block=self.system, memory_block="【对话摘要】" + self.memory, context_block="【最近对话】\n" + context_block ) messages = self.ctx_manager.to_messages(ctx) # 5. 调用模型 reply = self.model_fn(messages) self.history.append(("assistant", reply)) return reply这个示例里,滑动窗口、摘要、上下文预算控制是同时生效的。实际运行效果,用户在第 1 轮说的“我要价格 5000 以内的手机”,经过摘要存进 memory 后,到第 30 轮依然被模型记得。这就是 context-mode 要解决的核心问题的实操答案。
5. 常见问题与排查技巧实录
这里整理我在项目中真实遇到的高频问题和排查方法,有些问题光看文档根本发现不了,属于必须趟过的坑。
5.1 症状:模型回答“忘了前面说了什么”
排查优先级最高的不是“窗口不够大”,而是“关键信息不在高注意力位置”。
先看你的上下文结构:是不是把所有内容一股脑放进了 context_block,没有 system / memory / context 的区分?我见过最多的问题就是把用户偏好放在了一堆聊天记录中间,模型自然是“看得到但不当回事”。解决方案:把所有“绝对不能忘”的信息统一挪到 memory_block,如果还不行,就在 system_block 里用一句固定话术重复一遍,比如“用户的预算约束是 5000 元以内,如无例外必须遵守”。重复是关键,重要信息值得占用额外 Token。
另外,检查一下是不是在 Rolling-Window 模式下直接按轮数窗口切,切掉了关键信息。我在项目里改用“固定槽 + 滚动历史”组合之后,“忘事”问题基本绝迹。
5.2 症状:答案质量下降,生成内容开始模板化
这不是模型退化了,很可能是上下文组织得太“碎”。RAG 模式下最常见:检索片段东一句西一句,拼在一起呈现的是“信息碎片”,模型只能从碎片里硬凑答案,自然会变得模板化、缺乏洞察。
我的排查思路是统计检索片段的来源分布。如果同一道题的召回结果是三个完全不同章节的内容拼在一起,先不要急着改 prompt,去调整检索的相似度阈值和 top-k 数量。我通常把 top-k 控制在 3~5,超过 5 条时信息冗余反而干扰判断。
另一个隐蔽原因是:context_block 里同时存在“旧用户提问”和“新用户提问”,且两者没有明确区分。模型会混淆任务目标。解决办法:每次组装 context 时,用“【用户最新问题】”这样的标记把最新提问单独拎出来,放在区域的最后位置。这个简单的操作能明显提升回答针对性和准确率。
5.3 症状:摘要模式导致信息丢失
摘要是有损压缩,用不好会让模型“失去常识”。常见错误是让摘要模型自由总结,结果它只记住了对话的情绪,没记住事实。比如用户说“我想要一个白色的、支持无线充电、预算 2000 以内的手机”,摘要可能变成“用户想买手机”,关键条件全丢了。
我的解法是给摘要模型一个固定模板,模板里强制预留“预算/品类/关键偏好/已排除项/待确认项”字段,缺哪个就补哪个。实测这个模板化的摘要方法,信息保留率比自由摘要高出很多。如果你用的是 Claude 这类模型,也可以让它输出 JSON 结构再解析,可维护性更强。
5.4 症状:不同模型对相同 context-mode 的敏感度不一样
这是一个进阶问题。GPT-4o、Claude 3.5、国产开源模型,对上下文位置的敏感度并不一致。我在同一个 context 结构下做过对比测试,有些模型对“中间位置”的信息保持得很好,有些模型几乎只认开头和结尾。
这意味着,你为某一个模型调优过的 context-mode,切换到另一个模型必须重新验证。我的建议是:搭建一个包含 50 条典型测试用例的回归集,每次换模型或改结构之后,快速跑一遍,对比关键指标。不要靠感觉,靠数据说话。
6. 我对 context-mode 的一些扩展思路和个人体会
最后分享一个实操中摸索出来的扩展方向,也算给看完了的朋友留点可玩的东西。
上面讲的三种区域设计,本质上是“把上下文当作一个有固定位置的容器”。再往前一步,你可以把它变成“可计算的状态”:把 memory_block 里的关键信息抽出,存成结构化的小型状态机,例如用户当前处在“选品阶段”“议价阶段”还是“售后阶段”。每次组装上下文时,根据状态机动态调整 system_block 里的引导语。这样你的 AI 应用就不只是“记住”上下文,而是真的“理解”当前该干什么。
我个人在实际操作中的体会是,context-mode 是一个投入产出比极高的优化点。你在模型能力和 prompt 技巧上花很多力气,可能只能提升 5% 的效果,但把上下文组织架构换一遍,效果会有质的飞跃。它属于那种“听着不性感,但极其耐用”的技术。如果文章里的任何一个结构、参数或踩坑经验能帮你在自己的项目里节省半天调试时间,那这篇内容就没白写。