1. 这个项目到底在解决什么问题
先别急着看代码,我们把“context-mode”这五个字母拆开。它不是一个标准化的开源项目名,更像是一个约定俗成的代号——但凡做过多模态模型、RAG 检索增强、或者长上下文推理的朋友,看到这个词基本都会心一笑:这是在给模型“加上下文模式”,或者说给应用系统加一套上下文管理机制。
我最早接触这个词,是在一个跨平台系统项目里。那个项目要用同一个模型同时处理短对话、长文档问答和结构化的表格推理。最开始我直接复用同一套 prompt 模板,结果问题很明显:短对话时模型回答得挺好,一旦丢进去一份几千字的合同,模型开始“丢三落四”,前文信息遗忘严重,甚至出现角色混乱。后来我才意识到,这不是模型能力问题,而是我的应用层根本没有根据使用场景切换上下文的组织方式。所谓 context-mode,本质上就是一套“按场景动态管理上下文”的方案,它决定了一个模型在当前任务里应该看什么、不看什么、以及按什么顺序看。
这个项目的核心诉求有三点。第一,不同任务需要不同的上下文长度,短对话不需要塞进全部历史,长文档则需要把关键段落提前。第二,不同任务需要不同的信息结构,列表类任务要结构化输出,叙事类任务要保持连贯性。第三,不同任务需要不同的注意力分配策略,有些场景要突出最新信息,有些场景要保留全局信息。
所以 context-mode 不是一个独立算法,而是一个方法论。它可以落地为配置文件、prompt 模板、上下文窗口调度策略,也可以落地为带状态的系统模块。这篇文章我会从项目拆解、核心细节、实操流程、问题排查四个角度,把这个“模式切换”的思路完整讲透。无论你是在做大模型应用、Agent 工作流,还是传统的搜索推荐系统,这套逻辑都有很强的参考价值。
2. 整体设计思路:为什么“一种模式永远不够”
2.1 三种典型上下文模式的定位
我习惯把 context-mode 拆成三个基础模式:全局模式(Global Mode)、会话模式(Conversation Mode)、聚焦模式(Focused Mode)。
全局模式用来处理长文档和代码库问答。它的特点是上下文窗口尽量开大,系统会把整篇文档或最近 N 轮对话全部送入模型。好处是信息完整,坏处是消耗 token 高、响应慢、还可能造成注意力分散。会话模式适合短平快的多轮对话,系统只保留最近的 3 到 5 轮,加上当前用户问题,组成紧凑的上下文。聚焦模式最适合检索增强场景,系统先通过召回算法抽取出与当前问题最相关的片段,再把这些片段拼装成新的上下文,和全局模式相反,它是“能少则少,但必须精”。
这三种模式不是竞争关系,而是互补关系。一个成熟的系统往往会同时具备这三种模式,并根据当前任务的信号自动选择使用哪一种。比如用户发来一个短问题“这段合同的有效期是多久”,系统进入聚焦模式,只召回合同里关于期限的描述;用户接着说“从头帮我总结一下这份合同”,系统则切换到全局模式,把完整合同文本灌入模型。
2.2 为什么要设计模式切换而不是统一处理
我曾经试过只用全局模式处理所有请求,实测下来问题非常多。第一是成本问题,全局模式下每次请求把整份合同文本都送进去,按 token 计费的话很快就能烧掉一个可观的预算。第二是延迟问题,上下文越长,首字延迟越高,用户问一个一句话就能回答的问题,却要等上好几秒。第三是精度问题,模型在做短问答时,过长的上下文会引入太多无关信息,导致回答抓不住重点。
反过来,只用会话模式也不行。模型只能看到最近几轮,当用户询问很久之前提到的细节时,模型会直接说“我不知道”,而不是去文档里找。聚焦模式同样不能独立使用,它依赖检索质量,如果召回模块效果不好,回答质量会断崖式下降。
所以真正的方案是:把模式作为一个顶层调度变量,在请求进入模型之前,先经过一个“路由判断层”。这个判断层根据用户意图、任务类型、系统负载,决定应该走哪个模式。这个路由判断层可以是一个轻量分类模型,也可以是一组启发式规则,甚至可以是一个更小更快的基础模型先做预判断。我在实际项目中喜欢用规则加分类模型的方式,既能快速处理明显的短对话,又能兜底长尾场景。
2.3 数据结构的支撑:上下文包
无论采用哪种模式,都需要一个统一的内部数据结构来承载上下文。这个结构我习惯叫“上下文包”(Context Pack)。它包含四个字段:身份指令、任务说明、参考素材、对话历史。身份指令告诉模型它现在扮演什么角色;任务说明告诉模型本次要完成什么动作;参考素材是聚焦模式下召回出来的片段;对话历史是会话模式下保留的近期轮次。
通过统一结构,不同模式可以共享同一套 prompt 渲染逻辑,只是往四个字段里填充的内容不同。全局模式填充完整的文档和全部历史;会话模式只填最近几轮并清空参考素材;聚焦模式把参考素材作为核心,对话历史只保留最近一轮。这样做的好处非常明显——系统内部逻辑清晰,切换模式时不需要重构代码,只需要调整填充策略。这套结构我强烈推荐任何做 AI 应用的朋友都采用,不管你是用开源模型还是商业大模型,最终都要面对“怎么把不同来源的信息塞进同一个模板”的问题。
3. 核心细节拆解:上下文到底怎么组织、怎么切换
3.1 上下文的排序策略:不是所有信息都按时间排
很多人在刚开始设计 context-mode 时都会犯一个错误:把信息按时间顺序排列。这其实是惯性思维,因为聊天记录天然是按时间产生的。但模型对上下文的敏感度不是均匀的,开头和结尾的信息往往更容易被模型关注,中间部分容易被忽略。这个现象在注意力机制研究中经常被提到,实际体验中也确实如此。
所以我在设计排序策略时,会把最重要的信息放在开头和结尾两端。具体来说,身份指令和任务说明总是放在最前面,然后是参考素材,最后是对话历史。如果对话历史很长,我会把最近的一轮放在历史末尾,同时把最早的系统级约束也挪到历史之前。这样做的结果是,模型在开头能看到“你是谁、你要做什么、可以参考哪些文档”,在结尾能看到“用户上一个问题是什么”,中间的旧轮次作为背景存在,即使被部分忽略也不会影响大局。
3.2 动态长度控制:上下文不是越长越好
既然不同模式决定不同上下文长度,那长度具体怎么定?我建议给三种模式分别设置上限。会话模式的上限通常设为 2000 到 4000 token 左右,这个长度足够覆盖最近几轮对话,又不至于稀释注意力。聚焦模式的上限可以设到 3000 到 6000 token,因为召回的片段必须足够丰富,但又不能把整篇文档都塞进去。全局模式则根据模型最大窗口决定,如果你的模型支持 32K,那可以设 28K 作为安全上限,留出余量给生成部分。
但这里有个核心问题:上下文长度不可能每次都刚好等于上限,用户输入参差不齐,系统需要动态截断和填充。我的经验是先按优先级分层:身份指令不可截断,任务说明不可截断,参考素材按相关度从高到低保留,对话历史按时间从新到旧保留。当总长度超过上限时,从最不重要的层级开始丢弃。这个优先级策略放在配置中心里,允许运营同学随时调整,不需要改代码。
3.3 模式切换的判断逻辑:什么时候切、切到什么
模式切换的触发点可以有三类:显式指令、隐式信号、系统策略。
显式指令最简单,用户在提问中直接说“总结整篇文章”或者“根据刚聊的内容回答”,系统就可以判断需要切换到全局或会话模式。隐式信号稍微复杂一些,比如用户问题中包含“合同”“报告”“规格书”这类指向文档的名词,系统就可以推测需要聚焦模式,即使没有明说。系统策略则是一个兜底方案,比如当会话模式连续执行失败三次,就自动降级到聚焦模式,从文档中补充信息。
我见过很多项目试图用一个大模型来判断模式,这确实可行,但也昂贵。后来我更倾向于用一个小的分类模型来承担这个任务,哪怕是只有几百 MB 的精简模型,判断“当前请求需要全局、会话还是聚焦模式”完全够用。这个小模型只需要把用户问题文本输入进来,输出三个类别的概率,系统取最大概率类别路由即可。实测下来,判断准确率能达到 95% 以上,成本却只有大模型判断的几十分之一。
4. 实操过程:从零搭一套 context-mode 系统
4.1 环境准备与数据流设计
实操之前先把环境说清楚。我当时是在 Linux 服务器上用 Python 做原型的,OpenAI 兼容接口调用模型,向量数据库用开源的嵌入引擎和检索库封装了一个简单服务。其实这整套方案不强依赖任何特定平台,你把模型接口替换成任何国内开源模型的兼容层,完全能跑通。
数据流设计是整个系统的骨架。我把它分成五层:输入层接收用户问题;路由判断层输出模式标签;上下文组装层根据模式拉取对应数据并填充上下文包;模型调用层将上下文包渲染成模型输入并请求生成;输出后处理层对模型输出进行校验和格式化。这五层各司其职,哪一层出问题都能快速定位。
4.2 代码骨架:路由判断与上下文组装
下面我给出一个简化但可直接跑的代码骨架,重点看模式判断和上下文组装这段。
# context_mode_demo.py import json from dataclasses import dataclass, field from typing import List, Dict, Optional @dataclass class ContextPack: identity: str = "" task: str = "" references: List[str] = field(default_factory=list) history: List[Dict[str, str]] = field(default_factory=list) def render(self, max_tokens: int = 4000) -> str: # 组装顺序:身份 -> 任务 -> 参考素材 -> 对话历史 sections = [] if self.identity: sections.append(self.identity) if self.task: sections.append(self.task) if self.references: sections.append("参考资料如下:\n" + "\n".join(self.references)) if self.history: lines = [] for turn in self.history[-6:]: # 默认保留最近 6 条 lines.append(f"用户: {turn['user']}\n助手: {turn['assistant']}") sections.append("聊天记录如下:\n" + "\n".join(lines)) # 简单截断,实际项目可用 tokenizer 精确切分 text = "\n\n".join(sections) return text[:max_tokens] def route_mode(user_query: str, has_document: bool = False) -> str: # 启发式规则 + 显式关键词用户问题 explicit_global = ["总结全文", "全文", "整篇", "整体分析", "全面"] explicit_focus = ["根据文档", "原文", "第几条", "条款", "参考材料"] if any(k in user_query for k in explicit_global): return "global" if any(k in user_query for k in explicit_focus) or has_document: return "focus" return "conversation" def build_context_pack(mode: str, user_query: str, document_text: str, history: list) -> ContextPack: pack = ContextPack( identity="你是一个专业的文档分析与对话助手。", task=f"用户的问题是:{user_query}", ) if mode == "global": pack.references = [document_text] pack.history = history elif mode == "focus": # 简单模拟检索:按关键词分数截取文档片段 sentences = document_text.split("。") scored = [] for sent in sentences: score = sum(1 for k in user_query if k in sent) scored.append((score, sent)) scored.sort(reverse=True, key=lambda x: x[0]) pack.references = [s for s in scored[:3]] pack.history = history[-1:] else: # conversation pack.references = [] pack.history = history[-3:] return pack if __name__ == "__main__": doc = open("sample.txt", encoding="utf-8").read() history = [ {"user": "这个项目的验收标准是什么?", "assistant": "请查看文档中的交付清单。"}, {"user": "时间限制呢?", "assistant": "文档中说明完成期限为三个月。"}, ] query = "根据文档,违约金条款是第几条?" mode = route_mode(query, has_document=True) pack = build_context_pack(mode, query, doc, history) print("[当前模式]", mode) print("[渲染后的上下文]\n", pack.render())这段代码的核心价值在于让大家看到模式切换并不复杂,本质就是路由函数加组装策略。实际生产环境还需要接入真正的检索服务,这一点在 4.3 节展开。
4.3 检索增强的聚焦模式:让 context 真正聚焦
聚焦模式要做好的核心是“怎么从文档中找出最相关的片段”。如果文档很小,上面的简单关键词打分还够用;但如果文档超过十万字,就必须用嵌入向量检索。
操作步骤大概是:提前把文档按段落或固定长度切块,每块用嵌入模型转成向量,存入向量数据库。查询时用同样的嵌入模型把用户问题转成向量,然后做相似度搜索,返回 Top K 片段。这 K 值怎么定?我建议初始设为 3,如果回答质量不足再往上调,但不要超过 8。因为片段越多,模型注意力越分散,回答的聚焦性反而下降。检索出的片段不是简单拼接,我会做一个“相关性排序”后再放入 Context Pack 的 references 字段。
还有一个容易被忽视的细节:切块时需要保留段落语义完整性。按固定字符切容易把一个句意断开,导致检索到的片段不完整,模型读起来莫名其妙。正确做法是先按段落切分,再对过长段落做二次切分,同时为每个切块存储它所属的文档编号和原始位置,方便追溯和展示来源。
4.4 会话模式的工程化细节:轮次精简与系统约束
会话模式看似最简单,其实工程坑最多。纯粹把最近的几轮塞进去会导致两个问题:第一,上下文里的常识性信息(用户已经确认过的事情)反复出现,浪费窗口;第二,历史中的错误回答会被模型当作依据,出现“将错就错”。
我的做法是引入“会话摘要”机制。当对话超过 N 轮时,先把早期对话用模型压缩成一条摘要,存储在系统侧。真正送进上下文的是最近几轮完整对话加压缩后的早期摘要。这样做的核心原因是注意力资源有限,摘要既能保留关键信息,又不会占用太多窗口。这个摘要的生成不需要每次都调用大模型,可以在会话空闲时异步完成,放进缓存里。
会话模式还有个不可忽视的细节:系统约束不能丢。我见过很多系统在会话轮数变多后,系统级指令被历史冲掉,导致模型行为漂移。解决方式是在组装会话模式时,始终把身份指令和系统约束放在 context 的最顶部,并且这部分不参与截断。
5. 常见问题与排查技巧实录
5.1 常见问题速查表
我在实际调试 context-mode 时遇到的高频问题基本集中在下面这几个方面,整理成表方便大家对照排查。
| 问题现象 | 可能原因 | 排查思路 | 解决方案 |
|---|---|---|---|
| 模型回答偏题,忽略用户问题 | 上下文过长,无关信息过多 | 查看渲染后的上下文,检查参考素材是否杂糅 | 降低聚焦模式 K 值,提升检索相关度阈值 |
| 模型忘记早起对话内容 | 会话窗口被截断,或摘要未生效 | 查看历史信息填充状态,检查是否启用了摘要 | 启用会话摘要机制,保留全局摘要字段 |
| 聚焦模式回答仍然很空 | 检索切块语义断裂 | 检查切块边界,检查召回片段是否完整 | 改用语义段落切块,在切块间设置重叠冗余 |
| 模式切换错误,频繁切错 | 路由规则覆盖不全 | 查看路由判断日志,统计错误类型 | 增加关键词覆盖,引入小分类模型兜底 |
| token 消耗暴增 | 全局模式使用占比过高 | 查看各模式请求量占比和平均 token 数 | 优化路由策略,对长文档提前做结构化摘要 |
| 模型重复输出同一句话 | 上下文里既有完整文档又有摘要,信息冗余 | 查看参考素材与历史是否存在重复 | 去重策略:摘要与原始片段二选一 |
5.2 排查实录:模式切错导致合同条款回答失败
有一次我做合同问答测试,用户问“违约金的计算基数是合同总额还是已履行部分?”,系统判断为会话模式,结果模型直接回答“抱歉,我没有足够信息来回答这个问题”。查看上下文才发现,会话模式只保留了最近两轮对话,根本没有把合同原文放进去。
排查路径并不复杂。第一步查看路由判断日志,确认 route_mode 输出是 conversation;第二步查看 Context Pack 内容,发现 references 为空;第三步定位原因:问题中没有触发聚焦关键词,而且 has_document 参数没传,导致路由函数误判。修复方式是:在测试代码里把 has_document 改为 True,同时在路由规则里增加“包含金额、条款、合同名词时强制走聚焦模式”的规则。由此可见,路由规则需要持续迭代,靠一版规则一步到位基本不可能。
5.3 排查实录:全局模式下模型开始“胡言乱语”
另一个典型案例是长文档摘要场景。系统在全局模式下把一份两万字的文档全塞进上下文,模型虽然能看到所有内容,但回答时开始出现幻觉,甚至自己编造文档中不存在的结论。原因不是模型能力不够,而是上下文太长导致注意力分散,文档中间的大量细节被模型当成噪声,于是模型开始“脑补”连贯性。
我的解决方法是放弃一次性全文档摘要,改成“分块摘要再合并”的策略。把文档切块后每块单独做摘要,再把各块摘要合并成二级摘要,最后再用模型做总摘要。这样既保证了每个细节都被模型看到,又降低了单次上下文的压力。这个思路的本质也是 context-mode:在不同层级用不同上下文组织方式,避免用单一模式硬扛所有场景。
5.4 避坑技巧:上下文包必须可观测
调试 context-mode 最重要的习惯是:让上下文包可观测。我在模型调用之前,会把渲染好的上下文文本打印到日志中,同时把模式标签、token 数、各字段长度都记录下来。很多问题只凭最终回答根本定位不了,一旦输出上下文日志,一眼就能看出是参考素材丢了、历史截断了,还是路由切错了。这个习惯帮我节省了大量排查时间。
另外还有一个小技巧:给 Context Pack 增加一个“模式来源追踪”字段,记录当前上下文是由哪个模式、哪条规则生成的。这样你在做回归测试时,能直接看到模式变更对输出质量的影响。很多团队只测模型输出,从不测上下文组装,这是本末倒置。
6. 这个思路还能用在哪里
context-mode 虽然是在大模型应用场景里被广泛讨论,但它本质上是一种信息管理思想。往小了说,它可以用于搜索系统的查询改写与排序策略;往大了说,它甚至可以用于团队知识库的管理——谁在什么时候需要看到什么信息,就是一套人工版的 context-mode。
就我个人的体会,做这块内容最有价值的不是把某个模式调到完美,而是形成“按场景动态组织上下文”的思维习惯。很多系统最开始可能只需要一个会话模式,跑通后加上聚焦模式就能支撑文档问答,再往后配合摘要和分块,就能覆盖长文档场景。每次增加一种模式,都要回头检查路由判断逻辑,避免因为模式变多反而引入混乱。
如果你也想在自己的项目里落地 context-mode,我的建议是从最简单的三件套开始:一个上下文包数据结构,一个路由判断函数,三个组装策略。先跑通最小闭环,再逐步精细化。等你积累一段时间,自然会形成一套完全属于自己项目的上下文管理体系,那时候你就会理解,为什么一个看似简单的“模式切换”,会让模型表现发生质的改变。