前阵子调试一个多轮问答助手二十轮对话之后模型突然把用户早就设定好的“不喜欢铺张、预算控制在三千以内”这个偏好给忘了转头推了一个两万块的方案。那会儿我第一反应是模型不行后来翻日志才发现根本不是模型的锅——是我压根没做上下文管理。整个对话历史一股脑往窗口里塞早期的关键约束被淹没在一堆无关闲聊里模型想记住都难。打那之后我开始认真研究context-mode也就是上下文管理模式并且把它作为所有LLM应用的必选项。这篇就把我这一路的理解、踩坑以及最终落地的方法完整写出来给正在被上下文问题折磨的同学一个可直接参考的方案。这个内容不是什么高深论文就是一套非常实际的工程策略告诉你怎么把海量对话信息分类、压缩、检索、淘汰让模型在长对话和复杂任务里始终保持“清醒”。适合刚写完Hello World的LLM新手也适合已经在做Agent但被Token成本和效果漂移搞到头大的老手。接下来先从一次典型的失控现场说起。1. 先从一次“翻车”说起context-mode到底要解决什么问题1.1 一次典型的上下文失控现场我把那次事故的日志还原一下。我的助手任务是帮用户做旅行规划用户在第2轮说了“预算总共两万不想太累”第5轮说了“住宿偏好带浴缸的”第18轮问了“东京和镰仓怎么串路线合理”。正常来说第25轮用户说“按刚才说的帮我出个最终行程”时模型应该综合上面所有信息。结果实际输出完全不理预算和偏好推荐了全程高档酒店加特种兵式行程。看日志时我发现了经典问题第18轮前后用户聊了一堆“东京迪士尼排队大概多久”“台场高达像真不真”这类话题加上系统输出了一段长表格整个上下文已经超过模型窗口的一半。预算和住宿偏好在Token序列里已经被挤到了几乎看不见的位置注意力机制根本顾不上那么远的信息。更关键的是这些历史消息虽然还在窗口里但它们的“有效密度”极低——真正有用的决策约束被无关闲聊稀释了。这就是没有context-mode的后果上下文窗口不等于有效信息容量塞得进去的跟用得上的之间隔着一道鸿沟。很多工程师第一反应是“那我换更大窗口的模型”但窗口越大噪声越多成本越高问题只是被暂时掩盖了而已。1.2 context-mode的定义不是功能开关而是一整套策略现在行业里提到context-mode并没有一个公认的官方定义。我基于自己的工程实践把它总结成一句话围绕特定任务场景对上下文信息进行分类、压缩、检索与淘汰的完整管理策略。它不是模型自带的一个开关而是我们在应用层设计的一套规则。它可以拆成三个层次来看会话层Session Context管“这次对话从头到尾聊了什么”。核心解决连续性问题让模型不忘记前面轮次的信息。任务层Task Context管“当前这个业务目标进行到哪一步了”。核心解决结构性问题让模型知道现在该干什么、已经完成了什么。记忆层Memory Context管“这个用户之前积累过的偏好和事实”。核心解决个性化问题让模型在跨会话场景里记住人、记住偏好、记住规则。我见过很多团队的代码里把全部历史消息原封不动塞进Prompt这其实连第一层会话管理都没做好更别说任务层和记忆层了。真正好用的context-mode是在一次请求发出之前先想清楚这三个问题这次任务需要哪些上下文哪些上下文可以不要哪些上下文需要先做压缩或检索再放进来想清楚了再拼Prompt效果天差地别。2. 为什么说context-mode是Agent应用的“水电基础”2.1 上下文窗口是有限资源省Token就是省成本先算一笔账。假设你的应用每天处理一万次请求模型输入价格按每百万Token大约几十块钱算一次请求多塞2000个无用Token一天就是2000万Token的浪费一个月下来就是几十万Token的额外开销折成人民币那是相当可观的成本。而很多团队为了让上下文“够用”习惯性把整个历史记录带上这个浪费在企业级应用里会被放大得非常夸张。成本问题还不只是钱。窗口有上限比如有的模型是128K Token但你一旦把能塞的都塞满留给模型“思考”的空间就小了。一些模型在输入接近窗口上限时推理速度和准确率都会有肉眼可见的下降。我实测过一个场景同样一道逻辑题输入占用20%窗口时一次答对占用90%窗口时连续两次都答偏。上下文不是无限的保险箱而是需要精打细算的有限资源。context-mode的核心价值之一就是对Token做预算管理。就像装修房子面积固定你既要放沙发又要放书桌那就得规划好每个区域占多少空间而不是把家具全堆进去再看怎么下脚。我把这个Token预算拆分成系统提示、工作区、历史摘要、检索记忆等几个池子哪个池子超标了就裁剪或压缩哪个这样既保护核心信息也控制成本。2.2 上下文噪声会直接拉低任务准确率这是我最想强调的一点。很多人以为模型拿到的信息越多回答越准确实际上恰恰相反。斯坦福和UC Berkeley有一项被反复引用的研究叫“Lost in the Middle”结论是模型对输入中间位置信息的利用能力明显弱于开头和结尾。如果关键信息被埋在超长上下文的中间模型基本等同于没看到。拿我之前那次旅行规划的翻车来说预算和住宿偏好恰好就沉在中间位置。后面我又做了一组对比实验同样的任务一组把无关对话全部塞进去另一组先做压缩和关键信息抽取再塞进去结果后者的约束遵守率从62%提升到了91%。差距就是这么明显。为什么因为Transformer的注意力机制本质上是在所有Token之间做软关联上下文越长关键Token的注意力权重被稀释得越厉害。你想让模型“记住”一条信息就得让这条信息在输入里保持足够的显著性。直接删掉无关内容比让模型自己从一堆噪音里找重点可靠得多。这也是context-mode存在的根本原因——我们先帮模型做筛选而不是让模型自己扛。2.3 没有模式的管理多轮对话会“越聊越傻”长对话退化是一个渐进的过程。前10轮通常还好20轮开始出小错30轮之后可能连用户最基本的要求都开始走样。我把这种现象称为“上下文疲劳”。它产生的原因有三个信息稀释早期关键信息被后期大量无关内容掩盖。约束冲突某些场景下新旧信息互相冲突模型不知道该信哪个。方向漂移任务进行中用户改过需求但原始指令还留在上下文里模型就可能依旧按旧方向走。这三个问题靠“把历史都塞进去”完全无解。context-mode的做法是对上下文做“新陈代谢”该保留的保留该压缩的压缩该废弃的废弃该从记忆库重新捞回来的捞回来。会话级模式负责让对话连续任务级模式负责控制业务状态记忆级模式负责承载长期偏好。三层各管一段互相配合把“越聊越傻”变成一个可监控、可调试的工程问题。3. 三种核心模式与它们的应用边界3.1 会话模式滑动窗口配摘要压缩会话模式是最基础的一层它要解决的是“对话连续性”。最朴素的做法是维护一个轮次列表超过一定数量就把最早的几条丢掉。但这会带来一个问题早期信息可能包含用户明确声明的偏好直接丢就会失忆。所以实践中我几乎不用纯滑动窗口而是用“滑动窗口滚动摘要”的组合设定窗口大小比如保留最近20轮完整内容。当新消息进来导致窗口溢出时把最早的10轮抽取一份摘要。摘要在下次请求时作为一条“历史回顾”放在上下文靠前的位置。原始消息从窗口里移除只保留摘要。这个做法等于给上下文做了“存档”最近细节完整远期只留要点。要点包括什么我通常让摘要至少覆盖四类信息用户主动声明过的偏好、已经确认的决策、尚未解决的待办、以及任务相关的硬性约束。至于吃了几顿饭、聊了几次天气这种信息直接丢弃就好。摘要的生成本身也需要消耗Token所以不是每一轮都做而是设一个阈值或者按Token溢出触发。我习惯按Token量触发历史总Token超过某个水位线才执行压缩。压缩完还要注意把新摘要重新写回上下文顶部确保它在模型的注意力范围前端。摘要丢失细节这个问题我后面会在排查章节单独讲。3.2 任务模式结构化状态机锁定业务进度如果会话模式管的是“聊了什么”任务模式管的就是“活儿干到哪了”。我见过太多翻车案例用户已经走到下单确认步骤了模型还在问“你对这个产品还有什么要求吗”这就是因为模型根本不知道任务进行到哪一步。任务模式的核心是把业务流程抽象成一个状态机。状态包含四个要素当前节点比如“收集需求→生成方案→确认预算→下单支付”中的某一个环节。已收集字段比如目的地、天数、预算、出行人数哪些已经有值哪些还是空的。剩余必填项业务流程中还没有被满足的硬性条件。关键上下文标签当前状态对应的重点信息索引。在拼Prompt的时候任务状态不是简单地追加在历史后面而是放在系统提示之后的“任务工作台”区域用结构化字段表示。模型每次收到请求第一眼看到的是当前任务的进度和待办然后才是最新对话。我实测这种设计对多步骤Agent的准确率提升非常明显尤其是“自动规划工具调用顺序”的场景状态机制比让模型自己从对话里猜快得多也稳得多。任务模式还有一个好处它天然解决了“用户中途改需求”的难题。因为字段是可覆盖的用户改了预算任务工作台里那个“预算”字段直接更新成新值。只要字段更新正确模型就会按新值走不会被旧记录的残留干扰。这是纯历史对话做不到的。3.3 记忆模式向量检索托底长期记忆会话模式护住了短期连续任务模式锁住了业务进度但还有一个场景需要第三层用户隔了一周回来继续对话或者这个Agent要服务多个长期用户每个人的偏好不同。这时候你需要的是记忆模式。实现上我采用“双层记忆”架构长期档案库用向量数据库存储用户在历次会话中暴露出的偏好和事实比如“用户是素食主义者”“用户偏好安静环境”“上次定制方案选了轻奢风”。每一条记忆都带时间戳和来源会话ID。短期记忆区当前会话里识别出的新增偏好先临时保存在这里等会话结束时再归入长期档案。每次请求到来时根据当前任务的关键词去向量库做一次Top-K检索召回和当前场景最相关的记忆拼进Prompt。这个做法比“把所有历史会话全部检索一遍”便宜得多效率也高得多。召回条数我通常控制在5到10条再多就容易引入噪声和互相矛盾的记忆。记忆模式最大的坑是“记忆污染”用户某次随口说了一句“这个也行吧”结果模型把它当成了长期偏好之后每次推荐都默认这个方向。解决方案是在记忆写入之前加一道置信度判断。我现在的标准是连续两次以上出现的相同偏好或者在会话中被用户明确强调过的信息才允许写入长期档案。单次出现的模糊表达最多放在短期记忆区不升级。3.4 模式选择的判断标准三层模式不是每次请求都要全上。全上会带来Token浪费和复杂度爆炸。我的选择逻辑非常简单只有一轮对话、没有后续依赖只上会话模式甚至直接用当前问题拼Prompt。多轮连续对话、任务目标明确会话模式加任务模式。跨会话、需要记住用户长期偏好三层全上但记忆层只做轻量Top-K召回。按需组合不按套路全堆。上下文管理的复杂度应该跟业务场景的复杂度成正比而不是跟工程师的兴奋程度成正比。我见过有些项目一上来就搞三个向量库加五层记忆结果模型被各种召回内容搞得更乱了。从最少的模式组合起步跑通基础效果再逐步加层这个顺序我推荐给所有人。4. 从零搭建一套context-mode完整落地流程4.1 设计上下文数据结构我先给出一套我实际在用的数据结构。这个设计把“模式选择”变成了一个可执行的计算流程而不是每次靠拍脑袋组装Prompt。以下代码用Python展示跑通逻辑后你可以平移到你自己的语言和框架里。from dataclasses import dataclass, field from typing import Optional from enum import Enum class ContextLevel(Enum): SESSION session # 会话级短期连续 TASK task # 任务级业务进度 MEMORY memory # 记忆级长期偏好 dataclass class Message: role: str # system / user / assistant content: str timestamp: int dataclass class TaskState: node: str # 当前业务节点 fields: dict # 已收集的字段值 required: list # 剩余必填项 updated_at: int dataclass class ContextBundle: level: ContextLevel system_prompt: str # 系统提示词 task_state: Optional[TaskState] recent_messages: list field(default_factorylist) history_summary: str # 滚动摘要 memory_hits: list field(default_factorylist) # 记忆召回结果这里的关键是ContextBundle这个打包对象它在一次请求中承载了全部要发给模型的上下文信息。组装顺序也写在代码的字段顺序里先是system_prompt再是task_state然后是history_summary和memory_hits最后才是recent_messages。为什么要这个顺序因为前面讲了“Lost in the Middle”——你把最重要的状态信息放在Context开头模型对它的注意力最强。这也是我几次调试之后才定下来的顺序。4.2 实现模式分发与上下文组装数据结构定义好之后接下来是组装逻辑。我写了一个ContextManager类按不同模式拼接最终Prompt。核心思路是根据当前对话轮次和任务状态判断该启用哪层模式然后逐层补充信息。class ContextManager: def __init__(self, system_prompt, window_size20, compress_threshold8000): self.system_prompt system_prompt self.window_size window_size self.compress_threshold compress_threshold self.messages [] self.summary self.task_state None self.memory_store None # 向量库客户端按需注入 def add_message(self, msg: Message): self.messages.append(msg) self._maybe_compress() def _maybe_compress(self): total_tokens self._estimate_tokens(self.messages) if total_tokens self.compress_threshold and len(self.messages) self.window_size: overflow self.messages[: len(self.messages) - self.window_size] self.summary self._summarize(overflow) self.messages self.messages[-self.window_size:] def build_bundle(self, task_keywords: list None) - ContextBundle: bundle ContextBundle(levelContextLevel.SESSION, system_promptself.system_prompt) bundle.task_state self.task_state bundle.recent_messages self.messages[-10:] bundle.history_summary self.summary # 如果启用了记忆层且有关键词做一次向量召回 if self.memory_store and task_keywords: bundle.memory_hits self.memory_store.search(task_keywords, top_k5) bundle.level ContextLevel.MEMORY # 如果任务状态存在层级升级为任务模式 if self.task_state is not None: bundle.level ContextLevel.TASK return bundle def to_prompt(self, bundle: ContextBundle) - str: parts [f[System]\n{bundle.system_prompt}] if bundle.task_state: parts.append(f[Task State]\nnode{bundle.task_state.node}\n ffields{bundle.task_state.fields}\n frequired{bundle.task_state.required}) if bundle.history_summary: parts.append(f[History Summary]\n{bundle.history_summary}) if bundle.memory_hits: mem_text \n.join( f- {item.payload[keyword]}: {item.payload[content]} for item in bundle.memory_hits ) parts.append(f[User Memory]\n{mem_text}) if bundle.recent_messages: chat_text \n.join( f{m.role}: {m.content} for m in bundle.recent_messages ) parts.append(f[Recent Chat]\n{chat_text}) return \n\n.join(parts)_summarize方法我没有贴完整实现它的核心是用一个较便宜的小模型把溢出内容压缩成要点要求它尽量保留“用户明确表达的偏好、已确认的决策、未完成的待办”。_estimate_tokens则是对消息内容做粗略的Token估算通常用字符数除以3或者调用现成的分词器。这两个方法规模不大属于工程细节你自己实现的时候可以自由发挥。这个类的设计里有一个细节值得多说一句build_bundle里面判断层级的顺序。我们先用记忆层关键词做召回如果存在任务状态则升级为任务模式。这个顺序是有讲究的——任务状态代表“当前这件事的结构性进度”它的优先级天然高于记忆召回因为发散记忆不能覆盖业务流程的确定性。反过来如果没有任务状态纯闲聊场景就退化到记忆层加会话层的组合也不浪费Token。4.3 关键参数计算Token预算怎么分有了组装逻辑接下来是预算分配。我以本地用到的128K窗口模型为例讲一套我自己长期使用的分配比例。这个比例不是拍脑袋定的是根据多轮实测总结出来的。预算池上限比例实际Token数用途说明系统提示与任务状态10%约13K角色设定、业务流程状态、必填字段历史摘要与记忆召回15%约20K滚动摘要、长期偏好、跨会话信息最近对话40%约50K当前轮次的最近消息保留完整细节模型输出预留25%约32K留给模型思考和生成的空间安全余量10%约13K兜底防止单次消息过大突发超限这个分配表的核心逻辑就是给模型“留白”。很多团队把上下文塞到95%留给模型输出的只有5%结果模型经常生成到一半就截断或者生成质量明显下降。我建议任何任务模式下模型输出预留不低于窗口的15%推理类任务最好到25%以上。具体到你自己的模型如果窗口是32K把上表按比例缩放就对了。缩放时要注意一个细节系统提示和任务状态这两块尽量不要缩减因为它们是决策骨架。需要缩减的是“最近对话”可以调小窗口轮数或者对最近消息做单条Token裁剪。记忆召回也可以从5条减到3条。省哪里都别省系统和任务状态这是我在调参中反复验证过的一条铁律。4.4 多场景验证方法模式搭好之后不能只看一两轮对话效果就上线要做系统性的验证。我自己常用的方法是做一组“约束保持测试”构造一个需要多轮信息累计的任务比如旅行规划。在对话的前几轮分散埋入3到5个明确约束。连续对话超过20轮中间穿插大量无关闲聊。在第25轮左右提出一个“整合所有约束”的要求。检查模型输出是否全部命中约束。我跑这个测试时没有context-mode的情况下命中率大概60%到70%上了会话加任务双模式之后能到90%左右再加记忆层处理跨会话偏好命中率基本稳定在95%以上。剩余5%的问题通常不在上下文结构而在模型本身对冲突信息的取舍这个要单独处理。除了效果验证还要做成本回归。对比相同对话量下开启context-mode前后每次请求的平均Token数。我的经验是会话模式的滚动摘要大约砍掉40%的历史Token占用记忆模式的Top-K召回相比全量检索能省80%以上。省下来的这部分配额我一律分给模型输出预留效果提升非常直接。5. 实战中的坑与排查技巧5.1 症状压缩摘要后关键约束丢了这是滚动摘要最常见的坑。明明原对话里有“预算不超过三千”摘要生成之后模型却完全无视这条。我排查后发现摘要是用便宜模型生成的它在压缩时会倾向于保留对话中反复出现的词而不是主动识别约束。如果用户只说了一次“预算不超过三千”这条信息在摘要阶段很容易被省略。我的解决办法有两个。第一摘要生成时给模型明确的抽取规则让它必须逐项检查“用户偏好”“硬性约束”“已确认决策”这三类信息是否存在存在就必须保留宁多勿少。第二关键字段同时备份到任务状态里。这个就是双保险思路摘要交给自然语言结构化字段交给状态机只要字段在模型就一定能从任务工作台里读到约束。两条腿走路之后这个坑我再没踩过。5.2 症状上下文截断出现严重语义漂移窗口溢出时有些实现直接从头暴力截断消息结果模型突然不知道自己在做什么回答风格也变了。我遇到过最典型的一次模型从“帮用户整理行程”变成了“帮用户写旅游攻略”因为截断后的上下文里充满了景点介绍而任务指令被切掉了。解决这个问题要区分两种情况。如果是会话模式永远不要切断任务状态和系统提示这两层宁可把最近对话收紧到5轮也不能把任务状态管道挤掉。如果是长文档输入不要简单按Token位置截断而是按语义段落截断优先保留开头和结尾的语义。实在万不得已需要截断那么被截掉的部分必须做一个摘要块放在整体上下文的最前面。这个摘要块能挽尊让模型知道它的基本身份和任务方向。5.3 症状记忆检索结果污染主任务记忆层做Top-K召回时如果关键词太宽泛比如用户说“帮我推荐餐厅”而历史记忆里关于“美食偏好”的条目有几十条一下子召回5条里面可能混着用户三年前的口味。我遇到过一次用户之前说爱吃辣后来因为胃不好改吃清淡了但模型被旧记忆带着走推荐了一堆川菜被用户直接投诉。排查之后我做了三件事一是召回前先过滤近期活跃记忆超过90天没被触发的记忆降权二是每次召回后加一道“相关性打分”用轻量模型判断召回内容跟当前会话主题是否相关不相关的直接扔掉三是字段冲突时以任务状态和最近对话为准记忆层只做参考不做判决。这个优先级顺序最近对话任务状态记忆召回是我踩了不少坑之后总结出来的原则它保证模型遇到信息冲突时有明确的裁决规则而不是自己瞎猜。5.4 症状上下文膨胀导致响应变慢还有一种情况是context-mode本身出了问题滚动摘要没触发记忆召回条数过多最近对话全量保留导致每次请求的输入Token比裸奔时还多模型响应从1秒慢到3秒用户体验直线下降。排查时需要先确认压缩阈值是不是设得太高。我之前设过12000 Token才压缩后来实测在长对话场景里超过8000 Token就开始出现性能和准确率的双重下降阈值要往下拉。另外还要检查记忆召回是不是每次都在做。纯闲聊场景根本不需要向量召回只有检测到任务关键字段变化时才触发检索这样能把大量无意义请求的Token开销省下来。我给所有ContextManager都加了日志把每次请求的Token分布打印出来哪个池子超标一眼就能看到比瞎猜有用得多。5.5 调试习惯我每次必做的三件事最后分享三个我固定执行的调试习惯它们帮我省下了大量排查时间。第一在所有上下文组装入口加一个debug开关把最终拼好的Prompt完整打印出来。虽然这个Prompt可能很长但肉眼检查一遍能发现很多逻辑上没问题但实际放错了位置的上下文。第二做一次“盲测”——把模型输出盖住只看输入问自己一句如果我是模型光靠这份输入能不能知道用户要什么、任务到哪一步、有什么硬性约束如果连人都看不出答案模型也多半不行。第三建立黄金用例集。挑20个典型对话场景每次修改上下文策略后都跑一遍回归不做这个你根本无法确认一次优化是变好了还是把原来对的东西弄坏了。黄金用例集就是上下文的单元测试是保证长期稳定性的底线。我自己的项目稳定之后会固定每周跑一次全量回归顺便看看Token成本的变化趋势。Context-mode不是什么高端魔法它就是一套把上下文这个核心资源管好的工程方法论。你只要愿意在结构上多花一点心思效果上的回报绝对远超预期。这套方案我还在继续迭代后续打算在任务状态里加入自动字段纠错和冲突检测等有更多实践沉淀了再回来写续篇。
阅读完成 · 觉得有帮助?