☰
context-mode实战:解决LLM多轮对话记忆丢失与上下文管理问题
2026/10/17 3:39:13 网站建设 项目流程

做LLM应用开发的人,大概率都被同一个问题折磨过:模型第一轮回答得挺好,多聊几轮就开始胡说八道,甚至把你开头交代的关键约束忘得一干二净。你反复调整提示词,加各种强调,效果还是时好时坏。

我早期也踩过这个坑,直到后来认真梳理了context-mode(上下文模式)这套东西,才算是把这类问题治住了七八成。它不是什么高深的理论,而是一组关于“如何组织、压缩、注入上下文”的实操方法论。这篇文章就把我自己的做法、踩过的坑、以及可复用的配置模板都摊开来讲,希望能帮你少走点弯路。

1. 为什么模型总是“记不住”:context-mode要解决的底层问题

先说结论:模型本身没有“记忆”。你看到的对话连贯性,全靠每一次请求时把所有历史消息重新发给模型,模型再基于这堆文本生成回复。所以你让它“记住”某件事,本质上是让它在那次请求的上下文窗口里还能找到那条信息。

这带来两个直接后果。第一个是上下文窗口是有限的,不管窗口是几千还是几百万token,总有个上限。第二个是信息会稀释,窗口越大,你塞进去的无关内容越多,模型对关键指令的注意力就越容易被冲散。这也是为什么你明明在开头写了“只回答技术问题”,聊到后面它还是会跟你闲聊——那条指令还在,但它的“权重”已经被大量对话历史压低了。

context-mode的核心思路,就是在这两个约束下做管理:哪些信息必须常驻,哪些可以压缩,哪些干脆丢掉,以及用什么结构把上下文组织得让模型更容易“注意到”重点。它不是某一个具体功能,而是一套设计模式。

我习惯把它拆成三个独立维度来看:

  • 时间维度:多轮对话里,历史消息怎么截取、怎么衰减权重
  • 结构维度:不同类别的信息(用户意图、系统约束、对话历史、外部数据)分别放在什么位置
  • 冗余维度:哪些信息可以摘要化、抽象化,哪些必须保留原文细节

这三个维度互相影响,比如你压缩了历史,省出了空间,就可以让系统约束更完整,或者塞入更多的外部知识。整套context-mode的设计,本质上就是在三个维度之间做取舍。

适合读这篇文章的人,我默认你已经写过几个调用大模型接口的小脚本,知道什么是token、什么是temperature,但还没系统地想过“上下文工程”这件事。如果你刚入门也没关系,后面讲到参数和模板时我会把基础概念一并讲清楚。

2. 三种主流的上下文组织模式,以及怎么选

我总结了三种在实践中验证过比较靠谱的context-mode实现方式,各有各的适用场景。你不需要只选一种,实际项目里通常是三者的组合。

2.1 显式上下文模式:把关键信息钉死在固定区域

这种模式最简单直接:你在系统提示词里划分出一块固定区域,专门用来放“无论如何都不能丢”的信息。比如业务规则、输出格式、禁止事项,全都写在这里。

它的逻辑很朴素——大模型对文本开头和结尾的内容注意力相对更高,把最重要的约束放在固定位置,相当于用位置给信息增加权重。

我之前做过一个模拟项目X,是一个给客服人员用的工单总结工具。系统提示词开头就固定写清楚:

  • 你是客服工单分析助手,只分析工单内容
  • 所有回答必须以JSON格式输出,字段固定为summary、category、priority
  • 禁止输出Markdown格式

然后把每一条工单内容当作用户消息传入。这样无论用户怎么追问、怎么换说法,模型输出的格式都不会乱套。这个模式的缺点是占空间,所有常驻信息都完整写在每次请求里,token开销固定且不小。

2.2 压缩上下文模式:用摘要给历史瘦身

多轮对话场景下,历史消息是最大的token消耗来源。同样是聊了20轮,有的消息只是“嗯嗯”“好的”,有的消息包含了用户的真实需求,全部原样保留显然不划算。

压缩上下文模式的核心做法是:定期把旧消息做摘要,用一小段话概括关键信息,替代原本的大段对话文本。举个例子,原始对话里用户说了一大段他的项目背景、技术栈、遇到的报错信息,摘要后变成一句“用户正在用某跨平台框架开发一个小程序,遇到编译阶段报错,已尝试升级依赖但未解决”。

