☰
大模型对话上下文管理实践:context-mode 四模式设计
2026/10/8 23:38:33 网站建设 项目流程

1. 对话系统“失忆”之后,我决定为上下文设计模式

今年年初我在维护一个 AI 客服对话系统时,被同一个问题反复折磨:用户明明还在同一个会话里,AI 却开始“答非所问”。用户在第三轮提过的偏好,到了第十五轮再追问,模型已经把关键信息忘得干干净净。更头疼的是,同一个会话内同时掺杂着闲聊、参数确认、历史单据查询好几类任务,不管你往上下文里塞多少内容,模型总会在某个节点开始抓不住重点。

我当时把问题拆了很多遍,最后锁定在一个设计缺失上:我们一直在往上下文里“塞东西”,却从来没有为上下文本身设计“模式”。这个思路在内部后来统一叫context-mode——把上下文当成一个有状态、可切换模式的状态机来管理。上下文不再是“一堆历史消息的堆砌”,而是“根据当前对话目标动态选择的最优信息集合”。

如果你也在做大模型对话类应用,或者正在纠结“为什么上下文塞得越多,效果反而越差”,这篇内容应该能给你一些直接的启发。我会从根因、模式设计、代码落地、实测数据、踩坑复盘这几个角度,把一个可复现的 context-mode 方案完整拆开。这不是某个开源框架的说明书,而是我自己从业务里趟出来的实践路径,适合小规模项目、内部工具、以及所有被上下文爆炸折磨过的开发者参考。

先说一个反直觉的结论:上下文管理的关键不是“尽量多记住”,而是“尽量少而准地选择”。大模型在长上下文里存在明显的“注意力稀释”现象,信息越堆越多,关键信息被淹没的概率就越大。context-mode 的核心任务,就是给每一段对话内容标注它“此刻是否该被使用”,而不是让模型在一堆垃圾信息里硬找答案。

2. 对话失忆的根因:没有为上下文设计模式

2.1 我们最初的做法:简单粗暴地拼接历史

这个项目第一版的上下文处理非常原始:每个新问题来的时候,把最近二十轮对话全部拼接进 prompt,不加筛选、不做摘要、不分主次。上线后很快就出现三个典型问题。

第一个是信息稀释。二十轮对话里可能只有三轮真正相关,但模型要从二十轮的噪声里自己筛出那三轮。尤其是当用户中途换个话题,再接回之前的话题时,模型经常把新话题的上下文错误地带入旧话题的推断里。

第二个是token 成本失控。客服系统每天几千次调用,每次调用多几千 token,成本差距就非常明显。更麻烦的是,当上下文接近模型窗口上限时,极端情况下会出现拼接失败或者响应时间暴涨。

第三个是记忆漂移。同一个用户的会话如果跨天了,我们把之前所有历史都塞回去,模型反而会因为“早期对话”和“当前对话”在风格或意图上差异过大,产生错误的偏好判断。比如用户周一说“我预算低”,周三问“有什么高级方案”,模型却因为旧记忆存在而固执地推荐低价方案。

2.2 失忆的本质:上下文缺少“生命周期”和“角色”

这些问题表面上是工程实现粗糙,但根子在于一个认知缺失:对话上下文是有生命周期的,不同阶段的内容应该承担不同角色,它们的“新鲜度”“重要性”“可丢弃性”完全不同。

我把一个长期会话里的信息分成这么几类:

  • 当前目标信息:用户正在表达的需求、这一轮要解决的参数。这是上下文里优先级最高、必须完整保留的部分。
  • 近期延伸信息:前面几轮为当前目标做铺垫的内容,比如条件约束、偏好修正。这类信息需要保留,但不必全量,保留关键约束即可。
  • 背景知识信息:用户在这个会话里提过的历史需求、身份标签、历史操作。这类信息和当前话题可能无关,但在跨话题回溯时会被用到。
  • 过期噪声信息:闲聊、已经结束的话题、纠正前的错误表述。这些不但没用,反而会拉低模型注意力。

问题在于,旧方案把所有内容一视同仁地堆进上下文,等于强迫模型自己去做这个信息分类。模型确实有一定能力,但它在分类的同时还要完成推理任务,效果自然大打折扣。

context-mode 的思路就是把这层分类工作从模型手里接过来:我们定义好不同上下文模式,每个模式对应一种“窗口策略”——当前该看什么、该隐掉什么、该压缩什么、该丢弃什么。模型只拿到当前模式认为“最有用”的那部分上下文,推理负担大幅下降,输出质量自然更稳定。

