ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

大模型上下文管理实战:context-mode三种模式与工程选型

大模型上下文管理实战:context-mode三种模式与工程选型 1. 上下文爆炸AI应用跑着跑着就“变笨”问题出在哪这半年我一直在折腾大模型应用从客服工单摘要到企业文档问答前后做了好几个能跑的东西。有个现象特别典型第一天演示给老板看的时候问答流畅得像个专家上线跑了两三个星期用户开始反馈“机器人越聊越笨”之前明明答对过的问题现在开始瞎编。我一度怀疑是模型版本被偷偷换了后来把日志翻出来一条条对比才发现问题根本不在模型而在上下文。每一轮请求发往模型的内容里“聊天记录”这部分已经从最初的一千多字符膨胀到了几万甚至十几万字符。用户真正关心的核心信息——身份、诉求、历史结论、承诺时效——被淹没在大段寒暄、重复确认和无关闲聊里。模型不是变笨了是它要处理的信息里“噪音”太多了。这个问题在工程界有一个统一的名字叫上下文管理具体落地时我们会使用一套策略来控制“喂给模型的上下文”这套策略的组合就是我要讲的context-mode上下文模式。为什么现在这个话题这么热因为大模型的上下文窗口虽然越做越大从4K、16K一路卷到200K但窗口变大不代表你可以无脑全塞。有三个硬现实摆在那第一成本。API按token计费上下文翻倍单次调用成本基本翻倍。一个日活十万的聊天机器人每轮多塞5000个多余token一年要多烧掉的钱很可观。第二质量。上下文塞得越满模型对关键信息的注意力越容易被稀释。很多研究表明超长上下文中存在“信息迷失”关键内容落在中间位置时最容易丢。第三硬限制。就算窗口是200K多轮对话、长文档、多Agent协作加起来照样能把它撑爆一旦超窗请求直接报错整个流程就得被迫降级。所以“context-mode”到底是什么我用一句话概括它是你决定“哪些上下文进模型、哪些不进、进了之后以什么形态存在”的一套处理策略。做得好同样的模型能稳定跑几百轮不做再强的模型也会被你自己的日志拖垮。这篇文章我打算把这套策略拆开讲清楚几种主流模式的原理、代码级实现以及我踩过的那些坑。不管你是刚接触API的初学者还是已经踩过“越聊越笨”坑的开发者按这套思路走基本能把上下文管理这块补齐。2. 三种上下文模式的底层逻辑全量、检索、压缩2.1 全量上下文模式信息无损但贵且脆这是最原始也最容易想到的模式把从第一轮开始的所有用户消息、助手回复、工具调用结果全部拼在一起按时间顺序塞给模型。我先说它的优点因为它不是没有用。信息无损模型能看到完整的对话脉络对于长度可控的会话——比如单轮文档问答、短会话客服——这是最稳的。我自己做表单式多步询问用户最多回答五六个问题就结束时一直用的就是全量模式简单直接效果也好。但它的两个硬伤是绕不开的。一是成本随对话轮数线性上涨二是超过窗口上限之后各家API的处理方式不一样有的直接报错有的从最前面开始截断。如果是后者你会遇到一个最隐蔽的问题最早的用户指令被截掉了但后续的回复都依赖那条指令于是模型的行为悄悄“漂移”。我排查过的一个案例里用户在很前面说了一句“不要引用任何外部链接”模型一开始都遵守等到窗口被撑爆、系统从最前面截断之后它开始正常引用链接了。日志翻半天最后才发现是截断方向把这个约束给丢了。全量模式适合什么场景我建议只在“能严格限制对话长度”的场景使用。别用在大规模多轮对话上那是给自己埋雷。2.2 检索模式按需取用token稳定但怕漏检索模式就是RAG的思路在对话历史上的应用。每一轮请求前不是把所有历史都拿过来而是先把历史消息切片按时间、按主题、按语义再用用户当前的问题去向量库或倒排索引里召回最相关的几个片段拼进上下文。这种模式最大的好处是token消耗基本恒定不管对话进行到第10轮还是第1000轮每次塞进模型的内容量都差不多成本可控也不会被窗口上限追着打。但它有一个致命的毛病召回是一个概率过程它可能漏。用户的当前问题里如果没有出现历史里的关键人名、项目代号检索引擎很可能就召不回那条真正重要的信息。我遇到过最典型的案例用户在第5轮提起过“上次说的那个对公账户方案”到了第30轮他问“那个方案批了没有”——“对公账户方案”这几个字在历史里但第30轮的问题是“批了没有”相关性检索把“批了没有”相关的回复排前面那一条关键背景反而没有被召回模型只能瞎猜。这种漏召回带来的问题比全量模式更隐蔽因为表面上看一切正常只是答案偶尔“飘”你很难第一时间联想到是召回策略出了问题。2.3 压缩模式精华保留但压缩即失真压缩模式用LLM自己对历史做二次加工把过长的对话压成摘要保留“语义精华”丢掉唠叨和重复。具体做法是维护一个“摘要缓冲区”每积累几轮对话、当明文历史超过某个token阈值时就调用一次LLM把旧的历史和旧的摘要一起碾成新的摘要之后每次请求发送的内容是“当前摘要 最近N轮完整明文”。它的好处是既能控制token又能跨越多轮保留长期信息是目前多轮Agent里最常用的模式。缺点也很直白摘要过程是信息有损压缩LLM认为不重要的东西会被扔掉但“LLM认为不重要”不等于“业务认为不重要”。用户身份、金额承诺、时间要求、合规条款这些在闲聊式摘要里最容易被当噪音丢掉。后面第4部分我会用一个完整的案例讲这块的坑。2.4 三种模式的账本token消耗实测对比我拿一个典型场景算过一笔账假设每轮用户平均输入20个token模型回复平均200个token对话进行到第50轮。全量模式下第50轮请求会带上约11,000个token的历史50轮×220 token再加上结构损耗用检索模式每轮只召回4个片段每个片段约200 token加上查询本身大约1,300 token用压缩模式摘要控制在500 token最近5轮明文约1,100 token合计1,600 token。也就是说同样的对话历史全量模式在50轮时已经用掉了检索/压缩模式的7倍成本而且这个差距还在继续拉大。理解成本差异后很多人的第一反应是“那当然选压缩或检索”。但我的建议是别急——先搞清楚你能容忍丢什么。三种模式本质上是“信息完整性”和“资源消耗”的交换选错方向比不选更痛苦。下面这张表是我自己做选型时用的对照模式信息保留率token成本趋势适用场景主要风险全量最高无损随轮数线性增长短会话、表单式问答、一次性长文档分析成本失控、窗口溢出、截断丢前置指令检索中取决于召回质量基本恒定知识库问答、用户问题相对独立漏召回、切片切断语义、排序不稳定压缩中高但有损较低且可控多轮对话、Agent长期任务、客服摘要失真、关键条款丢失、二次压缩级联损耗3. 自己动手实现一个context-mode滚动窗口和摘要缓冲的代码级拆解3.1 token预算先算清后面才不慌实现任何上下文模式之前第一件事是搞清楚你有多少token预算可用于历史。模型上下文窗口是总的“房间”里面要住几类人系统提示词、工具定义、用户的当前输入、模型输出以及你想塞进去的历史。我的默认公式是history_budget total_window - system_prompt - tools_definition - current_input - max_output_tokens - safety_reserve以gpt-4o-mini默认8K窗口为例系统提示占600 token工具定义占200 token当前用户问题占100 token输出预留800 token安全余量留500 token那么历史的可用预算是8000 - 600 - 200 - 100 - 800 - 500 5800 token。这个数字就是滚动窗口的“水线”超过水线的历史直接截掉水线以下的保留。注意不同模型的tokenizer不一样别用字符数估算要用tokenizer算。OpenAI系的用tiktokencl100k_base开源的可以用transformers的tokenizer。凡是上了生产环境的人都应该把这个计算封装成一个函数而不是每次手写。我自己踩过字符数估算的坑中文场景下字符和token比例不固定同一个字符数代码、英文、中文的token消耗完全不同靠感觉估算迟早出事。3.2 滚动窗口的Python实现永远保留最新丢掉最旧滚动窗口的原理很简单从最新的一条历史消息开始向前累加token一旦超过预算就停止丢掉更早的内容。关键决策是“从哪个方向看”永远保留最新的丢最旧的因为对话里最近的内容与当前意图相关性最高。下面是我实际用的一个简化版本from typing import List, Dict def build_truncated_history( messages: List[Dict[str, str]], max_tokens: int 5800, ) - List[Dict[str, str]]: budget max_tokens selected [] # 注意从最新往最旧遍历 for message in reversed(messages): # content_token_count 使用 tiktoken / transformers 等工具计算 content_tokens count_tokens(message[content]) if budget - content_tokens 0: break selected.insert(0, message) # 按时间正序返回 budget - content_tokens return selected这段代码的逻辑我再解释一下reversed保证从最新的消息开始扣预算selected.insert(0, message)是为了最终列表保持时间正序一旦预算不够就break更早的消息被丢弃注意每条消息本身还有role、name等结构开销实际实现中要按格式估算每条约4~6个token别只算content。这个实现看着简单但生产环境里会有几个细节值得说系统提示词永远在历史之外不要占history_budget工具调用的返回结果通常很长建议单独设置上限比如每个tool result最多500 token超出就截断或摘要如果单条历史本身就超过预算只能保留这条消息的前max_tokens个token并加一个截断标记避免整个上下文被一条超长历史撑爆很多模型对响应体里的message数量也有隐形上限别一味切碎成几百条小消息必要时先合并同角色连续消息。3.3 摘要缓冲让历史“瘦身”而不是“截断”滚动窗口的短板在于丢掉的信息永远消失了。用户第3轮提过一个重要约束第40轮才用到第30轮时这条信息就被截掉了。解决办法是摘要缓冲不是直接丢而是先压进摘要再丢。核心思路是维护一个独立的running_summary变量。每当明文历史超过某个阈值就调用LLM做一次“摘要合并”LLM输入 旧的running_summary 近期一段明文历史输出 新的running_summary。这样信息以压缩形态留了下来虽然细节会丢失但大方向在。请求时上下文 系统提示 running_summary 最近N轮明文。示例触发逻辑def should_trigger_summary(raw_history: List[Dict], summary_token_limit: int 2000) - bool: raw_tokens sum(count_tokens(m[content]) for m in raw_history) return raw_tokens summary_token_limit一个值得注意的设计是“摘要的摘要”对话足够长时running_summary本身也会膨胀。必须为它单独设预算比如摘要最多到1000 token时就启动二次压缩。这个递归压缩的过程要有上限控制否则摘要本身会演变成新的“日志垃圾”。触发条件设得太频繁会额外花钱设得太少又会积累过多明文。我试下来2,000 token是一个性价比比较好的起点你可以按业务的对话密度调整。另外很多成熟的框架里已经有类似的实现了比如LangChain的ConversationSummaryBufferMemory原理就是我上面讲的这套。我不建议直接照抄框架理解原理后自己写一个最小实现出问题的时候你才知道去哪排查。3.4 为什么我建议先上滚动窗口再升级摘要缓冲很多人一听到摘要缓冲的好处就想一步到位。我的经验是第一版哪怕只用滚动窗口也要确保把token统计、历史切片、请求组装这些基础做对跑通之后再叠加摘要否则你根本分不清问题是出在“截断策略不对”还是“压缩失真”。先做简单正确的东西再往里面加复杂度排查问题的时候会轻松很多。我在多个项目里都是这个节奏稳定跑了两周之后再引入摘要对比前后效果是否提升每一步都可回退、可验证。4. 压缩模式的致命坑关键约束被压没了一次完整排查4.1 从“用户投诉”到“定位到摘要丢失”的四步排查我做过一个客服场景的Agent任务是根据聊天历史生成工单处理建议。有一个用户是平台的高等级会员开头第一句就明确说“我是钻石会员要求24小时内必须出方案否则投诉”。前几轮模型都记得这个时效承诺回复里引用了会员等级作为优先处理依据。结果第22轮的时候模型突然不再提会员等级给的方案也变成了普通用户的标准处理时长。用户很生气说“我说过我是钻石会员你们言而无信”。我刚开始以为是模型随机出错但连续三次复现都稳定在20轮左右开始“失忆”这就明显不是随机性了。我的完整排查链路是这样的第一步先把用户投诉的时间点定位到具体轮次。日志里每一轮请求都记录了session_id和时间戳拉出来看到底从哪一轮开始答复内容出现变化。第二步把那一轮实际发送给模型的请求完整拉出来。不是看数据库里的聊天记录而是看真正拼装好的payload——系统提示、摘要、明文历史、用户当前输入。这一步很关键因为很多问题恰恰出在“拼装逻辑”而不是模型本身。第三步肉眼检查摘要内容。在那一轮发送的running_summary里“钻石会员”和“24小时出方案”已经完全消失了摘要只留下了“用户咨询退款情绪激动”。而明文历史窗口里第1轮的原始消息早已被挤出滚动窗口。也就是说模型根本不是“失忆”是压根没收到这条信息。第四步回放摘要生成的输入和输出。那次触发摘要合并时输入是“旧摘要 第1到第15轮的明文”旧摘要本身就比较薄而第1轮的消息被夹在一堆类似于客服确认、用户重复抱怨的内容中间。LLM做压缩时把“钻石会员”识别成了可以丢弃的修饰性信息把“24小时出方案”当成用户可以灵活调整的心理预期两者都被过滤了。这就叫“摘要失真”——不是模型没能力记住而是压缩策略没有明确告诉它什么不能丢。4.2 修复方案关键字段锁定 分层摘要定位到根因之后我做了两个层面的修复。第一层是“关键字段锁定”。不再让LLM自由决定摘要内容而是先用正则和NER抽取出业务里的不可丢失字段用户等级、承诺时间、金额、退款单号、合规关键词。这些字段单独存一个结构化的列表每次组装请求时把列表原样放进上下文做摘要合并时把这部分内容作为独立段落固定保留不允许LLM“加工”或压缩。比如代码层是这样处理的critical_fields extract_critical_fields(raw_history) # extract_critical_fields 内部用正则 关键词表抽取 summary_prompt f 请压缩以下对话历史保留所有关键信息。 以下是不可丢失字段的清单必须原样保留 {critical_fields} 对话历史 {raw_history} 生成新摘要 第二层是“分层摘要”。一次调用LLM把20轮对话全部压成一段摘要失真率高是必然的。我改成先按主题或意图给对话历史分块每块单独生成一个小摘要如“退款诉求部分用户要求24小时处理”再把所有块摘要合并成总摘要。这样任何一段信息被压没时至少能和其它块的形成对照排查起来精确得多。修复后的压测结果我记得很清楚100轮长对话压测关键字段保留率从原来的82%提升到了99%以上。这个数字不是模型变强了是策略变对了。4.3 检索模式的两个坑切片切断语义与排序漂移压缩模式有失真的问题检索模式也不是银弹。我实际用下来有两个高频坑。第一个是切片切断语义。把历史消息按固定长度切片比如每512个字符一刀切时很容易把一句话从中间劈开导致语义残缺。最直接的例子切完后一段以“所以我认为不应该”结尾下一段开头是“同意这个方案”。单独召回后面一段模型会以为用户同意了召回前面一段模型又不知道他反对什么。解决思路是让切片尽量按“完整的语义单元”划分比如一条完整的历史消息、一组有明确上下文关联的问答对而不是傻傻地按字符数切。第二个是排序漂移。向量召回按相似度排序后你通常只取TopK但相似度最高的片段不一定包含了“当前最关键”的信息。还是那个例子用户问“批了没有”和它最相似的是历史里另一条“批了没有”的询问而不是背景方案。解决思路是给关键信息打一个业务权重对包含用户ID、订单号、方案编号的片段做加权或者直接把这些高优先级片段固定在召回结果的头部不完全依赖相似度排序。5. 怎么选结合业务场景匹配context-mode我的默认配置5.1 不同业务场景的选型对照讲了这么多原理和坑落到工程上到底怎么选我自己有一个选型矩阵基本是按“业务能容忍丢什么”来分的业务场景我推荐的模式理由闲聊陪伴型压缩模式 少量长期记忆对细节要求低信息保留个大方向就行客服工单结构化字段锁定 滚动窗口 摘要用户身份和承诺时效是刚需必须锁定文档问答检索模式为主短会话全量也行用户问题相对独立历史依赖低代码助手滚动窗口为主最近代码状态最关键摘要容易丢缩进和依赖细节多Agent协作共享上下文服务 按角色过滤每个Agent只看到相关上下文降低混淆判断逻辑是这样的先列一个list写出这种业务里“丢了会出事故”的信息比如客服场景的身份等级、医疗场景的过敏史、金融场景的金额条款。然后根据这个list倒推模式如果不可丢失字段很多且结构化用关键字段锁定如果丢一点没关系用纯摘要就行如果用户问题高度变化、彼此独立检索模式足够。5.2 我目前在生产环境用的默认配置经过大量试错我现在搭AI应用时的默认配置是这套直接供参考history_budget 6000 token以8K窗口模型为例预留输出和系统提示后的剩余最近明文轮数 8轮超过部分进入摘要缓冲摘要触发阈值 2000 token明文历史running_summary上限 1000 token超出则递归压缩关键字段正则表用户等级、承诺时间、金额、订单号、退款状态抽取后固定进上下文;检索模式召回数量 4个片段其中2个固定来自“不可丢失字段索引”2个来自语义相似度。这套配置不是一次性拍脑袋定的是在一个客服Agent上跑了三周逐步调出来的。它兼顾了成本、稳定性和关键信息保留率但每个业务的阈值真的不一样。聊天密集型的业务8轮明文可能不够要加到12轮技术支持的工单摘要触发阈值可以调大因为细节往往比闲聊重要。5.3 关于“引擎升级”的一点建议如果团队用的是模型厂商自带的“自动压缩”功能比如某些API自带的上下文压缩开关我建议还是把它当作兜底而不是主力方案。厂商的自动压缩针对通用场景设计的它不知道你业务里什么字段丢不得。我在框架里加了自己的关键字段锁定之后厂商自带压缩的影响就被控制在一个能接受的范围里了。我自己在实际操作中有个体会上下文模式这个事80%的价值来自“把不能丢的东西单独管起来”只有20%来自“用什么模型来压缩”。很多团队花大把精力换更强的模型却不肯花一个下午把关键字段清单列出来这是本末倒置。最后再分享一个小技巧上线前一定要做一个“长对话压测脚本”自动模拟50轮以上的对话每轮都校验几个关键字段是否还在上下文中。别等用户投诉了再排查那会儿你已经很难回放每一轮到底发生了什么。压测脚本写起来不复杂核心就是造数据、跑流程、断言字段存在与否但它能帮你把上面这些坑全部挡在上线之前。
返回列表