摘要在前,原文在后,模型回答时先看到摘要,对整段历史的把握比直接看20条消息要清晰得多。我实测下来,20轮以上的对话,用摘要模式能把token消耗降到原来的三分之一左右,同时回答的相关性反而更高。

但这里的坑在于,摘要本身也会丢信息。如果用户在那段历史里透露了一个关键细节,你摘要时没提炼进去,后面模型就可能忽略它。所以摘要模板里我一般会强制要求保留四类信息:用户的目标、已尝试的操作、当前状态、明确提到的约束。

2.3 结构化上下文模式:用格式引导模型抓取关键信息

第三种模式是给上下文一个明确的骨架,让模型不是在文本流里“找”信息,而是从字段里“读”信息。最常见的做法是用JSON或者YAML组织上下文内容。

比如同样要告诉模型“用户是个新手”,你写一句“用户是小白用户,请用简单的语言解释”,模型可能记得,也可能忘记。但如果你在上下文里放一个结构化的用户档案字段:"user_level": "beginner", "communication_style": "simple",模型每次读取时都会明确地把它当作一个属性来处理,遵循度会高非常多。

结构化上下文模式还有一个额外收益:它天然适合程序化处理。你可以在代码里动态更新某些字段,比如用户等级从beginner变成intermediate,你只需要改一个字段的值,而不是重写一大段提示词。

2.4 三种模式的对比与选型

模式适用场景token开销实现复杂度主要风险
显式上下文规则固定、输出格式要求严格的场景中高,固定开销低占用窗口空间,信息过多时会挤压其他内容
压缩上下文多轮对话、长会话历史低,随摘要策略浮动中摘要丢失关键细节,无法恢复
结构化上下文需要精确控制变量的场景中,取决于字段数量中高字段设计不合理会导致模型“读不懂”

我给一个选型建议:系统级规则直接用显式模式,历史对话长用压缩模式,凡是涉及用户属性、业务参数这类离散变量,一律用结构化模式。三者不冲突,实际项目中三层叠加才是常态。

3. 实操:在真实场景中把context-mode落地

理论说完了,来点能直接抄作业的。这里我以一个多轮对话的售后支持助手为例,把三种模式组合起来实现,代码用Python写,逻辑和数据结构都可以直接改成你自己的项目。

3.1 场景设定与整体架构

这个售后助手的业务约束是:只回答某公司自研硬件产品的使用和排障问题,不聊其他话题。输入可能是用户的一长段问题描述,输出需要按固定格式返回。会话可能持续很多轮,用户经常隔几天再回来继续问。

这样的场景,三种模式正好都用得上:

  • 显式模式负责“只聊产品排障”和“按格式输出”这两条铁律
  • 压缩模式负责处理跨天、多轮的历史会话
  • 结构化模式负责维护用户信息,比如设备型号、系统版本、已尝试的操作

整体请求的上下文组装流程是这样的:先拼系统级指令(显式模式),再拼用户档案(结构化模式),最后拼压缩后的历史摘要加当前问题(压缩模式)。

3.2 上下文模板设计

我实际用的模板长这样。注意看各个部分的排列顺序和边界标记,这里的细节决定了模型能不能准确区分哪些是“命令”、哪些是“数据”。

system_template = """ [系统指令] 你是某硬件品牌的售后技术支持助手。你的唯一职责是帮助用户解决该品牌产品的使用和故障排查问题。 对于与产品无关的问题,一律回复:"抱歉,我仅支持解答该品牌产品的相关问题。" [输出格式] 所有回复必须严格遵循以下JSON格式: { "category": "问题分类,取值为hardware/software/other", "confidence": "置信度,0到1之间的小数", "reply": "给用户的回复内容" } 禁止输出JSON之外的任何内容。 [用户档案] {user_profile_json} [历史摘要] {conversation_summary} 请基于以上信息和用户当前的问题进行回答。 """

虽然看起来只是几个中括号标题,但实际效果差异很大。给每个区域加上明确的标题,相当于给模型一个“分区导航”,它知道从哪个区块去取什么信息。没有这些标题,把用户档案和历史摘要直接堆在一起,模型偶尔会搞混两者的边界。

3.3 历史会话的压缩实现

压缩这一步是关键。我不建议每次把全部历史消息丢给模型做摘要,那样成本太高。我用的策略是滑动窗口摘要:保留最近两轮完整消息,更早的消息按每10轮一组的粒度做摘要,并把摘要结果追加到conversation_summary里。

实际代码大致是这样:

