尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Context-Mode上下文管理实战:滑窗、摘要与检索增强的分层方案

Context-Mode上下文管理实战:滑窗、摘要与检索增强的分层方案 1. 为什么 context-mode 突然成了绕不开的话题接触过大模型应用开发的朋友最近应该没少听到 context-mode 这个词。所谓 context-mode简单说就是在大模型对话、文档处理或 Agent 工作流里对上下文的一种专门管理方式——你告诉模型记住这件事它通过 context-mode 来决定到底记住哪一段、保留多少、什么时候忘掉、什么时候压缩。听起来好像只是上下文长度的问题但真正动手做过的人都知道这里面的坑远比想象中深。模型输入有上限比如早期模型可能只有 4K、8K token现在长窗口模型到了 128K、200K可窗口变长不等于真的能用满。我自己实测过超长窗口下模型对中间部分内容的注意力明显衰减检索不精确、关键信息被淹没、回答风格漂移这些问题都指向同一个根源——你不是缺窗口而是缺一套组织上下文的逻辑。这篇就想把我自己从硬塞整段上下文到设计一套可用的 context-mode这段过程里的方案选型、踩坑记录、可复现的代码逻辑完整梳理出来。适合谁看两类人一是正在做大模型应用、被长文本处理和多轮对话记忆折磨的开发者二是产品经理或技术负责人想搞清楚 context-mode 到底在解决什么、团队要不要投入做也能从里面看到完整的评估视角。不写晦涩论文全程是能直接抄作业的实操内容。2. 方案选型context-mode 的四种主流设计路线2.1 滑窗模式最朴素也最容易实现滑窗Sliding Window大概是所有上下文管理方案里最直观的一种。思路就是维护一个固定长度的 token 队列新消息进来最老的消息出去。像传送带一样永远只保留最近 N 条对话。我最早在企业内部做客服机器人知识库问答时第一版用的就是滑窗。实现上很简单用一个列表存消息历史每次追加新消息后从头部计算 token 数超过阈值就从最老的开始删。代码写起来十几分钟就搞定跑起来也快。但滑窗有个致命弱点它只认新旧不认重要。用户三小时前提过我的订单号是 A12345现在问帮我查一下这个订单如果三小时里穿插了太多其他话题那段关键信息早就被挤出窗口了。模型就一脸无辜地回你您还没有提供订单号——用户当场就想卸了你的应用。这是滑窗最典型的失败场景。它适合那种对话轮次短、依赖旧信息少的场景比如简单的闲聊机器人、临时性问答。用在正经业务上基本撑不过三天。2.2 滚动摘要模式逼着模型帮你划重点滚动摘要Rolling Summary比滑窗聪明一点。它的核心思想是老内容不直接扔而是让模型把它们总结成一段摘要放进上下文里继续用。窗口里既有实时对话的完整信息又有浓缩后的历史轨迹。我记得第一次实现这个方案时心里还挺得意觉得终于解决了记忆遗忘问题。等真上线才发现坑也一点不少。第一摘要会丢细节模型总结得再仔细也不可能把每个参数、每个专有名词完整保存第二摘要本身要花钱花时间每轮都触发重新总结的话延迟和成本都会肉眼可见地往上涨第三摘要多了之后会累积误差一开始的小错误会随着多次总结越变越大到后面模型可能记住了一堆被加工过的谎言。在实际工程里滚动摘要一般不会单独出场而是和其他方案搭配。比如我现在的做法是滑窗保底只对超过一定阈值的历史段才触发摘要并且给摘要设定一个上限比如最多占窗口三分之一让摘要作为一个压缩层存在而不是一股脑把全部旧消息都变成摘要。2.3 检索增强模式先找出来再放进去检索增强RAG 思路在 context-mode 里是另一个重要分支。它和滑窗、摘要都不同——后两者是想办法装下更多历史它是只装当前最需要的那一点历史。大概思路是这样把历史消息、外部文档全部切成块chunk做向量化存进向量数据库或内存索引里。用户每次提问时先用这个问题去检索最相关的若干块把它们取出来拼进 prompt再交给模型回答。这个方案最大的收益是——可以接受无限的历史。别说几千轮对话就是把几十万字的企业知识库全喂进去每次也只需要检索出几千 token 的相关内容。成本稳定响应稳定效果在事实性问答场景下远超滑窗。但检索增强也有自己的痛处检索出来的未必是真相关的。我遇到过特别典型的情况用户问上次那笔退款什么时候到账按关键词向量检索系统召回的可能是退款政策说明那段文档而不是用户自己对话历史里的那笔交易记录。vector 相似度认的是语义看着像可业务上真正需要的可能是这个用户之前提过的那件具体事。想解决这个问题就得在检索的时候叠加用户维度、会话维度做预过滤甚至用更小的 rerank 模型做二次精排复杂度一下就上来了。2.4 混合分层模式实践中最能打的方案做了三个方案踩了一圈坑之后我的结论是不要迷信任何一种单一模式真正可靠的是把上面三种组合成分层结构。我目前在生产环境里用的结构是这样四层——L0 基础层滑窗保留最近几轮完整对话比如最近 6 轮完整信息不经过任何处理。L1 会话摘要层更早的历史消息做结构化摘要分字段保存用户诉求、已确认信息、待办事项、关键实体。L2 知识检索层用户历史中的事实型信息订单号、时间、地点、金额等单独抽出来做结构化存储需要时精确检索。L3 外部知识层公司知识库、产品文档只在用户问题涉及时按需检索融入。每次构造 prompt 时按 L0 → L3 的顺序逐层拼装每层设定预算。预算不够时先砍 L3再压缩 L1L0 永远不动。这样既保住了最近对话的完整性又不会让旧信息彻底失忆还在成本上做了控制。这套结构才是现在大家口中context-mode比较完整的形态。如果只想看一个真正可落地的模板直接按这个四层去设计自己的系统就好。3. 核心细节拆解预算怎么分、摘要怎么做、检索准不准3.1 上下文预算分配先算账再干活做 context-mode第一步往往是确认你手里的窗口到底有多大然后把这个窗口当作预算来管理。设模型的 max tokens 是 M为了控制解析长度和处理速度我会把它当做一个预算约束context budget对外只暴露这个总量内部再去拆解。预留出输出空间 R提示词模板和任务指令约占 F那留给可动态分配的内容就是 B M - R - F。举个例子我用的是 32K 窗口模型保留 4K 给输出模版和指令占 2K那动态分配量 B 是 26K。这 26K 再按比例开会分配近期对话 40%、历史摘要 25%、检索知识 25%、额外指令 10%。一开会整个 prompt 一下就清晰了。不要直接把这个预算写死在代码里。Model 名称一变窗口大小就变了客户要的输出风格一变预留空间也要变。我会在配置中心里统一放一份预算模板按模型类型分别定义比例程序启动时自动拉取并计算。上线之后观察实际调用数据如果频繁触发预算超卖就去调整比例这属于 context-mode 上线后的长期运维环节。具体到某种模型时不同模型对长上下文的真实可用性差异很大。比如同样标称 128K 的模型A 厂商在长文本任务上的效果能保持到 100K 附近B 厂商到了 60K 就开始明显丢信息。这个没法只看宣传得自己实测。我的测试方法很简单写一个测试脚本数据里有 10 个关键事实随机埋在文本的不同位置分别在 8K/32K/64K/100K 四档真实长度下去询问这 10 个事实记录模型答对几条画一条真实可用长度曲线。选 context-mode 的窗口参数时以这条曲线为准而不是包装盒上印的参数。3.2 摘要压缩策略不是简单地上模型去总结滚动摘要听起来简单——调一次模型让它把对话提炼一下。可真做起来细节非常多。先说触发时机。我最开始是每两轮聊完就总结一次结果开销巨大质量还差。后来改成按 token 数触发实时对话累计超过 4K 时才触发摘要并且不是每次全量重写而是增量式做把上一次的摘要 新产生的这段时间的对话丢给模型产出一条新摘要。这样每一轮摘要的输入量小费用低也快。再说摘要的结构化。自由文本摘要用起来很不顺手你很难判断里面到底还有没有包含订单号。所以我做过一版高效结构搞一个固定模板要求模型按指定 JSON 来输出定义 segments 字段来存放对话中的关键片段。用户核心意图当前最想解决什么问题 已确认事实用户明说过的具体信息订单号、地址、金额等 待确认事项还没有落实、后续可能需要跟进的 最近动态用户最近一次情绪或需求变化这个结构化的摘要数据在后续的检索阶段有奇效。因为 JSON 有了固定字段就可以按字段做过滤和提取比如 user 问我之前说过的地址是什么直接去已确认事实字段里找地址相关的条目又快又准完全不需要重新读全文。还要提一个很多人会踩进去的坑——摘要模型的幻觉。如果你的摘要工作由同一个小模型承担而对话里某个细节它没把握住它会自动补全造一个看似合理的错误内容出来。关键应对办法就一条摘要只做压缩不许推理prompt 里明确写只提取原文中出现的信息不得补充原文没有的内容。并且对已确认事实这类高风险字段在摘录后做一个反向校验——把原始消息里包含实体关键词订单号、手机号等的句子单独抽出来匹配一下匹配不上的宁可丢弃也不要放进摘要。3.3 检索与召回向量不是万能的需要配合精确匹配检索增强的 context-mode 里检索准是终极追求。纯粹用向量检索效果上限很低。我的做法是把检索拆成两条平行通道通道一是关键词精确匹配通道。把历史消息全量建一个倒排索引ES 或者轻量的内存索引用户新问题里提取出强实体词订单号、手机号、日期、金额先用这些词做一次文本精确搜索。通道二是语义向量通道。用 embedding 模型把问题和全部历史分块做相似度检索返回 top-K再对两队结果按权重混合排序。精确匹配的权重通常高于向量结果因为业务对话里一模一样的那串订单号永远比意思差不多的那句话更可信。这带来一个额外的工程点——实体抽取。我试过纯粹用正则硬抽能覆盖订单号、手机号、金额这类结构化模式但上次说的那个蓝色包装的这种描述性实体就无能为力了。所以实际是正则抽硬实体 模型抽软实体两路并行抽出的实体统一存成对话元数据检索时优先用硬实体卡条件。做完这一步召唤准确率基本就能从看运气稳定到八成可预期了。剩下的误差空间交给 prompt 里的兜底指令——如果上下文中没有找到相关信息明确告诉用户没有找到不要猜测宁可说不知道也不要瞎编产品口碑靠这个兜底保住。3.4 长文本切片切得好不好直接影响检索效果做检索第一环就是 chunk。很多人随便按 500 字一段切效果差就说 RAG 不行。其实大多时候不是 RAG 不行是切得太暴力。我的切片规则是结构感知切法先按 Markdown 的标题层级###、##做一级切分再按段落切单个段落超过 800 字再按句子边界拆开注意不切断完整的句子。业务文档里表格单独提取——表格切成多行文本之后语义就废了应该整表作为一个 chunk 存起来。代码片段则是保留完整函数而不是按行数截断。切完之后再给每个 chunk 写一个摘要标题检索时额外用这个标题做一次匹配加权。之前做过数据统计只加这一步检索准确率能提升一到两成——跟先读目录再翻页一个道理。还有一点别忘了对原文做 chunk 的时候同时记录它的元信息——来源文档 ID、页码、标题路径、时间戳。这些元信息会在最后给大模型的 prompt 里变成引用标注。用户看到根据 {来源} 的第三节时会显著更信任你的回答。4. 完整实操从零搭一套可运行的 context-mode 核心模块4.1 定义数据结构context-mode 的操作对象不是散乱的字符串而是一条条带类型的消息记录。我在代码里定义了一个轻量结构class ContextItem: def __init__(self, msg_id, role, content, ts, msg_typechat, metaNone): self.msg_id msg_id # 全局唯一消息ID用于溯源 self.role role # user / assistant / system self.content content # 原始文本 self.ts ts # 时间戳排序用 self.msg_type msg_type # chat / summary / retrieval / fact self.meta meta or {} # 实体、关键词、来源等元信息这个结构允许列表里的元素不只是对话消息还可以混入程序生成的摘要块、检索块、事实块。context-mode 的最终目标其实就是把不同类型的信息统一成同一种列表结构通过排序和预算控制来动态组织。接下来是上下文存储对象class ContextManager: def __init__(self, max_dynamic_tokens26000): self.items [] self.budget max_dynamic_tokens4.2 滑窗基础层实现滑窗层就是最基础的内容管理直接管理一个 deque每条消息按 token 数记账超了就弹出最老的from collections import deque import tiktoken class SlidingWindow: def __init__(self, max_tokens, encoder_namecl100k_base): self.max_tokens max_tokens self.enc tiktoken.get_encoding(encoder_name) self.queue deque() self.token_count 0 def add(self, item): tokens len(self.enc.encode(item.content)) self.queue.append((item, tokens)) self.token_count tokens while self.token_count self.max_tokens: _, t self.queue.popleft() self.token_count - t def get_text(self): return \n.join(it.content for it, _ in self.queue)注意tiktoken的实际 token 计数与你线上模型使用的 tokenizer 必须一致很多上层框架隐藏了这个细节但实测下来不同模型的 token 口径不一致。不统一的话预算就是一张废纸。4.3 摘要触发器与增量摘要增量摘要模块是 context-mode 的记忆中枢。触发条件不复杂滑窗层的累计 token 数超过阈值而且距离上次摘要之后的新消息数量也超过了指定值两个条件同时满足才执行。class SummaryEngine: def __init__(self, llm, threshold_tokens4000): self.llm llm self.threshold threshold_tokens self.last_summary_index 0 def maybe_trigger(self, manager, current_index): # 计算 [last_summary_index, current_index] 区间的 token 总数 batch manager.items[self.last_summary_index:current_index] new_tokens sum(len(self.llm.encode(it.content)) for it in batch) if new_tokens self.threshold: return None return batch def summarize(self, prev_summary, new_batch): prompt self.build_summary_prompt(prev_summary, new_batch) result self.llm.chat(prompt, response_format{ type: json_object }) self.last_summary_index len(manager.items) return resultbuild_summary_prompt 的核心内容是那个 JSON 结构模板具体我会在 prompt 里写你是会话摘要器。请提取现有摘要与新增对话中的关键信息合并输出JSON。 JSON格式 {user_intent: ..., confirmed_facts: [...], pending_items: [...], recent_status: ...} 要求 1. 只提取原文明确出现的信息禁止推理或补充 2. 如果某项为空写null不要编造 3. 原摘要中的信息若无冲突则保留若有冲突以新增对话为准测试时你会发现禁止推理和补充这句话几乎是整个摘要环节的救命稻草。删掉它小模型摘要的幻觉率能涨一倍。4.4 事实抽取与结构化存储事实层是 context-mode 的记忆锚点。从对话里把实体和事实提取成记录单独存一张表。我用一个简单的内存表配合一点正则和模型抽取。class FactStore: def __init__(self): self.facts [] def extract_and_store(self, text, msg_id): # 硬实体正则抽取 order_pattern r[A-Z]{2}\d{8} phone_pattern r1[3-9]\d{9} # 软实体交给模型抽取这里不展开 extracted {order_ids: find(order_pattern, text), phones: find(phone_pattern, text)} if any(extracted.values()): self.facts.append({msg_id: msg_id, extracted: extracted, text: text})精确匹配通道的代码骨架就是取出当前问题里的实体去 FactStore 里倒查包含这些实体的原始消息把命中结果按时间倒序放到检索队列高位。这个用实体反查原文的动作代替了向量检索在事实类问题上的疲软表现尤其在订单、客服这类场景里它是整个 context-mode 里性价比最高的一段逻辑。4.5 检索接入与重排在真实项目里检索不只要搜 所有历史消息还要搜 向量库里的文档分块。两类来源在上一节 3.3 讲过落到实现层就两个动作第一是向量库查询得到候选第二是重排把两路候选按权重合并。我用的权重规则大概这样精确实体命中的权重加 30摘要层匹配的有 20向量相似度本身按分数乘 10时间近的再加 5。如果候选总 token 超过分配预算按最终得分从高到低截断。整个过程可以做成一个retrieve函数返回排序后的 context 块直接拼进 prompt 的context段。有一个值得说的小细节重排时不要只看一条消息把命中消息前后各一条也带上。很多对话线索是跨两条消息的——用户说我把钱转过去了下一条 assistant 说收到了如果你只检索到收到了没有前一条我把钱转过去了模型根本不知道发生了什么。这类上下文窗口用代码实现就是候选块扩展成本极低但效果明显。4.6 完整组装prompt 分层拼接最终 model 输入的长文本组装就是把上面所有模块输出统一汇入def build_prompt(manager, sliding_window, summary, facts, retrieval, user_query): # 动态预算分配 L0 sliding_window.get_recent(0.40hreshold()) # 最新对话占40% L1 summary.get_summary(0.20hreshold()) # 摘要占20% L2 facts.get_relevant(user_query, 0.15hreshold()) # 事实占15% L3 retrieval.get_top_k(user_query, 0.25hreshold()) # 知识库检索占25% prompt 请阅读以下上下文后回答问题。\n prompt ## 最近对话\n L0 \n prompt ## 历史摘要\n L1 \n prompt ## 已知事实\n L2 \n prompt ## 参考资料\n L3 \n prompt ## 当前问题\n user_query return prompt各层之间的比例不必死守但有几条底线我不能破L0 永远不砍它是模型理解当前对话状态的锚点L3 检索段落如果和 L0 重复检索段优先让位L2 事实层一旦命中实体匹配上的事实永远不允许丢失。prompt 拼装完成后真正发送前再做一个 token 检查超出则按 L3 → L1 → L2 的顺序逐层缩减。这一整套流程跑下来context-mode 的核心闭环就通了。5. 真实场景里的典型案例5.1 案例一客服会话用户前 5 轮还在问退货政策是什么第 6 轮说我要退昨天买的那个蓝色耳机。由于昨天与今天之间的对话里穿插了一些其他内容的咨询纯滑窗模式下早就把下单信息挤掉了。context-mode 的做法是当用户说出退昨天买的蓝色耳机时L2 事实层精确命中订单号 JY20240115和商品 蓝色头戴耳机并直接从历史对话里取出对应片段拼进 prompt模型立刻就知道用户说的是哪一笔订单。然后开始正常处理售后逻辑。从产品侧看用户的感受就是——这个机器人记得我买了什么实际上只是 context-mode 做了一次事实召回。如果把案例拆得更细一点就能发现这里的召回键不只有订单号。用户可能会说我昨天买的那副耳机这里有个模糊的那副要处理好需要依赖的事实不只是订单号还包括商品关键词耳机和描述性信息蓝色头戴。如果只抽了订单号这个查询同样会落空。所以事实层存储的实体类型最好覆盖三类唯一标识订单号、手机号、业务名词商品、服务类型、描述性特征颜色、型号、数量。覆盖全了召回率才真的稳。5.2 案例二企业知识库问答公司内部有 200 份产品文档几十万字。用户问cloud 版和本地版的计费差异context-mode 的检索层从文档库里分割出 6 个相关 chunk全部放进 L3 层事实层再补上用户之前问过我们现在的部署方式是本地版这个历史信息L0 层保存最近几轮问答。最终模型给出的回答既有通用计费规则的知识引用又有针对这个用户当下部署条件的个性化说明还注明了信息来源。用户不需要翻几十份文档也不需要从头讲一遍自己的环境背景。知识库问答做得好不好核心就在于知识和个性化背景这两类上下文在 prompt 里是否能和谐共存context-mode 的分层设计给了他们各自的入口。这两类案例跑通之后我对 context-mode 的判断已经从是一个技术方案上升为是一种体验设计思路——用户不关心你的 token 怎么分配他只在意你有没有记住他说过的话、有没有答到他问的点上。context-mode 做的所有事本质上都是用工程手段维护一种被理解感。6. 上线前必看常见问题排查与避坑速查6.1 高频症状与根因对照症状常见根因排查方向模型答非所问L0 滑窗太小最近对话被挤出检查滑窗口径确认 L0 优先级用户说你忘了事实层未命中或事实过旧检查实体抽取规则与召回通道摘要内容失真摘要 prompt 缺少禁止推理约束调整摘要 prompt增加反向校验费用涨得厉害摘要触发过于频繁或检索冗余调高摘要阈值压缩检索 token长文档问题效果差chunk 切分过于粗暴改为结构感知切片回答看起来很装检索到了相近但不相关的内容增加重排检查权重规则这张表是我在多个项目里反复验证过的。遇到问题不要急着调 prompt 或换模型先对照这张表定位到具体层级修改成本会小一个数量级。6.2 三个必踩的坑第一个坑token 计数口径不统一。模型中有的用 cl100k_base有的用 p50k_base计数结果相差 20% 以上。如果用错了计数方法预算就名存实亡。解决方案是在系统起步阶段就封装一个统一的 tokenizer 模块所有的预算校验只能走这个模块。第二个坑摘要触发生成死循环。每次新消息都越阈值摘要引擎忙到一直调用模型系统既不进也不退。我的解法是给摘要加上冷却时间——同一会话内两次摘要之间至少间隔 30 秒或 10 条以上新消息同时设每日摘要调用上限。保护住了预算也就保护住了响应时间。第三个坑prompt 过长导致的中间遗忘。上下文很长的时候模型对 prompt 开头和结尾的部分记忆较好对中间部分相对容易忽略。所以我在组装 prompt 时把 L0 最近对话放最后靠近新问题把关键事实 L2 放开头附近接近系统指令摘要层 L1 放中间垫底。这样即使截断损失的主要是摘要层L0 和 L2 这两块最容易影响回答质量的内容都处于相对安全的位置。6.3 一套好用的调试手段context-mode 调试最痛苦的地方在于——一切都在看不见的 prompt 里发生。我建议做两件事第一件给每个会话开一个上下文快照功能。在每次请求完成后把当时组装出的完整 prompt 结构、各层 token 占比、检索命中了哪几条原样保存成 JSON。线上出问题时直接用这个快照复盘比瞎猜可能没召回高到不知道哪里去了。我在代码里就留了一个debug_dump开关微服务环境里默认关闭需要时按会话 ID 开启用完再关。第二件设计一个 Context 体检提示词。在测试环境里给模型单独发一条隐藏指令要求它回答当前上下文里的已知信息有哪些、缺失信息有哪些。通过模型的回答检查各层是否真正落进了 prompt。这套自省式检查在早期找推荐召回问题时效率极高因为模型往往会诚实地告诉你我不知道比你自己翻日志快得多。7. 我把结论放在最后如果你正要做 context-mode我给的建议是不要一开始就追求复杂的分层结构。从滑窗开始跑通主流程然后加摘要解决忘的问题再加深检索解决响应内容缺乏依据的问题最后把这些按预算和优先级组合成分层结构。每一步都有明确的验收标准每一步都能独立上线。这个演进路径比我一开始直接设计四层架构再填内容的方式要稳得多后者很容易在没验证基础层的时候就盲目堆复杂逻辑出了问题你根本不知道是哪个环节的锅。以我自己的项目经验来说context-mode 没有一步到位的银弹。每次调优本质都是在一堆互相牵制的因素里找平衡记太多会贵会慢记太少会蠢会笨。成熟的工程团队不会追求最全的记忆而是追求精确的记忆。上下文管理的目标不是记住一切而是在合适的时候想起最合适的那一小段——这个目标可以被一整套分层的 context-mode 有效地逼近。最后再分享一个小技巧如果你只愿意先做一件事那就先做事实层。它代码量最小、结构最简单但效果通常立竿见影。因为大部分让人抓狂的对话场景出问题的根源都不是模型笨而是它手里没有那条关键时刻的关键信息。把这一件事做对了用户的体验提升可能比你换一个更强的模型还要明显。
返回列表