☰
大模型上下文模式实战指南:从注意力分配到RAG与压缩策略
2026/10/8 13:58:49 网站建设 项目流程

1. 为什么我开始认真研究 Context Mode

做了一段时间大模型应用开发的人,大概率都经历过这种困惑:模型明明是最新的,提示词写得也不短,知识库也接了,可回答就是时好时坏,同一个问题换个说法效果完全不一样。有一阵子我被这种不确定性搞得非常头大,后来把所有请求的完整输入日志都抓出来逐字比对,才意识到问题根本不在模型参数调得对不对,而在我们"喂给模型的那一坨上下文"的组织方式上。

所谓 Context Mode(上下文模式),说白了就是:在调用大模型之前,你决定哪些内容进入上下文、以什么结构组织、按什么顺序排列、塞多少量、要不要压缩、要不要临时检索补充。这一整套取舍策略,就是上下文模式。它直接决定模型的注意力分布,进而决定输出质量、稳定性和单次调用的成本。为什么同一个模型,有人调出花,有人调出一坨?差别往往就藏在这里。

这篇文章算是我过去几个项目里的阶段性沉淀,涉及企业知识库问答、客服助手、长文档分析这几类真实场景。内容偏工程实践,会讲原理但绝不掉书袋,会给你可以直接拿去改的代码骨架,也会把我踩过的坑原原本本摆出来。适合正在做 RAG、Agent、多轮对话记忆这类功能的开发者阅读。产品经理要是能耐心看完,也会对"为什么研发那边说效果上不去"多一层理解。

2. 先把瓶颈讲透:为什么上下文本身会成为问题

2.1 模型的注意力是有限预算,不是无限硬盘

Transformer 架构里的注意力机制理论上能处理任意长度的序列,但工程上模型都设了上下文窗口上限,常见的有 8k、32k、128k。窗口之内也不是"雨露均沾"。我自己做过一个很简单的测试:把一份百页文档拆成若干段落,随机挖掉几段,再问模型某一段内容讲的是什么,结果有非常明显的规律——放在最开头和最结尾的内容,模型记得更牢靠;藏在中间位置的段落,即使总长度远没到窗口上限,也经常被忽略或者答错。

这个现象在论文里被称为 Lost in the Middle,学术圈反复验证过。它说明一个道理:模型对长上下文的有效利用不是线性的,存在明显的位置偏差。所以别把上下文窗口当成一块可以随便填充的硬盘,它更像一笔"注意力预算",预算总量有限,而且花在开头和结尾最划算。你在有限预算里怎么分配,直接决定模型的输出质量。

2.2 Context Mode 解决的是注意力分配策略

理解了上面这一点,你就能明白"上下文模式"这个概念为什么会在圈子里迅速热起来。它真正要回答的是几个非常具体的问题:

  • 哪些信息值得进入上下文?
  • 它们以什么顺序和格式放置?
  • 如果内容太多,先压缩谁、保留谁?
  • 用户当前这句话,到底需要哪些背景?

打个比方:你准备一个演示文稿,不会把公司所有资料堆成一百页幻灯片,而是挑三页最相关的数据、一页客户痛点、一页方案描述。Context Mode 干的就是"挑三页"这件事,只不过对象换成了喂给大模型的信息。现在有个趋势叫上下文工程(Context Engineering),某种意义上比提示工程更值得投入精力。提示工程优化的是"怎么说",上下文工程优化的是"给它什么",两者叠加才是一个完整的大模型调用策略。

注意:不要只盯着提示词模板抄来抄去。提示词写得再花哨,上下文里塞满了无关噪音,效果照样拉胯。先解决"喂什么",再谈"怎么喂"。

2.3 一个让我印象深刻的对照组

为了说服团队,我做过一组很朴素的对照实验。同样一个问题:"我们上个季度的老客户复购率是多少?"用同一个知识库、同一个模型,唯一区别就是上下文组织方式。

  • 方式A:把整份经营分析报告(约3万字)直接拼进提示词,让模型自己找答案;
  • 方式B:先从知识库里检索出10段相关内容,按相关度排序后注入上下文,并明确标注"以下是检索到的资料片段"。