class ContextManager: def __init__(self, summary_model): self.summary_model = summary_model self.recent_messages = [] self.summary = "" self.summary_count = 0 def add_message(self, role, content): self.recent_messages.append({"role": role, "content": content}) if len(self.recent_messages) > 4: self._summarize_oldest() def _summarize_oldest(self): # 取最早的两条消息做摘要 oldest = self.recent_messages[:2] text = "\n".join([f"{m['role']}: {m['content']}" for m in oldest]) prompt = f"请用不超过50字概括以下对话中用户的真实需求、已尝试操作和当前状态,只输出概括内容:\n{text}" self.summary = self.summary_model(prompt) self.summary_count += 1 # 每次摘要后,摘要本身也需要被再次概括,否则无限膨胀 if self.summary_count % 5 == 0: prompt = f"请将以下摘要进一步压缩到100字内,保留所有关键信息:\n{self.summary}" self.summary = self.summary_model(prompt) del self.recent_messages[:2] def build_context(self): return self.summary, self.recent_messages

有个容易漏掉的点:摘要本身也会随着时间变长,如果不做二次压缩,摘要会越来越大,最后起不到省token的作用。所以我每做五次摘要后,会把已有摘要做一次“二轮压缩”,把关键信息浓缩到100字以内,保证摘要的长度始终可控。

3.4 拼接完整上下文时的细节处理

组装完整上下文时,我见过不少初学者把当前问题和历史摘要拼接得完全没区分。比如直接:

“以下是历史记录:...下面是用户问题:...”

这种写法其实不够稳妥。我推荐的做法是让当前问题以独立的用户角色消息传入,那在当前轮消息里,系统指令和用户问题分离得更干净。合在一起传入时,保持各区块有清晰的边界标记即可。实际组装代码:

cm = ContextManager(summary_model=my_summary_model) # 假设这是当前用户的问题 current_question = "我的设备固件升级之后频繁重启,之前你说可以把降级包发我,现在还能发吗?" # 获取压缩后的摘要与最近消息 summary, recent = cm.build_context() # 构建设备档案(结构化上下文) user_profile = { "device_model": "X1 Pro", "os_version": "3.2.1", "last_action": "请求固件降级包" } system_prompt = system_template.format( user_profile_json=json.dumps(user_profile, ensure_ascii=False), conversation_summary=summary ) # 完整上下文 = 系统指令 + 近期历史 + 当前问题 messages = [{"role": "system", "content": system_prompt}] messages.extend(recent) messages.append({"role": "user", "content": current_question}) response = call_model(messages)

这里有一个很实用的技巧:把结构化档案user_profile做成代码里可控的变量,而不是硬编码在提示词模板里。这样当用户的状态发生变化,比如固件版本升级了,只需要改dict里的字段值,模板不用动,模型拿到的信息就是最新的。这个习惯对于后期维护来说非常重要,否则每次需求变化都要改模板,迟早改出问题。

3.5 关键参数与上下文的配合

很多人忽略了生成参数和context-mode的联动。我在这个项目里调试出来的参数组合是:temperature设到0.3,因为售后场景要求回答稳定、口径一致,温度太高容易让模型在JSON格式里偶尔穿插多余语气词。top_p设到0.9,给输出保留一点多样性,避免回答过于死板。max_tokens根据模板里reply字段的预期长度设置,我这边设的是800,太长会浪费,太短会导致输出被截断成无效JSON。

另一个容易被忽略的参数是频率惩罚(frequency_penalty)。在多轮对话里,模型偶尔会重复之前已经给过的建议,尤其是排障步骤翻来覆去就那么几步。我把它设到0.4,能明显减少重复内容。但这个参数不能设太高,太高会导致回答表述过于跳跃,反而影响可读性。

说句实在话,参数这个东西没有万能组合,必须放到你自己的上下文结构里去试。你先固定一套context-mode模板,再调参数才有意义。模板不稳定的时候调参数,等于在一个移动的靶子上练枪法。

4. 实操中的拦路虎:问题排查与避坑经验

这套方案跑起来之后,我陆续遇到了一些典型问题。有些能从日志里直接看出来,有些得靠对比实验才能定位。这里挑五个我踩得最深的说。

4.1 上下文被截断:最隐蔽的“记忆”丢失原因

现象:对话到第10轮左右,模型突然忘了用户最开始的设备型号。

排查后发现不是模型“忘了”,而是整个请求的token数超过了上下文窗口上限,最前面的系统指令被悄悄截掉了。这种情况在多轮长对话里特别容易发生,而API调用往往不会报错,你只会发现模型表现变差。

