
1. 上下文管理到底在管什么第一次听到“上下文管理”这个词很多人会下意识觉得它是个很虚的概念像是某种架构师画图时才用得上的术语。但如果你真正写过稍微复杂一点的对话系统、Agent 应用或者哪怕只是一个带多轮记忆的客服机器人你就会发现上下文管理做得好不好直接决定了这个系统是“能用”还是“没法用”。我先把话说直白一点。所谓上下文管理本质上就是解决一个非常朴素的问题当模型一次能处理的文本长度有限时我们该把哪些信息塞进去把哪些信息丢掉以及用什么方式组织这些信息让模型在有限的窗口里做出最正确的判断。你可以把它想象成一个会议记录员。一场会开了三个小时老板只给你一页纸的篇幅去汇报。你不可能把每个人说的每句话都抄上去你得判断哪些是决议哪些是背景哪些是废话。上下文管理干的就是这个活只不过对象换成了 token判断标准换成了“对当前任务有没有用”。这个领域之所以最近被反复提起核心原因有三个。第一长对话场景爆发用户不再满足于问一句答一句而是希望系统记住前面聊过什么。第二Agent 类应用兴起一个任务可能要调用十几个工具、经历几十轮推理中间产生的中间结果如果全留着窗口瞬间就爆了。第三成本敏感token 是要花钱的上下文越长推理越慢、越贵没人愿意为了一堆无关信息买单。所以这篇文章我想聊的不是某个具体框架的 API 怎么调而是把上下文管理这件事拆开讲讲它背后的设计思路、核心手段、实操中会踩的坑以及我自己在项目里总结出来的一些土办法。适合正在做对话系统、Agent、RAG 应用的开发者也适合刚接触这块、想搞明白“为什么我的机器人聊三句就失忆”的朋友。2. 上下文管理的整体设计思路2.1 为什么不能“全都塞进去”新手最容易犯的错就是觉得“窗口不是有 128k 吗那我全塞进去不就行了”。我早期也这么干过结果很快被现实教育了。第一个问题是注意力稀释。模型在处理长上下文时并不是每个 token 都同等重要。当无关信息太多时真正关键的那句话反而可能被淹没。这在学术上叫“lost in the middle”就是放在中间位置的信息最容易被忽略。你塞了一堆历史对话进去结果模型偏偏忘了用户三分钟前说的那个关键约束。第二个问题是成本与延迟。假设一次对话平均 5000 token你每次都把完整历史带上聊到第 20 轮就是 10 万 token。按现在的价格算单次调用成本可能翻几十倍响应时间也从一秒变成好几秒。用户可不会管你技术多先进他只觉得“这破机器人怎么这么慢”。第三个问题是信息冲突。历史里用户可能改过主意先说“我要红色的”后来说“算了还是蓝色吧”。如果你把两条都原封不动塞进去模型有可能纠结甚至选错。上下文管理的一个隐藏任务就是帮模型消解矛盾保留最新、最有效的状态。所以核心思路不是“塞得越多越好”而是在有限的预算内最大化有效信息的密度。这跟做推荐系统有点像你不可能把整个商品库推给用户得排序、得截断、得做多样性。2.2 三种主流策略的取舍实际项目里上下文管理大致可以归为三类做法我按复杂度从低到高排一下。第一类是滑动窗口。最简单粗暴只保留最近 N 轮对话超出的直接扔掉。优点是实现快、成本可控缺点是“金鱼记忆”用户很早说的关键信息会丢。适合那种单轮为主、偶尔多轮的场景比如简单的问答。第二类是摘要压缩。把历史对话定期用模型总结成一段简短的话然后只带摘要加最近几轮。这个思路很符合直觉相当于会议纪要。但坑在于摘要本身会丢信息而且摘要也要花 token 和时间如果摘要频率没控制好反而更贵。第三类是结构化记忆。把对话中的关键信息抽出来存成结构化的键值对或者向量需要的时候再检索回来。这是目前 Agent 类应用的主流做法也是我认为最值得深入的方向。它把“上下文”从一段流水账变成了一个可查询的记忆库。我自己的经验是没有银弹通常是组合拳。比如最近几轮原文保留稍远的历史做摘要关键实体和用户偏好存成结构化记忆需要时再召回。下面几节我会把每一块拆开讲。2.3 一个容易被忽略的原则上下文要“可解释”这一点很少有人提但我觉得特别重要。你塞进模型的每一段上下文最好都能回答一个问题它为什么在这里如果你自己都说不清某段历史为什么被保留那模型大概率也用不好它。我在项目里会要求每条上下文都带一个来源标记比如“来自用户第 3 轮输入”“来自工具调用结果”“来自系统摘要”。这样出问题时能快速定位也方便做 A/B 测试看看到底是哪类信息在起作用。3. 核心手段拆解与实操要点3.1 滑动窗口不是简单截断那么简单滑动窗口听起来最简单但真要做好也有讲究。最粗糙的做法是按轮数截断保留最近 10 轮。但问题是一轮对话的长度可能差很多有的一轮就一句话有的一轮带了一大段代码。按轮数截断很容易出现“保留了 10 轮废话丢掉了 1 轮关键需求”的情况。更合理的做法是按 token 预算截断。你先设定一个总预算比如 4000 token 给历史然后从最近的消息往前累加直到接近预算为止。这样能保证窗口利用率稳定。def build_window(messages, budget4000): selected [] used 0 for msg in reversed(messages): cost count_tokens(msg[content]) if used cost budget: break selected.append(msg) used cost return list(reversed(selected))这里有个细节系统提示词和当前用户输入要优先保留不能被截断。所以实际预算应该是总预算 - 系统提示 - 当前输入剩下的才给历史。我见过有人把系统提示也一起截了结果模型人格都变了这就很尴尬。注意截断时尽量以“完整消息”为单位不要把一条消息从中间切断。半句话的上下文比没有更糟模型可能基于残缺信息瞎猜。3.2 摘要压缩什么时候摘、摘多细摘要压缩的关键参数有两个触发时机和压缩粒度。触发时机上常见做法是“当历史 token 超过阈值时触发一次摘要”。比如历史超过 3000 token就把最早的一半压缩成摘要。这样能保证摘要不会太频繁也不会等到爆窗口才手忙脚乱。压缩粒度上我建议分层摘要。不要把所有历史压成一段话而是保留一个“滚动摘要”加若干“阶段摘要”。滚动摘要记录整体背景和长期偏好阶段摘要记录最近几个话题的结论。这样模型既知道大局也知道细节。摘要的 prompt 也有讲究。我一般会要求模型输出固定结构比如【用户目标】... 【已确认事实】... 【待解决问题】... 【用户偏好】...结构化之后后续检索和拼接都方便很多。纯自然语言的摘要用起来很灵活但不好做程序化处理。实操心得摘要一定要保留“否定信息”。用户说“不要用红色”摘要里如果只写“讨论了颜色”那这个约束就丢了。我一般会强制要求摘要包含“用户明确排除的选项”。3.3 结构化记忆把对话变成可查询的数据库这是我认为最有价值的一块。核心思想是对话是流但记忆应该是表。具体做法是在对话过程中实时抽取关键信息存成结构化字段。比如用户说“我住在杭州平时喜欢喝美式”就抽成{city: 杭州, coffee: 美式}。下次需要的时候直接查这个表而不是去翻历史记录。抽取的时机可以是每轮结束后异步做避免阻塞主流程。抽取的内容一般包括实体人名、地点、时间、偏好喜欢什么、讨厌什么、任务状态做到哪一步了、约束条件必须满足什么。存储上简单场景用 JSON 或 Redis 就够了复杂场景可以上向量库做语义检索。我的建议是两者结合结构化字段做精确匹配向量做模糊召回。比如用户问“我之前说的那个地方”精确匹配匹配不到但向量能召回“杭州”。memory { facts: {city: 杭州, coffee: 美式}, preferences: {language: 中文, tone: 简洁}, task_state: {step: 已选好酒店, next: 订机票} }拼接上下文时把这块记忆格式化成一段简短文本放在系统提示后面模型就能“记得”这些事而且不占太多 token。3.4 检索增强需要时才召回结构化记忆解决的是“已知要记什么”但有些信息你事先不知道会不会用到。这时候就需要检索增强也就是常说的 RAG 思路用在上下文管理上。做法是把历史对话切块、向量化、存库。当前用户提问时先用这个问题去检索最相关的几段历史只把这几段拼进上下文。这样既保留了长期记忆又控制了 token 消耗。这里的关键是检索质量。我踩过的坑是直接用用户原话检索效果往往一般因为用户提问和历史表述用词可能完全不同。改进办法是先做查询改写让模型把用户问题改写成几个可能的检索关键词再去召回。这一步能明显提升命中率。另一个坑是召回太多。检索出来 20 条全塞进去又爆了。所以要设一个 top-k一般 3 到 5 条就够再配合一个相关性阈值低于阈值的直接不要。4. 完整实操流程与关键环节4.1 一个可落地的上下文组装流程把前面几块拼起来我给一个我自己项目里用过的组装流程你可以直接参考。整个流程分五步。第一步接收用户输入先做意图识别和查询改写。第二步从结构化记忆里取出相关字段。第三步用改写后的查询去向量库召回相关历史片段。第四步取最近 N 轮原文作为短期上下文。第五步把系统提示、结构化记忆、召回片段、短期原文、当前输入按顺序拼起来并做最终的 token 预算检查。顺序很重要。我一般把最稳定的信息放前面系统提示、长期偏好最相关的信息放中间偏后召回片段最新的信息放最后当前输入。这样符合模型“近因效应”最近的输入影响最大。def assemble_context(user_input, memory, retriever, recent_msgs): query rewrite_query(user_input) recalled retriever.search(query, top_k4, threshold0.75) facts format_memory(memory) short_term build_window(recent_msgs, budget2000) context [ {role: system, content: SYSTEM_PROMPT facts}, *[{role: system, content: f相关历史{r}} for r in recalled], *short_term, {role: user, content: user_input} ] return trim_to_budget(context, total_budget8000)4.2 参数怎么定给几个参考值参数没有绝对标准但可以给几个我实测下来比较稳的起点你再根据自己场景微调。参数参考值说明总 token 预算8000留足余量给模型输出短期原文预算2000约最近 5 到 8 轮召回片段数量3 到 5 条太多会稀释注意力相关性阈值0.7 到 0.8低于此值不召回摘要触发阈值3000历史超过就压缩结构化记忆上限20 个字段太多反而干扰这些值不是拍脑袋来的。比如短期预算 2000是因为我观察到大部分对话的关键信息集中在最近 5 轮内再往前就明显衰减。召回 3 到 5 条是因为超过 5 条后模型对每条的平均注意力下降得很快实测准确率反而降低。4.3 实操现场一次多轮订票对话的上下文变化我拿一个订票场景举例看看上下文是怎么随对话演进的。第一轮用户说“帮我订一张去北京的票”。此时上下文里只有系统提示加这句话结构化记忆为空。第三轮用户补充“要下午的靠窗”。结构化记忆更新为{destination: 北京, time: 下午, seat: 靠窗}。短期原文保留这三轮。第八轮用户问“刚才说的那个时间还有别的班次吗”。这时候“刚才说的那个时间”指代不明直接检索可能失败。查询改写把它变成“下午 北京 班次”召回第三轮的偏好片段模型就能正确理解。第十五轮历史已经很长触发摘要。前七轮被压缩成一段阶段摘要结构化记忆继续保留关键字段。此时上下文里是系统提示 结构化记忆 阶段摘要 最近八轮原文 当前输入。总 token 依然控制在预算内。这个过程里结构化记忆是骨架摘要和召回是血肉短期原文是皮肤。三者配合才能让模型在长对话里不迷失。5. 常见问题与排查技巧实录5.1 模型“失忆”了怎么定位失忆是最常见的问题但原因可能有好几种得逐个排查。先看结构化记忆有没有更新。如果用户说了新信息但记忆没变那是抽取环节的问题可能是抽取 prompt 没覆盖这类信息或者异步任务失败了。再看召回有没有命中。把召回结果打日志看看相关历史到底有没有被检索出来。如果没命中是查询改写的问题还是向量库的问题。如果命中了但没进上下文那是预算裁剪把它挤掉了。最后看拼接顺序。有时候信息在上下文里但被放在了很靠前的位置模型没注意到。试着把它挪到靠近当前输入的地方往往就“想起来”了。5.2 上下文越长效果越差怎么办这个现象很典型通常不是模型不行而是信噪比太低。我的处理办法是三步。第一步做去重。历史里经常有大量重复表述比如用户反复确认同一件事。去重能省下不少 token。第二步做冲突消解。同一字段有多个值时只保留最新的。比如用户先说“要红色”后说“要蓝色”记忆里只留蓝色并在摘要里注明“用户已更改偏好”。第三步做重要性排序。给每条上下文打个重要性分预算不够时优先保留高分项。重要性可以简单按“是否包含实体、是否是用户明确指令、是否是最新”来算。5.3 常见问题速查表现象可能原因排查方向聊几句就忘窗口太小或没做记忆检查预算和记忆更新记错信息冲突未消解检查同字段是否多值响应变慢上下文过长看 token 数和召回量答非所问召回不相关检查查询改写和阈值摘要丢关键点摘要 prompt 太粗加结构化输出要求成本飙升每次都带全量历史引入摘要和检索避坑技巧上线前一定要做上下文长度监控。我见过太多项目上线后才发现平均 token 是预估的三倍账单直接爆炸。埋个点统计每次调用的 token 分布心里才有底。5.4 几个我踩过的坑第一个坑是摘要太频繁。一开始我设的阈值很低结果每两轮就摘要一次不仅慢而且摘要本身也消耗 token算下来比不摘要还贵。后来把阈值调高只在必要时才摘。第二个坑是结构化记忆字段太多。我一度想把所有信息都结构化结果记忆表几十个字段拼进上下文反而成了噪音。后来砍到只留最关键的十几个效果明显变好。第三个坑是忽略时区。用户说“明天下午三点”如果系统不记录当前时间和时区模型可能算错日期。这类隐含信息一定要在系统提示里明确。6. 上下文管理的扩展方向6.1 从单会话到跨会话记忆现在大部分实现都是单会话内的上下文管理会话一结束记忆就没了。但真实产品里用户希望系统“记得我上次说过什么”。这就需要把结构化记忆持久化按用户 ID 存储下次会话开始时加载。跨会话记忆的难点在于隐私和隔离。不同用户的数据必须严格分开同一用户的不同场景也要区分。我的做法是给记忆加命名空间比如user_id scene检索时限定范围。6.2 让模型自己管理上下文目前上下文管理大多是规则驱动的什么时候摘要、召回几条都是人定的。一个有意思的方向是让模型自己决定。比如给模型一个工具它可以主动调用“记住这个”“忘掉那个”“检索关于 X 的历史”。这样上下文管理就从被动变成主动更接近人类的工作记忆机制。我试过简单的版本让模型在每轮结束时输出“需要记住的要点”效果还不错。复杂版本还在探索主要是稳定性和成本的问题。6.3 多模态上下文现在对话里越来越多图片、语音、文件。这些内容的 token 占用和文本完全不同一张图可能就顶几千 token。多模态上下文管理需要额外考虑图片要不要压缩、要不要只保留关键帧、语音要不要先转文字再管理。这块我经验还不算多但可以肯定的是不能简单套用文本那套预算逻辑。我个人在实际操作中的体会是上下文管理这件事七分靠设计三分靠调参。设计阶段想清楚“什么信息值得留、以什么形式留、什么时候召回”比后面反复调阈值有用得多。另外别指望一次做到位先上一个能跑的简单版本观察真实对话里的问题再逐步加摘要、加检索、加结构化记忆。每加一层都要用数据验证它到底有没有带来提升而不是凭感觉堆功能。最后分享一个小技巧把每次调用的完整上下文存一份日志出问题时回放一遍你会发现自己对“模型到底看到了什么”的理解和实际情况经常差很远。