结果差异非常明显。方式B的回答准确率从62%跃升到91%,生成时间缩短了近30%,单次调用成本也从六千多token降到一千八左右。没有加任何神秘提示词,纯粹是"给什么、怎么给"的差别。这就是上下文模式最直白的价值证明。

3. 我在项目里沉淀出来的几种 Context Mode

下面按"原理-适用场景-关键参数-优缺点"的顺序,把我在生产环境里真正用过的几种模式讲清楚。它们之间不是互斥关系,很多场景需要组合着用。

3.1 全量直连模式(Full-context Direct Mode)

这是最直接的一种:把系统指令、对话历史、用户问题原样拼接,尽量把所有相关信息一次给足。

原理上,模型看到的是完整未被裁剪的上下文,信息损失最少。它适合单轮问答、文档总量不大、原型验证阶段,以及需要快速响应的任务——因为少了检索和拼装的额外延迟。

但缺点同样突出。第一,Token 成本随对话轮数线性增长,多轮对话很快就撞到窗口上限。第二,无关信息太多会稀释注意力,模型反倒抓不住重点。第三,延迟随输入长度增加而明显变慢。

我在早期做客服机器人时用过这种模式,结果发现聊到第八、九轮之后,系统提示词里强调的"语气规范"开始逐渐失效。历史消息占据了绝大多数篇幅,模型早期收到的指令被活生生"挤"出了注意力范围,这让我第一次直观感受到上下文管理不是可有可无的优化,而是必需品。

关键参数:

  • max_tokens:输出长度上限,注意别和上下文窗口混为一谈;
  • truncation_strategy:超出窗口时的截断策略。常见有"从头截断"和"从中间截断",我实测后者效果通常更好,因为开头往往是系统指令,不能丢。

3.2 检索增强模式(RAG-Based Context Mode)

这是目前企业知识库场景里最主流的一种。核心思路:不让模型记住所有知识,而是在每次调用前,从外部知识库检索出与当前问题最相关的若干片段,再组合成上下文。

完整链路一般包括:查询改写、向量检索、重排序、上下文拼装。每一步都有讲究。

以我常用的实现为例,流程是这样的:

  1. 用户输入问题;
  2. 对问题做轻度改写或扩展,比如把"上季度"规范到具体日期范围,或者补全缩写;
  3. 用同一套 Embedding 模型把问题向量化;
  4. 在向量数据库里执行相似度检索,取回 top_k=6 的候选片段;
  5. 在内存里对候选片段和原始问题再做一次相关性重排(Rerank),由轻量级模型打分排序,取 top_n=3 进入最终上下文。

为什么要重排?因为向量检索的相似度不等于语义相关度。很多时候排第一的片段看着像、实际答非所问,真正需要的答案落在第五、第六位。重排这一步能把命中质量明显拉上来。

上下文拼装也有讲究。我会把检索结果放进一个明确的区块,用"资料片段"标签包裹,并在每个片段前面标注来源文件名和页码。别小看这个动作。对模型来说,来源可追溯是一种强提示,能帮它区分"这是资料里的信息"和"这是你现有知识"。

关键参数(基于我的实测经验):

  • chunk_size:文档切块大小,经验值500-800字符,具体看文本语言和结构;
  • chunk_overlap:相邻块重叠,建议50-100字符,避免把一句话从中间切断;
  • embedding_top_k:召回候选数,6-10比较平衡,太少漏召回,太多增加重排负担;
  • rerank_top_n:最终注入数,3-5为宜,再多边际收益很低,成本却实打实上涨。

这种模式的优点是对知识库实时更新友好、Token可控、答案可溯源。缺点是多了检索链路,存在"检索不到就答错"的额外风险。这个我在第5节会专门讲排查方法。

3.3 折叠压缩模式(Compaction / Summary Mode)