解决办法:在每次调用前主动做token统计,接近上限时对历史消息强制二轮压缩,或者直接把最近的完整消息砍到只剩一条。提前做度量,好过事后从结果反推。

我这边会在调用模型前先算一算:

approx_tokens = len(system_prompt) // 2 + sum(len(m["content"]) // 2 for m in recent) + len(current_question) // 2 if approx_tokens > 3000: # 动态收缩近期消息,只保留最后一条完整消息 recent = recent[-2:]

这个估算方法是拿中文字符数除以2近似当成token数,不一定精确,但作为告警阈值够用了。触发收缩条件时自动调整消息数量,能大概率避免截断问题。

4.2 结构化字段被模型无视

现象:JSON里明明写了"user_level": "beginner",模型还是用很技术的术语回答。

排查后发现是用户档案在提示词里的位置太靠后,被历史摘要挤到了对注意力权重较低的区域。把用户档案挪到系统指令之后、历史摘要之前的位置后,问题立刻缓解。

还有一个相关的问题:字段名要起得足够直白。我曾经用过"exp_level"这个字段,模型的理解明显不如"user_level"稳定。你是在写给模型看的“数据结构”,不是在写代码里的变量名,越接近自然语言越好。

4.3 压缩摘要把关键信息“压没了”

现象:用了压缩模式之后,用户在第6轮提到的“我已经自己试过重启了”,在第10轮时模型完全没参考,又建议用户去重启。

查了下摘要内容,发现那句“已尝试重启”没有被写进摘要。原因是我最初的摘要指令重点是“用户的需求和当前状态”,没有明确要求记录“用户已经尝试过的操作”。这个信息其实对排障特别重要——重复建议已试过的操作,会让用户觉得你在敷衍。

后来我在摘要模板里单独加了一行强制要求:必须保留“用户已尝试的操作”,并且放在摘要的开头。从那以后这个问题基本绝迹。

4.4 格式时好时坏

现象:输出JSON的格式不稳定,时而是合法JSON,时而在JSON前后多一句解释。

最开始我以为是temperature太高,降到0.1之后有所改善,但没根治。真正的原因是我在一次模板改动中不小心删掉了“禁止输出JSON之外的任何内容”这句约束。加回去之后,配合0.3的temperature,格式稳定性达到了99%以上。

这类问题给我的教训是:当输出格式出问题时,先检查是不是系统指令里的边界约束不见了,再去调参数。参数是辅助,明确的约束才是根基。

4.5 问题速查表

现象可能原因优先排查项
模型忘记早期信息上下文被截断检查token总量是否接近窗口上限
结构化字段不生效字段位置太靠后/命名不直白调整字段位置,改自然语言风格字段名
重复建议已试操作摘要丢失“已尝试操作”信息检查摘要模板是否强制包含该内容
JSON输出不稳定边界约束缺失检查是否写明“禁止输出其他内容”
长对话回答质量下滑摘要没有二次压缩检查摘要长度是否已经失控膨胀

5. 把context-mode沉淀成一套自己的工作流

走到这里,其实已经不只是“怎么让模型记住东西”了,而是一套关于上下文资产管理的方法论。我自己做下来的体会是,上下文跟代码一样,需要版本管理。模板改一句话,可能直接影响所有下游行为,但我经常改完就忘了,等出问题才反应过来。

后来我养成了一个习惯:每套模板都带上一个版本号注释,改动模板时要记录“改了哪里、为什么改”。遇到模型行为异常,先看模板最近一次改动是不是罪魁祸首。这听起来很基础,但真的能省掉大量排查时间。

另外建议你给自己的项目也建一个“上下文日志”:每次请求记录一下摘要长度、最近消息条数、token总量、模型输出是否合规。跑一段时间之后,你能用这些数据反过来校准自己的压缩策略和窗口阈值,而不是一直拍脑袋决定。我大概集了500条日志之后,才找到当前项目里summary长度和回答质量之间的平衡点。

最后再分享一个小技巧:context-mode的调试尽量用固定测试集。准备20条有代表性的用户输入和对应的期望输出,每次改完模板,先跑这20条,对比结果。别指望肉眼在对话里发现问题,那太随机了。有固定测试集,你才能确定改动到底是变好了还是变坏了。这套工作流让我从“被模型的不确定性带着走”,变成了“能预测和控制它的输出”,我觉得这也算是LLM应用开发里最值得修炼的基本功了。

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

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

立即咨询