
1. 先搞明白context-mode 到底解决什么问题做 AI 应用开发这两年我自己踩得最深的一个坑就是“对话一长模型就开始装失忆”。明明开头明确告诉过它的规则跑到第 20 轮它就能给你忘得干干净净你要是把所有历史消息一股脑全塞进去token 成本先炸响应速度也跟着变慢最后模型还被一堆无关紧要的旧信息干扰得答非所问。我第一次接触 context-mode 这个概念是在重构一个客服问答机器人时。当时的痛点是同一个用户会在一次会话里聊售前咨询、售后故障、发票报销好几个完全不同的事务如果所有上下文混在一个桶里模型很容易把售后问题的背景带到售前回答里结果就是越答越偏。后来我把上下文按“模式”拆开管理每个事务有自己独立的上下文空间问题立刻缓解了大半。简单说context-mode 不是某个特定的函数库而是一套“按场景、按粒度管理上下文”的设计模式。它的核心思路是把“上下文”从一坨无法控制的长文本变成可分层、可切换、可压缩、可检索的结构化资源。它不是替代 prompt 工程而是解决 prompt 工程解决不了的问题上下文失控。这套东西适合谁主要是三类人一是做 LLM 应用、智能客服、Copilot 类产品的开发者二是深度使用大模型 API 做自动化脚本、Agent 流程的爱好者三是想优化 token 成本、把长对话做稳定落地的技术决策者。下面我会从设计思路、核心参数、代码接入和踩坑实录几个方面把我实际跑通并验证过的方案完整拆给你。2. 核心设计思路把上下文拆成四层而不是一味堆长2.1 先忘掉“把所有内容塞进上下文”这个习惯大部分人在做大模型应用时第一反应是“上下文越长模型记得越清楚”。这个直觉在短对话里成立但在真实生产环境里几乎总是失效。原因有三第一transformer 架构对超长上下文的注意力分布会变稀疏超过一定长度后模型对靠前信息的“注意力权重”会显著下降表现出来就是“明明有但用不上”第二token 费用按量计费上下文每翻一倍单次调用成本就翻一倍这对高频业务是不可接受的第三上下文里混入与当前任务无关的信息会产生严重的“信息干扰”比信息缺失更可怕。context-mode 的第一个设计原则就是打破“单一上下文”的假设把上下文拆成四个层次来管理。层级名称存储内容更新频率典型容量L1原始上下文Raw Context完整对话历史、原始文档实时追加大受窗口限制L2结构化上下文Structured Context抽取后的身份、偏好、约束、任务清单增量更新小通常 1-2K tokenL3压缩上下文Compressed Context对早期对话摘要、旧文档归纳定期重写中2-4K tokenL4检索块Retrieved Chunks按需检索出的相关片段按需触发可变1-3K token这个分层逻辑很像我们平时整理办公桌L1 是堆满文件的档案柜L2 是贴在显示器上的便签记着最重要的规则和待办L3 是季度工作总结L4 是你需要某份具体合同细节时再去档案柜里翻出来的那几页纸。每一次请求不需要把所有档案全摆在桌面上只需要带一张便签、一份总结和当前要用的那几页纸。2.2 三种运行模式global-mode、workspace-mode、session-mode在四层上下文的基础上context-mode 定义了三种运行模式用来控制“哪些上下文应该被激活”。这是我实践下来最关键的一层抽象session-mode会话模式上下文只包含当前会话的内容会话结束即释放。适合一次性咨询、独立问答。这是一个隔离性最强的模式缺点是跨会话信息为零。workspace-mode工作区模式上下文包含当前“工作区”——比如一个客服工单、一个代码仓库、一个文档项目——下的所有相关 session 摘要和历史。适合多轮协作、同一主题下的持续沟通。global-mode全局模式上下文包含用户的长期档案、组织级规则、偏好基线。它不随会话变化而消失适合做个性化服务和合规约束。这三种模式是层层嵌套的关系global 是底座workspace 是中层的项目维度session 是最上层的即时对话维度。context-mode 的核心调度逻辑就是根据当前任务类型动态决定把哪几层接入到 prompt 中。注意它是“接入”不是“全部携带”——模型每次看到的总上下文 激活模式的上下文 当前输入的格式化消息。2.3 为什么是“模式切换”而不是prompt拼接有人会问我在系统提示词里手工拼一段“你是一个客服助手用户偏好是XXX”这不也能实现类似效果吗能但它是静态的。真实业务里用户偏好会变工单状态会变会话主题会变靠手工维护字符串拼接根本撑不住。context-mode 把“当前处于什么上下文”这件事变成一个可编程状态。代码里可以随时声明switch_mode(workspace)或update_global(language_preference, 简体中文)系统自动把对应层级的上下文重新组装成最终请求。这个模式还有个重要的副作用可观测性变强了。每个请求到底用了哪些上下文全部可以被记录和回溯对排查问题非常有帮助。提示我见过很多团队把“上下文管理”等同于“把历史消息数组做截断”这是个典型误区。截断是最后手段不是管理手段。真正的管理是让模型在正确的时间看到正确的信息。3. 关键参数与核心实现阈值、窗口、Top-k 怎么定模式定义清楚后接下来进入实操前必须搞定的三个核心参数问题。这些参数我没有一个是从文档里抄来的都是反复跑评估测出来的。3.1 压缩阈值什么时候把 L1 转成 L3所谓压缩就是把早期原始对话交给一个摘要模型生成浓缩版的结构化结论。我最初以为压缩得越勤越好结果代码里每 3 轮就压缩一次很快发现两个问题一是 token 开销反而增加因为摘要本身要消耗一定量的输入输出二是频繁压缩容易丢失细节尤其是数字、时间、人名这类精确信息。我的实际方案是设置一个原始上下文水位线。当一个 session 的原始上下文L1超过总窗口的 60% 时触发一次 L3 压缩。以 32k 窗口为例当原始消息累计超过 19k token 时把最早 60% 的历史做一次摘要压缩压缩后的摘要保持在原内容的 15% 到 20% 体量然后从 L1 中移除被压缩的部分。计算公式我一般这样用窗口大小W 模型上限 × 0.8给输出和工具调用留冗余触发压缩的临界值T_cW × 0.6被压缩的片段比例R_c0.6压缩最老的 60%目标压缩后体量S 原始片段 token 数 ×0.15到0.2注意压缩不是“摘要所有内容”而是“只摘要不再会被直接引用的部分”。最近几轮对话通常包含用户最直接的诉求属于高频引用区应该留在 L1 不压缩。我实际测试中老对话压缩到 15% 的信息保留率最均衡。3.2 滑动窗口与重叠策略session-mode 下我会维护一个滑动窗口但窗口不是简单的“最后 N 条消息”。它是一个带重叠率的滑动窗口窗口长度L 最近 12 轮对话这里“一轮”指用户消息助手回复工具调用结果重叠率O 25%相邻两个窗口之间保留最近 3 轮为什么保留重叠因为上一轮窗口尾部往往是当前位置的决策依据如果硬切掉新窗口开头会缺上下文。我在做多轮表格问答时吃过这个亏表格状态在第 10 轮被切走第 11 轮模型直接不知道用户在问哪个 sheet加了 25% 重叠后这个问题消失了。3.3 检索增强模式下 Top-k 与相似度阈值L4 检索块引入的是 RAG 思路但和传统 RAG 有一个关键区别检索的目标文档除了知识库还包括过去会话的结构化记录。在 context-mode 里我通常用 embedding 模型对 L3 摘要建立索引然后按当前用户输入向量做相似度检索。两个最关键的参数top_k检索返回的片段数量。我调试下来问答场景 3-4 段最优超过 6 段后模型开始出现信息冲突两份文档说法略有出入时模型不知道信谁。如果你做的是连续长文档分析可以放宽到 5-6 段但每段必须带来源标识。相似度阈值sim_min低于阈值的片段会被丢弃。这个值不能拍脑袋定死因为不同 embedding 模型的分数分布差异很大。我的做法是先用小样本把“正确相关”和“无关”两组文档的相似度分数画出来取两者的分界点。以我常用的 bge-large-zh 为例0.38 是一个不错的起点再根据实际命中率微调。低于 0.3 的内容基本可以直接判为不相关放进去只会干扰模型判断。3.4 两个容易被忽略的监控指标参数调好之后光看业务效果还不够。我强烈建议在 context-mode 的运行日志里记录两个指标一个是上下文命中率即检索块中被模型最终引用的比例间接统计方式可以通过在检索块前加不可见标记符然后检查回复中是否出现标记内容另一个是上下文冗余度即每次请求实际携带的 token 中有多大比例与最终结果无关。这两个指标直接反映压缩和检索策略是否健康。拿我自己线上项目的数值举例优化前命中率只有 34%冗余度高达 52%调整阈值和窗口重叠后命中率升到 61%冗余度降到 19%最终 token 消耗下降了近一半。4. 实操手把手接入 context-mode4.1 前置分析三步判断你适不适合用接入之前先做个 5 分钟的评估别急着写代码列出你的业务的对话轮次分布。如果 90% 的用户问题在 5 轮内结束那 context-mode 的价值有限做好基础 prompt 就行。列出上下文来源。如果只有当前对话历史没有跨会话需求用 session-mode 就够了不需要上 global。估算单次请求的 token 峰值。如果峰值已经接近或超过模型窗口上限这就是你引入分层管理的直接信号。我自己的判断标准是只要出现下面三个现象的任何一个就值得接入用户重复提问、模型用旧事务信息回答新事务问题、token 费用每月增长超过业务增长。4.2 最小可运行实现session 级上下文管理下面这个 Python 示例是我实际用的最小实现。不依赖重型框架核心就是用ContextManager保存当前 session 的 L1 和 L3。import json from dataclasses import dataclass, field dataclass class SessionContext: session_id: str raw_messages: list field(default_factorylist) compressed_summary: str profile: dict field(default_factorydict) # 用户偏好、关键约束 def add_message(self, role: str, content: str): self.raw_messages.append({role: role, content: content}) def should_compress(self, window_limit: int) - bool: total_tokens sum(len(m[content])//2 for m in self.raw_messages) return total_tokens int(window_limit * 0.6) class ContextMode: 极简 context-mode 引擎 def __init__(self, window_limit32000, compress_ratio0.15): self.window_limit window_limit self.compress_ratio compress_ratio self.sessions: dict[str, SessionContext] {} def activate(self, session_id: str) - SessionContext: session-mode激活指定会话返回独立上下文 if session_id not in self.sessions: self.sessions[session_id] SessionContext(session_id) ctx self.sessions[session_id] if ctx.should_compress(self.window_limit): self._run_compression(ctx) return ctx def _run_compression(self, ctx: SessionContext): 把最老的 60% raw_messages 压缩为摘要保留最近部分 old_msgs ctx.raw_messages[:int(len(ctx.raw_messages)*0.6)] new_msgs ctx.raw_messages[int(len(ctx.raw_messages)*0.6):] summary_prompt ( 请把以下对话压缩为结构化摘要保留用户核心诉求、已确认信息、 所有数字/时间/名称、未解决问题。输出 Markdown 格式\n json.dumps(old_msgs, ensure_asciiFalse) ) summary self._call_llm(summary_prompt) ctx.compressed_summary summary # 覆盖旧摘要 ctx.raw_messages new_msgs logging.info(f[context-mode] session {ctx.session_id} 压缩完成摘要 {len(summary)} 字) def build_prompt(self, session_id: str, user_input: str) - str: ctx self.activate(session_id) parts [] if ctx.compressed_summary: parts.append(f【历史摘要】\n{ctx.compressed_summary}\n) if ctx.profile: parts.append(f【用户偏好】\n{json.dumps(ctx.profile, ensure_asciiFalse)}\n) history ctx.raw_messages[-12:] # 窗口 12 轮 parts.append(【近期对话】) for m in history: parts.append(f{m[role]}: {m[content]}) parts.append(f【当前输入】\n{user_input}) return \n.join(parts) def _call_llm(self, prompt: str) - str: # 你的模型调用代码返回摘要文本 raise NotImplementedError这个实现虽然简陋但把 context-mode 的核心模式演示得很清楚activate负责按需触发压缩、build_prompt负责按当前模式组装上下文、compressed_summary承载 L3。我实际线上版本在此基础上加了 Redis 存储和分布式锁但逻辑骨架完全一致。4.3 进阶接入global workspace session 三层联动如果业务涉及多会话、多事务就需要把单一的 SessionContext 扩展为三层结构。我用的方案是global 层存用户 ID 维度的档案workspace 层存某一类事务按workspace_key区分的综合摘要session 层存即时对话。dataclass class WorkspaceContext: workspace_id: str sessions: dict[str, SessionContext] field(default_factorydict) workspace_summary: str # 对多个 session 提炼出的项目级摘要 class ContextModeV2: def __init__(self): self.global_profiles: dict[str, dict] {} # user_id - profile self.workspaces: dict[str, WorkspaceContext] {} def switch_mode(self, user_id, workspace_id, session_id): 核心切换到指定模式的组合返回当前激活上下文 profile self.global_profiles.get(user_id, {}) ws self.workspaces.get(workspace_id) session ws.sessions[session_id] if ws else None return {mode: workspacesession, global: profile, workspace_summary: ws.workspace_summary if ws else , session_raw: session.raw_messages if session else []} def sync_workspace_summary(self, workspace_id: str): 定期把 session 摘要汇总为 workspace 级摘要 ws self.workspaces[workspace_id] all_sess_summaries [ s.compressed_summary for s in ws.sessions.values() if s.compressed_summary ] merged self._call_llm( 把以下多个会话摘要合并为一份项目级进展文档保留目标、已完成、阻塞点、时间线、关键决策。\n \n.join(all_sess_summaries) ) ws.workspace_summary merged这个版本启动后每次用户提问我组装出的 prompt 结构是全局偏好1K token 内项目摘要2K token 内最近 12 轮会话。相比之前把所有历史全塞进去的做法同样的场景 token 消耗只有原来的三成。4.4 动态模式切换一个真实场景下的编排示例动态切换是 context-mode 的杀手锏。我做一个多轮数据分析助手时用户会在“闲聊”“数据查询”“图表解读”三个模式间反复横跳。我定义了一个简单的路由函数def route_mode(user_input: str, ctx: ContextModeV2): # 简化版意图路由 if any(k in user_input for k in [你好, 你是谁, 谢谢]): return session # 闲聊只带当前会话 elif any(k in user_input for k in [查询, 统计, 数据, 报表]): return workspacesession # 数据查询需要项目级摘要 elif any(k in user_input for k in [图, 趋势, 解释, 为什么]): # 图表解读需要检索历史结论 ctx.enable_retrieval(top_k4, sim_min0.38) return workspaceretrieval return workspacesession user_id u_10086 wid w_analysis sid s_1024 mode route_mode(user_input) activated ctx.switch_mode(user_id, wid, sid) prompt build_final_prompt(activated, user_input, mode)这样切换的好处是闲聊时模型上下文里没有任何业务残留不会被无关历史干扰到了数据查询模式模型能看到项目级摘要却不会被上上上周的聊天记录拖慢响应。实测切换后闲聊响应快了 40%数据查询准确率还提升了 8 个点——因为模型终于“注意力集中”了。5. 常见问题与排查心得5.1 上下文漂移切换模式后模型“忘事”症状从 workspace 模式切回 session 模式后模型突然忘了用户刚说过的项目要求。排查思路检查模式切换时 global/workspace 层的数据是否正确合并。最常出错的地方是sync_workspace_summary没有在切换前执行导致项目摘要还是旧版本。我的做法在每次 workspace 会话结束前强制同步一次 summary切换读取时永远拿到最新版本。并记录同步时间戳超过 5 分钟未同步的直接返回“摘要生成中请稍后”的降级提示。5.2 压缩失真摘要丢掉关键数字症状压缩后模型把时间“2024-07-15”说成“上周”或者把“金额超过 5000 元”漏掉。排查思路摘要 prompt 必须显式列出不可丢失字段。我最开始的 prompt 只写了“保留关键信息”模型自由发挥空间太大。后来改成“必须保留所有数字、时间、金额、姓名、ID、状态字段”失真率下降了七成。一个补充技巧把压缩摘要和原始片段都保留一份在压缩 prompt 里要求每条关键信息都必须标注来源消息序号。这样即使摘要漏了检索层也能按序号回原片段找。5.3 token 成本不降反升症状接入 context-mode 后费用反而比之前全量携带时更高。这是第一个月最容易踩的坑。原因基本是压缩动作本身调用模型的费用压缩后摘要仍然很长检索块过多过杂。我建议按这个顺序排查检查压缩触发频率是否过于频繁低于 5 轮就压缩一次多半没必要。检查摘要长度压缩后摘要超过原始内容 30% 说明 prompt 没约束好。检查检索块top_k是否设置过大、是否加了不必要的相似度阈值兜底。我自己的量化目标L3 摘要 L4 检索 L2 档案的总 token必须控制在完整原始上下文的 25% 以内。超过这个比例说明分层逻辑失效了。5.4 最容易被忽略的坑检索块的内容打架当检索返回两个相关但结论矛盾的片段时模型会陷入“两个都对”的困境回复质量骤降。我在做多文档综合分析时遇到过两次。解决办法有三层第一检索块中给每段加“来源引用标签”并 prompt 里提醒“如果信息冲突请指出冲突并倾向于最新时间戳的结论”。第二把相似度阈值提高 0.03-0.05减少边缘相关片段进入。第三对检索结果做一次重排序rerank只保留与当前问题最相关的 2-3 段。下面是一个排查速查表现象可能原因检查项解决方案模型重复问已答信息L1 窗口切掉了关键历史检查重叠率是否过低把重叠率从 10% 提到 25%摘要漏数字和时间压缩 prompt 约束不足抽查摘要内容压缩 prompt 明确列出必保留字段回答用旧事务信息干扰模式切换未隔离 session确认激活模式组合闲聊时强制只启用 session-mode检索片段矛盾多源信息冲突检查 top_k 与阈值加 rerank限定 2-3 段费用下降不明显压缩触发太频繁看日志压缩次数调高临界值到 65%最后分享一点个人体会context-mode 真正让我受益的不是某个具体算法而是“上下文也是需要被设计”的这种思维方式。最开始我总想着让模型“记得多一点”后来我开始思考“在某个时刻模型应该看到什么”。如果你也正在被长对话、高 token 费用、任务互相干扰折磨我建议你先别急着买更大的上下文窗口或者换更贵的模型试一试把上下文拆成模式、把历史分成层级。根据我跑下来的数据大部分应用换这套思路后效果提升往往比升级模型参数来得更直接、更可持续。踩过几次坑之后我现在新项目的第一件事就是定义上下文模式而不是写业务逻辑——这个顺序一旦倒过来后面都是返工。