context-mode:让大模型上下文可治理、可回溯、可压缩的分层管理模式
context-mode一套让上下文可治理、可回溯、可压缩的分层管理模式做智能助手类应用做得越久越能体会到一个词上下文失控。我维护的某个内部问答系统最开始就是一个把所有东西都塞进Prompt的方案——系统指令、多轮对话历史、检索到的知识片段、工具返回的JSON全部拼接成一个超长字符串丢给模型。前几轮还好一旦对话超过十轮各种问题就冒出来了模型开始把历史对话里的玩笑话当成用户需求把工具返回的错误信息当成事实复述把某个已经结束的任务目标死死记着不放。更麻烦的是你根本说不清它为什么会这样因为上下文是一锅粥你没法定位是哪一段话导致了错误行为。后来我停止拼字符串的思路参考操作系统的运行模式设计了一套显式的上下文管理模式也就是这个叫context-mode的项目。它的核心思想很简单所有进入模型视野的内容都必须归属到某一个明确的模式Mode中每个模式有自己的生命周期、优先级、权限边界和裁剪策略。今天这篇文章就把这套方案的完整设计、代码实现和实测中踩过的坑全部写出来。如果你正在被多轮对话、长文档、多Agent场景下的上下文混乱问题折磨这套方案可以直接抄作业也能帮你看清上下文管理的本质问题到底出在哪里。1. 为什么需要一套显式的上下文模式失控对话背后的根因先说说我一开始为什么非要做这件事而不是继续调Prompt。当时我负责的某跨平台系统有一个多角色协作的问答助手它既要做自然语言理解又要调用工具去查数据、执行操作还要在长时间对话里记住用户偏好。崩溃现场是这样的用户在某天上午说了一句这个功能太垃圾了到了下午助手在回答一个完全不相关的问题时居然把太垃圾当成对产品的不满信号在回复里主动道歉并提出改造方案。我检查日志后发现那段用户的情绪表达始终躺在历史对话里模型根本分不清它只是一次性吐槽还是当前问题的约束条件。这就是上下文混叠Context Aliasing的典型症状。所有信息源——系统指令、对话历史、工具输出、知识库摘要——在同一个窗口里相互污染模型没有机制去辨别这段话是规则还是闲聊这个数据是临时的还是需要长期记住的。我当时把问题归纳成三个来源第一是工具输出污染。助手调用一个接口查询数据接口返回了格式不完整的结果模型没有意识到这是错误反而把错误信息当成有效答案展示给用户。工具的数据应该只是参考它的生命期应该很短不能被模型当作持久化事实。第二是用户情绪的误入。用户随口一句抱怨、一个反问被模型当成了新指令或新约束。用户的话需要按意图区分有的是任务目标有的是过程反馈有的是无关的杂音。第三是任务目标的残留。用户先让助手查A项目的测试报告查完之后又让助手对比B和C的报价如果旧任务的目标还停留在上下文里模型就会继续拿A项目的测试数据去干扰B和C的对比。这三个问题有一个共同点信息没有边界。如果每一条信息都有明确的归属模式——它属于长期记忆还是会话记录它是一次工具返回还是一条系统约束——模型读取上下文时就不需要自己去猜我们也可以在构建上下文时决定放什么、不放什么、什么时候清掉什么。我参考的是多进程操作系统里的运行模式概念让上下文具备显式的模式身份。每个上下文片段携带一个模式标签模式决定它的保留时间、优先级、可见范围、冲突时的裁决方式。这就是context-mode的由来它不是一个新技术框架而是一套可执行的约定——外加一套轻量级的管理器代码——让哪些内容该在什么时候生效变得显式、可配置、可审计。2. 五个模式的分层设计与各自生命周期在设计模式集合的时候我最初列了十几个候选后来发现追平性太强反而增加配置负担。最终收敛为五个模式覆盖了智能助手类应用绝大多数上下文需求。这五个模式不在一个平面上而是有明确的分层关系下面这张表是我项目里实际使用的配置概览。模式职责生命周期优先级典型Token预算System Mode模型角色、安全边界、输出格式永久冻结仅人工变更1001200~1500Session Mode当前会话的基本事实、用户ID、会话级偏好随会话创建和销毁80500~800Task Mode当前任务目标、约束、已完成的子步骤任务开始创建任务结束清除601500~2000Memory Mode跨会话沉淀的用户事实、项目常量按TTL和置信度衰减40800~1200Tool Mode最近一次工具调用的输入输出摘要默认一次有效20800~1000这个设计有三个关键约定。第一优先级严格递减——System Mode永远不能被其他模式的内容覆盖或稀释第二生命周期严格分离——System和Session属于稳定的基底Task是动态聚焦区Memory是长期沉淀池Tool是短命的临时区第三所有内容进入模型之前都要经过一次归类和裁剪。2.1 System Mode全局指令层的冻结与变更审批System Mode放的是不可随意变动的部分模型应该以什么角色回应、必须遵守的安全红线、输出格式和字段约定。这类内容有几个特点它必须每次都出现在上下文里不能被历史对话挤出去它不允许被工具输出或用户话语覆盖它的更新必须走明确的代码路径而不是在某个运行时被动态追加。我这里有一个很实用的做法把System Mode的原始内容单独存放在一个配置文件里运行时只做读取-组装绝不动态修改。如果需要在运行中调整行为比如用户要求切换语气我不会改动System Mode而是在Task Mode里追加一条本轮对话请使用平实语气的新约束。这样做的原因很朴素系统级指令应该是可审计、可回滚的任何一次变更都应该能追溯到原因而不是被某个上下文片段悄悄改写。2.2 Session Mode会话级状态与滑动窗口Session Mode记录的是这个会话是谁在进行、说过什么基础事实、默认语言是什么这类信息。它比对话历史更精简是对话历史的摘要级表示。比如用户在多轮对话里提到过一次自己用的是某地区版本我会把这条事实抽出来放到Session Mode里而不是让模型每次从长历史里找。设计Session Mode的时候我踩过一次不小的坑它和Memory Mode的边界很容易模糊。用户说我在用精简版客户端这到底是会话内的事实还是需要长期记住的偏好我的约定是如果这条信息在当前会话的活跃轮次里会影响回答就先放Session Mode如果它表现出明显的跨会话稳定性——比如用户两次会话都提到同一偏好——就升级到Memory Mode。会话结束后Session Mode整体归档不再参与下一次对话。2.3 Task Mode当前目标的临时聚焦区Task Mode是整个方案里最动态的部分。每一次识别出用户的明确意图就创建一个Task实体把目标、约束、子步骤、相关的工具调用结果都挂到这个Task下。用户问查一下第三季度的收入并生成图表这个Task Mode里就会记录目标查询Q3收入并生成图表已完成查询了收入数据待完成生成图表关联工具某数据库接口。Task Mode最关键的机制是释放。任务完成后这个Task必须被归档或清除不能让旧目标残留在上下文里。我在后面的实测部分会专门讲这个释放不及时导致的连锁问题这里先说设计原则新任务到来时旧任务要么进入已完成归档区只保留一条摘要要么被标记为superseded被取代绝不继续保持Active状态。2.4 Memory Mode跨会话沉淀与衰减策略Memory Mode是唯一一个跨会话持续存在的模式。用户常驻偏好、项目约定、需要注意的坑都会沉淀到这里。但我并不建议把所有长期记忆都堆给Memory Mode它有明确的容量上限而且必须带衰减机制。我采用的衰减维度有两个时间和命中率。一条记忆如果30天内没有被任何会话触发它的权重就会逐步降低最终从活跃记忆池里移除。同时每次它被实际检索命中并使用到它的修订时间会刷新权重会回弹。这是模仿人脑的记忆巩固机制做出来的效果比固定TTL要自然得多。Memory Mode里存储的不是原始对话而是抽取出的原子事实每一条都有字段标注来源、记录时间、置信度。如果一条事实用户亲口确认过两次以上它的置信度就高衰减也更慢。2.5 Tool Mode工具结果区与一次有效原则Tool Mode在最底层也是我踩坑最多的区域。工具返回的数据、API的错误码、检索到的候选片段都先进入Tool Mode。这里的原则非常简单粗暴默认一次有效。也就是说除非后续某轮对话显式引用并确认了这条工具输出否则它在下一轮组装上下文时就不会被带上。这个原则一开始执行起来有点别扭因为有时候工具返回的数据是用户后续追问的基础。比如用户先问A公司去年的营收是多少工具返回了数据用户下一句问那它的毛利率呢这时前一轮的工具结果并不是完全无效的。我的处理方式是当工具输出被明确引用进当前Task的已完成区时它会脱离Tool Mode升级为Task Mode里的已确认事实如果只是路过式的Query不进入Task就按一次有效处理。这样一来工具数据既不会长期霸占上下文又不会在真正需要时被误杀。3. context-mode协议用可配置的规则定义上下文行为只有模式还不够我还需要一套让模式运转起来的协议。context-mode的协议本质上是一份声明式的配置文件外加一个上下文管理器ContextManager来执行这些规则。这样做的好处是模式的行为规则不再是散落各处的硬编码而是可以被版本管理、被测试、被非技术人员审核的独立文件。3.1 一份实际的模式配置示例我在项目里用的配置是这样的context_mode: version: 1 modes: system: priority: 100 ttl: null # 永不过期 max_tokens: 1500 update_policy: manual # 仅允许手动更新 session: priority: 80 ttl: session # 随会话结束 max_tokens: 800 task: priority: 60 ttl: dynamic max_tokens: 2000 clear_on: task_complete # 任务完成后清除 stale_after_rounds: 5 # 超过5轮未被引用标记为stale memory: priority: 40 ttl: 30d max_tokens: 1200 decay_factor: 0.8 min_confidence: 0.6 tool: priority: 20 ttl: one_round # 默认一次有效 max_tokens: 1000每一行都不是随便写的。clear_on: task_complete对应前面说的任务释放机制stale_after_rounds: 5是给Task Mode加的保险丝——如果一个任务5轮内没有任何新的进展或引用它会被自动标记为可能已过期避免僵尸任务长期占用预算decay_factor: 0.8意味着记忆权重每过一个衰减周期乘以0.8min_confidence: 0.6则直接滤掉了那些来源不明、用户没有确认过的低置信度事实。3.2 模式的优先级与冲突裁决规则模式之间有冲突时怎么裁决我建立了一套简单的规则列表按顺序执行System Mode内容不可被任何其他模式覆盖。如果用户话语与系统约束冲突以System为准并记录冲突日志。Task Mode的显式约束优先于Session Mode的暗示性信息。比如Session里记录用户喜欢简洁回答但当前Task明确要求输出详细的对比表格以Task为准。Tool Mode默认是参考证据不参与对用户直接陈述的否决。工具说文件不存在但用户明确说文件我已经上传了此时优先信任用户的直接陈述工具结果标记为与用户陈述冲突待复核。Memory Mode的低置信度内容不参与优先级裁决。它只在没有更高优先级信息冲突时才会被使用。这套规则听上去很自然但实际落地时有一个大坑如果不把这些规则显式编码模型自己去做判断它几乎总是偏向最近的上下文。工具刚返回文件不存在模型立刻就对用户说您的文件不存在完全忽略了用户两分钟前刚上传过文件的事实。所以我把冲突裁决做成ContextManager的一个独立方法在组装上下文时就打上合法的标签而不是把裁决责任交给模型。3.3 ContextManager核心类的实现思路协议文件的背后是一个大概两百行以内的ContextManager核心类。它的主要职责就是三件事接收信息并归档到正确的模式按规则裁剪Token组装最终上下文。from dataclasses import dataclass, field from datetime import datetime, timedelta from typing import Optional dataclass class ContextEntry: mode: str # system / session / task / memory / tool content: str source: str # 来源user / tool / llm / system_config created_at: datetime field(default_factorydatetime.utcnow) ttl: Optional[timedelta] None priority: int 0 confidence: float 1.0 meta: dict field(default_factorydict) class ContextManager: def __init__(self, config: dict): self.config config self.entries [] def add(self, entry: ContextEntry): mode_cfg self.config[modes][entry.mode] entry.priority mode_cfg[priority] if mode_cfg[ttl] session: entry.ttl None # 会话清理时统一处理 elif mode_cfg[ttl] one_round: entry.ttl timedelta(seconds1) # 在一次组装后被标记删除 elif mode_cfg[ttl] dynamic: entry.ttl None else: entry.ttl timedelta(days30) self.entries.append(entry) self._prune() def _prune(self): # 1. 淘汰过期项 now datetime.utcnow() self.entries [ e for e in self.entries if e.ttl is None or (now - e.created_at) e.ttl ] # 2. 按模式汇总Token超出max_tokens的部分从低优先级开始裁 # 3. 标记tool模式为待清理核心逻辑就这三个方法add、_prune、assemble_for_inference。assemble_for_inference会把System、Session、Task、Memory、Tool按优先级排序并用清晰的分隔标签拼装成最终Prompt。实际工程里还会加一个resolve_conflicts()方法专门处理上面说的冲突裁决。3.4 为什么不直接拼接字符串非要走这套协议有同事问过我加这一层管理是不是有点重。我的回答是如果你只做一个Demo拼接字符串确实更快但只要你的上下文来源超过三个对话、工具、知识库并且需要支持长会话直接拼接的成本会快速超过协议的实现成本。走协议有三个直接收益一是可审计性。用户抱怨模型为什么突然这么回答时我可以用ContextManager导出一份完整清单它看到了哪些System约束、哪条Task目标、哪条Memory记忆、哪次Tool返回。定位错误的时间从猜模型变成了查记录。二是可回滚性。某条记忆或某个任务导致行为异常时可以直接从配置或存储中删除对应条目下一轮自动生效。不需要重新改Prompt模板也不需要清空整个会话历史。三是可测试性。协议配置是数据文件可以写单元测试。我在项目里做了一套回归用例用固定对话序列跑模型检查模型是否在旧任务残留时还受旧目标影响检查工具错误信息是否被当成事实传播。这些测试在纯字符串拼接阶段根本没法做因为你无法稳定地控制模型哪句话会被记住。4. 上下文检索与组织从全量塞入转向按模式取用模式定义清楚之后真正的工作才开始怎么在每一轮对话里决定哪些内容该进入上下文。过去是全量塞入简单但低效现在需要按照模式标签做选择性取用。这个环节我分了四步每一步都踩出过可复用的经验。4.1 每轮组装管线的四个步骤第一步确定当前活跃模式集合。大多数轮次里活跃的是System、Session、TaskMemory按需激活——只有当用户提问涉及过往事实时才会召回Tool只在当前轮次有工具调用时才存在。有个经验之谈Memory不要每轮都全量召回那会让上下文里堆满无关的记忆反而干扰模型。我的做法是先对用户当前输入做一个意图分类命中查询过往信息类意图时才触发Memory检索。第二步按模式执行检索。System和Session是直接读取Task需要提取当前任务块Memory用关键词和向量双路召回取TopKTool则直接读取本次工具调用的输出缓存。这里要强调一点Memory检索的召回应尽量克制我限定每轮最多召回3条长记忆宁可少召回也不要把不相关的旧事实堆给模型。第三步冲突裁决与优先级排序。按3.2的规则处理冲突然后从高到低排列。排列顺序就是最终Prompt的呈现顺序System在最前Tool和低置信度Memory在最后。第四步Token预算分配。这是最容易失控的一步。我给不同模式分配了预算上限当一个模式的内容超出上限时不是简单截断而是做压缩或摘要。下面是项目里实际使用的分配策略总预算TokenSystemSessionTaskMemoryTool20006002006003003003000700250100050055040008003001500600800比例不是固定的理论上如果当前轮不需要Memory它的份额就全部让给Task。我用的分配算法是先按比例分再按需求动态调整系统指令永远占固定份额Task如果本轮有新增工具调用结果会临时扩容Tool作为最底层一旦超预算首先被裁剪。4.2 上下文摘要与压缩的坑预算不够时我的第一反应是压缩历史对话但这里有一个典型的坑摘要压缩会丢失细节而丢失的细节往往正是模型需要的。比如用户三轮回前说我只想看销售额不要利润数据这个约束如果不进Task Mode只躺在摘要里模型十轮之后可能就忘了这个约束细节。后来我把压缩策略改成分层压缩System和Task的核心内容绝不压缩只压缩Session的历史Session历史压缩时确保不丢失用户显式表达过的约束、喜好和否定句。为了做到这一点我在Session Mode内部会单独维护一个关键事实清单每次从历史对话里抽取用户显式表达的事实放进这个清单。后面的对话即使不压缩模型也能从清单里快速获得稳定约束。4.3 快照机制与回滚能力还有一点很多人会忽略上下文本身需要快照。我在系统里维护了一个快照表每一轮对话组装完成后把最终的上下文模式状态哪些模式活跃、各自内容是什么、Token占用多少存一份。出现异常行为时可以直接翻到出问题的那一轮看模型看到了什么而不是靠事后猜测。快照的存储不是每轮都全量保存那样太占空间。我采用差异快照只有模式状态发生变化时才记录一份全新快照否则只记录增量变化比如Task Mode新增了一条约束Memory Mode某条记忆置信度提升。整个实现大概几十行代码换来的调试效率提升是非常明显的。5. 实测三轮对话压测与高频坑位最后分享一下这套方案在真实业务里的表现和踩坑记录。我在某业务场景跑了将近三个月的压测主要对比的是纯字符串拼接方案和context-mode方案在长会话中的表现。5.1 多轮压测结果对比我设计了一个20轮对话脚本里面刻意加入了几类棘手情况用户中途改任务、工具偶发错误、用户说反话、跨会话引用旧偏好。对比结果如下表情况旧方案拼接表现context-mode表现第8轮之后跑题约40%概率受旧任务干扰低于5%受旧任务干扰工具错误信息被复述常见模型会相信工具输出被标记模型依据Session存储判断旧任务残留常见Task归档机制有效残留率大幅下降记忆冲突模型取最近内容容易前后矛盾按置信度和优先级裁决相对稳定平均Token消耗随轮次直线上升第20轮接近9000稳定在3500~4000这里要说明压测数据不能代表所有场景但它揭示了一个实质性的趋势显式的模式管理在轮次一长、任务一多的情况下收益是指数级的。前五轮内两种方案几乎没有差别但从第8轮开始拼接方案的失控概率肉眼可见地上升。5.2 高频坑一Task Mode的旧目标不释放这是我最先遇到的坑实现初期我在Task释放逻辑上偷了懒只在任务看起来完成的时候手动调用clear_on结果模型经常误判任务已完成而用户下一个问题实际还在同一个大任务下。表现为用户让助手查A公司的营收助手返回了数据用户接着问那它的利润呢助手居然认为上一个任务已经完成新任务被判定为一个独立任务于是把A公司这个关键实体丢了。修复方案很简单但有效新intent进入时先检查是否有Active Task如果有对比新旧Task的实体集合如果实体高度重叠就将新intent合并进旧Task而不是新建。这行逻辑让连续追问的流失率大幅下降。因为追问场景里用户说那它的利润呢它指向上文实体合并比新建更符合人的对话直觉。5.3 高频坑二Tool Mode与Session Mode的返回冲突工具返回错误时模型会天然倾向信任工具输出。我在压测里专门设计了一个例子工具返回文件不存在读取失败但Session Mode里记录了用户两分钟前说文件已上传。旧方案里模型回答您的文件不存在请重新上传用户自然火大。context-mode的裁决规则是用户直接陈述优先于工具结果但光有规则不够模型仍然可能违背。我的处理是在组装Prompt时给这样的冲突场景显式加一条提示语例如把Tool Mode里与Session冲突的内容标记为该结果与用户陈述冲突请优先采信用户陈述并核实。这个做法听起来很笨但它非常有效——模型看到冲突标签后行为改变率显著提升。这说明一个问题不要指望模型自动执行隐含规则规则要显式放进上下文里。5.4 高频坑三Memory Mode的衰减阈值Memory阈值设置是另一个容易翻车的点。一开始我把TTL设得很短7天decay_factor设成0.5结果老用户半个月前确认过一次的偏好在第10天就被洗掉了。用户再次提到这个偏好时模型一脸茫然。我在日志里看到这种失忆现象后把TTL调成30天min_confidence维持在0.6同时对高置信度记忆增加了命中刷新逻辑——只要被实际使用过一次TTL就重新计时。这里的关键认知是记忆衰减不是为了让所有记忆都消失而是为了让噪声消失。用户显式确认过的事应该比模型自行推断的事活得更久。所以我后来给每条Memory加了字段confirmed_by_user: bool用户确认过的记忆不参与衰减只有模型推断的记忆才走衰减流程。这个字段来自一个简单观察用户亲口说的几乎不会错模型自己猜的经常是错的。5.5 这套方案的适用范围与边界最后说清楚context-mode不是什么。它不能替代Prompt工程也不是一个能自动解决所有对话问题的框架。它的价值在于建立上下文治理的入口让每一段内容都有归属、有边界、有生命周期、有追溯能力从而让其他更高级的技术比如记忆抽取、意图识别、Agent编排可以在一个干净的基础上工作。如果你现在面临的只是偶尔模型答非所问调调Prompt可能就够用了但如果你和我一样需要处理的是多个信息源长期叠加的复杂场景那么花一个下午把上下文改成mode模式是很划算的投资。它不会让你的模型变得更聪明但它会让你的系统在变复杂之后仍然可控。我在实际部署过程中最大的体会是context-mode真正解决的不是省Token或提准确率这种量化指标而是把上下文从一个看不见摸不着的黑盒变成了可以打开、检查、插拔的模块。每次模型表现异常我不再一脸茫然地猜Prompt哪里写错了而是打开ContextManager的快照直接看到它那轮看到了哪条Task、哪条Memory、哪条Tool输出问题往往一眼就定位了。后面我还在这套机制上扩展出了多Agent场景的上下文隔离功能——每个Agent拥有独立的ContextManager实例System和Session隔离Task共享时走明确接口这套基础架构一直沿用到现在。如果你也在做类似的系统建议从最简单的模式定义开始跑通一轮对话再逐步加Memory和Tool的边界规则不用一开始就追求大而全毕竟上文本治理这件事做到能解释每个决策就已经成功了一大半。