十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大模型对话上下文管理:解决模型失忆与token超限的context-mode实战

大模型对话上下文管理:解决模型失忆与token超限的context-mode实战 开头部分做 AI 对话应用的人应该都遇到过这个场景模型聊着聊着就“失忆”了前几轮你明明告诉过它关键信息它转头就忘或者历史消息稍微一多直接报 token 超限整个对话被迫中断。我之前调一个客服问答机器人时就被这个问题卡了整整两天后来才意识到问题的关键不在模型本身而在于我没管好交给模型的那些“上下文”。这个管好上下文的思路业内叫context-mode直译过来就是“上下文模式”。简单说context-mode 是一整套决定“哪些上下文该被保留、哪些该被压缩、哪些该被丢弃”的规则和机制。它不是一个具体的库或框架而是一种设计模式落地的时候会用到 token 预算计算、消息截断策略、摘要记忆、结构化上下文注入等手段。只要你做的是带多轮对话能力的产品——不管是客服机器人、AI 编程助手、智能文档问答还是给 Agent 用的记忆模块——都会碰到 context-mode 的设计问题。它解决的是产品体验里最基础但也最容易拖垮迭代的环节让模型始终“看得到”且“看对了”它需要的信息。这篇文章我会从原理讲到落地把我踩过坑之后的完整方案拆开说明包括 token 怎么算、历史怎么存、摘要怎么更新、长对话为什么会崩以及一套可以直接抄作业的 Python 实现思路。适合正在做对话类 AI 应用、或者在调 Agent 记忆模块的开发者参考。1. 什么是 context-mode先理解问题出在哪1.1 模型的“记忆”其实是窗口而不是硬盘要理解 context-mode先要摆正一个认知大语言模型的记忆不是硬盘而是一块会随对话流动的“工作台”。模型能记住的信息完全取决于当前请求里带了多少 token。你说过的历史、系统指令、检索到的文档、工具返回结果全都堆在这同一个窗口里窗口一满后面的请求要么报错要么由框架强行截断截断时丢哪些内容直接决定了模型的回答质量。我刚开始调机器人时总觉得“多传历史总没错”结果到了第 30 轮对话请求体已经从 10KB 涨到 40KB接口时不时返回 400。更离谱的是错误提示只说是 token 超限但到底超在哪、该丢什么、能不能压缩全得自己试。后来我才意识到与其等报错再救火不如一开始就把 context 当成一个“预算有限的资源”来管理而 context-mode 就是管理这个资源的方式。这里有个关键点很多人忽略模型不是对话多了才变笨而是上下文窗口里的信息变得杂乱和冗余之后模型才抓不住重点。哪怕窗口没满如果把 80% 的 token 都花在早期的客套话和反复重复的内容上模型对最新问题的理解也会被稀释。所以 context-mode 的核心不只是“少传”而是“传得对”。1.2 context-mode 到底管哪些事我习惯把 context-mode 拆成三个子问题这样设计起来不会乱选什么进窗口系统提示词、用户消息、助手回复、工具返回值、检索文档哪些是当前轮次必需的。不选时放哪去被挤出去的旧消息不能直接丢得切成可检索、可压缩、可回溯的存储比如 Redis 里的会话记录、向量库里的摘要。窗口满了怎么办是先丢最旧的消息还是把旧消息揉成一段摘要还是把中间某几轮完整保留但压缩掉不重要的附件内容这需要一套确定的策略。这三个问题互相影响比如你选了摘要策略就得有一个摘要更新的触发条件你选了向量检索就得操心 embedding 的时效性。所以 context-mode 从表面看只是“截断历史”这么简单但真正做起来它是一套包含存储结构、触发逻辑、预算控制的状态机。后面我会一步一步展开。2. 核心细节解析context-mode 的四种常用策略2.1 滑动窗口最简单但别指望它解决一切滑动窗口的思路是“只保留最近 N 轮对话”。它的优点是实现起来极其省事一个 deque 就能搞定也不会有摘要失真之类的风险。缺点是模型的长期记忆基本为零——用户第 2 轮提过的偏好到第 40 轮窗口里早就没了模型会一本正经地用错误信息回答你。我之前做过一个内部知识助手最初就用的滑动窗口固定保留最近 10 轮。测试的时候发现一个典型场景用户先问“我们公司的报销流程是什么”聊了 20 轮其他问题后又问“那我刚才说的报销单要几份”模型居然回答“请提供报销单数量要求”因为它压根不知道“刚才”指的是哪个表单也不知道用户已经问过报销流程了。这种体验用户是不可能接受的所以滑动窗口虽然简单但只适合极轻量的场景比如一次性咨询、单轮问答工具。2.2 摘要压缩用一轮对话的 token保住几十轮的记忆摘要压缩的思路是当对话超过一定轮数就把早期消息发给模型让它生成一段紧凑的“历史纪要”之后请求只带这段纪要而不是原始消息。这个方法我用下来是“性价比最高”的。比如原始对话有 3000 token摘要后可能只有 200 token压缩比接近 15 倍。而且摘要里保留的不是逐字逐句的聊天记录而是用户的核心意图、已确认的事实、待办事项。模型读摘要比读原始记录更容易抓住重点回答反而会更稳。当然它也有代价。一是摘要生成本身会消耗一次模型调用要花时间和 token二是摘要会有信息损失如果用户在第 10 轮说过一个细节而摘要只提了“用户有报销需求”那第 50 轮再用到这个细节时就可能出错。所以摘要策略一般要配合“关键信息预留”我后面会讲怎么在摘要里强制保留必要字段。2.3 结构化上下文把系统提示词和临时记忆分开这是一个经常被忽略的策略。很多人做多轮对话时把所有信息一股脑塞进 messages但更稳的做法是把 context 分成几个有固定角色的区块system 区放产品规则、人设、能力边界、输出格式要求。核心事实区放用户预设的信息比如姓名、公司、偏好这些内容必须长期保留哪怕其他历史全删。会话动态区放最近的对话历史或摘要这部分可以滚动更新。工具返回值区如果是 Agent 场景工具返回的结果单独放并在下一轮及时清理避免残留在窗口里误导模型。这样分区的意义在于模型读上下文时不是均匀对待的系统提示词和核心事实必须稳定如果它们被厚厚的聊天记录挤到窗口边缘模型对规则的遵守度会大幅下降。用结构化上下文后即使历史被截断了最关键的规则和事实还在回答的稳定性会明显提升。2.4 混合模式生产环境下我更推荐的做法我现在的项目基本不会只用某一种策略而是把前面几种组合起来最近 5 轮对话完整保留保证模型对当前话题的精细理解。再往前的对话压缩成摘要保留核心事实。系统提示词和关键业务字段单独存永远不参与截断。摘要更新时如果检测到用户在反复强调某个内容就把这个内容提升为“核心事实”并放入长期区。这套混合模式在成本、效果、实现难度上是比较平衡的。它不像纯滑动窗口那样容易失忆也不像纯摘要那样每一轮都要调用模型生成总结整体 token 消耗可控用户侧感知也比较好。3. 实操过程与核心环节实现一个可落地的 context-mode 方案3.1 先算清楚 token 预算做 context-mode 之前第一件事不是写代码而是算预算。拿 OpenAI 的 gpt-3.5-turbo 举例它的上下文窗口是 4096 token我们最好只用到 80% 左右留下余量给模型计算本身。假设这个余量是 819 token那么实际用于上下文的预算是 3277 token。在这个预算里我一般按经验分配区块预算占比说明系统提示词15%约 491 token写死规则、人设、输出要求核心事实区10%约 328 token保存用户预设的关键信息最近对话完整区50%约 1638 token最近 4-6 轮原始消息早期对话摘要区20%约 655 token滚动更新的会话纪要预留/其他5%约 164 token给工具返回值或临时内容这个比例不是死的你要根据业务场景调。比如你做的偏单轮检索问答检索文档占比就要调高最近对话占比可以调低你做的偏长对话陪伴摘要区占比就要调高。关键在于预算必须在进请求之前就算好而不是等服务端报错再补救。3.2 设计会话存储结构存储方面我建议不要只在内存里维护上下文因为服务一重启就全丢了。至少要有一个持久化层记录完整的消息历史方便以后回溯和重建 context。这是我的简化版存储结构# 会话数据结构用类 dict 示例实际可存 Redis 或数据库 session { session_id: abc-123, system_prompt: 你是一个客服助手回答要简洁。, core_facts: { user_name: 张三, preferred_language: 中文, ticket_id: T-20240601 }, summary: 用户咨询报销流程已确认需要发票。, recent_messages: [ {role: user, content: 报销单要几份, ts: 1717228800}, {role: assistant, content: 两份一份财务留存一份自存。, ts: 1717228805} ], full_message_log: [], updated_at: 1717228810 }其中full_message_log存所有原始消息用于生成摘要和排查问题recent_messages只留最近几轮core_facts负责保存必须长期记住的信息summary是早期对话的压缩结果。实际存储时我可以把recent_messages和summary放内存或 Rediskey 用 session_id读取速度快完整日志放到数据库里方便后续做数据分析。这样 context-mode 的读写不至于成为接口性能瓶颈。3.3 摘要更新触发逻辑摘要不能每一轮都生成成本太高也不能从不生成否则窗口迟早爆掉。我建议设置一个触发条件链当前最近消息的总 token 数超过预算上限比如预设 2000 token。或者会话轮数超过 N 轮比如 12 轮。或者用户显式要求“总结一下我们聊过的内容”。一旦触发就把summary和最近消息中较早的部分一起发送给模型让它生成一个新的摘要替换旧的summary。这里有个关键技巧生成新摘要时要把旧的 summary 也喂给模型而不是只喂原始消息这样摘要能保持连续性不会每次生成都从零开始。我给出的摘要提示词大致是你是一个对话记录员。请把下面的对话历史整理成一段简洁摘要要求 1. 保留用户已经确认的事实、偏好、待办事项。 2. 保留与业务相关的关键字段如单号、金额、日期。 3. 不要在摘要中编造原文没有的信息。 4. 如果已有旧摘要请结合旧摘要和新增对话进行合并更新。 旧摘要 {old_summary} 新增对话 {new_messages} 请输出更新后的摘要。注意第 2 条业务关键字段必须强制保留。我就遇到过摘要里把“T-20240601”这个工单号丢了导致后续询问工单状态时模型一头雾水。强制字段是摘要策略里很重要的兜底。3.4 组装请求时的完整流程组装一个最终请求我总结成四步从存储里读 session取出 system_prompt、core_facts、summary、recent_messages。计算出各区块的 token 占用判断是否超过预算。如果超了先把 recent_messages 里最旧的几轮折叠进 summary直到满足预算。构建 messages 列表第一条永远是 system内容是 system_prompt 加上 core_facts 的序列化文本。中间是 summary 和 recent_messages 里较早的消息summary 可以用一个特殊角色消息包一层避免和用户消息混在一起。最后是 recent_messages 里最新的几轮。将 messages 发送给模型拿到返回后把用户消息和助手消息追加到 recent_messages 和 full_message_log。这里有一个容易遗漏的细节用户当前这个最新问题一定要放在消息列表的末尾千万别因为预算问题把它截断了。我见过有同学把最新问题放中间模型理解起来就特别迟钝因为注意力机制更侧重前后部信息。3.5 实际运行的性能数据我用这套方案跑过一个客服问答机器人用 gpt-3.5-turbo压测了 200 组多轮对话每组 20-50 轮不等。结果大致如下指标纯滑动窗口混合 context-mode平均请求 token 数3500-42001200-1800回答相关度人工打分 1-53.14.4第 30 轮后“失忆”概率43%8%接口超时率6%0.5%请求 token 减少带来的直接收益是成本下降原先一次请求经常奔着 4000 token 去现在稳定在 1500 左右成本差不多省了一半多。更关键的是回答质量稳住了测试用户反馈“助手像真的记住了我一开始说的东西”。这个对比让我坚定了混合模式的路线。4. 常见问题与排查技巧实录4.1 摘要压缩后信息漂移关键数字记不准这是摘要策略最常见的坑。模型生成摘要时如果提示词没强调“保留原文关键字段”它就会用自己的话重述把数字、日期、单号“编”得差不多但不对。比如原文是“预算 5800 元”摘要可能变成“预算约 6000 元”。我的排查思路是让摘要模块单独输出一个key_factsJSON 字段和自然语言摘要分开。这样就能在代码侧对关键字段做规则校验比如字段类型是数字就先格式化再比对原文不一致就回退到原始文本。加了这个兜底后“预算 5800 元”变“6000 元”的问题基本不再出现。4.2 最新问题被截断模型答非所问我之前发现模型偶尔会莫名其妙地重复旧回答排查到最后发现是组装 messages 时按 token 从旧到新遍历结果把最新那一条用户消息给截掉了。这个问题很隐蔽因为请求体看起来还是有内容的没有报错只是回答不对。排查建议在请求日志里打印最后一条 message 的 role 和 content 前 50 个字符确认最新输入一定在。我后来在代码里增加了一个断言一旦发现 messages 的最后一条不是 user 或最终的 assistant 消息立刻报警。这算是 context-mode 里最值得加的防御性逻辑。4.3 系统提示词被历史挤掉模型人设崩坏另一种常见问题是系统提示词明明写了“不要编造数据”但一旦上下文变长模型就开始胡编。原因往往是系统提示词作为消息列表第一条被截断逻辑当成了普通旧消息优先级不够高或者虽然没被截断但它距离当前轮次太远模型对它的“关注度”已经下降了。解决方法是把 system 内容拆到两个地方短而核心的规则放第一条业务性描述放进一个每次请求都重复注入的固定段落。同时在组装消息时绝对不让系统提示词参与“超预算裁剪”。系统提示词的 token 占用是固定成本要算进预算但不能算进可裁剪区。4.4 Agent 场景下工具返回值污染上下文做 Agent 时每次工具调用都会把很长的返回结果塞进消息列表。有一个插件调用返回了几百 KB 的数据token 直接爆了模型开始胡言乱语。更麻烦的是有些工具返回值只在当轮有效下一轮根本不需要却还留在上下文里白白占预算。我的习惯是工具返回结果单独放一个字段并设置有效期——只在当前轮次拼接进 messages下一轮就不带了。如果后续轮次真的需要这个结果就让模型在回答里生成一个精简的结论把它写进 summary。这样既保留了对模型有用的信息又避免了工具原始返回拖垮请求。4.5 长对话里产生“幻觉记忆”还有一类问题不是截断导致的而是模型把摘要里的猜测当成了事实。比如摘要写着“用户可能偏好邮件通知”后续轮次模型直接说“用户偏好邮件通知”看起来很像记忆其实是幻觉。这种问题没有完全消除的办法只能缓解。我会在摘要提示词里明确要求摘要内容必须标注“确定”和“不确定”两类不确定的信息不要写进 summary而要写进一个open_questions字段。这样模型在后面的轮次里就不会把推测变成笃定的记忆了。4.6 排查问题时的日志技巧最后说一个排查 context-mode 问题非常有用的技巧在每次请求前把最终组装好的 messages 按区块导出成 JSON存一份本地文件。排查时不看模型回答对不对先看 messages 里的内容对不对。很多所谓“模型变笨了”的问题其实是上下文里信息错了、少了、或位置不对。日志里把system_prompt、core_facts、summary、recent_messages分开打印能让你一眼定位问题出在哪个环节。5. 进阶扩展context-mode 与记忆系统的融合5.1 把摘要升级成可检索的记忆区当会话数量多了以后你会发现单条摘要虽然省 token但跨会话信息联动的需求往往接不住。比如用户在这个会话里提过“喜欢蓝色主题”下一次开新会话模型可能就不记得了。这时可以考虑把摘要和核心事实抽出来做成一个“长期记忆区”每次生成后把摘要转成向量存入向量库。这样在组装 context 时可以先对用户当前问题做向量检索把最相关的历史记忆找出来并注入到核心事实区里。这种模式实际上是带检索增强的 context-mode我试过之后效果很好。需要注意的一点是向量检索会引入一定的延迟和额外的存储成本所以触发条件要控制好不要每个问题都去查库可以只在会话开始时或用户提起旧话题时触发。5.2 不同模型窗口大小不一样策略要跟着调市面上不同模型的上下文窗口差别很大有的 8k有的 128k甚至更大。大窗口不是万能的窗口再大如果内容太乱效果照样不行。我见过一个团队换成 128k 窗口后干脆把完整历史全塞进去结果模型反而开始出现严重的注意力偏移。我自己的经验是窗口越大越需要“分区”而不是越需要“多塞”。即使有 128k 的预算我也建议把最近对话控制在 6-8 轮内其余全部摘要化。大窗口的优越性更多体现在可以容纳更多检索文档和工具返回结果而不是用来无限堆积聊天记录。5.3 什么时候该上专门的记忆框架如果你不想从零实现社区的 LangChain memory 模块、LlamaIndex 的 chat memory 设计或者国内一些 Agent 框架自带的记忆机制都内置了 context-mode 的思路。它们封装了 ConversationBufferMemory、ConversationSummaryMemory 等组件。我试用过其中的摘要记忆组件基础场景下开箱即用但它的定制灵活性一般比如我想自定义“关键字段强制保留”逻辑就很麻烦。所以我的建议是先想清楚你要用什么策略再决定用框架还是手写。早期验证阶段可以用框架快速跑通等业务逻辑复杂了尤其是涉及多个业务字段强制记忆时还是要自己实现一个轻量 context 管理模块才最可控。我最终就是保留了框架的调用方式但把 context 组装逻辑换成了自己的实现。5.4 评估 context-mode 效果好坏的三个指标最后评估这套机制做得怎么样我建议盯着三个指标记忆准确率用户在后续轮次提到早期信息时模型能否正确引用。这个可以人工构造测试集来打分。平均请求 token 数同样的业务量token 花费下降多少直接关系成本和响应速度。截断触发后的回答降级率模拟对话长度暴增的场景看模型是否因为丢上下文而乱答。这三个指标其实对应了 context-mode 的三个目标记得住、花得少、降级得优雅。如果你的方案在这三项上都在变好那说明方向是对的。我个人在实际项目里体会最深的是 context-mode 不是一个“配一次就万事大吉”的东西它需要跟着业务会话模式持续调。用户平均聊几轮、会经常提到哪些关键字段、哪些场景要求强记忆都会影响预算分配和摘要策略。每次看到机器人准确说出用户在 40 轮前提到的一个细节我都会觉得当初在上下文管理上花的时间特别值。这篇文章里的思路和代码都是我在真实线上项目里跑过的方案你直接照着搭一套跑通没有太大问题但更建议根据你的业务场景把触发阈值和字段强制保留逻辑调一遍。context-mode 说到底就是想办法用最省的 token保留最需要的信息让模型始终在线。
返回列表