1. 什么是 context-mode它到底在解决什么问题先说说我为什么会盯上这个概念。过去两年我一直在做 AI 应用相关的开发从最早的 API 调用、提示词调优到后来的 Agent 编排、知识库问答几乎每个项目都会遇到同一个问题模型记不住之前说过什么。用户问了两三轮之后模型就开始答非所问或者干脆把前面对话里的关键信息忘得一干二净。这个场景你一定不陌生——跟 AI 聊天聊到第 5 句它还在聊到第 20 句它就糊涂了。问题不在模型本身而在于我们没有把上下文当回事。context-mode简单说就是一套管理、组织、传递上下文信息的工作模式。它不是一个单一的技术而是一组策略的集合决定什么信息该进入模型的上下文、以什么形式进入、保留多长时间、超了限制怎么办。这套逻辑在文档里通常被笼统地称为上下文管理但在真实工程里你会发现做得好的团队基本都是靠一套明确的 context-mode 机制在运作。这个机制能解决什么问题往小了说它让 AI 在长对话里保持记忆连贯往大了说它直接决定你的 AI 应用是玩具还是产品。我见过太多团队模型换了最强的、API 费用烧得飞快但用户反馈依然是AI 像个傻子。根子就在 context 策略上。这篇文章我会把 context-mode 的底层原理、主流实现方式、以及我实际落地的一套完整方案都拆开讲适合正在做 AI 应用、做知识库问答、做 Agent 开发的工程师参考。如果你只是对 AI 好奇也能从中理解为什么 ChatGPT 和 Claude 这类产品会给你越聊越懂你的体验。2. 拆开 context-mode 的底层原理要理解 context-mode必须先弄明白模型处理上下文的机制。这里的核心单位是 token。平时我们说的上下文窗口本质就是一个 token 数量上限——模型一次能看到的文本长度不能超过这个上限。以主流的商用模型为例窗口规格从 4K、8K到 128K、200K 不等看起来差距巨大但实际使用中真正能有效利用的远没有宣传的那么乐观。2.1 上下文窗口不等于有效记忆很多人有个误区只要窗口够大就能把整个对话和历史文档全塞进去。理论上没错但工程上完全不是这么回事。第一窗口越大输入成本越高API 按 token 计费塞进去的每一丁点内容都在烧钱第二模型对长上下文的注意力会衰减这是 transformer 架构的固有特性——距离当前问题越远的内容被模型想起来的权重就越低。我实测过当上下文长度超过 100K token 时模型回答靠前部分信息的准确率会明显下降这不是玄学是注意力分布的自然结果。所以 context-mode 的第一个核心任务是在有限的窗口里决定什么内容值得占位置。我自己的经验是用三层筛选逻辑来做这个决策相关性层内容与当前用户问题的语义关联有多强时效性层内容是不是最近产生的越近的对话信息优先级越高重要性层某些内容比如用户明确指定的事实、系统指令里强调的规则无论多久都要保留这三层逻辑听起来简单但真正落地时每一步都有讲究。相关性判断不能只靠关键词匹配至少要用 embedding 语义检索时效性要结合对话轮次和真实时间戳双重判断重要性则需要你在指令设计阶段就给系统提示词里的内容设定不可覆盖的标记。2.2 token 计算context-mode 的地基所有上下文管理策略第一步都是精确计算 token。这里我要特别提醒一个新手几乎必踩的坑不要用 API 返回的 usage 字段作为唯一依据更不要自己按字符数估算。中文环境下不同分词器的 token 切分差异很大同一个句子在不同的模型编码器下token 数能相差 30% 以上。我建议的做法是在当前模型的官方 tokenizer 基础上封装一层本地计算服务。如果你用的是 OpenAI 兼容接口就用 tiktoken 库配合对应的编码器名称来做预计算。为什么要本地算因为你在做上下文裁剪、滑动窗口调整的时候需要高频、零成本地估算 token 量每次都调 API 既不现实也白白花钱。我封装过一个小工具核心代码大致是这样import tiktoken def count_tokens(text: str, model: str gpt-4) - int: try: encoder tiktoken.encoding_for_model(model) except KeyError: encoder tiktoken.get_encoding(cl100k_base) return len(encoder.encode(text)) # 示例估算一段对话历史的总 token conversation [ {role: user, content: 帮我查一下上季度的销售数据}, {role: assistant, content: 好的我找到了相关报表需要我详细解读吗}, ] total sum(count_tokens(msg[content]) for msg in conversation) print(total)这里有一个细节不同模型的编码器不同gpt-4 和 gpt-3.5 用的是 cl100k_baseClaude 系列则是自己的 tokenizer。所以封装的时候一定要做模型到编码器的映射表别图省事一股脑全用同一个。token 计算准确了后面的所有管理策略才有意义。2.3 注意力衰减为什么塞得越多效果越差刚才提到注意力衰减我再展开说下这个现象的本质。Transformer 模型在计算注意力时每个 token 会跟序列里的其他 token 做相关性打分但这个打分机制存在近因偏差——距离越远的 token在位置编码和层叠计算中被稀释得越厉害。你可以把它理解成一个开会开了一整天的人上午讲的细节到下午已经只剩模模糊糊的印象除非那件事特别重要被打了个标签否则很难精准回忆。这对 context-mode 的启示是不要试图让模型硬记而是让它在需要时能看到足够清晰的笔记。所以好的上下文管理不是把原始对话一股脑塞进去而是做内容重构——把长对话中真正有价值的信息抽取出来重新组织成精炼的摘要或结构化数据再放入上下文。这相当于帮模型做了一轮笔记整理让它在有限的注意力预算里把力气花在最值得关注的内容上。3. context-mode 的三种主流实现策略理解了底层原理之后我们来看实际工程中常用的三种 context-mode 策略。这三种不是互斥的成熟项目里往往是组合使用只是侧重点不同。3.1 滑动窗口模式简单直接但不够聪明这是最基础、也是最多人在用的方案。思路很简单维护一个对话历史队列只保留最近 N 轮或者最近 M 个 token的内容超过部分直接丢弃。实现上就是一个先进先出队列我见过不少项目用 deque 就能搞定from collections import deque class SlidingWindowContext: def __init__(self, max_rounds: int 10): self.max_rounds max_rounds self.history deque(maxlenmax_rounds) def add_message(self, message: dict): self.history.append(message) def get_context(self) - list: return list(self.history)滑动窗口最大的优点是实现成本极低几乎不会出错。但缺点也很致命它没有内容价值的概念。用户在第 3 轮提过一个关键事实比如我的预算是 5 万到第 12 轮已经被挤出去了模型完全不知道这个约束给出的建议全部偏离方向。实测下来这个模式只适合对话轮次少、关键信息密度低的场景比如单次客服咨询。3.2 语义压缩模式把历史提炼成摘要为了弥补滑动窗口的失忆问题语义压缩模式出现了。核心做法是当对话历史超过阈值时不是简单丢弃旧消息而是调用模型把旧内容总结成精炼摘要然后带着摘要继续对话。这相当于人类在会议中途叫秘书把上午的讨论整理成一份要点下午我们按这个来。我实现过一个简单的压缩逻辑思路如下设定一个触发阈值比如累计 6000 token超过后把最早的一半消息打包发给模型做摘要然后删掉原文、保留摘要继续对话。这个方案比滑动窗口聪明得多能保留散落在长对话里的重要事实。但要注意几个坑摘要本身也要占 token不能无限压缩通常我会给摘要设一个上限比例比如原始长度的 20%压缩是有损的压缩频率越高信息丢失越严重。我踩过最惨的一次用户在第 20 轮提到取消周三的预约到第 25 轮我问周三的安排是什么模型完全答不上来因为在某次压缩时这个信息被概括遗漏了要减少这种损失我建议在摘要指令里强制要求保留用户明确表达的事实、数字、日期、决策——这些是压缩过程中最不该丢的硬信息。3.3 RAG 增强模式与外部知识的协作第三种策略是 RAG检索增强生成它解决的问题不是对话历史太长而是模型知识库之外的信息怎么进上下文。典型的场景是企业知识库问答用户问的是公司内部的规章制度模型没学过你得先从向量数据库里检索出相关片段拼进 prompt 里让它回答。RAG 模式的 context-mode 核心是按需取用。对话窗口多少不重要重要的是你每次递进给模型的内容是不是用户当前问题真正需要的。我做过一个完整的企业知识库助手流程是用户提问 - 用 embedding 对整个文档库做向量检索 - 找出 top 5 相关片段 - 拼接到系统提示词后面 - 让模型基于片段回答。整个过程中上下文里只出现和问题相关的信息无关文档一概不占用。这里有个关键的度的问题检索出来的片段越多模型回答越全面但被不相关片段干扰的概率也越高。我实测的结果是5 到 8 个片段每个 400 到 600 token是性价比最高的区间。少于 3 个信息可能不全多于 10 个模型容易东拉西扯把不相关的检索结果也硬融进回答里。4. 实操过程搭建一套带 context-mode 的问答系统下面把我最近做的一个项目完整复盘一遍。这个项目的需求是企业内部智能问答用户可以通过对话查规章制度、提交请假申请。核心需求就是三句话对话要连贯、知识要准确、费用不能失控。这三点逼着我把三种 context-mode 策略整合到了一套流程里。4.1 整体架构设计与选型理由先交代技术选型。我用的是 OpenAI 兼容的 API国内可用渠道embedding 模型选用 text-embedding-3-small向量数据库用 Chroma因为部署轻量、不依赖云端服务适合企业内部离线场景。对话模型选的是 gpt-4o-mini精度够用、成本可控。架构上分四个模块输入处理、上下文管理、知识检索、模型调用。上下管理模块是核心它内部维护两个结构——短期消息队列和长期摘要缓存。短期队列保存最近 6 轮对话原文用于保证实时连贯性长期摘要缓存保存前面所有内容的压缩摘要用于兜底重要信息。这套组合的本质是短期内让模型看到原文长期内让模型看到提炼后的要点。选择这种拆分而不是直接用一个超大窗口我的考虑很简单成本。gpt-4o-mini 输入价格虽然不高但企业内部问答每天几千次调用每次多塞 2000 token一个月下来也是一笔不小的开销。而滑动窗口加摘要的组合能把单次请求的 token 消耗控制在一个稳定区间费用可以提前预算。实测下来平均单次请求 token 从长窗口模式的 12000 降到了 6500 左右几乎腰斩。4.2 核心代码实现上下文管理模块这个模块是系统的调度核心我把它独立成了一个类方便复用和测试。先看关键实现import json from typing import List, Dict, Any class ContextManager: def __init__(self, max_rounds: int 6, max_total_tokens: int 6000): self.short_history [] # 最近对话原文 self.summary # 长期摘要 self.max_rounds max_rounds self.max_total_tokens max_total_tokens def add_user_message(self, content: str): self.short_history.append({role: user, content: content}) def add_assistant_message(self, content: str): self.short_history.append({role: assistant, content: content}) self._maybe_compress() def _maybe_compress(self): # 估算当前总 token current_tokens self._estimate_tokens() if current_tokens self.max_total_tokens: return # 触发压缩把最早一半的消息合并后做摘要 # 这里会调用 LLM 完成摘要实际项目中单独封装 messages_to_summarize self.short_history[: len(self.short_history) // 2] self.summary self._generate_summary(messages_to_summarize) self.short_history self.short_history[len(self.short_history) // 2:] def _estimate_tokens(self) - int: total 0 for msg in self.short_history: total count_tokens(msg[content]) if self.summary: total count_tokens(self.summary) return total def build_prompt(self, user_query: str) - List[Dict[str, str]]: messages [] if self.summary: messages.append({ role: system, content: f以下是对历史对话的总结供参考\n{self.summary} }) messages.extend(self.short_history) messages.append({role: user, content: user_query}) return messages这段代码有几个关键设计点值得细说。压缩触发时机我选的是每次助手回复之后因为这时候对话完整地多了一轮是检查长度的好时机。压缩时选择最早一半消息做摘要保留了最近的精华内容不动的策略是为了减少最近信息被压缩工具遗漏的概率。summary 以系统角色放入 prompt为的是让模型把它当作全局背景知识来参照而不是当作一段普通对话内容来回应。实际运行中我还会在 summary 里附上生成时间戳和来源轮次方便排查信息归属。这个细节帮我快速定位过不少问题强烈建议你也加上。4.3 检索增强部分把知识库接到上下文里有了对话上下文管理还不够企业问答的核心是知识库里的文档。这部分我用 Chroma 做向量检索流程拆解如下第一步离线阶段把企业规章制度文档切分成 500 token 左右的片段用 embedding 模型生成向量存入 Chroma。切分粒度很讲究——太小100 token语义容易被截断太大1000 token检索噪音多精确匹配率下降。500 是我在多种切分下对比出来的平衡点好效果需要多试几组不同大小的数据选准确率最高的那个。第二步在线阶段用户提问后先用相同 embedding 模型给问题生成向量在 Chroma 里做相似度检索取 top 5 条相关片段。第三步把这 5 条片段和上下文管理模块生成的 messages 拼在一起调用对话模型。拼接顺序上有个细节知识片段内容放在用户消息之前、系统摘要之后。这样模型在读到用户问题之前已经先接触到了相关的背景资料更利于它形成基于资料回答的思维定式。放后面也不是不行但实测准确率会掉几个点。检索代码核心如下import chromadb class KnowledgeRetriever: def __init__(self, collection_name: str company_docs): self.client chromadb.PersistentClient(path./chroma_store) self.collection self.client.get_or_create_collection( namecollection_name, metadata{hnsw:space: cosine} ) def retrieve(self, query: str, top_k: int 5) - List[Dict]: result self.collection.query( query_texts[query], n_resultstop_k, ) docs [] for idx in range(len(result[documents][0])): docs.append({ content: result[documents][0][idx], metadata: result[metadatas][0][idx], }) return docs检索出来的片段我会做一个简单的去重和相关性过滤如果某条片段的相似度分数低于 0.3直接丢弃。这个阈值是我基于多次测试定的低于 0.3 的片段基本是噪音强行拼进去反而会影响模型判断。4.4 参数调优与效果对比整套系统上线后我做了一轮参数调整这里分享几个最有体感的发现max_rounds 设置 6 轮是最优解。低于 4 轮时用户多问几个问题就断片高于 8 轮时prompt 里的内容太多模型回答速度明显变慢。6 轮加摘要的组合既能保证连贯性又不拖慢速度。摘要压缩时机设置在 6000 token 触发。我试过 4000压缩太频繁摘要满天飞历史信息反复被碾碎和 8000单次请求费用偏高且长上下文注意力衰减明显。6000 是一个费用和信息保留率的平衡点具体数字要根据你的模型调整。知识检索的 top_k 从 3 调到 5准确率从 78% 涨到 86%。继续调到 8 反而因为引入过多噪音掉到 83%。每个知识库都有它的最佳 top_k上线后拿真实问题跑一版对比不同数值下的准确率很有必要。5. 常见问题与排查技巧实录再好的设计上线后总会遇到奇奇怪怪的问题。我把这段时间踩过的坑整理成了一份排查手册遇到类似现象可以按图索骥。5.1 模型开始忘事了怎么定位是哪一环丢的这个问题出现时别急着调模型或者骂 API按顺序排查。第一步先确认消息是否真的发到了模型那里。在调用 API 之前打印完整的 prompt看你期望的关键信息在不在里面。我遇到过好几次明明用户在第 3 轮说了预算 5000 以内第 5 轮问答时 prompt 里根本没有这句话——因为压缩模块把最早的消息摘要后摘要生成时丢了这个数字。这就是压缩损失问题需要在摘要指令里加硬性要求。我写的是务必保留所有具体的数字、日期、人名、产品名、金额上限等硬性事实即使用户没有强调。第二步如果 prompt 里有但模型回答依然不对那就是注意力问题。虽然内容在上下文里但它被埋在了长文本中段模型没能注意到。这时候要调整位置——把核心信息放到 prompt 的开头系统提示词里或结尾靠近用户消息的位置这两个位置是注意力相对集中的区域。中间的容易被忽略这是 transformer 注意力分布的特征。第三步如果位置没问题还可能是检索部分返回了太多不相关片段稀释了关键信息的权重。把检索结果打印出来人工看一下很像。如果是这个问题调低 top_k 或提高相似度阈值。5.2 token 费用突然暴涨该查哪里这是每个读账单时心惊胆战的开发者都会碰到的事。我总结的排查路径是打印每次请求的完整 token 明细对比前后几天同一类型请求的 token 量变化重点检查 summary 的长度是否失控。我出过一次问题摘要模块没有限制输出长度某次长对话压缩出的 summary 竟然有 1800 token直接把预算打崩了。后来我给摘要指令加了总长度不超过 300 字的限制同时也加入了硬性截断逻辑兜底检查是否有重复注入。例如检索模块把同一篇文档的多个片段都放进了上下文而这些片段 90% 的内容重叠白白浪费 token。按文档 ID 去重同一个文档最多放 2 个片段5.3 用户反馈我昨天跟你说过的事你今天就不记得了这种跨会话的记忆context-mode 处理不了因为上下文本来就是单次会话内的概念。解决思路是把记忆持久化——把每次对话的重要信息抽取出来存进数据库下次新会话开始时把与当前用户相关的历史记忆重新注入上下文。我实现过一个轻量方案每轮对话结束后调用模型用固定格式抽取用户画像信息比如偏好、约束、历史决策以 JSON 格式存入 MongoDB。新会话建立时把用户的最近 20 条画像记录拼进系统提示词。这个方案效果好得出乎意料用户明显感受到被记住了。当然也有成本每次抽取画像需要多调一次模型费用增加约 15%换取体验提升是值的。谨慎评估后再决定要不要做。再补充一个容易被忽视的细节上下文管理模块要记录每次压缩的日志包括压缩时间、压缩了哪些消息、生成的摘要原文。出了问题可以直接回溯那句话是在哪次压缩时丢的定位效率翻倍。没有日志的上下文管理系统上线等于裸奔这句话我反复对团队讲过。常见问题根因排查手段解法长对话后模型忘得离谱摘要丢失硬信息检查 prompt 中是否含关键数字、日期摘要指令强制保留硬性事实回答被旧话题带偏旧内容干扰新问题打印完整 prompt 人工检查提升短期历史轮次、压缩更早的内容单次费用超预算summary 过长或片段重叠查看 token 明细限制摘要字数、按文档 ID 去重检索出来的内容答非所问相似度阈值过低打印检索分数提高阈值到 0.3 以上新会话不记得用户信息上下文未持久化检查数据库画像记录增加记忆抽取与注入流程6. 一些基于我实际经验的小结与建议说句实在话context-mode 这个概念并不存在一条万能的配置曲线它的每一项策略选择都绑定在我们对具体业务场景的理解上。比如我给知识库问答做的方案如果换到一个自由闲聊型应用上摘要压缩策略就要推翻重来——闲聊内容更冗余更需要控制成本但信息准确性要求低很多可以压缩得更狠。我的一个体会是最先要想清楚的是产品和用户场景然后设计上下文管理的目标是成本的边际、信息的保留率还是响应的时延目标一明确具体策略顺理成章就出来了工程量也没有想象的大。还有个小技巧想分享给大家。上线之后我习惯每两周拿同一组真实用户问题跑一遍回放测试。把这期间用户真实问过的问题重新喂给系统对比模型回答和人工标注的标准答案不断发现 context-mode 状态不佳的地方。有一次复盘发现用户问法里经常出现那个、之前说的这类指代词单纯的历史摘要和检索都接不住这种隐含引用。后来我在预处理层加了一个指代词消解逻辑如果检测到之前说的/上次那个自动把最近的用户消息和助手之前的回答重新拼接强调一次准确率立刻回升。这类问题都是文档里学不到的必须在真实数据里反复碰。context-mode 没有一劳永逸的终点它是个需要持续打磨的环节。好的实现会像一个合格的助手从不打扰你但在你需要的时候永远清楚你之前说过什么、你需要什么。