对话一旦拉长,历史消息会迅速撑爆窗口。折叠压缩模式的思路是:保留一份"精简记忆",把早期原始对话提炼成摘要,而不是一字不差地全带着。

典型的结构是三层记忆:

  • 短时记忆:保留最近 N 轮原始消息(我常用6轮),保证当前话题连贯;
  • 长期记忆:更早的对话被压缩成若干条事件化摘要,比如"用户在某日询问退货政策,我们答复支持7天无理由";
  • 全局记忆:用户画像、偏好、长期目标,单独维护,不参与对话历史压缩。

我在实现时给折叠压缩加了几个关键参数:

  • compact_threshold:触发压缩的阈值,比如对话超过16轮,或者历史token超过2000,就执行一次压缩;
  • summary_keep_topics:摘要中必须保留的主题列表,比如"用户过敏信息""订单编号""预算上限",防止压缩把关键事实丢掉;
  • compact_debounce:防抖时间,比如10分钟内只压缩一次,避免每轮触发压缩导致额外成本。

压缩摘要看似简单,实际很考验质量。我的一个教训是:不要让摘要生成过程吃掉过多预算。如果历史累计5000 token,压缩成600 token,省了很多,但如果每一轮都重新生成摘要,成本反而更高。所以触发阈值和"摘要+保留最近原文"的三明治结构要一起设计,不要只盯着单轮省了多少。

3.4 结构化分区模式(Structured Context Mode)

前面几种模式主要解决"选什么、省多少",这一种解决"怎么摆"的问题。结构化分区模式要求把不同类型的信息放进上下文的不同区块,每个区块用明确标签标记。

我在实践中常用的区块划分:

  1. 系统指令区:身份、语气、任务边界、输出格式要求;
  2. 事实数据区:知识库检索结果、业务规则、最新数据,只陈述事实不做过度解释;
  3. 工具能力区:当前可用的工具、每个工具的用途、调用约束;
  4. 对话历史区:按时间顺序排列的最近交互记录;
  5. 当前任务区:用户最新问题、待解决问题、明确目标。

为什么要分区?因为模型在冗长上下文里区分"身份设定"和"实时信息"是有额外认知成本的。如果你把所有内容揉成一团,它可能把知识库里某段话当成系统指令来"遵守",或者把用户闲聊当成事实背景来采信。分区之后,相当于给模型画了一张地图:哪块是路标、哪块是路面、哪块是终点,一目了然。

我的实测结果是:同一套 RAG 数据,从"全量拼接+检索结果堆在末尾"改成"结构化分区+检索结果放事实数据区",回答准确率提升约7个百分点,输出格式违规率几乎降到了0。这个提升没有引入任何新模型,纯粹是布局变化带来的。

关键点有两个:

  • 区块标签建议用 XML 标签或者 Markdown 标题,模型对这两种格式的理解都比较好;
  • 区块顺序不要乱动,系统指令在最前,当前任务在最后。模型对最前和最后的内容记忆最强,把最重要的两件事放在这两个位置,是成本最低的注意力分配手段。

3.5 动态混合模式(Adaptive Hybrid Mode)

这是我最推荐在生产环境落地的模式。说白了就是前面几种模式的组合拳,由一个轻量路由模块根据当前请求特点,动态决定走哪条上下文策略。

我的实现思路分三步:

  • 第一步:意图识别。用一个小模型或者规则引擎判断请求类型,比如"闲聊/追问""事实查询""文档分析""需要调用工具";
  • 第二步:策略选择。事实查询走检索增强模式;多轮追问走短时记忆加折叠压缩;文档分析走全量直连或分区模式;工具调用则在上下文里补充工具能力区;
  • 第三步:降级链。如果检索结果相关性普遍低于阈值,自动丢弃检索内容,退化为纯对话模式,并在回答里告诉用户"资料库未找到直接答案"。

动态混合模式看起来复杂,收益却很实在:它把每次调用的上下文控制在最经济范围,既保证效果,又控制成本和延迟。

