1. 从“上下文模式”说起:一个被低估的工程概念
第一次看到“context-mode”这个词,很多人会以为是某个新出的框架或者库的名字。其实不是。它更像是一种设计思路、一种架构层面的取舍策略,核心问题只有一个:当系统需要处理的信息越来越多时,到底该把哪些信息放进当前的处理上下文里,哪些该被剥离出去?
这个问题听起来很抽象,但你只要写过稍微复杂一点的应用就能体会到。比如你做一个对话系统,用户聊了二十轮,每一轮都往上下文里塞,很快就会发现响应变慢、成本飙升、关键信息反而被淹没。再比如你做一个前端状态管理,组件层级一深,props 层层传递,最后连自己都忘了哪个状态是从哪儿来的。这些问题的本质都是一样的:上下文膨胀。
“context-mode”要解决的就是这个问题。它不是一个具体的代码库,而是一套关于如何组织、裁剪、传递上下文的工程方法论。适合谁看?如果你正在做对话系统、Agent 应用、复杂前端状态管理,或者任何需要维护“当前处理状态”的系统,那这套思路对你直接有用。如果你只是写简单的 CRUD,那可以先了解一下,等遇到瓶颈再回来翻。
我自己在几个项目里踩过上下文失控的坑,后来慢慢总结出一套还算好用的模式,下面完整拆开讲。
2. 上下文模式的核心设计思路拆解
2.1 为什么不能把所有信息都塞进上下文
先算一笔账。假设你做一个对话应用,每轮对话平均产生 200 个 token 的内容,用户聊了 50 轮,那就是 10000 个 token。这还只是对话历史,没算系统提示、工具描述、检索到的文档片段。如果再加上这些,轻松突破 30000 token。按当前主流模型的定价,每千 token 几分钱到几毛钱不等,单次请求成本可能就到几毛甚至一块。一天一千次请求,成本就是几百块。
但这还只是钱的问题。更麻烦的是注意力稀释。模型处理长上下文时,并不是每个 token 都同等重要。当上下文里塞了大量无关信息,真正关键的那几句反而容易被忽略。我实测过一个场景:把用户最新问题放在 8000 token 历史的最后,和放在最前面,回答准确率能差 15% 以上。这不是模型不行,是上下文组织方式的问题。
所以“context-mode”的第一个核心原则就是:上下文不是越多越好,而是越精准越好。每一段放进上下文的信息,都要能回答“它为什么必须在这里”。
2.2 三种典型的上下文模式及其适用场景
在实际工程中,我总结出三种最常用的上下文模式,它们不是互斥的,而是可以根据场景组合使用。
第一种:滑动窗口模式。只保留最近 N 轮对话或最近 M 条记录,更早的直接丢弃。这是最简单的策略,实现成本极低,适合对历史依赖不强的场景,比如简单的问答机器人、实时翻译。缺点是会丢失早期的重要信息,用户如果前面说了“我对花生过敏”,聊了三十轮之后又点了一份含花生的菜,系统可能就忘了。
第二种:摘要压缩模式。不丢弃历史,而是把早期内容压缩成摘要。比如每十轮对话生成一段 100 字以内的摘要,保留关键事实和意图。这样上下文长度可控,同时不丢失核心信息。适合长对话、客服系统、个人助理这类需要记住用户偏好的场景。实现上可以用一个小模型做摘要,或者用规则提取关键实体。
第三种:检索增强模式。把所有历史信息存到外部存储,每次请求时根据当前问题检索最相关的片段,只把检索结果放进上下文。这是目前 Agent 类应用最常用的方式。优点是理论上可以处理无限长的历史,缺点是检索质量直接决定效果,检索不准就会答非所问。
这三种模式的选择,本质上是在成本、延迟、信息完整性三者之间做权衡。滑动窗口成本最低但信息损失最大,检索增强信息最全但延迟最高。我的经验是:短对话用滑动窗口,长对话用摘要压缩,知识密集型任务用检索增强。
2.3 上下文模式与状态管理的本质联系
很多人把“context-mode”只理解成对话系统的优化技巧,其实它的适用范围远不止于此。任何需要维护“当前处理状态”的系统,都面临同样的上下文管理问题。
举个例子。前端开发中,一个复杂的表单页面可能有几十个字段,每个字段的状态都放在顶层组件里,然后层层往下传。这就是典型的上下文膨胀。后来大家用 Context API 或者状态管理库,本质上就是在做“上下文模式”的优化:把状态放到一个集中的地方,组件按需订阅,而不是全量传递。
再比如后端的工作流引擎。一个订单处理流程有十几个步骤,每个步骤都需要读取和修改订单状态。如果把整个订单对象在每个步骤之间传来传去,代码会变得极其臃肿。更好的做法是定义一个“处理上下文”,只包含当前步骤需要的最小字段集,步骤完成后把结果写回主存储。
所以“context-mode”的本质是:定义清楚当前处理单元需要的最小信息集,并建立一套机制来动态组装这个信息集。这个思路在对话系统、前端状态、后端工作流、甚至游戏 AI 的行为树里都是通用的。
3. 核心细节解析与实操要点
3.1 上下文裁剪的优先级规则怎么定
裁剪上下文不是随便删,得有优先级。我一般按这个顺序从高到低排:
- 系统指令和约束:绝对不能删,这是行为边界。
- 当前任务的直接输入:用户最新问题、当前步骤的输入参数。
- 与当前任务强相关的历史信息:比如用户刚提到的偏好、刚确认的事实。
- 近期对话历史:最近几轮的完整内容。
- 早期对话的摘要:压缩后的关键信息。
- 检索到的相关文档片段:按相关度排序,只取 top K。
- 工具描述和示例:如果工具很多,只保留当前可能用到的。
这个优先级不是固定的,要根据场景调整。比如做代码生成时,工具描述和示例的优先级要往上提;做闲聊时,近期对话历史的权重更高。
实操中有一个容易忽略的点:裁剪的粒度。是按轮次裁,还是按 token 数裁,还是按语义单元裁?我的建议是按语义单元裁。一轮对话可能包含多个意图,如果整轮保留或整轮丢弃,都会造成信息损失。更好的做法是把每轮对话拆成“用户意图 + 关键实体 + 系统响应”三个部分,分别决定保留还是压缩。
3.2 摘要压缩的质量控制要点
摘要压缩听起来简单,做起来坑很多。最大的坑是摘要丢失关键细节。比如用户说“我下周三下午三点要去机场,帮我订个车”,摘要如果写成“用户要订车”,那就把时间、地点全丢了,后面根本没法用。
我试过几种摘要策略,最后稳定下来的做法是结构化摘要。不写自然语言段落,而是用固定的字段来记录:
- 用户意图:订车
- 时间:下周三 15:00
- 地点:机场
- 状态:待确认
这样每次生成摘要时,模型只需要填充字段,不会漏掉关键信息。而且后续检索时,可以直接按字段匹配,比语义检索更准。
另一个要点是摘要的更新频率。不要每轮都重新生成摘要,那样成本太高。我的做法是每 5 到 10 轮更新一次,或者当检测到新意图、新实体时触发更新。更新时把旧摘要和新内容一起输入,生成新摘要,而不是从头生成。
注意:摘要压缩会引入信息损失,所以对于关键事实(如用户身份、过敏信息、金额),不要只存摘要,要在外部存储里保留原始记录,需要时直接查。
3.3 检索增强中的召回与排序策略
检索增强模式的效果,八成取决于召回质量。我见过太多项目,模型能力很强,但检索出来的东西驴唇不对马嘴,最后回答质量很差。
召回策略上,我一般用混合检索:关键词匹配 + 向量相似度。关键词匹配保证精确命中,向量相似度保证语义相关。两者结果合并后去重,再交给排序模型。
排序环节,除了相关度分数,还要考虑时效性和重要性。比如用户三天前说的偏好,和刚才说的偏好,权重应该不一样。我的做法是给每条记录打一个“新鲜度”分数,越近的分数越高,排序时相关度占 70%,新鲜度占 20%,重要性占 10%。这个比例可以根据场景调。
还有一个实操技巧:检索结果不要直接塞进上下文,先做一次过滤。把明显不相关的、重复的、过长的片段去掉。我一般限制每个片段不超过 200 字,总检索内容不超过上下文预算的 40%。剩下的预算留给系统指令和当前输入。
3.4 上下文预算的分配与动态调整
上下文窗口是有限资源,得学会分配。我通常按这个比例来分:
| 组成部分 | 预算占比 | 说明 |
|---|---|---|
| 系统指令 | 10% | 固定不变 |
| 当前输入 | 15% | 用户最新问题或任务输入 |
| 近期历史 | 25% | 最近 3-5 轮完整内容 |
| 摘要信息 | 15% | 早期历史的压缩结果 |
| 检索内容 | 30% | 按相关度排序的文档片段 |
| 预留缓冲 | 5% | 防止意外溢出 |
这个比例不是死的。如果当前输入很长(比如用户贴了一大段代码),就要压缩其他部分。如果检索内容质量很高,可以适当提高检索占比。
动态调整的关键是实时监控 token 消耗。每次组装上下文前,先算一下各部分占了多少 token,如果超了,就按优先级从低到高裁剪。我一般会留 5% 的缓冲,因为 token 计算本身有误差,不同模型的分词方式也不一样。
4. 实操过程与核心环节实现
4.1 搭建一个最小可用的上下文管理器
下面用一个 Python 示例来演示核心逻辑。这个管理器负责组装上下文、裁剪超长部分、调用模型。
class ContextManager: def __init__(self, max_tokens=8000): self.max_tokens = max_tokens self.system_prompt = "" self.recent_history = [] self.summary = "" self.retrieved_docs = [] def set_system_prompt(self, prompt): self.system_prompt = prompt def add_history(self, role, content): self.recent_history.append({"role": role, "content": content}) # 保持最近 5 轮 if len(self.recent_history) > 10: self.recent_history = self.recent_history[-10:] def set_summary(self, summary): self.summary = summary def set_retrieved_docs(self, docs): self.retrieved_docs = docs def _count_tokens(self, text): # 简化估算:中文约 1.5 字/token,英文约 4 字符/token # 实际项目应使用对应模型的分词器 return len(text) // 2 def build_context(self, current_input): parts = [] budget = self.max_tokens # 1. 系统指令 sys_tokens = self._count_tokens(self.system_prompt) parts.append({"role": "system", "content": self.system_prompt}) budget -= sys_tokens # 2. 当前输入 input_tokens = self._count_tokens(current_input) budget -= input_tokens # 3. 摘要 if self.summary: sum_tokens = self._count_tokens(self.summary) if sum_tokens < budget * 0.3: parts.append({"role": "system", "content": f"历史摘要:{self.summary}"}) budget -= sum_tokens # 4. 检索内容 for doc in self.retrieved_docs: doc_tokens = self._count_tokens(doc) if doc_tokens < budget * 0.5: parts.append({"role": "system", "content": f"参考信息:{doc}"}) budget -= doc_tokens else: break # 5. 近期历史(从新到旧,直到预算用完) history_parts = [] for msg in reversed(self.recent_history): msg_tokens = self._count_tokens(msg["content"]) if msg_tokens < budget: history_parts.insert(0, msg) budget -= msg_tokens else: break parts.extend(history_parts) # 6. 当前输入放最后 parts.append({"role": "user", "content": current_input}) return parts这段代码的核心逻辑就是按优先级组装,预算不够就从低优先级开始砍。实际项目中,_count_tokens要换成对应模型的分词器,比如 tiktoken 或者 HuggingFace 的 tokenizer。
4.2 摘要生成的具体实现
摘要生成我一般用一个小模型来做,成本低、速度快。提示词这样写:
SUMMARY_PROMPT = """请从以下对话中提取关键信息,按以下格式输出: - 用户意图:[一句话概括] - 关键实体:[列出所有提到的人名、地点、时间、数字] - 待办事项:[如果有] - 重要约束:[如过敏、偏好、限制条件] 对话内容: {conversation} 摘要:"""每 5 轮调用一次,把旧摘要和新对话一起输入,生成新摘要。这样摘要会越来越精炼,但关键信息不会丢。
4.3 检索模块的搭建要点
检索模块我一般用向量数据库加关键词索引的组合。向量部分用现成的 embedding 模型,关键词部分用简单的倒排索引。
import numpy as np from sklearn.metrics.pairwise import cosine_similarity class HybridRetriever: def __init__(self, embedding_model): self.embedding_model = embedding_model self.documents = [] self.embeddings = [] self.keyword_index = {} def add_document(self, doc_id, text): self.documents.append({"id": doc_id, "text": text}) emb = self.embedding_model.encode(text) self.embeddings.append(emb) # 简单关键词索引 for word in text.lower().split(): if word not in self.keyword_index: self.keyword_index[word] = [] self.keyword_index[word].append(doc_id) def retrieve(self, query, top_k=5): # 向量检索 query_emb = self.embedding_model.encode(query) sims = cosine_similarity([query_emb], self.embeddings)[0] vector_results = np.argsort(sims)[-top_k*2:][::-1] # 关键词检索 query_words = query.lower().split() keyword_ids = set() for word in query_words: if word in self.keyword_index: keyword_ids.update(self.keyword_index[word]) # 合并去重 results = [] seen = set() for idx in vector_results: doc = self.documents[idx] if doc["id"] not in seen: results.append(doc) seen.add(doc["id"]) for doc in self.documents: if doc["id"] in keyword_ids and doc["id"] not in seen: results.append(doc) seen.add(doc["id"]) return results[:top_k]这个实现很粗糙,但核心思路是清楚的:向量保语义,关键词保精确,合并去重后取 top K。实际项目中可以用更成熟的向量数据库,但逻辑是一样的。
4.4 完整调用流程与参数记录
把上面几个模块串起来,一次完整的请求处理流程是这样的:
- 接收用户输入,记录时间戳。
- 调用检索模块,获取相关文档片段。
- 检查是否需要更新摘要(每 5 轮或检测到新意图时)。
- 调用上下文管理器,组装最终上下文。
- 调用模型,获取响应。
- 把本轮对话加入历史,更新存储。
- 记录本次请求的 token 消耗、延迟、检索命中率等指标。
参数记录很重要。我一般会记录:上下文总 token 数、各部分占比、检索召回数量、模型响应时间、用户反馈(如果有)。这些数据积累下来,就能看出上下文模式的效果,方便后续调优。
5. 常见问题与排查技巧实录
5.1 上下文模式落地中的典型问题速查
| 问题现象 | 可能原因 | 排查方向 | 解决方法 |
|---|---|---|---|
| 模型回答忽略早期信息 | 摘要丢失关键细节 | 检查摘要字段是否完整 | 改用结构化摘要,关键事实单独存储 |
| 响应变慢、成本飙升 | 上下文膨胀 | 统计每次请求 token 数 | 设置硬预算,超了强制裁剪 |
| 检索结果不相关 | 召回策略单一 | 检查关键词和向量权重 | 混合检索,加排序模型 |
| 多轮对话后意图漂移 | 历史信息干扰 | 检查近期历史是否过长 | 缩短近期历史窗口,提高摘要权重 |
| 工具调用失败 | 工具描述被裁剪 | 检查工具描述优先级 | 把工具描述提到高优先级 |
| 重复回答同一问题 | 检索到重复内容 | 检查去重逻辑 | 加去重步骤,按内容哈希过滤 |
5.2 三个我踩过的坑和对应的解法
第一个坑:摘要更新太频繁导致成本失控。一开始我每轮都重新生成摘要,结果摘要生成的调用量比主模型还大。后来改成每 5 轮更新一次,并且只在检测到新实体或新意图时才触发,成本直接降了 60%。
第二个坑:检索内容太长挤占了对话历史。有一次检索到一篇很长的文档,直接塞进去占了 70% 的上下文,结果模型完全忽略了用户刚才说的话。后来我加了限制:每个检索片段不超过 200 字,总检索内容不超过预算的 40%。如果文档太长,就先做一次摘要再放进去。
第三个坑:不同模型的分词方式不一样,token 计算不准。我一开始用字符数除以 2 来估算,结果换了一个模型后,实际 token 数比估算多了 30%,导致上下文溢出。后来老老实实用对应模型的分词器,虽然慢一点,但准确。
5.3 性能与成本的平衡技巧
上下文模式调优,本质上是在性能和成本之间找平衡点。我总结了几条实用技巧:
- 分级处理:简单问题用滑动窗口,复杂问题才启用检索增强。不要所有请求都走最重的流程。
- 缓存摘要:摘要生成后缓存起来,下次请求如果历史没变,直接复用。
- 异步检索:检索可以和模型调用的其他准备工作并行,减少延迟。
- 预算硬限制:设置一个硬性的 token 上限,超了直接裁剪,不要试图“再塞一点”。
- 监控告警:对 token 消耗、检索命中率、响应延迟设置告警,异常时及时排查。
提示:上下文模式不是一成不变的,要根据实际数据持续调优。建议每周回顾一次指标,看看哪些参数需要调整。
6. 上下文模式的扩展应用与个人体会
6.1 从对话系统到更广泛的场景
“context-mode”这套思路,我后来发现可以用在很多地方。比如做代码补全时,上下文就是当前文件的内容、光标位置、最近编辑历史。不是把所有文件都塞进去,而是根据光标位置检索相关代码片段。再比如做推荐系统时,上下文就是用户最近的行为序列,不是全部历史,而是最近 N 次交互加上长期兴趣的摘要。
甚至在做项目管理时,这套思路也能用。一个项目有几百个任务,当前迭代只需要关注最近两周的任务和相关依赖,更早的用摘要记录。这样每天的站会就不会被无关信息淹没。
核心思想是一样的:定义当前处理单元的最小信息集,动态组装,按需检索,定期压缩。
6.2 我个人的几条经验
第一,不要追求完美上下文。你永远不可能把所有相关信息都放进上下文,接受这一点,然后专注于把最重要的放进去。
第二,摘要要结构化,不要自然语言。自然语言摘要看起来漂亮,但容易丢信息,而且后续检索不好用。结构化字段虽然死板,但可靠。
第三,检索质量比模型能力更重要。我见过太多项目,模型换了一个又一个,效果提升不明显,最后发现是检索出来的东西不对。先把检索做好,再考虑换模型。
第四,监控比调优更重要。没有数据支撑的调优都是瞎猜。把 token 消耗、检索命中率、用户反馈这些指标记录下来,调优才有方向。
这套上下文模式我用了大半年,从最初的滑动窗口一路迭代到现在的混合模式,中间踩了不少坑,但整体效果是值得的。如果你也在做类似的事情,希望这些经验能帮你少走点弯路。