如果你最近在技术社区里搜过 context-mode 这个词,会发现它同时出现在好几个完全不同的语境里:有人问某个编辑器的插件里怎么开"上下文模式",有人在 AI 编程工具里反复调上下文相关配置,还有人在大模型应用的项目区里讨论到底该用什么策略处理超长对话。表面看是不同的问题,内核其实指向同一件事:当模型的"工作记忆"装不下整个项目时,我们怎么决定让模型看到什么、忽略什么。
这篇文章我想把这些零散的讨论收敛成一个工程话题来讲:context-mode 在 AI 应用里究竟该怎么理解、怎么选、怎么配。我会顺着一个真实项目的调试过程,把全量塞入、滑动窗口、摘要压缩、检索增强、分层混合这几种主流上下文模式逐个拆开,附上可运行的配置示例、参数计算方法和踩坑记录。如果你在做 AI Agent、用大模型处理长代码库或长文档,或者只是被"token 爆炸"和模型"失忆"折腾过的新手,这篇应该能帮你少走不少弯路。
1. 为什么"上下文模式"这个技术概念突然火了
1.1 从 AI 编程工具的"失忆"问题说起
如果你在过去半年里用 AI 编程工具改过大一点的项目,下面的场景应该不陌生。第一轮对话,模型表现像个熟悉你代码库的同事,能准确说出某个接口的定义、某个函数的调用关系;可一旦连续改到第三个、第四个文件,它就渐渐"失忆"了——忘了你在第一轮就定下的命名,重复修改你已经改好的代码,甚至出现前后自相矛盾的两套实现。
我第一次意识到这是个严肃问题,是在一个 Flask 商城项目里。项目不大,就一万多行代码,但牵扯到认证、中间件、数据库会话好几个模块。我让 AI 帮我修一个"用户登录后 session 不生效"的 Bug,第一轮它定位到了登录视图,第二轮它开始改中间件,第三轮它居然把 session 配置推倒重来了。我回头看对话记录,发现它每次调用时看到的上下文都不一样——我后来才搞清楚,工具默认的上下文策略一直在丢前面的关键信息。
这不是模型能力问题,而是上下文策略问题。模型每次调用能"看到"的,只是上下文窗口里装下的那一小部分信息。窗口之外的内容,无论多重要,对模型来说都不存在。所以你会发现一个奇怪的现象:上下文窗口越开越大,模型反而越来越不靠谱。原因很简单,窗口里塞了大量低信噪比的代码和对话,真正关键的约束被淹没了。
1.2 上下文模式的本职:给 LLM 做"工作记忆"管理
为什么大模型会"失忆"?我用工作记忆来类比。人的工作记忆容量大概只有 7±2 个组块,专家和普通人的差距,不在于"记住更多",而在于知道该把注意力放在哪里。大模型也一样,它的上下文窗口虽然从几万做到了几十万 token,但一个正经的软件仓库动辄几十万上百万 token,窗口永远不够用。于是就有了 context-mode——上下文模式,本质上是给模型做"工作记忆调度"。
调度要做三件事:哪些信息常驻窗口(比如系统提示、架构约定),哪些信息按需临时加载(比如某个具体文件),哪些信息压缩成摘要后留档(比如已经讨论完的早期对话)。你选择不同的上下文模式,就是在调整这三件事的比例。
还有一个经常被忽略的事实:上下文窗口不是越大越聪明。长上下文研究里有个知名结论,信息放在长文本中间位置时,模型关注度和准确度会明显下降,开头和结尾的信息更容易被模型利用,行业内把这叫做"中间丢失"(Lost in the middle)。换句话说,就算你塞进去一百万 token,模型真正高效利用的可能只有开头和结尾那一段。这也是为什么现在越来越多工具开始把 context-mode 作为一个显性配置项交给用户——因为简单堆窗口这条路走不通了。
下面我把目前主流工具和框架里常见的上下文模式逐个拆开,讲清楚原理、成本和适用场景。
2. 五种主流上下文模式的本体拆解:从全量塞入到按需召回
2.1 全量上下文(Full Context):简单粗暴但代价最高
全量模式是我最开始用的方式,也是最符合直觉的模式:把整个项目、整份文档、全部历史对话一股脑塞进窗口。它在小型项目里确实好用,比如一个几千行的单文件脚本,全量塞入后模型对代码的把握是最完整的;问一个一两页纸的文档,全量模式也是最好的选择。
但全量模式的代价是线性增长的。输入 token 越多,费用越高、延迟越长,而且窗口一旦逼近上限,调用直接失败,你得手动删减再来。我实测过一个 8 万 token 左右的输入,单次调用花费能抵得上二三十次普通问答,耗时也从几秒拉长到一分多钟。更要命的是,全量不等于全对——大量无关代码会把关键信息稀释掉,模型反而容易跑偏。全量模式的正确用法是"小项目全量、大项目救急",而不是无脑堆。
2.2 滑动窗口(Sliding Window):记忆只有最近几轮
滑动窗口是另一种极端:只保留最近 N 个 token 或最近 N 轮对话,更早的内容直接丢弃。它的实现最简单,成本也最低,很多聊天类工具底层默认就是这种策略。客服机器人、闲聊助手这类任务用滑窗很合适,因为用户的问题通常只跟前几轮对话相关。
但滑动窗口有两个硬伤。第一,早期定下的重要约束会被悄悄丢掉。我跟 AI 约定了"所有时间统一用 UTC 存储",聊了 20 轮之后这个约定被滑出窗口,它又开始给我生成北京时间,还理直气壮。第二,截断会引发幻觉——窗口把某个函数定义切掉了,模型不会承认"我没见过",反而会根据残留的片段推断一个自洽的假实现。在代码场景里,这种幻觉比直接说"不知道"危险得多。所以滑窗模式只能作为最底层的保底策略,要在上面叠加别的机制。
2.3 摘要压缩(Summarization):忘掉细节,只留主线
摘要压缩模式的思路是把早期内容用一次额外的模型调用压缩成摘要,再让摘要替代原始内容常驻窗口。相当于给模型配了一个"笔记本":具体细节可以丢,但主线结论要记下来。这个模式在长对话、长文档处理里非常实用。我处理一份上百页的技术合同时,就是先把前几十页压缩成"已确认条款"的摘要,再把几个关键章节原文放进去,效果比硬塞全文好得多。
代价也很明显:摘要过程是一次额外调用,增加延迟和成本;而且摘要一旦生成,细节就永久丢失。我踩过最典型的坑是,摘要模型压缩时把"cookie 必须带 Secure 属性"这种带条件的细节精简成了"使用安全 cookie",结果后续所有判断都建在这个不完整的结论上,排查了两小时才发现源头在摘要这一步。压缩模式适合保留"主线",不适合保留"精确参数"。
2.4 检索增强(RAG / Selective Retrieval):按需召回相关上下文
检索增强模式,也就是大家常说的 RAG,做法是把知识库切块、向量化、建索引,用户提问时先检索出最相关的若干块,再拼进上下文让模型作答。它是目前大模型知识问答的标准方案,也是控制成本最有效的手段。一套一万行代码的仓库,传统做法是把全部代码塞进窗口,检索增强则只把跟当前问题相关的那几百行代码抽出来,输入 token 可能只有全量模式的十分之一。
但检索增强的成功率完全押在召回环节。分块粒度、向量模型的相似度判断、甚至索引的更新时机,任何一个环节出问题,模型就拿不到该看的内容。我见过太多人以为接上向量数据库就万事大吉,结果检索出来的代码块跟当前问题只是"字面相似",真正的根因文件根本没被召回。检索模式解决的是"怎么省成本、怎么扩展规模",但单靠它解决不了"上下文质量"的全部问题。
2.5 分层混合(Hierarchical Context):Agent 编程的主流工程方案
我目前最推荐、也实际用得最多的,是分层混合模式。它的思路很简单:给模型提供一份"全局地图",再按需加载"局部细节"。全局地图可以是一段精心写的项目架构说明、一个文件索引、或者一份两百行的概览摘要;局部细节就是当前任务真正需要的几个文件全文。
这其实是模拟人读代码的习惯:先看目录结构,锁定可疑模块,再打开对应文件精读,而不会把整个仓库从头到尾读一遍。分层混合在执行 Agent 型任务时优势特别明显:Agent 先靠全局地图规划步骤、选择工具,再调用检索或文件读取工具获取局部内容。这样既保留了全局视野,又控制了 token 成本。现在不少编程 Agent 工具,内部本质上就是这种架构。注意,这一节不是让你在界面里点一个"混合模式"开关,而是要在设计 Agent 时,把"总-分"的信息加载逻辑写进工作流里。
3. 我在一个 1.2 万行代码项目里的模式配置实战
3.1 评估任务与日志:没有度量就没有优化
光讲原理用处不大,我拿一个真实项目说事。去年我维护了一个个人用的 Flask 商城后台,代码量大概 1.2 万行,包含认证、商品、订单、支付几个模块。项目虽小,但模块之间有大量交叉引用。那段时间遇到一个线上问题:用户登录成功后,session 并没有真正生效,刷新页面又回到未登录状态。
为了搞清楚不同上下文模式对这个问题的解决效果差异,我设计了一个很简单的评估方案:固定同一个 Bug 描述,重置对话,分别用四种上下文模式跑一遍,然后记录四个指标——总输入 token 数、单次平均耗时、是否能准确定位根因、修复方案验证是否通过。这个方法本身不复杂,但非常管用。很多人在调上下文策略时全凭感觉,今天换个窗口大小,明天换个索引库,却说不清哪次改动带来了提升。没有度量,优化无从谈起。
3.2 代码级配置:以 LangGraph 为例实现四种模式
我的实验基于 LangChain/LangGraph 做了一版可切换上下文策略的 Agent 原型,环境是 Python 3.11、langchain 0.3.x、langgraph 0.2.x。下面这些代码片段可以直接参考,但依赖版本比较关键,建议按你的实际环境锁定版本。
先看滑动窗口的配置。LangChain 提供了 trim_messages 工具,核心逻辑是保留最近的 N 个 token:
from langchain_openai import ChatOpenAI from langchain_core.messages import trim_messages model = ChatOpenAI(model="gpt-4o", temperature=0) trimmer = trim_messages( max_tokens=8000, # 窗口预算 strategy="last", # 保留最近的对话 token_counter=model, # 按模型的tokenizer计数 include_system=True, # System消息始终保留 allow_partial=False, # 不允许截断到半句 start_on="human", # 从人类消息开始截取 )摘要压缩模式我写成自定义的 Graph 节点:当消息总长超过阈值时,把靠前的若干轮对话交给摘要模型,生成一段"已确认信息"替换原文。核心逻辑大概是下面这个流程:
from langchain_core.messages import SystemMessage SUMMARY_THRESHOLD = 6000 def summarize_old_messages(state): messages = state["messages"] total = sum(len(m.content) for m in messages) if total < SUMMARY_THRESHOLD: return {"messages": messages} old, recent = messages[:-4], messages[-4:] summary_prompt = "请压缩以下对话为要点摘要,保留所有约定、参数和结论。\n\n" + \ "\n".join(f"{type(m).__name__}: {m.content}" for m in old) summary = model.invoke(summary_prompt).content return { "messages": [ SystemMessage(content=f"早期对话摘要:{summary}"), *recent, ] }至于检索增强模式,我用的是常见的切块加向量库方案。切块我选 300 字符一块,重叠 50 字符,向量化后存入 FAISS。检索时取 top_k=8,把命中的代码块拼进 Prompt。这部分代码太通用,就不整段贴了,值得强调的是下面这组参数。
3.3 参数调优的关键计算:窗口、阈值与 top_k 怎么定
很多人卡在"参数到底设多少"。这组参数不是拍脑袋,是有估算逻辑的。先说 token 估算:不同文本类型的 token 密度差异很大,我按实际经验总结了下面这张表,基本够用。
| 文本类型 | 估算规则 | 备注 |
|---|---|---|
| 中文正文 | 每千字约 1100-1500 token | 口语化文字取低值,书面语取高值 |
| 英文正文 | 每千字约 200-300 token | 实际按词数:每词约 1.3 token |
| 代码 | 每千字符约 250-350 token | 换行和缩进会显著增加 token |
窗口预算的公式我习惯这样算:上下文窗口总长乘以 0.8 作为输入预算,预留 20% 给模型的输出 token。比如一个 8K 窗口的模型,输入最多控制在 6400 token 左右,否则输出很容易触顶报错。滑动窗口的 max_tokens 就直接填这个输入预算。
top_k 的估算我给一个实用公式:先估计你希望覆盖的目标代码范围大概有多少字符,除以单块字符数,得到需要的块数,再乘以 3 作为冗余。比如目标代码约 2000 字符,单块 300 字符,那么约需要 7 块,乘 3 冗余后取 top_k=16 左右。为什么乘 3?因为单次检索的召回率和相关性打分都不完美,适当放大候选池,让重排阶段去筛,比一开始就掐死候选数量更稳。
4. 同一任务的横向对比:全量、滑窗、压缩、检索的实测数据
4.1 测试任务设计与执行方法
实验执行方式我尽量控制成单变量:同一个模型(gpt-4o)、同一个 Bug 描述、同一个代码库快照,只切换上下文策略。修复是否成功以"模拟会话验证通过"为准,就是让模型修复后,我在本地跑一遍登录流程,确认 session 能跨请求保持。
这个问题本身很有代表性,因为它不是一个文件里的局部错误,而是跨模块的问题:登录视图设置了 session,中间件却因为一个配置参数错误提前把 session 清掉了。模型能不能定位到根因,取决于它有没有同时看到登录视图、中间件和配置这几处代码——单个文件的局部视角几乎不可能解决。
4.2 横向对比数据与分析
跑完四轮,数据如下。成本按我当时使用的模型输入价估算,单位是人民币,汇率按当时牌价折算,不同时间会有浮动,重点看比值而不是绝对值。
| 模式 | 总输入 token | 平均耗时 | 是否定位根因 | 修复验证 | 单次输入成本(约) |
|---|---|---|---|---|---|
| 全量上下文 | 约 10.4 万 | 约 115 秒 | 模糊定位到登录视图 | 未通过,中间件未改到 | 约 7.5 元 |
| 滑动窗口 | 约 0.4 万 | 约 18 秒 | 未定位 | 未通过 | 约 0.3 元 |
| 摘要压缩 | 约 0.6 万 | 约 42 秒 | 部分定位 | 未通过,丢了 Secure 配置 | 约 0.5 元 |
| 检索增强 | 约 0.7 万 | 约 33 秒 | 准确定位 3 个相关文件 | 通过 | 约 0.5 元 |
| 分层混合 | 约 0.9 万 | 约 55 秒 | 准确定位并给出完整修复 | 通过 | 约 0.7 元 |
全量模式花了最贵的成本,却没有一次解决问题,这个结果当时让我挺意外。展开说:它确实"看到了"所有代码,但在 10 万 token 的输入里,中间件里那行关键的错误配置恰好落在中间位置,被模型的注意力忽略了。它给出的修复集中在登录视图,方向偏了。
滑动窗口是最惨的,因为窗口里只有最近几轮对话,没有任何代码内容,模型只能泛泛地回答"建议检查 session 配置",属于完全没进入任务。摘要压缩表现不错,它靠主线摘要锁定了会话流程的大方向,但正如前面讲的,摘要把"清空 session 的中间件在检查用户之前执行"这个关键时序细节丢了,最后方案依然不通过。
检索增强和分层混合都通过了验证。分层混合的路径是:先把项目架构摘要和可疑文件列表放入上下文,模型判定需要细读登录视图和中间件,再调用工具加载这两个文件全文,最终定位到错误配置。这个"先总后分"的过程,正是我想在工程里复现的。
4.3 两个让我意外的结果
第一个意外前面已经说了:全量模式不是最准的。这件事对我冲击挺大,因为我之前的直觉一直是"信息给得越全,模型判断越准"。但长上下文实际会带来注意力稀释:当窗口里 90% 的内容和当前问题无关时,模型反而容易被无关代码带偏。现在我做上下文设计,仔细考虑的不是"放多少信息",而是"如何让关键信息高信噪比地出现在窗口里"。
第二个意外是摘要压缩在细节上的脆弱。摘要模型在压缩时,天然倾向于"保留结论、丢弃限定条件",而这些限定条件恰恰是 Bug 修复里最要命的部分。后来我调整了摘要 Prompt,明确要求"压缩时不得丢弃任何带否定词、条件词和具体参数值的内容",情况好了很多。这个细节我想特别提醒读者,摘要压缩的坑往往不在摘要模型能力,而在你没有告诉它"什么不能丢"。
5. 长上下文里那些看不见的坑:注意力稀释、截断幻觉、召回失败
5.1 "Lost in the middle":中间位置的信息被注意力忽略
我在前面反复提到中间丢失现象,这里展开讲。我自己做过一个小实验:把同样的关键信息分别放在 Prompt 的开头、中间、结尾,让模型回答同一道题,中间位置的准确率明显低于两端。这个现象在不同模型上都会出现,只是程度略有差异。也就是说,面对超长上下文,模型更像在读一份"开头结尾加粗、中间小字排版"的材料,而不是均匀地处理每个 token。
这给上下文工程带来一个可操作的原则:最重要的约定、最关键的约束,一定要放到 System Prompt 或对话的最后一条消息里;需要模型分析的长篇材料,可以把关键结论前置,而不是埋在文档中段等模型自己去找。代码场景里,如果一个 Bug 的根因写在一个项目的第 5 个文件中间位置,你用全量模式喂给模型,很可能就是找不到——不是模型傻,是位置太差。
5.2 截断面引发的幻觉:滑窗模式的隐性失败
滑动窗口的截断有一个特别隐蔽的副作用:模型会脑补被切掉的内容。我遇到过这样一个案例,窗口把某个工具函数的定义截掉了,但函数名还在前面的残留文本里。模型继续工作,凭名字推断了一个"看起来合理"的函数签名,甚至成功编译通过——直到运行时报错,我才发现它生成的调用方式跟真实函数完全对不上。
这个问题的根源在于,大模型训练目标里包含了"补全合理后续"的能力,面对缺口它倾向于生成一个自洽的答案,而不是承认信息缺失。规避手段有两条:一是滑窗的保留区间必须包含"契约区",也就是接口定义、配置常量、全局约定这类被反复引用的内容,不能只按最近对话简单滑动;二是给模型显式的指令,遇到未知标识符时必须报"上下文缺失",而不是自行推断。
5.3 检索模式的召回率陷阱:关键词匹配的边界
检索增强看起来最省钱,但它有一个容易忽略的前置条件:向量检索是按"语义相似度"排序的,而代码问题往往不是语义相似,而是调用关系相关。比如报错发生在登录视图,根因却在中间件的初始化参数上,纯按文本检索,模型拿到的 top_k 里可能全是 login 相关的代码,中间件压根没被召回。
我现在的做法是多路召回加召回后重排:向量检索一路、关键词语法检索一路、代码引用关系再补一路,三路结果合并去重后,再做一次重排,保证跟问题相关的调用链代码能进入上下文。如果你刚接触 RAG,可以先接受"检索可能漏"这个事实,通过调大 top_k 缓解,但更好的方案始终是结合代码结构,把"谁调用了谁"这类关系补进索引。这部分升级比较重,适合在检索质量成为瓶颈时再做。
5.4 token 估算误差:中文、代码与 JSON 结构
最后一个坑不是模型问题,而是工程问题:token 估算不准,成本预算和窗口规划都会偏。我见过有人按字符数算 token,以为 4 万字符等于 4 万 token,结果实际超出窗口一倍,白白失败。不同内容的 token 密度差异很大,中文的压缩率低,代码因为缩进和符号密集,密度也比英文正文高。JSON 这类带大量结构字符的文本,token 密度尤其高,接近每千字符 350-400 token。
实操上我给自己定了一条规矩:任何批量任务上线前,先抽样一批真实输入走一遍 tokenizer 统计,确定本场景的经验密度,再按最大密度上浮 20% 做预算。宁可预算多一点,也不能在关键任务跑到一半时发现窗口溢出。毕竟 context-mode 的优化,前提是永远不要让"装不装得下"这个问题打断思路。
6. 我的选型优先级与个人经验
6.1 按项目类型的上下文模式选型建议
看完上面的对比,你大概能感觉到,没有哪个模式是绝对最优的。我根据自己的实践整理了一张选型表,不一定适用所有人,但可以帮你快速起步。
| 场景 | 我的推荐 | 原因 |
|---|---|---|
| 小型项目、单文件脚本 | 全量上下文 | 信息完整,成本可接受 |
| 大型代码库的 Bug 修复 | 分层混合为主,检索增强为辅 | 先看地图再精读,控制成本又保留全局 |
| 长文档问答、合同审查 | 摘要压缩 + 检索 | 主线靠摘要,细节靠检索,兼顾覆盖和精确 |
| 客服、闲聊系统 | 滑动窗口 + 摘要保底 | 成本最低,定期把旧对话压缩成摘要留档 |
| 对扩展性和成本敏感的知识库问答 | 纯 RAG | 规模可以做到非常大,单次调用便宜 |
6.2 给新手的上手路径:从全量到压缩再到检索
如果你刚开始接触 context-mode,我的建议是别急着上检索那一套,按下面这个顺序逐步演进。
第一步,先在小项目上用全量模式跑通,感受模型在"信息充足"时的表现,建立基线。这里的关键是把自己的任务写清楚,确认模型在没有上下文压力时的能力上限。第二步,给对话接上滑动窗口,观察成本下降的同时,哪些问题开始出现——这能帮你直观理解"丢上下文"的代价。第三步,叠加摘要压缩,先让它保住主线,再在 Prompt 里明确"哪些内容不许丢"。第四步,当你发现全量已经装不下、成本也扛不住时,再上检索增强。每一步都用日志记录前后指标,这样你能明确知道是哪个改动带来了提升。
6.3 最后一点心得
做了这么多上下文模式的配置和调优,我个人最大的体会是:上下文工程本质上是在做系统设计,而不是写 Prompt。你选择哪种模式、参数设多少、信息放在哪个位置,这些决策对结果的影响,往往比换一个更强的模型更明显。给模型一万行代码让它找 Bug,不如先让它看到错误信息、相关文件索引和三个可疑文件;就像让一个资深工程师修线上问题,你不会把整个仓库打印出来扔给他,而是告诉他问题现象、最近的变更和怀疑方向。
最后分享一个我一直沿用的技巧:把项目里最重要的约束写进 System Prompt 常驻,而不是散落在各个代码文件里。比如"登录后必须校验 session 有效期""所有金额以分为单位""评论内容需要转义输出"。这些约定一旦被滑窗丢掉或埋没在长文中间,模型就会开始自由发挥。把它们提炼成常驻约束之后,我在 Agent 类任务里的稳定性提升了非常明显的一截。context-mode 的选择说到底就是一句话:让模型在最省的地方看到最该看的内容,别指望它自己在海量信息里做取舍。