在做某个文档问答项目之前,我对“context-mode”这个词的理解还停留在“把一大段 prompt 拼好丢给模型”。直到我面对一份两百多页的产品手册和一个动辄几千行的代码仓库,才发现上下文怎么管理、怎么切分、怎么取舍,才是这类应用真正的分水岭。所谓 context-mode,就是“给模型看什么、按什么结构看、看多少、以什么顺序看”抽象成几种可复用的策略模式,让系统在成本和效果之间做平衡。这篇文章是我在一轮轮改造中沉淀下来的完整设计思路、路由代码、参数依据以及翻车记录,希望能帮你少走弯路。
1. 为什么需要 context-mode:被长文档和代码库逼出来的设计
1.1 上下文窗口不是无限货架
现在不少模型动辄宣称支持数万甚至更长的上下文窗口,很多人的第一反应是“那我全塞进去不就完了”。这个想法听着合理,实际用起来会连续撞墙。
第一个问题是成本。上下文按 token 计费,塞进去的东西越多,单次调用的费用越高。而且长输入的定价通常不是线性便宜,某些服务对超长 prompt 还会额外加价。我见过一个团队做文档问答,为了“省事”把 300 页 PDF 全部注入,一次调用花掉几块钱,线上每天几千次调用,成本直接报表。
第二个问题是延迟。模型处理长 prompt 时,prefill 阶段要并行计算所有输入 token 的注意力,这个时间会随着长度明显拉长。实测下来,一段 80k token 的输入,首 token 延迟比 8k token 高出好几倍。对交互式应用来说,用户等 8 秒和等 2 秒的体验差距是决定性的。
第三个问题更隐蔽:有效注意力。大量研究都指向一个现象——模型对长上下文中间部分的信息利用效率会显著下降。你可以理解成人在读一本 200 页的书时需要“翻回去找”,但模型没有书签,它只能在注意力矩阵里靠概率找回早期内容,找着找着就丢了。窗口装得下,不等于模型记得住、用得上。
1.2 信息越多,不等于回答越准
这是一个反直觉但极其重要的结论:上下文是概率空间,不是数据库。把模型当成一个注意力有限的人,如果你给它 20 页无关材料和 3 行关键表格,它反而比只看那 3 行的人更容易答错。
我在项目里做过一次对照实验:同一个事实型问题,一组只注入相关章节,另一组注入整本手册。结果整本注入组的准确率明显更低,而且出现了不少“一本正经编参数”的答案。原因在于,冗余信息制造了大量与问题无关的注意力竞争,模型被噪音带跑的概率变大。
这也是为什么后来所有做长文本处理的应用,无论叫 RAG 还是叫 Agent,本质上都在做同一件事:把“全量注入”改成“聚焦注入”。context-mode 的精髓也在这里——它不是为了省 token 而省 token,是为了把模型注意力引导到真正有用的地方。
1.3 从“临时拼上下文”到“模式化管理”
我早期的实现非常朴素:把整个文档塞进去,跑不动就截断,再跑不动就换更大的模型。结果又贵又不稳,而且每次出问题都不知道是文档的问题、截断的问题还是模型的问题。
后来我把主流方案梳理了一遍,发现大家翻来覆去其实都在用几种固定套路:全量注入、滑动窗口、摘要压缩、检索聚焦、层级结构化。把这些套路统一抽象成 context-mode,带来的好处是系统层面的。
第一,路由可编程。系统可以根据任务类型和输入规模,自动选择模式,而不是每次都由业务代码临时拼 prompt。第二,参数有地方调。每种模式的窗口大小、压缩频率、检索数量都有明确配置,调试时有据可依。第三,日志可复盘。每次请求记录用了什么 mode、消耗了多少 token、命中了哪些片段,出问题时可以回溯。
2. context-mode 的五种形态:从全量注入到层级聚焦
2.1 Full 全量模式:最简单,但也最奢侈
Full 模式就是把能塞的上下文全部塞给模型,不做任何裁剪。这是最直觉的做法,也是成本最高、可扩展性最差的方案。
它的适用场景其实很窄:输入体量本身不大,且任务需要模型逐字核对原文。典型如合同条款审查——你问“第三条和第五条是否存在矛盾”,模型必须同时看到两条完整条款,任何摘要或切片都可能导致误判。又比如单文件代码补全,文件只有几百行,全量注入反而是最稳的。
实操时我给 Full 模式设了一个硬约束:输入长度不得超过上下文窗口的 40%~50%。为什么不是 100%?因为模型还需要空间生成回答、调用工具、接收后续消息。如果把窗口撑满,输出的空间被压缩,模型很可能会产出被截断的回答,或者被迫压缩思考。
一个常见的误区是“Full 就是最好的,因为它信息最全”。前面已经说过,信息全不等于效果好。Full 模式只适合信息密度高、任务强依赖全局关系的场景,一旦输入超过窗口的合理比例,就该换模式了。
2.2 Sliding 滑动窗口模式:保住“最近”的记忆
Sliding 模式模拟的是人的短期记忆机制:保留最近一段时间的内容,更早的自然遗忘掉。它在多轮对话和长流式输出中非常常见。
实现思路是固定一个窗口预算,比如“最近 6 轮对话”或“最近 4000 token”,每次请求把窗口内的消息拼到 system prompt 后面。需要特别注意,窗口单位不能只按“轮数”算,因为每轮消息的长度可能差异巨大。我见过有人按 10 轮切窗口,结果某一轮里塞了一份完整文件,窗口直接爆炸。更稳的做法是按 token 预算切,比如窗口总量 5000 token,从最近的会话开始往前收,收到 5000 为止。
滑动窗口还有一个细节容易被忽略:语义断点。如果窗口正好切在一个讨论主题的中间,模型就会“失忆”——它记得你聊过某个话题,但记不清前因后果。我在工程里加了两个补救措施:一是窗口与窗口之间保留 10%~15% 的重叠,相当于给记忆做了一点缓冲;二是尽量按语义块切分,而不是纯按字符数切,这个后面会展开讲。
2.3 Summary 摘要压缩模式:用损失换空间
当对话或文档很长,滑动窗口已经装不下时,摘要压缩就上场了。核心思想是:把历史内容压缩成摘要,再加上最近几轮原始内容,组成新的上下文。
我强烈推荐“两级结构”,而不是简单地把整段历史一次性压缩。具体做法是:维护一个长期摘要,每隔固定轮次或固定 token 量更新一次;同时保留最近几轮原始消息作为短期记忆。这样模型既能看到全局脉络,又不丢失本地细节。更新摘要时,可以在已有摘要基础上增量追加,而不是每次从头压缩全量历史,那样成本会失控。
摘要生成本身可以用较便宜的模型或专门的 summary prompt。如果要对超长文档做摘要,可以走“MapReduce”式的两级压缩:先把文档按块切分,每块生成局部摘要,再把局部摘要汇总成全局摘要。代价是细节进一步丢失,所以只适合内容综述类任务,不适合需要精确定位事实的任务。
2.4 Retrieval 检索聚焦模式:只带最相关的片段
Retrieval 模式是 RAG 类应用的核心,也是我在长文档问答里最终的主力方案。它不试图把所有内容塞进上下文,而是先从文档库里检索出与问题最相关的 top-k 个片段,只把这些片段交给模型。
实现上有三个关键参数需要调:chunk size(切片大小)、top-k(返回片段数)、相似度阈值。我的默认值是 chunk 控制在 300~800 token 之间,top-k 取 3~8,相似度阈值需要根据实际 embedding 模型和文档风格滚动调。
top-k 不是越大越好,这是新手最容易踩的坑。k 越大,噪音越多,token 预算也膨胀。如果你检索出的前 5 个片段里有 2 个完全不相关,模型就可能被带偏。我把 k 从一个极端调到另一个极端做了测试:k=1 时准确率很低,因为正确答案不一定排在最前面;k=20 时准确率也低,因为噪音太多;k=5~6 附近效果最好。这本质上是“召回率”和“精确率”的权衡。
另一个容易被忽略的点是注入顺序。检索结果按相关度倒序排,还是正序排,会影响模型注意力的分布。一些框架习惯把最相关的片段放在 prompt 尾部(离问题最近),一些放在开头,我在实践中发现因模型而异,最好在评测集上做 A/B。
2.5 Hierarchical 层级结构模式:把代码库当目录读
这个模式是我在处理代码库时被逼出来的。代码库不像文档那样线性,模块之间有依赖、函数有调用关系,如果平铺成一段文本,模型很难建立结构感。
Hierarchical 模式不把整个代码库切成长文本,而是先建立一棵“结构树”:顶层是模块 / 文件列表,中层是类 / 核心函数 / 章节标题,底层是具体代码块或段落。查询时,系统先让模型或规则引擎定位到相关模块,再下钻到具体片段,而不是一次性把整个代码库塞给模型。
你可以把它类比成读书先看目录。如果直接甩给你一本 800 页的书让你找某个细节,效率极低;但先翻目录定位到第 13 章,再翻到对应小节,就能快速找到。模型也是一样的。
实现上有两种常见做法:一种是预处理阶段手动或自动解析目录结构(比如按 markdown 标题、按代码语法树),把结构索引存起来;另一种是在问答链路中先用一次轻量请求做“导航”,让模型决定下一步下钻到哪个节点,然后再注入那个节点的内容。两种做法可以叠加,导航层用较小的模型也不会太伤效果。
2.6 模式对比速查表
| 模式 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Full 全量 | 全部注入 | 信息最全,适合逐字核对 | 成本高、延迟高、易超窗 | 短文本、合同核对、单文件代码 |
| Sliding 滑动窗口 | 保留最近内容 | 实现简单,天然适合对话 | 会丢早期信息,切割点破坏语义 | 多轮对话、流式输出 |
| Summary 摘要压缩 | 历史做摘要 | 控制 token 成本,保留全局脉络 | 丢失事实细节,否定词/数字易出错 | 超长对话、内容综述 |
| Retrieval 检索聚焦 | 只注人相关内容 | 成本低、效果好、可控 | 依赖检索质量,可能引入相似噪音 | 知识库问答、RAG 类应用 |
| Hierarchical 层级结构 | 按结构树定位 | 适合复杂结构,上下文利用率高 | 需要预处理,导航链路复杂 | 代码库问答、长文档结构化分析 |
3. 让系统自己选模式:路由策略与关键参数设定
3.1 决定模式选择的三个维度
有了模式,下一步是设计路由,让系统能自动选模式。我总结了三个核心维度:任务类型、输入规模、事实密度。
任务类型决定了基础策略。如果任务是合同条款核对、代码单文件调试这种强依赖完整上下文的,优先 Full 或 Hierarchical;如果任务是多轮客服对话、逐步执行的 Agent 任务,优先 Sliding 或 Summary;如果任务是“从一堆资料里回答问题”,优先 Retrieval。
输入规模决定了模式的可行性边界。一个 1 万 token 的文档,Full 完全没问题;一个 200 页的 PDF,Full 就不现实了,必须走 Retrieval 或 Hierarchical。
事实密度是我后来加上的维度。如果问题答案强依赖精确数字、专有名词、否定表达,摘要和压缩要非常小心,最好走 Retrieval 或 Full;如果只是概括性、闲聊性内容,Summary 完全够用。
3.2 一个可落地的路由实现
路由逻辑本身不要写得太复杂,本质是一张决策表加上少量动态判断。我给出一个简化版的 Python 伪代码,展示核心思路。
def select_context_mode(task_type: str, input_len: int, fact_density: str) -> str: # 任务类型先定基调 if task_type in ("contract_review", "code_debug_single_file"): if input_len < 4000: return "full" return "hierarchical" if task_type == "multi_turn_chat": if input_len < 5000: return "sliding" return "summary" if task_type == "doc_qa": if input_len < 8000 and fact_density == "high": return "full" return "retrieval" # 默认兜底:宁可多花点钱,也别答错 if fact_density == "high": return "retrieval" return "summary"实际工程里,这个函数不会到处散落,而是封装在统一的“Context Builder”里。业务层只声明任务类型和事实密度,由 Context Builder 决定具体模式、组装 prompt、计算 token 预算。这样后续加新模式或者调参数,不需要改业务代码。
3.3 参数设置的合理区间与推导依据
参数不能拍脑袋,但也不必过度精确,关键是理解每个参数的敏感方向。
滑动窗口的预算分配,我一般按“70% 历史 + 30% 当前输入和输出”来切。为什么留 30%?因为多轮对话里最近一轮的问题和潜在回答长度很难预测,留出余量可以防止截断。窗口总量如果上下文中窗口是 8k token,我会让历史窗口限制在 5k 左右。
摘要压缩的时机,我习惯“每 5~10 轮或每 2000 token 触发一次”。太频繁压缩会丢失更多细节;太晚触发则会让单次请求的输入 token 膨胀。摘要的目标长度控制在原文的 10%~20%,既能保留框架,又不至于变成口号式概括。
Retrieval 的 chunk size,我前面提过 300~800 token。为什么不是越大越好? chunk 太大会让一个块里混入多个主题,检索定位变粗糙;太小则语义碎片化,embedding 向量表达力不足。如果文档结构清晰(比如 markdown 有标题),我会优先按标题切块,而不是按固定 token 切。top-k 默认 5,在检索质量明显好时可以降到 3,在文档碎片化严重时升到 8。
3.4 工程落地:缓存、日志与可观测性
模式路由只是第一步,真正决定系统是否好维护的,是工程化细节。
首先,所有模式共用一个上下文组装模块。我见过太多项目把 prompt 拼接逻辑散落在业务代码里,换一个模式就要翻遍全项目。统一 Context Builder 的好处是,出问题时只需要排查一个入口。
其次,缓存要分层做。Retrieval 模式的 embedding 检索结果,如果查询相似可以缓存,避免每次重新向量化;Summary 模式的长期摘要是逐步更新的,可以做成独立缓存,不随单次请求丢失。缓存命中的日志也要打出来,方便分析哪些路径走缓存、哪些走了全量链路。
最后,可观测性是救命稻草。每次请求的结构化日志至少要记录:选中的 mode、输入 token 数、输出 token 数、检索命中的 chunk ID、摘要更新的轮次、延迟分位。没有这些数据,线上出问题连排查方向都没有。我后来还加过一个“上下文利用率”指标,统计最终模型回答中实际引用了哪些注入片段,用来发现“注入了但没用上”的浪费。
4. 实测数据与四个翻车现场
4.1 200 页文档问答评测:数字不会说谎
我在模拟项目 X 上做了一组对比评测,构造了一份 200 页的产品手册问答集,包含 30 个事实型问题和 20 个综合归纳型问题。四种主要模式的实测数据大致如下:
| 模式 | 事实型准确率 | 平均输入 token | 平均延迟 | 相对成本 |
|---|---|---|---|---|
| Full | 82% | 78k | 8.4s | 3.2x |
| Sliding | 61% | 35k | 4.2s | 1.4x |
| Summary | 68% | 22k | 3.1s | 1.0x |
| Retrieval | 87% | 12k | 2.3s | 0.6x |
这个结果验证了两个判断:全量注入在长文本上确实又贵又低效;滑动窗口和摘要模式因为丢太多细节,事实型任务准确率反而不如检索聚焦。Hierarchical 模式我没有放进表格,因为它在代码库上的表现远好于 PDF,单独测更公平,事实型准确率约 84%,输入 token 控制在 18k 左右。
注意,这个表格不能直接套用到你的项目里。评测指标必须按任务分型,单一准确率会骗人。比如摘要模式在综合归纳题上表现不错,因为归纳本身不依赖细节;但如果你的业务是“查参数”,摘要模式就是灾难。
4.2 翻车现场一:摘要模式吞掉了“不”字
这是我在调试中印象最深的一次翻车。原文有一句话是“该版本不支持离线部署”,摘要生成之后,模型把短语压缩成了“该版本支持离线部署”。从“不支持”到“支持”,意思完全反了。
原因不难理解:摘要模型在压缩时倾向保留名词和主干结构,否定词“不”经常被视为可省略的虚词,上下文范围标识词也容易被吞。这个问题不只在 Summary 模式里出现,在 Hierarchical 模式的中间层摘要里同样会出现。
我的解决办法分两层。第一层是在摘要指令里显式强调:必须保留数字、专有名词、否定词和范围限定词,不允许改写事实性表述。第二层更实用:对强事实型内容,不依赖摘要,而是从原文里检索并保留原文片段。也就是说,Summary 模式和 Retrieval 模式可以配合使用,摘要提供脉络,检索保留锚点。
4.3 翻车现场二:滑动窗口切断了一条语义链
滑动窗口一个非常阴险的问题是,窗口边界可能正好切在一个推理链条中间。我在做多轮 Agent 任务时遇到过这种情况:前一轮说“价格上调 10%”,后一轮问“现在的价格是多少”,窗口只保留了后一轮和一句模糊的历史,模型不知道上调的基准价是多少,直接给了一个错的推导结果。
代码场景也一样。一个函数定义在某个文件的窗口内被截断了,引用它的地方在窗口外,模型在补全时根本不知道自己引用的是什么。表面上看模型“失忆”了,其实是窗口切错了位置。
修复思路是“按语义块切,而不是按 token 切”。我给文本先做块化,块边界放在段落、代码函数、列表项这些语义完整的位置,然后滑动窗口以块为单位组装,而不是逐 token 数。同时相邻窗口保留少量重叠。这样做虽然略显浪费,但能明显减少语义断链导致的错误。
4.4 翻车现场三:检索模式拿“像但错”的内容当事实
Retrieval 模式不是银弹,它的上限取决于检索质量。我在一次问答中问“如何开启夜间模式”,知识库里其实有两个版本的设置路径,旧版本的路径和新版本完全不同。检索返回了旧版本的内容,相似度还很高,模型就理直气壮地照着旧路径回答了,结果是错的。
这个问题的根源是“相似 ≠ 正确”。embedding 检索擅长找语义相近的段落,但没法判断哪个版本才是当前的正确答案。只靠提高相似度阈值并不能完全解决,因为新旧版本在语义上就是很像。
我的应对方案有三点。第一,给检索结果附加来源信息和置信度,注入 prompt 时明确标注“以下内容来自多个来源,如果有冲突请说明不确定”。第二,在 prompt 里约定“如果没有找到明确答案,可以直接回答不知道”,而不是强行编一个。第三,设置一个低于阈值的“拒绝回答”门限,检索结果整体置信度太低时,直接走兜底流程,而不是硬答。
4.5 翻车现场四:模式叠加把预算悄悄吃干
不同模式是可以组合的,但组合不是免费午餐。我见过一个配置,在 Summary 模式外层又套了一层 Retrieval,在 Hierarchical 模式下每一层都生成一份摘要,结果 token 消耗翻了好几倍,效果却没有显著提升。
问题在于缺少预算控制。后来我在 Context Builder 里加了硬性约束:每次请求的输入 token 上限由配置决定,组装上下文时如果超了,宁可减少一些非核心片段,也要保证不超过预算。同时监控里加了 p50 / p99 输入 token 曲线,一旦发现某个模式组合的消费明显异常,就能第一时间发现。
模式组合的正确姿势是“各司其职”。摘要和检索配合可以,摘要负责全局脉络,检索负责关键事实;滑动窗口和摘要配合也可以,滑动窗口负责短期细节,摘要负责长期脉络。但同类功能的模式叠加(两个摘要、两个检索)基本是浪费。
4.6 一点工程心得:先审计,再优化
翻车现场讲完,我想说一个我认为比调参更重要的习惯:先做“上下文使用率”审计。说白了就是,在每个请求的日志里记录模型回答时实际引用了哪些注入片段,然后统计有多少片段从头到尾没被引用。
我做过一次粗糙的统计,一个优化前的 RAG 系统,注入的检索片段里有接近三成在最终回答里完全没被用到。这部分就是纯浪费。把它们砍掉或者提高相关性过滤,效果和成本同步改善。先看哪些注入真正起了作用,再决定是调 chunk size、top-k 还是换模式,顺序不能反。盲目加参数调优,就像不看地图开车,跑得再快也可能是反方向。
context-mode 这个设计思路帮我解决了一个很实际的问题:不是所有内容都值得让模型看,也不是所有模式都适合某个任务。把上下文当作系统里的一等公民来管理,先定模式,再定参数,最后靠日志和数据持续校正,这条路走下来是稳定且可控的。