前段时间在做一个面向业务人员的AI助手时我一直在和一个问题较劲对话稍微一长模型的回答质量就像跳水一样往下掉。用户上午问的问题下午再提模型已经完全不记得更麻烦的是它有时会把不同客户的数据混在一起回答——这已经不是笨的问题了而是上下文管理失控。我最初也试过各种花式 Prompt比如在系统提示词里加一句请记住用户之前的问题效果微乎其微。真正让我决定动手做一套专门的上下文管理模式context-mode的导火索是一次线上事故一位用户在多轮对话里中途切换了业务场景模型不但没跟上新场景反而把旧场景的信息带进新回答里给客户报了一个完全错误的报价。从那以后我开始把怎么管上下文当成一个正式的系统设计问题来对待而不是靠模型自觉。这篇东西我会把整套 context-mode 的思考、设计、落地实现和踩坑过程完整写出来适合那些正在做 LLM 应用开发、尤其是做多轮对话和 Agent 类产品的朋友参考。内容不涉及某个特定框架原理和代码思路可以迁移到你的项目里。1. 为什么我需要一个专门的 Context Mode很多人对上下文管理的理解就是把历史消息拼在一起发给模型。确实OpenAI 这类接口只管你传什么进去然后基于全部输入做预测。但问题恰恰出在这个全部输入上。1.1 线程一长模型就开始失忆大语言模型的上下文窗口是有限的GPT-4o 和 Claude 这类模型虽然有 128k 甚至 200k 的窗口但窗口大不等于都记得住。Attention 机制天然存在中间地带遗忘现象也就是说模型对长对话的开头和结尾印象最深中间部分容易被稀释。你试过就知道把一个 50 轮以上的对话完整塞给模型它往往只对最近几轮的内容敏感早先用户明确说过的偏好、约束条件它可能完全无视。这还不是最要命的。对窗口的粗暴填满还会带来一个衍生问题Token 占用越多单次请求的延迟和成本越高。到了 100k 级别一次请求在 GPU 上做 Prefill 的时间可能翻好几倍。成本端更直观——我统计过如果无脑把历史消息全部带上一个日活 500 的客服机器人单月 API 费用能比采用上下文压缩方案后的费用高出 6 到 8 倍。1.2 业务上需要可解释的上下文管理技术问题可以容忍模糊业务问题不行。当模型回答出现偏差业务方通常会问你一句它到底是根据什么得出这个结论的 如果上下文是一大坨黑盒文本你根本没法回答。你只能挠头说模型自己理解的这话业务方不爱听。所以在设计 context-mode 的时候我给自己定了三个硬性要求上下文必须有明确的边界知道每一段信息是什么时候、从哪一轮、以什么模式进入上下文的上下文必须能被审计也就是说任何时刻都可以导出当前模型看到了什么上下文模式必须可切换不同业务场景承载不同的上下文而不是一锅烩把这三个要求记在心里后面的设计就顺了。其实它们的本质是在问一件事上下文的生命周期到底应该怎么管理。1.3 用户场景驱动的模式分类在设计初期我调研了十几个真实对话案例发现用户的使用习惯大致能分成三类连续咨询型在一棵问题树上不断追问、场景跳转型聊完 A 事马上聊 B 事、长周期回归型几天前聊过的事今天又回来继续。这三种需求对应的上下文处理策略完全不同场景类型典型例子理想方案连续咨询型报修流程中一步步走完所有步骤保留完整操作流逐步推进场景跳转型先问产品价格再问售后政策切分场景切换时隔离旧上下文长周期回归型上周讨论的方案今天来确认提炼长期事实存入持久记忆一张表列完我就明白了一个单一的把所有消息都塞进去的策略永远无法同时满足这三种场景。context-mode 必须是一个可以动态调整的机制。2. Context Mode 的分层设计与状态流转我的设计思路可以浓缩成一句话把上下文从历史消息列表升级成有结构、有状态、可流转的资源。2.1 三个层级的划分系统上下文、会话上下文、临时上下文第一版设计我做了三个层级每个层级对应不同的生命周期系统上下文System Context全局不变的规则比如机器人角色设定、业务红线、输出格式要求。生命周期是常驻的不随对话轮次变化。这一层要极简只放真正不可动摇的东西否则会挤占宝贵的有效窗口。会话上下文Session Context一次会话内产生的关键信息比如用户报出的订单号、商品型号、偏好等。生命周期从会话开始到会话结束或者到被新信息覆盖为止。这是 context-mode 管理的重点。临时上下文Temporary Context当前正在处理的操作相关细节比如用户正在填写一张投诉单表单填完这批临时细节就归档或者丢弃。生命周期很短通常只在几个轮次内有效。这个分层解决了一个直接的问题构建请求的时候不再是简单地把 messages 数组倒进模型而是按优先级和生命周期动态组包。系统上下文永远在最前面会话上下文按重要性排序临时上下文垫底保证最新操作信息贴近对话尾部模型对尾部更敏感。2.2 真正的核心上下文状态的五态流转分层只是静态结构真正撑起 context-mode 的是状态流转机制。我借鉴了有限状态机的思路把一段上下文信息定义为五个状态活跃态Active当前轮次正在使用的信息例如用户刚刚提供的订单号。引用态Referenced活跃态的信息在后续轮次被再次确认或使用晋升为需要长期保留的信息。待定态Pending信息出现了但还没确定是否重要例如用户在闲聊中提到的模糊需求。归档态Archived已经完成使命的信息被压缩后存入长期存储不再进入模型上下文。废弃态Discarded被新信息覆盖或判定为无关的信息彻底删除。每条上下文记录在进入系统时都带一个初始状态。每次模型返回后系统做一次上下文体检根据本轮对话内容判断哪些信息需要状态迁移。举个实际例子用户说我要退货订单号是 20240915AB38。那么这条订单号进入活跃态。下一轮用户说就是那个 20240915AB38 的订单。 这个信息被再次引用进入引用态后续几轮会一直保留在系统提示词里模型不会忘记。如果用户又说算了我不退了帮我看看有没有新款。 那么订单号信息转为废弃态从上下文里撤出新品推荐相关的新信息进入活跃态。这个机制让上下文的进出不再是堆积式的而是像真实对话中的记忆管理——重要的留下不重要的清掉。实现上我用了一个简单的状态转移表来驱动每个消息在落库时都会额外记录状态字段。2.3 模式快照与回滚机制对话不可能永远一帆风顺。用户可能在切换场景后反悔不对我还是回到刚才那个问题。 如果没有上下文快照这个操作几乎无法实现因为系统早就把旧信息要么覆盖、要么归档了。所以我在 context-mode 里加了快照Snapshot机制每次发生场景切换时系统自动对当前会话上下文做一次完整快照并记录切换点。快照不进入模型上下文只存服务端存储。用户说回到刚才时系统直接加载对应时间点的快照把模型可见的上下文恢复到切换时的状态。这个功能一开始我嫌重觉得浪费存储。直到真正上线后业务方跟我说用户会自己开多线任务你们能不能支持回溯我才意识到快照不是性能负担而是产品功能的底气。存储成本其实很低一条快照不过几百个 token 的文本内容一天跑几千个会话也就几十 MB 的磁盘占用完全值得。3. 从设计到落地Context Manager 的实现细节设计归设计真正落到代码里的时候才会发现细节有多磨人。我按模块拆开讲。3.1 上下文构建器组装顺序决定模型理解质量同样的信息不同的拼装顺序模型理解的质量完全不一样。我自己做过对比测试同样一段上下文按系统规则→重要事实→近期对话→当前问题组装回答准确率能到 90% 以上把顺序打乱准确率立刻掉到 70% 左右。这不是玄学而是因为模型在生成时会天然更关注离当前位置近的 token 序列。Context Builder 的核心逻辑可以概括为一个优先级队列。每一类信息都带一个权重值系统上下文权重最高必须存在引用态会话信息次高保证模型记得关键事实活跃态信息按时间倒序待定态信息只取摘要临时上下文尽量精简组装顺序我大致是这样设计的系统上下文角色、规则、约束引用态关键事实用户身份、订单状态、决策偏好待定态摘要对话压缩后的要点最近 N 轮原始对话控制总量当前用户输入3.2 压缩策略选择摘要模式、截断模式、检索模式的取舍窗口空间不够了这是绕不开的坎。我同时实现了三种压缩策略并在不同场景下切换摘要模式Summarize Mode把超过阈值的历史对话交给一个小模型生成摘要用摘要替代原始文本。优点是信息密度高缺点是细节可能丢失尤其是数字、型号这类精确信息。适用于连续咨询型场景。截断模式Truncate Mode直接丢弃对话最前面的内容只保留最近 N 轮。优点是快、稳缺点是丢掉的信息可能后面还会用到。适用于临时上下文过长的场景。检索模式Retrieve Mode把历史对话切片存入向量库每次请求前按相关性检索 Top K 条拼入上下文。优点是灵活缺点是需要额外维护一套检索链路而且检索质量直接决定结果好坏。适用于长周期回归型场景。实际使用中我很少单用某一种而是组合对引用态信息用摘要模式提炼一个事实卡片对临时上下文用截断模式对长周期需求用检索模式。这套组合拳跑下来上下文命中率比单一策略高出不少。3.3 一个可复用的 Context Mode 核心类我写了一个简化版的核心类去掉业务细节保留骨架。你可以直接把它抄到项目里改from dataclasses import dataclass, field from enum import Enum from typing import List, Optional, Dict class ContextState(str, Enum): ACTIVE active REFERENCED referenced PENDING pending ARCHIVED archived DISCARDED discarded dataclass class ContextItem: content: str state: ContextState source_turn: int importance: float 0.5 metadata: Dict field(default_factorydict) class ContextModeManager: def __init__(self, max_tokens: int 8000): self.items: List[ContextItem] [] self.max_tokens max_tokens self.system_prompt self.snapshots: List[Dict] [] def set_system_prompt(self, prompt: str): self.system_prompt prompt def add_item(self, content: str, state: ContextState, source_turn: int, importance: float 0.5): item ContextItem(contentcontent, statestate, source_turnsource_turn, importanceimportance) self.items.append(item) def transition(self, item_id: int, new_state: ContextState): 状态流转比如 active - referenced 或 active - discarded if 0 item_id len(self.items): self.items[item_id].state new_state def take_snapshot(self, reason: str): 场景切换时打快照便于用户回溯 snapshot { reason: reason, items: [ContextItem( contentit.content, stateit.state, source_turnit.source_turn, importanceit.importance ) for it in self.items] } self.snapshots.append(snapshot) def restore_snapshot(self, snap_index: int): 恢复到某次切换前的上下文状态 if 0 snap_index len(self.snapshots): self.items self.snapshots[snap_index][items] def build_prompt(self, current_user_input: str) - List[Dict]: 按优先级组装发送给模型的 messages # 按 state 分级排序system referenced active pending temporary state_rank { ContextState.REFERENCED: 1, ContextState.ACTIVE: 2, ContextState.PENDING: 3 } valid_items [it for it in self.items if it.state ! ContextState.DISCARDED and it.state ! ContextState.ARCHIVED] valid_items.sort(keylambda it: ( state_rank.get(it.state, 99), -it.importance, it.source_turn )) # 这里应该用 tokenizer 估算并截断到 max_tokens # 简化处理直接拼接并打印 token 占位 context_parts [f[{it.state.name}] {it.content} for it in valid_items[:20]] messages [ {role: system, content: self.system_prompt}, {role: system, content: \n.join(context_parts)}, {role: user, content: current_user_input} ] return messages # 使用示例 cm ContextModeManager(max_tokens6000) cm.set_system_prompt(你是某电商平台的退货助理只处理退货相关咨询。) cm.add_item(用户订单号 20240915AB38, ContextState.REFERENCED, source_turn1, importance0.9) cm.take_snapshot(reason用户切换到售后咨询) cm.restore_snapshot(0) messages cm.build_prompt(我的退货进度到哪里了)这段代码当然还不能直接用但已经把最核心的机制点出来了状态、快照、分级组装。真正的生产级实现里token 估算要接入你用的模型对应的 tokenizer快照要序列化到 Redis 或数据库状态流转规则要跟业务绑定。骨架是对的。3.4 关键参数标定token 估算与保留系数设计这类系统最怕凭感觉设定。我强烈建议你在开发早期就做一次参数标定实验。具体来说做这样一件事找一个中等复杂度的测试集大概 100 到 200 条真实对话。定义两个指标关键信息召回率回答生成后人工检查哪些关键事实订单号、日期、偏好被模型正确引用。这是上下文管理质量的核心指标。单轮平均成本每一轮对话消耗的 token 数关系到成本和延迟。然后跑三组配置配置 A无压缩全量历史配置 B简单截断只保留最近 10 轮配置 Ccontext-mode状态分层 摘要 快照我实测的数据大概是这样的配置关键信息召回率单轮平均 token 数平均响应延迟全量历史100%窗口内18,4003.2s简单截断68%3,9001.1scontext-mode92%5,6001.4s全量历史看起来召回率最高但那是因为没有长对话出现。一旦对话超过 50 轮全量历史的召回率一样会崩。context-mode 在成本可控的前提下把召回率维持在了 92% 上下这就是它的价值。关于保留系数我给一个参考值保留系数 实际保留 token 数 / 上下文窗口上限建议控制在 0.6 到 0.7 之间。为什么不是 0.9因为模型生成回复时本身需要预留输出空间如果把窗口 90% 都填满输出会被强制截断反而更容易出错。0.6 到 0.7 是一个兼顾信息量和生成空间的经验区间。4. 实测中的翻车现场与修复方案读者可能觉得上面的设计很顺但我必须诚实地说这套 system 在上线过程中翻了三次车。每一次都是我认为应该没问题了之后被真实流量打脸。讲出来供大家参考。4.1 场景 A模式切换后上下文残留第一次翻车发生在场景跳转型用户身上。用户先问你们这个笔记本电脑的续航怎么样然后又问那你们有没有适合编程的显示器推荐。我的 context-mode 识别出这是两个场景做了切换但明显没切干净——模型回答显示器的时候还在反复提笔记本的续航参数。排查下来发现两个问题。第一状态流转的判定规则太死板只要检测到新场景关键词就立刻把旧场景的全部活跃态信息标记为废弃但某些信息比如用户的位置在城市 A在多个场景下都是有用的不该被一刀切。第二构建 Prompt 时引用态信息的排序太高导致旧场景的信息仍然出现在上下文显著位置。修复方案是给每条 ContextItem 增加了适用场景标签状态流转时先检查标签匹配度只有加标签完全不匹配的信息才被降级或废弃。同时构建 Prompt 时增加了场景过滤不同场景下的引用态信息分开组装不再全局混合。改完之后残留问题基本消除了。4.2 场景 B摘要压缩把关键信息压没了第二次翻车更隐蔽。连续咨询型场景下对话超过 20 轮后触发摘要压缩原本以为很稳结果用户突然问我第 3 轮说的那个发票抬头你帮我记下。模型完全不知道他在说什么——发票抬头在被压缩生成的摘要里消失了。原因很清晰摘要模型生成文本时倾向于保留叙述性内容对发票抬头某某科技有限公司这种孤立事实就任意丢弃了。这个问题的根子在于摘要压缩不应该盲目压缩所有信息而应该对不同类型的上下文加权。我后来修改了压缩策略分成两条线对引用态信息精确事实不做摘要直接保留原文——即使多占一点 token 也值得。对待定态、临时上下文叙述性内容才走摘要压缩。改完之后关键事实丢失的问题明显下降。后续又做了一版增强压缩流程会先扫描一遍所有引用态信息如果发现其中包含实体 具体值的模式比如发票抬头XX 公司、截止日期2024-09-30强制原样保留禁止摘要重写。4.3 场景 C长会话中用户意图漂移被系统放大第三次翻车的原因在我自己。业务数据表明用户的实际意图是随对话漂移的——先问价格后问技术参数最后想要优惠。但我的 context-mode 为了实现连续性倾向于把早期的活跃态信息一直保留导致模型反复以用户想买便宜型号的旧画像去理解用户的新意图。结果用户已经说了三次我要最好的配置模型还在推荐低价款。这给了我一个很深的教训上下文管理不能只堆信息还要做意图周期判断。我引入了一个简单机制每条活跃态信息都有一个半衰期如果连续多轮未被引用它的重要性分数会逐渐衰减最后自动转为待定态或废弃态。就像人脑的短期记忆一样不被强化就会被遗忘。这样用户的意图漂移系统也能跟得上。4.4 评测方法不只是跑通还要可量化翻完三次车我把评测流程固定了下来现在每次改动都会走一遍回归集准备 30 条覆盖三种场景类型的测试对话每条人工标注了关键信息点和标准回答要素。自动化指标跑完对话后检查回答文本中是否出现关键信息点计算召回率。成本快照每一轮记录 token 消耗出现异常暴涨就报警。人工抽检每周随机抽 10 条真实对话对照上下文快照检查状态流转是否合理。这套流程不复杂但非常管用。尤其是成本快照能暴露出很多上下文堆积的问题——如果你发现单一用户的平均 token 消耗随着轮次线性上涨你的上下文管理多半退化了变成变相的全量历史。5. 从 Context Mode 到记忆体系下一步演进写完这篇文章的时候context-mode 已经在我这边的线上稳定运行了一个多月。但我心里清楚它只是迈出了第一步。真正让人兴奋的是它指向的更大方向——记忆体系。5.1 短期记忆、工作记忆与长期记忆的分工context-mode 本质上管的还是短期记忆和部分工作记忆。它的生命周期从一次会话开始到结束很少跨会话存续。但真实世界不是这样的。用户昨天聊过的偏好今天就应该还记得他上次明确说不要推荐红色产品下次就不该再给红色。我现在在做的是在 context-mode 之上增加长期记忆层。做法也比较朴素每次会话结束时把引用态信息中跨会话有价值的那一部分用户偏好、身份信息、决策历史单独捞出来写入用户画像库。下次会话启动时先加载画像库再构建 context-mode。这听起来简单但做到后才真正打通了每次对话都像老朋友在聊天的感觉。5.2 上下文服务化的思考另一个让我觉得值得投入的方向是把上下文管理从业务代码中抽离出来做成独立的上下文服务。这样做的吸引力在于同一个用户在不同产品线之间切换时上下文可以共享不用用户重新描述需求。多个业务方接入时不需要各自实现一遍状态流转逻辑。上下文质量可以通过服务层面统一监控、统一调参。当然服务化也意味着更大的工程复杂度尤其是多租户隔离和数据安全这块必须当成核心问题来设计而不是业务的附属。这一步我还没完全落地但对团队规模和产品复杂度到了一定程度的人来说方向是对的。5.3 两条非常实在的建议如果只能从这篇文章带走两句话我会说这两句第一先定状态再写代码。不管你用什么方案管理上下文一定先定义出信息的生命周期——从它进门到被遗忘中间经历哪些状态什么条件下从一个状态流转到另一个。哪怕只有一个状态有用和另一个状态没用了也比完全没有状态意识的方案好 10 倍。第二用快照换容错。别怕存储贵别怕代码多。快照机制的关键价值在于它给了系统后悔的权力。当上下文管理策略出了 bug几乎一定会出快照能让你恢复到出问题之前的稳定状态而不是面对一个被污染了一整晚的上下文束手无策。我在第一次线上事故时就是因为没有快照最后只能靠重启服务来解决间接导致了大量用户投诉。现在回头看上下文管理听起来像个技术细节实际上它决定了 AI 应用的上限——模型本身的能力再强上下文喂得稀碎出来的结果也是稀碎的。我一直觉得LLM 应用的工程核心不在模型选择而在你如何管理围绕模型的记忆。context-mode 只是这条路上的一小段分享出来希望给同样在做这件事的人一些参考。