3. 四种模式的设计取舍:聚焦、漫游、压缩、重建

context-mode 不是一个单一开关,而是一组可切换的运行状态。我在项目里定义了四种模式,每种模式对应不同的上下文窗口策略。

3.1 聚焦模式(Focus):只保留当前任务的最高优先级信息

聚焦模式适合正在处理一个明确任务时的默认状态。比如用户正在确认订单信息,那上下文窗口里只放以下内容:

  • 当前轮次的用户问题
  • 最近一轮系统回复
  • 当前任务相关的结构化信息(订单号、商品参数、用户已确认的条款)
  • 不超过两轮的直接对话上下文

这个模式的核心理念是“截断一切非必要信息”。表面上冒进,但实测中,聚焦模式在单任务连续对话里效果最好,因为模型不需要在庞大的历史里翻找焦点。它就像你在写代码时只打开当前文件,不把整个项目几十个文件都贴在屏幕上。

实现上,聚焦模式需要给每轮对话标记一个 task_id 或者 intent_id,每次只捞当前 task 相关的消息。这个标记可以在应用层用简单规则(比如自定义意图识别)完成,也可以交给模型做一次轻量分类。

3.2 漫游模式(Drift):需要跨话题回溯时启用

漫游模式解决的是“用户突然问起十轮之前说过的一件事”的场景。它的窗口策略是:优先把最近两轮完整对话放进去,再根据关键词或向量相似度,从整段会话历史里检索出与当前问题最相关的若干条内容一起拼进上下文。

漫游模式和“全量拼接”的区别在于,它不是把历史全部塞进去,而是只找回“最相关的那几块碎片”。我在项目里用了两种检索方式:

  • 关键词倒排:适合客服系统这种垂直领域,命中率可控,实现简单。
  • 向量检索:适合开放式对话,但需要额外维护 embedding 服务,成本高一些。

漫游模式不能一直开着,否则就退化成全量拼接了。它的启用条件是“检测到当前问题和最近两轮话题不一致,同时和更早的历史消息存在潜在关联”。检测逻辑可以用意图标签比对,也可以用 embedding 相似度的差值来判断。

3.3 压缩模式(Compress):控制 token 预算的关键手段

压缩模式负责把“需要长期保留但不适合全文展示”的信息浓缩成短摘要。它的触发时机有两个:一是上下文累计长度超过预设阈值(比如接近窗口的 60%),二是会话跨度超过一定轮次(比如超过二十轮)。

压缩的常见做法有三种:

  1. 用模型对过期内容做二次摘要,把十轮对话压成两三句。
  2. 抽取出关键结构化字段更新到用户画像里,例如“预算:低”“地区:上海”。
  3. 保留原消息的时间戳和指向 ID,替换正文为超短占位符。

压缩模式最容易被忽略的一点是:摘要本身也会迭代失真。你压缩一次之后,第二次压缩基于上次的摘要,信息层层递减。这个问题我在后面的踩坑章节会专门展开。

3.4 重建模式(Rebuild):会话恢复与跨越时间线的对齐

重建模式应对的是“会话长期挂起后重新激活”的场景。比如上午聊到一半,下午用户又回来了,直接说“继续刚才那个事”。这时候上下文窗口如果只保留最近两轮,完全无法接上;如果全量保留,又会被上午的噪声干扰。

我的处理方式是在会话挂起时,把当时的关键摘要、结构化信息、未完成任务清单快照下来,存在本地。恢复时,把快照注入上下文开头,再往后拼接最近几轮内容。这样模型能快速“对齐状态”,不需要重新读一遍整个上午的记录。

重建模式的精髓在于“快照的设计”:不只是存摘要,还要存“未完成目标”。比如“用户正在比较 A 和 B 两个套餐,尚未决定,待确认预算”。这个未完成任务清单,比任何摘要都更适合引导模型快速回到工作状态。

四种模式不是孤立的,它们组合起来形成一条上下文生命周期管理的流水线:聚焦模式处理当下任务,漫游模式负责跨时空召回,压缩模式控制规模膨胀,重建模式完成会话重启。接下来我给出具体的代码实现,让你能直接落地这个思路。

4. 核心实现:从零搭建一个 ContextManager

4.1 数据结构:把对话拆成带权重的片段

要让上下文可管理,第一步是把“消息列表”升级成“带元信息的片段集合”。我在项目里用一个简单的ContextSegment结构:

