做AI应用做得久了,你会发现一件特别有意思的事:同一个模型,同一个提示词模板,别人跑出来的效果像定制了专属助手,你跑出来的却像在跟一个记忆力只有三秒的客服对话。差别大多不在模型本身,而在 context-mode——上下文模式。
这里的 context-mode,不是一个具体产品,而是你组织、管理和利用模型"能看见的信息范围"的方式。它直接决定了模型在手头有多少依据、这些依据按什么顺序排列、关键信息会不会被淹没。说实话,我见过太多"这个模型不行"的结论,最后排查下来,八成是上下文没用好,是方案设计的问题。
这篇文章我会结合自己在真实项目里的实操经验,把 context-mode 涉及的窗口选型、内容组装、动态压缩、检索增强、常见坑位一次讲透。无论你是刚接触大模型应用开发,还是已经在做 RAG、Agent 类的工程落地,这篇文章应该都能给你一些可以直接抄作业的内容。
1. context-mode 到底在控制什么
1.1 大模型的"桌面工作"机制
先讲认知。大模型本身没有记忆,模型权重里面存的是训练时学到的知识,但模型权重不会因为你和它聊了几句就改变。每次调用,模型看到的输入就只是你的 prompt,输出完之后,这次会话的信息并不会自动留在模型里。想要模型"记住"任何事情,都得通过上下文来完成。
我特别喜欢用一个类比:把模型当成一个特别聪明但记性极差的临时助手。你给它布置任务的时候,必须把所有资料都打印出来放在桌上——客户背景、历史对话、产品说明、注意事项,一样都不能少。它干活的时候只看得到桌上的这些纸,桌上的纸越多、越乱,它就越容易找错重点。context-mode 就是这个"桌面的布置策略"。
所以你会明白,为什么同样是那个模型,有人用来做客服机器人效果好,有人做出来却老是答非所问。很多时候不是模型笨,而是你没有往桌上放对纸。
1.2 上下文模式的三种形态
我平时会把 context-mode 拆成三个维度来看,避免一上来就陷进细节。
第一种是上下文窗口(Context Window),也就是模型的"桌面容量",一次最多能放多少 token。这是硬指标,直接决定你能不能把一段长文档完整放进去。
第二种是上下文学习(In-Context Learning),指的是模型从上下文里的示例中提炼规律、完成任务的能力。你不用微调模型,只要在 prompt 里塞两三个例子,模型就能照着干,这就是 ICL 的威力。
第三种是我自己最看重的,上下文管理(Context Engineering),也就是怎么去构建、排序、压缩、检索这些上下文。它属于工程实践,比前两个维度更考验设计功夫,也是我们大多数应用开发者真正能发力的地方。
| 维度 | 核心问题 | 谁来关心 | 典型参数/手段 |
|---|---|---|---|
| 上下文窗口 | 能放多少 token | 模型选型阶段 | 8K / 32K / 128K |
| 上下文学习 | 模型能否从示例学会任务 | prompt 设计 | few-shot 示例数量 |
| 上下文管理 | 如何让有用信息不被忽略 | 系统架构 | 检索、压缩、重排 |
2. 上下文窗口的真相:参数表上的数字别全信
2.1 名义窗口和有效记忆的差距
第1节讲了三种形态,现在先从窗口这个硬指标说起。很多人选模型的时候只看参数表,看到128K就觉得很安心。但到了实际项目里你会发现,128K窗口不代表模型能有效利用128K。
这里有个在业内已经被反复验证的现象:当上下文很长时,模型对最开头和最结尾的内容利用得最好,对中间部分的内容记忆最差。我自己做一个长文档问答项目的时候,把一份30万字的技术手册整体塞进去,问它中间章节的一个参数,它愣是答串到了另一章节,而且发生得相当稳定。后来查了不少研究资料,发现这个"中间丢失"现象不是偶发问题,而是 Transformer 结构下的普遍倾向。
为什么会出现这种情况?往浅了说,注意力机制计算的是每个 token 和其他所有 token 之间的关联权重,上下文越长,注意力权重被摊得越薄,中段内容的信号就被周围环境稀释了。加上很多模型用的位置编码带有距离衰减特性,相对较远的 token 之间相关性会被进一步削弱。所以中间内容天然就不占优势。
再加上一个更现实的问题:很多支持所谓"长窗口"的模型,是通过位置编码插值(比如把训练时的4096窗口外推到128K)来实现的。这种外推不是没有代价,位置信息相对模糊,有效记忆长度会打折扣。参数表上的"128K",和它真正能保证质量的"有效上下文"完全是两个概念。
2.2 长窗口不是万能的
上面讲的还是记忆能力的问题,接下来要说的这个坑更隐蔽:即便模型真的记住了所有细节,给了它太多不相关的内容,它也容易被噪声带偏。
我还是拿客服知识库项目举例。第一版图省事,把500多页的产品手册全量塞进上下文,让模型自己找答案。结果它经常把两种不同型号的参数混在一起说,还会从手册的偏僻角落挖出一句过时的话当作标准答案。后来改成只从手册里检索出和用户问题最相关的3到5个片段,再塞进上下文,准确率直接上了一个大台阶。这说明什么?长窗口是"被动容量",不是"主动智能"。你不能指望塞进去的垃圾会被模型自动滤掉。
所以在方案设计上,我的原则是:不要为了用长窗口而用长窗口。RAG、上下文压缩这些手段,本质上是在帮你往桌上放更有用的纸,而不是把整间仓库都搬上桌。
2.3 窗口选型的实操建议
根据场景选择窗口长度,我一般按下面的思路来:
| 应用场景 | 推荐窗口 | 选型理由 |
|---|---|---|
| 日常对话、意图识别 | 8K ~ 32K | 对话轮次有限,历史信息少 |
| 通用知识问答 | 32K | 兼顾成本和上下文余量 |
| 文档问答(RAG) | 32K ~ 128K | 检索片段 + 历史会话 + 指令留余量 |
| 代码仓库分析 | 128K+ | 一个项目可能有多个文件同时需要上下文 |
| 长文档全文摘要 | 128K+ | 需要在同一窗口内覆盖全文 |
这里有一个细节:选择窗口时不要只看"够不够放资料",要给输出留空间,还要给对话历史留余量。比如你算出来参考资料2000 token,随着对话进行历史记录还会增长,那就得按照一个更宽裕的量级来选型,而不是刚好卡在某个临界值。在长对话过程中,上下文会动态膨胀,窗口吃满之后就必须触发压缩或者裁剪,处理不当会严重影响体验。
3. 上下文内容组装:策略比模型更决定效果
3.1 把重要的信息放到开头和结尾
窗口理解了,接下来才是最体现功力的一块:上下文内容怎么排。
咱们前面说了"中间丢失"这个现象,那策略就很明确了:把最需要模型严格遵循的内容放在开头,把当前要处理的目标问题放在结尾,参考资料和对话历史放在中间。为什么这么放?因为模型在生成的时候,注意力天然更偏向开头(设定角色与任务基调)和结尾(最近的指令信号)。
很多 prompt 工程师总结过一个通用布局:
- 第一层:系统提示词,定义角色、任务范围、输出格式、禁止事项
- 第二层:few-shot 示例,让模型理解"这个任务要这么干"
- 第三层:参考资料,检索到的文档片段或知识库内容
- 第四层:当前用户输入,以及附带的相关约束
拿客服机器人来举例。系统提示词里写"你是某产品的客服助手,只能基于提供的产品资料回答,不要编造价格",用户提问放在最后面,中间是检索到的产品片段。这就是最基础但很稳妥的上下文布局。
有个小技巧是,对特别关键的限制(比如"如果资料里没有答案,就明确说不知道"),可以在开头和结尾各写一遍。模型对重复出现的指令会更敏感,这样能有效降低幻觉概率。我自己实测过,双写关键约束比只写在系统提示词里,幻觉率能低一半左右。
3.2 few-shot 示例的设计要点
前面提到上下文学习(ICL),这块实操起来有个容易犯的错:示例贪多、贪全。不少人觉得例子越多越好,于是把每个类别都写一个示例,结果示例本身占了大量 token,挤占了真正需要的参考资料空间,效果还未必提升。
我的经验是,few-shot 示例不必多,2到5个高质量示例通常就够。关键是示例要贴近真实查询分布。比如做意图分类,先看看用户最常问的是哪些类型,把高频类型的正反例做出来,而不是平均分配十几个类型。正反例配合效果最好——一个正确的示例,一个"看似相近但实际是另一种意图"的反例,模型能更快理解边界。
还有一个细节:示例的格式要和最终任务一致。如果你希望模型输出 JSON,那示例里就必须有 JSON 输出;如果你希望模型在不确定时说"不知道",示例里也要出现这种输出。模型会模仿示例的格式和风格,这是一个常常被忽略但影响很大的点。
3.3 上下文压缩:窗口快满时的保命手段
长对话中,上下文迟早会膨胀到窗口上限。这时候就得动刀了,压缩策略我常用以下几种:
- 对话历史摘要。把早期对话压缩成一句到两句摘要,只保留关键信息,比如"用户之前询问过某产品的退货政策,已告知需要15天审核期"。
- 关键信息抽取。从历史中提取结构化标签,比如用户ID、产品型号、订单状态,用结构化的方式存到上下文里。
- 滑动窗口裁剪。只保留最近 N 轮对话。对大多数对话型应用,用户的当前意图和最近几轮强相关,更早的内容大概率已经不影响最终回答。
- 检索排序。在长文档场景,先做一次检索,只把得分最高的片段放进上下文。
这里要注意一个权衡:摘要压缩会丢失细节,滑动窗口会丢失早期信息。什么时候用哪种?我的经验是:如果历史中涉及事实型信息(订单号、地址、合同条款),必须用关键信息抽取,而不是只靠摘要——摘要一模糊,后续所有回答都会跟着模糊。如果只是普通闲聊,滑动窗口就够了,简单高效。
3.4 别忘了提示词缓存
最后提一个工程上直接省钱省时间的手段:提示词缓存。现在不少模型服务都支持对输入前缀做缓存,重复的前缀 token 不会再走一遍完整计算,延迟和成本都能明显下降。
使用上有两个要点。第一,把上下文里固定不变的部分(系统提示词、工具定义、few-shot 示例)放在最前面,动态部分(用户问题、检索结果)放在后面。因为缓存通常按前缀匹配,只有前缀一致才命中。第二,尽量保持前缀内容稳定,哪怕系统提示词里加一个空格,前缀变了,缓存就废了。我有一次在调试时给系统提示词加了一句日志说明,结果整天的调用都打不中缓存,成本直接翻了几倍,这个教训印象很深。
4. 实战:一个 RAG 问答系统的 context-mode 设计
4.1 场景与第一版问题
前面理论说了一大堆,现在看一个真实案例,把上下文管理的思路串一遍。
需求很简单:做一个企业内部文档问答机器人,知识库大概2000份技术文档,用户会问"某产品支持单点登录吗""某接口的限流参数是什么"这类问题。第一版我们图省事,把所有文档全量塞进上下文,让模型自己找。结果:
- 回答经常张冠李戴,A产品的功能安到B产品上
- 长文档被截断,后半部分内容永远"看不到"
- 单次调用耗时很长,成本也很高
后来改成 RAG 增强加精细化上下文组装,问题基本都解决了。
4.2 整体流程与上下文模板
改造后的流程分离线在线两条线。
离线阶段:把文档按照一定策略切块(我习惯按章节和语义切,每块大约300到500 token),然后做向量化,存进向量数据库。
在线阶段:用户问题进来之后,先用向量检索找出和问题最相关的 TopK 块,然后组装上下文,最后调用模型生成答案。
组装上下文的伪代码大概长这样,用 Python 示意:
def build_context(query, retrieved_chunks): system_prompt = """你是一名企业内部技术文档助手,请基于提供的资料准确回答用户问题。如果资料中没有相关信息,请明确说明"资料中未找到",不要编造。""" # 固定部分放前面,便于命中提示词缓存 context = system_prompt # 中间放检索片段 context += "\n\n【参考资料】\n" for i, chunk in enumerate(retrieved_chunks, 1): context += f"[片段{i}]\n{chunk}\n" # 用户问题放最后 context += f"\n【用户问题】\n{query}\n" return context注意这里的顺序:系统提示词在最前,参考资料在中间,用户问题在最后。这就是利用了"开头结尾注意力更强"的规律。系统提示词负责定基调和约束,用户问题作为最后的强指令,检索片段作为支持材料放在中间。
4.3 参数选择与计算过程
接下来是参数设计。我按下面的思路估算上下文大小:
假设每块文档平均长度400 token,检索 TopK 取5,那么参考资料大约2000 token。系统提示词约300 token,用户问题平均约100 token。单轮完整上下文约2400 token,选 32K 窗口完全够用,还能给对话历史留出很大余量。
那 TopK 为什么不取10或者更大呢?一开始我也试过 TopK=10,结果准确率没有明显提升,反而出现了一些无关片段混进来,干扰模型判断。后来做了个简单实验:在验证集上分别测 TopK=3、5、10 的效果,TopK=5 性价比最高,3 略信息不足,10 噪声偏多。这个结果在不同知识库上可能不一样,但方法是一样的,先小样本测再定参,不要拍脑袋。
4.4 检索质量对上下文效果的放大效应
这里要特别强调一点:上下文组装得再好,如果检索出来的片段本身不对,结果照样拉胯。RAG 里"检索"和"生成"是一荣俱荣一损俱损的关系。
我记得有一次线上反馈某个内网工具不会用,用户问"怎么导出报表",检索出来的片段却是"报表导入",一字之差,答案完全跑偏。后来在检索环节加了查询改写:先让模型判断用户问题的关键实体和意图,再拿改写后的查询去检索。比如"怎么导出报表"改写为"报表导出 操作步骤",命中率明显提升。这就是上下文工程里常说的"好的输入才有好的输出"。
另外,文档切块策略也直接影响检索质量。切得太碎,片段缺乏上下文,模型看不懂;切得太大,片段里混着大量无关内容,检索精度下降。我一般按文档结构(章节、标题)来切,而不是死板地按字数切,这样每个片段在语义上更完整。
5. 常见问题与排查技巧实录
5.1 答非所问:先查上下文是不是被截断了
很多团队把"模型回答不对"归结为模型能力问题,我建议先排查上下文。最常见的坑就是上下文超出窗口,被服务端静默截断。截断之后,用户的问题可能在最末尾被砍掉,模型根本不知道用户问了什么,只能根据中间残留的内容猜,当然答非所问。
排查方法很简单:把发送给模型的输入打印出来,检查最后的内容是不是完整的。如果被截断了,要么缩短参考资料长度,要么调整压缩策略,要么换更大的窗口,而不是去改提示词。
5.2 指令被"淹没"了怎么办
另一个高频问题:模型突然不遵守系统提示词里的约束,比如让它"不要编造",它还是编造。这种情况通常是因为上下文过长,约束指令被其他内容稀释了。
解决思路有三条。第一,关键约束在开头和结尾各写一遍,前面提过这个技巧,实测有效。第二,减少上下文里无关 token 的数量,给指令腾出注意力空间。第三,如果约束特别重要,可以把它拆成独立判断步骤:先让模型判断"资料中有没有答案",再让模型生成回答,而不是一口气让它"既要又要"。
5.3 上下文越长模型越"笨"
还有一个反直觉的现象:上下文越多,模型反而表现得越差。这其实是信息噪声带来的干扰。很多人迷信"信息越全越好",实际上模型并不是一个完美的信息筛选器,塞进去的无关信息越多,它被带偏的概率越大。
我的处理原则是"最小化上下文":只给模型完成任务必要的信息,多一句废话都不放。这句话说起来简单,做起来需要你在每次构建上下文时都问自己一句——这段内容真的需要放进去吗?如果拿不准,就先不放,跑一遍看看效果再说。
5.4 速查表
整理一份排查速查表,方便遇到问题时快速定位:
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 答非所问 | 上下文被截断、问题在末尾被裁掉 | 检查输入长度,缩短内容或换大窗口 |
| 张冠李戴 | 检索片段不相关或 TopK 过大 | 增加查询改写,调小 TopK,优化切块 |
| 不遵守约束 | 指令被长上下文稀释 | 首尾双写关键约束,减少无关 token |
| 越给资料越错 | 噪声干扰、信息冗余 | 最小化上下文,只保留必要片段 |
| 回答风格不一致 | 示例格式与目标不一致 | 调整 few-shot 示例,确保格式统一 |
我个人在实际操作中的体会是,玩好 context-mode 最大的门槛不是理解"上下文窗口"这个概念,而是养成一个习惯:每次往 prompt 里塞内容之前,都先问自己,这段对模型完成任务到底是加分的还是减分的。多数时候你会惊讶地发现,长长的 prompt 里有一半内容是可以删掉的。
最后再分享一个小技巧:准备一个覆盖常见场景的测试集,日常跑着,每次改动上下文结构,不靠感觉判断效果,而是用测试集量化对比。我在项目里就是用三十条覆盖常见场景的测试问题做回归,任何上下文策略的调整都能立刻看出有没有收益。上下文这个东西,看起来玄学,其实你把变量控制住了,它就会变得很可控。