去年做AI应用集成的时候我被“上下文怎么传”这个问题折磨了整整两周。当时接了一个内部客服机器人项目需求不复杂多轮对话、能查工单、能翻历史订单。结果上线没几天用户连续问了七八个问题之后模型就“失忆”了有时甚至把上一个用户的问题安到下一个用户头上。后来我把所有涉及“模型能看到哪些信息”的逻辑单独抽出来管理才发现这其实就是标题里那个词——context-mode上下文模式。它不是某个官方库而是一套关于“怎么组织、保留、切换上下文”的设计思路。这篇就聊聊我在这套模式下总结的落地经验包括三种主流的上下文模式选型、一个可以直接抄走的通用管理器实现以及我自己踩过的几个坑。1. 先搞明白context-mode 解决的是哪一类“现场的混乱”1.1 用合同类比理解上下文模式的两层含义context-mode 拆开看是“上下文”加“模式”。我在项目里把它分成两层理解。第一层是窗口层。模型一次能读的 Token 有限像一张桌面所有历史消息、检索结果、工具返回的数据都堆在上面堆不下就得收拾。窗口管理管的是“桌面上放哪些东西”。第二层是状态层。同样是面对一堆信息不同任务需要不同的“记忆形态”。你是要模型像普通聊天那样记住每句话的原话还是只记住用户的核心诉求还是按角色把信息分门别类这个选择决定了上下文里数据的组织方式也就是 mode。我见过很多人纠结“上下文窗口不够大”但其实大部分问题不是窗口不够而是没有选择正确的 mode。窗口就像仓库容量mode 就像仓库怎么分区、怎么摆放。仓库再大东西乱堆照样找不到。1.2 没做上下文管理会踩到的四个典型问题我把自己和身边朋友踩过的坑归成四类你对照着看看自己项目里有没有类似现象。第一类Token 膨胀。系统提示词两三百字历史对话全量塞进去每轮都重复发送。用户聊到第十轮一次请求的 Token 数轻松突破一万费用翻着倍往上涨响应延迟也从 800ms 涨到快三秒。第二类检索噪声。做 RAG 的时候召回了一堆文档片段但没做相关性过滤模型被无关信息带偏。我遇到过最离谱的一次用户问“退款多久到账”检索模块把“退款政策修订历史”的文档片段也召回了模型一本正经地分析这个政策是什么时候改的。第三类角色污染。多个角色复用同一份上下文。客服机器人既要答常见问题又要帮用户查订单如果不分模式模型很容易把订单查询的中间结果当成最终答案直接告诉用户。第四类多会话串扰。这在接入了多用户入口时尤其严重比如一个 Telegram Bot 同时服务多个用户上下文共享用户 A 问的事用户 B 接着问模型就答串了。这四类问题表面上是“上下文没管理好”本质就是没有明确“当前对话处于哪种 context-mode”。对了模式一切都有章法不对模式临时加规则永远治标不治本。2. 三种可行的 context-mode 设计模式2.1 精简窗口模式适合短任务、快节奏对话这种模式适用场景很典型客服闲聊、售前咨询、单轮工具调用。特点是人机交互节奏快历史信息时效性强不需要对很早之前的消息做深度理解。做法是维护一个滚动窗口只保留最近 N 轮对话更早的内容统一压缩成一段摘要。摘要本身也参与窗口计算当摘要加最近消息超过阈值时再触发一次新的摘要生成。核心参数就两个窗口长度和摘要触发阈值。窗口长度我建议从 6 轮起步试别一上来就 20 轮。你想想平均一轮对答拆分成 2 到 5 条消息6 轮就是 12 到 30 条对大多数短任务来说信息量完全够用。摘要触发阈值一般设为总 Token 预算的 60%留出 40% 给模型输出。比如你期望的最大输入 Token 是 4000那摘要就在累计输入达到 2400 时触发。2.2 检索增强模式RAG 场景的标配RAG 场景下context-mode 的重点从“留多少历史”变成了“检索哪些内容、按什么顺序放进上下文”。我踩过最大的坑是分块大小失控。起初我用 500 token 一块召回 5 块每块 500 token总共 2500 token。但实际效果差得出奇因为 500 token 的块往往把两个不相关的小节捏在一起检索命中一个关键词连带把无关段落也带进来了。后来我把分块调小到 200 token 左右召回数量从 5 块提高到 8 块反而更精准。原因不难理解小块信息密度高噪声少多召回几个小块引入的无效信息总量反而更可控。这种模式下还要加一道**重排Rerank**环节。第一次向量检索是粗筛重排才是细选。我在项目里先用 bge-large 做向量召回再用 reranker 模型重排只取 Top 3 进上下文。这一步能把准确率提升 10 到 15 个点代价是多花几十毫秒很值。2.3 结构化上下文模式多 Agent 与工具调用的救星如果你在做多 Agent 协作或者模型需要频繁调用工具那前面两种模式都不够用。因为它们都是把上下文当成一条流而多 Agent 场景需要的是分区管理。我用的做法是把上下文按功能区隔开每个区有明确的读写规则事实区用户的身份信息、订单号、业务状态只写不改任务区当前目标、已完成步骤、待执行动作每次更新工具区最近几次工具调用及返回结果保留最近三次输出区输出格式要求、语气限制、安全约束这样分区之后我给模型下一条全局指令需要回答时只看事实区和输出区执行任务时必须先读任务区工具返回后先更新任务区再决定下一步。这就是结构化的 context-mode上下文不再是直线而是像文件夹一样分门别类模型每次只需打开需要的抽屉。这种模式的代价是系统提示词更复杂我一般会写到 600 到 800 字。但换来的是长期运行的稳定性尤其在 Agent 需要循环调用工具的环节结构化上下文能明显减少“模型忘了自己已经做过哪一步”的低级错误。3. 动手实现一个通用上下文管理器3.1 设计思路与核心数据结构我不建议每次接入新项目都把上下文逻辑重写一遍。更靠谱的办法是抽一个通用的上下文管理器内置几种模式用配置文件决定当前使用哪种。管理器核心数据结构只有三样会话状态SessionState记录用户 ID、对话 ID、当前模式、模式切换历史消息队列MessageQueue按时间顺序存放所有消息但只保留窗口内的内容上下文包ContextBundle最终拼装好的上下文内容包含系统提示词、事实区、任务区、窗口消息模式切换逻辑用配置驱动。我准备好了一个 JSON 配置文件里面定义每个模式的行为窗口大小、是否开启摘要、检索参数、是否启用分区。建议你先把代码框架搭出来利用周末半天时间就能跑通后面接新项目时改配置比改核心逻辑快得多。3.2 上下文模式路由先分类再分配模式路由听起来高级本质就是一套 if-else 加映射表。我的做法是先定义意图分类函数根据用户输入判断当前应该进入哪种模式。判断规则不复杂纯规则也能用包含“帮我查”“订单”“退款”“物流”等词进入检索增强模式模型内部 RAG 服务命中率低、用户连续对话超过五轮进入精简窗口模式对话中出现了“先做 A 再做 B”“完成后调用 C 工具”等意图进入结构化模式如果判断不出来默认走精简窗口模式保证基础体验。路由决策发生在每次用户消息进来时。如果模式与当前状态不同就触发模式切换回调先固化当前模式的上下文摘要再初始化新模式需要的结构。这一步特别关键很多项目上下文乱就是切换时没做好“交接”。3.3 完整代码一个可直接复用的上下文管理器我用 Python 给你写了一个精简版实现核心逻辑可以跑通你可以根据自己的模型接口和数据库做替换。import json import time from collections import deque from dataclasses import dataclass, field from typing import Optional, Dict, List from enum import Enum class ContextMode(Enum): SLIDING_WINDOW sliding_window RAG_ENHANCED rag_enhanced STRUCTURED structured dataclass class SessionState: user_id: str session_id: str mode: ContextMode ContextMode.SLIDING_WINDOW mode_history: list field(default_factorylist) def switch_mode(self, new_mode: ContextMode): self.mode_history.append((self.mode.value, time.time())) self.mode new_mode dataclass class ContextBundle: system_prompt: str fact_zone: dict field(default_factorydict) task_zone: dict field(default_factorydict) tool_zone: list field(default_factorylist) output_zone: dict field(default_factorydict) messages: deque field(default_factorylambda: deque(maxlen12)) class ContextModeConfig: 每种模式的参数配置 MODE_CONFIGS { ContextMode.SLIDING_WINDOW: { window_size: 6, summary_threshold_ratio: 0.6, enable_summary: True }, ContextMode.RAG_ENHANCED: { chunk_size: 200, top_k: 8, rerank_top_k: 3, similarity_threshold: 0.35 }, ContextMode.STRUCTURED: { group_zone_enabled: True, tool_zone_keep: 3, task_zone_required: True } } class ContextManager: 统一上下文管理器 - 负责维护 SessionState - 根据用户输入路由到合适的 ContextMode - 显式支持模式切换切换前固化摘要切换后初始化新结构 def __init__(self, model_api, summary_fn, retrieve_fn): self.model_api model_api self.summary_fn summary_fn # 摘要生成函数 self.retrieve_fn retrieve_fn # 向量检索函数 self.sessions: Dict[str, SessionState] {} # ---------- 会话管理 ---------- def get_session(self, user_id: str, session_id: str) - SessionState: key f{user_id}:{session_id} if key not in self.sessions: self.sessions[key] SessionState(user_iduser_id, session_idsession_id) return self.sessions[key] # ---------- 模式路由 ---------- def route_mode(self, user_input: str) - ContextMode: 简化版路由按关键词/意图分配模式实际可用意图分类模型替代 rag_keywords [帮我查, 订单, 退款, 物流, 库存, 价格] structured_keywords [先做, 再, 然后调用, 完成后, 工具] if any(kw in user_input for kw in rag_keywords): return ContextMode.RAG_ENHANCED if any(kw in user_input for kw in structured_keywords): return ContextMode.STRUCTURED return ContextMode.SLIDING_WINDOW # ---------- 模式切换 ---------- def switch_mode(self, session: SessionState, new_mode: ContextMode, bundle: ContextBundle): if session.mode new_mode: return # 切换前把当前窗口内消息固化成摘要 if len(bundle.messages) 0: summary self.summary_fn(list(bundle.messages)) bundle.fact_zone[history_summary] summary # 清空工具区工具结果属于上一模式状态不能跨模式复用 bundle.tool_zone [] # 切换后更新状态 old_mode session.mode session.switch_mode(new_mode) print(f[ContextManager] MODE_SWITCH: {old_mode.value} - {new_mode.value}) # ---------- 上下文组装 ---------- def build_context(self, session: SessionState, user_input: str, retrieval_result: Optional[str] None) - ContextBundle: bundle ContextBundle() cfg ContextModeConfig.MODE_CONFIGS[session.mode] if session.mode ContextMode.SLIDING_WINDOW: bundle.messages.append({role: user, content: user_input}) if cfg[enable_summary] and len(bundle.messages) cfg[window_size]: # 摘要压缩旧消息 old_messages list(bundle.messages)[:-cfg[window_size]] summary self.summary_fn(old_messages) bundle.fact_zone[history_summary] summary for m in old_messages: bundle.messages.remove(m) # 实际项目中 deque 保持最大长度即可 elif session.mode ContextMode.RAG_ENHANCED: # 组装检索结果到 fact_zone if retrieval_result: bundle.fact_zone[retrieved] self._rerank_and_cut(retrieval_result, cfg) bundle.messages.append({role: user, content: user_input}) elif session.mode ContextMode.STRUCTURED: self._apply_structured_zones(bundle, user_input, cfg) return bundle def _rerank_and_cut(self, retrieval_result: str, cfg: dict) - str: # 简化版实际可用 reranker 模型打分 chunks [c for c in retrieval_result.split(\n) if c.strip()] return \n.join(chunks[: cfg[rerank_top_k]]) staticmethod def _apply_structured_zones(bundle: ContextBundle, user_input: str, cfg: dict): bundle.task_zone[current_input] user_input bundle.output_zone[format] json bundle.output_zone[tone] professional # ---------- 对外主入口 ---------- def handle_message(self, user_id: str, session_id: str, text: str): session self.get_session(user_id, session_id) # 1. 路由模式 new_mode self.route_mode(text) # 2. 如果模式变化触发切换 old_bundle self.get_or_create_bundle(session) if session.mode ! new_mode: self.switch_mode(session, new_mode, old_bundle) # 3. 构建新的上下文按模式组装 retrieval_result if new_mode ContextMode.RAG_ENHANCED: retrieval_result self.retrieve_fn(text, top_kContextModeConfig.MODE_CONFIGS[new_mode][top_k]) bundle self.build_context(session, text, retrieval_result) # 4. 调用模型 final_messages self._compose_model_messages(session, bundle) response self.model_api(final_messages) return response def get_or_create_bundle(self, session: SessionState) - ContextBundle: # 实际项目里 bundle 会持久化到 Redis / DB return ContextBundle() def _compose_model_messages(self, session: SessionState, bundle: ContextBundle) - List[dict]: system You are a helpful assistant. Use the provided context to answer.\n if session.mode ContextMode.STRUCTURED: system 结构化管理任务区内容用于执行计划事实区内容用于事实回答工具区仅保留最近三次调用。\n system f事实区: {json.dumps(bundle.fact_zone, ensure_asciiFalse)}\n system f任务区: {json.dumps(bundle.task_zone, ensure_asciiFalse)}\n system f工具区: {json.dumps(bundle.tool_zone, ensure_asciiFalse)}\n system f输出约束: {json.dumps(bundle.output_zone, ensure_asciiFalse)}\n messages [{role: system, content: system}] messages.extend(list(bundle.messages)) return messages # 非结构化模式把事实区内容拼进 system prompt system f事实区: {json.dumps(bundle.fact_zone, ensure_asciiFalse)}\n messages [{role: system, content: system}] messages.extend(list(bundle.messages)) return messages # ---------- 使用示例 ---------- if __name__ __main__: def fake_summary(messages: list) - str: text_parts [m.get(content, )[:20] for m in messages] return f[摘要] | .join(text_parts) def fake_retrieve(query: str, top_k: int) - str: return \n.join([f检索片段{i}: {query[:6]}相关文档内容 for i in range(top_k)]) def fake_model_api(messages: list) - str: # 模拟模型调用 last messages[-1][content] return f模拟回复({last[:20]}...) cm ContextManager(model_apifake_model_api, summary_fnfake_summary, retrieve_fnfake_retrieve) # 同一用户连续发消息验证自动路由和模式切换 print(cm.handle_message(U001, S001, 你好我想咨询一下退款政策)) # 触发 RAG_ENHANCED 模式 print(cm.handle_message(U001, S001, 帮我查一下订单号 20241001 的物流状态)) # 触发 STRUCTURED 模式 print(cm.handle_message(U001, S001, 先查库存再调用供应商接口更新订单状态))这份代码的要点有三个。第一ContextManager只负责上下文维护不负责业务逻辑。所有和模型接口相关的代码都通过model_api回调接入方便替换不同的模型服务商。第二模式切换的“交接”是显式的。switch_mode在切换前先固化摘要到fact_zone再清空工具区。这样即使用户从闲聊跳到查订单旧对话的核心信息也不会丢更不会出现“上一个任务的中间结果污染下一个任务”的情况。第三路由函数目前是纯规则实现的。如果你要做复杂路由可以换成意图分类模型或小模型分类器但接口保持一致——输入用户文本返回一个ContextMode。3.4 关键参数怎么定Token 预算分配上下文管理器不管采用哪种模式最终都要面对一个现实问题总 Token 预算怎么分。我在项目里维护了一张分配比例表长期实测下来比较稳定用途占比说明系统提示词与模式指令15%包括分区域提示词、路由规则事实区检索结果、摘要30%动态内容按检索数量浮动任务区与工具区20%结构化模式下单独分配窗口消息20%最近 N 轮对话模型输出预留15%必须保证生成空间假设模型输入上限是 8000 Token按这个比例模式指令占 1200、事实区 2400、任务工具区 1600、窗口 1600、输出预留 1200。这个分配比例不是死的。如果你的场景是长文档问答事实区可以上调到 40%窗口消息砍到 10%。关键是在做上下文组装时先预留输出空间而不是等所有内容都塞满了才想起模型还要输出。这个习惯帮我避免了很多次“上下文过长被截断”的线上事故。4. 实操里最容易翻车的五个细节4.1 变量污染比想象中更容易发生结构化模式下事实区、任务区、工具区在逻辑上是隔离的但如果组装 prompt 时没有用明确的标签区分模型还是会混。我的经验是每个区域前加一行显式标记像这样[事实区] 用户的订单ID是 A-20241001当前状态是已付款未发货。 [任务区] Step 1: 查询库存Step 2: 调用供应商接口Step 3: 更新订单状态。 [工具区] 工具查询结果: {stock: 15}这个分隔技巧来自一个很惨的教训。早期我把事实区和任务区混在一个段落里模型经常把“任务计划”当成“既成事实”回答给用户。加分隔符之后这类问题基本绝迹。4.2 检索结果不重排等于白检索RAG 模式下我第一次做检索就发现向量召回的前几个结果未必是用户需要的。直接用 top_k 结果拼上下文模型容易被无关片段带跑。后来我在上下文中增加了一个“相关性阈值”逻辑检索结果先计算相似度分数低于 0.35 的片段直接丢弃然后再重排取 Top 3。这个阈值的调法是从 0.3 开始逐步上调观察回答质量曲线找到一个准确率不掉、召回率可接受的平衡点。4.3 模式切换后丢失了必要的长期信息模式切换做“交接”很关键但交接过度也不行。我在一个项目里发现从精简窗口模式切到检索增强模式时固化的摘要太长占了事实区一大半空间导致检索结果只能塞进很小一部分。后来我调整策略摘要里只保留三件事——用户的原始诉求、已经确认的关键信息如订单号、收货地址、尚未解决的问题。其他细节全部丢掉。这样交接成本从原来的 500 Token 降到了 200 Token 以内。4.4 日志里看不到模式变化问题定位靠猜上下文管理器最容易被忽视的是可观测性。没有日志一旦线上回答异常你根本不知道模型是在什么 context-mode 下回答的。我在 ContextManager 里埋了两类日志路由日志输入文本、命中的模式和切换日志旧模式、新模式、切换触发词。每次线上出问题第一件事就是查这两个日志。如果你也打算实现一个上下文管理器一定记得从第一天就把日志埋好这是后期排查线上问题最重要的工具。4.5 跨用户上下文串扰这个问题在多用户入口的场景下特别隐蔽。如果 Session 的 key 只用了用户 ID而用户短时间内开了多个窗口不同窗口的上下文就会互相串。我建议 Session 的标识用user_id session_id双字段每个窗口单独维护一个 SessionState。上面的代码示例里已经是这样处理的别省这一步省了后续一定会付出代价。5. 我在实际使用中的体会上面讲的这些都是在真实项目中被验证过的做法。最后分享一点我个人的使用感受。context-mode 这个概念本身并不复杂复杂的是你要在项目里持续贯彻“上下文是可管理资源”这个意识。很多 AI 应用开发到后期最大的成本反而不是模型推理而是“模型总是用错上下文”。我花在这套管理器上的时间和它帮我省下的排查时间相比完全值得。如果你准备在自己的项目里落地这套方案我建议从最小闭环开始先做单一模式的窗口管理跑通后再加路由路由稳定后再引入结构化分区。不要一开始就上全量功能否则你会被模式切换的问题淹没。还有一个小技巧在开发阶段就把摘要函数和检索函数做成可 mock 的接口用假数据调通之后再对接真实模型。我在几个项目里都是这么做的确实能少走不少弯路。就这些祝你的 AI 应用少踩几个坑。