from dataclasses import dataclass, field from datetime import datetime from enum import Enum from typing import List, Optional class ContextMode(Enum): FOCUS = "focus" DRIFT = "drift" COMPRESS = "compress" REBUILD = "rebuild" @dataclass class ContextSegment: seg_id: str role: str # user / assistant / system content: str timestamp: datetime task_id: Optional[str] # 关联的任务标识,用于聚焦模式 intent: Optional[str] # 意图标签,用于漫游模式触发判断 weight: float = 1.0 # 重要程度,用于压缩时决定保留顺序 summary: Optional[str] = None # 压缩后的摘要 anchor: List[str] = field(default_factory=list) # 锚点关键词,防失真

这个结构的核心设计是task_id、intent和weight三个字段。task_id让聚焦模式可以快速筛选;intent让漫游模式可以判断“话题是否跳跃”;weight让压缩模式知道哪些片段优先保留原文。

4.2 模式切换的触发条件与状态转移

模式切换不能拍脑袋,我在ContextManager里设计了一个规则引擎,根据当前对话和新输入自动判断是否切换模式。规则优先级从高到低:

  1. 如果会话挂起超过 30 分钟,切到REBUILD,先恢复快照再继续。
  2. 如果当前片段累加 token 数超过窗口的 60%,切到COMPRESS,先压缩再响应。
  3. 如果当前问题的intent与最近两轮不同,且与更早某段历史intent相同,切到DRIFT。
  4. 否则保持FOCUS不变。
class ContextManager: def __init__(self, max_token_budget: int = 6000): self.max_token_budget = max_token_budget self.segments: List[ContextSegment] = [] self.current_mode: ContextMode = ContextMode.FOCUS self.mode_switched_at = None self.min_switch_interval_sec = 30 # 冷却期,防止频繁切换 def add_turn(self, role: str, content: str, task_id: str = None, intent: str = None): seg = ContextSegment( seg_id=f"seg_{len(self.segments)}", role=role, content=content, timestamp=datetime.now(), task_id=task_id, intent=intent, ) self.segments.append(seg) self._maybe_switch_mode() def _maybe_switch_mode(self): # 冷却期检查:切换太频繁会引入震荡 if self.mode_switched_at and ( datetime.now() - self.mode_switched_at ).total_seconds() < self.min_switch_interval_sec: return next_mode = self._decide_next_mode() if next_mode != self.current_mode: self.current_mode = next_mode self.mode_switched_at = datetime.now()

冷却期的设计很关键。最初没有冷却期时,系统会在漫游和聚焦之间来回跳,每次跳变都要重新组装上下文,既增加延迟又让输出不稳定。加了 30 秒冷却之后,这个震荡问题基本消失。

4.3 构建当前上下文的完整流程

核心方法是build_context(),根据当前模式把片段组合成最终交给模型的 prompt 前缀:

def build_context(self) -> str: if self.current_mode == ContextMode.FOCUS: return self._build_focus_context() elif self.current_mode == ContextMode.DRIFT: return self._build_drift_context() elif self.current_mode == ContextMode.COMPRESS: return self._build_compress_context() elif self.current_mode == ContextMode.REBUILD: return self._build_rebuild_context() def _build_focus_context(self): # 只保留当前任务相关的片段,最多补上最近1轮完整对话 focus_segments = [s for s in self.segments if s.task_id == self._current_task_id()] recent = self.segments[-2:] merged = focus_segments + recent return self._format_segments(merged) def _build_drift_context(self): # 最近两轮全量 + 检索到的历史相关片段 + 当前任务关键摘要 recent = self.segments[-2:] query = self.segments[-1].content retrieved = self._retrieve_relevant_segments(query, top_k=5) task_summary = self._get_task_summary(self._current_task_id()) return self._format_segments(task_summary + recent + retrieved) def _build_compress_context(self): # 压缩阈值控制:优先压缩最早且权重最低的片段 compressed = [] for seg in sorted(self.segments, key=lambda x: (x.timestamp, x.weight)): if seg.summary: part = seg.summary else: part = self._summarize(seg, max_chars=80) seg.summary = part compressed.append(part) return "\n".join(compressed[: int(len(compressed) * 0.7)])

_retrieve_relevant_segments可以接关键词匹配,也可以接向量检索。为了不引入额外基础设施,我在客服场景里先用关键词加权,效果达到预期后才考虑升级。代码如下:

