news 2026/9/11 10:36:59

大模型应用的上下文管理:context-mode 调度方案实战总结

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型应用的上下文管理: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_rounds=2, use_summary=True, use_knowledge=False, history_budget=800, knowledge_budget=0), "research": ContextPolicy("research", max_history_rounds=6, use_summary=True, use_knowledge=True, history_budget=2500, knowledge_budget=2000), "operation": ContextPolicy("operation", max_history_rounds=3, use_summary=True, use_knowledge=False, history_budget=1200, business_budget=2000), }

同时维护会话状态:

@dataclass class ConversationState: session_id: str mode: str = "fast" raw_history: list = field(default_factory=list) # 原始消息 summary: str = "" # 滚动摘要 business_facts: dict = field(default_factory=dict) # 业务事实

这里最关键的细节就是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_tokens=400) 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 模式抖动:对话中途来回横跳

另一个高频问题,是模式在fastresearch之间来回跳。用户先问“你们的配送范围”,被判定成 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 条问题的回归评估集,就足够支撑上线了。等真实对话数据积累起来,你自然会知道下一步该往哪里加东西。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 10:35:54

G-Helper:华硕笔记本轻量控制工具,一个 exe 替代 Armoury Crate

G-Helper&#xff1a;华硕笔记本轻量控制工具&#xff0c;一个 exe 替代 Armoury Crate 【免费下载链接】g-helper Lightweight Armoury Crate alternative for Asus laptops with nearly the same functionality. Works with ROG Zephyrus, Flow, TUF, Strix, Scar, ProArt, V…

作者头像 李华
网站建设 2026/9/11 10:35:13

MNIST手写数字识别CNN实战:从数据下载404到99%准确率

刚开始学手写数字识别 CNN 模型的时候&#xff0c;我以为最麻烦的部分在卷积层怎么设计、梯度怎么回传。结果真正动手第一天就被数据集卡住了——torchvision 下载 MNIST 一直报 404&#xff0c;进度条走到一半直接失败&#xff0c;重试三次都一样。后来把问题彻底查清楚&#…

作者头像 李华
网站建设 2026/9/11 10:34:27

新人入职第一天,Agent 就能告诉他“这个接口为什么这么设计“

新人问&#xff1a;"这个接口为什么要用回调而不是同步&#xff1f;"以前只有老员工知道答案。如果 Agent 也能回答呢&#xff1f; 新人上手慢&#xff0c;从来不是因为不会写代码 做了几年技术 TL&#xff0c;带过的新人不少。我发现一个规律&#xff1a;上手慢的…

作者头像 李华
网站建设 2026/9/11 10:34:06

金属切削仿真常见误区与优化实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 10:32:21

Linux last命令完全指南:从登录查看到安全审计

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华