
做之前想先跟你聊一个我自己的真实经历。早先做AI对话类应用的时候我踩过一个特别典型的坑用户问一句模型答一句单独看每个回答都没毛病但只要对话超过五六轮模型就开始“失忆”把最早聊过的关键信息全丢了。我当时的第一个反应是“把整个历史都丢给模型”结果token消耗直接翻了几倍费用暴涨响应也慢得让人抓狂。后来我又试了“只截取最近的几轮”省是省了但用户如果问“我之前跟你说过的那件事”模型照样一脸懵。问题的核心不在模型而在于上下文——你用什么方式把“记忆”喂给模型。今天想跟你聊的就是我自己在项目里打磨的一套“context-mode”上下文模式方案。它不是一个库也不是某个模型自带的功能而是一套管上下文的思路加落地工具。简单说它解决的是“要在有限的上下文窗口里装下最值得模型看到的信息同时保证输出的质量和稳定性”这件事。如果你也在做多轮对话、文档问答、Agent这类AI应用这篇文章值得你花几分钟看完基本可以少走我当年那些弯路。1. 项目背景到底卡在哪儿了1.1 对话流程里的“记忆黑洞”市面上绝大多数大模型都有上下文窗口上限比如4K、8K、32K、128K不同模型的数字差别很大。可窗口再大也是有限的钱和有限的算力。我们不可能把所有历史消息、工具返回、文档片段、系统提示全部塞进去。就算塞得下模型也未必知道该重点关注哪些内容反而容易被大量冗余信息干扰回答质量直线下降。我把这个问题拆开看觉得它其实是“三个层面”叠加出来的存储层历史对话和外部知识放在哪儿内存数据库向量库这里涉及的是数据的组织方式。调度层每次请求到底该取哪些内容进来按时间按相关性按重要性这里决定的是上下文的长相。适应层内容进来之后怎么按模型的要求格式化、压缩、去重避免超出token限制这里决定的是模型能不能真正用上这些内容。早先我用的办法是“暴力全量拼接”。就是把所有对话历史用分隔符拼起来塞进system prompt。这种做法在小规模demo里可以跑但一上生产环境就出问题token超限报错、响应延迟变高、用户等得不耐烦。而且模型面对一大坨文本时注意力会被均匀分散关键信息往往淹没在冗长的记录里。后来我换了“简单截断”只保留最近N轮。这个办法能控制token但代价是模型完全丢失了早期信息。用户要是问“我们最开始提到的那份合同怎么付款”系统根本无从回答。这两个极端方案本质上都没有回答一个老问题什么样的信息绝对不可以丢什么样的信息丢了也没关系。context-mode 想解决的就是这个取舍问题。1.2 context-mode 的定位一把“可插拔”的上下文管理方案说白了context-mode 是我给自己的应用层加的一层“上下文调度器”。它不关心下游用的具体模型是GPT还是Claude还是国产开源模型它只负责解决一件事在每次请求发出前把“该给模型看的信息”准备好、拼装好。它把上下文当成一种可以“显式管理”的资源而不是被动地往prompt里堆字。这个方案有几个突出特点多模式可选不同任务、不同阶段可以套用不同的上下文策略。可组合多种策略可以串联形成一条处理流水线。可观测每次请求都用到的上下文内容会被记录方便调试。低成本接入不依赖特定框架只要在业务层加一个中间处理环节就行。我自己的项目里这个方案已经跑了大半年服务过几个垂直场景。下面我会从设计思路、实现细节、实战效果和踩坑记录这几个方面把完整经验摊开讲清楚。2. 核心概念与方案选型为什么不能只用一种模式2.1 我总结出的五种上下文模式在做方案调研的时候我看了很多RAG框架和Agent框架的实现几乎每个系统都有自己的“上下文处理黑话”。但剥开外衣大家都在用下面这些模式的变体。我给它们起了自己的叫法方便内部沟通模式核心做法优点缺点全量模式 (Full)把所有历史消息和知识片段完整拼进请求信息无损耗适合短对话token成本高、延迟高、长对话溢出滑动窗口模式 (Window)只保留最近N轮或最近M个token实现简单成本稳定会丢早期关键信息摘要模式 (Summary)用模型或规则把历史压缩成摘要再放入上下文可控压缩、保留主线摘要本身有失真风险、生成摘要要额外耗时检索模式 (Retrieval)把历史/知识写入外部存储按语义相似度拉取相关片段可处理超长历史、信息精准需要维护索引、有召回失败风险代表性子图模式 (Highlight)从对话中抽取关键实体、行为、诉求以结构化清单形式注入格式稳定、模型易理解抽取依赖模型能力复杂语义照样丢这些模式没有绝对的好与坏纯粹看场景。拿客服机器人举例如果只是查物流状态滑动窗口就够如果是售后纠纷处理用户可能在第1轮就说明了订单号第20轮还在追问进度这时候就必须把订单号这种关键信息摘出来或者用摘要模式保存主线。两种模式一结合效果立刻不一样。2.2 为什么单一模式走不远我见过不少人把“简单截断”当默认方案觉得省事。可只要业务一复杂它就会暴露出明显缺陷用户上下文存在多种粒度。年龄、地址、偏好、情绪、历史结论这些信息的重要程度不一样。截断是按时间粒度切但人的记忆是按重要性组织的。只按时间切自然会把重要信息切没了。任务的上下文依赖关系不一样。有的任务只依赖最近几轮比如“把上句话翻译成英文”有的任务依赖整个会话的全局信息比如“根据我们所有讨论内容写一份总结报告”。用一套固定窗口处理不了这种差异。成本与体验需要动态平衡。大促期间流量高可能想牺牲一点上下文丰富度来换速度平时可以多花点token换质量。一个固定模式没法调。所以我在设计context-mode的时候刻意把它做成了“上下文处理流水线”而不是“某个单一策略”。它有个基础调度器根据当前会话长度、任务类型、预算约束自动选择、组合不同的上下文模式。这样既能利用每种模式的长处又能互相弥补短板。3. 从零搭一个 context-mode核心代码与配置3.1 先把上下文的“原材料”结构化不管用什么模式都得先把散乱的对话历史整理成结构化数据。我自己定义一个最简的会话消息模型大致长这样from dataclasses import dataclass, field from typing import Optional, List, Dict, Any from enum import Enum class MessageRole(str, Enum): USER user ASSISTANT assistant SYSTEM system TOOL tool dataclass class Message: role: MessageRole content: str timestamp: float meta: Dict[str, Any] field(default_factorydict) # meta 里可以放 token数、消息ID、意图标签、来源文档ID等 dataclass class Session: session_id: str messages: List[Message] field(default_factorylist) # 独立的会话属性比如用户ID、场景类型、上下文预算 profile: Dict[str, Any] field(default_factorydict) context_budget_tokens: int 8000这里有个容易被忽略的点meta字段很重要。每条消息不仅要存“谁说了什么”还要存这条消息的附加信息比如这条消息是用户主动陈述还是系统给的提示它引用了哪个文档片段它的token数是多少这些元数据会在后面做摘要、检索、突出关键信息时派上大用场。没有元数据的消息就是一堆死文本没法被智能调度。3.2 上下文处理流水线从原始历史到模型输入context-mode 的完整处理流程我按顺序拆成四个环节读取从数据库/缓存里拉出当前会话的消息列表。分析数token、识别任务类型、检测当前是否出现“需要长程记忆”的用户提问。规划根据上下文预算决定用哪种模式或模式组合。渲染把选定内容按目标模型的 prompt 格式拼好生成最终的 messages 列表。第2步“分析”是整个流水线的灵魂。我加了一个轻量分类器判断当前用户意图属于“最近依赖型”还是“全量依赖型”。举个例子如果用户说“把上一条回复改得幽默一点”这就是最近依赖型滑动窗口完全够用如果用户说“还记得我最初提的核心诉求吗”这就是全量依赖型必须触发摘要模式加检索模式。代码层面我建议直接用函数式写法让每个模式都实现同一个接口。这样后续扩展新策略很方便class ContextMode: def process(self, session: Session, user_input: Message) - List[Message]: raise NotImplementedError class SlidingWindowMode(ContextMode): def __init__(self, window_size: int 10): self.window_size window_size def process(self, session: Session, user_input: Message) - List[Message]: recent session.messages[-self.window_size:] return recent [user_input] class SummaryMode(ContextMode): def process(self, session: Session, user_input: Message) - List[Message]: # 假设 summary_engine 是一个调用LLM做摘要的模块 summary summary_engine.summarize(session.messages) return [summary, user_input] class RetrievalMode(ContextMode): def __init__(self, vector_store, top_k: int 5): self.vector_store vector_store self.top_k top_k def process(self, session: Session, user_input: Message) - List[Message]: query user_input.content hits self.vector_store.search(query, top_kself.top_k) return hits [user_input] class PipelineContextMode(ContextMode): 组合多个模式的流水线实现 def __init__(self, modes: List[ContextMode]): self.modes modes def process(self, session: Session, user_input: Message) - List[Message]: combined: List[Message] [] for mode in self.modes: combined.extend(mode.process(session, user_input)) return deduplicate_and_truncate(combined)deduplicate_and_truncate这个函数是我自己写的作用是去掉重复片段并按预算截断。因为不同模式可能会拉到同一段内容比如摘要模式和检索模式同时命中了好消息不合并的话会重复占用token。合并逻辑可以简单按内容hash查重也可以稍微智能一点用字符串相似度判断近义重复。3.3 模式切换的触发阈值怎么定模式调度器不能拍脑袋乱切。我用的是一套可配置的阈值体系所有阈值都带默认值可以在项目里按需求覆盖。会话长度低于2000 token直接全量模式不做任何压缩。短对话损失最小。会话长度在2000到8000 token之间默认滑动窗口摘要模式。窗口保留最近10轮早期内容压成300 token以内的摘要。会话长度超过8000 token切到检索摘要组合。把历史整体写入向量库增量索引每次请求按当前用户输入做召回再配合一个全局摘要兜底。这套阈值不是什么金科玉律但它是从我的生产环境实测出来的一个起点。你可以根据模型上下文窗口大小、任务复杂度、预算重新调。当然这里有一个重要细节如果下游模型本身支持超长上下文比如128K是不是就可以无脑用全量模式我的体会是不建议。窗口越大模型对中间信息的注意力越容易分散成本也越高。实测下来同样一个问题4K上下文明明白白回答清楚的硬塞到128K全量模式下反而容易被无关历史带偏。能少花钱还更准为什么要用笨办法4. 实战案例一个多轮客服助手接入 context-mode4.1 场景建模与参数选择我用自己做过的一个售后客服Bot来举例。这个场景有比较强的“长程记忆”需求用户会在第1轮报出订单号和问题描述之后的多轮可能需要翻回来看。但整个会话也不宜保留太多感性聊天内容比如“你好”“谢谢”这种寒暄对任务没有任何价值。我当时定的参数是这样的模型一个上下文窗口为8K的中型开源模型跑在自有GPU上。预算单轮请求上限5K token留出3K给模型生成。全量开关首轮直接全量因为首轮信息密度高。摘要策略每满4轮把前4轮压成一段150字以内的摘要替换原文。这样实际喂给模型的是一份不断滚动的“会议纪要”。检索策略把“订单号”“地址”“问题的核心描述”抽取成结构化字段时刻保留在上下文的工具区域。就算摘要失真了核心元数据也不会丢。这套方案跑起来之后效果让我挺意外。简单截断模式下用户平均要重复自己的订单号1.5次接入context-mode后需要重复的次数降到了0.1次以下。4.2 效果对比与数据反馈我拿三个方案做了个小型评测同一个测试集、同一组模型只是上下文处理逻辑不同方案单轮平均token消耗关键信息召回率用户满意度说明全量拼接7.8K92%高但评分不稳成本最高且超出8K后只能截断或报错固定滑动窗口(最近5轮)2.4K48%中低省token但忘得太快context-mode组合3.9K89%高且稳定成本可控信息召回接近全量模式关键信息召回率怎么算我在测试集里标好每一轮对话涉及的“必提实体”比如订单号、地址、型号。每次模型生成结果后我检查这些实体是否正确出现在回答里命中比例就是召回率。这个指标比人工看回答圆不圆滑更客观因为它直接衡量“模型有没有使用该用的信息”。当然这个方案不是没有代价。摘要模式每几个来回就要调用一次LLM做压缩这会产生额外的延迟和费用。我后来做了一层优化只有摘要涉及的历史消息里真的包含“高价值实体”时才触发压缩纯寒暄内容直接丢弃压根不占空间。这样既降低了摘要频率也保留了上下文的核心价值。4.3 细节不要让“压缩动作”本身干扰模型这里有个我踩过的小坑特别提醒一下。把历史消息替换成摘要之后模型看到的对话痕迹会突然从完整句子变成一段总结性文字。如果摘要写得过于“客观”比如“用户表示订单未收到”模型可能不知道这句话到底是谁说的、在哪个环节说的。所以我要求摘要必须带说话人和时序标识。一个合格的摘要条目大致长这样[用户·第1轮] 反馈订单 #A12345 已付款但未收到要求在3天内处理 [客服·第1轮] 回复会优先核查物流轨迹 [用户·第3轮] 追问物流信息未更新情绪激动要求升级处理这种带角色和时间戳的摘要模型一看就知道“这是用户之前说过的原话要点”而不是一段凭空冒出的叙述。它维护了对话视角的一致性对多轮任务特别重要。5. 踩坑记录与排查手册5.1 典型问题速查表我把自己在这个项目里遇到的典型问题整理成了一张表希望你能少走弯路现象可能原因排查方法解决办法模型回答突然开始胡言乱语摘要丢了关键约束或注入内容顺序错乱打印实际发往模型的完整messages给摘要加“必须保留的约束字段”校验token超出限制预算设置没算输出token给上下文预算设置小于窗口上限预留20%的buffer给生成回答检索内容牛头不对马嘴向量化模型领域适应差查看召回的相似度分数和原文换用领域微调的embedding模型或改混合检索历史信息有矛盾摘要更新不及时新旧信息并存检查会话状态版本号摘要写入时递增版本调度器只认最新版响应变慢明显摘要或检索步骤串行调用埋点统计各阶段耗时把摘要做成异步预生成或并行检索相同问题不同答案上下文内容不一致模式切换不稳定固定随机种子并复现同一对话对关键任务固定使用同一模式组合每条问题后面都有一条核心经验一切上下文问题先看“最终进入模型的magic content”是什么。我经常在自己的代码里加一行日志在发送模型前把messages打印出来。很多看起来诡异的问题一看到实际prompt立刻就知道病根在哪。5.2 三个容易忽略的细节第一个细节是上下文里的“位置间竞争”。模型对不同位置的注意力分布是不同的。头尾信息往往更容易被模型记住中间内容容易被“注意力稀释”。我后来在设计顺序时会把最重要的系统指令放在最前面把用户当前问题放在最后面历史摘要和检索片段放中间。这是一个小调整但对稳定性的提升非常明显。第二个细节是工具的返回结果要及时纳入上下文管理。如果你的应用接入了工具调用比如查订单、查天气工具返回的结果也是上下文的组成部分。我在程序里会把工具返回的较长JSON做一层“字段裁剪”只保留当前决策需要的关键字段而不是把整个JSON原样塞进历史。不然工具返回一大坨无用字段照样会挤占宝贵空间。第三个细节是上下文管理要区分“开发环境”和“生产环境”。开发时你可能不在乎token成本可以随意全量但生产环境要保证响应延迟和费用可控。我建议给context-mode加一个“环境开关”开发模式强制全量以便调试生产模式走正常调度。这能避免“本地一切正常上线就超时”的尴尬。5.3 一段真实的“翻车”修复经历说一个具体案例。上线初期我遇到过用户连续追问同一件事模型却前后矛盾的情况。第一次用户说“我住在北京”系统返回“根据你的地址提供北京门店”。后来用户说“我一直用上海的手机号”系统立刻改口“根据上海号码所在地提供上海门店”。结果用户质问你到底记不记得我在北京排查后发现摘要模式只保留了地址和手机号的最近提及没有做冲突检测。旧信息被覆盖新信息直接顶替模型自然就“忘”了前文。修复方案很简单在摘要里增加“用户属性”独立区块地址、偏好这类长期属性不随对话滚动删除而是做版本化更新。用户后面提到新地址我会保存“新地址 更新时间”但不会覆盖旧地址。需要用到地址时调度器会看上下文中的最新值并额外提示“用户曾经提到过旧地址”。这样一来模型既知道现在的地址也不会忽略历史。这种“保留旧值标记更新时间”的做法本质上是给模型提供了一个“记忆的时间轴”。它比单纯覆盖新值要可靠得多尤其适合那些需要追踪用户变更信息的场景。6. 进阶方向与个人体会6.1 从单轮上下文到多智能体共享上下文如果context-mode只服务于单场对话它的能力边界还是比较窄的。我现在正在做的扩展是把它从“会话级别”拉升到“组织级别”。怎么理解多个智能体在协同处理一个复杂任务时A智能体产出的结论可能是B智能体下一轮推理的输入。每个智能体如果各自管理自己的上下文信息就会在环节之间断裂。我现在尝试的做法是把多个智能体共享的核心事实写入一个叫做“shared_context”的状态区context-mode 在渲染每个智能体的提示词时先从这个共享区拉取与当前任务相关的片段。比如A智能体负责分析用户情绪B智能体负责给出业务方案B的上下文里要能看到A的判断结论同时又不被A处理的大量原始文本淹没。这个方向还在摸索中但有一个原则我始终没有变上下文调度永远面向“当前环节需要做决策的最小信息集”。不是信息越多越好而是让正确信息恰好出现在正确位置。6.2 我对 context-mode 的几点体会文章写到这里我自己也顺了一遍思路。关于 context-mode我最后想分享三条个人观点不保证全对但都是实操中验证过的第一上下文管理不是模型能力的附属品而是应用层的核心能力。很多团队花大价钱换更大窗口的模型却不改进自己的prompt和上下文调度逻辑。结果模型升级了问题依旧存在。换模型不如先把自己的上下文策略理清楚。第二不要迷信某一个“高级模式”。RAG很流行但它不是万能的。很多对话场景里一个带角色标记的滚动摘要比向量检索更直观、更稳定。正确做法是把各种模式当作工具箱里的扳手和螺丝刀按场景选而不是按潮流选。第三所有上下文策略都必须在“可观测”的前提下运行。没有日志记录的上下文管理就是个黑盒。一旦用户反馈模型行为异常你连当初给了模型什么内容都不知道排查就无从谈起。我甚至建议在自己项目的根目录下加一条启动日志# 每次启动时打印当前 context-mode 配置 python -m context_mode --show-config # 输出示例 # modepipeline, budget_tokens5000, summary_threshold4, # retrieval_top_k5, dedupTrue, envproduction有了这条信息线上遇到的问题才能快速与配置关联起来。现在回头去看当时我拿着两套方案来回折腾的那些日子并不算浪费。context-mode 真正帮到我的不是某一个具体的压缩算法或检索技巧而是让我养成了一种习惯每次请求模型之前先问一句“我现在给它看的信息是不是它做出正确判断真正需要的那部分”这个问题看起来简单但能做好的人不多。你只要沿着这条线想下去大概率会自己遇到本文提到的那些细节然后发现办法总比问题多。