def _retrieve_relevant_segments(self, query: str, top_k: int = 5): # 简易检索:先按 intent 过滤,再按关键词重合度排序 scored = [] q_words = set(self._tokenize(query)) for seg in self.segments: if seg.intent != self._current_intent(): continue seg_words = set(self._tokenize(seg.content)) score = len(q_words & seg_words) if score > 0: scored.append((score, seg)) scored.sort(key=lambda x: (-x[0], x[1].timestamp)) return [seg for _, seg in scored[:top_k]]

这个简易检索虽然简陋,但有一个很大的优势:可解释性极强。如果你发现某次召回不对,可以直接打印出命中的关键词和片段,快速定位问题。向量检索虽然省心,但排查“它为什么召回这一段”会非常痛苦。

4.4 快照机制:重建模式的数据基础

重建模式的实现依赖快照,我在每次会话挂起时调用snapshot():

def snapshot(self) -> dict: return { "task_id": self._current_task_id(), "unfinished_goals": self._collect_unfinished_goals(), "key_facts": self._extract_key_facts(), "last_segments": [s.content for s in self.segments[-4:]], "timestamp": datetime.now().isoformat(), } def _extract_key_facts(self): # 用规则抽取:包含“预算、地址、姓名、日期”等实体关键词的片段 facts = [] keys = ["预算", "地址", "姓名", "日期", "需求", "偏好"] for seg in self.segments: if any(k in seg.content for k in keys): facts.append(seg.content) return facts[-5:]

快照不是简单的摘要,而是一个结构化的小型记忆包。unfinished_goals是重建时最关键的引导项——它告诉模型“上次离开时还没做完什么事”,模型就能快速进入“继续干活”的状态,而不是先困惑半天。

5. 实测结果:模式切换在真实任务上的差异

5.1 测试设计与任务类型划分

光有设计还不行,我在内部做了一个受控对比测试。测试会话池来自客服系统的真实脱敏对话,共抽取 120 个会话,覆盖两类典型任务:

  • 信息寻址类:用户在第 1~5 轮提供某个偏好信息,之后隔了 10 轮以上再问回这个信息,模型能否正确引用。
  • 推理决策类:需要在上下文里综合多个约束做判断,例如“预算不高、对性能有要求、品牌不考虑 X”,最后给出推荐。

对比对象是三套上下文方案:旧的“最近 20 轮全量拼接”、纯聚焦模式(不切换)、完整 context-mode(四种模式可切换)。

5.2 结果数字与关键指标对比

实验跑了大概两周,下面是收敛后的数据:

方案信息寻址正确率推理决策正确率平均每轮上下文 token平均响应延迟
全量拼接 20 轮68%61%82002.4s
纯聚焦模式74%79%24001.1s
完整 context-mode86%82%37001.4s

两个最值得注意的结论:

一是纯聚焦模式在推理决策上已经大幅超过全量拼接。原因不难理解——推理任务最怕噪声,你给模型一堆无关历史,它要做两件事:先过滤再推理,注意力分散,错误率自然上升。聚焦模式强制让模型只看到当前任务相关的信息,推理负担轻得多。

二是完整 context-mode 在信息寻址任务上拉开了明显差距。纯聚焦的问题是,当用户问回一个十轮前的问题时,聚焦模式找不到信息;而 context-mode 通过漫游模式触发检索,精确找到历史片段,正确率直接拉升到 86%。

5.3 token 效率的长期收益

token 数据同样有说服力。全量拼接方案的 8200 token 里,真正被模型有效利用的往往不到一半。context-mode 用更少的 token 换来了更高正确率,这就是“少而准”策略的直接回报。

在成本层面,按每 1000 token 约 0.002 美元计算,单次调用从 8200 降到 3700,直接省约 55% 的上下文成本。如果每天调用量上万次,这个差距会非常可观——对自费跑模型的小团队来说,context-mode 不只是效果优化,更是成本优化。

我再强调一遍:这个实验的绝对值肯定受特定业务影响,但趋势是一致的——上下文管理从“堆砌”转向“调制”之后,效果只会更好不会更差。

6. 模式切换踩坑笔记:断层、误触发与失真

6.1 切换瞬间的上下文断层

第一个坑出现在模式切换的瞬间。原来的实现是:切换成功之后,下一次用户提问时直接按新模式构建上下文。结果出现了一个尴尬局面——用户在某次提问触发模式切换后,紧接着又说了一句话,模型居然完全不知道上一轮在聊什么。

