上下文模式(Context-Mode)工程实践:多轮对话与Agent的上下文管理设计
1. 从“上下文模式”说起:一个被低估的工程概念
第一次听到“context-mode”这个词,很多人会下意识觉得它是个抽象得没边的东西——上下文?模式?听起来像是架构师在评审会上才会蹦出来的黑话。但如果你真正在工程一线待过,就会发现这个概念其实无处不在,只是大多数人没给它起个正式名字。它描述的是一个系统、一段代码、一个工具甚至一次对话,在处理信息时“看多远、记多少、怎么用”的那套底层规则。
我最早接触这个概念是在做对话系统的时候。当时团队里有个争论:用户问“那它多少钱”,系统到底该不该知道“它”指的是上一轮提到的哪款产品?有人说要记住全部历史,有人说只记最近三轮就够了。吵了两天没结果,最后发现问题的本质不是“记多少”,而是“在什么模式下记”——这就是context-mode要解决的事。它不是一个具体的技术栈,而是一种设计视角:把“上下文如何被组织、传递、截断和消费”当作一等公民来对待。
这篇文章适合谁看?如果你正在做对话系统、Agent应用、RAG检索增强生成、代码补全工具,或者任何需要“记住前文”的软件,那context-mode就是你绕不开的课题。哪怕你只是写一个带多轮交互的客服机器人,理解这个概念也能让你少踩很多坑。我会从设计思路、核心机制、实操落地到问题排查,把我在实际项目里积累的经验完整拆开讲,尽量说人话,让刚入门的同学也能跟上。
2. 内容整体设计与思路拆解
2.1 为什么需要“上下文模式”这个概念
在没有明确context-mode之前,大多数系统的做法很粗暴:把所有历史消息拼成一个超长字符串,一股脑塞给模型或处理函数。短对话还行,一旦轮次多了,问题立刻暴露——token爆炸、延迟飙升、关键信息被淹没在噪声里。我见过一个客服机器人,用户聊到第15轮时它突然开始胡言乱语,排查半天发现是上下文窗口被撑爆了,最早的订单号信息被挤掉了。
context-mode的核心价值就在于:它把“上下文管理”从隐式行为变成显式策略。你可以为不同场景定义不同的模式,比如“严格最近N轮”“摘要压缩”“关键实体提取”“分层记忆”等等。每种模式对应一套明确的取舍规则,而不是让系统在黑暗中随机截断。这样做的好处有三个:可预测、可调试、可优化。你知道系统会看到什么,就能针对性地调优;出了问题也能快速定位是哪一层上下文丢了。
从工程角度看,这其实是一种“关注点分离”。把上下文处理逻辑从业务逻辑里抽出来,单独设计、单独测试。我后来在项目里强制要求:任何涉及多轮交互的模块,必须有一个独立的context manager,明确声明它用的是哪种mode。这个习惯帮我省了至少三次线上事故。
2.2 常见上下文模式的分类与选型逻辑
实际项目里,我总结出四种最常用的context-mode,每种都有明确的适用场景和代价。
第一种:滑动窗口模式(Sliding Window)。只保留最近N轮对话或最近M个token。实现最简单,延迟最低,适合问答类、指令类交互,比如“帮我查天气”“再翻译一下”。缺点是早期信息会丢失,如果用户在第3轮给了关键约束,到第10轮可能就没了。我一般把N设在6到10之间,具体看单轮平均长度。
第二种:摘要压缩模式(Summary Compression)。把历史对话定期总结成一段短文本,保留语义但丢弃细节。适合长对话、咨询类场景。代价是需要额外调用一次总结模型,增加延迟和成本。我的经验是每5到8轮触发一次总结,总结提示词要明确要求保留“实体、数字、用户偏好、未完成任务”这四类信息。
第三种:实体记忆模式(Entity Memory)。不保留原始对话,只抽取并维护一个结构化记忆库,比如用户提到的产品名、订单号、日期、偏好。适合任务型对话,比如订票、下单。实现上通常配合一个轻量抽取器,每轮更新记忆。这种模式token效率最高,但抽取错误会导致后续全错,所以抽取器需要高准确率,并且要有纠错机制。
第四种:分层混合模式(Hybrid Layered)。结合上面几种:最近几轮用原始文本,中期用摘要,长期用实体记忆。这是目前我见过最稳的方案,适合复杂Agent。代价是复杂度高,需要仔细设计各层的触发条件和优先级。我通常把最近3轮设为原始层,4到12轮设为摘要层,12轮以上只保留实体层。
选型时我会问三个问题:这个场景对早期信息的依赖有多强?延迟预算有多少?有没有结构化抽取的可行性?答案不同,模式就不同。没有银弹,只有取舍。
2.3 设计时的关键权衡:信息量、延迟与成本
做context-mode设计,本质上是在三个维度上做平衡:信息量、延迟、成本。信息量越大,模型表现通常越好,但延迟和成本也越高。我见过团队为了追求“完美上下文”,把窗口开到最大,结果单次响应从800毫秒涨到4秒,用户直接流失。
我的做法是先定延迟预算。比如对话场景要求首字响应在1.5秒内,那就反推上下文能占多少token。假设模型处理速度是每token 20毫秒,1.5秒预算里上下文最多占1500 token左右。然后在这个约束下选择模式。如果1500 token不够用滑动窗口,就上摘要或实体记忆。
成本方面,摘要模式每次总结都要额外调用,如果对话轮次多,成本可能翻倍。实体记忆模式虽然省token,但抽取器本身也有推理成本。我一般会做一个简单的成本模型:预估平均对话轮次,乘以每轮上下文处理成本,再乘以日活,看看是否在预算内。这个计算不复杂,但能避免上线后才发现账单爆炸。
还有一个容易被忽略的权衡:可调试性。滑动窗口最好调试,因为你能直接看到原始文本。摘要和实体记忆是“有损”的,出了问题不好回溯。所以我在关键业务里会保留一份完整的原始日志,只用于排查,不进入推理链路。这样既不增加推理成本,又保留了可观测性。
3. 核心细节解析与实操要点
3.1 上下文窗口的截断策略与优先级规则
截断是context-mode里最容易被做糙的环节。很多人直接按时间倒序砍,砍到够为止。但这样会丢掉一些“虽然早但很重要”的信息,比如用户第一轮说的“我对花生过敏”。这种信息一旦丢了,后面推荐餐厅就可能出大问题。
我的做法是给每条上下文打优先级标签。优先级分三档:硬约束(过敏、预算上限、法律要求)、任务关键(当前目标、已确认参数)、一般背景(闲聊、已完成的子任务)。截断时先保硬约束,再保任务关键,最后才按时间砍一般背景。实现上可以在消息对象里加一个priority字段,截断函数按优先级排序后再按时间排序。
具体算法我通常这样写:先计算所有消息的总token,如果没超限就直接用。超限了,先把硬约束全部保留,然后从最新的任务关键消息往前加,直到接近上限的80%,剩下的空间用一般背景填充。留20%余量是为了给模型输出留空间,也防止token计算误差导致溢出。
注意:不同模型对token的计算方式不同,中文和英文的token比例也不一样。我一般用目标模型自己的tokenizer来算,不要用估算值。估算值在短文本上误差可能到20%,长文本上更夸张。
还有一个细节:截断时不要从句子中间砍。我见过系统把“订单号是12345”砍成“订单号是12”,导致后续查询全错。所以截断要按消息为单位,或者至少按句子为单位。如果单条消息就超限,那说明这条消息本身有问题,应该先做摘要或分片。
3.2 多轮对话中的状态维护与传递
多轮对话的上下文不只是文本,还有状态。状态包括当前任务阶段、已填槽位、待确认项等。context-mode必须明确状态怎么存、怎么传、怎么和文本上下文对齐。
我一般把状态和文本分开管理。状态用一个结构化的字典,每轮更新;文本上下文按前面说的模式处理。推理时把状态序列化成一段简短描述,放在上下文最前面,作为“系统提示”的一部分。这样模型既能看到结构化的当前状态,又能看到原始对话细节。
状态维护最容易出的问题是“状态漂移”。比如用户先说“我要订明天的票”,后来说“算了改成后天”,如果状态更新逻辑没处理好,可能两个日期都留着,模型就懵了。我的经验是:状态更新要遵循“后覆盖前”原则,同一槽位的新值直接覆盖旧值,除非用户明确说“两个都要”。同时每轮更新后要做一次一致性检查,比如日期不能是过去、人数不能是负数。
传递方面,如果是单进程,状态放内存就行。如果是分布式,状态要序列化后随请求传递,或者放共享存储。我倾向于随请求传递,因为这样无状态,好扩展。但要注意序列化后的体积,别把状态搞得比上下文还大。
3.3 上下文压缩的粒度控制与信息保留
压缩是门手艺。压得太狠,信息丢了;压得太松,等于没压。我试过几种粒度,最后发现“按语义单元压缩”最实用。具体说,就是把对话按话题切分成若干段,每段压成一句话,保留谁、做了什么、结果如何。
压缩提示词很关键。我常用的模板是:“把以下对话压缩成不超过50字的摘要,必须保留:人名、数字、日期、用户明确表达的偏好、未完成的任务。不要添加对话中没有的信息。”这个模板我迭代了七八版,每一条要求都是踩坑换来的。比如“不要添加”这条,是因为早期模型会自作主张补充“用户可能还想要……”,导致后续推理跑偏。
压缩频率也要控制。太频繁会增加延迟,太稀疏又起不到压缩效果。我的经验值是每5轮或每800 token触发一次,取先到者。压缩后的摘要要标注时间范围,比如“第1到5轮摘要”,方便后续追溯。
还有一个坑:压缩后的摘要如果直接替换原始对话,一旦摘要出错就无法恢复。所以我在关键业务里采用“摘要+最近原始”的混合方式,摘要只覆盖较早的部分,最近几轮始终保留原始文本。这样即使摘要有点偏差,最近的信息还是准的。
3.4 不同模型对上下文模式的适配差异
不同模型对上下文的“消化能力”差别很大。有的模型对长上下文里的中间部分注意力很弱,俗称“lost in the middle”。有的模型对结构化文本更敏感,有的对自然语言更敏感。这些差异直接影响context-mode的设计。
我的做法是:换模型时先做一个小测试。构造一段包含关键信息的长上下文,关键信息分别放在开头、中间、结尾,看模型能不能准确召回。如果中间召回率低,那截断策略就要调整,尽量把重要信息放在开头或结尾。如果模型对结构化文本敏感,那状态序列化就用JSON或YAML格式;如果对自然语言敏感,就用短句描述。
另外,不同模型的上下文窗口大小不同,但“有效窗口”往往小于标称窗口。比如标称128K的模型,实际在32K以后表现就明显下降。所以我在设计时不会把窗口用满,一般用到标称的50%到70%就触发压缩。这个比例也是测出来的,不是拍脑袋。
提示:模型更新频繁,今天的测试结论下个月可能就变了。所以我会把上下文相关的测试做成自动化用例,每次模型升级都跑一遍,确保模式设计仍然有效。
4. 实操过程与核心环节实现
4.1 搭建一个最小可用的上下文管理器
先给一个我实际用过的Python实现骨架,不依赖任何框架,方便你理解核心逻辑。
class ContextManager: def __init__(self, mode="sliding", max_tokens=2000, window_size=8): self.mode = mode self.max_tokens = max_tokens self.window_size = window_size self.history = [] # 原始消息列表 self.summary = "" # 摘要层 self.entities = {} # 实体记忆层 self.state = {} # 任务状态 def add_message(self, role, content, priority="normal"): self.history.append({ "role": role, "content": content, "priority": priority, "tokens": self._count_tokens(content) }) self._update_entities(content) self._maybe_compress() def build_context(self): if self.mode == "sliding": return self._sliding_context() elif self.mode == "summary": return self._summary_context() elif self.mode == "hybrid": return self._hybrid_context() else: raise ValueError(f"Unknown mode: {self.mode}")这个骨架里,add_message负责接收新消息并更新各层,build_context负责按模式组装最终上下文。_count_tokens用目标模型的tokenizer实现,_update_entities做实体抽取,_maybe_compress判断是否触发压缩。
滑动窗口的实现很简单:从最新消息往前取,直到token接近上限。
def _sliding_context(self): selected = [] total = 0 for msg in reversed(self.history): if total + msg["tokens"] > self.max_tokens * 0.8: break selected.append(msg) total += msg["tokens"] return list(reversed(selected))注意这里用了0.8的系数,留20%余量。这个系数可以根据实际表现调整,我一般设在0.75到0.85之间。
4.2 混合模式的完整实现与参数计算
混合模式是我最常用的,这里展开讲。它分三层:最近原始层、中期摘要层、长期实体层。参数计算如下:
假设max_tokens=3000,分配比例是原始层50%、摘要层30%、实体层20%。原始层1500 token,按单轮平均150 token算,大约保留最近10轮。摘要层900 token,每段摘要约100 token,保留最近9段摘要,覆盖约45轮。实体层600 token,存结构化实体,通常够用。
触发规则:每新增5轮或原始层超过1500 token时,把最旧的原始消息移入摘要层;每新增3段摘要或摘要层超过900 token时,把最旧的摘要合并进实体层。
def _hybrid_context(self): raw_budget = int(self.max_tokens * 0.5) summary_budget = int(self.max_tokens * 0.3) entity_budget = int(self.max_tokens * 0.2) raw = self._take_recent_raw(raw_budget) summary = self._take_recent_summary(summary_budget) entities = self._format_entities(entity_budget) return { "entities": entities, "summary": summary, "recent": raw, "state": self.state }组装成最终提示词时,顺序很重要。我一般按“状态→实体→摘要→最近原始”排列,因为模型对开头和结尾注意力最强,状态和最近原始放两端,中间放摘要和实体。
4.3 实体抽取与记忆更新的落地细节
实体抽取我试过两种方案:规则匹配和模型抽取。规则匹配快但覆盖窄,模型抽取准但慢。最后我用了混合方案:先用规则抓明显模式(订单号、日期、金额),再用小模型补漏。
规则部分用正则就够了。比如订单号通常是字母加数字的组合,日期有固定格式,金额带货币符号。这些规则我维护了一个配置文件,方便按业务调整。
模型抽取部分,我用一个轻量提示词:“从以下文本中抽取实体,输出JSON,字段包括:产品名、数量、日期、地点、偏好。没有的字段留空。不要推测。”这个提示词的关键是“不要推测”,否则模型会脑补。
更新记忆时,我遵循“新值覆盖旧值,但保留历史”。具体说,entities字典里每个键存一个列表,新值append进去,但使用时只取最后一个。这样既保留了变更历史,又保证当前值是最新的。如果某个实体被用户明确否定,比如“不是这个订单”,就在列表里标记为invalid,使用时跳过。
注意:实体抽取错误会级联放大。我见过把“不要辣”抽成“辣”的,导致后续全错。所以关键实体要有确认机制,比如在下一轮回复里自然地带一句“您是要不辣的对吧”,让用户有机会纠正。
4.4 上下文组装的最终提示词结构
最终送给模型的提示词,我一般按这个结构组织:
[系统指令] 你是XX助手。当前任务状态:{state}。 已知实体:{entities}。 历史摘要:{summary}。 [最近对话] {recent} [当前用户输入] {user_input}这个结构的好处是层次清晰,模型容易定位信息。系统指令里放状态和实体,让模型一开始就知道“现在是什么情况”。历史摘要放中间,作为背景。最近对话放最后,紧挨着用户输入,保证连贯性。
我试过把摘要放最后,结果模型经常忽略摘要,直接基于最近对话回答,导致早期约束丢失。后来改成现在这个顺序,问题就少了。这个顺序不是理论推导出来的,是A/B测试测出来的。
还有一个细节:系统指令里不要放太多内容。我见过有人把整个实体库都塞进去,几千token,模型反而抓不住重点。我的经验是系统指令控制在300 token以内,只放当前任务最相关的状态和实体。其他的放摘要层。
5. 常见问题与排查技巧实录
5.1 上下文丢失导致的“失忆”问题排查
“失忆”是最常见的问题,表现为模型突然问“您刚才说的订单号是多少”或者给出和前面矛盾的回答。排查时我按这个顺序查:
第一,看原始日志,确认信息确实在历史里。有时候是前端没传过来,不是上下文的问题。
第二,看截断逻辑,确认信息没被砍掉。我会在截断函数里加日志,记录每条消息是被保留还是被丢弃,以及原因。
第三,看组装顺序,确认信息在最终提示词里。有时候信息保留了,但被放在了模型注意力弱的位置。
第四,看模型召回,用同样的提示词手动测一次,确认模型能不能找到。如果手动测能召回,说明是线上组装有问题;如果手动测也不行,说明是模型或提示词的问题。
我遇到过一次典型case:用户在第2轮说了预算5000,第8轮模型推荐了8000的产品。查下来发现预算信息在摘要层,但摘要写的是“用户有预算考虑”,没写具体数字。这就是压缩时丢了关键数字。后来我在压缩提示词里强制要求保留所有数字,问题就解决了。
5.2 上下文过长引发的性能问题与优化
上下文过长不只是token问题,还会导致延迟增加、成本上升、模型注意力分散。我遇到过上下文从2000 token涨到6000 token后,响应时间从1.2秒涨到3.5秒,用户投诉明显增多。
优化手段有几个。最直接的是降低压缩触发阈值,更早压缩。其次是优化摘要质量,让每段摘要承载更多信息,减少摘要层体积。第三是实体层用更紧凑的格式,比如用短键名、去掉冗余字段。第四是分级加载,不是每轮都组装完整上下文,简单问题只用最近几轮,复杂问题才加载全量。
我一般会监控三个指标:平均上下文token、P95响应时间、上下文相关错误率。这三个指标一起看,能快速判断是否需要调整。如果token涨了但错误率没降,说明多出来的上下文没产生价值,应该压缩。
5.3 多轮对话中的指代消解与歧义处理
指代消解是context-mode的硬骨头。“它”“那个”“刚才说的”这些词,模型需要从上下文里找指代对象。如果上下文组织得不好,指代就容易错。
我的做法是在状态里维护一个“最近提及实体”列表,按时间倒序排列。每轮更新。组装上下文时,把这个列表放在系统指令里,标注“最近提及:产品A、订单123、明天”。这样模型看到“它”时,有明确的候选。
对于歧义,比如用户说“改成那个”,而上下文里有多个候选,我会让模型主动澄清,而不是猜。提示词里加一句:“如果指代不明确,先向用户确认,不要假设。”这个策略虽然多一轮交互,但避免了错误累积。
还有一个技巧:在摘要里保留指代关系。比如不写“用户确认了”,而写“用户确认了产品A”。这样即使原始对话被压缩了,指代链还是完整的。
5.4 常见问题速查表
| 问题现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 模型突然失忆 | 上下文被截断或压缩丢失 | 查截断日志和摘要内容 | 调整优先级或压缩提示词 |
| 响应变慢 | 上下文token过多 | 监控token和延迟指标 | 降低压缩阈值或优化摘要 |
| 指代错误 | 指代对象不在上下文或歧义 | 检查最近实体列表 | 补充实体列表或让模型澄清 |
| 状态矛盾 | 状态更新逻辑有误 | 查状态变更日志 | 修正覆盖规则或加一致性检查 |
| 摘要失真 | 压缩提示词不够明确 | 对比摘要和原文 | 强化提示词约束 |
| 实体抽取错误 | 规则或模型不准 | 抽样检查抽取结果 | 加确认机制或换抽取方案 |
这张表是我从多次线上事故里总结出来的,基本覆盖了80%的上下文相关问题。每次出问题先查表,能省不少时间。
5.5 几个我踩过的坑和独家经验
第一个坑:过度依赖摘要。早期我为了省token,把所有历史都压成摘要,结果模型经常丢细节。后来改成混合模式,最近几轮保留原始,问题就少了。教训是:摘要适合做背景,不适合做精确推理的依据。
第二个坑:忽略token计算误差。我用估算值算token,结果线上经常溢出。后来改用目标模型的tokenizer,误差降到1%以内。这个改动很小,但效果立竿见影。
第三个坑:状态和上下文不同步。有一次状态更新了但上下文没更新,模型基于旧上下文回答,和状态矛盾。后来我在组装上下文时强制从状态重新生成系统指令,确保同步。
第四个坑:压缩触发太晚。等到上下文快满了才压缩,压缩时已经很被动,只能砍掉很多信息。后来改成提前压缩,在70%容量时就触发,从容很多。
第五个坑:没有保留原始日志。有一次摘要出错,但原始对话没存,无法回溯。后来强制要求所有对话原始日志保留至少7天,只用于排查,不进推理。
这些经验看起来简单,但每一条都是真金白银换来的。context-mode这个领域,理论不难,难的是细节和边界情况。把细节做扎实,系统就稳了。
6. 上下文模式的扩展与进阶方向
6.1 从单会话到跨会话的记忆延续
单会话的context-mode做好之后,自然会遇到跨会话的问题。用户今天聊了一半,明天接着聊,系统能不能记住?这就是跨会话记忆。
我的做法是把实体层和摘要层持久化,会话层不持久化。具体说,用户ID关联一个长期记忆库,存实体和摘要;每次新会话开始时,加载长期记忆,和当前会话的上下文合并。合并时要注意时间衰减,太旧的记忆权重降低,或者标注“这是上次会话的信息”。
跨会话记忆最大的风险是信息过期。比如用户上次说预算5000,这次可能预算变了。所以我在加载长期记忆时,会标注时间,并在提示词里说明“以下信息来自上次会话,可能已过期,请以本次会话为准”。这样模型会优先使用当前会话的信息。
6.2 多Agent协作中的上下文共享
多Agent场景下,context-mode变得更复杂。每个Agent有自己的上下文,但又要共享一些信息。我的做法是分两层:私有上下文和共享上下文。私有上下文各Agent独立管理,共享上下文用一个中心化的存储,所有Agent读写。
共享上下文要解决并发和一致性问题。我一般用乐观锁,每个Agent更新时带版本号,冲突了就重试。共享上下文的内容要精简,只放跨Agent必需的信息,比如任务目标、全局约束、已完成子任务。细节信息留在各自私有上下文里。
还有一个问题是上下文格式统一。不同Agent可能用不同的模式,共享时要转成统一格式。我一般用JSON作为交换格式,字段包括:来源Agent、时间、内容、优先级。这样各Agent都能解析。
6.3 上下文模式的可观测性与持续优化
context-mode上线不是终点,而是起点。要持续优化,必须有可观测性。我一般埋三类点:上下文组装日志、模型召回测试、用户反馈。
上下文组装日志记录每轮最终提示词的token数、各层占比、截断情况。这个日志用于分析上下文效率。模型召回测试是自动化的,定期用固定用例测模型能不能召回关键信息。用户反馈包括显式评分和隐式行为(比如重复提问、纠正)。
基于这些数据,我会定期做优化。比如发现摘要层召回率低,就调整压缩提示词;发现实体层太大,就精简字段;发现某类问题总是出错,就针对性加规则。这个迭代过程是持续的,没有一劳永逸。
我个人在实际操作中的体会是:context-mode的难点不在技术,而在对业务的理解。你得知道哪些信息重要、哪些可以丢、用户最在意什么。这些不是看文档能学会的,得在真实场景里泡出来。我建议刚接触这个领域的同学,先从一个简单场景做起,把滑动窗口跑通,再逐步加摘要和实体。别一上来就搞混合模式,容易把自己绕进去。先把一种模式吃透,理解它的边界和代价,再扩展。这样每一步都扎实,系统才稳。