做AI工具落地这一年多,我几乎每天都在跟"上下文"这个词打交道。不管是接大模型API、调AI编程助手,还是搭企业内部的知识库问答系统,最后发现性能瓶颈往往不在模型本身,而在上下文的管理方式。网上把这个话题搜出来,热词就挂在"context-mode"上,但真正能讲清楚它在做什么、怎么设计、有哪些坑的内容,其实少得可怜。
这篇文章我想用自己做过的几个项目当例子,把context-mode这件事拆开揉碎。它本质上是系统对"当前对话/任务环境"的感知与切换策略,核心就三件事:我该记住什么、我该忘掉什么、我该以什么状态响应。适合正在做LLM应用、写AI Agent、搭自动化工作流的朋友,哪怕你只是重度使用AI助手的普通用户,搞懂这个概念也能帮你少走很多弯路。
1. 先理清楚context-mode到底是什么
1.1 它不是一个新词,只是以前没人这么叫
context-mode,直译就是"上下文模式"。我第一次听到这词的时候觉得挺唬人,后来琢磨透了发现,其实我们早就在用类似的概念,只不过没给它起这么个名字。
举一个特别生活化的例子。你去咖啡馆点单,前面排了十个人,店员肯定不会挨个问"您要点什么口味、什么温度、什么杯型、什么甜度",而是看一眼你这个人的状态来切换询问方式。熟客来了,直接问"老样子?";新客来了,才详细解释菜单差异和推荐理由;赶时间的上班族,会优先推荐能快速出杯的品类。这种根据"现场情况"自动调整交流方式的能力,就是最朴素的context-mode。
放到软件系统里,context-mode就是一套状态管理策略。它决定了系统在某一时刻怎么理解输入的信号、保留哪些历史信息、采用哪种行为模式来产出输出。拿大模型对话来说,上下文窗口里装什么、不装什么、按什么规则更新,这就是context-mode要管的范畴。
1.2 为什么2024年之后这个词突然火了起来
原因是多方面的,但最直接的一个就是大模型应用从"能聊"走向了"能用"。纯粹的天儿聊式对话,上下文要求不高,把最近的几轮消息塞进去就行。但一旦牵涉到工具调用、多步骤任务、代码库理解、业务流程执行,上下文的管理就直接决定了系统能不能稳定完成任务。
我去年接过一个企业内部的文档问答项目,初期效果非常差。客户反馈说:"上午聊得好好的,下午再问同一个问题,它好像全忘了。"后来排查发现,会话的上下文被整体截断,只保留了最近两千字,前面沉淀下来的用户偏好、项目背景、之前确认过的条件全部被丢掉,系统被迫从头理解。这其实就是典型的"没有设计好context-mode"导致的翻车现场。
另一个推力来自模型能力本身。上下文窗口越做越大,从最早的几千token到现在的几十万甚至上百万,看起来好像不需要再纠结上下文管理了——反正都能装下。但用过的人都知道,窗口大不等于效果好。模型对窗口中间部分信息的注意力天然偏弱,业内俗称"lost in the middle",你把一百万字全丢进去,模型真正关注到的可能还是开头和结尾那部分。所以就算窗口再大,你还是需要一套机制来决定"当前这个任务,我应该聚焦哪块上下文"。
2. 核心设计思路:三种主流的上下文模式实现方案
要么不做,要做就做全套。我梳理下来,业内主流的context-mode实现基本可以归纳成三种:滑动窗口模式、摘要递归模式、检索增强模式。
2.1 滑动窗口模式:最简单,也最容易翻车
滑动窗口的核心逻辑特别好理解:会话过程中,永远只保留最近N条消息或最近M个token,超出部分直接丢掉。
这个模式我在最早期的客服机器人项目里用过。当时的设定是窗口大小为20条消息,每产生一轮新的对话,最老的一条就被挤出去。好处很明显,实现起来几乎不费脑子,token消耗可控,接口响应速度稳定。
但它的毛病也同样明显。当任务依赖早期信息时,窗口一滑动就把关键上下文冲走了。举个真实场景,用户说"帮我查一下上个月的报销单",系统查完回复了;过了一会儿用户又说"第二张单子的金额帮我改一下"。如果中间插入了十几条其他消息,最早关于"查报销单"的上下文可能已经被挤出窗口。模型再听到"第二张单子"时,根本不知道指的是哪张。
所以说,滑动窗口只适合短任务、单轮交互、或者上下文之间耦合度不高的场景。但凡你的任务链条稍微长一点,纯滑动窗口就是给自己埋雷。
2.2 摘要递归模式:牺牲细节换全局记忆
为了弥补滑动窗口"丢掉过去"的问题,很多人会想到:既然留不下全文,那我留个摘要总行吧。这就是摘要递归模式。
它的玩法和"读书划重点"很像。每轮对话结束后,系统调用一次模型,把已经发生的对话内容压缩成一段摘要,然后在下一次请求时,把摘要+最近几轮原文一起送给模型。再往后,新的对话又产生,系统再对"旧摘要+新对话"做一次压缩,周而复始。
这种模式我用在一个项目进度管理的Agent上,跑起来效果确实比纯滑动窗口好很多。系统能在跨天对话后仍然记得项目的阶段目标、关键决策和待办事项,因为这些都被摘要层捕捉住了。
不过要提醒的是,摘要的代价是信息有损。压缩过程中必然捡芝麻丢西瓜,如果摘要策略写得不够好,经常会漏掉用户埋下的细节。比如用户强调过"预算不能超过三万",这条信息在原始对话里随处可见,但到了摘要里可能就被一句"讨论了预算问题"给带过去了。等执行到具体环节,模型已经感知不到硬性约束。
2.3 检索增强模式:该记的永远在场
第三种模式是目前我在生产项目中用得最多的,也是效果最稳定的——检索增强模式。它的思路是:不是把所有历史都塞进上下文,而是根据当前这轮输入,动态检索出最相关的历史片段,再拼装成上下文。
打个比方,这就像你手里有一本厚厚的项目档案,你不需要每次把它整本背下来,只需要在客户问某个问题的时候,快速翻到对应的那一页。检索增强模式做的就是这个"翻页"的动作。
具体落地时,通常会把历史的对话切成片段、做向量化,存进向量数据库。每来一个新问题,先对问题做向量化,然后做相似度检索,把得分最高的几个历史片段捞出来,拼在系统提示词后面,交给模型生成。
这套方案最明显的好处是,不受窗口大小限制,理论上你需要记住多少就能记住多少,因为记住的内容都在数据库里,上下文里只需要带着"当前问题最相关的线索"。我在做法律咨询问答系统的时候,用它处理过上千轮的对话历史,模型依然能准确调用第一周聊过的关键证据信息,这在另外两种模式下是做不到的。
2.4 三种模式怎么选,我给你的判断标准
每次分享到这里都有朋友问我,到底哪种模式最好。答案是看场景,没有银弹。我的经验判断标准如下:
如果任务流程短、上下文依赖弱,直接用滑动窗口,别做多余的事。省钱省时省心。如果任务流程长、但核心信息相对稳定,做摘要递归,兼顾记忆和成本。如果任务是开放式探索型的,用户可能问到任何一个历史节点上的内容,直接上检索增强,别犹豫。
另外它们也不是互斥的,完全可以组合用。我最近的一个项目中,就是滑动窗口兜底、摘要做中期记忆、向量检索做长期记忆,三层层叠。效果比单用一种模式稳定得多。
3. 实操指南:给系统设计一个可用的context-mode
说完了理念,我们直接上手实操。这一节我会带你过一遍,如果要从零设计一套context-mode,到底该走哪些步骤,每一步的决策依据是什么。
3.1 第一步:定义你的"上下文边界"
这是我认为最重要、但最容易被人忽略的一步。很多人在设计上下文模式的时候,第一反应是"我用多大的窗口""我用什么模型来压缩摘要",这些当然要管,但都不是最优先的。最先要回答的问题是:这个系统里,什么叫一条"独立的上下文"?
我举个例子你就明白了。假设你做一个在线教育助教,学生可以一门课一个会话地提问。如果系统把"高等数学的提问"和"英语口语练习的提问"混在同一个上下文模式里,那不管用什么技术策略,模型都会两头犯糊涂。因为这两类任务的语境完全不同,需要的背景知识也完全不同。
所以我在项目启动时,都会拉着产品团队先做一轮"上下文边界梳理"——你希望系统在什么时候"重新开始"?按用户划分?按项目划分?按任务类型划分?还是按时间段划分?这个边界的颗粒度,直接决定了上下文模式的设计复杂度。边界越细,模式越容易做精准;边界越粗,管理难度越大。
如果实在没有头绪,我一般建议先按"用户+任务类型"来切分。这个方案对大多数工具类应用都适用,既不会细分到难以维护,也不会粗放到上下文互相干扰。
3.2 第二步:设计上下文的切换与触发机制
context-mode这个"mode"的另一个关键含义,就是切换。系统需要有能力判断当前处于什么场景、应该启用什么上下文策略,并且在合适的时候完成切换。
我之前在写一个自动化测试工具时,设计过两种模式:调试模式和执行模式。调试模式要求上下文里包含完整的测试步骤、参数变更记录、历史报错信息,方便模型理解问题;执行模式只要求上下文包含当前测试任务的必要参数,其他历史一概不带,保证响应快速、准确。
切换的触发条件我设置了两类。一类是显式触发,用户明确说"开始执行"或者"进入调试",系统就切换上下文配置;另一类是隐式触发,系统根据当前对话内容和任务状态自动判断。比如连续三轮都在讨论同一个报错,就自动切到调试模式;用户转而询问任务进度了,就自动切回执行模式。
现在回头看,隐式触发是这套设计里工作量最大的一块,因为你需要定义"什么样的信号意味着场景变了"。我的经验是从小处做起:先列出三种你最确信的信号(比如关键词、意图分类、工具调用结果),把它们做成规则,跑一段时间后,再用数据去补充新的触发条件。一上来就想做一个万能场景识别器,大概率会把自己累死还做不成。
3.3 第三步:上下文持久化与恢复
为什么要把这一步单独拎出来讲?因为太多系统挂着context-mode的名头,其实只做了内存态的管理,程序一重启,上下文全没了。
用户不会管你的服务什么时候重启过,他只知道自己上周聊着聊着,这周再打开,你最好还记得上周聊到哪儿了。特别是企业内部的知识库问答、行政服务机器人、项目管理助手这些场景,跨会话记忆几乎是刚需。
做法上说白了也简单,就是给每个上下文会话分配一个固定的ID,然后把这个ID对应的全部上下文状态(摘要、向量索引、原始消息、当前模式标识)存到数据库里。每次新的交互进来,先按ID把上下文拉起来,恢复当时的状态,再继续往后走。
这里面一个比较隐蔽的坑是,上下文恢复不等于消息回放。系统恢复的应该是"已经被模式处理过的状态",而不是一股脑把原始历史全翻出来。举个例子,如果用摘要递归模式,持久化的核心是那份摘要和最后几轮原文;如果用检索增强模式,持久化的是向量库里的切片和索引元数据。如果你把整段历史全部恢复到上下文中,那持久化就没意义了,一切还是会被窗口截断。
4. 关键参数计算:你的context-mode到底能装多少东西
很多人设计上下文模式时,脑子里是没数的。"感觉够用""大概这么大吧"是特别危险的说法。因为你设置的窗口大小、摘要长度、检索条数,最终都直接决定模型性能、响应速度和接口费用。这一节,我分享一套我经常用的参数测算方法。
4.1 先把token的概念盘明白
你要管上下文,就必须知道token是什么概念。你可以把它粗略理解为"模型看到文本的碎片单位",中文场景下,平均一个汉字约等于1到1.5个token,英文一个单词大概是1.3个token左右,标点符号、格式空格也都要计入。
不同模型计费方式不同,但基本都是按token走。打个比方,如果你的模型上下文窗口是128K token,翻译成中文大概是8万到10万个汉字,相当于一部中篇小说的体量。看起来很大,对吧?但别高兴太早,系统提示词要占、历史信息要占、用户问题要占、模型预留输出空间也得占,真正留给"有效推理"的空间并没有想象中那么充裕。
我的习惯是:先把输出预留空间固定下来。假设模型最大支持4096 token的输出,那就最早给输出留出4096的预算,剩下的空间再进行上下文分配。
4.2 上下文窗口的分配比例该怎么定
拿一个128K窗口的模型来举例,我会这样分配:
系统提示词一般控制在2K token以内,把角色设定、任务规则、输出格式写清楚,这部分是每次请求都要带的。长期记忆层如果做摘要,控制在8K以内,太多摘要本身就会挤压有效注意力。短期记忆保留最近20轮对话,大约10K到15K token,用于保证对话连续性。检索增强捞出来的历史片段控制在10K以内,每次按相关性排序取Top 5到Top 10条。
剩下的空间全部留给用户本次输入和最终输出。
这样一套分配下来,整个上下文里真正"主动管理"的部分大概在30K到35K之间,剩下的是即时计算的部分。有了这样的预算清单,你再回头看那些"窗口不够用"的抱怨,大多数其实不是模型窗口小,而是系统设计时没有做预算管理,任由冗余信息塞满了上下文。
4.3 费用与延迟的预估公式
做技术方案永远躲不开钱和时间。上下文模式的设计对这两项的影响是决定性的。
费用方面,一次API请求的总费用大致等于输入token数乘以输入单价,加上输出token数乘以输出单价。如果你的上下文窗口每次请求都带着30K token进去,那单次请求的费用就已经是纯短对话的好几倍。所以在生产环境上线前,一定要先算清你的上下文平均体积,再乘以预期的调用量,估算出月成本,否则做出来的功能再好,老板看到账单也开心不起来。
延迟方面同样如此。模型处理输入的时间跟输入长度强相关,上下文越长,首字延迟越高。如果你的业务对响应速度有要求,比如实时客服、在线编程助手,那你就必须压制单次请求的上下文体量。我自己定的一个经验红线是:单次请求上下文不超过20K token,首字延迟控制在两秒以内。超出这个范围,就该考虑优化上下文模式了,而不是让用户等着。
5. 常见问题与排查技巧实录
这部分是我最想写的,因为网上理论讲得一套一套的,落地时踩的坑却很少有人说。我把这一年多来在context-mode相关的项目里遇到的典型问题整理如下,每一条都是真实发生过的。
5.1 问题一:明明有历史记录,但模型就是"忘了"
现象表现:对话里之前明确说过的信息,换个话题再聊回来,模型接不上。
首先排查的不是模型,而是你的上下文模式到底把什么放进了请求里。很多系统的"历史记录"存在数据库里,但并没有进入每次请求的上下文中。这种情况最常见于用了滑动窗口模式的系统——历史确实还在,但早被滑出窗口了。
解决办法分两类。如果确认需要用到更早的信息,就得升级上下文策略,走摘要递归或者检索增强。如果只是偶尔需要,那可以做一个"关键词唤醒"机制,当用户提到之前的某个主题时,系统临时把相关历史重新注入上下文。我曾经在这个问题上卡了整整一周,最后发现只是窗口调太小了,所有方案里最笨的一个。
另外一个特别隐蔽的原因:系统提示词太长了。有些默认指令写得冗长无比,占掉了大量上下文空间,导致真正有效的历史信息挤不进来。排查方式很简单,把提示词精简,看看同样的历史能不能被模型正确引用。能,说明问题出在上下文分配比例上。
5.2 问题二:上下文里塞了大量无关内容,干扰判断
现象表现:模型回复变得"又臭又长",抓不住重点,甚至被无关历史带偏。
这个问题的根源通常是对"检索增强"的使用姿势不对。我只按相似度取Top N条,但语义相似不等于任务相关。用户问"预算怎么审批",检索出来的历史片段可能全是跟"预算"两个字有关但完全不涉及"审批流程"的内容,反而把真正相关的"审批时间节点"信息挤掉了。
我现在的做法是,检索之后加一个相关性过滤层。拿一个问题生成的多组检索结果中,先让规则做一轮粗筛,比如按实体匹配、按时间优先级、按消息类型过滤,再做语义排序,最后才决定放哪些片段进上下文。
此外,给每个历史片段打标签这种事,前期看起来麻烦,后期收益极大。比如"用户明确决策""用户提供背景信息""系统输出结果""用户表达情绪",不同类型的信息在不同场景下的权重是不同的。带上标签,上下文组装时能做到按需取用,精确得多。
5.3 问题三:上下文越长,响应越慢,费用越高
现象表现:系统功能越做越多,但响应速度越来越慢,账单也越来越吓人。
一个必须正视的现实是,上下文模式和业务功能的复杂度往往成正比。你每加一个功能,上下文里就多一堆相关的背景设定和历史信息。不做控制的话,系统很快就会被拖垮。
我的解决方案是第一,建立上下文预算制度,每次新功能上线前评估它会给单次请求增加多少token,超预算就砍需求或者优化方案。第二,做分层调度,不同的请求走不同的上下文配置,低复杂度的请求直接走瘦身版上下文,不把所有能力都挂在同一个对话上。第三,启用缓存机制,对于高频且稳定的系统提示词和固定知识片段,充分利用厂商提供的缓存功能,能省下不少重复计费。
5.4 问题四:多用户共用一套上下文模式,互相串味
现象表现:用户A聊的内容,用户B在提问时,系统居然引用了A的历史。
这个问题几乎只出现在设计阶段偷懒的项目里。偷懒点是,把所有用户的会话消息都存在同一个全局上下文里,不做隔离。一旦切换了mode或者筛选条件写得不对,就会查出了别人的历史记录。
至少要做两层隔离。读写隔离是基础,用户在写入时只写自己的会话,在读取时只读自己的会话,这靠会话ID和用户ID双重校验即可。上下文策略隔离相对深一层,就是上文说的,不同的用户场景可能还需要搭配不同的上下文模式。企业内部用得比较多的是,管理层用户要的是全景摘要模式,执行层用户要的是精准任务模式,二者基于同一份数据,但组装上下文的逻辑完全不同。模式不隔离,效率就上不去。
到这里,我没有打算写一个面面俱到的总结,因为context-mode这件事还在快速演化中,今天觉得对的设计,明天可能就会被模型能力更新给推翻。我个人在实际操作中的体会是:别迷信某一种模式,也别指望有一套配置能适用所有场景,最靠谱的做法是在自己的业务里先把问题定义清楚,再小步快跑,持续用真实数据来调优上下文策略。套用那句老话——上下文管理的本质,不是技术问题,而是取舍问题。想清楚该记住什么,比能记住多少重要得多。