ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

上下文模式设计:让大模型长对话不“失忆”的工程实践

上下文模式设计:让大模型长对话不“失忆”的工程实践 上周我帮一家电商公司的客服机器人做技术咨询对方反馈的问题只有两个字“失忆”。用户前 3 轮问得好好的聊到第 6、7 轮明明刚才报过订单号模型却像从没听过一样甚至开始“一本正经地瞎编”。我拉出调用日志翻了半天发现模型没选错、接口没报错、prompt 也没语法问题真正缺的是一整套 context-mode 的设计——也就是“上下文模式”。这里说的 context-mode不是某个模型特有的私有协议也不是必须引入的框架而是你决定“每一轮请求到底把哪些历史信息塞进上下文窗口、以什么结构塞进去、塞多少”的完整规则。很多人做 LLM 应用时一上来就喊“换大窗口模型”但真正影响长对话质量的往往是这个看不见摸不着的上下文模式。这篇文章是我最近重构一个知识库问答机器人的完整复盘从底层机制讲到三种可落地的设计模式再到我花了 48 小时踩出来的坑。如果你正在做客服、知识库、对话式 AI 这类产品被长对话质量困扰这篇应该能给你一份可以直接抄作业的参考。1. 先打破两个直觉长窗口不等于好记忆多塞不等于更聪明1.1 案例一200K 大窗口模型在长对话里“高开低走”先说一个让我印象最深的对照实验。同一段“连续咨询售后退换货流程”的对话我把两份完全相同的对话历史分别喂给两套配置一套是传统 32K 窗口模型只保留最近 6 轮消息另一套是 200K 大窗口模型把全部 22 轮历史原封不动丢进去。前 3 轮两者回答质量几乎没差别。但从第 8 轮开始大窗口方案开始“犯迷糊”——我问“刚才说的运费险是谁承担”它给出了和前面完全相反的答案反而那套只保留最近 6 轮的配置因为上下文里刚好有“上一轮刚确认运费险归卖家承担”这条信息答得干净利落。这个结果颠覆了我的直觉。后来查了不少公开的实验总结和论文笔记才发现这叫 “lost in the middle” 现象当上下文窗口里塞了大量信息时模型对处于中间位置的内容关注度会明显下降。开头和结尾的信息容易被记住中间地带容易被忽略。这就像你让一个人听 40 分钟会议他大概率能记住会议刚开始的目标和最后五分钟的结论但中间那段谁说了什么关键数据往往就模糊了。所以“窗口大”只是给了你更多空间不代表模型真的会好好利用这些空间。真正决定对话质量的是你如何组织放进窗口里的内容。1.2 案例二同样的对话不同 context-mode 成本差 6 倍第二个案例是成本维度。同样一轮客服对话我分别在“无脑全量塞入”和“摘要压缩 最近 8 轮”两种模式下跑测试。无脑全量塞入时一次请求的输入 token 大约是 18000。按当时用的模型单价粗算折合人民币约 0.36 元一次请求。而用了摘要压缩之后历史 14 轮对话被压成一段 400 token 的摘要再加上最近 8 轮原文输入 token 掉到 3500 左右单次成本约 0.07 元。两者相差超过 5 倍。一个日请求量 10 万次的客服机器人一天的成本差距就是小三千块。更关键的是便宜的那套回答质量反而更稳定——因为挤掉了大量无关闲聊模型注意力更集中。这两件事让我下了一个结论context-mode 的本质不是“如何增加记忆”而是“如何做减法”。你要在每一个请求发生的瞬间为模型挑选出当前这一刻真正值得看的上下文。1.3 我理解的 context-mode 到底是什么我现在给 context-mode 下的定义很简单它是你在每次调用大模型接口时对“系统指令、历史消息、外部检索内容、当前用户问题”这四类信息进行选择、排序、压缩和注入的一整套规则。说人话就是——模型没有跨请求的记忆它只在你把内容放进 prompt 的那一刻才“看到”这些信息。你放进去什么它就用什么回答你没放进去的它一概不知道。context-mode 就是决定“放什么、怎么放、放多少”的那套策略。接下来我先把底层机制补清楚再讲三种真正能落地的模式设计最后用完整案例把整个调优过程串起来。2. 前置知识token、窗口和注意力的底层逻辑2.1 用“行李箱”类比理解上下文窗口你可以把上下文窗口理解成一个固定尺寸的行李箱。假设你只能带 20 公斤行李上飞机你的系统指令是换洗衣服历史对话是纪念品检索到的知识库内容是特产当前用户问题是刚到机场临时买的一瓶水——行李箱就那么大你必须做取舍。每家大模型都有明确的“行李箱容量”有的 8K token有的 32K有的 200K。这里的 token 是模型处理文本的最小单位。对中文来说一个 token 大约对应 0.6 到 1 个汉字具体看模型分词器实现但你可以粗略按“1 个汉字约等于 1 到 1.5 个 token”来估算。行李箱超重会怎样不同模型处理方式不一样有的是直接报错有的是自动截断最前面的内容有的是把超出的部分静默丢掉。最危险的是第三种——你不会收到任何警告但模型可能已经把你最关键的指令给“挤”出去了。2.2 为什么塞满窗口反而会稀释注意力大语言模型的核心机制是自注意力self-attention它在生成下一个字时理论上会“看”一遍上下文窗口里的所有 token计算每个 token 和当前问题的关联权重。问题就在这里当窗口里塞了大量跟当前问题无关的闲聊、重复的客套话、冗长的商品说明时这些无关 token 会分摊掉一部分注意力权重。模型不是不知道关键信息在哪而是它在海量噪音中更难把注意力聚焦到那个关键点上。这就是为什么同样的信息放在一个干净的上下文里能正确回答混在一堆无关内容里反而答错。我用生活场景类比一下你在嘈杂的自习室里复习考试桌子对面正好坐着一位反复大声讲电话的同学你看书的效率和准确率会明显下降。不是因为你不知道重点在哪而是外部噪音分散了你的注意力。上下文窗口里的“噪音 token”扮演的就是那位同学。2.3 每个请求花多少 token是可以提前算出来的很多人在调试时凭感觉“差不多得了”结果上线后发现成本完全失控。实际上每次请求的 input token 是可以精确预估的。输入 token 的构成公式很简单input_tokens 系统指令 token 数 历史消息 token 数 检索内容 token 数 当前用户问题 token 数 预留缓冲 token 数我拿一个真实的客服场景算给你看。假设你的系统指令写了 800 token最近 10 轮历史消息约 2500 token每轮还附带一段 600 token 的商品知识检索结果当前用户问题约 100 token再预留 10% 缓冲。结果就是组成部分token 数系统指令800历史消息10 轮2500检索结果每轮附 1 段600当前用户问题10010% 缓冲400合计4400如果这个数字接近甚至超过了模型窗口上限比如 4K 模型你就得马上做减法减少历史轮数、压缩摘要、缩短检索片段选一样或几样同时做。这个计算过程应该成为每次改动前的肌肉记忆。3. 三种实用 context-mode轮次截断、滑动摘要、检索增强我这几周试下来真正能落地的上下文模式就是三种其他花哨方案大多是这三者的组合变体。3.1 轮次截断模式Rolling Window最简单、最便宜轮次截断的逻辑最直白永远只保留最近 N 轮对话N 以前的内容直接丢弃。我通常会把 N 设在 6 到 10 之间。N 太小模型记不住前面提过的关键事实N 太大会重新引入噪音而且成本线性上升。对大部分客服场景8 轮是一个比较稳的起点。它的优点是实现成本极低只需要维护一个固定长度的消息队列新消息进来、旧消息出队。缺点是它没有真正的“长期记忆”——用户在第 2 轮提到“我要退一件 M 码的蓝色卫衣”到第 12 轮再问“那我那个卫衣什么时候能退”如果第 2 轮已经被截掉了模型就完全不知道你在说哪件。所以轮次截断适合高频、轻量、每轮相对独立的对话。适合的场景是售后咨询、订单查询、FAQ 问答这类“每一轮都自带完整问题背景”的场景。3.2 滑动摘要模式Summary Recent把旧对话压成一页备忘录滑动摘要是我在高复杂度对话场景里最常用的一种。它的核心思路是超过轮次阈值后不再直接丢旧消息而是先把旧消息交给模型“浓缩”成一段摘要然后每次请求都带上“历史摘要 最近几轮原文”。举个具体例子。第 9 轮时系统会把第 1 到 6 轮的对话交给一个摘要模型让它提炼出“用户购买了什么商品、申请了什么服务、客服已确认哪些内容”然后清空这段原文只保留摘要和最近 3 轮原文。到第 15 轮再把摘要和最近的对话一起重新压缩形成更精炼的摘要。这样做的价值是既能保留关键事实又不会让上下文无限膨胀。用数据说话我做过一次 30 轮对话的测试如果全量保留原文输入 token 会膨胀到 18000用滑动摘要后稳定维持在 3500 到 4500 之间而且对关键信息的记忆保持率比全量方案还高。缺点是摘要是有损压缩。一旦摘要模型漏掉了一个关键实体比如用户强调的收货地址这个信息就彻底丢了且不可恢复。所以用这种模式时我强烈建议对关键实体单独做结构化提取这在第 4 节会展开讲。3.3 检索增强模式RAG Retrieval不靠“记住”靠“召回”检索增强模式换了一个思路我不试图把整个知识库或全部历史塞进上下文而是在每次用户提问时先用向量检索或关键词检索从外部存储中找出最相关的 2 到 3 段内容只把这几段注入上下文。这种模式特别适合知识库类场景。比如用户问“你们家 48 小时发货是真的吗”系统先把问题转成向量在商品政策库、物流说明、历史工单里做相似度检索只返回最相关的政策条款然后连同问题一起交给模型回答。上下文里没有无关广告词、没有两百条类似政策只有真正匹配的那一条。RAG 的优势是成本可控、信息新鲜。知识库更新后不需要重新训练模型只要更新检索库就行。劣势是检索质量直接决定回答质量如果检索召回的内容不准确模型再聪明也答不对。所以 RAG 模式下你需要花大量精力优化分块大小、embedding 模型和检索策略。3.4 三种模式的选型对比表维度轮次截断滑动摘要检索增强实现难度低中中高上下文成本低中低低长期记忆能力弱中强依赖检索质量适合场景FAQ、轻量客服复杂多轮对话知识库问答、长文档典型失败模式关键信息被截掉摘要丢失细节检索不到/召回错误实际项目里三种模式从来不是互斥的。我经手的多数生产级项目都是“轮次截断 滑动摘要 检索增强”三种混用。下面我拿一个完整案例来讲怎么混。4. 一个知识库客服机器人的完整调优过程4.1 原始日志里看到的三个异常特征接手的这个客服机器人主要服务电商售前售后咨询知识库里有商品参数、物流政策、退换货规则。用户反馈“聊久了就乱”我先拉了连续 50 个失败会话日志发现了三个共性第一70% 的失败会话都发生在第 6 轮以后前 5 轮几乎无问题。第二用户在第 2 到 4 轮确认的关键实体订单号、商品型号、地址在后续回复中频繁被模型“遗忘”。第三日志显示每次请求的 prompt 里带着全部历史消息最长的一个会话单次请求居然带进了 15000 个 token。这三点对应到 context-mode 上的问题非常明确没有做截断、没有做摘要、没有做实体结构化提取。模型确实“看到了”所有内容但它的注意力已经被几千 token 的闲聊稀释殆尽。4.2 我采用的组合方案三层 context-mode针对这些特征我最终设计的方案是三层组合第一层轮次截断只保留最近 8 轮对话原文更早的直接不进 prompt。这是成本地板防止上下文无限膨胀。第二层滑动摘要每隔 6 轮做一次历史摘要把“更早的内容 最近 8 轮原文”浓缩成一段 300 到 500 token 的事实清单放在 prompt 的中间位置。摘要里必须包含实体信息、已确认事项、未解决事项。第三层结构化记忆槽位注入在服务端维护一个 JSON 结构单独记录用户对话中提取出的关键实体比如用户ID、订单号、商品型号、退换货意向、确认过的地址。每次请求时这个 JSON 结构作为独立段落注入 prompt并且放在摘要之前保证高优先级。这第三层是很多人忽略的。摘要模型虽然有损但不会“有损”到丢掉订单号——前提是你把订单号单独摘出来而不是混在自然语言摘要里。结构化槽位本质上是对抗摘要有损的一层保险。4.3 关键实现示例这个方案的逻辑可以用下面这段 Python 伪代码来表达def build_context(conversation_history, user_query, knowledge_base): # 第一层轮次截断只保留最近 8 轮 recent_rounds conversation_history[-8:] # 第三层结构化记忆槽位 memory_slots extract_memory_slots(recent_rounds) # {order_id: JD20240915, product: 蓝色卫衣 M 码, intent: 退货} # 第二层滑动摘要压缩超过阈值触发 if len(conversation_history) 14: summary summarize_history(conversation_history[:-8], memory_slots) else: summary # 检索增强按当前问题检索知识库片段 retrieved_docs knowledge_base.search(user_query, top_k2) # 组装 prompt系统指令 → 记忆槽位 → 摘要 → 最近轮次 → 检索片段 → 用户问题 prompt_sections [ SYSTEM_PROMPT, f[关键记忆]\n{json.dumps(memory_slots, ensure_asciiFalse)}, f[历史摘要]\n{summary} if summary, f[最近对话]\n{format_rounds(recent_rounds)}, f[相关资料]\n{retrieved_docs}, f[当前问题]\n{user_query} ] return \n\n.join(filter(None, prompt_sections))这里有个细节值得展开组装 prompt 时我把“关键记忆”放在“历史摘要”之前“最近对话”之后紧跟着“相关资料”。为什么这样排因为根据“lost in the middle”现象的规律开头和结尾的 token 更容易被关注中间容易被忽略。系统指令放最开头保住行为规范用户当前问题放最后确保不回偏关键记忆放次开头获得高关注度真正可能被忽略的是中间的摘要和检索内容——所以我确保摘要足够精简检索结果不超过 2 段避免中间区域堆太多噪音。4.4 评估10 轮连续对话的测试结果方案上线后我做了一组对照测试拿同一份 10 轮连续追问的对话脚本分别跑“全量塞入”和“三层组合”两套配置每套跑 20 遍统计关键实体保持率和最终回答准确率。指标全量塞入三层组合方案第 5 轮关键实体保持率72%98%第 8 轮关键实体保持率41%95%第 10 轮回答准确率55%92%单次平均输入 token145004200单次估算成本元0.290.085结果很清楚不是模型变聪明了而是我们教会了系统“什么时候该看什么”。成本下降了约 70%准确率反而显著提升。这就是 context-mode 设计的价值——用更少的 token 拿到更好的效果。5. 48 小时踩坑记录context-mode 最容易翻车的五个细节方案落地不是一帆风顺的。下面这五个坑是我实打实踩过的每一个都浪费了不少调试时间分享出来希望大家能绕开。5.1 坑一摘要压缩把关键实体压丢了第一次实现滑动摘要时我天真地让摘要模型“概括一下前面的对话”。结果摘要模型为了追求简洁把“用户要求用顺丰到付寄回”压缩成了“用户要求寄回”把“顺丰到付”这个直接影响售后结算的关键信息弄丢了。用户后面质问“我不是说了到付吗”系统完全答不上来。后来我把摘要策略改成了“清单式摘要”明确要求摘要输出三段结构已确认事实、待办事项、用户诉求并且强制要求实体原样保留。如果摘要模型抽取的实体与原文不一致宁可把原文对应片段也带上。简单说摘要要面向“事实抽取”而不是“语义概括”。5.2 坑二系统提示词写了 2000 token把自己的“视野”挤没了做客服机器人的朋友总想把所有规则都塞进系统提示词商品政策、安抚话术、禁止承诺的语气规范、转人工条件……我见过最夸张的一条系统提示词写了 2000 多 token几乎占了 8K 窗口的四分之一。这会导致什么真正留给对话历史和检索内容的空间被严重压缩。模型记住了“要礼貌、不能承诺具体时效”却没有空间容纳“用户这一单的具体商品信息”结果说话永远漂亮内容永远错误。我的经验是系统提示词控制在一页 A4 纸以内大约 300 到 500 token只保留最核心的角色设定、回答规范和三项以内的行为红线。其余细节放到检索增强的“相关资料”里按需注入。5.3 坑三没有区分“事实记忆”和“可丢弃闲聊”还有个隐蔽问题不是所有历史消息都值得保留。用户第 3 轮可能只是随口说了一句“今天下雨快递会不会延迟”这类闲聊对后续回答毫无帮助但在上下文里占了 50 token长期累积下来就是几百 token 的噪音。我之后的方案里加了一步“消息重要性标记”在每轮消息进入上下文前先用规则判断它是否包含订单号、商品型号、时间节点、诉求变更等关键要素。没有关键要素的寒暄轮次在被摘要覆盖后就彻底清理不进记忆槽位也不占摘要配额。5.4 坑四只跑短对话测试上线被长对话击穿这个坑特别典型。我在自测环境下只跑了 5 到 6 轮对话效果很好就上线了。结果真实用户一聊聊到 20 多轮旧消息被截断后模型开始反复问已经提供过好几次的信息用户直接给了差评。教训是评估 context-mode 不能只测短对话至少要准备 15 轮以上的长对话脚本并且专门检查第 1 轮提到的信息到第 15 轮是否仍然能被正确引用。现在我把“长对话压力测试”写进了验收清单没有跑过 20 轮连续对话的方案不允许上线。5.5 坑五只算了 token 单价没算并发下的总量最后这个坑是关于预算的。初期我确实做过 token 成本估算但只算了单次请求的价格忽略了并发翻倍后的总量。上线当天用户量突然涨了三倍每秒钟 20 多个请求每个请求 4000 token日成本直接爆了预估的三倍多。后来我加上了一个简单的预算公式日成本 日均请求数 × 单次平均 token 数 × 单 token 单价 × 倍数系数。每做一个 context 改动都先拿这个公式重新算一遍再决定要不要上线。这个习惯帮我避免了好几次成本事故。写在最后我对 context-mode 的核心体会搭完这套方案后我对大模型应用的工程化有了一个更清晰的判断模型能力的边界很大程度上取决于你喂给它的上下文的组织方式。你给模型一个干净的、恰到好处的视野它就表现得聪明、稳定、省钱你给它一个塞满噪音的巨型窗口它反而会变得迟钝、健忘、昂贵。如果只能分享一条经验我会说先别急着换更贵的模型、开更大的窗口先把你现有的 context-mode 认真设计一遍。把“该记的记成结构化槽位该忘的交给截断策略该查的交给检索该压的交给摘要”这四件事做对了你手里的模型性能会立刻上一个台阶。这套方法论我目前还在持续迭代后续如果做了多模态场景下的上下文组织再写一篇和大家细聊。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进