我遇到过一个很有意思的情况:用户问"你还在吗",如果系统仍然触发向量检索、等待数据库返回结果,就会白白多花三五百毫秒。加了路由之后,这类闲聊直接走纯对话模式,快且省钱。别小看这种优化,生产环境里日调用量上了几十万次,省下的费用相当可观。

4. 实操:搭一套可切换的 Context Mode 框架

理论讲了不少,下面落地。我给出一个基于 Python 的简化示例,把几种模式抽象成统一接口,方便自由切换和组合。这不是唯一写法,但结构清晰,适合起步。

4.1 统一上下文构建接口

首先定义一个基础抽象类,让所有模式实现同一个接口:

from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Optional, List @dataclass class ContextInput: query: str history: list system_prompt: str user_profile: Optional[dict] = None @dataclass class BuildResult: messages: list meta: dict # 记录模式名、token数、检索命中数等,方便观测 class BaseContextBuilder(ABC): mode_name: str = "base" @abstractmethod def build(self, inp: ContextInput) -> BuildResult: """把各类输入组装成大模型调用所需的messages结构""" pass

这里把返回结果设计成 messages + meta 两个部分。messages 直接给模型调用,meta 专门记录本次构建的过程信息。我强烈建议从一开始就保留 meta,后面排查问题全靠它。

4.2 实现检索增强模式

检索增强模式的实现,核心是调用检索器、重排、拼装三个环节:

class RAGContextBuilder(BaseContextBuilder): mode_name = "rag" def __init__(self, retriever, reranker=None, top_k=6, top_n=3): self.retriever = retriever self.reranker = reranker self.top_k = top_k self.top_n = top_n def build(self, inp: ContextInput) -> BuildResult: query = inp.query # 1. 召回候选 candidates = self.retriever.retrieve(query, top_k=self.top_k) # 2. 重排(如果有) if self.reranker: candidates = self.reranker.rerank(query, candidates) candidates = candidates[:self.top_n] # 3. 拼装上下文,明确分区 fact_block = "\n".join( f"[来源:{c.meta.get('source')},页码:{c.meta.get('page')}]\n{c.content}" for c in candidates ) messages = [ {"role": "system", "content": inp.system_prompt}, {"role": "system", "content": f"以下是相关资料片段:\n{fact_block}"}, ] messages.extend(inp.history[-8:]) messages.append({"role": "user", "content": inp.query}) return BuildResult(messages=messages, meta={ "mode": self.mode_name, "retrieved": len(candidates), "token_estimate": estimate_tokens(messages), })

代码里有两个容易出错的点,我专门提醒:

  • 检索片段不要无止境地塞。top_n 超过5之后,边际收益下降很明显,成本却在涨;
  • 历史轮数控制在8轮以内,超出部分交给折叠压缩模式管。否则对话一长,历史反客为主,把检索结果挤出了注意力范围。

4.3 实现折叠压缩模式

折叠压缩的逻辑:先判断是否需要压缩,需要就调用一次摘要模型生成历史摘要,再把摘要和最近 N 轮原始消息一起保留。

class CompactContextBuilder(BaseContextBuilder): mode_name = "compact" def __init__(self, llm, max_history_rounds=8, compact_threshold=2000, debounce_seconds=600): self.llm = llm self.max_history_rounds = max_history_rounds self.compact_threshold = compact_threshold self.debounce_seconds = debounce_seconds def build(self, inp: ContextInput) -> BuildResult: history = inp.history recent = history[-self.max_history_rounds * 2:] older = history[:-self.max_history_rounds * 2] if len(history) > self.max_history_rounds * 2 else [] # 检查是否需要压缩摘要 if older and estimate_tokens(older) > self.compact_threshold: summary = self._summarize(older) # 用LLM生成精简摘要 else: summary = older # 未达到阈值就原样保留 messages = [{"role": "system", "content": inp.system_prompt}] if summary: messages.append({"role": "system", "content": f"早期对话摘要:\n{summary}"}) for item in recent: messages.append(item) messages.append({"role": "user", "content": inp.query}) return BuildResult(messages=messages, meta={"mode": self.mode_name})

