先问一个扎心的问题:你负责的AI应用,能不能稳定跑完30轮对话不乱套?我见过太多项目,demo阶段人见人夸,一上线就原形毕露——要么回答串台,要么聊着聊着把用户最开始的需求忘了,要么Token账单高得让老板直皱眉。这些问题,根子几乎都在Context(上下文)的管理上。
“context-mode”是我最近在AI应用工程化里反复折腾的一个主题,说白了就是:给对话上下文设计一套可切换的运行模式,让模型该记的记得住、该忘的忘得掉、该省的省得下。这篇文章把我从架构设计到落地的完整过程拆开讲,包括模式怎么设计、参数怎么定、代码怎么写、坑怎么踩。目标读者是正在做AI产品、写Prompt、搞Agent的朋友,不管你是从零起步还是已经在线上被上下文问题折磨过,这篇都能给你一份可以直接照着改的作业。
1. 为什么“上下文”必须模式化?
1.1 先看清楚上下文失控的三个症状
先聊三个最常见的翻车现场,你大概率都见过。
第一个是“串台”。用户上午让AI帮忙写周报,下午问午饭吃什么,模型看着满屏的周报素材,硬把“西红柿牛腩”推荐成了“本周重点事项”。这不是模型笨,而是上下文里的历史信息没有按用途隔离,所有内容都糊在一起,模型分不清哪条是当前任务的“操作背景”,哪条是已经结束的“历史噪音”。这种问题在小规模demo里几乎不会出现,因为对话轮次少,上下文干净;一旦真实用户聊了几十轮,各种话题混在一起,模型就开始犯迷糊。
第二个是“失忆”。产品功能是让用户分多次完善一份需求文档,结果用户隔天回来继续写,模型已经把前一天定的口径全忘了,甚至跟用户最新说法打架。这种情况往往是系统把整段上下文直接丢给模型,没有做持久化和恢复,聊到哪算哪。用户会觉得“这AI怎么跟金鱼一样,七秒记忆”,然后转身就去投诉。
第三个是“账单爆炸”。上下文越长,Token费用越高,延迟也越高。有些团队图省事,每轮对话把全部历史一股脑塞进去,到了第50轮,一次请求的Token量已经是首轮的十几倍,响应速度肉眼可见地变慢。更麻烦的是,这种慢不是线性增长的,有些模型的推理耗时在长上下文下会翻好几倍,用户体验直接从“秒回”变“转圈圈”。
这三个症状指向同一个结论:上下文不能是“一股脑的全量堆积”,必须有一种机制来管理它。而“模式化”就是给上下文管理定义出一套显式的、可切换的运行方式。换句话说,我们不能指望模型自己从一团乱麻里找出重点,得由系统帮它把桌面理清楚。
1.2 把上下文想象成你的办公桌
为什么非要用“模式”这个词?我想用一个桌面类比来解释。
假设你在办公桌上处理三件事:写季度总结、回客户邮件、整理下周出差行程。如果你把所有材料全摊在一张桌子上,三个任务的材料互相压着,你找东西的时候肯定崩溃。但如果你把桌面分成几个区域——A区放总结材料、B区放邮件、C区放行程——同时手头正在做哪个任务就把哪个区域的材料摊开,做完就收起来,是不是清晰多了?
AI应用的上下文管理也是同一个道理。模型能“看到”的内容完全由我们决定,我们完全可以把上下文分成不同的区域和状态:有的区域是长期稳定的(比如系统指令、产品约束),有的是短期临时的(比如当前问题的检索结果),有的是正在处理的(比如本轮对话),有的是已经结束的(比如历史问答)。针对不同的任务阶段,让系统以不同的“模式”运行,本质上就是在动态调整这张办公桌上材料的摆放方式。
这个类比还说明了一件事:没有哪个区域需要一直摆满材料。你写邮件的时候不需要把出差行程单摊在眼前,AI处理当前这轮问题的时候,也不需要把上上周的闲聊全部加载进来。上下文管理追求的不是“越多越好”,而是“该出现的时候出现,该收起来的时候收起来”。
1.3 三种基础形态对应三种模式
根据我自己的项目经验,绝大多数AI应用跑不出三种基础形态。
第一种是“一次性问答”,用户问一句、系统答一句,上下文很简单,不需要记住前面聊过什么,比如智能客服的常见问题解答、文档问答里的单一查询。这种场景对应全量直通模式就够了,把系统指令和用户当前问题拼在一起直接发模型,简单高效。
第二种是“连续对话”,用户和AI多轮交互,需要记住前面聊过的话题,但对话规模可控,比如产品咨询、方案讨论、代码调试。这种场景对应滑动窗口模式比较合适,只保留最近N轮,既能维持对话脉络,又不会让上下文无限膨胀。
第三种是“长线任务",比如用户连续一周在同一个会话里完善商业计划书,中间还穿插着查资料、改措辞、算数据等不同子任务。这就需要在每次交互时重构上下文,甚至把前几天的内容压缩成摘要,对应摘要压缩模式。这种模式的关键是让AI既能带着"历史记忆"干活,又不会被海量旧内容拖垮性能。
这里有一个关键认知:没有一种模式能通吃所有场景。我见过不少项目死磕某一个方案,结果就是要么短会话浪费Token,要么长会话照样崩。正确的做法是让系统根据会话长度、任务复杂度、用户意图自动切换模式,这就是"context-mode"这个主题的核心价值。它不是一个高深的理论框架,而是一套非常务实、可落地的工程手段。
2. 核心设计:上下文窗口的内部结构与预算分配
2.1 上下文里到底装了什么
决定模式之前,先搞清楚上下文窗口里到底应该放哪些东西。我把它拆成五层。
- 系统指令(System Prompt):角色的身份、行为准则、输出格式。这一层最稳定,每次请求基本都要带,相当于模型的"岗位说明书"。
- 用户目标(User Goal):用户当前这次交互要完成的具体任务,包括隐含意图。比如用户说"帮我看看这个报错",隐含意图是"定位错误原因并给出修复方案"。
- 历史对话(Conversation History):多轮问答里之前的用户输入和助手输出,这是上下文里最占地方的部分,也是膨胀的主要来源。
- 工具结果(Tool Results):调用外部API、数据库查询、代码执行返回的数据,Agent类应用尤其依赖这一层,相当于模型的"实时情报"。
- 外部知识(External Knowledge):通过RAG(检索增强生成)临时拉进来的文档片段,属于"按需加载"的内容,不对话时可以不加载。
这五层不是简单叠加的关系。系统指令和用户目标决定模型"怎么干活",历史对话决定"顺着什么脉络干活",工具结果和外部知识决定"拿什么干活"。搞混了它们的定位,后面分预算就会出现问题。我见过很多团队把所有东西都塞在system prompt里,结果系统指令越来越长,模型反而抓不住重点。
2.2 为什么“整窗口塞满”是错的
有不少团队在最初写Prompt时,习惯把背景说明、历史记录一股脑塞进去,认为“给模型的信息越多,它答得越好”。一开始确实如此,毕竟模型没见过这些内容时,你给得越全它回答越有依据。但到了第20轮、第30轮,问题就来了。
模型对上下文的注意力并不是均匀分配的。对于当前这个问题,真正起作用的是系统指令中最核心的约束、最近几轮的关键诉求、以及本轮引用的知识片段;而那些十几轮之前的寒暄和无关讨论,不仅没有帮助,反而会稀释注意力。这就像你开会时,秘书把过去一个月的会议纪要全铺在桌上,你反而找不到今天要讨论的那页。
所以,设计context-mode的时候,首先要干的一件事是“分层编预算”:给每一层内容规定一个Token配额上限,超过上限的旧内容要么截断、要么汇总、要么淘汰。我给出一个通用预算表,以常见的8K窗口为例:
| 上下文层级 | 职责 | 典型Token占比(以8K窗口为例) |
|---|---|---|
| 系统指令 | 角色、规则、输出格式 | 800~1200 |
| 用户目标 | 当前任务与关键约束 | 300~500 |
| 历史对话 | 多轮对话脉络 | 2000~4000 |
| 工具结果 | 最近调用的返回值 | 1000~2000 |
| 外部知识 | RAG检索片段 | 1500~3000 |
这个占比不是拍脑袋定的。系统指令给1000左右就够写清楚角色和规则了,再多就是浪费,而且系统指令太长本身也会消耗模型的注意力;历史对话留3000左右能让模型记住十来轮的核心内容;工具结果和外部知识按实际功能弹性分配。定了预算之后,不同模式的差异就体现在“怎么在这套预算里取舍历史对话”上了。
这里补充一个Token估算的实用技巧:中文场景下,一个汉字大约对应1到1.5个Token;英文场景下,大约4个字符对应1个Token。不需要精确计算,按这个粗略估算就能在开发阶段把预算表搭出来。我在项目里一般写一个简单的estimate_tokens()函数,用字符数除以系数估算,不要为了精确引入太重的Tokenizer解析,反而拖慢开发节奏。
2.3 会话状态的迁移与模式触发点
上下文管理的另一个关键设计是“会话状态迁移”。我习惯把一个会话的完整生命周期定义成几个状态:
- 初始态(session_started):用户刚进入会话,还没产生有效对话,此时系统只需要加载系统指令和用户目标,不加载任何历史内容。
- 进行态(session_active):对话在正常进行,系统根据当前轮次和累计长度选择使用滑动窗口还是全量上下文。
- 压缩态(session_compressed):累计Token量超过阈值,系统触发摘要压缩,把历史对话归纳成一份“记忆摘要”,然后带着摘要继续跑。
- 休眠态(session_idle):用户长时间未发言,系统把关键状态持久化到数据库或缓存里,下次会话时再恢复。
状态迁移的触发点不建议做得太复杂。我的经验是:会话总Token量达到窗口的70%左右,就该触发压缩评估了;用户隔了超过2小时再次发言,就先走一次休眠恢复;如果单轮问题本身就需要大量检索内容(比如对比多份文档),那就临时切换到“重知识”模式,把更多的预算让给外部知识层。
这套迁移规则,本质上就是对前面那张办公桌的“动态收拾”。我见过有人想用复杂的状态机框架来管理这些状态,说实话在大多数项目里真没必要。一个配置文件加几个if判断,比引入一整套状态机库要可靠得多,排查问题的时候也直观得多。
3. 可落地的三种实现方式:从代码到参数
3.1 模式A:全量上下文模式
全量模式适用于一个会话还比较短、且每轮对话都有价值的场景,比如用户在做一次完整的方案评审,每轮的修改建议都前后关联。在这种模式下,历史内容尽量完整保留,细节不丢失。
实现上就是“不删旧内容,只按预算截断超长单条消息”。关键点是:要给单条消息的长度设上限,比如用户一次性贴了5000字的文档,要先做切分而不是硬塞。很多人在这一步踩坑,以为全量模式就是把所有东西原样塞进上下文,结果一条超长消息就把整个窗口撑爆了。
def build_context_full(system_prompt, messages, max_total_tokens=8000): context = [ {"role": "system", "content": system_prompt}, *messages ] # 估算token数,超出则优先截断最早的消息 total = estimate_tokens(context) while total > max_total_tokens and len(context) > 2: context.pop(1) # 保留system和最新消息,从最早的对话开始丢 total = estimate_tokens(context) return context这里有个细节:context.pop(1)而不是pop(0),因为第0条是系统指令,不能丢。我见过有人图省事直接pop(0),结果跑了几轮之后模型连自己的角色都忘了,输出风格变得乱七八糟。全量模式下的“全量”指的是“尽可能保留”,但系统指令永远是优先级最高的那一条,任何时候都不应该被挤出窗口。
3.2 模式B:滑动窗口模式
对话超过一定轮数之后,全量模式就撑不住了。滑动窗口的核心思路是:只保留最近N轮对话,更早的内容直接裁掉。它比全量模式省Token,也比全量模式干净,因为旧内容不会干扰当前焦点。
def build_context_sliding(system_prompt, messages, window_size=10): recent = messages[-window_size:] # 只取最近10轮 context = [{"role": "system", "content": system_prompt}, *recent] return context窗口大小怎么定?我的经验公式是:窗口轮数 = (窗口Token预算 - 系统指令和工具结果的Token数) ÷ 每轮平均Token数。假设窗口预算是4000 Token,系统指令加工具结果占1500,每轮对话(一问一答)平均消耗500 Token,那么(4000-1500)/500=5轮,窗口就设成5轮比较稳。
不要拍脑袋写一个10,否则会出现一种很尴尬的情况:窗口名义上是10轮,但因为某几轮问答特别长,真实能容纳的历史连5轮都不到,反而用户觉得AI“记忆力变差了”。每个项目的单轮对话长度差异很大,一定要先跑几天日志,统计出自己项目的平均Token消耗,再去定窗口大小。
3.3 模式C:摘要压缩模式
滑动窗口解决不了“跨度很长但每轮都重要”的场景,这时候就需要摘要压缩模式。核心逻辑是:把窗口中被挤出部分的历史对话,用一次额外的LLM调用压缩成摘要,放进上下文里作为“记忆锚点”。
def summarize_history(dropped_messages): prompt = f""" 请把以下对话压缩成300字以内的中文摘要, 保留所有用户明确提出的需求、关键决定、尚未完成的事项: {format_messages(dropped_messages)} """ summary = llm_call(prompt) return summary def build_context_summary(system_prompt, messages, memory_summary=None): context = [{"role": "system", "content": system_prompt}] if memory_summary: context.append({"role": "system", "content": f"[历史记忆摘要] {memory_summary}"}) recent = messages[-5:] # 近期对话 context.extend(recent) return context注意摘要的存放位置。我推荐把摘要放在system角色里,或者用特殊的system标记包裹,而不是混在普通对话里。原因是:摘要本质上是“系统给模型的指令性信息”,它告诉模型“你之前和用户聊过这些”,把它和普通对话混在一起,模型容易把摘要内容也当成用户原话,导致引用错误。这个细节我在测试阶段踩过坑,改完之后效果立竿见影。
摘要模式下还有一个隐藏成本:为了生成摘要,每次触发压缩都要多花一次LLM调用的钱。所以触发条件要设得保守一些,建议只有当累计Token量超过窗口的80%时才执行。而且,如果压缩频率达到一定次数后(比如每5轮触发一次),就要考虑是不是该建议用户开启新会话了。毕竟摘要也堆到一定长度之后,继续压缩只会让信息密度越来越低,服务质量反而下降。
3.4 模式切换的“决策点”怎么设计
有了三种模式之后,还缺一个“调度器”。我在项目里用的是很简单的规则引擎,没有上复杂的状态机框架。一个select_mode函数就能搞定大部分场景:
def select_mode(session, current_turn_tokens): total_tokens = session.accumulated_tokens + current_turn_tokens if total_tokens < 0.5 * MAX_TOKENS: return "full" elif session.idle_minutes > 30: return "summary" # 长时间空闲后回来,先恢复记忆 elif total_tokens < 0.8 * MAX_TOKENS: return "sliding" else: return "summary"这段规则的逻辑很直白:会话早期预算充足,用全量模式保住细节;中期用滑动窗口控制成本;后期或跨天恢复时用摘要保住主线。排序上有个小技巧——把“长时间空闲”的判断放在“滑动窗口”之前,因为用户隔了很久回来后,旧轮次的重要性已经大幅下降,直接压缩对比近几轮硬截断更自然。
我还建议在调度器里加一个“强制刷新”入口:如果用户主动说“我们换个话题”或者“重新开始”,系统应该清空上下文中除系统指令外的所有内容,而不是继续累积旧上下文。这个功能看似简单,但很多团队忘了做,导致用户明明开了新话题,AI还一直在旧话题里打转。
4. 踩坑实录:上下文管理最容易翻车的四个问题
4.1 切换模式后,会话“失忆”了
我第一次把滑动窗口接到线上时,用户的反馈是“AI怎么突然不记得我上午说过什么了”。查了下日志,原因是窗口裁掉的内容里包含用户上午明确说的“报告用PDF格式”。这个要求对后续输出格式影响很大,被裁掉之后模型自然就“忘了”。
这个问题现在回头看很基础,但当时确实没意识到:滑动窗口按轮数裁剪,没有考虑消息的重要程度。后来我加了一个“关键信息提取层”,在裁掉旧消息之前,先用一次轻量LLM调用扫描被裁剪的部分,把类似“用户偏好、明确要求、未完成事项”抽出来,作为短标签挂在上下文的系统层里。
def extract_key_facts(dropped_messages): prompt = f""" 从对话中提取出不超过5条关键信息,每条不超过20字。 只提取:用户的明确偏好、硬性要求、未完成的事项。 {format_messages(dropped_messages)} """ facts = llm_call(prompt) return facts这个方案的代价是每轮多花几百Token,但换来的效果是:窗口只管“大事记”,关键要求一个不掉。实测下,用户对“AI记得我说过什么”的满意度是最高的。如果你的应用场景对记忆连续性要求很高,这个“关键信息提取层”可以说是性价比最高的优化点。
4.2 摘要压缩把关键约束“压丢了”
摘要压缩模式也有它的坑。有一次同事反馈,AI回答业务问题时把产品的最低价格说错了。排查后发现,摘要生成时的Prompt里写的是“压缩成300字”,结果模型为了凑字数,把“价格不能低于500元”这个约束合并成了“价格有下限”,甚至直接省略了。
改进方案有两个。一是在摘要Prompt里增加“必须逐条保存所有数字约束和否定性表述”,并且要求输出为结构化条目,而不是散文。比如“1. 价格下限500元。2. 不提供试用版。”这种格式比一段话更容易让模型在后续对话中引用。二是压缩完成后跑一次“关键约束校验”,用一套正则规则匹配价格、日期、数量这类数字信息,如果发现原文有而摘要没有,就把缺失项追加回去。
这个“摘要后校验”步骤看起来多了一道工序,但在业务场景里能避免大量低级错误。尤其是涉及钱、时间、数量的AI应用,宁可多花几十毫秒的校验时间,也不能让模型把关键约束丢了。
4.3 工具结果把上下文“污染”了
Agent类应用经常遇到一种怪现象:模型开始“幻觉”工具结果,比如明明没有查询数据库,却一本正经地告诉你“根据系统查询,您的账户余额是88元”。根因是历史对话里保留了之前的工具返回结果,模型误以为它仍然有效。
我在设计context-mode时给工具结果加了一个“有效期”概念:默认只保留最近一次工具调用的结果,并且用明确的标记包裹,例如“[工具结果-时间戳],仅用于回答当前问题”。如果下一轮用户没有主动追问该结果,这个标记会在下次请求前被系统清掉,不给模型“拿着过期数据编故事”的机会。
def add_tool_result(context, result, timestamp): tagged = f"[工具结果-{timestamp}] {result}(仅用于回答当前问题)" context.append({"role": "tool", "content": tagged}) return context有人可能会问,工具结果对后续对话也有参考价值,全删了会不会浪费?我的经验是:偶尔需要参考旧工具结果的情况,用户通常会在当前问题里重新问一次或者明确提及,这时候再重新调用工具获取一次最新结果更可靠。与其让模型用过期的工具结果“将错就错”,不如让它用最新结果“实话实说”。
4.4 几个方便排查的度量指标
做上下文管理最怕“凭感觉”。我建议每次请求时把以下三个指标打点记录:
- 上下文总Token量:看预算有没有超预期增长,如果某几轮突然暴涨,多半是工具结果或外部知识塞多了。
- 裁剪比例(被裁减掉的Token数 ÷ 总Token数):比例长期大于50%,说明你的模式切换得太晚了,应该更早触发滑动或摘要。
- 摘要触发次数与平均摘要Token数:摘要越来越长说明压缩Prompt写得不够精炼,或者该建议用户开启新会话了。
这三个指标配合日志一起看,基本能定位大多数上下文问题。再附一个速查表,方便现场排查:
| 现象 | 可能原因 | 快速处理 |
|---|---|---|
| 回答串台 | 历史对话层级混用 | 按5层结构分区域,功能区隔开 |
| 失忆 | 窗口/摘要裁掉了关键要求 | 加关键信息提取层,保留数字约束 |
| 幻觉工具结果 | 旧工具结果未清理 | 只保留最近一次,加有效时间戳 |
| Token账单暴涨 | 全量模式用太久 | 提前触发滑动/摘要模式 |
5. 参数初始化与个人最终推荐配置
5.1 一套可以直接抄的初始参数
我给出一套稳妥的初始参数,适合大部分中小规模AI应用,可以直接作为第一版上线的默认值。这套配置不需要一开始就调到最优,关键是让系统先跑起来,然后根据日志数据慢慢修正。
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 系统指令Token | 1000 | 再长就该精简Prompt了 |
| 单轮窗口轮数 | 6~10 | 根据单轮平均Token微调 |
| 摘要触发阈值 | 总Token的80% | 太早触发浪费额外调用 |
| 摘要长度上限 | 300字 | 超过说明压缩Prompt有问题 |
| 工具结果保留数 | 最近1次 | 更多只会误导模型 |
| 空闲恢复间隔 | 30分钟 | 超过就做休眠态持久化 |
如果你用的是更大的模型,比如上下文窗口是32K或128K的,上面的绝对值可以等比放大,但比例关系基本不变。关键在于:系统指令不要因为窗口变大就拼命加长,历史对话的预算比例可以适当放宽,因为大窗口模型对长上下文的处理能力更强,但还是要给工具结果和外部知识留出位置。
5.2 更进一步的场景定制建议
不同的业务场景,对上下文各层的敏感度完全不一样。当你把context-mode的框架跑通之后,后面要做的就是微调预算分配,这里我针对三类常见应用给点定制思路。
如果你是做文档问答类的应用,建议把外部知识的预算调到上限,甚至可以做“动态预算”——用户问复杂对比题时,临时砍掉历史对话预算,把位置让给检索片段。因为文档问答的核心是“答得准”,历史对话反而从第二位降到了第三位。
如果你是做角色扮演或情感陪伴类的应用,历史对话的权重最高。建议把摘要压缩的频率调低,宁可多花Token也要保留原对话的语气和细节。这类应用的User Retention(用户留存)靠的就是“AI记得我们之前聊过什么”的亲密感,摘要压缩得太多反而破坏体验。
如果你是做代码生成类的应用,重点要在系统指令里写清楚代码规范和输出格式,上下文模式反而次要,优先保证工具结果的实时性。代码生成最怕的是模型“自以为调用了接口”,实际上用的是旧返回值,所以在预算分配上,工具结果层的实时性优先级最高。
这些定制都不需要改架构,只需要调整预算表里各层的占比。这正是把上下文“模式化”之后带来的最大好处——切换和调参变得非常廉价。
我在实际项目里跑这套方案跑了小半年,最大的体会是:上下文管理没有一劳永逸的答案,只有不断根据数据调优的过程。刚开始可以照抄我这套参数,但上线之后一定要盯着三个度量指标跑两周,用真实数据修正你的窗口大小和触发阈值。有些团队上来就追求“完美方案”,结果在架构上花了太多时间;我更建议先用一套简单的规则跑起来,再逐步演进。记住,context-mode不是让你一次性设计出一套终极方案,而是让你拥有一个可以不断迭代的框架——这个框架能让你在遇到新的上下文问题时,知道该从哪里下手、怎么调整,而不是推倒重来。