大模型多轮对话上下文管理:context-mode实战指南

发布时间:2026/9/10 4:50:31
大模型多轮对话上下文管理:context-mode实战指南 开头做 AI 应用最怕什么不是模型不够聪明是模型“记性太差”。我之前带着团队做一个知识库问答助手上线第一天就被用户吐槽“我刚问的内容换个说法再问一遍它居然不记得了。”排查来排查去问题就出在没有一套上下文管理机制。后来我们把 context-mode 这套方案完整落地到系统里多轮对话的连续性才真正稳下来。这篇文章就是围绕 context-mode 这个主题把我们在实际项目里的设计思路、实现路径、工程落地和踩坑记录完整梳理一遍。不管你是正在做智能客服、Copilot 工具、AI 搜索还是 Agent 工作流只要你的产品需要跟大模型做多轮交互这套东西就值得仔细看一遍。文章不会只讲概念会直接给出可以复用的模块设计和代码片段你看完就能在自己项目里抄作业。1. 先搞明白 context-mode 到底在解决什么问题1.1 为什么多轮对话的“上下文”这么难管大模型的 context window 是有限的哪怕像目前主流模型一样能支持几十万 token 的上下文也不能无限塞。而且塞得越多推理越慢、成本越高模型还容易“注意力漂移”——也就是被一堆无关紧要的历史信息干扰抓不住用户当前最重要的意图。更麻烦的是用户说话是有“信息密度”的。一次 20 轮的对话里真正对后续回答有长期影响的内容可能只占 10%用户确认过的偏好、明确拒绝过的方案、某个专有名词的定义、过程中的关键决定。剩下的 90% 都是寒暄、重复确认、废话和中间尝试。如果把这些全部塞给模型它分不清哪些是“需要长期记住的事实”哪些只是“本轮临时聊天的背景音”回答自然会飘。这里可以用一个生活化的类比。你让一个实习生去旁听一个 1 小时的会议然后让他写会议纪要。如果他只是把录音全文丢给 AI 转写最后出来的东西一定没法看。一个合格的实习生会记下结论、待办、责任人忽略中间的争论和废话。context-mode 干的活本质上就是给大模型当这个“会做会议纪要的实习生”。1.2 context-mode 的产品定位和适用场景从产品视角看context-mode 不只是一个开关而是一整套“上下文处理策略”的组合它决定了哪些历史信息需要保留、以什么形式保留、在什么时机注入到当前请求里、以及超出预算时如何降级。我们当时做的是企业知识库问答助手典型对话长这样用户先问一个制度问题然后接着问“如果员工请了病假这个制度还适用吗”再过几轮又问“综合刚才说的两种情况能不能给我一个判断流程”。你会发现第三轮的问题天然依赖前两轮的讨论结论。没有上下文管理模型根本接不上这种对话。适合接入 context-mode 的场景我大致归纳成四类多轮客服机器人用户在同一个 session 里反复修改需求系统要记住每次修改的内容。AI Copilot / 编程助手用户在对话里定义了代码风格、指定了某个函数名后续所有生成都要遵守。AI 搜索与问答需要结合历史问答意图理解当前问题的真实指向。Agent 工作流模型要基于之前多步工具调用的结果继续推理上下文一旦断裂整个流程就废了。只要你的产品落在这些场景里context-mode 就是刚需不是可选项。2. 三种主流的 context-mode 实现路径2.1 显式持久模式把关键信息“钉”在上下文里显式持久模式的核心思路是把对话里出现的“关键信息”抽出来用结构化的形式独立存储每次请求构造 prompt 时直接把这段结构化信息注入进去不依赖对话历史的完整性。那什么是“关键信息”在我们的知识库助手里定义了三类用户意图用户到底想干什么、实体部门、员工、日期、制度编号等、约束条件用户明确说过的限制比如“只看销售部”“不考虑临时工”。抽取动作可以在每一轮对话结束时触发也可以由意图识别模块在特定节点触发。举个例子用户说了这么一段话我们销售部的员工下个月请了三天病假按照公司制度绩效奖金怎么算对了不要考虑那些已经离职的人。抽取出来的结构化信息就是{ intent: query_leave_and_bonus, entities: { department: 销售部, leave_type: 病假, leave_duration: 3天, time: 下个月 }, constraints: [排除已离职员工] }每次构造 prompt 时这段 JSON 放在对话历史之前模型就知道这些信息是“已经确认过的事实”不需要再从历史里重新推断。优点是稳定性极高只要 schema 设计得好关键信息基本不会丢。缺点是需要额外的抽取逻辑和字段定义业务复杂时 schema 会变得很胖。这种模式最适合任务明确、流程固定的场景比如请假审批、订单查询、表单填写类对话。2.2 窗口滑动模式让模型始终看到“最近发生的事”窗口滑动模式是最简单粗暴的做法只保留最近 N 轮对话超出窗口的部分直接丢弃。很多初版产品都是这么干的因为一行代码就能实现。但它的缺陷非常明显。第一窗口长度不好定设短了容易被用户投诉“失忆”设长了 token 成本扛不住。第二信息丢失没有区分度用户第 3 轮说的关键需求如果对话到第 15 轮才被用到不好意思已经滑出窗口了。我们一开始踩过这个坑窗口设为 10 轮结果用户在第 11 轮问“我刚才说的那个方案行不行”模型完全不知所云。后来我们的改进是按 token 数滑动而不是按轮数滑动。因为一轮对话可能是 5 个字也可能是 500 个字。用轮数切分误差太大用 token 数才可控。实现上也简单维护一个对话消息队列每次新消息进来就累加 token超过阈值就从头部弹出。为了让“最近的对话”始终贴近用户当前输入我们还会在最终组装时调整顺序最近的对话永远放在 prompt 里离当前问题最近的位置。单纯用窗口滑动模式适合那些“本轮对话基本独立历史参考价值有限”的场景比如简单的天气查询、翻译工具。但做复杂业务它只能当最底层的基础设施不能当作完整的 context-mode 方案。2.3 关键摘要模式压缩历史保留记忆精华摘要模式是对窗口滑动模式的一种升级与其把历史粗暴丢进回收站不如把历史“压成一句话”塞进上下文里。具体做法是对话累积到一定轮数或 token 阈值时触发一次摘要。摘要的内容要回答几个固定问题用户确认了什么需求、拒绝了什么方案、留下了哪些偏好、待办事项有哪些。然后把摘要作为一整个系统消息放在每次请求的 prompt 里。模型既不需要读完整历史也能通过摘要了解前因后果。我们在项目里用的是混合触发策略每满 8 轮触发一次固定摘要同时一旦发现单轮 token 超过预算的 20%立刻触发一次紧急摘要。摘要不是每次都完整重写而是增量更新——新摘要会引用旧摘要的内容再补充最近几轮的新信息。这样才能避免摘要越做越长。必须强调一点这三种模式不是互斥的真正靠谱的 context-mode 往往是它们的组合。窗口滑动负责兜底近几轮、摘要负责压缩中长历史、显式持久负责锁定最关键的实体和约束。我们最终的线上方案就是“显式持久 窗口滑动 增量摘要”三合一具体怎么组合下面会说。实现模式稳定性成本实现难度适合场景显式持久模式高低中任务型对话、结构化信息多的场景窗口滑动模式低低低历史参考价值弱的场景、兜底基础设施关键摘要模式中中中高长对话、需要压缩历史的场景2.4 实际选型时我的建议别一上来就追求最复杂的方案先回答三个问题第一用户的后续问题依赖前文信息的比例有多高如果高窗口滑动模式必须加摘要或显式持久。第二对话里有没有强结构化的关键信息如果有优先做显式持久因为它最稳。第三上下文预算紧不紧张紧张就优先上摘要避免成本失控。我们知识库助手的组合方案是这样的实体和约束走显式持久最近 6 轮对话全量保留超过 6 轮的部分让摘要模块接管。相当于把信息分成三层第一层是“钉子”永远在第二层是“临时桌布”近几轮随便铺第三层是“仓库”旧东西打包好要用的时候再拆。3. 工程化落地把 context-mode 真正跑起来3.1 核心模块与数据结构设计理论讲再多不落地都是空谈。我直接给你看我们项目里可复用的最小实现。用 Python 写一个 ContextManager它管三件事维护各类上下文数据、控制 token 预算、生成最终 prompt。class ContextManager: def __init__(self, total_budget: int 8000): self.total_budget total_budget # 第一层显式持久化信息不走 token 滑动常驻上下文 self.sticky_info { entities: {}, constraints: [], user_preferences: {} } # 第二层最近对话缓存按 token 计算超出就淘汰 self.recent_messages [] self.recent_token 0 self.recent_limit 3000 # 第三层历史摘要由摘要模块生成和维护 self.history_summary self.summary_token 0 self.summary_limit 1500 # 预算分配时剩余部分留给系统指令和当前用户输入 self.system_prompt_token 2000 def add_message(self, role: str, content: str): token_count estimate_tokens(content) self.recent_messages.append({role: role, content: content}) self.recent_token token_count self._trim_recent() def _trim_recent(self): # 按 token 从旧到新淘汰始终保持最近的消息在队列尾部 while self.recent_token self.recent_limit and len(self.recent_messages) 1: removed self.recent_messages.pop(0) self.recent_token - estimate_tokens(removed[content])这个结构对应三种模式sticky_info 对应显式持久recent_messages 对应窗口滑动history_summary 对应关键摘要。分开管理互不干扰后续优化某个模块不会拖累其他部分。3.2 上下文注入顺序与预算分配拿到了三部分上下文接下来最关键的步骤就是组装 prompt。组装顺序是有讲究的不能随便排。我们最终固定的顺序如下系统指令System Prompt定义角色、能力边界、回答规范。显式持久信息用户实体、约束、偏好让模型明确知道哪些是既定事实。历史摘要如果存在说明之前的对话结论。最近对话仅保留近几轮。用户当前的输入。这里有两个原则。第一系统指令和显式信息必须放在前面因为它们是需要被模型“优先服从”的内容越靠前注意力权重越高。第二最近对话必须紧贴当前输入保证模型在回答时能看到完整的“前因”。预算分配上我建议按比例切分而不是按绝对值切分因为不同模型的总窗口不一样。用一个 8k 总预算的例子budget_plan { system_prompt: 2000, sticky_info: 500, history_summary: 1500, recent_messages: 3000, current_input_reserved: 1000, } def build_prompt(self, user_input: str): sections [ fSystem: {self.system_prompt}, fStickyInfo: {json.dumps(self.sticky_info, ensure_asciiFalse)}, fHistorySummary: {self.history_summary}, *[f{m[role]}: {m[content]} for m in self.recent_messages], fUser: {user_input}, ] prompt \n\n.join(sections) # 最后一个动作超限检查 if estimate_tokens(prompt) self.total_budget: prompt self._truncate_to_budget(prompt) return prompt如果你用 OpenAI SDK 这类工具消息数组的顺序也是同样的逻辑。关键点在于不是把所有内容堆给模型而是分层给。模型跟人一样一次性读太多信息会疲劳但给它一个清晰的层次结构它的表现会稳定得多。3.3 更新策略与触发时机上下文管理最容易被忽略的是“什么时候更新”这个问题。一开始我们做的是每次请求都全量重算上下文结果 token 消耗巨大而且频繁刷新摘要反而导致信息波动。后来改成事件触发式更新效果立刻改善。触发时机我们定了四类用户完成了关键操作比如提交了一个表单、确认了一个方案立刻更新 sticky_info。消息轮数达到阈值我们设为 8 轮触发增量摘要。单轮消息 token 异常大超过预算 20%触发紧急摘要并压缩 recent_messages。新用户输入进来时只做读取和组装不主动改任何状态。def process_user_input(self, user_input: str): self.add_message(user, user_input) if self._should_extract_sticky_info(user_input): extracted extract_entities_and_constraints(user_input) self._merge_into_sticky_info(extracted) if self._should_trigger_summary(): self._update_summary() return self.build_prompt(user_input)这里面最核心的经验是更新的时机比用什么算法更重要。你摘要算法再强触发频率不对效果也好不了。触发太频繁摘要一直在变模型无法建立稳定的“记忆”触发太少摘要跟不上新信息等于白做。3.4 可观测性一定得能“回放”上下文这一点我放到工程化里讲是因为吃过亏。之前线上 prompt 偶尔“抽风”输出质量突然下滑但我们不知道模型到底看到了什么。没有上下文快照排查无从下手。后来我们给 ContextManager 加了快照日志每次请求都记录三样东西组装后的完整 prompt、各部分的 token 占比、以及 sticky_info 的变化历史。日志量级不大但这些数据在排查问题时价值极高。def log_snapshot(self, prompt: str): log_entry { timestamp: time.time(), prompt: prompt, token_breakdown: { system: estimate_tokens(self.system_prompt), sticky: estimate_tokens(json.dumps(self.sticky_info)), summary: estimate_tokens(self.history_summary), recent: self.recent_token, }, sticky_version: self.sticky_version, } logger.info(json.dumps(log_entry, ensure_asciiFalse))建议你把上下文快照和业务日志、模型响应日志关联到同一个 trace_id。这样用户反馈一个问题你直接按 trace_id 拉出当时的完整上下文立刻能判断是模型的问题、上下文管理的问题、还是用户本身输入就有歧义。4. 常见问题与排查技巧实录4.1 上下文漂移模型回答着回答着就跑题了现象同一个 session 内前几轮回答很精准越到后面越偏甚至开始自说自话跟用户问的完全不搭边。排查思路先看上下文快照重点检查 sticky_info 和 history_summary 是否被“污染”。我们遇到过这样的情况摘要模块把用户的一句玩笑话“我可以接受任何方案”当成了真实偏好写进了 sticky_info结果后续每一轮模型都在迎合这个并不存在的偏好。定位方式很简单打开快照日志看模型突然跑题的那一轮前后sticky_info 或摘要里多了什么奇怪内容。解决办法给摘要和实体抽取都加置信度阈值。抽取出来的实体或约束如果置信度低于 0.7宁可不进 sticky_info。另外每过一段时间比如每 20 轮把摘要和 sticky_info 发给模型做一次“一致性校验”让它判断这些信息是否与最近对话矛盾有矛盾就触发更新。4.2 token 超限和成本失控现象某个用户对话特别长请求报 400 错误或者月底看账单发现成本翻倍。排查思路token 超限多半是摘要模块写得太长或者 sticky_info 里的实体被后续过程不断追加越攒越多。成本飙高通常是因为某些请求没有走 context-mode 的预算控制完整历史一股脑全塞进去了。解决办法给 total_budget 设硬上限组装完 prompt 之后必须做一次校验超了就按优先级丢弃——先丢 history_summary 的冗余内容再丢 recent_messages 里最早的消息最后显式降级模型给出简短回答不做深度分析。注意token 估算一定要用实际模型的 tokenizer不能简单按字数算。中文一个字可能对应 1-2 个 token英文一个词也可能拆成多个 token。估算不准前面所有的预算分配都是空中楼阁。4.3 关键信息丢失用户说“我刚刚说的那个”却接不上现象用户明明在前几轮提到了一个具体方案后面用指代词提问模型完全不知道指的是什么。排查思路这类问题几乎都是显式持久层没做好的表现。用户说的是“那个方案”但方案名称、方案内容都没有被抽取成结构化实体只存在于 recent_messages 里一旦被滑出窗口就丢了。解决办法强化实体抽取模块把用户的关键指代词与历史实体做“指代消解”。实现上可以简单粗暴用户输入里出现“那个”“这个”“刚才的”等指代词时自动从 sticky_info 里最近更新的三个实体中做匹配把匹配结果拼接到用户输入后面。比如User: 那个方案可以落地吗 AugmentedInput: 用户指代的“那个方案”是【降低销售提成至5%的方案】针对该方案提问那个方案可以落地吗这个技巧非常有效而且实现成本低。4.4 结果不稳定同样的问题前后答案不一致现象用户把同样的问题换了种说法模型给出的答案关键数字或结论明显冲突。排查思路这里要区分是上下文没生效还是模型本身的采样随机性导致的。先在 context 快照里确认两次请求的 prompt 是否一致。如果不一致问题大概率出在上下文组装没做好如果完全一致则是模型温度参数和采样策略的问题。解决办法把 temperature 调低同时引入约定俗成的“回答一致性自检”——在系统指令里加一句“回答前先对比当前信息和历史结论若存在矛盾请向用户说明并询问确认”。模型本身有对比能力只要你给它对比的依据也就是完整的摘要和 sticky_info效果立竿见影。4.5 常见问题速查表把上面这些经验整理成一张速查表你在排查时直接对着查。现象可能原因优先排查项解决方案跑题、答非所问摘要或实体被污染排查 sticky_info 最近更新加置信度阈值做一致性校验token 超限摘要过长/sticky 膨胀看 token breakdown 日志设硬上限按优先级丢弃指代词接不上显式持久层缺失检查实体抽取结果做指代消解增强附加实体上下文同问不同答prompt 不一致或采样随机对比两次 context 快照固定 prompt 骨架调低 temperature成本飙升部分请求绕过预算控制看请求日志的 token 分布统一走 ContextManager禁止裸拼 prompt另外补一个通用技巧任何 context-mode 相关 bug先复现再修。复现不了的问题因为有快照日志也能直接定位到当时的输入和输出。一定要把“可回放”当作硬性要求不要省这一步。5. 进阶扩展面向 Agent 场景的 context-mode 演进5.1 从单轮对话到多步工具调用如果你的产品不是简单问答而是 Agent 自动化context-mode 要管理的内容就更多了工具调用历史、中间返回结果、子任务状态、多 Agent 之间传递的数据。上下文一旦断裂整个任务链条会完全瘫痪。我的建议是引入“任务栈”机制每次 Agent 要执行一个子任务就把当前任务上下文入栈包括目标、输入参数、依赖的历史结果。子任务执行完毕把结果和状态合并回栈顶。一旦某个环节出错可以从栈的某个位置回滚而不是让整个上下文作废。这种设计其实跟多人协作很像——每个人只负责自己的部分但必须清楚地知道上游给了什么、下游要什么。5.2 用向量召回补充长期记忆摘要模式住的时间长了也会遗忘细节。比如用户在第 100 轮提了一句“我偏好用数据透视表呈现”第 200 轮想引用这个偏好光靠摘要大概率拉不回来。这种情况下可以考虑给关键历史片段做向量化存储。每当摘要模块生成或更新时同步把摘要和历史消息切成小块存进向量数据库。当前用户输入进来时先用 embedding 检索最相关的历史片段把命中的片段临时加到 prompt 的上下文里。我们实测下来这个方案在长会话场景里能把相关信息的召回率提升不少适合对上下文深度有苛刻要求的场景。但代价是多了向量检索这一跳响应时延会增加几十毫秒需要你根据业务场景做取舍。5.3 上下文分层与生命周期管理最后分享一个项目架构层面的建议把上下文分成 L0、L1、L2 三层。L0常驻层。系统指令、角色设定、全局不可变约束基本不随对话变化。L1会话层。sticky_info、摘要、最近对话跟着当前 session 走session 结束就释放。L2归档层。历史会话摘要、用户长期偏好存储在外围需要时按需召回。生命周期上L0 永远存在L1 随会话创建和销毁L2 跨会话存在。这套分层不只让上下文管理更清晰也让之前的三种模式各有归属显式持久对应 L1、窗口滑动对应 L1 里的临时区、摘要和向量召回对应 L1 到 L2 的桥梁。架构一旦理顺后面加功能、做扩展都轻松很多。说句大实话context-mode 这个方向没有银弹。不同的业务形态、不同模型能力、不同成本约束都需要不同的组合策略。但底层逻辑是共通的你要么让模型记住该记住的要么帮它忘掉该忘掉的两头都不做光靠堆窗口长度早晚会被用户教做人。我现在每做一个新项目都会默认把“分层 快照 预算控制”这三个基础设施先搭好再谈上层业务省去了后面无数的返工和扯皮。你们要是有更好的方案欢迎一起交流。