大模型应用的上下文管理:context-mode 调度方案实战总结 你搜一下 context-mode 这个词能看到很多种答案编辑器里的上下文模式、终端工具的上下文感知、甚至游戏设备的按键配置方案。但在我做大半年大模型应用之后对这个词有了自己的理解——它是夹在会话状态和大模型 API 之间的一整套上下文调度方案尤其是做对话式 AI 产品时context-mode 直接决定了你的应用是“能用”还是“贵到不敢用”。如果你最近在碰大模型应用八成也撞到过这个场景对话聊到第 8 轮模型突然开始“失忆”把用户半小时前随口提的需求忘得干干净净稍微把历史记录多塞一点账单上的 token 数肉眼可见地往上跳再激进一点直接做截断结果更惨——模型回答得驴唇不对马嘴。我在给一家企业做客服机器人改造的时候几乎把这些问题都撞了一遍。最后沉淀下来的方案不是调某个参数而是一整套被我叫做 context-mode 的上下文管理设计模式把上下文从“仓库”变成“资源”根据会话状态和用户意图动态调度决定这一轮该喂什么、该压什么、该查什么。这篇文章把完整的思路、核心代码和踩坑记录都整理出来希望能给正在搞 AI 应用、尤其是对话式应用的你一点参考。1. 为什么非折腾不可上下文失控的三个现场先说结论上下文管理不是优化项而是对话式大模型应用的生存项。我用三个真实现场来说明。1.1 窗口耗尽模型开始“选择性失忆”很多人觉得上下文窗口不够用是模型的问题换个大窗口模型就行。但实际上大部分时候是我们根本没用对。我当时的系统 prompt 加上角色设定大概 1200 token知识库切片命中的内容平均 2000 token每轮对话历史以 500 token 的速度增长。也就是说一个 8k 上下文的模型聊到第 10 轮就已经见底。这时候如果你不做任何处理模型会做出一个它自己都没意识到的操作默默丢弃较早的内容。每个模型对“窗口内哪些 token 真正参与注意力计算”都有内部取舍越早的内容越容易被弱化。用户觉得模型失忆了其实模型也很委屈它的有效工作区就那么点你们的对话又那么长。我见过最朴素也最可怕的截断操作是“从最早的消息开始删”。这在短期来看能救急但代价是用户开头说的关键约束比如“预算不要超过 5000 块”“我是做餐饮的”统统被删光了。到了第 12 轮模型开始一本正经地给餐饮客户推荐陈年红酒。这就是典型的窗口耗尽可能造成的后果。更隐蔽的问题是大窗口模型不等于大有效工作区。我实测过把 2 万字的历史一股脑塞进去模型对最新问题的回答质量反而不如只给 4000 字精选内容。别把“能塞进去”当成“用得好”这两件事差着十万八千里。1.2 成本失控上下文是企业级应用最大的隐性支出聊完质量聊钱。按聊天记录里全量塞历史的方式每次请求的 token 数会随轮数线性增长。假设一个客服机器人平均每天 1 万次会话、每次会话 15 轮平均每轮输入 3000 token输出 500 token那么一个月的输入 token 就是 1 万 × 15 × 3000 4.5 亿输出 7500 万。用当时主流的模型定价粗略一算每月光上下文传输就烧掉一大笔而这里面相当一部分 token 是模型根本不需要的废话历史。我后来做过统计在未做任何管理的系统里真正影响回答质量的 token 占比经常不到 20%剩下的 80% 是重复的问候语、固定的语气词、已经失效的中间结论。等于你花了大价钱买了模型一个字都不看的“背景板”。这账怎么算都不划算。1.3 注意力稀释上下文越长重点越模糊第三个问题来自模型本身的注意力机制。给模型喂的上下文越长它反而越难聚焦在最新、最关键的信息上。这不是玄学是我压测时反复验证过的规律同一个问题在干净上下文里回答准确率能到 90% 以上塞入大量无关历史后可能掉到 70% 以下。类比一下这就像你开会时如果桌上有三十份没人看的材料你也很难保证自己每次拿起的是最关键的那一份。模型也是一样给它一堆“可能相关”的信息它就得花注意力去分辨哪些是噪声。而 attention 资源是有限的噪声多了信号自然就弱了。所以 context-mode 要解决的就是这三件事防止有效信息因截断而丢失、控制 token 成本不随对话轮数线性上升、以及保证模型始终在一个“干净而足够”的工作台面上思考。2. context-mode 到底是个什么东西把上下文当资源而不是当仓库2.1 一句话定义context-mode 是我给这套方案起的名字它的定位是夹在会话状态和大模型 API 之间的一层决策层。每次调用模型之前这层决策层会先问三个问题当前会话处于什么状态是开局、追问、切换业务还是收尾用户此刻的真实意图需要哪些信息系统指令、业务规则、历史关键点、知识库哪些该进槽位这些信息里哪些必须原样保留哪些可以压缩后再给回答完这三个问题它才真正组装出这一次请求的 messages 数组。换句话说context-mode 不是一个具体的算法而是一套“按模式调度上下文”的工程模式。你可以把它理解成电视机的观影模式同样一块屏幕看球赛用运动模式看电影用影院模式打游戏用游戏模式——屏幕没变但每个模式对画面的处理策略完全不同最终体验差很多。2.2 四条分层原则我把上下文分成四类并给每一类定了调度规则这是整个方案的地基系统上下文模型人格、回复风格、安全边界、当前模式说明。这部分永远在最前面固定预算比如 1000 到 1500 token不随对话增长。业务上下文订单信息、用户资料、关键约束、进行中的任务状态。按模式抽取跟当前意图相关的才进不相关的宁可放库里。历史上下文过往对话的事实与结论。采用压缩策略用摘要替代原文滚动更新。知识上下文外部检索出来的文档片段。按需拉取只取当前问题命中的片段并做重排。这个分类看起来简单但解决了我在 1.1 里说的“不知道哪些该留哪些该删”的根源问题你不需要在大杂烩里挑重点因为根本不让大杂烩出现。2.3 模式粒度从会话级到意图级有了四层分类接下来得定义“模式”。我给常见业务定义了四种模式模式适用场景上下文策略fast快速问答、闲聊系统上下文固定历史只保留最近 2 轮摘要不触发知识检索research深度查询、文档分析系统 完整摘要历史 知识库切片历史可放宽到 6 轮operation下单、改单、查物流系统 业务上下文优先历史压缩成“事实清单”handoff转人工、跨部门交接把所有未闭环的事实打包成交接单输出给下一环节模式的确定可以走两条路。一条是规则匹配通过关键词、意图分类模型的结果来映射另一条是让大模型自己在每轮开头做一次极短分类。我实际用的是混合方案能用规则的先走规则规则兜不住的才让模型分类。因为分类调模型又省钱又降低延迟。3. 代码落地一个最小可用的 context-mode 管理框架嘴上说了半天不如上代码。这一节给一个我在生产环境用过的简化版本核心是数据结构 调度器 压缩器。3.1 数据结构设计我先定义模式策略from dataclasses import dataclass, field from typing import Optional dataclass class ContextPolicy: mode: str max_history_rounds: int 4 # 该模式最多保留几轮原始对话 use_summary: bool True # 是否启用历史摘要 use_knowledge: bool False # 是否触发知识检索 system_premium: int 1200 # 系统上下文 token 预算 history_budget: int 2000 # 历史上下文 token 预算 business_budget: int 1500 # 业务上下文 token 预算 knowledge_budget: int 1500 # 知识上下文 token 预算 POLICIES { fast: ContextPolicy(fast, max_history_rounds2, use_summaryTrue, use_knowledgeFalse, history_budget800, knowledge_budget0), research: ContextPolicy(research, max_history_rounds6, use_summaryTrue, use_knowledgeTrue, history_budget2500, knowledge_budget2000), operation: ContextPolicy(operation, max_history_rounds3, use_summaryTrue, use_knowledgeFalse, history_budget1200, business_budget2000), }同时维护会话状态dataclass class ConversationState: session_id: str mode: str fast raw_history: list field(default_factorylist) # 原始消息 summary: str # 滚动摘要 business_facts: dict field(default_factorydict) # 业务事实这里最关键的细节就是business_facts。它不是简单粗暴地把订单信息存起来而是存成 key-value 形式例如{budget: 5000, user_type: restaurant, order_id: A123}。为什么这么做因为这类信息一旦被压缩进摘要里就面临丢失和变形风险。企业应用里订单号、金额、日期这类强结构化信息必须原样保留绝不能靠模型摘要来兜底。这也是我在 1.1 里说的问题的根治手段与其担心截断会删掉关键约束不如让关键约束根本不在普通历史里流转。3.2 核心调度逻辑组装最终请求调度器的核心方法长这样import tiktoken _tokenizer tiktoken.get_encoding(cl100k_base) def count_tokens(text: str) - int: return len(_tokenizer.encode(text)) def assemble_context(state: ConversationState) - list[dict]: policy POLICIES[state.mode] messages [] budget {history: policy.history_budget, business: policy.business_budget, knowledge: policy.knowledge_budget} # 1. 系统上下文 system_prompt build_system_prompt(state.mode) messages.append({role: system, content: system_prompt}) # 2. 业务上下文优先于历史 if budget[business] 0 and state.business_facts: facts_text format_facts(state.business_facts) if budget[business] count_tokens(facts_text): messages.append({role: system, content: f[业务事实]\n{facts_text}}) budget[business] - count_tokens(facts_text) # 3. 历史摘要 最近原始轮次 if policy.use_summary and state.summary: messages.append({role: system, content: f[历史摘要]\n{state.summary}}) history_payload [] for msg in state.raw_history[-policy.max_history_rounds * 2:]: history_payload.append(msg) messages.extend(history_payload) # 4. 知识上下文按需注入 if policy.use_knowledge: chunks retrieve_knowledge(history_payload[-1][content]) knowledge_text rerank_and_format(chunks, budget[knowledge]) if knowledge_text: messages.append({role: system, content: f[知识参考]\n{knowledge_text}}) # 5. 全局裁剪兜底 messages truncate_to_budget(messages, policy) return messages这里有三个值得注意的设计。第一history_payload并不是全部原始历史而是最近 N 轮原文 之前所有内容的滚动摘要。这样模型既能拿到最近对话的精确细节又不牺牲更早的事实传承。第二max_history_rounds是指“保留最近 N 轮原始消息”但它不意味着更早的消息完全消失了——它们已经被融进摘要里。所以模型看到的是“摘要 最近原文”的双层结构这比单一的历史截断方案信息密度高得多。第三所有正文组件都过了 token 预算检查。因为生产环境你永远不知道用户会贴多长的文档进来必须在组装时就做硬限制而不是依赖模型自己判断更不是事后才截断。3.3 摘要压缩与回填机制滚动摘要更新是整套机制最容易写坏的地方。我的实现逻辑是每当原始历史超过阈值比如 8 轮就把最老的 4 轮交给摘要模型融合进旧摘要生成新摘要然后把这 4 轮从原始历史里删掉。async def roll_summary(state: ConversationState): if len(state.raw_history) 8: return messages_to_merge state.raw_history[:4] state.raw_history state.raw_history[4:] combined f旧摘要:\n{state.summary}\n\n新增对话:\n{format_messages(messages_to_merge)}\n\n请提炼并更新摘要保留关键事实。 new_summary await call_llm(combined, max_tokens400) state.summary new_summary注意摘要刷新不是简单地把新对话追加到旧摘要后面而是要让总结模型在“旧摘要 新增对话”的完整视角下做复核。尤其是旧摘要中的关键实体——人名、数字、明确结论——必须要求原样保留。我踩过最经典的坑是第二次刷新摘要后上一轮还在的订单号突然消失了因为新摘要模型觉得那不重要。这属于质量事故后面专门讲怎么堵。3.4 与工具调用、RAG 的衔接context-mode 不应该是一座孤岛。如果你的应用要调工具比如查库存、下订单那么工具返回结果的处理也属于上下文调度范畴。我的经验是工具返回的结果尤其是结构化 JSON必须在同一次请求里作为“最近事实”注入同时把关键字段丢进business_facts固化。这样即使后续几轮对话把工具结果挤出了窗口业务状态也不会丢。RAG 的接入点就是assemble_context里的第四步。有一个细节建议知识检索的 query 不要直接拿最后一条用户消息而是用“当前模式的意图 最近一句去掉语气词后的核心诉求”。比如用户说“那上次那个还能做吗”直接拿去检索大概率失败但如果你在前面先根据摘要确认“上次那个”指的是“定制 500 份伴手礼”检索质量完全不一样。这就是把模式状态和 RAG 结合的价值。4. 实测数字开了 context-mode 之后系统变成了什么样空谈架构没用得看数据。我挑选了同一套客服语料分别用朴素全量历史方案和 context-mode 方案跑了一个月的线上压测结果如下。4.1 Token 消耗与成本对比指标朴素方案context-mode变化平均单次请求输入 token32001450减少 54.7%平均单次请求输出 token520480减少 7.7%每万次会话 token 成本基准约为原来的 45%省一半以上P95 响应首字时间1.6s0.9s减少 43.8%让我解释一下为什么响应时间下降这么多。首字响应时间极大受输入长度影响输入短了预填充时间自然就下来了。这个收益不需要换硬件、换模型纯粹是上下文调度带来的。4.2 回答质量与稳定性成本下降的同时质量不能滑坡。我用三组指标做了评测关键信息召回率在对话结果中能否找到用户提供的订单号、金额、截止日期等关键实体从 78% 提升到 95%。主要归功于business_facts的强结构化保留摘要丢了它也不丢。上下文一致性让判断模型每 5 轮检查一次“模型是否还记得第 1 轮的关键约束”从 82% 提升到 97%。这得益于滚动摘要而不是简单的截断。幻觉率按人工抽检 200 条回答计算从 11% 下降到 6%。上下文干净了模型乱编的概率确实低了。4.3 边界场景context-mode 不是银弹有得必有失这套方案也有明显边界。第一如果业务事实特别多比如售后场景动辄十几个字段business_facts依然会顶到预算上限需要做字段分级哪些必须全量保留、哪些只要摘要。第二摘要模型本身也是一次 LLM 调用会带来额外的成本和约 200ms 的延迟不过均摊到每 8 轮才触发一次完全可以接受。第三模式分类如果不够准整个策略就会歪掉。比如把 research 模式的复杂问题误判成 fast知识检索不触发回答深度明显下降。所以模式判定这一环一定要有回流标注和修正机制。5. 踩坑集中营context-mode 最容易翻车的几个细节代码写得再漂亮实战里也一样会翻车。我把这段时间踩过的坑按频率从高到低排了一下每一个都是真金白银换来的。5.1 摘要漂移二次摘要后事实开始变形这是我把摘要刷新到第二轮时发现的第一轮摘要里明明写着“客户预算是 3 万以内”第二轮摘要刷新后这个数字变成了“预算 3 万左右”再过两轮直接变成“预算较高”。原因是摘要模型在信息压缩时对模糊的定性描述更保守对精确数字反而不太敏感一旦上下文里出现相似数字就容易被干扰。解决办法是我前面说的双轨制强结构化信息丢进business_facts用 key-value 保留摘要里只承载非结构化叙事。同时每次刷新摘要时把旧的 key 集合传给模型要求“下列关键字段必须原样保留”相当于给摘要模型加了硬约束。这个改动之后数字漂移的概率大幅下降。5.2 模式抖动对话中途来回横跳另一个高频问题是模式在fast和research之间来回跳。用户先问“你们的配送范围”被判定成 fast紧接着说“具体到朝阳区哪些街道”research 规则命中切到 research回了一句“好的明白”又掉回 fast。每切换一次模式摘要预算和知识检索策略都变一次体验极其撕裂。解决办法是给模式切换加滞回hysteresis模式一旦进入 research至少要连续两轮满足 fast 条件才允许切回。或者用一个滑动窗口统计最近 3 轮的模式建议取多数。我用的就是滞回简单有效。这里的核心思想是策略不能对单轮输入过度敏感要给系统一点“惯性”。5.3 工具调用结果影响业务事实第三个坑是工具返回结果居然会污染business_facts。之前我把工具返回的 JSON 全量塞进事实表结果有一次库存接口返回了“可用库存 -5”这个负数被当成事实写进去了模型后续开始一本正经地讨论负库存的逻辑。后来我在工具结果入口加了 schema 校验只允许白名单字段进入business_facts其他一律只作为临时消息。这个坑的教训是上下文调度的边界要清晰不是所有数据都有资格成为“事实”。5.4 并发会话的状态污染我早期图省事把ConversationState存在模块级字典里结果压测时发现用户 A 的摘要跑到用户 B 的请求里了。原因很简单异步环境下会话 id 从请求上下文里取出来的时候用了默认值。后来我强制所有状态存取都走 session-scoped 的仓库并加了一组注入测试才把这类问题堵住。这个坑特别适合那些从单机 demo 转向多用户服务的同学注意。context-mode 这套东西单用户测试时怎么跑都对一旦并发上来状态隔离没做好就是事故现场。5.5 没有评估基线调参全靠玄学最后一个坑不算技术问题是流程问题。我一开始没有准备评估集每次改压缩策略、改预算都只能靠人工看几条对话判断“好像效果不错”。但你已经知道了感觉不可靠。后来我建了一个 200 条真实问题的评估集每条标注了必需的关键实体和期望回答要点每次改动都跑一遍回归。这套评估机制虽然搭建时费时间但之后所有优化都有据可依效率反而更高。6. 进阶思路从 context-mode 走向 context-aware agent做好了 context-mode并不能高枕无忧。我自己的路线图上还有几件事值得分享。6.1 让模式具备自动进化的能力模式定义目前靠人工策略但会话数据积累到一定程度后完全可以挖掘出新的高频模式。比如我发现物流咨询的会话量很大而且用户关注的字段高度相似就专门加了一个logistics模式固定注入物流查询工具、常见异常话术、以及必要的订单字段。这个模式上线后这一类的解决率明显上涨。建议你也按周维度看会话聚类让模式跟着数据长出来而不是永远吃老本。模式不是设计出来的是从数据里长出来的——这句话我用了三个项目才彻底理解。6.2 把压缩升级成真正的记忆系统滚动摘要本质上是“总结式记忆”它的天花板是信息密度而更进一步的方案是“结构化记忆 向量记忆”双通道。业务事实走结构化叙事细节走向量检索需要的时候按需召回。这样即使经过几十轮对话模型也能想起第一轮某个细节性描述。我现在正在做这个升级进展是把business_facts泛化为实体图谱把摘要泛化为事件流。改造成本不低但收益明显。6.3 监控指标建议最后说运维。context-mode 上线之后我建议至少盯三个指标模式分布、摘要刷新成功率、以及按模式的 token 消耗占比。模式分布能帮你发现策略是否合理。比如如果fast模式占了 80%但用户满意度没有提升那说明模式切分和业务需求不匹配。摘要刷新失败率如果偏高多半是摘要模型超时或格式解析出错需要加重试和非法格式兜底。按模式的 token 数据则能验证你到底把钱花在了哪里这个指标对和老板汇报也很有用。我个人最大的收获是大模型应用里的“记忆问题”并不存在一个神奇参数能一键解决。真正可靠的是把它当作系统工程来做——定义模式、分层调度、压缩回填、绑定业务事实、加上回归评测。每一层都不复杂但合在一起才能让对话系统既省钱又不失智。最后再分享一个很朴素的体会context-mode 这套东西最难的不是代码是判断力——判断哪些信息必须结构化、哪些可以压缩、哪些直接丢掉。这个判断力只能靠业务理解和反复试错积累。别一上来就追求最完整的方案先搭一个最小版本——模式策略表、滚动摘要、业务事实表三件套再配一份 100 条问题的回归评估集就足够支撑上线了。等真实对话数据积累起来你自然会知道下一步该往哪里加东西。