注意我用older和recent区分"老家底"和"近期对话",避免每轮都对全部历史重复做摘要。摘要生成建议用便宜且快的小模型,甚至可以异步执行,不让用户等摘要生成完才收到回复。工程上要优先保证响应速度,用异步更新摘要来换取体验,这是非常实际的一个妥协。

4.4 路由模块实现动态混合模式

动态混合模式的代码并不复杂,关键是策略表:

class AdaptiveContextBuilder(BaseContextBuilder): mode_name = "adaptive" def __init__(self, routers: dict): # routers: {"chat": builder_a, "rag": builder_b, "tool": builder_c} self.routers = routers def build(self, inp: ContextInput) -> BuildResult: intent = detect_intent(inp.query, inp.history) builder = self.routers.get(intent, self.routers["chat"]) return builder.build(inp)

detect_intent可以用规则,也可以是一个更小、更快的模型。我建议先用规则跑通,再慢慢往模型迁移。规则的好处是行为可预期、出问题容易定位;模型的好处是能覆盖更复杂的表达。不要一上来就上模型,先把链路跑稳。

4.5 关键参数速查表

我把项目里用过的参数经验整理成一张表,不是标准答案,但可以作为初值参考:

参数含义经验值/策略
chunk_size文档切块长度500-800字符
chunk_overlap块重叠长度50-100字符
embedding_top_k向量召回候选数6-10
rerank_top_n重排后注入数3-5
max_history_rounds保留原始消息轮数6-8
compact_threshold触发压缩的历史token阈值1500-2500
compact_debounce压缩防抖时间5-10分钟
system-first系统指令放第一个区块固定建议
task-last用户当前问题放最后固定建议

这张表能看出一个共性:几乎所有关于"数量"的参数都有合理区间,不是越大越好。top 拉到10、历史全留,看着信息全,实际上模型消化不了,还会拖慢响应。做上下文工程,很多功夫花在"做减法"上。

5. 常见问题与排查实录

框架搭好、参数调完,剩下来就是各种真实运行中的问题。下面这些问题都是我和团队实际踩过的,按"现象-原因-排查方法-解决方案"整理,方便直接当速查表用。

5.1 效果越改越差,可能上下文被污染了

现象:某次改动后,模型开始把知识库里的内容当成操作指令来执行,或者语气突变、输出格式错乱。

原因:不同信息源的边界没有区分。最常见的是把检索结果和系统指令直接拼在一个文本块里,模型分不清"这是规矩"还是"这是数据"。

排查:把当次请求的完整 messages 导出来,逐段看结构,特别注意检索文本前后有没有和指令性内容混在一起。

解决:用结构化分区模式,给系统指令、事实数据、历史记录各自独立的区块标签;同时,检索结果本身不要出现"你必须""你应当"这类带指令色彩的表达,尽量中性陈述事实。这个细节我调了很久才发现,检索段落里一个"请注意"都会让模型误判。

5.2 Token 成本飙升,一个请求吃掉几千 token

现象:账单暴涨,或者单次请求频繁提示超出窗口上限。

原因:全量直连模式把历史消息一股脑全带上;RAG 模式下 chunk_size 过大、top_n 过大;或者每次都做全量摘要。

排查:输出日志里记录每次请求的 token 数,统计平均值和P95,定位是哪个环节贡献最多。建议从第一天接日志就顺手把这个字段加上,别等账单爆炸了才想起来。

解决:历史消息设置保留轮数,超出部分走折叠压缩;检索片段控制在3-5个;摘要触发加防抖;超长文档做分层摘要,而不是全文进上下文。

5.3 检索结果命中但模型不用

现象:知识库里明明有答案,检索也把片段取回了,但模型视而不见,反而自己编了一个答案。

原因:大部分情况是注入位置和格式的问题。检索结果放在太靠后的位置,模型注意力已经疲软;或者检索片段格式混乱,模型没有意识到这是可信资料。

