☰
如何设计LLM上下文管理模式?从状态流转到压缩策略的实战指南
2026/10/9 1:01:36 网站建设 项目流程

前段时间在做一个面向业务人员的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 的是状态流转机制。我借鉴了有限状态机的思路,把一段上下文信息定义为五个状态:

  1. 活跃态(Active):当前轮次正在使用的信息,例如用户刚刚提供的订单号。
  2. 引用态(Referenced):活跃态的信息在后续轮次被再次确认或使用,晋升为需要长期保留的信息。
  3. 待定态(Pending):信息出现了,但还没确定是否重要,例如用户在闲聊中提到的模糊需求。
  4. 归档态(Archived):已经完成使命的信息,被压缩后存入长期存储,不再进入模型上下文。
  5. 废弃态(Discarded):被新信息覆盖或判定为无关的信息,彻底删除。

每条上下文记录在进入系统时都带一个初始状态。每次模型返回后,系统做一次"上下文体检",根据本轮对话内容判断哪些信息需要状态迁移。举个实际例子:

  • 用户说:"我要退货,订单号是 20240915AB38。"
  • 那么这条订单号进入活跃态。
  • 下一轮用户说:"就是那个 20240915AB38 的订单。" 这个信息被再次引用,进入引用态,后续几轮会一直保留在系统提示词里,模型不会忘记。
  • 如果用户又说:"算了我不退了,帮我看看有没有新款。" 那么订单号信息转为废弃态,从上下文里撤出,新品推荐相关的新信息进入活跃态。

这个机制让上下文的进出不再是堆积式的,而是像真实对话中的"记忆管理"——重要的留下,不重要的清掉。实现上我用了一个简单的状态转移表来驱动,每个消息在落库时都会额外记录状态字段。

2.3 模式快照与回滚机制

对话不可能永远一帆风顺。用户可能在切换场景后反悔:"不对,我还是回到刚才那个问题。" 如果没有上下文快照,这个操作几乎无法实现,因为系统早就把旧信息要么覆盖、要么归档了。

所以我在 context-mode 里加了快照(Snapshot)机制:每次发生场景切换时,系统自动对当前会话上下文做一次完整快照,并记录切换点。快照不进入模型上下文,只存服务端存储。用户说"回到刚才"时,系统直接加载对应时间点的快照,把模型可见的上下文恢复到切换时的状态。

这个功能一开始我嫌重,觉得浪费存储。直到真正上线后,业务方跟我说"用户会自己开多线任务,你们能不能支持回溯",我才意识到快照不是性能负担,而是产品功能的底气。存储成本其实很低,一条快照不过几百个 token 的文本内容,一天跑几千个会话也就几十 MB 的磁盘占用,完全值得。

3. 从设计到落地:Context Manager 的实现细节

设计归设计,真正落到代码里的时候,才会发现细节有多磨人。我按模块拆开讲。

3.1 上下文构建器:组装顺序决定模型理解质量

同样的信息,不同的拼装顺序,模型理解的质量完全不一样。我自己做过对比测试:同样一段上下文,按"系统规则→重要事实→近期对话→当前问题"组装,回答准确率能到 90% 以上;把顺序打乱,准确率立刻掉到 70% 左右。这不是玄学,而是因为模型在生成时会天然更关注离当前位置近的 token 序列。

Context Builder 的核心逻辑可以概括为一个优先级队列。每一类信息都带一个权重值:

  • 系统上下文:权重最高,必须存在
  • 引用态会话信息:次高,保证模型记得关键事实
  • 活跃态信息:按时间倒序
  • 待定态信息:只取摘要
  • 临时上下文:尽量精简

组装顺序我大致是这样设计的:

  1. 系统上下文(角色、规则、约束)
  2. 引用态关键事实(用户身份、订单状态、决策偏好)
  3. 待定态摘要(对话压缩后的要点)
  4. 最近 N 轮原始对话(控制总量)
  5. 当前用户输入

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_factory=dict) 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(content=content, state=state, source_turn=source_turn, importance=importance) 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( content=it.content, state=it.state, source_turn=it.source_turn, importance=it.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(key=lambda 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_tokens=6000) cm.set_system_prompt("你是某电商平台的退货助理,只处理退货相关咨询。") cm.add_item("用户订单号 20240915AB38", ContextState.REFERENCED, source_turn=1, importance=0.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 轮)
  • 配置 C:context-mode(状态分层 + 摘要 + 快照)

我实测的数据大概是这样的:

配置关键信息召回率单轮平均 token 数平均响应延迟
全量历史100%(窗口内)18,4003.2s
简单截断68%3,9001.1s
context-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 评测方法:不只是跑通,还要可量化

翻完三次车,我把评测流程固定了下来,现在每次改动都会走一遍:

  1. 回归集:准备 30 条覆盖三种场景类型的测试对话,每条人工标注了"关键信息点"和"标准回答要素"。
  2. 自动化指标:跑完对话后,检查回答文本中是否出现关键信息点,计算召回率。
  3. 成本快照:每一轮记录 token 消耗,出现异常暴涨就报警。
  4. 人工抽检:每周随机抽 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 只是这条路上的一小段,分享出来,希望给同样在做这件事的人一些参考。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询