
做过上下文相关的项目同学大概率都遇到过这么一幕对话一长模型突然开始“失忆”前面明明交代过的偏好后面就像从来没发生过或者一个工具调用参数被截断整个流程直接崩掉。我最初以为这是模型能力的问题后来踩了无数次坑才发现症结往往出在context-mode——也就是上下文管理模式的设计上。简单说context-mode不是一个开关而是一套处理对话记忆的动态策略它决定了哪些信息留在窗口里、哪些被压缩、哪些被转移到长期存储。这篇文章就是把我在实际项目里落地context-mode的完整思路、代码雏形和排障经验摊开来讲适合正在做聊天机器人、AI Agent、代码生成助手以及任何被上下文长度困扰的开发者参考。1. 上下文模式到底在解决什么问题1.1 不只是一个窗口而是一套记忆策略大模型的能力边界之一是上下文窗口比如8K、32K、128K tokens。很多人以为窗口够大就万事大吉实际上窗口越大只是把“遗忘”延后了并没有解决“有效使用”的问题。你把半本《红楼梦》塞进去模型照样会漏掉关键细节因为注意力在整个长序列上被摊薄了。context-mode的核心任务是在窗口有限、成本有限、延迟有限的前提下把最有价值的信息放到模型最容易触达的位置。我做第一个对话机器人时用的是最粗暴的办法把聊天记录全部拼接然后一股脑丢给模型。前几十轮还好到了两三百轮首尾信息开始互相打架模型把用户两个小时前说过的需求错误地理解成最新的指令业务方差点让机器人把订单状态改错。后来我意识到问题不在于“全部放进窗口”而在于没有“分级”。context-mode就是把上下文按照重要程度、时效性、检索频率划分成不同层级让模型每次读取时只面对整理好的精简版本。这套策略听起来抽象但在工程上非常具体你需要一个窗口管理器、一个压缩器、一个记忆存储再配一条路由规则。窗口管理器负责动态调整当前会话里保留的消息条数压缩器负责把过期但有用的信息提炼成摘要记忆存储负责保存跨会话的长期事实路由规则则决定什么时候该走检索、什么时候该走完整记录。这些组件合在一起才是真正意义上的context-mode。1.2 最常见的三类翻车场景先聊我实际遇到过的三类典型场景你对照一下就知道自己是否需要引入context-mode。第一类是长对话遗忘。用户在一个客服机器人里连续咨询前5轮说了“我的是企业版账号”到第20轮机器人却在推荐个人版功能。原因很简单那条关键信息早就被挤出了上下文窗口或者被后面的消息淹没在中间位置。模型不是没有能力理解是压根没看到。第二类是工具调用参数被截断。AI Agent在执行多步骤任务时需要反复调用查询接口和写入接口。有一次我发现某个Agent在调用一个更新接口时总是把系统返回的成功标识误判为失败反复重试。排查到最后发现是上下文里塞了太多中间过程的调试日志把真正的工具返回值挤到了窗口末尾模型生成参数时参考了被截断的噪声数据。第三类是跨会话语义断裂。用户第一次会话里详细描述过自己的写作风格第二次会话开头说“继续”如果系统没有把上次会话的关键信息持久化下来模型就只能瞎猜。这也是很多“AI助理”看起来不够智能的重要原因它们把“上下文”默认成了“当前会话上下文”而不是“用户全生命周期上下文”。这三种场景看起来各不相同但本质一致上下文没有被管理只是被堆积。引入context-mode之后处理方式就变成——关键事实进核心记忆长对话分批压缩工具调用保留最新状态跨会话从持久化存储恢复。2. 模式设计与核心机制2.1 滚动窗口模式最朴素但最可靠滚动窗口是context-mode的入门方案也是很多生产系统的兜底策略。它的逻辑很简单始终保留最近N条消息超出的旧消息直接丢弃或者只留下摘要。实现起来非常直白但我见过不少团队连这一步都没做好。比如一个常见的错误是按照消息条数保留却不看token长度。用户消息很短工具返回却可能几千字结果窗口里存了同样数量的消息实际长度翻了几倍费用和延迟都失控。我后来统一改成“按token预算分配”滚动窗口优先保留最近的消息再往前只保留压缩摘要。具体可以用一个简单的优先级策略最近20轮完整保留20轮之前每10轮合并为一条摘要。下面是我在一个项目里用过的滚动窗口维护逻辑缩写成伪代码def rolling_window(messages, max_tokens): budget max_tokens result [] # 从最新消息往前遍历保留完整消息直到预算耗尽 for msg in reversed(messages): cost estimate_tokens(msg) if cost budget: result.insert(0, msg) budget - cost else: break # 剩下的预算交给摘要 recent_msgs result older_msgs messages[:len(messages) - len(recent_msgs)] summary summarize(older_msgs) if older_msgs else None return summary, recent_msgs滚动窗口的优点是稳定、容易理解、不依赖复杂的检索系统缺点是“记忆”随着窗口滚动很快消失。如果用户在第3轮提过一个关键要求到第100轮时这个要求可能已经被挤掉模型行为自然漂移。所以它适合对话深度不深、单轮交互为主的场景比如表单填写、短客服会话。2.2 摘要压缩模式用成本换记忆摘要压缩模式解决的是滚动窗口丢记忆的问题。核心思路是当旧消息快要被挤出窗口时用一次额外的LLM调用把一段长对话压缩成带有结构化字段的摘要。这个摘要代替原始消息占据历史位置从而保留长期信息。我第一次实现时走了一个弯路等到窗口快满的时候才去做压缩。结果在峰值流量下压缩请求和用户请求同时争抢模型配额延迟飙升。后来我改成“提前压缩”设置一个压缩阈值比如当历史消息总长超过窗口的60%时就把前面的消息段异步送去压缩而不是卡在用户请求的同步路径上。压缩不是简单说“把这段话总结一下”而是要面向后续提问。我会要求摘要包含用户关键偏好、已经确认的事实、未完成事项、明确拒绝过的方案。我常用的压缩prompt结构是这样的你是上下文压缩器。请把下面对话压缩为结构化摘要保留 1. 用户身份与偏好 2. 关键事实与状态变更 3. 已执行和未执行的任务 4. 模型此前的承诺 不要保留寒暄、重复提问、调试噪声。压缩摘要其实是在用成本换记忆每一次摘要都要花token摘要质量好坏还会直接影响后续上下文的准确性。所以我会在摘要末尾加一个“置信度”字段让下游环节知道这段信息是压缩出来的还是原文保留的便于排查矛盾。2.3 分层记忆模式核心记忆、工作记忆、外部记忆如果你做的不是单轮客服而是真正的AI助手或Agent建议直接上分层记忆。这是我认为目前最接近人类记忆机制的方案它把上下文分成三层核心记忆、工作记忆、外部记忆。核心记忆是跨会话稳定不变的事实比如用户的企业账号类型、工作领域、常用工具偏好。它的特点是一个字都不能丢必须常驻系统提示词或独立缓存。工作记忆是当前会话中的短期状态比如正在处理的任务步骤、最近一次工具返回结果这部分会随着会话推进持续更新。外部记忆是历史会话的完整或摘要记录平时放在数据库或向量库里等需要时再检索回来。一个很形象的类比是冰箱核心记忆是贴在冰箱门上的便利贴每天都能看到工作记忆是灶台上正在炒的菜手边材料要随手拿得到外部记忆是冰箱冷冻层里的存货要吃的时候才翻出来解冻。很多失败的Agent项目问题不是冰箱太小而是全部食材都堆在灶台上火一开就糊了。我在工程上的落地方式如下核心记忆存Redis有效期设为30天以上每次会话开始时加载工作记忆存内存随着工具调用结果不断覆盖外部记忆用向量数据库按会话分块存储用户在新会话里说“上次那个方案”时先做一次相似度检索把相关历史片段拉回来注入上下文。这套结构改起来也不难关键是每轮结束要做一次“记忆更新”把新确认的事实回写进核心记忆否则下次会话又得从头积累。2.4 内容感知路由让系统自己决定用什么模式滚动窗口、摘要压缩、分层记忆不是单选题更多时候要组合使用。我搭的context-mode里有一个router模块专门负责根据当前请求的特征选择策略。它要回答的问题很简单这一次请求该用完整历史、摘要、还是向量检索我的路由判据有三个维度。第一是会话时长新会话少于10轮直接全部保留超过20轮进入摘要模式超过50轮先检索外部记忆再拼摘要。第二是任务复杂度如果Agent需要连续调用超过三个工具我会保留工具的最近一次返回结果和用户的最终目标而不是把所有工具日志全堆进去。第三是引用密度如果消息里频繁出现“之前说过”“上次提到的”这类代词说明需要更强的记忆恢复路由会把权重偏向外部检索。实际开发中不要一开始就设计一个需要十路分支的超级路由器那是过度设计。先写一个简单的规则引擎能识别三种状态就够用了EXHAUSTIVE全量、SUMMARY摘要、RETRIEVAL检索。跑一段时间收集真实流量日志再看看哪些规则不准确慢慢细化。路由本身也是推理频繁调用大模型来决策会拖慢响应所以我尽量用轻量规则或者小模型判断只有真正的关键请求才走大模型路由。3. 实操落地构建一个可供生产的context-mode3.1 技术选型与模块划分把context-mode做成生产级功能我一般划分五个模块context_router、window_manager、compressor、memory_store、injector。它们各管一摊接口之间有清晰的输入输出方便单独压测和替换。context_router负责决定当前请求的上下文策略window_manager负责维护当前会话的消息顺序和token预算compressor调用大模型把旧消息压成摘要memory_store是持久化层存核心记忆和外部记忆injector负责把最终整理好的上下文拼装成模型API需要的格式。技术选型上如果团队规模小可以用Redis加内存队列起步。Redis存放核心记忆和最近会话内存里放工作记忆。等到跨会话检索需求变强再引入向量数据库。我见过有人一开始就上完整向量库和Agent框架结果问题没解决倒是引入了一堆运维负担。记住一句话context-mode的本质是数据管道的设计不是某个框架的插件。模块之间通信我建议用简单的JSON结构不要搞复杂的事件总线。下面是一个内部统一的上下文数据格式{ session_id: abc123, mode: summary, core_memories: [企业版账号, 偏好邮件沟通], working_memory: {task: 查询订单, last_tool_result: success}, history: { recent: [ {role: user, content: ..., ts: 123} ], summary: 用户在前20轮确认了发货地址并拒绝了加急选项。 } }3.2 与LLM API的交互协议与模型API的交互我习惯把最终上下文组装成统一的message list。这里有几个重要的设计细节稍不注意就会让context-mode白干。第一系统提示词要拆成“静态系统指令”和“动态记忆区”。静态指令是模型遵循的行为准则动态记忆区是核心记忆和摘要。两条混在一起会导致记忆频繁变化时模型对指令的遵循也出现波动。第二工具调用要用独立的消息角色标记不要塞进普通文本。很多模型API给工具返回单独的消息槽位放着不用硬把工具返回拼进assistant消息会让模型分不清哪段是工具输出、哪段是模型回答。context-mode里尤其要谨慎因为压缩摘要时很容易把工具结果和对话文本混在一起造成语义污染。第三对过期的工具返回要做标记。我会在工具返回前加一句前置说明例如“以下结果来自5分钟前的查询可能已过期”这样模型在生成新请求时就不会盲目复用陈旧数据。组装消息的伪代码如下def build_messages(state): messages [] messages.append({role: system, content: static_system_prompt}) if state.core_memories: messages.append({role: system, content: 核心记忆 , .join(state.core_memories)}) if state.summary: messages.append({role: system, content: 历史摘要 state.summary}) for m in state.recent_history: messages.append({role: m.role, content: m.content, name: m.get(name)}) if state.working_memory.get(tool_result): messages.append({role: tool, content: state.working_memory[tool_result]}) return messages3.3 上下文预算的计算方式token预算如果不算清楚context-mode一定会出幺蛾子。我一般把一次完整请求的token预算切成四块系统指令、核心记忆与摘要、近期对话、预留输出与工具调用。假设模型的上下文上限是32K tokens我的分配方案是这样的系统指令控制在2K以内核心记忆和摘要合计不超过8K近期对话不超过12K剩余大概10K留给模型输出、工具定义和临时预留。需要注意不同的模型对系统提示词和工具定义计费方式不同有些模型的工具定义要按每个工具几百token算调用工具多的时候仅工具定义就能吃掉不少预算。有一个值得反复测试的细节模型输出token不是固定上限而是动态变量。如果你把max_tokens设成8192那模型实际生成可能远低于这个值但API预留的额度仍然是8192这会影响输入token的实际可用空间。所以我在组装输入上下文时不会把窗口占满而是留出20%的缓冲。这是我用便宜模型踩过坑之后学到的窗口算得满打满算一旦输出稍微长一点API直接报context_length_exceeded重试成本更高。预算分配的参考表如下你可以根据自己的场景微调区块占比说明系统指令6%固定不随意扩容核心记忆 摘要25%按重要程度动态调整近期对话40%保留最近消息按轮数控制工具定义 外部检索结果9%按需求注入输出预留 缓冲20%避免超限、避免输出截断3.4 核心流程实现步骤一个典型的context-mode处理流程我会分成六步。第一步接收用户请求解析session_id第二步从memory_store加载核心记忆和外部记忆索引第三步调用context_router判断当前模式第四步window_manager根据预算和模式组装消息第五步调用LLM并拿到结果第六步更新工作记忆把必要的事实回写核心记忆。这套流程里最容易写崩的是第四步和第六步。第四步容易出现“装多了”或“装少了”两个极端装多了浪费token装少了模型信息不足。我的建议是先做一个离线回放工具把历史真实请求作为输入记录每次系统拼装的上下文对比模型输出质量以此调预算比例。第六步则容易变成“每轮都写核心记忆”导致核心记忆被低频事实灌满。我加了一个规则只有当用户明确表述一个稳定偏好或者系统确认完成一项重要状态变更时才允许写入核心记忆。下面是一个简化的流程实现def handle_request(session_id, user_message): state load_state(session_id) mode context_router.route(state, user_message) if mode retrieval: related memory_store.search(user_message) state.external_context related summary, recent window_manager.assemble(state, mode) messages build_messages(state) response llm.chat(messages) memory_store.update_working_memory(session_id, response, user_message) return response4. 常见问题排查与评测4.1 上下文污染的症状与修复context-mode上线后最常见的故障不是“模型能力不行”而是上下文被污染。污染有几个典型症状模型突然用旧会话的人称说话工具调用老带上上一轮的参数回答内容里混进摘要注释文本。有一次我的模型在回答里出现了“[压缩摘要开始]”这种字样一看就是注入格式写得不严谨被模型当成正文了。我排查污染问题时第一件事永远是打印最终发给模型API的完整消息列表。这个方法虽然笨但能快速定位问题出在哪个环节是core memory写重了、摘要里有角色标签、还是recent history塞进了重复的tool call。看完再修比盲目改prompt高效得多。修复方案我总结成三条。第一所有动态注入内容要加明确边界标识并且这些标识不要和对话内容混在同一段文本里。第二tool消息和普通消息分开存储在组装阶段也不要合并成一个字符串。第三给每条记忆加时间戳和来源字段如果发现记忆冲突按“核心记忆优先、近期对话其次、摘要最后”的优先级处理。4.2 摘要丢失关键信息怎么处理摘要压缩模式最大的痛点是摘要丢信息。有些信息对用户重要但压缩器觉得不重要比如用户随口提过的“下周不在办公室”在后续行程安排里却是关键约束。这个问题靠“更好的prompt”只能缓解不能根治。我现在的做法是加一条“关键事实抽取”的旁路。每次会话结束时我会让压缩器先别急着写摘要而是先用一个独立步骤抽取“事实三元组”把用户偏好、状态变化、承诺事项归类。摘要只负责保留叙事线索事实三元组则被单独存进memory_store。后续组装上下文时摘要用于理解上下文事实三元组用于精准决策。如果用户回头询问一个摘要里确实丢失的细节比如“我两周前让你记过报销账号”该怎么办我加了一个反馈回路把用户的提问转成检索请求去向量库里翻历史消息原文而不是去摘要里找。只要原始消息还在外部存储里没删就有机会恢复。所以我建议不要把旧消息彻底丢弃而是压缩后放进冷存储一旦用户追问可以随时按session_id和原文检索。4.3 性能指标与成本观测清单做context-mode一定要有可观测性否则优化无从下手。我会重点关注四个指标有效token率、压缩成本占比、检索命中率、用户可感知延迟。有效token率是指“模型生成最终答案时真正用到的输入token”占全部输入token的比例。这个指标没法直接测但我用代理来判断把输入的摘要和近期对话清空只留核心记忆看模型回答质量下降多少。如果下降不多说明之前塞了大量无效信息。压缩成本占比需要盯紧。摘要压缩本身也是一次LLM调用如果不限制频率压缩成本可能比正常推理还高。我设定了一条规则只有历史消息的token数超过阈值或者session进入新的会话时才执行压缩。压缩调度做成异步队列绝不阻塞用户请求。检索命中率的观察方法相对简单在外部记忆检索的日志里记录每一次拉取结果是否真的被模型引用。如果检索结果频繁注入但模型从来不提或者回答质量没有变化说明查询向量和对话场景不匹配需要调整嵌入规则或者路由触达条件。4.4 评测集与迭代方法最后说说评测。context-mode这类系统不能只在线上看反馈还必须建一个离线评测集。我建评测集时固定三种难度简单场景是10轮内的短对话要求模型准确回答事实中等场景是30轮长对话穿插多个话题反转困难场景是跨会话任务用户在第一段会话里给过约束在第二段会话中要求执行。评测方法我建议用“关键行为校验”而不是“相似度打分”。例如设计一个用例用户A说要企业版用户B要个人版系统能否在共享会话背景下区分权限。这种用例只看模型是否给出正确操作不看生成文本是否漂亮。跑分时我会对比不同context-mode配置的结果纯滚动窗口、滚动摘要、分层记忆三种方案各跑一遍记录准确率和平均token消耗。我自己的经验是不要同时改多个变量。很多团队上线一周后发现效果变差了但说不清是摘要prompt改坏了还是路由阈值变了。因为这个系统是链路式的变量一多互相干扰根本没法定位。我的习惯是每次只调整一个参数跑两天观察期再决定下一次改动。5. 进阶思路与个人经验5.1 从context-mode走向长期记忆context-mode做好之后最自然的延伸方向是把短期会话记忆演变成长期用户记忆。之前我记忆库里存的是“这个会话发生过什么”后来我把它升级成“这个用户还需要什么”。做法是定期离线扫描已完成会话抽取出稳定的用户偏好和业务约束写入全局用户画像。离线扫描通常放在低峰期执行比如凌晨两点。扫描进程会读取一天内全部完成的会话调用压缩器生成一份“长期记忆草稿”再由规则引擎筛选出符合更新条件的内容。筛选条件包括该事实在会话中出现两次以上用户明确肯定过或者与核心业务字段直接相关。以此避免把偶然话题当成长期偏好。5.2 我在生产环境学到的几个实践心得第一条心得是先记录再优化。context-mode上线前最好先埋点一两个星期收集真实对话长度、token消耗、检索延迟等数据。没有这些基线数据任何优化都是在猜。第二条心得是给所有记忆打版本。记忆不是单纯的键值对它有生命周期会更新、会过期。我在核心记忆里加了一个version字段每次更新都会自增。这样当线上出现行为漂移时我能快速判断到底是哪一次记忆更新引起的。第三条心得是小步快跑别追求完美路由。现在这个项目里的context-mode已经迭代了三个月最初的版本只有滑动窗口和关键词替换后来才一点一点加上摘要、检索和分层记忆。每次只加一个机制配合评测集跑分确认收益大于成本后再往下走。这种做法听起来不酷但胜在稳尤其适合生产环境里不能随便“重构一把梭”的团队。如果你正在被上下文溢出、Agent状态错乱、长对话失忆折磨可以先别急着换更大的模型老老实实把context-mode的四个组件搭出来窗口管理、摘要压缩、分层存储、内容路由。按这套思路落地哪怕初期只先做滚动窗口和摘要你也会明显感觉到模型“记性”变好了。