聊一个这几年我在各种大模型应用项目里反复踩坑、又反复优化的核心概念:context-mode。
只要你在做 Agent、做 RAG、做多轮对话、或者用 API 搭任何带"记忆"的东西,你早晚都会撞上同一堵墙——模型能记住的内容是有限的,但真实业务里的信息是无限的。把什么塞进上下文、用什么顺序塞、塞满了怎么办、下一轮对话要不要清掉,这一整套处理逻辑,就是上下文模式。它直接决定了你的应用是"看起来聪明"还是"三句话就失忆",也直接决定了你的 API 账单是"勉强能撑"还是"离月底破产不远"。
这篇文章不聊教科书定义,只聊我在真实项目里怎么设计、怎么选型、怎么带货踩坑,最后怎么把 context-mode 从“一个模糊的参数”落地成“一整套可复用的工程方案”。适合正在做 LLM 应用开发、但又没系统梳理过上下文策略的工程师,也适合那些被 ChatGPT 项目折磨过、想搞清楚为什么自己的产品总是"答非所问"的产品经理。
1. 先搞清楚:Context Mode 到底在解决什么问题
1.1 术语来源:从命令行到智能对话
“context-mode”这个词最早被大量使用,其实是来自 grep、diff 这类命令行工具。grep 查关键词的时候,加上-C 3表示“把命中行前后 3 行也一起显示出来”,这就是最朴素的上下文模式——只给你核心命中的行没有意义,因为脱离了前后文,你根本不知道这段内容在讲什么。
到了大模型时代,context mode 的含义被彻底放大了。一个 LLM 其实就是一台“根据当前输入预测下一个 token”的机器,你说的每一句话、它回答的每一个字,都会被塞进一个有限长度的上下文窗口里。窗口越大,它能“参考”的信息越多,推理越准,但代价是计算量更大、响应更慢、费用更高。
这里就出现了一个核心矛盾:业务场景是无限复杂的,而模型窗口是有限且昂贵的。context-mode 就是用来调和这个矛盾的一套调度策略——它决定哪些信息配得上进入上下文窗口,哪些信息该被压缩,哪些该被遗忘。
1.2 核心矛盾:窗口不是越大越好
很多人刚开始做应用时有一个错觉:上下文窗口越大,效果就越好。于是他们一看到模型支持 128K、200K 上下文,就拼命往里塞文档、塞历史消息、塞各种系统提示词,以为“反正装得下”。
实测下来这个想法会让我很想捶桌子。拿 API 计费来说,绝大多数大模型是按 token 收费的,输入 token 和输出 token 都计费,而输入占大头。你把 80K token 的文档一把梭塞进去,用户问一个“第二页第三段写了什么”,其实只有几百个 token 是有用的,剩下 79K 都是白烧钱。更糟糕的是,上下文过长会导致模型“注意力稀释”,它根本不知道应该重点看哪部分,结果就是越长的窗口反而产生越差的回答质量,这跟人一样,一次让你读 20 万字,你可能连第一篇的中心思想都记不准确。
所以我会在项目里反复强调:与其无限扩大窗口,不如把内容控制在“刚好够用”的范围。这才是设计 context-mode 的真正目的——不是在追求“装得更多”,而是在追求“装得精准”。
2. 四种主流 Context 处理模式:各有利弊,没有银弹
2.1 全量模式:简单粗暴,但成本最高
全量模式就是字面意思:把所有相关信息一次性全部拼进 prompt 里,不做任何筛选。比如做客服助手,就把产品手册、常见问题、用户历史订单全部扔进去,让模型自由发挥。
优点很直接:实现成本极低,所有信息模型都能看到,不存在“因为被过滤掉所以答不上来”的问题。适合那种需要全局信息才能做判断的场景,比如让模型总结一份完整合同的风险点,你总不能只让它看节选。
缺点也致命——成本和延迟都高得离谱。我有一次做内部知识库问答,PDF 转文本后有 14 万 token,全量塞进去,单次请求的输入费用就不是“分”这个量级了,而且首字延迟接近 30 秒,用户直接抱怨“这破机器是拨号上网吗”。所以全量模式的适用面其实非常窄,它只能解决“一次性、文档量可控、时效性没有太高要求”的任务。
2.2 检索模式:把上下文变成按需加载
检索模式是当前 RAG(Retrieval-Augmented Generation)体系的核心。它借鉴了“按需加载”的思想,像图书馆一样,先建好索引,用户提问时再去做检索,只把相关的片段拼进上下文。
我常用的做法是:把文档切块(chunk)、做 embedding 向量化、存到向量数据库里,用户提问时先做语义检索,取出 top-k 个高相关片段,再连同用户问题一起发给模型。这个模式的好处是上下文体积大幅缩水,成本可控,而且因为每次只塞“和问题最相关的内容”,回答针对性和准确率反而更高。
但检索模式也有自己的头痛之处:最怕检索不到。如果你的切块策略不好、向量模型不太行、或者问题里的措辞和文档里的措辞差异太大,召回结果可能就是一堆无关内容。我见过不少团队兴冲冲搭了 RAG,结果用户问“退货流程”模型答非所问,因为文档里写的是“退款相关规定”,语义检索没命中。检索模式不是万能的,它只能解决“库里有、且能搜到”的问题。
2.3 压缩模式:丢细节,保主线
压缩模式也很好理解:既然上下文窗口有限,那就先把原始内容做一道“摘要提炼”,把核心观点、关键数字、结论先抽出来,再塞给模型。
做长文本分析、需要连续几轮讨论复杂文档时,压缩模式的效率优势很突出。比如你有一份 5 万字的行业报告,前两轮对话已经在逐章讨论了,到了第三轮你想让模型结合全文来回答整体判断,每次都全量重塞显然不现实,那么预先压缩出一份 2000 字的“核心要点版”,既能覆盖主线信息,又能让模型站在全局视角上回答。
压缩模式的问题是信息损失不可控。摘要算法再好也难免丢掉细节,尤其是一些具体数字、条件限定语、转折关系,一旦在压缩环节被丢弃,后面模型再聪明也救不回来。所以在那种“必须精确、必须能追根溯源”的场景里,我一般不会单独用压缩模式,而是会把它作为检索或全量模式的“辅助层”——既保留原文入库,又准备一份压缩摘要,让模型根据摘要判断要不要调取原文细节。
2.4 分层滚动模式:让模型自己决定记住什么
这种模式我越来越常用,做法是维护一个“分层记忆池”,把上下文分成几层:系统层(固定指令)、任务层(当前这轮的核心输入)、历史层(过去几轮的关键信息摘要)、候选层(按需检索出来的临时资料)。
每一轮对话结束后,系统会把历史层重新压一遍,旧的 key 信息会滚动成更短的摘要,无关信息会被遗忘。做过 GPT 类聊天产品的朋友应该会很有体感:默认情况下,大多数框架对多轮对话的处理就是“把过去 N 轮消息原样拼回去”,这会导致一个严重问题——上下文窗口很快被历史聊天占满,且早期的内容全是噪声。
我搭会员问答 Bot 时就这样玩过:用户问了一堆不同类目的问题,如果不做分层滚动,第 10 轮时模型可能已经忘了第 1 轮的内容,但如果全留着,窗口又会被无关内容污染。于是我把历史层做成了“分类记忆槽”,每个槽保留最近 3 条相关消息和一条摘要,用户再问到之前的主题时,模型还能从摘要中“回想”起来。这个思路的核心就是:上下文不该是一个被动的队列,而应该是一个有生命周期的记忆系统。
3. 实操落地:为 Agent 应用设计一套 Context 处理体系
3.1 需求拆解:先定义你的上下文里有哪几类内容
在动手写代码之前,我习惯先做一张“上下文物联网关”。拿我最近做的一个合同审查助手来说,在它每一次向模型发请求时,可能进入上下文的候选内容有六类:
- 系统指令:审查规则、输出格式、禁忌事项
- 待审合同原文:可能是几十页 PDF 转出来的长文本
- 用户需求:本次要重点审查“付款条款、违约责任、知识产权归属”
- 历史问答:用户前几轮提到过“我们是乙方,希望多争取账期”
- 参考法条/类似合同条款:需要从知识库检索的外部材料
- 中间推理结果:模型或工具在过程中生成的表格、摘要
这六类内容的优先级、体量、生命周期完全不同。系统指令必须永远保留,而且不能截断;待审合同原文体量巨大,可以根据用户需求按章节“局部加载”;历史问答该做摘要滚动;参考法条则完全依赖检索实时召回。
这一步看着简单,但很多人栽跟头就是没做这个拆解。他们把所有内容一视同仁地拼进 prompt,结果系统指令被一堆历史消息挤出窗口,或者法律条款被原文淹没。我先用表格把清单列出来,后面每一步都好操作得多。
3.2 上下文生命周期的四个阶段
我设计 context-mode 时,会按“收集-筛选-注入-清理”四步来走,下面是我总结的标准流程。
第一个阶段是收集。所有可能进上下文的候选信息先不急着塞进 prompt,而是一股脑收进来,存在一个结构化的容器里。这里用伪代码描述大概是这样的思路:
function collectContext(request) { return { system: loadSystemPrompt(), documents: loadRelatedDocs(request.docIds), history: memoryStore.getRecent(10), userInput: request.message, fetched: vectorSearch(request.message, topK=5) } }第二阶段是筛选。根据本次请求的具体场景,给每类内容做取舍。比如用户只问“第三页的支付条款怎么改”,你硬把整份合同塞给他就是浪费 token;这时候只加载“第三页原文 + 合同整体目录 + 与该条款关联的法条”就够了。筛选时要设定预算,我的经验值是:把单次请求的输入 token 预算分为三档——常规档(全文窗口的 60%,避免撑满)、高配档(全文窗口的 80%,用于复杂长文分析)、经济档(全文窗口的 30%,用于简单问答)。
第三阶段是注入。按“系统指令→用户需求→检索片段→历史摘要→文档局部→结构化中间结果”的优先级顺序排列。排序是有讲究的:模型对 prompt 开头和结尾的内容注意力相对更强(这是业界常说的 anchor effect,虽然并非绝对,但实践里确实明显),把最重要的系统规则放最前面,把本次要处理的具体目标放最后面,让模型带着目标去读中间的材料。
第四阶段是清理。每次响应结束后,不能把整段对话原封不动地存起来。我会把历史层做一次摘要压缩,新摘要合并旧摘要后回写存好,然后把超过 N 轮的原始消息移到冷存储。这样才能保证下一轮请求时,历史层的开销是固定且可控的。
3.3 关键参数怎么定:窗口、检索条数、压缩比例
很多刚入行的朋友会问我:“有没有一个万能参数推荐?”答案是没有,但有实测过的经验参考值,可以作为起点再慢慢调。
- 检索条数(top-k):我一般从 4 开始调。对于大部分问答场景,top-k = 4 条 200~300 token 的片段,大概 800~1200 token,已经能覆盖答对 80% 的问题;只有“多文档综合对比”这种场景我才会上调到 8~10 条。
- 切块大小(chunk size):切得太小块会丢失上下文逻辑,切得太大块又会让检索命中精度下降。我的习惯是 400~600 token 一个 chunk,段落之间保留 10%~15% 的重叠,给每个 chunk 加一行来源路径元数据。
- 压缩摘要的触发阈值:历史决策在 6 轮以内我保留原文,超过 6 轮就开始做滚动摘要,每 3 轮合并一轮。这个数字是纯粹凭业务节奏定的,因为实测 6 轮以内模型对早期细节的“还能回忆起来”的程度还高,超过之后错误率会明显上升。
- 窗口预留比例:我始终不让 prompt 占满整个窗口,至少预留 15%~25% 给模型的输出 token。如果你用的模型窗口是 128K,输出可能占 8K,prompt 里就尽量压在 90K 以内,留白给模型思考和生成的空间,否则经常出现“请求超预算”的错误。
4. 踩坑实录:我在这条路上遇到过的常见问题
4.1 上下文污染:历史残留信息反向干扰
第一次听说“上下文污染”这个词时,我还觉得危言耸听。直到有一次做客服机器人,用户前一句问“你们的退款多久到账”,客服答完“3~5 个工作日”;用户紧接着问“这个能加急吗”,模型居然回答“可以免费加急”,因为模型在历史消息里把“退款”误读成了“所有订单都支持加急”。
这就是典型的上下文污染——历史信息被模型过度泛化,你没有及时清理掉上一轮任务中的临时限定条件。我的解决办法是:对历史层做分区存储,把“用户画像型事实”和“任务型临时状态”分开。任务型状态(比如“用户目前正在咨询退款”)每轮任务结束后就清掉或弱化,只保留画像型事实(比如“用户是会员”)。
4.2 检索命中率低:召回不到关键内容
RAG 失效的最典型现场是:你在知识库里明明存了“A 型号打印机如何换墨盒”的文档,用户问“A 打印机没墨了怎么办”,结果语义检索返回的全是“B 型号打印机清洁喷头”。这种现象叫“表达习惯错位”——文档表述和用户口语之间有语义差距,常规向量检索模型很难跨越。
我的排查有三板斧。第一板斧是先做召回诊断,把用户问题和召回片段打出来看,确认是不是 embedding 本身出了问题。第二板斧是给检索过程加“关键词加权”,也就是在向量语义之外,用 BM25 或者简单的词法检索获得一个混合分数,互补语义盲区。第三板斧是重写查询,先让一个轻量模型把用户问题改写为“更接近文档表述的检索语句”,再去做 embedding。实测下来,第三招提效最明显,但会多花 300~500 token 的开销,属于一种“以 token 换命中”的取舍。
4.3 成本爆炸:明明没多少用户,账单却高得吓人
我在某个项目上线首周就收到过一封让我后颈发凉的账单——单周 API 费用直接顶上一个月的预算。排查后才发现,问题出在产品前端疯狂触发会话续接:用户每打开一次页面就重新发起一次全量上下文请求,且由于我没有做“请求级别的缓存”,同样的历史摘要反复被重新计算。
后来我总结了三个省钱硬规则。第一,相同内容的向量检索结果做短时缓存,TTL 设 5 分钟,防止同一问题反复检索相同片段。第二,历史摘要的更新采用“增量式”,不是每次都全量重算,而是只把新增的几轮对话和上一轮的摘要合并再压缩。第三,把高频启动场景做成“单轮模式”,牺牲一部分记忆连续性,换回成本和响应速度的平衡。
4.4 长上下文的“幻觉放大”问题
上下文越长,模型越容易编造“好像存在但其实不存在”的内容。这里最坑的是,长上下文中某个片段如果只在开头出现过一次,模型在生成时可能会记混,把 A 文档的条款套到 B 文档上。我检查过一条典型的幻觉输出:合同原文里根本没有“违约金为合同总额 20%”这句话,但模型煞有介事地引用说“根据贵方合同第 7.2 条”。结果一查,7.2 条写的是“争议解决方式”。
解决这个问题的思路是“证据要求”:在 prompt 里强制要求模型所有关键信息必须标注来源片段 ID,且规定“如果原文中没有该信息,必须明确说没有,不能自行推断”。这一步可以把幻觉率降低一半以上,强烈建议所有做文档审查类应用的人加上。
5. 进阶方向:从静态模式到自适应的 Context-Aware 体系
5.1 根据用户难度动态调整模式
做到前面这些步骤,你的 context-mode 已经比 80% 的简易集成方案要成熟了。但如果想让产品体验再上一个台阶,可以试试“场景感知式切换”——不用一套模式打通所有问题,而是先判断当前用户的行为特征,再动态选择上下文策略。
比如用我的会员问答 Bot 来举例。新用户上来问“你们平台怎么收费”,这种问题不需要任何历史记忆,我会直接走“经济档”,把系统指令压缩到最短,完全不做检索。但如果是老用户已经连续问了七八个问题,且明显在围绕某一个产品做深度调研,我会自动切换到“高配档”,开启历史摘要、文档局部加载和检索式上下文的完整组合。
这种动态切换的本质,是把 context-mode 从一个“静态配置项”变成一个“随场景调度的策略集合”。实现上其实不复杂,就是加一个输入分类器(可以是 100 行代码规则,也可以是轻量模型做意图判断),在请求进入系统之前把当前会话的“复杂度标签”打出来,再映射到对应策略。
5.2 让上下文跨会话"长期驻留"
很多产品除了单次会话内的上下文管理,还需要跨会话记住用户的长期偏好。这就涉及到“长期记忆层”的设计。我通常做法是:为每个用户维护独立的记忆槽位,包括事实槽(用户的公司规模、行业、偏好)、关系槽(用户与某个项目/文档的关系)、事件槽(用户过去做过哪些操作)。
每次会话结束时,我会把这次会话中抽取出来的“结构化事实”写入记忆槽,但不把原始聊天记录全量存着。下次用户再登录,系统先加载记忆槽里的精简摘要,再叠加本次会话的上下文,这样用户会觉得这个产品“居然记得我是谁,记得我上次聊到一半的事”。这其实就是很多 AI 应用正在追求的“personal memory”能力,而它的底层,仍然是 context-mode 的合理组合。
我踩过不少弯路的总结是:context-mode 不是一个“打开就有效”的开关,而是一套需要好好设计的工程体系。从我最开始全量塞 prompt 的粗打法,到后来检索、压缩、分层滚动、动态切换的组合方案,每一步都是被账单、延迟和答非所问逼出来的。如果你现在刚开始设计自己的 AI 应用,我的建议是:先别急着上最复杂的方案,从全量模式做起,用真实的数据去观察你的应用到底在哪里“失忆”,再按这篇里的思路一步步加上检索、压缩和记忆层。等你跑通了,你大概率会和我一样,觉得 context-mode 才是决定产品“智商”的那层隐形地基。