开头
聊一个最近折腾得最多的东西:context-mode,也就是 LLM 应用里的「上下文管理模式」。
做 AI 应用快两年了,我发现 80% 的功能翻车,最后都翻在上下文处理上。模型本身没毛病,是喂进去的上下文不对——要么太长爆了 token 上限,要么太短丢了关键信息,要么顺序乱了导致模型前言不搭后语。context-mode就是为解决这个问题而设计的一套上下文管理策略,它不是一个开箱即用的工具,而是一套"在什么场景下、用什么方式组织历史对话信息"的决策机制。
这篇文章会把我在实际项目中落地的 context-mode 完整拆开:核心设计思路、几种模式的适用场景、关键参数怎么定、常见坑有哪些。适合正在做 AI 对话产品、有 agent 开发经验或刚接触 LLM 应用开发的工程师参考。我能保证的是,这些内容不是文档里抄来的概念,而是踩过坑之后沉淀下来的实操经验。
1. 内容整体设计与思路拆解
1.1 context-mode 到底解决什么问题
先举个贴近生活的例子。你跟一个人连续聊了三个小时,对方每隔几分钟问一句"刚刚那个事情你怎么看"。如果你已经完全忘了前两个小时聊了什么,你给出的回答大概率会前后矛盾。人类是这样,LLM 更是这样——它的"记忆"完全依赖于每次请求时喂进去的上下文。
我做的一个 AI 客服机器人项目,早期版本特别天真:把所有历史消息一股脑塞进 prompt。头一两天测试没问题,一旦对话超过 20 轮,prompt 直接超过上下文窗口上限,程序报错。就算有些模型支持 128K 上下文,塞满之后响应延迟从 1 秒飙到 8 秒,而且模型对中间信息的注意力明显下降——后来我才明白,这叫做"lost in the middle"现象,模型对长上下文中间的细节记忆很差。
context-mode 的设计目标就是解决两件事:
- 控制 token 消耗。对话无限增长,但上下文窗口是有限的,必须有一套裁剪策略。
- 保留关键信息。不是简单粗暴地截断,而是理解"哪些信息对当前问题最重要",把重要内容留下来。
1.2 几种核心模式的设计选型
我在项目里最终沉淀出四种 context-mode,对应不同的对话场景。这不是什么标准协议,是我自己在实践中验证过的组合。
全量模式(Full Mode)。适合短对话或需要高精度回忆的场景。逻辑最简单:把全部历史消息按顺序拼进系统提示词。优点是不会漏信息,缺点是 token 消耗大。我只在对话轮次较少(低于 10 轮)或用户明确要求"回顾全部历史"时才用。
窗口模式(Window Mode)。只取最近 N 轮对话作为上下文。这是成本最低的方案,也是大多数简单 Chatbot 的默认选择。N 的选择很讲究,太小模型失忆,太大成本高还容易注意力分散。我在实际项目中一般用 6~12 轮作为默认值。
摘要模式(Summary Mode)。把早期对话用 LLM 本身提炼成摘要,只保留"用户关心什么、结论是什么、承诺过什么"这类长期信息,然后与最近的原始对话组合。这个模式解决的是"对话很长但关键结论分散在不同时间段"的问题。
检索模式(RAG Mode)。适用于知识密集型场景。每轮对话进来,先从历史消息和知识库中向量检索最相关的片段,只把这些片段拼入上下文。这是效果上限最高的方案,但工程复杂度也最高,需要管理向量库和 embedding 流程。
实际项目中这四种模式不是互斥的,而是组合使用。我的生产架构是:最近 8 轮走窗口模式,更早的对话走摘要模式,同时用检索模式从知识库补充事实性信息。整个 context-mode 就是一个"调度器",根据对话轮次、用户意图和 token 预算来决定当前请求使用哪种模式或模式的组合。
1.3 为什么不能只用单一策略
做个简单推演你就明白了。假设每轮对话平均消耗 400 token,10 轮之后就是 4000 token。如果对话持续到 50 轮,全量模式需要 20000 token,按月调用量 10 万次计算,仅 prompt 费用就可能破万。更糟的是,即使你愿意付这个钱,模型在超长上下文上的表现也会退化。
但纯窗口模式同样有问题。用户在第 40 轮突然问"我刚开始说的那个需求,你记得吗",而那个需求在第 3 轮出现过,窗口模式已经完全把它丢掉了。模型只能瞎猜或者承认不记得,用户体验非常差。
这说明上下文管理本质上是一个"遗忘与记住的平衡问题"。好的 context-mode 必须在成本和效果之间做动态取舍。我的经验是:要给每种模式设定明确的使用条件和切换阈值,而不是拍脑袋随机切换。比如对话轮次小于 10 用全量,10 到 30 轮用"摘要+最近窗口",超过 30 轮则启用检索模式进一步压缩。
2. 核心细节解析与实操要点
2.1 窗口模式的轮次选择逻辑
窗口模式最核心的参数就是 N(保留的对话轮数)。我一开始拍脑袋设了 20 轮,结果效果很差,原因是很多历史轮次对于当前问题已经完全没有信息量。后来我通过实验对比发现了一个规律:对于大多数任务型对话,相关性集中在最近 4~8 轮;对于闲聊型对话,相关性分布更均匀,但总量也不超过 15 轮。
具体如何确定 N?我没有用复杂的数学公式,而是做了个简单的梯度测试:
- 从 N=4 开始,逐步增加到 N=16,每档跑 50 条历史对话样本,人工打分评估回答的完整性和准确性。
- 记录不同 N 值下的 token 消耗与回答质量分数。
结果很有意思:从 N=4 增加到 N=8,质量分数上升明显;从 N=8 到 N=12,分数微涨但 token 增加 50%;超过 12 之后,分数基本不再增长,偶尔还下降(因为引入了噪声信息)。所以我把默认 N 定在了 8,并根据业务复杂度设置 6 和 12 两个档位。
还遇到过一个特殊情况——用户会在对话中粘贴大段日志或文档,这种消息一轮就可能占掉 3000 token。如果按"轮数"来切,很快就把上下文窗口塞满了。所以我改成了按 token 数切窗口:优先保留最近 N 轮,但同时累计计算 token 总数,超过阈值(比如 6000 token)时强制截断早期消息。用轮数的保底,用 token 总量兜底,两者取交集。
2.2 摘要模式的关键实现细节
摘要模式的核心是"什么时候触发摘"和"摘要里写什么"。
触发时机我建议用两个条件同时判断:对话轮次超过阈值(比如 15 轮),且最近 5 轮中没有出现新的关键实体或主题变化。如果用户正在紧锣密鼓地讨论一个连续话题,过早做摘要反而打断信息的连贯性;如果话题已经结束、用户开始聊新内容,那旧的对话就该立即被压缩。
摘什么内容也是要动脑子的。一开始我用最朴素的 prompt:"请总结以上对话",结果模型把天气问候和闲聊都摘进去了,关键决策反而被稀释。后来我把摘要结构固定成四段式:
- 用户画像:用户提到的身份、偏好、约束条件。
- 核心需求:用户明确提出的目标,尤其是带数字、时间、范围的表述。
- 达成的共识:已确认的结论、方案、承诺。
- 待办与疑问:尚未解决的问题、需要用户确认的事项。
写成模板大概是这样的:
请从以下对话中提取信息,按四个维度组织摘要: 1. 用户画像:可推断的用户身份、偏好、习惯。 2. 核心需求:用户的主要目标、关键要求,保留数字和限定条件。 3. 已达成的共识:双方确认过的结论、方案细节。 4. 待办与疑问:未完成事项、需要进一步澄清的问题。 忽略寒暄和与主题无关的内容。每次生成新摘要后,不会直接覆盖旧摘要,而是把旧摘要和新增对话合并后再提炼。这个"摘要的摘要"过程看似多消耗了一次 LLM 调用,但能防止信息越滚越臃肿。我一般限制摘要总长度在 500 token 以内,超出就做二级摘要。
2.3 检索模式的关键决策点
检索模式需要回答三个问题:用什么 embedding、检索哪些内容、怎么混排。
embedding 选型。中小项目直接用现成的 embedding API 就行。我在云上部署用的是通用的 text-embedding 模型,实测在中文语义相似度上表现不错。需要注意的一个坑是:embedding 模型也有 token 上限(通常是 512 或 2048),超过了会截断。所以我在入库时强制切片,每段控制在 300 字以内。
检索内容范围。不是所有历史都适合向量化。我的做法是白名单机制:只对包含具体实体(订单号、产品名、用户 ID)或决策性内容的消息做切片和向量化,纯粹的闲聊直接排除。这一步能显著降低向量库的噪声。
怎么与上下文组合。检索到的片段不能直接塞到 prompt 最前面,那样模型会搞不清哪些是当前对话。我通常这样组织:
系统提示词 【相关历史片段】(检索得到的,标注来源时间) 【最近 N 轮对话】 【用户当前问题】把检索片段放在最近对话之前,模型能理解"这些是较早的背景信息,而后面的对话才是当前主线"。一开始我把检索片段放在最后,结果模型经常把它们当作对当前问题的直接回答,输出质量很差。
2.4 系统提示词对 context-mode 的配合
很多人忽略了一点:上下文模式切换之后,系统提示词也需要跟着变。如果用窗口模式,系统提示词里就要明确告诉模型"你只能看到最近的信息,如果用户问更早的内容,请礼貌说明并引导用户重新描述";如果用了摘要模式,系统提示词要说明"较早的信息经过压缩,部分细节可能丢失"。
我踩过一个教训:给模型开了摘要模式,但系统提示词没说,结果用户问"我上周说的预算具体是多少",模型看到摘要里有"预算"两个字就自信地编了一个数字。后来我在系统提示词里加了一句硬性规则:"当用户询问的信息不在当前上下文中时,必须明确回答'我没有找到相关信息',禁止推测。" 这一步把幻觉率降了差不多 60%。
3. 实操过程与核心环节实现
3.1 context-mode 调度器:整体架构
我建议把 context-mode 做成一个独立的调度模块,不要跟业务逻辑耦合。架构分三层:
- 调度层:接收用户消息,计算当前对话的 token 数、轮数、主题变化,决策使用什么模式。
- 处理层:执行摘要、窗口截取、向量检索、历史消息组装。
- 组装层:把系统提示词、历史片段、最近对话、当前问题按顺序拼接成最终 prompt。
下面是我项目里调度器的核心代码骨架,用 Python 写的,伪代码性质,但结构是完整的:
class ContextManager: def __init__(self, max_tokens=6000, window_rounds=8, summary_threshold=15): self.max_tokens = max_tokens self.window_rounds = window_rounds self.summary_threshold = summary_threshold self.summary_store = {} # session_id -> summary text def build_prompt(self, session_id, history, current_question): total_tokens = estimate_tokens(history) rounds = len(history) if rounds <= self.window_rounds and total_tokens <= self.max_tokens: # 全量或窗口模式(合并处理) relevant_history = history[-self.window_rounds * 2:] else: # 摘要模式 + 窗口模式 summary = self.summary_store.get(session_id, "") recent = history[-self.window_rounds * 2:] relevant_history = self._merge_summary_and_recent(summary, recent) # 如果还是超长,触发检索模式 if estimate_tokens(relevant_history) > self.max_tokens: relevant_history = self._rag_retrieve(history, current_question) prompt = self._assemble_prompt(relevant_history, current_question) return promptestimate_tokens这个函数单独说一下。千万不要逐字调用模型的 tokenizer API,那样太慢而且付费。我用的近似公式:token数 ≈ 中文字符数 x 0.7 + 英文字符数 x 0.3。对 GBK 编码的中文文本来说,这个估算误差在 15% 以内,足够用来判断是否超限。生产环境里如果对准确性有要求,可以对常用消息做一次真实 tokenizer 估算后缓存结果,不用每次现算。
3.2 摘要模式的运行流程
摘要处理的完整流程我拆成四个步骤,每一步都有明确产出:
- 判定时机:当对话轮次超过 15 且距离上次摘要超过 10 轮时,触发摘要任务。
- 生成增量摘要:把旧摘要 + 新增的 10 轮对话合并成一个文本块,调用 LLM 按四段式模板生成新摘要。
- 质量校验:检查新摘要中是否保留了旧摘要里的关键数字和实体。我会写一个简单的校验脚本,用正则把旧摘要里的数字提取出来,跟新摘要比对,如果关键数字丢失就重试一次。
- 存储迁移:把新摘要写入存储,同时把已摘要的对话从待处理队列中清除。
第 3 步是我觉得最值得借鉴的经验。LLM 在摘要时天然会丢失精确数字,比如价格、日期、数量。用程序化校验去兜底,比完全依赖模型自觉可靠得多。
摘要存储我用的就是普通的数据库字段,没有引入额外组件。每条会话记录加一个summary字段,每次摘要更新就是一次普通的 UPDATE。只有当检索模式需要全局搜索时才用到向量库,那是另一套逻辑。
3.3 检索模式落地配置清单
如果你决定上检索模式,我建议按下面的顺序操作,不要跳步:
- 数据清洗:历史消息中过滤掉纯表情包、链接分享、系统通知。这些对理解用户意图没帮助。
- 切片入库:每段控制在 200~300 字,段与段之间保留 10% 重叠,避免语义断裂。
- embedding 批量生成:建议离线批量跑,不要线上实时生成,耗时太长。
- 存储选型:数据量在百万条以下用单机向量库就够,超过百万条再考虑分布式方案。
- 检索阈值:召回 TOP 3~5 条,设定相似度阈值 0.75,低于此分数直接丢弃。
这里说下为什么相似度阈值要设成一个合理的值。我把阈值设得太低(0.5)时,检索出来的片段很多是"字面相关但语义无关"的,比如用户问"怎么退款",结果检出一条"退款政策已更新"的通知,模型参考完之后也跑偏了。调到 0.75 之后,虽然召回变少了,但每条都是有价值的,反而提升最终回答准确率。
3.4 组装 prompt 的细节规范
组装 prompt 的阶段看似简单,实际坑很多。我要求团队遵循几条硬规范:
- 顺序固定:系统提示词在最前,然后是相关历史片段、最近对话、当前问题。顺序一旦定了就不要改,因为模型对位置的敏感性很高。
- 分隔符要醒目:我在不同区块之间用
###和分隔线划分,并且告诉模型这些分隔符的含义。比如 "【以下为历史信息】"、"【以下为当前对话】"。这样模型能清晰地知道每一段的角色。 - 结尾不要放多余的引导语。很多人习惯在 prompt 最后加一句"请根据以上对话回答",其实没有必要。用户的当前问题本身就是最强的引导,加多了反而干扰。
还有一个细节:如果当前问题碰巧很长(比如用户贴了一大段文字),我建议把问题放在历史信息之前。一是避免长问题把历史信息挤出注意力窗口,二是模型读问题时已经忘了前面的历史的话,反而会更聚焦在问题上。这个 switch 不是所有场景都适用,但当你发现模型开始"看着历史说话,忽略当前问题"的时候,值得一试。
4. 常见问题与排查技巧实录
4.1 模型"失忆"了怎么办
这是 context-mode 上线后最常见的用户投诉:明明之前聊过的东西,模型好像完全不知道。排查顺序很重要:
- 先把出问题的会话完整拉出来,看实际传给模型的 prompt 是什么(一定要打日志)。80% 的情况不是模型出了问题,而是 context-mode 把关键信息裁掉了。我当时就靠这个定位了一个 bug:窗口模式里我用的是"最近 8 轮",但用户问的"那个需求"发生在第 9 轮——差一轮就没到窗口里,信息被裁了。把窗口从 8 调到 10 就解决了。
- 如果 prompt 里有信息但模型没用好,检查
系统提示词是不是让模型"过于关注最新消息"。模型有一定的近因偏好,越新的信息权重越高。我给系统提示词加了一条:"注意,历史信息中同样包含关键内容,请综合所有信息回答",情况明显改善。 - 如果以上都排除了,才考虑模型能力本身的问题。换一个更强的基础模型测试,对比输出差异。
我建议在生产环境中,每一轮请求的 prompt 内容都要落日志。这个日志文件看起来占存储,但在排查问题的时候就是救命的稻草。没有它,你只能靠猜测定位问题,那效率太低了。
4.2 摘要质量越滚越差的问题
摘要模式跑久了之后,会出现"摘要的摘要"信息逐级丢失的问题。我第一次发现是在一个超长客服会话里,第 5 次摘要时,原本的"用户所在城市为成都,需要发票"变成了"用户需要发票",城市信息消失了。
排查过程很有意思:我检查中间每一代的摘要,发现丢失不是集中发生的,而是每代丢一点。旧摘要里的实体被新摘要在合并时"优化"掉了。
解决方案是在每个新摘要生成后做一次实体完整性校验。我把校验逻辑展开一下:
- 从上一代摘要中提取所有实体关键词,包括地名、金额、日期、产品名、数字。
- 检查新一代摘要中是否包含这些实体。
- 如果发现丢失,就把丢失的实体列表附加到生成 prompt 中,让 LLM 强制保留:"新摘要必须包含以下实体:['成都', '发票']"。
- 如果补了一次仍然丢失,就走降级策略——这部分信息直接用原文片段拼接在摘要后面,不再依赖 LLM 二次提炼。
这套机制上线后,实体保留率从 70% 提升到了 95% 以上,代价是约 3% 的额外 token 消耗,值得。
4.3 token 估算偏差导致的爆窗
我用"字符数 x 0.7"的估算公式,在纯中文场景下还好,但遇到中英混排的场景偏差就大了。有一次用户贴了一个英文报错日志,实际 token 数是估算值的 1.8 倍,结果 prompt 超限,模型直接报错。
后来我把估算公式改成分段计算:
def estimate_tokens(text): chinese_chars = sum(1 for c in text if '\u4e00' <= c <= '\u9fff') other_chars = len(text) - chinese_chars return int(chinese_chars * 0.8 + other_chars * 0.35)英文和技术文本每个字符约 0.3~0.4 个 token,中文每个字约 0.7~0.8 个 token(在大多数开源 tokenizer 下)。修正之后,估算偏差控制在 10% 以内。
还要注意一个边界条件:不要让 prompt 的实际 token 数恰好卡在上限附近。我在调度器里设置了85% 安全阈值:估算 token 达到上限的 85% 时就触发压缩逻辑,而不是等到 100%。这样即便估算有误差,也留了缓冲。
4.4 context-mode 的测试方案
没有测试就上线的 context-mode 一定会出事故。我的测试方案分三层:
- 单轮单元测试:每轮对话单独验证 prompt 组装是否正确。我用快照测试,把组装结果存成 JSON 快照,任何代码变更后跑一遍 diff,防止无意的行为改变。
- 多轮场景测试:先用脚本自动生成一段包含 50 轮对话的会话记录,内容覆盖闲聊、任务、知识咨询等类型。然后分别在四种 context-mode 下运行完整流程,人工评估每一轮的回答质量和上下文连贯性。
- 回归测试集:把历史线上出过问题的会话全部收集起来,做成回归样本。每次改造完之后跑一遍,确保"修复过的 bug 不会复发"。
我强烈建议把用户的真实对话(脱敏后)加入测试集。因为你自己构造的测试对话永远不具备真实对话的杂乱性和跳跃性,只有真实数据才能暴露 context-mode 的短板。
4.5 多会话隔离的陷阱
如果你的产品同时服务大量用户,每个用户有独立的会话上下文,要注意不能让不同会话的摘要和检索结果互相污染。我最初在检索模式里犯过一个低级错误:向量库查询时没有加session_id过滤条件,结果 A 用户的客服会话里检索出了 B 用户的订单信息——这不仅是技术 bug,还是隐私事故。
检索隔离的强制原则:
- 向量库里每条数据都必须带
session_id或user_id字段。 - 所有检索查询必须带过滤条件,代码里要强制校验,而不是靠开发人员自觉。
- 摘要存储的表,读取时必须走带
session_id的索引查询。
我自己写过一个自动化的数据泄漏检测脚本:随机抽 100 个会话,检查返回的检索结果中是否存在非当前会话的片段,有就触发告警。直到现在这个脚本还在 CI 流程里跑着,一分钟能检查完一批数据,成本极低但收益很大。
5. 扩展思考与实践建议
5.1 context-mode 与模型能力的关系
你可能会问:如果模型本身的上下文窗口足够大(比如 200K),是不是就不需要这么复杂的 context-mode 了?
我的回答是:不是。窗口大只代表"放得下",不代表"效果好"。长上下文的注意力分散问题目前依然是模型架构的固有限制。"Lost in the middle"在 Claude 和 GPT 上同样存在,只是程度差异。你让模型读 10 万 token 的对话历史,它能记住开头和结尾,中间部分基本等于没用。
所以 context-mode 的核心目标不是"节省 token"这么简单,而是让模型只看到它该看的信息。哪怕未来模型窗口扩大到 1M,这个原则依然有效——因为真正决定回答质量的是信息的信噪比,而不是信息的总量。
5.2 扩展到 Agent 和工具调用场景
如果你做的不是单纯聊天机器人,而是带工具调用的 Agent,context-mode 需要加一层"工具调用记录"的处理。工具调用的输入输出往往包含大量结构化数据(API 返回的 JSON),这些内容不能跟普通对话一样做摘要,否则 Agent 会丢失关键状态。
我的做法是把工具调用的结果分成两路:
- 精简化记录:把完整 JSON 存一份到外部存储,只在上下文里放一个精简摘要(例如"调用订单查询 API,返回 5 个订单")。
- 按需回溯:如果后续对话中 Agent 需要完整订单数据,通过检索模式把完整结果取回来。
这套设计避免了工具调用结果把上下文撑爆的问题,同时保证了 Agent 在需要精确数据时能拿回原文。在多个 Agent 应用的实践中,这个折中方案比"全量塞入"或"直接丢弃"都更稳。
5.3 监控指标和踩坑清单
最后给出一份我在生产环境中持续监控的核心指标,你可以当成 checklist 用:
| 监控指标 | 预警阈值 | 说明 |
|---|---|---|
| 平均每请求 prompt token | 超过 5000 | 如果持续走高,说明压缩策略可能失效 |
| 摘要实体保留率 | 低于 90% | 通过程序比对两代摘要的关键实体得出 |
| 检索命中率 | 低于 60% | 命中率低说明检索质量差或阈值设置不合理 |
| 上下文切割触发率 | 超过 40% | 说明默认窗口参数设置过小 |
| 用户重问率 | 超过 15% | 用户反复追问同一件事,说明 model 失忆 |
监控数据不需要做得很复杂,记录原始日志、定时跑统计脚本、异常告警就够了。我见过不少团队在上下文管理上投入大量精力,但完全没有监控,导致问题雪崩了才发现。这是我把个人经验浓缩成一句话的建议:先加日志,再加监控,最后才加策略。顺序错了,后续所有优化都是盲人摸象。
回想最开始的 AI 客服项目,我用了整整三个月才把 context-mode 调到满意的状态。中间试错过无数回,最深刻的教训是:不要一上来就追求花哨的检索模式和复杂算法,先把全量、窗口、摘要这三种模式跑通,用真实的用户对话去暴露问题,然后再逐步升级。context-mode 的本质不是一套花哨的架构,而是对"模型该怎么看世界"的一种克制而务实的约束。它值得你花时间打磨,因为它直接决定了你的 AI 应用是"聪明"还是"失忆"。