1. 先搞清楚 context-mode 到底解决什么问题
做 LLM 应用的人,聊着聊着一定会撞上一堵墙:上下文窗口。"窗口不够了"、"上下文被无关信息污染了"、"模型把前面的关键意图忘了",这些问题我踩了不下几十次。context-mode 这个词,说白了就是针对这一类问题诞生的——它不是某个大模型的固有功能,而是一套在应用层实现的上下文管理策略,核心目标是让模型始终拿到"当前最该看的信息",而不是"所有信息"。
我最早接触这个思路是在给一个客服机器人做对话系统的时候。当时直接用"把所有历史消息都塞进 prompt"的笨办法,结果 token 消耗爆炸,回答质量却越来越差。后来把上下文切成"模式"来管理——根据当前用户意图、当前任务阶段、当前会话紧急程度,动态决定塞哪些内容进 prompt、哪些内容压缩、哪些内容直接丢弃——整个系统才真正稳下来。
context-mode 能解决的痛点很具体:
- 长会话场景下,早期的重要信息(用户需求、约束条件)会被淹没在大量寒暄和无效对话里。
- 上下文窗口有硬上限,塞满之后被迫暴力截断,截断的部分往往恰好是模型最需要的。
- 多任务混合的会话中,模型分不清当前到底在做什么,回答经常串台。
- 成本失控,每轮请求都带全量上下文,token 费用呈线性甚至超线性上涨。
这篇文章不是讲某个具体产品的使用说明书,而是把我自己从零搭建 context-mode 调度系统的完整思路、代码实现、踩坑记录都摊开来讲。不管你是做 RAG 应用、Agent 工作流、智能客服、还是本地知识库问答,这套方法都可以直接借鉴。我会尽量少讲抽象概念,多给能抄作业的代码和配置。
2. 核心机制:上下文怎么收集、怎么排序、怎么压缩
2.1 三层上下文结构:全局层、会话层、即时层
context-mode 的第一个关键动作,是给上下文分层。很多人管理上下文是"一个大口袋,什么都往里装",这必然导致优先级混乱。我自己的实践是分成三个层次:
| 层级 | 内容 | 更新频率 | 典型大小 |
|---|---|---|---|
| 全局层 | 系统设定、用户画像、固定业务规则 | 几乎不变 | 1~2K tokens |
| 会话层 | 当前会话的目标、已确认关键信息、任务进度 | 每轮更新 | 2~4K tokens |
| 即时层 | 最近几轮对话、当前提问、临时检索结果 | 每轮重建 | 1~3K tokens |
全局层对应的是"这个 AI 是谁、它在帮谁干活、有什么规则不能违反"。比如客服系统里,全局层就是"你是某品牌的客服助手,退款政策是xxx,不能承诺xxx"。这部分永远放在 prompt 最前面,保证每次调用都在。
会话层是 context-mode 的灵魂。它不存原始对话,而是从原始对话里抽出来的"结构化结论"——用户真正想要什么、已经确认了哪些需求、还有哪些待定项。这就像是会议纪要,开会时候的闲聊不用保留,但结论和待办必须在。
即时层最像传统聊天方案的上下文,直接带上最近几轮对话原文,让模型感知语气和连续性。但它的长度是受控的,通常只保留最近 2~3 轮。
这里有个很反直觉的点:会话层比原始对话更有价值。原始对话里用户说"我希望价格便宜一点,但也不能太便宜,质量要有保障",会话层提炼后就是"用户关注性价比,价格敏感度中等,拒绝低价低质"。提炼后的信息密度高得多,模型不需要从一大段话里自己猜。
2.2 Token 预算分配:给每个模块发"额度"
有了分层,还要有预算。Token 窗口说白了就是内存,不规划就会被各种模块抢光。我常用的分配策略是:
- 总预算设为模型上下文窗口的 60%~70%,预留余量给生成回复和即时输入。
- 全局层固定占 10%,这部分不可压缩。
- 会话层占 30%~40%,根据重要性动态调整。
- 即时层占 20%~30%,最近对话原文和检索结果在这里竞争名额。
实际计算时,我用一个简单的 token 预估函数,中文按"一个字约等于 1~1.5 个 token"来粗估,英文按"一个词约等于 1.3 个 token"。不用精确到个位,上下文裁剪本来就是个近似工程。
预算分配的目的是建立"稀缺感"。当系统知道全局层只能占 1K token,就不会允许某次产品说明塞进 3K 的内容。所有模块都必须证明自己的价值,才能进入 prompt。
2.3 上下文压缩与遗忘策略
压缩和遗忘,是 context-mode 里最容易做砸的部分。很多人一上来就想着"让模型帮我总结上下文",结果每次总结都要额外烧一轮 token,而且总结抽象程度不可控——可能把关键细节丢光,也可能把废话全保留了。
我采用的策略是消息级衰减,给每条消息标一个权重,权重随时间衰减:
- 用户明确的关键指示(带"必须""千万不要""最重要的是"等措辞)权重最高,永久保留。
- 业务操作结果(如"订单已创建,编号20250301")权重次之,保留 10~20 轮。
- 寒暄、确认性的话、语气词("好的""嗯嗯""谢谢")权重最低,直接丢弃。
实际操作里,保留用户的关键指示比保留机器人自己的回答重要得多。模型最怕的是"用户已经说过的东西,被自己生成的平庸回答淹没了"。所以我会对每条机器人回答做清洗,只保留其中有信息量的部分(比如生成的结果、必要的确认信息),其余丢掉。
遗忘策略的核心原则是:宁丢细节,不丢意图。细节丢失了用户可以重新说,意图丢失了模型就开始胡扯。
3. 实操:从零实现一套 context-mode 调度系统
3.1 第一步:定义上下文数据结构和优先级
先定义一个干净的上下文数据结构。我用 Python 实现,核心是 ContextItem 和 ContextManager 两个类:
from dataclasses import dataclass, field from enum import IntEnum from typing import Any, Optional class ContextLevel(IntEnum): GLOBAL = 3 # 全局层,最高优先级 SESSION = 2 # 会话层 RECENT = 1 # 即时层 class ItemType(IntEnum): SYSTEM_RULE = 4 # 系统规则 USER_INTENT = 3 # 用户意图 FACT = 2 # 业务事实 ACTION_RESULT = 1 # 操作结果 CHITCHAT = 0 # 寒暄,最低优先级 @dataclass class ContextItem: content: str level: ContextLevel item_type: ItemType created_round: int # 创建时的会话轮次 last_access: int # 最近一次被"用到"的轮次 weight: float = 1.0 # 基础权重 metadata: dict = field(default_factory=dict) def decayed_weight(self, current_round: int) -> float: # 衰减公式:权重随时间指数衰减,但最低保留 30% age = current_round - self.last_access return max(0.3, self.weight * (0.9 ** age))这里有个细节值得说明:我给每条 Item 单独维护last_access,而不是created_round。因为上下文的价值不取决于"它存在多久",而取决于"它多久没被用到了"。一个在 50 轮前创建、但每一轮都被检索命中的用户需求,应该永远保留;一个 5 轮前创建、再也没用过的临时信息,反而该早点淘汰。
3.2 第二步:实现上下文采集与注入管线
采集管线解决的是"一条新消息进来,它算什么"。这一步用规则 + LLM 辅助的混合方案最稳。
class ContextCollector: def collect(self, message: str, role: str, round_num: int) -> ContextItem: if role == "system": return ContextItem( content=message, level=ContextLevel.GLOBAL, item_type=ItemType.SYSTEM_RULE, created_round=round_num, last_access=round_num, weight=1.0 ) if role == "user": intent_level = self._detect_intent_level(message) # 检测到关键意图词,提升权重 if any(kw in message for kw in ["必须", "千万不要", "最重要的是", "一定"]): return ContextItem( content=message, # 完整保留,不压缩 level=ContextLevel.SESSION, item_type=ItemType.USER_INTENT, created_round=round_num, last_access=round_num, weight=1.0 ) # 短消息(小于15字)且无关键信息,直接降级 if len(message) < 15 and intent_level == 0: return ContextItem( content=f"[简短确认] {message}", level=ContextLevel.RECENT, item_type=ItemType.CHITCHAT, created_round=round_num, last_access=round_num, weight=0.3 ) # 一般消息,进入会话层 return ContextItem( content=self._extract_and_condense(message), level=ContextLevel.SESSION, item_type=ItemType.FACT, created_round=round_num, last_access=round_num, weight=0.8 )_extract_and_condense是采集管线的重头戏。我的做法是先用正则做粗提取(抓数字、订单号、日期、地点、明确的动作词),再用一个轻量 LLM 调用做二次提炼,把消息压成"一句结构化描述"。注意这里用的 LLM 调用很便宜——输入不超过两百字,只要求输出按压后的结论,每次调用成本几乎可以忽略。
注入管线的逻辑刚好反过来:从 ContextManager 里按优先级取 Item,拼成最终的 prompt。
class ContextManager: def __init__(self, max_tokens: int = 8000): self.items: list[ContextItem] = [] self.round = 0 self.max_tokens = max_tokens self.budget = { ContextLevel.GLOBAL: int(max_tokens * 0.1), ContextLevel.SESSION: int(max_tokens * 0.35), ContextLevel.RECENT: int(max_tokens * 0.25), } def add_item(self, item: ContextItem): self.items.append(item) self.round += 1 def build_prompt(self, recent_history: list[dict]) -> str: # 用当前轮次计算每个 item 的衰减权重 scored = [(it, it.decayed_weight(self.round)) for it in self.items] # 按层级优先、权重次优排序 scored.sort(key=lambda x: (x[0].level, x[0].decayed_weight(self.round)), reverse=True) # 按预算逐层拼装 result = [] used = {level: 0 for level in ContextLevel} for item, weight in scored: token_est = estimate_tokens(item.content) if used[item.level] + token_est <= self.budget[item.level]: result.append(item.content) used[item.level] += token_est item.last_access = self.round # 命中即刷新活跃度 # 最后拼上即时层的最近对话 for msg in recent_history[-3:]: result.append(f"{msg['role']}: {msg['content']}") return "\n".join(result)这里有个在线上系统里验证过的细节:排序时层级是主键,权重是次键。全局层永远排在会话层前面,哪怕某条会话层消息的权重是 1.0 而全局层是 0.5。这不是拍脑袋定的——全局层放的是系统规则,规则被挤掉了,整个系统就失控了。权重只能决定"同一层级内谁先进窗口",不能跨层竞争。
3.3 第三步:动态上下文裁剪算法
静态预算有个问题:会话刚开始时,所有消息都新鲜、权重都高,预算很快被塞满;会话进行到中段时,真正重要的信息反而可能被早期积累的冗余挤出去。所以我在预算分配之后,又加了一个动态裁剪环节。
裁剪的核心算法是"多头注意力式筛选"——从当前用户的问题里抽关键词,计算每条上下文与关键词的相关度,然后按相关度调整实际注入顺序和注入比例。
import re from collections import Counter class ContextPruner: def __init__(self): self.stopwords = set("的了在是和我有就" \ "都而及与着或者好吧啊嗯呢呀".strip()) def extract_keywords(self, question: str) -> list[str]: # 粗分词:按常见分隔符切分,过滤停用词 tokens = re.findall(r"[\u4e00-\u9fa5]{2,}|[a-zA-Z0-9]+", question) filtered = [t for t in tokens if t not in self.stopwords] counter = Counter(filtered) return [w for w, _ in counter.most_common(5)] def relevance_score(self, context_item: ContextItem, keywords: list[str]) -> float: if not keywords: return 0.5 # 无关键词时给中性分 hit = sum(1 for kw in keywords if kw in context_item.content) return min(1.0, hit / 2.0)实际运行时,我会把相关度得分乘到衰减权重上,作为最终的排序分值。这样一条 20 轮前的"用户要求次日发货",如果当前用户问"什么时候能到?",它依然能排进前列;如果用户问的是"退款怎么操作",这条旧消息就该让位给退款政策相关内容。
这个机制的灵感来自搜索引擎的 TF-IDF 思路——上下文本质上也是文档集合,当前问题就是查询词。做开发时不用把这个机制想得多玄妙,它就是"让模型每次看到的上下文都跟当前问题最相关"。
裁剪后的总预算再做一次余额检查:如果相关度高的 Item 加总后仍然超出预算,就按从低到高逐条裁剪,优先裁掉CHITCHAT类型,最后才动USER_INTENT类型。
3.4 第四步:接入 LLM 调用层
管线搭好后,接入层反而最简单。关键是把build_prompt()的产物作为系统提示词注入,而不是塞到用户消息里。
def run_llm_call(user_input: str, manager: ContextManager, llm_func, recent_history: list[dict]): # 1. 采集新输入 new_item = collector.collect(user_input, role="user", round_num=manager.round) manager.add_item(new_item) # 2. 动态裁剪 keywords = pruner.extract_keywords(user_input) manager.apply_prune(keywords) # 内部调 relevance_score 重排序 # 3. 构建 prompt context_prompt = manager.build_prompt(recent_history) # 4. 调用 LLM messages = [ {"role": "system", "content": context_prompt}, {"role": "user", "content": user_input} ] response = llm_func(messages) # 5. 反馈回采集:LLM 的回复里有新事实,也要入上下文 assistant_item = collector.collect(response, role="assistant", round_num=manager.round) # 助手消息默认权重打 7 折,避免机器人的话盖过用户意图 assistant_item.weight = 0.7 manager.add_item(assistant_item) recent_history.append({"role": "user", "content": user_input}) recent_history.append({"role": "assistant", "content": response}) return response接入层有个容易忽略的坑:千万不要把每个模块独立串行的中间结果也塞进上下文。我最开始做的时候,把意图识别结果、关键词抽取结果、相关度得分全部拼进 prompt,觉得"信息越全模型越聪明"。结果 token 被大量元数据占满,模型反而开始分析"我的调参日志",不回答用户问题了。经验教训是:prompt 里只放用户能理解的信息,中间过程数据自己内部消化,别让模型看到。
4. 常见问题与排查技巧实录
4.1 上下文越聊越乱,怎么收敛
我遇到过最经典的现象:会话进行到 30 轮左右,模型开始重复问已经确认的问题,或者把早先用户的需求张冠李戴。排查下来,根因通常是会话层里抽出的"用户意图"条目被后续的低权重消息挤出了预算。
解决方案是给USER_INTENT类型的 Item 一个保底名额——不管预算怎么紧张,至少保留最近 3 条最关键的用户意图。实现方式很简单,在build_prompt里先预留这部分空间:
def build_prompt(self, recent_history: list[dict]) -> str: # 先强制保留最高优先级的用户意图 reserved = [it for it in self.items if it.item_type == ItemType.USER_INTENT and it.weight >= 1.0] reserved_tokens = sum(estimate_tokens(it.content) for it in reserved) # 从 SESSION 预算里扣除保留名额 self.budget[ContextLevel.SESSION] -= min( self.budget[ContextLevel.SESSION], reserved_tokens)这个保底机制上线之后,"模型忘事"的问题基本绝迹。核心逻辑是:用户明确说出口的需求,是对话的锚点,锚点不能被冲走。
4.2 Token 被无关信息占满
另一个高频问题是:prompt 里塞满了无关信息,用户问 A 事项,模型却老想着 B 事项的细节。这多半是采集阶段"什么都收"导致的。
我的处理思路是加一道相关性闸门。新消息进来后,先计算它与当前活跃主题的相关度。如果相关度阈值太低(比如< 0.2),就只进即时层不进会话层——保留它的上下文,但不让它长期占用高优先级位置。
def should_promote_to_session(self, item: ContextItem, active_topic: str) -> bool: if item.item_type in (ItemType.SYSTEM_RULE, ItemType.USER_INTENT): return True # 与当前活跃主题相关的才升入会话层 return active_topic in item.content or len(item.content) > 80这个阈值逻辑要按业务微调。客服场景里,如果用户在倾诉一个复杂问题,content > 80字的长消息应该升入会话层,哪怕主题有点偏离——因为用户可能正在表达一个还没被你完全理解的新需求。但如果是简短消息("嗯""好的""那就这样吧"),无论如何都不该升层。
4.3 多轮对话的"记忆漂移"问题
"记忆漂移"是指模型回答的内容跟早前确立的目标越来越不一致。比如用户一开始说要"轻量级方案",聊到后面模型开始推荐重量级方案。根因通常是:会话层里"用户要轻量级"这条信息权重不够高,而后来的一系列技术讨论(都是重量级相关)把它挤下去了。
解决办法是引入目标一致性校验:每一轮生成回答之前,把当前会话层里的用户意图和系统将要生成的回复风格做一次快速比对。比对不用模型,用关键词 + 向量余弦相似度都行。不一致时,强制把最相关的用户意图提到 prompt 最前面,并在系统提示里加一句"先确认用户原始诉求"。
我在生产环境里用的是轻量向量化方案,每条用户意图存一个 embedding,每次新问题进来也做 embedding,算相似度。相似度最高的那条意图强制优先注入。这个方案跑得很稳,唯一的成本是 embedding 接口的调用费用,但单次不过几厘钱,完全划得来。
4.4 性能与成本的平衡
context-mode 本身是省 token 的,因为它的目标就是"少带、带准"。但如果在采集阶段频繁调用 LLM 做提炼,省下来的钱又会被提炼花掉。
我的经验是给提炼操作设一个触发条件,不要每条消息都走 LLM 提炼:
- 消息长度小于 30 字:不做提炼,直接原样存。
- 消息里有明确的结构化信息(订单号、日期、数字):用正则抽,不调 LLM。
- 消息长度大于 30 字且无结构化信息:才调用 LLM 提炼。
- 每轮最多调用一次提炼 LLM,多出来的消息走规则降级。
这个策略实测下来,提炼成本占整体 token 成本的比例能控制在 5% 以内。很多人做 context-mode 做成烧钱机器,就是忽略了"哪些步骤该用规则,哪些才配用 LLM"。
还有个性能细节:estimate_tokens别用精确的 tokenizer,否则每次 build_prompt 都会卡在分词上。我直接用len(text) // 2做中文 token 估算,偏差在 20% 以内,对裁剪决策来说足够。接口真正的耗时瓶颈在网络调用,不在估算。
4.5 一个贯穿始终的调试技巧
最后分享一个调试 context-mode 系统的实用技巧:给每条进入 prompt 的上下文打上溯源标记。
def build_prompt(self, recent_history: list[dict]) -> str: # 调试模式:输出注入日志 if self.debug: print(f"[Round {self.round}] 注入 {len(result)} 条上下文") for item in injected_items: print(f" - [{item.level.name}] " f"type={item.type.name} " f"weight={item.decayed_weight(self.round):.2f} " f"content={item.content[:50]}...")线上出问题时,先看日志里每个 round 注入了哪些条目、权重多少、哪些被裁剪掉了。我踩过的坑里,有一半是"看日志就秒懂,不看日志猜半天"。context-mode 属于那种"黑盒感很强、但一旦可视化就非常透明"的系统,花 20 分钟把日志做好,后面能省 20 个小时的排查时间。
5. 结语前的最后几条经验
5.1 从最简单的分层开始,不要一上来就做全套
如果你刚开始做 context-mode,别急着把所有机制全上。我的建议是分阶段落地:
- 先做分层:全局层、会话层、即时层三件套,手动指定每层的 token 预算。
- 再加衰减:用
last_access做活跃度刷新,让常用信息自动留在窗口里。 - 然后上相关度:接关键词或 embedding,让当前问题能"拉取"相关上下文。
- 最后才是动态裁剪和 LLM 提炼:这两块对系统的稳定性要求最高,等前几步稳定了再动。
我见过太多团队第一步就想做个"全自动、自学习、完美压缩"的上下文系统,折腾两个月还在调参。实际上 80% 的收益在前三步就拿到了,第四步只是锦上添花。
5.2 上下文管理的本质是取舍
做 context-mode 最深的体会是:上下文管理的本质不是"存储",而是"取舍"。你不可能把所有信息都塞进窗口,塞得越多,模型越分不清重点。真正好的 context-mode 系统,是让模型每次调用时面对的都是"最精炼、最相关、最新鲜"的信息集。
这就像给一个人做简报——把 50 页的报告浓缩成两页要点,比把 50 页原封不动放他桌上,效果好十倍。模型和人也一样,输入质量决定输出质量。
5.3 这套思路也可以往反方向扩展
context-mode 不只用于"省 token、提准确率",它还能反过来用于知识注入。我在另一个项目里把同样的分层机制用在了企业知识库问答上:全局层放企业规章制度,会话层放用户的提问历史和相关文档摘要,即时层放实时检索的文档片段。效果比直接做 RAG 要好——因为 RAG 只解决"检索什么",不解决"检索到的内容怎么组织优先级",而 context-mode 恰好补上了这一环。
这也是我觉得这套方法最值得分享的原因:它不是一个"一次性脚本",而是一个可以反复套用的架构模式。无论你是在做大模型应用、Agent、还是传统推荐系统的特征拼接,上下文优先级的组织思路都能迁移过去。
以后你要是遇到"模型总是答非所问""token 成本压不下来""长会话聊着聊着就崩"这类问题,可以先想想:你的上下文是"一锅烩",还是真正有层级、有取舍、有优先级的"context-mode"?这个转变,可能比换更大的模型、调更长的窗口实在得多。