做AI应用开发,最头疼的问题之一就是大模型的"临时记忆"——上下文。你刚交代过的任务要求,多聊几轮之后模型就开始按自己的理解发挥;换个用户进来,上一轮对话的痕迹还残留在提示里,导致答非所问;同一个模型接口在不同场景下表现忽好忽坏,其实也不是模型抽风,大部分时候是上下文没有梳理好。
context-mode这个项目,核心就是把"上下文"从不可控的隐变量,变成一个可以显式定义、灵活切换、精确控制的对象。它不是一个具体的开源框架,而是一套我在多个项目里沉淀下来的组织方式和设计模式:把上下文按照会话、任务、检索、继承、隔离这五种模式进行管理,每种模式有自己的数据来源、组装方式和生命周期。做对话应用、AI客服、知识库问答、Agent类工具的同学,都能从这套思路里找到对应的落地方案。
以下是我在实际工程里反复调整后的完整拆解,以及可以直接照抄的实现结构。
1. 项目解读:context-mode到底在管什么
1.1 上下文失控的三个典型场景
我先说结论:大部分AI应用出问题,尤其是那种"功能看起来都对,但体验总差一口气"的问题,十个里有八个出在上下文管理上。这里我举三个我真实踩过的场景。
第一个是任务约束丢失。最早我做模拟项目X(一个智能客服助手)的时候,给系统提示里写了"回答必须控制在200字以内,不能编造数据"。前几轮对话效果很好,但用户追问两三次之后,模型就开始长篇大论,甚至把没有的数据也说得有鼻子有眼。排查到最后发现根本不是提示词写得不好,而是聊天历史的长度增长之后,最初的任务约束被稀释了,模型在后续生成时根本"看不到"那条指令。
第二个是场景串场。还是同一个系统,我后来加了一个"文档解读"功能,用户可以在对话框里粘贴合同片段让模型分析。问题来了:有的人在上一个文档还没结束的时候,又开始聊别的话题,结果模型一会儿分析合同、一会儿聊闲天,角色来回横跳。原因也很简单:不同场景需要的上下文是不一样的,但当时我没有对不同场景做隔离,所有内容全塞在一个上下文里。
第三个是信息超载。一个知识库问答系统,几千字的检索结果全量塞进提示词,模型可能确实能"记住",但生成的回答变得非常啰嗦,因为你塞进去了一大堆其实跟当前问题无关的段落,模型分不清主次。更麻烦的是token开销直线上升,钱花得快,速度还慢。
这三个问题都属于典型的"上下文失控"。临时记忆该记什么、不该记什么、按什么规则组织,这是context-mode要回答的核心问题。解决方案也不是单独调整提示词,而是把上下文管理作为一个独立中间层显式地设计它。
1.2 context-mode的设计目标与适用边界
这套模式的定位很明确:它是应用代码和模型接口之间的一个"上下文管理层"。应用层只负责把业务动作发过来,管理器负责从各种数据源收集信息、按模式组装上下文、裁剪压缩、再交给模型。业务侧不用关心最终提示词长什么样,只需要声明"当前处在什么模式"。
它解决的核心问题有三个:约束不被冲淡、场景不互相干扰、上下文长度可控。实现方式概括成一句话,就是"按模式定义上下文,按优先级组装,按规则切换"。
适用边界我需要强调一下:它不是"提示词工程"的替代品。提示词工程解决的是"同一份上下文里,怎么把指令写得让模型更听话"的问题;context-mode解决的是"哪些信息该进入上下文、哪些不该进、怎么进去"的问题。两者是上下游关系。如果你的场景只是简单的一次性问答,用一个system prompt就能搞定,那没必要上这套东西;只有对话轮次多、知识来源杂、场景切换频繁的应用,这套模式才能真正体现价值。
2. 五种核心上下文模式怎么选型
这一章直接给结论:在多数业务场景里,把上下文拆成五种模式基本就够用了——会话模式、任务模式、检索模式、继承模式、隔离模式。下面逐个说明。
2.1 会话模式:对话内的短期记忆
会话模式对应的是多轮对话本身。它记录的是"刚才说了什么",包括用户消息、助手回复、中间产生的临时状态。这是最基础的模式,没有它,模型就是个每次回复都"失忆"的接口。
设计会话模式时最容易犯的错,是只存文本不存结构化信息。我一开始也是把所有历史消息拼成一个长字符串,后来发现真正需要的时候根本没法精细化处理——比如想单独取出用户上一条消息做意图判断,就得从一大段文本里找。后来我改成用列表存结构:每次交互存一条记录,每条记录带上role、content、timestamp,以及自己加的round_id。这样无论是做裁剪、做摘要、还是定位某一条关键信息,都很方便。
另一个关键参数是会话长度的阈值。模型接口的上下文窗口是有限的,会话模式不可能无限制增长。我一般按token数设定阈值,超过阈值就触发压缩策略(这个在第四章展开)。阈值怎么定?可以粗略按"最大上下文窗口的70%"来计算:留出30%给任务模式、检索模式等其他模块。举个例子,如果窗口是8k,那会话模式最多占5.6k左右,剩下2.4k给任务指令和检索结果。
2.2 任务模式:压住模型的"行为基调"
任务模式对应的是"模型在本次对话里应该怎么表现"的指令层。它固定了角色、约束、回答风格、输出格式这些不随对话内容变化的东西。这一层就是把原来散落在system prompt里的公共指令抽出来,独立成一个稳定的上下文块。
之所以要单独抽出来,是因为它会和会话模式产生冲突:如果任务指令混在很长的对话历史里,前面说过,会被稀释。我见过一个极端案例,某开发者的机器人助手,任务要求是"无论用户说什么都要保持冷静和礼貌",但因为任务文本排在几千字的历史之后,模型在某次激烈对话中直接模仿用户的语气开始回怼。单独抽出来以后,我把任务模式块的优先级设为最高,每次组装上下文时固定放在最前面,这个问题就再没出现过。
任务模式还有一个容易被忽略的作用:它适合放"动态参数"。比如当前时间、用户姓名、所在页面、是否需要联网等。这些信息每次请求都会变化,但又不算对话历史,放在任务模式里作为系统级动态上下文,非常合适。
2.3 检索模式:外部知识补全
检索模式是知识库问答、文档助手类应用的核心。它的作用是把外部检索回来的相关信息,作为上下文片段注入给模型。注意我这里说的是"检索到的结果",不是"整个知识库"——这是设计上很重要的一个点:知识库是数据层,检索模式是上下文层,两个不能混。
检索模式的难点在于"注入的粒度"。全量塞进去,信息交叉污染严重;只给Top1,有时候答案又不够。我实操下来的经验是:按段落粒度注入,单段控制在300到500字以内,一次最多注入3到5段,并且每段要带上来源标识(比如"来源:文档A,第3节")。一个隐蔽的好处是,带上来源标识之后,模型在回答中会更倾向于引用对应来源,编造内容的概率明显下降。
另外检索模式需要设置检索结果与用户当前问题的相关性阈值。常见的做法是设置相似度下限,低于阈值的段落干脆不注入。很多问答系统体验差,不是检索不到,而是把一堆相关性都在0.6左右的模糊内容塞进去,反而干扰了模型对正确答案的判断。
2.4 继承模式:跨会话的经验复用
继承模式解决的问题是"下次对话还想用上次的结论"。用户和AI聊了一轮,产出了一个重要结论(比如"这个项目采用方案B"),下次新会话里,如果没有继承模式,模型完全不记得。继承模式就是把那些关键结论从上一个会话中提取出来,打包下沉,在新会话启动时自动加载。
实现上我通常给关键信息做一个"沉淀标记"。在会话过程中,用规则或小模型识别哪些内容是"需要长期记住的结论",一旦命中就写入一个持久化的记忆存储。下次新会话启动时,管理器从记忆存储里捞取与当前任务相关的结论,作为继承模式块注入。
这里有一个实用的坑:继承信息不要全量注入。很多人把"记忆"理解成"把所有历史结论全塞进去",结果几十个结论堆在一起,模型反而不知道当前该重点参考哪个。我的做法是限制继承模式最多注入3条、每条一两句话,且必须与当前会话主题强相关。
2.5 隔离模式:多场景不串味
隔离模式解决的是开头提到的场景串场问题。它的设计思路一句话就能说清:不同场景(domain)使用独立的上下文空间,切换场景时旧场景的上下文可以部分保留或归档,但不参与新场景的组装。
典型的落地场景是"文档分析+自由聊天"混合的对话框。用户在分析模式下讨论一份合同,切到自由聊天模式后系统应该把合同相关上下文归档,而不是继续让模型带着合同的上下文聊其他话题。实现上就是在上下文中加一个scene_id字段,组装上下文时只取当前scene_id下的块。
隔离模式和继承模式经常配合使用:隔离保证当前场景不被其他场景污染;但如果你希望某个结论跨场景传递,就显式地把信息从隔离空间中提升到继承模式。这个"显式提升"的操作很重要——它要求你在设计业务逻辑的时候就明确哪些信息可以跨场景,而不是让系统自动乱猜。
3. 核心实现:一个可运行的上下文管理器
光讲概念没用,这一章给出一个可以直接落地的最小实现框架。我以Python为例,但思路本身和语言无关。
3.1 整体架构与调用流程
管理器放在应用代码和模型接口调用之间。假设原来你的代码是这样调用模型的:
resp = model_client.chat(messages=[{"role": "system", "content": system_text}, ...历史消息...])改造之后变成两步。第一步,应用侧声明会话和模式:
manager.start_session(session_id="s_001", scene="customer_service") manager.switch_mode("task") manager.set_task_params(agent_name="小助理", tone="professional")第二步,应用侧发业务动作,管理器自动组装完整上下文,然后调用模型:
manager.add_user_message("你们产品的退款周期是多久?") reply = manager.get_reply()这样做的好处是业务代码里再也不用拼装字符串提示词了,所有上下文组装逻辑收拢到管理器内部。出了问题,只需要调试管理器,不用满项目去找散落的prompt拼接代码。
调用流程拆解下来是这样一个环:动作进入管理器 -> 按当前模式收集上下文块 -> 按优先级排序 -> 执行压缩(若超限) -> 拼成最终消息 -> 调模型 -> 收到回复 -> 归档回会话记录。
3.2 上下文对象的结构设计
上下文块是最小单元,我的经验是至少包含下面几个字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| mode | str | 属于哪种模式(session/task/retrieval/inherit/isolate) |
| content | str | 实际文本内容 |
| priority | int | 组装时的优先级,值越大越靠前 |
| scene_id | str | 所属场景,隔离模式靠它区分 |
| timestamp | float | 写入时间,用于排序和过期淘汰 |
| token_estimate | int | 预估token数,用于压缩判断 |
| source | str | 来源标识,如"user_msg"/"doc_retrieval"/"rule" |
这里特别提醒:token_estimate不要用精确的tokenizer实时计算,性能扛不住。用字符数除以3或者0.75词数之类的粗估就行。我们只要知道大约多少token用于判断是否超限,没必要精确到个位数。
块的组装逻辑是这样:管理器维护一个blocks列表,新信息到达时append进去。准备调用模型时,执行_compile_context:
def _compile_context(self): # 按场景过滤 scene_blocks = [b for b in self.blocks if b.scene_id == self.current_scene] # 按优先级排序 scene_blocks.sort(key=lambda b: b.priority, reverse=True) # 逐块累加,控制总量 result = [] total = 0 for block in scene_blocks: if total + block.token_estimate > self.max_tokens: break result.append(block.content) total += block.token_estimate return "\n\n".join(result)这段代码很简单,但它体现出两个关键设计:按场景过滤保证了隔离;按优先级排序保证了任务指令这类高优先级内容不被会话历史挤掉。
3.3 模式切换与调度逻辑
模式切换的调度是整个管理器最容易出bug的地方。我建议的规则是:切换模式时不删除已有块,只改变当前活动模式的作用范围。也就是说,切换到任务模式后,会话模式的历史块仍然存在,只是组装时是否包含这些块由模式决定,而不是把历史清空。
具体到每种模式的路由规则,我设置了一个注册表:
mode_registry = { "task": {"build": build_task_block, "scope": "global"}, "session": {"build": build_session_block, "scope": "scene"}, "retrieval": {"build": build_retrieval_block, "scope": "scene"}, "inherit": {"build": build_inherit_block, "scope": "global"}, "isolate": {"build": build_isolate_block, "scope": "scene"}, }scope字段区分"全局块"和"场景块"。全局块(task、inherit)在所有场景下都参与组装,场景块(session、retrieval、isolate)只在本场景下参与。切换场景时,场景块不再参与组装;切换模式时,真正变的是路由逻辑会去构建哪些新块,以及哪些块被激活参与组装。
一个很容易踩的坑:模式切换时丢失上下文。比如用户从"文档分析"切回"自由聊天",你把分析模式的块全部删了,然后用户说"刚才那份合同的有效期是多久",模型就答不上来。正确做法是归档而不是删除——分析模式的块仍然保存在存储里,只是不在当前上下文中参与组装;如果业务需要,可以随时通过检索或者显式回退再唤回来。
4. 上下文压缩:让长对话不"失忆"的几种手段
上下文压缩是context-mode里最后一道关键工序。无论模式划分得多好,长期对话累计的信息总会涨到超限。这里给出三种我在实战中验证过的手段,以及它们的取舍。
4.1 滑动窗口:简单但粗暴
滑动窗口的思路是只保留最近N轮对话,更早的直接丢弃。实现最简单,开销也最低,但缺点明显:较早的、可能仍是重要信息的内容会被无差别丢弃。比如用户在第3轮说过"我的预算上限是5万",聊了30轮之后这个约束就被窗口挤掉了,模型后面会按无此约束的方式回答。
滑动窗口适合那些对话主题变化快、历史依赖很弱的场景,比如闲聊、轻量咨询。窗口大小一般按轮数或token数设定:轮数通常7到15轮比较合适;token数则要和max_tokens联动,通常设在max_tokens的40%到50%。
4.2 摘要压缩:保留骨架
摘要压缩是把历史消息用模型或算法压缩成一段概述。比如"用户咨询了退款流程,要求提供书面说明,客服已承诺48小时内回复"。它解决了滑动窗口丢信息的问题,但也引入新问题:摘要会损失细节。
实操上我推荐一个折中方案:会话最近3轮保留完整消息,更早的历史做成摘要,每5轮再对摘要做一次二次压缩。这样既保住了最近对话的即时性,又让早期内容不会无限膨胀。摘要本身也是上下文块,按优先级放在会话块之后。
4.3 关键信息提取:结构化记忆
关键信息提取比摘要更进一步:不是保留"大概讲了什么",而是提炼出"必须记住的事实"。它可以是一组键值对:预算=5万、退款周期=48小时、用户身份=企业客户。结构化之后,这些信息就不再是一段需要重新理解的文本,而是可以直接作为条件参与后续决策的硬约束。
用大模型做信息提取,会多一次模型调用,成本和时间都要算进预算。我的实测建议是:不用每次都提取,而是设一个触发条件。比如每满3轮或检测到用户表达了某种"结论性语句"时,才执行一次提取。这样既控制了成本,又能及时把重要结论固化下来。
4.4 三种手段的实测取舍
我自己在项目里实测过一组对比数据,能直观看出三者的区别:
| 手段 | 上下文保持能力 | 成本与耗时 | 适用场景 |
|---|---|---|---|
| 滑动窗口 | 低 | 极低 | 闲聊、轮次少 |
| 摘要压缩 | 中 | 中 | 客服对话、长篇交互 |
| 关键信息提取 | 高 | 高 | 合同分析、决策支持类 |
我现在的默认策略是组合拳:最近3轮原文保留 + 早前历史摘要 + 关键结论结构化,三层叠加。实测下来,在max_tokens为8k的场景里,能把有效对话延续到30轮以上,而且早期关键信息(如预算、时间约束)的保真度从不到40%提升到了接近90%。
5. 典型问题排查与避坑经验
最后一部分,把我在实际调试过程中踩过的坑整理成一份速查表。有几个问题特别隐蔽,如果你正在调试类似系统,直接对照排查。
5.1 模式定义太多反而互相干扰
我见过有人把模式拆到十多个,结果组装上下文时不仅没好处,反而因为各模式块之间内容重复,模型被互相矛盾的信息弄糊涂。经验是:模式数量控制在5个左右,且每个模式要有明确的功能边界,切忌为了"架构好看"而拆。
排查方法:在配置里打开调试开关,打印每次组装后的完整上下文,人工扫一眼哪些块是冗余的。冗余的块删掉,比调整提示词有效得多。
5.2 压缩后模型"前后矛盾"
压缩本身不会让模型变傻,但压缩策略如果丢了关键约束,模型就会在后续回复里和之前"打架"。比如前面承诺了"周一给结果",压缩后这条信息没了,模型在回答时可能会说"可以尽快,具体时间看情况"。
我的解法很朴素:压缩前把"硬约束类信息"(时间、金额、承诺、身份)单独抽出来,不管怎么压缩,这些键值对始终保留在任务模式或继承模式里,不参与摘要压缩的丢弃范围。相当于给这些信息开了"免死金牌"。
5.3 上下文构建耗时严重
上下文管理器如果每个请求都做检索、做提取、做压缩,延迟会非常难看。我实测发现最耗时的是关键信息提取和外部检索。优化手段有三个:缓存、异步、预构建。
缓存:同一场景、同一会话的检索结果可以按问题哈希缓存,相同问题直接命中。异步:关键信息提取不阻塞用户回复,可以在生成回复的同时后台异步做,下一轮再使用。预构建:对任务模式和继承模式这种高稳定性的块,提前拼好存起来,每次请求直接复用,只有会话模式和检索模式需要动态构建。
5.4 敏感信息泄漏风险
隔离模式做得不好,最严重的后果是信息泄漏。我在项目早期犯过一个错:两个用户共用一个场景ID,导致用户A的对话内容被用户B的请求检索到了。虽然只是测试环境,但当场吓出一身冷汗。
排查建议:场景ID一定要绑定到会话源头,不能全局复用;每次组装上下文前,检查所有块的scene_id是否都等于当前场景ID;涉及用户隐私的数据,在进入上下文之前做脱敏处理。这些不是性能问题,是底线问题,优先级最高。
再提醒一个细节:模型输出也可能泄漏上下文。如果系统允许用户分享对话记录,分享出去的记录可能会带上其他场景的检索内容。做分享功能时要把隔离模式的范围一起带过去,或者干脆只导出当前场景的可分享内容。
写到最后,我个人的体会是:context-mode这套方法不是一次设计出来的,而是被真实业务里一个个问题推着迭代出来的。刚开始我也觉得"不就拼个上下文吗",直到对话一长、场景一多、约束一复杂,各种奇怪的表现都冒出来,才意识到上下文管理本身值得用一个中间层来专门治理。如果你正在被"模型答非所问""上下文越聊越乱"之类的问题折磨,不妨先别急着改提示词,试试把这五类模式梳理清楚。模式理清了,很多问题会自己消失。