如果你最近在折腾 AI 编程助手或者大模型应用,肯定没少听到context-mode这个词。我最早被这个词搞晕,是在连续两个项目里分别撞上同一个问题:和 AI 聊了三十分钟之后,它开始忘记最初的需求;把一份项目文档喂进去之后,它反而被无关的细节带偏。后来我意识到,这些问题全都可以归结为一句话——上下文管理没做好。
context-mode不是一个官方名词,更像是一类设计思路的总称。它要解决的是:大模型虽然有看似巨大的上下文窗口,但我们在真实使用中,不能把所有历史记录一股脑丢给它。到底保留什么、压缩什么、检索什么、按什么顺序注入,这些组合起来就是一套上下文管理模式。这篇文章我会从工程视角把context-mode讲透,包括它在模型侧和应用侧分别指什么、为什么非做不可、怎么从零搭一个能落地的上下文管理系统,以及我实际操作中踩过的坑。无论你是做 AI 应用开发的、用 AI 写代码的,还是打算给团队做个智能问答机器人,应该都能从这里拿到一点可以直接用的东西。
1. context-mode 到底是什么:一次理清这个被用滥的词
1.1 从一个让我抓狂的场景说起
先说个我自己的真实经历。有次我用 AI 辅助重构一个老服务,一开始聊得很顺,AI 准确说出了某个模块的依赖关系,还给了详细的改动方案。但四十多分钟聊下来,我回头问它“你最开始不是说这个接口可以拆成三个吗?后来怎么改成保留两个了”,它的回答让我血压直接上来:“我们之前没有讨论过拆成三个接口的方案。”那一刻我确认了两件事:第一,它确实“忘”了;第二,不是它的模型能力问题,而是我的上下文管理方式有问题——我把关键信息埋在了大量无效对话里,最终被系统截断或覆盖了。
类似的场景在很多人身上都发生过:长对话到后半段,AI 开始用笼统话术回答;在一个问答机器人里,用户问出和半小时前相关的问题,它完全答不上来;给 AI 喂了一堆资料之后,它反而只盯着其中一段无关内容猛输出。这些问题看起来五花八门,本质都是同一件事:我们没有显式地告诉 AI“哪些信息是重要的、当前任务是什么、哪些内容已经过期”。
context-mode这个词就是在这种背景下被反复提起的。它不是一个按钮,也不是某个具体模型参数,而是一整套关于“如何在每次请求中组织上下文”的策略。做得好,AI 像是一个记忆力极好且知道轻重缓急的助手;做不好,你花再多钱换更大窗口的模型也没用。
1.2 两种最常见的 context-mode 理解
我在不同社区里看到的讨论,基本可以分成两种理解。第一种是模型侧的context window,也就是上下文窗口本身。这指的是模型一次能处理的 token 数量上限,比如常见的 8K、32K、128K、200K。这个值是在模型训练和部署时就确定的硬件级约束,不是我们能随意改的。超出的部分要么被截断,要么在服务端做隐式摘要。
第二种是应用侧的context management mode,也就是我们作为开发者在调用模型时,如何选择、排列、更新那些即将进入上下文窗口的内容。这包括系统提示词怎么设计、历史对话保留多少轮、关键事实存在哪里、什么时候触发检索、什么时候做摘要压缩。这部分是我们真正能设计和优化的空间。
打个比方,模型上下文窗口像是一间会议室的容量,会议室的墙和座位数是固定的。而 context-mode 就是会议主持人手中的那些权限:安排谁坐第一排、谁的发言记录要保留、上轮讨论留下的白板内容要不要擦掉、什么时候从档案柜里调出新材料。同一个模型,换了不同的主持人,会议效率和参会者记忆完全不同。工程上做 context-mode,本质上就是当好这个主持人。
2. 为什么要做 context-mode:模型记忆的三大真相
2.1 上下文窗口不是内存
很多第一次接触大模型开发的人都会有一个误解,觉得模型跟人一样,聊过的东西它会“记住”。真相是大模型在绝大多数应用形式下都是无状态的:你发出去的每一条请求,它都是独立处理的。之所以看起来像记得,是因为我们把全部或部分历史对话重新放回了输入里。
这个区别非常致命。如果说模型是一台计算器,那上下文窗口就是这台计算器上临时摆放的数字纸条。计算器本身没有任何记忆,纸条放多少它就处理多少,放不下就翻车。context-mode要做的事情,是在每次计算开始之前,决定把哪几张纸条摆上台面、哪些收到抽屉里、哪些直接扔进废纸篓。
理解了这一点,你再看市面上那些号称“长记忆”的 AI 产品,思路就清楚了。它们没有一个真的长记忆,都是在每次请求前检索数据库、拼接历史、重放上下文。产品体验的差异,全在检索准不准、拼接好不好、更新快不快这些工程细节上。
2.2 token 既是成本也是约束
为什么要费劲管理上下文,而不是直接把窗口拉满?最现实的原因是 token 真的很贵。不管是按输入输出计费的商业 API,还是自己部署模型占用的算力,每一轮请求的 token 消耗都在烧钱。
举个例子,一个常见大模型 API 每百万输入 token 的价格按几十元量级计算看起来不算夸张,但如果你做一个每轮请求都要把 10 万 token 项目文档全量塞进去的问答机器人,一次对话就是十几次请求,一天的调用量上来之后成本会非常吓人。而且请求里塞的东西越多,响应延迟越长。一个 200K 窗口的服务,你每次都顶格输入,首字返回时间可能翻两三倍,用户根本等不起。
所以 context-mode 的核心收益有两个:省成本、降延迟。它不是让模型变聪明,而是让模型把有限的预算花在真正重要的信息上。我自己的经验是,把无用的长历史压缩掉、只在需要时检索相关文档,同样质量的回答能把单轮 token 消耗降低 60% 以上,响应速度提升也非常明显。
2.3 信息衰减比想象中更快
还有一个容易忽略的真相:就算 token 没有超出窗口上限,模型对上下文不同位置的关注度也不是均匀的。这个问题在圈内被讨论得很多,通俗地说就是“中间内容更容易被忽略”。
我做过一个小实验:把一份 30 页的说明文档放在对话中间位置,然后在后面的提问里特意引用了文档第 16 页上一条不起眼但关键的规则。AI 的回答经常忽略这条规则,而当我把它放到系统提示词里或对话开头,同样的回答就非常精准。后来我才了解到,这个问题在论文里被叫做“lost in the middle”,大规模语言模型对长上下文中间部分的信息利用率明显偏低。
这给 context-mode 提出了一个硬性要求:不能只保证信息在窗口内,还要保证信息在合理的位置上。核心事实要放到开头或结尾这些模型关注度高的区域,辅助信息可以放中间,过期信息干脆移除。很多 AI 应用“该记住的记不住”,不是模型不行,而是工程师把重要信息塞进了最容易失忆的位置。
3. 手把手构建一个可用的 context-mode 系统
如果你只是用现成聊天工具,前两章已经够你理解什么是 context-mode 了。但如果你是做开发的,我更想把这一章写详细一点。我早期在内部实现过一个小工具,代号就叫context-mode,功能很简单:给对话加上“分层记忆”和“动态注入”。下面把设计思路和核心代码拆开讲,你可以直接照着改。
3.1 决定架构的四个关键选择
动手之前,我先定了四个原则,后面所有的代码和配置都是围绕它们展开的。
第一是以会话为最小单位。不是说每次提问都临时拼上下文,而是每个会话维护一个状态对象,里面存着会话ID、目标描述、关键事实清单、最近对话轮次、相关文档引用。这样任何一次请求都能快速知道“我是谁、我在聊什么、什么最重要”。
第二是三层记忆架构。我把信息分成工作记忆、压缩记忆、外部记忆三层。工作记忆是最近几次对话原文,模型可以直接用;压缩记忆是把更早的对话定期做成摘要,保留主线逻辑;外部记忆指文档库或知识库,平时放向量检索里,需要时把命中片段取出来注入。这三层对应不同的访问成本和信息精度。
第三是注入顺序有讲究。我最终固定下来的拼接顺序是:系统提示词在最前,接着是当前的会话目标,然后是被检索到的相关片段,最后才是最近几轮对话。前面讲过模型对开头和结尾关注度高,所以最重要的目标和最新指令绝对不放中间。
第四是压缩要设触发条件。不是聊几句就摘要,而是按照 token 预算阈值触发。比如预设 12K token 的软上限,当最近对话原文超过这个值时,就把最靠前的旧对话折叠成一段摘要,并把其中提到的决定、日期、关键数字提取为结构化事实。
3.2 项目结构:先跑通再优化
整个工具我保持得很轻,核心就六个文件:
context-mode/ ├── config.py # 模型参数、token 预算、路径配置 ├── models.py # 数据结构定义:会话、消息、片段 ├── session.py # 会话管理:新增、保存、更新状态 ├── summarizer.py # 对话摘要与关键事实提取 ├── retriever.py # 向量检索:从文档库召回相关片段 └── cli.py # 命令行入口:对话、调试、查看上下文这个结构很朴素,没有上什么重型框架,就是为了方便把每一步都看明白。你如果做产品化,完全可以把 retriever 换成 ES、Milvus、Redis 向量库之类的东西,但核心逻辑不需要变。
3.3 核心代码:会话状态、压缩策略、检索注入
先看models.py,定义三个最基本的数据结构。
from dataclasses import dataclass, field from typing import List, Optional, Dict @dataclass class Message: role: str # "system"、"user"、"assistant" content: str timestamp: float # 时间戳,用来判断新旧 extra: Dict[str, str] = field(default_factory=dict) # 元数据,比如来源文件 @dataclass class SessionState: session_id: str goal: str # 当前会话目标,始终放在显眼位置 facts: List[str] # 提取出的关键事实,例如“用户要求保留旧接口” work_memory: List[Message] # 最近的完整对话 compressed: List[str] # 早期对话的摘要列表 doc_refs: List[str] = field(default_factory=list) # 关联的文档ID @dataclass class RetrievedChunk: doc_id: str text: str score: float metadata: Dict[str, str]SessionState里有一个容易被忽略的字段facts。这个字段非常重要,因为摘要本质上是模型的二次创作,保不齐会丢掉细节。而 facts 是明文列出的关键结论,比如“确认使用方案 B,放弃方案 A”。每次注入时我把 facts 放在靠近尾部的位置,代替一部分摘要,模型回答时会优先参考。
然后是session.py里负责压缩的方法:
from summarizer import summarize_old_messages, extract_facts class SessionManager: def __init__(self, max_work_tokens: int = 12000): self.max_work_tokens = max_work_tokens def push_message(self, session: SessionState, msg: Message): session.work_memory.append(msg) if self._estimate_tokens(session.work_memory) > self.max_work_tokens: self._compress(session) def _compress(self, session: SessionState): # 只拿最靠前的 60% 消息做摘要,留最近 40% 做原文 split_point = int(len(session.work_memory) * 0.6) old_messages = session.work_memory[:split_point] session.work_memory = session.work_memory[split_point:] summary = summarize_old_messages(old_messages) session.compressed.append(summary) new_facts = extract_facts(old_messages) session.facts.extend(new_facts)_compress的逻辑有讲究。为什么是 60% 而不是全部?因为模型对太遥远的信息利用率低,但对刚才几轮对话还有较强记忆。只压缩旧的部分,保住最近的原文,这种滑窗式做法能在摘要损失和时效性之间取得平衡。我在实际跑下来的效果比整体摘要好不少,推荐你直接抄这个比例。
最后是cli.py里组装最终请求上下文的方法,也是整个 context-mode 最关键的一步:
def build_request_messages(session: SessionState, user_query: str, retrieved: List[RetrievedChunk]) -> List[Message]: system_prompt = ( "你是一名严谨的工程助手。请优先依据下方提供的『会话目标』、" "『关键事实』和『参考资料』回答;如果参考资料与关键事实冲突," "以关键事实为准。" ) messages = [Message(role="system", content=system_prompt)] messages.append(Message(role="system", content=f"会话目标:{session.goal}")) messages.append(Message(role="system", content="关键事实:" + "\n".join(session.facts))) if retrieved: ref_block = "\n---\n".join(f"[{c.metadata.get('source','未知')}] {c.text}" for c in retrieved) messages.append(Message(role="system", content=f"参考资料:\n{ref_block}")) messages.extend(session.work_memory) messages.append(Message(role="user", content=user_query)) final_messages = [] for msg in messages: text = msg.content # 给不同来源的信息加上标记,后续调试或日志可以直接溯源 final_messages.append(Message(role=msg.role, content=text, extra={"src": msg.extra.get("src", "state")})) return final_messages注意几个细节。第一,目标、事实、参考资料都放在system角色里,模型对 system 部分的指令遵守度一般高于普通对话内容。第二,参考资料通过“来源”元数据标注,这样出问题时能找到是哪份文档带偏了模型。第三,work_memory放在参考文献之后、用户新提问之前,保证最新的对话状态离模型更近,减少中间丢失问题。
整个工具的闭环是这样的:用户提问 → 检索器召回片段 → 构建 request messages → 调用模型 → 把结果 push 回会话 → 若超预算则自动压缩。整个流程跑通之后,AI 等于有了一个“能整理笔记、会翻档案柜”的助手,失忆现象大幅减少。
4. 实操中的常见问题与排坑记录
4.1 token 预算失控
第一个栽跟头的地方就是 token 预算。你以为压缩到 12K token 以内就稳了,但模型返回的输出也在消耗 token,如果输出特别长,再加上 System Prompt 本身的长度,极容易直接冲过窗口上限。更隐蔽的是,不同模型分词器不一样,同样的字符数在不同模型下 token 数可能差很多。
我的解决方法是两件事:第一,给上下文预留 25% 缓冲,比如窗口是 32K,那我就把压缩阈值设在 21K 到 22K,再往上就触发更强的压缩。因为系统提示词、事实列表、工具定义这些“隐藏消耗”加起来很容易超过你的直觉。第二,调模型之前显式调用分词库做估算,不要靠字符串长度猜。
def _estimate_tokens(messages, encoding_name="cl100k_base"): enc = tiktoken.get_encoding(encoding_name) # 简化估算:按角色前缀 + 内容拼接后统计 raw = "".join(f"{m.role}:{m.content}" for m in messages) return len(enc.encode(raw))4.2 检索召回了错误的信息
context-mode 依赖检索,但检索这个环节特别容易出问题。最常见的现象是:你问“用户登录失败如何处理”,检索器从知识库召回了一段“登录日志字段说明”,里面出现了“失败”这个词,语义上却根本不相关。模型看到了这段内容,反而被带到沟里,给出一个答非所问的答案。
我后来给检索环节加了两个增强:一是混合检索,不能只用向量相似度,要配合关键词匹配和权重排序;二是每个片段必须带元数据,至少包含来源文件、标题、更新时间,并在注入时标明。这样即使召回了一段不相关的内容,模型也能在该部分打上“来自XX文档某章节”的标签,回答时会更谨慎,不会把不相关的资料当绝对事实。另外建议给检索结果设置分数下限——分数过低的片段宁可不注入,也不硬塞。
4.3 上下文污染:AI 被旧信息带偏
比“检索错误”更烦的是“上下文污染”。当你的工作记忆或摘要里保留了一段已经被推翻的结论,模型会在之后几轮里反复参考这段过时信息。最典型的是改需求:上午说用方案 A,下午改成方案 B,但摘要里还留着 A 的详细理由。模型重新读到摘要时,会觉得 A 也是候选方案,在两套方案之间摇摆。
针对这个问题,我在 facts 结构里加了状态标记,每条关键事实要么是“active”,要么是“superseded”,并在压缩时保留一条“已被方案 B 取代”的显式记录。同时在push_message里增加了“事实更新”接口,当检测到用户明确说“不要 X / 改成 Y”时,把旧的 fact 标记为失效。这个逻辑看起来简单,但它直接决定了多轮对话中模型对“最新决定”的敏感度。
4.4 多轮对话中的角色崩坏
还有一个很容易被忽略的坑:当上下文里同时存在系统指令、文档片段、多轮历史时,模型偶尔会“角色混乱”。比如你本来让它当客服助手,结果它突然开始模仿某篇文档里的技术作者口吻,或者用文档里的第二人称来称呼用户。这是因为拼接时不同来源的文本边界没有做清楚,模型分不清“哪些话是对它说的”“哪些话是要理解的材料”。
解决办法是在拼接时给不同来源的内容加显式标签,我在代码里写入的“参考资料:”和“关键事实:”就是一种给模型做信号标记的方式。更彻底的做法是用 XML 风格的块标签,比如<context source="doc_123">...</context>,让模型明确区分“指令”和“材料”两种语言。我实测下来,加上这些边界标记之后,角色错乱出现的频率明显下降。
5. context-mode 的进阶玩法与实际场景复盘
5.1 AI 编程场景下的 context-mode
如果你主要用 AI 写代码,会发现context-mode的思路同样适用。现在主流的 AI 编程工具里,常见有三种工作模式:纯对话模式、上下文模式、代理执行模式。纯对话模式只是聊,代理执行模式让 AI 自己改文件跑命令,而“上下文模式”则介于两者之间——它的重点不是写代码,而是先帮你把代码库结构、关键文件、相关函数调用链读清楚,再回答具体问题。
我用下来最顺的方式是:先让 AI 扫描项目目录,生成一个结构清单;然后针对要改的功能点,明确告诉它“只关注 controller 层和 service 层,不深入 model 层”。这其实就是手动管理最短上下文。而如果要让 AI 高效干活,最好是给它一个“当前任务上下文”文件,里面写清楚项目背景、技术栈、相关文件路径、测试命令,并且每次会话开始时把这份文件注入到开头,比现场让 AI 慢慢猜高效得多。
5.2 团队知识库问答场景
另一个我实际做过的是团队知识库问答机器人。这个场景下 context-mode 要额外考虑权限和时效性。同样的文档,A 组能看,B 组不能看,检索时就不能只按关联度返回,必须先过滤权限再进入候选集。时效性方面,知识库里的制度文档经常更新,如果摘要层缓存了旧内容,用户会拿到过期答案。我当时的做法是给每份文档加一个版本号,每次生成摘要时带上“当前版本”字段,凡是版本不匹配的片段,宁可重新检索也不使用缓存摘要。
整个系统上线后有个很有意思的现象:用户反馈“这个机器人好像分得清轻重”。比如同样一个问题,在不同部门上下文里会给出不同侧重,销售组偏向活动规则,技术组偏向接口细节。这是因为我在注入时不是把命中的全部摘录塞进去,而是在 system 部分写了“当前用户所属部门为技术部,请优先关注开发相关内容”。一个简单的字段,让同样的检索结果回答了不同的问题。
5.3 几个配置模板
最后分享三个可以直接抄作业的配置模板,适用三种常见场景。
轻量对话场景,适合客服闲聊或简单问答:
系统提示词 + 当前会话目标 关键事实(通常是用户身份、订单号、问题类型) 最近 6-8 轮对话原文文档问答场景,适合知识库/PDF 阅读:
系统提示词(强调引用来源) 检索到的 Top 5 片段(每条带文件名与章节) 当前用户原始问题代码助手场景,适合项目仓库辅助:
项目背景说明 + 技术栈清单 相关文件路径列表(按重要性排序) 当前任务的约束条件(“不要改动测试文件”) 终端报错或最新执行结果 最近 3 轮对话这三个模板的共同点,都是把“当前目标”放在最前、“原始材料”放中间、“最新反馈”放最后。你可以按自己的场景调比例,但顺序最好不要乱。我遇到过很多人把用户问题放到中间,结果模型回答时总是慢半拍,调整顺序后立刻就好了。
这个内容后续还能扩展的方向很多,比如根据用户历史行为做个性化上下文权重、把 context-mode 和长期记忆数据库结合、在压缩时用更强的模型保留更多决策信息。但不管往哪个方向走,核心思路都是同一句话:不要试图让模型记住所有东西,而是主动帮它决定记住什么。我自己的习惯是把上下文当成产品的一部分来设计,而不是出了问题再修补。最开始我做 context-mode 只是为了解决 AI 失忆,后来发现它直接改变了使用者对工具的信任度——一个记得住关键的 AI,和一个总在反复询问的 AI,用户在心理上完全当成两种东西。建议你从最小的会话状态压缩做起,跑通之后再慢慢加检索、加权限、加记忆分层,这条路是可以越走越顺的。