排查:生成时开启流式输出,观察模型是从哪一段开始"走神"的;同时检查检索结果有没有来源标注和明确区块边界。

解决:把事实数据区放到系统指令之后、对话历史之前;给每个片段标注来源;在系统指令里明确写"请优先依据资料片段回答,资料中没有相关信息时明确说明",而不是"根据知识库回答"这种含糊表述。

5.4 多轮对话越聊越"失忆"

现象:对话进行到20轮之后,模型忘了用户最开始提到的关键约束,比如"价格不能超过500元"。

原因:折叠压缩模式把早期原文替换成了摘要,但摘要过于泛化,没有保留关键约束条件。生成摘要的模型会下意识提炼"大意",而"大意"通常会丢掉数字和限制词。

排查:核对生成的摘要内容,看关键约束是否落进去了。如果没有,就是摘要提示词和保留策略的问题。

解决:在摘要生成时维护一个"关键信息清单"。把用户的明确约束、订单号、偏好这类高价值信息单独记忆,不参与压缩丢弃。摘要提示词里点明:"请保留所有数字、日期、否定条件、用户明确的限制性表述"。哪怕摘要稍微长一点,也比丢失关键约束强。

5.5 延迟过高,用户等得不耐烦

现象:整个链路平均耗时超过3秒,其中上下文构建环节占了大头。

原因:向量检索、重排、摘要生成全部串行执行,累加时延;或者检索链路本身没有做缓存。

排查:给每一段链路打点计时,从用户进入到你拿到模型首 token,拆出检索、重排、拼装、模型调用各阶段耗时。

解决:判断 query 是否命中缓存,比如完全相同的近义词查询,命中就直接返回上次的上下文构建结果,省掉检索;路由模块把闲聊类请求直接拨到纯对话模式;重排和检索可以用并发;摘要生成异步化,不让用户等。这几个优化叠加,能把 P95 延迟从三秒压到一点二秒左右。

5.6 我的个人排查工具箱

最后分享几个平时调试上下文特别有用的习惯:

  • 打印"上下文快照":每一条系统级消息都记录下来,存成只读日志,后面可以完整回放当时的上下文,出任何问题都能复盘;
  • 给每个片段打标签:来源、检索分数、重排分数、注入顺序。一旦效果出问题,能快速定位是召回烂、排序烂还是拼装烂;
  • 用"消融实验"定位变量:固定模型和窗口,分别测试去掉重排、去掉来源标注、去掉历史轮数限制,观察效果变化。几次消融下来,你就知道自己项目里哪个环节最值得投入优化,而不是每次都面对一整套黑盒瞎猜。

6. 写在最后的个人心得

这篇文章写到这里,我其实最想说的不是某一个具体模式有多好,而是"上下文模式"这个词背后反映出的趋势:大模型应用开发已经从"堆提示词"的阶段,进入到了"精细化管理输入"的阶段。模型的能力是底座,而上下文是你和模型之间的"临时工作记忆"。你怎么管理这份记忆,决定了这个模型在你的具体问题上能发挥出多少水平。

我个人在实际项目中的体会是:不要一上来就追求部署一套什么完美架构,先把请求日志和上下文观测配齐,再多做几组消融实验。你可能会发现,很多效果问题不是模型不够强,而是上下文没喂对。与其不停换模型、买更贵的 API,不如先把 RAG 的检索质量、分区的规范性、压缩的保留策略打磨扎实,往往花小钱办大事。

最后分享一个小技巧:在设计 Context Mode 的时候,一定给自己的系统留一条降级链。我见过很多团队因为执着于复杂链路,外部服务一抖动整个系统就挂掉。一个健壮的做法是:检索不可用时自动转为纯对话模式,向量库超时后直接走关键词搜索,重排模型太慢就退回到向量原始顺序。链路可以复杂,但故障时要有能兜底的最简路径。这个思路我在多个项目里反复验证过,带来的稳定性收益远超想象。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询