
1. 上下文模式到底管的是什么从Token预算说起1.1 为什么上下文长度不等于记忆力先聊一个我经常在开发者社群里看到的现象有人把模型上下文参数直接拉满比如把max_tokens或者窗口配置设成 32K、128K然后觉得“既然模型都能记住这么多内容那我随便往里塞就行了”。结果对话进行到后面模型开始前言不搭后语甚至把前面明确说过的事实给“忘”了。于是大家第一反应是“模型变笨了”第二反应是“是不是我的提示词写得不够好”。但我的经验是八成问题出在“上下文模式”这个词被理解得太浅了。所谓 context-mode不是简单地把一堆历史记录拼在一起丢给模型而是你在构建输入时到底用哪种方式组织这段文本、保留哪些信息、压缩哪些内容、以及如何让模型在有限窗口里高效地“值班”。上下文长度只是硬件意义上的窗口大小真正决定模型表现的是这个窗口里放的“货”是否合理。举个特别常见的例子你让模型帮你写一篇一万字的行业调研报告写到一半你发现它开始重复第四章里已经写过的段子甚至把第三章的观点安到第五章去。你去检查历史消息发现前面所有轮次的全文都还在上下文里。问题就在这里——模型不是记忆力差而是它的注意力被大量冗余内容稀释了。上下文模式要解决的核心矛盾就是“窗口有限”和“信息无限”之间的冲突。你不可能把整个项目的所有资料都塞进一次对话里所以你必须决定哪些内容必须完整保留哪些内容可以压缩成摘要哪些内容根本不该进入上下文。1.2 三种常见的context-mode构建姿态根据我做过的十几个实际项目最常见的上下文构造方式可以归结为三种姿态我习惯叫它们“流水账模式”“翻旧账模式”和“值班笔记模式”。流水账模式就是最原始的做法把所有历史对话按时间顺序全部保留每次请求都把用户问题、助手回答、用户反馈、助手修正……全部作为上下文传给模型。这种做法的好处是信息零丢失坏处是Token消耗巨大而且越往后模型越容易“看花了眼”。我见过一个客服机器人项目跑了三个月后单次请求的上下文已经超过 8000 Token其中 70% 是早期的寒暄和无意义修正真正有用的近期信息反而被挤在后面。翻旧账模式是流水账的改进版只保留最近 N 轮对话更早的内容丢掉。这个方案简单很多开源框架默认就是这么干的。但它有个隐蔽的问题——如果某个关键信息出现在第 2 轮而重新问起它是在第 40 轮中间隔着 37 轮无关闲聊采用滑动窗口后模型完全看不到那个信息只能回你一句“抱歉我不记得了”。翻旧账模式适合短会话场景比如一次性问答、单轮翻译但绝对不适合长周期项目。值班笔记模式是我现在最推荐的一种思路。它的核心思想是每完成一轮有效对话我们就从这轮对话中提炼出一小段“笔记”笔记记录最关键的事实、决策、约束条件然后每次请求时把笔记作为上下文的固定组成部分再加上最近的对话历史一起传给模型。这个笔记不是人写的而是让模型自己总结或者用程序规则从结构化数据里提取。它就像是给模型配了一个值班交接本每次“交接”时只需要读取浓缩后的关键信息而不是把所有历史录音重播一遍。1.3 动手算一下你自己的Token预算很多人忽略一件事上下文窗口的大小和你真正能用的有效上下文之间是有差距的。原因在于你除了要放对话历史还要放系统提示词、工具定义、示例样本、用户当前输入这些都在“吃掉”窗口。所以投产之前应该先算一笔账。我分享一个简单公式可用历史长度 窗口总长度 - 系统提示词长度 - 工具定义长度 - 固定示例长度 - 当前输入长度 - 安全冗余安全冗余至少留 10% 到 20%。为什么因为有些模型会把max_tokens生成的最大输出长度也算进窗口预算如果你把窗口用完生成到一半就可能截断。我见过一个项目把上下文塞到接近 99%结果每次回答都只出开头几句就中断用户以为模型坏了其实是Token预算算错了。实际操作中我会先跑一个脚本把系统提示词、工具定义、示例样本逐项调用模型的 Tokenizer 进行统计然后根据业务峰值估算“当前输入长度”的最大值。假设你的模型窗口是 128K系统提示词 2K工具定义 5K示例样本 3K当前输入最长可能 20K那么留给历史的就只有128K - 2K - 5K - 3K - 20K - 15%约19.2K冗余≈ 78K这个 78K 才是你真正可以放心放历史内容的预算。很多团队直到线上出问题才意识到这一点但那时候已经晚了。所以第一个提醒就是在项目启动第一天就写一个 Token 预算计算脚本放在 CI 里每次改提示词或工具定义就跑一遍。2. 长会话场景下的上下文维护从截断到分层2.1 滑动窗口不是唯一解如果你的产品形态是“长对话”而不是“单次问答”比如 AI 写作助手、AI 代码助手、长期陪伴式聊天你很快会发现滑动窗口这个默认方案撑不住。滑动窗口的问题在于它一视同仁今天早上聊的午饭吃什么和你昨晚定下的架构选型决定在窗口看来都是“历史文本”时间到了就一起被挤出去。但业务上它们的重要性天差地别。我实际负责过的一个 AI 写作助手项目就是这样用户上午让模型帮拟了一份文章大纲下午接着写正文结果模型把上午定的大纲给忘了自己重新瞎编了一个方向。我们当时查了日志上午的大纲确实在 20 轮之前就被滑动窗口挤掉了。后来我们把方案改成“关键信息夹带”就是把那些标记为“长期有效”的信息单独提取出来每次请求都重新放在上下文最前面。效果立竿见影。这里的关键是你要为上下文设计分层结构而不是单纯地用“时间远近”来决定去留。我现在的做法是把上下文分成三层——固定层、工作层、临时层。固定层放永不变化的信息比如产品身份、用户核心偏好、项目硬性约束工作层放当前任务相关的资料、最近几轮的关键决策临时层才放那些聊完就算的对话内容比如“今天天气不错”“你帮我看看这段代码有没有 bug”。临时层可以随便用滑动窗口去截断工作层用轮数或者 Token 阈值来控制固定层则永远保留。这个思路看着简单但实际落地时有一个执行细节你需要在每次请求时让模型或者程序判断“这轮对话里有没有值得提升到工作层的新信息”。我建议用结构化输出来做这个动作而不是让模型自由发挥。2.2 分层摘要让模型自己维护“外脑”说到让模型自己维护摘要这就涉及一个我非常看重的实现方式定期摘要滚动。思路是这样的假设我们允许上下文里最多保留 20 轮详细对话当第 21 轮进来时我们把最早的 10 轮比如第 110 轮取出来调用一次模型生成一份 500 字以内的摘要然后把这 10 轮详细内容替换成摘要再把第 21 轮加到尾部。这样窗口总量被控制在恒定范围内但长期信息没有完全消失而是变成了摘要形态。这个方案听起来简单实际坑很多。第一个坑是摘要粒度。如果你每 5 轮就做一次摘要摘要会变得特别细碎反而占用大量 Token如果你每 50 轮才做一次中途模型就已经因为上下文过长而开始表现降级。我验证下来的经验是当历史轮数的 Token 总量超过窗口的 40% 时就应该触发下一次摘要滚动而不是死按轮数。第二个坑是摘要生成的上下文污染。你让模型生成摘要时必须给一个非常明确的输出模板例如请对以上对话生成一份工作摘要包含 1. 已确定的事实 2. 未决事项 3. 用户的偏好 4. 下一步计划 要求总字数不超过500字不要复述完整对话。如果不给模板模型可能会把摘要写成“用户问了 A我回答了 B用户又说 C……”这种流水账同样浪费 Token。给模板后摘要的质量会高很多而且后续模型基于摘要回忆信息时准确率明显高于直接读原始长对话。第三个坑是摘要的叠代准确性。如果长期对话持续几天第一轮摘要已经是对原始对话的压缩第二轮摘要又对第一轮摘要继续压缩信息丢失会逐级放大。所以我在长期项目里会给摘要加时间戳和版本号每次需要查找某个历史信息时如果摘要里找不到就回退到原始日志去检索。说白了上下文模式不是用来替代存储的它是用来给模型提供“当前最需要的背景信息”的不是所有信息都必须在上下文里。2.3 记录实验一种可复用的上下文协议分层摘要再往前走一步就是给对话建立“记录卡”。我管它叫 Context Record本质是一份结构化数据每次请求时把它序列化成 JSON 或者 YAML 放进上下文的固定位置。记录卡里通常有这些字段session_id: chat_20240516_xxx user_profile: name: 某用户 domain: 文案创作 preference: 口语化避免缩写 project_state: current_stage: 大纲阶段 confirmed_points: - 受众是25-35岁的创业者 - 全文字数控制在8000左右 open_questions: - 是否加入竞品案例 history_summary: last_updated_turn: 34 summary_text: 前34轮已完成需求访谈... recent_turns: - turn: 35 speaker: user content: 帮我调整第三章开头 - turn: 36 speaker: assistant content: 已调整...你可能会问搞这么结构化的东西有什么好处我的体会是它的价值有三个。第一排查问题时效率极高——模型回答不对你直接看记录卡里的confirmed_points立刻知道模型到底有没有记住关键约束。第二记录卡可以跨请求稳定存在即使对话窗口换了一个你也能用同一个卡牌恢复会话状态这一点对产品体验很重要。第三记录卡本身就是给上层业务用的“缓存键”后面我们要聊的上下文命中缓存也是基于这种结构化粒度来做的。但记录卡也有代价它本身要吃一部分 Token。好消息是一份设计良好的记录卡一般控制在 8001200 Token 之间相对于动不动就几千 Token 的原始历史性价比极高。我的原则是让程序自己维护记录卡的数据结构只在特定时机调用模型来更新summary_text字段其他字段用规则代码更新这样既可控又省钱。3. 检索增强场景下的上下文组装让context-mode服务于事实性3.1 把RAG结果编排成上下文的结构方法如果说长会话场景关注的是“时间维度上的上下文”那么 RAG检索增强生成场景关注的就是“空间维度上的上下文”——需要从知识库里找出相关资料然后拼成一段可供模型引用的输入。这个场景下context-mode 的核心问题从“怎么记住”变成了“怎么挑选和摆放”。很多初学者做 RAG 的时候把知识库返回的前五条片段一股脑丢给模型然后期望它给出正确答案。结果有时好有时坏用户问“2024年新规是什么”系统却把2020年的资料也检索出来还排在前面模型就被带偏了。这里的根源在于上下文里放了太多无关信息噪声直接淹没了信号。我验证下来比较有效的一种组装方式叫“三段式上下文结构”第一段是“任务锚点”用两三句话告诉模型“你现在要做的是基于以下资料回答用户问题如果资料中没有足够信息请明确说明”。第二段是“检索知识”按相关性降序排列每条知识前面标一个引用编号比如[1]、[2]。第三段是“用户指令”把用户这次的问题放在最后面并加上一句“请优先引用 [1] [2] 等编号对应的资料”。这样做的好处是模型在生成推理的时候能够通过编号把答案和资料来源绑定既降低了幻觉率也方便你做“可解释性”。不要小看这个排序上下文里资料的位置直接影响模型的注意力权重。我做过一个实验同一组资料打乱顺序给模型回答准确率能差 15% 以上。把最相关的资料放在最前面效果通常最好因为模型对开头部分的内容记忆更牢固。3.2 上下文重排与噪声抑制更进一步我们可以做“上下文重排”这是我从一个开源项目里学来的技巧检索回来的 Top-K 条资料不要直接按向量相似度排序而是先做一次过滤和重排。过滤包括去重、去掉与用户问句明显无关的段落、去掉互相矛盾的资料。重排则可以考虑用一个轻量级模型给每条资料打分或者用规则判断资料与问题的实体重叠度、时间时效性等信息。举个例子假设用户问“云服务器的带宽怎么选”知识库里检索出 10 条资料其中有一条讲的是“物理服务器的网卡配置”虽然语义相似度不低但实体完全不在一个维度。如果不做过滤这条噪声会挤占上下文分散模型注意力。我的做法是先把所有候选资料按“实体覆盖”过滤一遍问题的核心实体是“云服务器”“带宽”过滤规则要求资料中必须出现至少一个核心实体否则直接丢弃。这一步能去掉大约 20% 的明显噪声。过滤完之后如果发现候选资料目标太多比如仍然有 8 条每条 500 Token加起来 4000 Token上下文压力会大。这时候就要用摘要压缩把 8 条压缩成 3 条摘要每条保留关键事实和源引用。注意RAG 场景的摘要不能破坏事实细节比如“罚款金额是 500 元”不能压缩成“有罚款规定”否则模型就会因为信息不足而瞎编。我一般会让摘要模型使用“保留所有数字、日期、专有名词”的指令宁可摘要长一点也不能丢失关键事实。3.3 指令与上下文的边界什么时候该让模型“无视”资料一个非常反直觉的点是上下文模式不只是决定“放什么”还决定“让模型看什么、不看什么”。很多 RAG 项目的失败不是资料不够而是模型把资料当成了必须无条件遵循的“圣旨”。如果用户问的问题和资料里的内容发生冲突或者资料本身过时了模型往往会优先相信资料导致回答明显错误。我处理这个问题的方式是在系统提示词里加一段“资料质量判断规则”你将收到若干参考资料它们来自知识库但可能存在过时、错误或不完整的情况。 当用户问题与资料内容冲突时优先采用逻辑和常识判断并在回答中说明“知识库资料可能未更新”。 如果你认为资料与问题无关允许忽略它们只根据通用知识回答但必须声明“当前检索资料不足”。这条规则看起来简单但效果非常明显。加了这条之后我负责的客服问答系统里模型“硬抄”错误资料的比率下降了一半以上用户反馈也好了很多。核心原因是模型需要被明确告知它有“拒绝权”和“质疑权”否则默认情况下它会迎合你给的上下文。另外指令和上下文的位置也有讲究。我推荐把“资料质量判断规则”放在系统提示词末尾紧贴着输入资料的位置而不是放在最开头。因为模型读上下文是一个连续过程开头部分会被当成“大背景”而紧贴资料前的位置会被当成“对资料的直接解读指令”这个位置能让模型的注意重心落在“如何对待资料”上。4. 工程里的Context Mode落地缓存、命中和持续化4.1 前缀缓存上下文复用背后的经济账上下文模式的工程化不止是逻辑层面的设计还涉及成本优化。现在主流的 LLM API 都支持上下文缓存也就是“前缀缓存”机制。原理很简单如果多次请求的输入内容前缀完全一致平台可以缓存这部分输入的计算状态从而降低后续请求的价格和延迟。那这和 context-mode 有什么关系关系大了。如果你把固定层、工作层这些不变的上下文放在输入的开头把变化较大的临时层放在末尾那么大部分请求都能命中前缀缓存。反过来如果你傻乎乎地把每次不同的用户问题放在输入开头后面跟着历史记录那每次请求的前缀都是新的缓存几乎完全失效。所以工程上有一个很具体的建议先搭固定前缀再接可变内容。固定前缀包括系统提示词、工具定义、记录卡中不变的字段可变内容放在最后比如用户这次的问题、刚刚发生的对话轮次。我见过一个团队因为这个顺序调整直接把 API 账单砍了 40%延迟也降了差不多三分之一。不过要注意不同平台的缓存策略不同有些按字符精确匹配有些按 Token 规范化匹配。你需要在本地用 Tokenizer 确保字符串高度稳定——不要因为一个空格或者换行符的变化就让缓存命中失败。我的做法是把固定前缀字符串做成一个常量任何对提示词的修改都走版本发布流程避免跑着跑着悄悄变了点内容缓存全部失效。4.2 持久化会话上下文的数据结构设计如果你的服务需要支撑几百万用户每个用户都有长期会话那么上下文模式就不只是内存里的字符串操作它需要一个可靠的存储方案。我推荐用两张表来管理会话上下文第一张表叫session_context存放每个会话的“记录卡”和“固定层内容”字段大概是字段类型说明session_idstring会话唯一标识user_idstring用户标识fixed_system_prompttext固定系统提示词workspace_datajsonb记录卡中的结构化数据history_summarytext最新摘要文本summary_turn_idbigint摘要对应的最新轮次号updated_atdatetime更新时间第二张表叫session_turns存放原始对话轮次字段包括turn_id、session_id、role、content、created_at、extracted_fact_ids。每次请求时我们取session_context中的固定内容和记录卡再根据预算从session_turns里取最近若干轮次组装成上下文。当轮次过多时异步任务负责把最早的轮次做摘要并更新history_summary。这套设计的好处是上下文组装是一个可回溯、可审计的过程。如果模型出了严重错误工程师可以直接从数据库里复现当时的完整输入而不需要靠日志里的随机抽样。我甚至会把每次发送给模型的完整上下文串存在另一个表request_context_log里虽然占空间但排查问题的时候你会感谢这个决定。4.3 监控与告警上下文大小失控的几种征兆上下文模式跑久了系统一定会出问题——不是你应用逻辑的问题就是用户使用模式变了导致上下文快速增长。我总结出几个需要重点监控的指标上下文利用率单次请求的 Token 与窗口上限的比值。如果平均值超过 60%就要警惕因为突发流量可能很快打爆上限。摘要触发频率如果摘要任务每小时触发超过 N 次说明会话吞吐量大或者摘要粒度设置不合理需要检查。缓存命中率这是排查效率的窗口。缓存命中率突然下降八成是有人改了固定提示词却忘了通知团队或者你的上下文顺序出了问题。历史回退率用户重新问“我们之前不是说过……吗”的频率。如果这个指标升高说明我们的上下文模式没有保住关键信息摘要策略需要调整。我见过最离谱的一次事故是运营同学在后台直接把系统提示词里加了一整段 2000 字的营销活动说明没有走代码发布流程结果所有会话的上下文利用率瞬间逼近上限摘要任务疯狂触发很多用户反馈模型开始复读。好在缓存命中率监控及时发现异常我们用二分法定位到是提示词变更导致的问题。从那次以后我坚持任何提示词变更必须经过代码仓库并触发缓存预热。5. 我踩过的坑与现在的推荐配置5.1 无脑max token的代价早期我刚接触大模型应用时觉得上下文窗口越大越好于是把所有能开大的参数全开到最大。后果很快就来了模型回答质量急转直下。原因后来我知道了——长上下文的“注意力稀释”效应非常明显当输入序列超过一定长度后模型对中间部分的关注度会下降尤其是那些不是最近也不是开头的内容几乎形同虚设。所以现在我的经验是并非所有内容都需要送到模型面前能不放就不放能压缩就压缩。所谓 context-mode本质上是“上下文管理策略”不只是一个参数。我宁可让模型少看一些内容但保证它看到的都是高密度的有效信息。推荐配置可以这样起步如果你的模型窗口是 128K那么系统提示词控制在 2K 以内工具定义 3K 以内示例样本 2K 以内记录卡 1K 以内临时对话历史控制在总窗口的 30%约 38K检索资料控制在 15%约 19K输出预留 4K。剩余约 37% 作为动态缓冲。这个比例不是绝对标准你可以根据自己的业务调整但务必保证任何一类内容都不要超过预算的 50%。5.2 上下文溢出的隐形错误这个坑特别隐蔽你以为自己的输入没有超过窗口上限但实际上系统在拼接时已经悄悄超了然后报一个400 context_length_exceeded错误。很多团队直接把错误抛给用户说“对话太长请开新会话”但这是最差的体验。真实情况往往是你在代码里计算 Token 用的是模型 A 的 Tokenizer但实际调用的模型是模型 B 的最新版本Tokenizer 换了算法同一段话 Token 数变化 20% 到 30%。还有一种是你统计的是字符串长度但没把多字节字符、Emoji、Markdown 标记的额外 Token 算进去。我实测过一段 800 个字符的 Markdown 表格Token 可以达到 1200 以上比纯文本高出 30%。所以项目里一定要统一 Token 统计口径用标准 Tokenizer 脚本跑在一个固定样本集上每次模型升级后都要重跑对比。另外错误处理也要做成上下文模式的一部分。我建议在 API 调用层加一个三段式回退逻辑先尝试完整上下文如果报错自动把过期的工作层内容折叠成摘要再试一次如果还报错就丢弃临时层只保留固定层和记录卡。这个回退逻辑能大幅降低用户可见的失败率实测下来我们的报错率从 2.3% 降到了 0.4% 以内。5.3 一组可以直接抄的默认参数最后分享一组我目前在不同场景下比较稳定的默认配置你可以当作起点去调。场景窗口长对话RAG问答单轮工具调用固定层策略记录卡系统提示词系统提示词资料规则工具定义少量示例工作层策略最近20轮详细历史摘要检索Top-5重排后资料仅当前输入临时层策略最早轮次滚动丢弃不涉及不涉及摘要触发阈值历史Token超窗口40%不触发不触发缓存顺序固定前缀在前问题在后指令资料编号连续全部固定前缀需要注意的是这组参数是基于我在日常业务项目里的经验值它不是公式。每个项目的业务类型、用户交互频率、答案质量要求都不一样参数必须基于自己的日志数据去调。我的最终建议是把上下文模式当成一个持续迭代的模块而不是上线就完事的配置项。每次换模型、改提示词、调工具定义都应该重新审视一遍你的上下文预算、缓存命中率和用户反馈。我在实际项目里最大的体会是“上下文模式”这四个字表面上是个技术开关实际上是一套完整的工程体系。从 Token 预算的精确计算到长会话的分层摘要再到 RAG 场景的上下文重排每一步都在回答同一个问题在有限的窗口里如何让模型看到它最应该看的东西。如果看完了这篇分享你至少能把之前那个“把历史全塞进去然后祈祷模型记住”的版本升级成有结构、有缓存、有监控的工程方案那我们的功夫就没有白费。下次再遇到模型“失忆”别急着骂模型先看看你自己传给它的上下文是不是一团乱麻。