
说个很典型的场景。你在开发一个AI助手调试了半天发现模型回答总是不对劲上一轮用户明明说“把预算控制在5万以内”下一轮问“那时间呢”模型一本正经地回答“时间大概需要5万”——上下文全乱套了。再往后查用户多聊几轮后历史消息堆得越来越长token预算爆炸模型开始胡言乱语。这问题我前前后后踩了半年最后沉淀出一套叫“context-mode”的上下文管理模式今天把它完整拆开讲清楚。这套模式不是什么高深算法而是一整套关于“上下文如何组织、如何流转、如何过期、如何与业务逻辑解耦”的工程规范。它适合正在做AI对话系统、Agent应用、知识库问答的人参考也适合在前端项目里被React Context搞得焦头烂懦的兄弟——思路是通用的。核心价值就一句话让上下文像水管一样该通的时候通该断的时候断绝不回流。1. 为什么需要context-mode从一次线上事故说起1.1 上下文混乱的真实故障先说一个我实际经历的事故。当时做一个客服智能应答系统模型基于OpenAI的ChatCompletion接口。用户和机器人聊到第8轮时模型突然开始重复回答第2轮已经回答过的内容并且引用了一段早就被撤回的消息。排查了很久最后定位到根因系统把所有聊天记录一股脑塞进messages数组没有做任何过滤、压缩和角色区分。用户撤回的消息照样在数组里躺着系统提示词里关于“你是客服”的角色设定也被用户的某条消息给“覆盖”了——因为消息顺序里用户消息排在系统消息后面模型搞不清谁是客服了。这就是典型的“上下文无状态管理”问题没有模式全靠临时拼接。当时我就在想如果有一套明确的上下文模式把系统角色、用户输入、工具返回、历史摘要分门别类并且规定各自的优先级、有效期和拼接规则这个事故根本不会发生。1.2 context-mode的定义与核心价值我所说的“context-mode”是一套关于上下文生命周期的显式设计方案。它包含四个核心要素上下文的分层结构、上下文的流转规则、上下文的过期机制、上下文与业务逻辑的隔离方式。拿生活类比一下。你把上下文想象成办公室里的便利贴墙。没有管理模式时所有便利贴乱贴重要信息被盖住过期信息没人撕有context-mode时墙面分区紧急事项红区、项目进度蓝区、长期备忘绿区并且每天下班前有人专门清理过期便利贴。模型就像这个办公室里的新员工他看到的墙面是整理过的自然知道哪些信息重要、哪些信息已失效。这套模式的价值不只是“不出错”还包括token成本可控不会无限堆积历史、响应速度稳定输入长度可控、调试体验好每次请求都能说清楚上下文里有什么、多业务复用一套上下文框架服务多个功能。1.3 适用场景与边界context-mode不是银弹。它最适合下面这些场景多轮对话系统尤其是对话轮次可能超过10轮的场景Agent应用需要反复调用工具、并把工具结果回填给模型需要对上下文做审计、回放、测试的系统多个业务功能共享同一套模型的场景它不适用的情况也很明确一次性单轮请求比如文本分类、情感分析完全没有状态概念极简原型验证先跑通再设计上下文也不迟以及那些根本不需要“记忆”的纯计算任务。强行套用反而增加复杂度。2. 核心设计思路把“上下文”当一等公民2.1 上下文的四层结构在设计context-mode时我第一件事就是抛弃“messages数组”这个扁平结构把上下文拆成四层。第一层会话层Session Layer。这是上下文的“容器”包含会话ID、用户ID、开始时间、结束时间、会话状态。它不直接参与模型推理但它是所有上下文的索引入口。没有这一层你没法做多用户隔离、历史回放、会话恢复。第二层任务层Task Layer。这是容易忽视的一层。一个会话里可能包含多个任务用户先问“帮我写个活动方案”又突然问“昨天的日志看了吗”这是两个完全不同的任务。任务层负责记录当前正在执行什么任务、任务的目标是什么、相关的背景资料有哪些。有了任务层上下文切换才成为可能——模型知道现在应该聚焦哪个任务。第三层状态层State Layer。这是业务语义的核心。它保存的是“已经确认的事实”比如用户预算5万、项目截止时间是月底、用户偏好蓝色配色。这一层是结构化的键值对而不是自然语言。它负责回答模型和业务系统“我们现在知道什么”。第四层消息层Message Layer。这是最接近原始API的层保存真实的对话消息、工具调用记录、模型输出。但注意这层不应该是无脑全量的——它应该只保留当前任务相关的、未过期的消息。2.2 上下文的生命周期管理有了分层就得定义生命周期。我总结了五个状态创建、激活、挂起、过期、归档。上下文创建发生在会话开始时。激活状态表示当前正在使用的上下文挂起发生在任务切换时当前任务暂时不参与推理但状态保留过期是自动触发的比如某个临时信息超过30分钟未更新、或某条消息被用户明确撤回归档是会话结束后的处理压缩后存入持久化存储供后续审计或统计。这里有个关键设计原则状态流转必须是显式的不允许隐式变更。什么意思就是“上下文从激活变成挂起”这件事必须由代码明确调用接口而不是靠“刚好没人用就自动挂起”。隐式流转是bug温床——你不知道它什么时候变了出问题也无法复现。2.3 上下文与业务逻辑解耦很多项目把上下文管理和业务逻辑写在一起比如在业务代码里直接拼接prompt。这样做前期很爽后期想死。业务代码改一行prompt可能就崩了prompt改一个词业务逻辑可能无感知地受影响。context-mode强制要求业务逻辑只能通过接口读写上下文不能直接操作消息数组或字典。这就像数据库和业务代码之间必须有ORM或Repository层一样隔离带来的是可测试性和可维护性。我在项目中是这样落地的业务层发指令“记录用户预算”“切换任务”context-mode的上下文管理器负责执行并决定怎么拼接给模型。3. 实操实现一个可落地的context-mode骨架3.1 数据结构与存储设计直接用代码说话。我用Python实现了一个精简版数据库层用了Redis做缓存、PostgreSQL做持久化但核心结构不依赖具体存储。from dataclasses import dataclass, field from typing import Dict, List, Optional, Any from enum import Enum import uuid import time class ContextStatus(Enum): CREATED created ACTIVE active SUSPENDED suspended EXPIRED expired ARCHIVED archived dataclass class SessionContext: session_id: str field(default_factorylambda: uuid.uuid4().hex) user_id: str status: ContextStatus ContextStatus.CREATED created_at: float field(default_factorytime.time) updated_at: float field(default_factorytime.time) current_task_id: Optional[str] None dataclass class TaskContext: task_id: str field(default_factorylambda: uuid.uuid4().hex) session_id: str goal: str status: ContextStatus ContextStatus.ACTIVE created_at: float field(default_factorytime.time) meta: Dict[str, Any] field(default_factorydict) dataclass class StateContext: session_id: str states: Dict[str, Any] field(default_factorydict) updated_at: float field(default_factorytime.time)消息层我单独设计因为它需要支持截断和摘要dataclass class MessageItem: role: str # system | user | assistant | tool content: str task_id: str timestamp: float field(default_factorytime.time) message_id: str field(default_factorylambda: uuid.uuid4().hex) token_count: int 0 expired: bool FalseRedis里的Key设计为session:{session_id}- SessionContext JSONtask:{task_id}- TaskContext JSONstate:{session_id}- StateContext JSONmessages:{session_id}:{task_id}- ZSetscore为timestamp这个结构的好处是每个维度都可以独立访问和过期互不干扰。3.2 核心接口与调用约定context-mode不是让你直接操作这些数据结构而是通过一个管理器对外提供服务。接口设计遵循最少暴露原则。class ContextManager: def create_session(self, user_id: str) - str: ... def set_state(self, session_id: str, key: str, value: Any) - None: ... def get_state(self, session_id: str, key: str) - Any: ... def switch_task(self, session_id: str, goal: str) - str: ... def add_message(self, session_id: str, role: str, content: str, task_id: Optional[str] None) - None: ... def get_prompt_messages(self, session_id: str, max_tokens: int 3000) - List[Dict[str, str]]: ... def expire_message(self, session_id: str, message_id: str) - None: ... def archive_session(self, session_id: str) - str: ...这里最核心也最复杂的接口是get_prompt_messages它负责把四层上下文组装成模型能理解的messages数组。我把它单独拉出来讲。3.3 prompt组装的核心逻辑这个接口要回答一个问题给模型的输入里到底放哪些东西、按什么顺序放、超出长度怎么压缩。我的组装顺序固定为系统级指令固定提示模型身份和行为边界状态层关键信息转成“当前已知信息”的陈述当前任务的目标与背景当前任务相关的历史消息按时间正序用户最新输入顺序不能乱。系统指令放最前面是为了让模型尽早建立行为基线状态层放前面是让模型带着“已知事实”去看历史对话避免理解偏差历史消息放后面是因为越靠后的信息模型关注度越高最近的消息权重理应最高。token预算控制我用一个简单但有效的策略设定总预算max_tokens按比例分配系统指令固定10%状态层占20%任务目标占10%最新用户输入不裁剪、保底20%剩下的40%给历史消息。超出40%时采用“滑动窗口摘要压缩”两段式处理先保留最近N条完整消息更早但重要的内容用摘要替代摘要本身也作为一条system消息插入。def get_prompt_messages(self, session_id: str, max_tokens: int 3000) - List[Dict[str, str]]: session self._get_session(session_id) state self._get_state(session_id) task self._get_task(session.current_task_id) history self._get_active_messages(session_id, task.task_id) system_base 你是一个智能助手基于以下上下文信息进行回答。 state_text 当前已知信息 ; .join(f{k}{v} for k, v in state.items()) task_text f当前任务{task.goal} budget { system: int(max_tokens * 0.1), state: int(max_tokens * 0.2), task: int(max_tokens * 0.1), history: int(max_tokens * 0.4), latest: int(max_tokens * 0.2), } messages [{role: system, content: system_base[:budget[system]]}] if state_text: messages.append({role: system, content: state_text[:budget[state]]}) messages.append({role: system, content: task_text[:budget[task]]}) history self._trim_history(history, budget[history]) messages.extend(history) return messages_trim_history里的滑动窗口是从后往前保留消息直到累计token超过预算如果还有更早但标记了important的消息就把它们合并成一条摘要消息放在历史最前面。这个important标记是业务层在调用add_message时主动设置的比如用户明确说“记一下截止日期是月底”。3.4 与LLM API对接的细节组装好的messages直接透传给模型API。这里有一个我反复强调的细节不要把API返回的完整历史无脑塞回上下文而是先经过add_message重新入层并更新状态层。举例子。用户说“我喜欢蓝色”模型说“好的我记住了”。如果你只是把这两条消息追加进messages数组那状态层并不知道“用户喜欢蓝色”这个事实。下次用户换一个话题模型很可能就忘了。正确的做法是业务代码解析用户消息提取结构化事实喜欢蓝色写入状态层同时把对话消息追加到消息层。这样状态层成为事实的唯一来源消息层只是原始证据。def handle_user_message(session_id: str, text: str): cm.add_message(session_id, user, text) facts extract_facts(text) # 业务层的NLP逻辑 for k, v in facts.items(): cm.set_state(session_id, k, v) prompt cm.get_prompt_messages(session_id) reply call_llm(prompt) cm.add_message(session_id, assistant, reply)4. 踩坑记录与排查技巧实录4.1 经典问题速查表我整理了过去项目里最常遇到的context-mode相关问题做成速查表建议直接收藏。问题现象根因解决方案模型“失忆”同一会话内模型忘掉前几轮信息状态层未提取结构化事实只靠消息层原始文本在业务层增加事实提取写入状态层模型“串台”用户切换任务后模型还在答上一个任务任务层和消息层没有按task_id隔离切换任务时显式调用switch_task并过滤历史token突然暴涨服务端账单异常历史消息无上限追加设置max_tokens预算滑动窗口摘要压缩撤回消息仍生效用户撤回后模型仍引用过期机制未实现调用expire_message并在组装时过滤expired消息模型角色混乱模型开始模仿用户说话消息顺序中用户消息干扰了系统角色系统指令置顶用户消息严格放靠后并用分隔符包裹4.2 上下文污染与隔离的实战处理上下文污染是最隐蔽的问题比token爆炸还难查。我遇到过一个案例一个Agent应用同时服务多个任务任务A的用户问了一句“帮我查一下上海天气”任务B的用户也问“上海天气怎么样”结果任务B的回答里居然带了任务A的决策依据——“因为您之前说过要带伞”。排查后发现问题出在状态层的key用了简单字符串比如“location”、“weather”两个任务共享同一个session时发生了覆盖。解决方法是给状态层的key增加任务维度前缀task:{task_id}.location。这也印证了分层设计的必要性状态不是全局的必须绑定任务。另一个常见污染是工具调用结果的残留。Agent调用工具查询数据库后把原始SQL结果整个塞进了上下文。下次用户问一个不相关的问题模型仍然引用上一轮的SQL结果。我的处理方式是在工具结果入层时加一个expired_after_turn标记下一次组装prompt前自动清除这类临时性上下文。4.3 性能与持久化的取舍上下文做得很重之后性能问题会浮出水面。Redis缓存命中率高还好一旦缓存未命中每次请求都要从PostgreSQL查历史消息再组装延迟直接翻倍。我做了两个优化。第一组装结果缓存。把get_prompt_messages的输出按session和消息数量hash缓存到RedisTTL设为5秒。同一用户短时间内连续请求可以直接复用组装结果。第二异步归档。会话结束后的归档操作不放在请求链路上而是投递到消息队列异步处理。这样会话关闭接口的响应时间不会因大段历史序列化而抖崩。持久化的取舍上我建议消息层全量落库但定期清理原始内容保留摘要状态层必须全量持久化不能丢它是业务逻辑的事实来源会话层只保留必要索引。这套组合兼顾了成本和分析价值。4.4 调试与可观测性没有可观测性的上下文管理等于裸奔。我在context-mode里加了三个维度的日志每次组装prompt时记录各层实际使用的token数量、是否触发了截断或摘要每次状态更新时记录旧值和新值以及触发更新的业务行为每次过期发生时记录过期对象ID和原因这样线上出了任何上下文问题直接查日志就能快速定位到“哪一层、什么时候、因为什么发生了变化”。我还加了一个对比工具输入两条不同的会话路径输出它们组装出的prompt差异用来回归验证上下文规则修改的影响。5. 从context-mode延伸不同场景的适配方案5.1 前端场景下的context-mode这套思路稍加改造也能用到前端特别是React的场景。React原生Context的主要槽点是value变化时所有消费组件都会重渲染性能容易出问题。用context-mode的思路来做前端状态管理核心是分层和隔离。我实践过一种方案把全局状态拆成AuthContext、ThemeContext、BusinessContext三个独立Context业务Context下面再按功能模块拆分。每一层只订阅自己关注的状态变化并通过selector做浅比较避免大范围重渲染。这里的“过期机制”对应的是组件卸载时清理状态避免内存泄漏。关键收获是前端状态混乱的根源往往是“所有状态塞进一个全局Store”context-mode的分层哲学同样适用——给状态分域、给状态定生命周期、用接口约束读写。5.2 多模型与多Agent场景下的context-mode如果项目发展到多个模型协作比如一个规划模型加一个执行模型context-mode的重要性会进一步放大。每个模型其实只需要上下文的某个子集规划模型需要任务层和状态层的全局视图执行模型只需要当前任务的详细消息。context-mode的接口天然支持这个需求——为不同模型定义不同的get_prompt_messages变体各自指定关注层级和token预算就能实现。我曾经做过一个竞品分析Agent规划模型负责拆解问题维度执行模型负责抓取和总结。最初共用一套上下文经常出现执行模型引用规划模型的内部推理导致输出混乱。接入context-mode后给执行模型单独建了一个“只见局部”的上下文视图只包含与当前子任务相关的消息和状态问题直接消失。5.3 context-mode的扩展方向上下文压缩这块还有很大空间。目前我用的是“滑动窗口关键消息摘要”但摘要质量依赖摘要模型本身。后续可以考虑做基于重要度的评分机制每一条消息在写入时打一个重要度分0到1组装时按分数过滤而不是无脑保留最近N条。这样既能压缩token又能保留真正影响回答质量的历史信息。另一个方向是上下文的版本化。把状态层的每次更新作为一个版本记录下来对话过程中随时可以回滚到之前的上下文版本。这在多轮修改类任务里用户让AI改文案、改代码非常有用——用户可以明确说“回到我让你改之前的那版”。目前我已经在研究实现方案底层用Redis的stream结构存版本链成本可控。最后说点实在的重构上下文管理这件事看着是技术活本质上是对“什么是重要信息”的重新思考。我踩过最大的坑不是技术方案选型错误而是想一开始就设计得无比完美结果迟迟落不了地。context-mode这一版也是先跑起来再一层一层补才慢慢稳定的。如果你准备开始做我的建议是不要一口气把四层结构全部实现先做“消息层滑动窗口”跑通之后再加上状态层然后按需加任务层。每一层上线都配好可观测性日志这样出问题时能快速定位到具体层。核心记住一句话上下文不是历史记录的堆砌而是带生命周期的业务事实集合。想清楚这个context-mode就成功了一半。