排查后发现原因:漫游模式拼接上下文时,为了控制长度,只保留了最近两条完整对话,加上检索片段。但如果检索出的历史片段与上一轮的当前任务重叠度不高,模型相当于“失忆”了。

修复方案是加一个过渡缓冲区:切换模式时,先把最近一轮的完整上下文原样保留在新上下文的底部,再叠加新模式的内容。这样即使检索结果不理想,最近一轮的完整信息仍在,模型不会断层。

6.2 模式误触发:把闲聊当成跨话题回溯

第二个坑更隐蔽。漫游模式的触发条件是“当前问题与最近两轮 intent 不一致”。但用户有时候只是随口一句“那个东西怎么样了”,没有任何明确 intent,系统却因为“检测到话题漂移”切到漫游模式,触发一次全历史检索。结果就是:响应变慢、token 消耗升高,而模型拿到一堆无关历史之后,反而开始胡猜。

排查链路是这样的:我发现日志里漫游模式的切换频率异常高,达到全部会话的 37%。正常预期应该在 12% 左右。逐条查看触发记录后发现,大量触发都发生在用户输入非常模糊、几乎没有任何意图标签的场景下。

修复方式是给切换规则加意图置信度阈值:只有当新问题的 intent 置信度大于 0.6 且与最近两轮不一致,才允许切换到漫游模式。没有明确意图的输入,一律留在聚焦模式,只做最近两轮拼接。

6.3 压缩迭代失真:摘要被二次摘要“洗掉”了关键信息

第三个坑花了我最长时间去定位,因为它不报错、数据也正常,但用户反馈“AI 越来越没记性”。后来我单独写了个脚本,把同一会话每次压缩后的摘要拉出来对比,才发现问题:摘要被反复迭代压缩后,关键实体信息在逐轮衰减。

第一轮压缩时原文是“用户预算 3000 元左右,倾向静音,需要无线”,摘要变成“预算低、静音、无线”。第二次压缩时,上一层摘要继续被压缩,变成了“低预算、静音”。第三次之后,“3000 元”这个具体数字彻底消失,模型只能凭“低预算”去推断,价值取向完全偏离。

修复方案是在压缩时引入锚点标签:在切分摘要时,通过实体识别把“数字、日期、人名、地点、型号”等硬信息摘出来,单独存进 anchor 列表。压缩时 anchor 字段永不参与二次摘要,始终原样保留。重建时,先把 anchor 拼回上下文头部,再补摘要。

这个改动很小,但对长期会话的效果提升非常显著。我后来把所有会话的“信息存活率”做了一次对比:压缩模式引入 anchor 前,三十轮之后关键信息存活率大概只有 45%;引入后,能维持在 85% 以上。

7. 从单会话到多智能体:context-mode 的扩展方向

这套模式虽然是为单会话对话设计的,但它的思路延伸出去之后,在多智能体场景里也有很强的借鉴价值。我在另一个项目里尝试了一种简化版:多个子 Agent 共享同一个记忆池,但每个 Agent 只允许用自己角色对应的上下文模式。

比如负责售前的 Agent 永远用聚焦模式,只看当前询单相关的片段;负责跨部门协调的 Agent 使用漫游模式,需要检索多个会话的历史;负责周报汇总的 Agent 用压缩模式,只拿摘要不需要细节。这个做法让上下文隔离做得非常干净,子 Agent 之间的干扰大幅降低。

还有一个值得探索的方向是模式重放:把一天内用户的会话模式切换轨迹保存下来,作为第二天会话重建的参考。例如用户习惯早上查订单、下午进行售后沟通,重建模式就可以预判性地把订单相关快照优先装入上下文,而不是等模型被问到了才去检索。

context-mode 不是什么高深理论,它就是把“上下文应该怎么组织”这件事从模型手里接回来,用可解释的规则、可维护的结构、可观测的模式切换来管理。如果你也在和上下文爆炸、记忆漂移、token 成本做斗争,可以从最小实现开始:先试聚焦模式,再逐步加漫游和压缩。要做的也只是给对话标上 task_id 和 intent,让上下文第一次拥有“模式”这个维度。

我最后的体会是:模型的能力再强,也顶不住你在上下文里喂垃圾信息。给上下文设计模式,本质上就是替模型先把“该看什么”这个决定做掉,它才能把力气花在“怎么回答好”上。

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

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

立即咨询