
做对话类 AI 应用的人迟早都会撞上context-mode这个词。它直译是“上下文模式”但在实际工程讨论里大家更习惯直接说 context mode指的就是一个对话系统在把消息发给大模型之前用什么策略来组织和管理历史上下文。这个东西听起来简单却是很多 AI 应用能不能落地、效果稳不稳定的分水岭。同样一个模型不懂 context mode 的人可能聊到第五轮就开始答非所问懂的人可以在几万字的长对话里保持角色设定和关键信息不丢。差异不在模型而在你怎么把历史消息喂给它。这篇文章我想把我做过的几种 context mode 实现方式、参数计算、踩坑过程完整拆一遍给正在做客服机器人、聊天助手、知识库问答这类应用的朋友一个可以直接抄作业的参考。不管你是刚接触大模型开发的初学者还是已经在生产环境里跑对话服务的工程师这篇文章都适合你。我会先说清楚上下文模式的设计逻辑再给完整可运行的核心代码最后把我在实际项目中遇到的坑和排查思路全部列出来保证每一段都是实操过的经验不是说书本上抄来的概念。1. Context Mode 到底是什么整体设计与思路拆解1.1 一句话说清 Context Mode 在解决什么问题我们先从一个最简单的现象说起。你调用大模型接口的时候好消息是它可以理解你输入的一大段文本坏消息是它不是真的“记住”了你说过什么。每次请求都是无状态的它只看着你这次请求里带过来的消息然后根据这堆消息来生成回答。这段带过来的消息业内就叫“上下文”。所以当你做一个连续对话应用第一轮用户的“我是小明我喜欢喝茶”到第五轮你问他“你知道我喜欢什么吗”如果你没有把第一轮的内容跟着发过去模型是根本不知道“小明”和“茶”的。这时候你就需要一套策略决定哪些历史消息该带上、哪些可以丢、带上多少、带不下怎么办。这套策略就是 context mode。我见过很多新手犯的第一个错误是把所有历史消息一股脑往消息数组里塞。一开始还行聊到后面就是tokens: 65000, your request is too large直接报错。也见过另一种极端每次只带最后一轮结果模型每轮都像失忆一样。好的 context mode本质上是在“信息完整性”和“token 成本”之间做取舍取舍的尺度取决于业务场景。1.2 三种主流实现形态截断、摘要、检索我做了几个项目之后发现实践中基本收敛到三种形态每种都有自己适合的地方第一种是“最近多轮截断”。就是把对话历史的最后 N 轮或者最后 N 个 token提取出来再加上系统提示词拼成完整的请求。这是实现成本最低的方案十几行代码就能跑特别适合用来做第一个版本。缺点是当 N 设得偏小时很早之前的关键信息会丢。第二种是“摘要压缩模式”。每当历史消息超过一定 token 阈值就先调用一次大模型把旧消息提炼成摘要然后请求里只带“摘要 最近几轮完整消息”。这个方案在信息保留和成本之间比较平衡适合中长对话场景比如客服工单跟进、长文档访谈。缺点是摘要本身会引入信息损耗有可能把不重要的细节记住了反而把关键细节丢了。第三种是“检索召回模式”也叫记忆检索 / RAG 风格。它把历史消息切成小块用 embedding 向量建索引每次请求前先根据当前用户的问题去检索最相关的历史片段只召回 Top-K 个片段放进上下文。这个方案最适合那种“对话很长、但每次真正相关的只有一小部分”的场景比如智能助教、私有知识库。缺点是你得维护一套向量库和切分逻辑架构复杂度明显上升。这三种形态不是互斥的。我最后落地的生产方案里就是“摘要 检索 近几轮完整消息”三者组合。1.3 选型地图不同任务适合哪种 Context Mode很多朋友问我到底选哪种模式我会让他们先回答三个问题用户的单次会话多长丢掉信息之后能不能补救预算有多少如果是单轮问答为主的业务比如翻译、改写、分类历史对话本来就不长直接截断模式就行了没必要上检索。如果是长对话业务比如心理倾诉陪伴、深度客服摘要模式会更稳因为这类场景里用户核心诉求往往在很前面的轮次。如果是知识库问答用户问题会随机落到文档任意位置截断和摘要都不靠谱必须检索模式。我还总结了一个粗糙的选型表项目初期可以照着选业务场景平均轮次推荐模式核心理由单轮工具调用1-3截断模式成本最低无需额外逻辑售前咨询客服5-15截断 摘要兼顾成本与信息完整性长程陪伴 / 深度访谈20摘要 近期完整消息长期记忆需要提炼重点知识库问答 / 文档分析不定检索召回 截断精准定位关键片段复杂智能体 / 多步骤任务10摘要 检索 工具链需要把任务中间态也保留这个表格是我踩过不少坑之后的总结可以直接作为技术方案评审的起点省得从零开始试。2. 核心参数与预算法则把每一步算明白2.1 Token 预算先算账再写代码世界上没有免费的上下文。每往请求里塞一个 token就多付一份钱、多增加一点延迟。所以做 context mode 的第一步不是写代码而是先算账。假设你用的是某个 128K 上下文的模型注意这个 128K 是“输入 输出”共用的不是你输入可以占满 128K。系统提示词固定占 1000 个 token最后模型生成回答至少要留 2000 个 token 的空间。那么真正留给历史的 Token 预算就是128000 - 1000系统提示 - 2000输出预留 - 50消息结构的格式开销 ≈ 124950 tokens这个 124950 才是你可以拿来塞历史消息的总空间。如果某条消息特别长比如用户贴了一篇 50000 token 的文档那么光这条消息就可能把预算吃掉一半。所以设计上我会再加一条铁律任何超过阈值的单条消息必须先做截断或者摘要不能让原始长文直接进请求。我实际工作中用的估算方式是这样写一个estimate_tokens(text)函数简单点可以用字符数除以 3 估算中文如果用了 OpenAI 就调 tiktoken 精确计算。做预算分配时宁可留 15% 的余量也不要刚好压线因为有些模型的 tokenizer 对不同语言计数差异很大压线的真实风险是请求被拒。2.2 滑动窗口到底保留多少轮对话截断模式最核心的参数有两个一个是窗口大小一个是窗口步长。窗口大小决定每次携带多少轮历史步长决定旧消息是“完全丢弃”还是“部分保留”。我在项目里最常用的窗口规则是“按 token 数滑动”而不是“按轮数滑动”。原因很简单对话里每个人说话长度差异太大按轮数设窗口极不稳定。同一句话有人问一句“在吗”有人贴一段 3000 字的报错日志按轮数截断的话 token 数会剧烈抖动。具体做法从最新一条消息开始往前累加 token累加值到达设定的历史预算阈值比如 20000 token时停止把更早的消息丢掉。如果更早的消息里包含 system 指令记得 system 始终要保留不能丢。至于窗口重叠我做摘要模式时会留一点重叠区域比如窗口大小 20000 token、重叠 2000 token。这样摘要和最近消息之间有个衔接带避免在边界处把重要的承接关系切断。截断模式不需要重叠重叠只会浪费 token。2.3 摘要压缩触发时机与压缩比摘要模式的第一个问题是什么时候触发摘要这个不能拍脑袋定。我的经验值是设置两道阈值。第一道是“软阈值”比如历史 token 达到预算法案的 50% 时开始对最老的部分做摘要第二道是“硬阈值”比如达到 75% 时必须做一轮强力压缩保证请求不会爆。具体说假设历史预算 60000 token那么当累计到 30000 token 时我会把最早 20000 token 的消息交给一个“压缩模型”提取摘要当累计到 45000 token 时再把第二老的部分也进行摘要压缩。摘要文本本身一般控制在 800-1200 token 以内压缩比可以达到 10:1 甚至更高。这里有个重要的实操细节每一次做新摘要不要在旧的摘要基础上反复缩而是把“上一个摘要 被摘要的消息”合在一起重新提炼一次。否则摘要会一层层衰减最后变成“用户谈了一些话题”这种废话。我就是在这个问题上踩过大坑后面专门在排障章节会细说。还有一个很容易漏的参数是“摘要指令”。大模型做摘要时你不能只说“总结一下”你一定要告诉它“这是一个对话历史片段未来会被作为上下文使用请保留用户偏好、关键决定、待办事项、实体关系跳过寒暄和无关细节”。加这样一段指令之后摘要质量会明显提升。2.4 检索召回切分粒度与召回数量如果你的场景需要检索历史对话或知识库有两个核心参数决定效果文本块切分大小和召回数量。文本块切分也就是 chunk size。我系统性测过 256、500、800、1200 token 四档。256 token 的块召回精度高但上下文碎片化模型可能看到一堆不完整的片段1200 token 的块信息完整但检索命中率下降而且塞进上下文时占用大。折中下来500 token 左右、块与块之间重叠 50 token对大多数对话类场景最稳。召回数量也就是 top_k。我一开始设的是 3结果经常漏东西后来调到 10又发现上下文里塞了很多不太相关的片段反而干扰模型回答。最后我总结的经验是这取决于最终需要的上下文总预算。假设你给检索召回分配 5000 tokenchunk 是 500那 top_k 上限就是 10。具体到回复质量建议先试 top_k5再根据人工评测往上调不要一上来就大手笔。还有一个参数容易被忽略相关性阈值。很多向量库默认会返回距离最近的结果哪怕距离很远完全不相关。我会设定一个最低相似度阈值召回结果低于阈值就直接不注入上下文。宁可少一点也不要给模型塞噪音。3. 实操全流程从零实现一个带 Context Mode 的对话服务3.1 环境准备与消息结构约定进入代码环节之前先把消息结构说清楚。主流大模型接口基本都支持消息数组形式每条消息有role和content两个字段。role通常有三种system表示系统提示user表示用户输入assistant表示模型回复。我会在代码里专门写一个Message的 dataclass方便统一管理。代码就用 Python 示例如果你用的是 Node.js 或者 Java思路完全一致只是对应语言语法不同而已。from dataclasses import dataclass from typing import Optional dataclass class Message: role: str # system, user, assistant content: str tokens: Optional[int] None这里的tokens字段可以缓存每条消息的 token 数省得每次组装请求时反复计算。需要注意的是缓存只在消息内容不变时有意义如果对消息做了截断或摘要一定要记得更新 token 数否则后面所有预算判断都会失准。3.2 第一步实现“最近多轮截断”模式第一种模式最简单。流程是把系统提示词固定放在最前面然后从历史消息末尾最新一条往回遍历累加 token直到超过预算阈值把更早的部分丢掉。def build_truncated_context( system_prompt: str, history: list[Message], max_history_tokens: int 20000, ) - list[dict]: # 始终保留系统提示 messages [{role: system, content: system_prompt}] # 从最新往最旧累加 token current_tokens 0 selected [] for msg in reversed(history): msg_tokens estimate_tokens(msg.content) if current_tokens msg_tokens max_history_tokens: break selected.append(msg) current_tokens msg_tokens # 恢复正确的发问顺序并把消息格式化成接口需要的 dict selected.reverse() messages.extend( {role: msg.role, content: msg.content} for msg in selected ) return messages这段代码的核心逻辑是“倒序遍历 正序返回”。读代码的时候注意别把顺序搞反了我最初实现时就因为忘了reverse()导致模型看到的上下文是倒叙的效果一团糟。调用流程也一并给出# 示例每轮用户输入后把当前轮拼进 history再调用模型 history [Message(user, 我是小明我喜欢喝茶)] # 第二轮 history.append(Message(user, 你还记得我的喜好是什么吗)) context build_truncated_context( system_prompt你是一个友好的聊天助手。, historyhistory, max_history_tokens20000, ) # 这里把 context 发给模型接口我自己实测下来这种模式在 10 轮以内的简单对话中表现不错回答稳定、费用低。但超过 15 轮之后如果用户早期提过关键信息就很容易丢。3.3 第二步实现“摘要 最近消息”模式接下来是摘要模式。和截断模式的最大区别是丢掉的旧消息不是直接放弃而是先经过大模型提炼浓缩成一小段摘要放在上下文中充当“长期记忆”。def summarize_messages(messages: list[Message]) - str: # 将消息序列转换为纯文本便于交给摘要模型 text \n.join(f{m.role}: {m.content} for m in messages) summary_prompt 请对以下对话历史进行摘要压缩保留用户偏好、关键决定、待办事项、实体关系跳过寒暄与无关细节。摘要控制在 1000 token 以内。 --- text # 这里调用你的模型接口比如 gpt-4o-mini 等 return call_llm([{role: user, content: summary_prompt}]) def build_summary_context( system_prompt: str, history: list[Message], max_history_tokens: int 30000, recent_tokens: int 8000, ) - list[dict]: messages [{role: system, content: system_prompt}] # 累计历史 token total sum(estimate_tokens(m.content) for m in history) if total max_history_tokens: # 还没到阈值直接用全部历史 messages.extend({role: m.role, content: m.content} for m in history) return messages # 超过阈值时把最老的部分提取摘要最新 recent_tokens 保留完整消息 recent_messages [] recent_tokens_used 0 for msg in reversed(history): mt estimate_tokens(msg.content) if recent_tokens_used mt recent_tokens: break recent_messages.append(msg) recent_tokens_used mt recent_messages.reverse() old_messages history[: len(history) - len(recent_messages)] summary summarize_messages(old_messages) messages.append({role: system, content: f以下是历史对话摘要{summary}}) messages.extend({role: m.role, content: m.content} for m in recent_messages) return messages这里涉及的补充说明有两点都是实操中得来的第一摘要我放在system消息里而不是用user消息。原因是摘要本质上是“系统给模型的背景信息”放在 system 里权重更高、更不容易被后续对话覆盖而且如果放在 user 里模型可能会把它当成用户说的话引发角色混乱。第二摘要模型不一定要和主模型一样强大。我用gpt-4o-mini级别的小模型做摘要就够了便宜且快。只有在摘要质量明显影响回答准确度时才升级到更强的模型。另一个细节是我建议给摘要增加一个时间戳或序号比如“摘要 v2 生成于 2025-01-10 14:32”这能帮助模型识别摘要的新鲜程度。如果同一段历史被多次摘要模型会知道用哪个版本减少混乱。3.4 第三步实现向量检索增强模式第三步是更复杂的检索模式。流程要先离线建立索引再在线检索注入。先看索引部分import chromadb from openai import OpenAI client OpenAI() def split_text(text: str, chunk_size: int 500, overlap: int 50) - list[str]: # 简化版切分按段落粗切实际项目中建议按语义边界切 chunks [] for i in range(0, len(text), chunk_size - overlap): chunks.append(text[i:i chunk_size]) return chunks def index_dialog(history: list[Message]): # 把历史对话按 8 条一组切片便于检索到上下文承接 groups [] for i in range(0, len(history), 8): group history[i:i 8] text \n.join(f{m.role}: {m.content} for m in group) groups.append(text) collection client.get_or_create_collection(dialog_memory) for idx, g in enumerate(groups): emb client.embeddings.create( modeltext-embedding-3-small, inputg, ).data[0].embedding collection.add( ids[fchunk_{idx}], embeddings[emb], documents[g], )检索注入部分def build_rag_context( system_prompt: str, current_query: str, history: list[Message], top_k: int 5, max_context_tokens: int 5000, ) - list[dict]: messages [{role: system, content: system_prompt}] # 把当前问题向量化召回 TopK 相关片段 query_emb client.embeddings.create( modeltext-embedding-3-small, inputcurrent_query, ).data[0].embedding collection client.get_or_create_collection(dialog_memory) results collection.query( query_embeddings[query_emb], n_resultstop_k, ) retrieved_chunks results[documents][0] # 注实际项目还要过滤低于相似度阈值的结果 context_text \n.join(retrieved_chunks) messages.append({ role: system, content: f以下是检索到的相关历史片段\n{context_text}, }) # 再加最近两轮完整对话保证即时性 recent [{role: m.role, content: m.content} for m in history[-2:]] messages.extend(recent) messages.append({role: user, content: current_query}) return messages这个方案在“知识库问答 历史语义召回”场景下非常能打。但是我必须强调这里的代码只是骨架生产环境远不止这些。至少还要处理切分时的语义完整性、不同用户的隔离、向量集合的增量更新以及阈值过滤。3.5 模式编排与成本监控模式实现之后还要做编排调度。我的习惯是做一个ContextBuilder类里面根据当前会话状态自动选择模式。比如会话轮次少于 5 直接返回截断模式轮次多但业务场景固定则用摘要模式如果配置了知识库并且当前问题适合检索则走检索模式。class ContextBuilder: def __init__(self, config: dict): self.mode config.get(mode, truncate) self.system_prompt config.get(system_prompt, ) self.max_history_tokens config.get(max_history_tokens, 20000) def build(self, history: list[Message], query: str) - list[dict]: if self.mode summary: return build_summary_context( self.system_prompt, history, self.max_history_tokens ) if self.mode rag: return build_rag_context( self.system_prompt, query, history ) return build_truncated_context( self.system_prompt, history, self.max_history_tokens )监控方面我在每次请求后记录三件事context 的 token 数、模式类型、响应延迟。当模式下线部署后如果发现 token 数波动异常或者延迟突增第一时间看是不是切错了模式。我曾经一次线上事故就是因为一个会话的轮次恰好卡在两种模式的阈值边缘导致模型一会儿带摘要一会儿不带回答风格完全漂移。4. 真实踩坑常见问题与排查技巧实录4.1 症状一模型“失忆”回答完全忽略历史这是最高频的故障。我排查这类问题时会看三件事第一看传给模型的消息数组里到底有没有历史消息。很多 bug 出在状态管理上前端没把历史传上来或者服务端用了不同 key 存会话结果取出来是空的。直接在入口处打印 message 数量立刻能判断。第二看 system 提示词里有没有冲突指令。如果你的 system 说“忽略所有历史对话”然后你又在上下文里带历史模型自然按 system 的优先级忽略。这种情况不是 context mode 的问题是指令冲突。第三确认截断窗口是不是太小。比如窗口只有 1000 token用户每轮都说很长一段那实际可能只能留下最近一两轮看起来就像失忆。这时候调大窗口参数或者换摘要模式。4.2 症状二请求报错Token 超出上限Token 超出上限是 context mode 最常见的报错。大多数情况下是预算计算和实际 tokenizer 计数不一致导致。我建议所有涉及 token 判断的地方统一用一个函数计算不要一个地方用字符数估算、另一个地方用 tiktoken 精确计算否则会把误差放大。还有一个伪装成“超出上限”的坑单条消息超长。比如用户上传了一本书这条消息 90000 token即便其他历史全清掉也超过了模型上限。这种情况必须对单条消息做前置截断或摘要而不是依赖历史窗口去卡。我给自己定了一条规则所有user消息进入历史数组之前先做一次长度检查超长则先摘要再存储。这条规则上线之后线上 token 超限的报错几乎归零。4.3 症状三摘要模式下关键信息凭空消失在摘要模式下最关键的信息丢失往往不是模型摘要能力差而是重复摘要引起的“信息衰减”。第一次摘要可能保住 80% 的信息第二次在已有摘要基础上再摘要可能只剩 50%到后面就全变成了废话。解决方法是摘要时永远合并“上一个摘要 新增的原始消息”而不是只对着上一次的摘要做压缩。具体到代码就是维护全局唯一的摘要对象每次触发摘要时把旧摘要作为输入之一与新增消息一起提交给摘要模型。这样才能把信息衰减控制在可控范围。另外摘要触发时机也很讲究。我建议不要在每轮都做摘要而是当旧消息达到阈值时才做。频繁摘要不仅烧钱而且每次摘要都会重写一次历史记忆模型对早期信息的权重会越来越弱。4.4 症状四检索模式召回的上下文不相关检索模式最让人头疼的问题就是检索结果词面很接近但语义风马牛不相及。这通常是因为 embedding 模型对业务术语不敏感。比如用户问“退款”文档里大量出现“售后”但从不出现“退款”两个字纯向量检索就可能召回失败。我的方案是“关键词兜底 向量召回”双通道。先做基于关键词的粗筛得到候选集再做向量精排。有些向量库也支持内置的 filter按元数据比如时间范围、文档类型过滤把不相关的语料事先排除。同时要做相似度阈值过滤。我项目里最初 top_k10 全部塞进上下文结果模型经常被无关片段带偏。后来加了 0.75 的相似度阈值召回数量经常降到 4-5 个反而回答准确率上升了。如果相似度普遍很低就要考虑是不是切分太大导致语义稀释把 chunk_size 调小再试。4.5 快速排障对照表把高频问题整理成一张表方便团队排障时直接对照现象优先怀疑检查方法常见解决回答忽略历史截断窗口过小打印实际进入请求的消息数与 token 数调大窗口/换摘要模式角色时而正确时而跑偏system 与上下文冲突检查 system 文案与消息顺序统一指令system 前置token 超限预算算法不一致用 tiktoken 统一核验统一 token 估算入口摘要后信息丢失重复摘要导致衰减查看摘要生成链路是否合并旧摘要每次合并旧摘要重提炼检索结果无关联切分粒度不合适抽查 chunk 语义完整性调小 chunk加关键词兜底成本突增模式选择不收敛看请求日志的 mode 分布增加模式切换稳定性这张表我贴在了团队的技术文档里每次排查先对号入座。实践下来80% 以上的上下文相关问题都能在十分钟内定位到根因。5. 落地工程化多用户隔离、缓存与后续扩展5.1 会话上下文隔离一个用户不能串到另一个用户当你把 context mode 从单机 demo 推到多用户生产环境时第一个炸雷就是会话串号。用户 A 的对话历史被拼接到了用户 B 的请求里这在对话产品里属于严重事故。解决办法是引入 session_id 概念。每个会话有一个唯一 ID历史消息记录和缓存全部以 session_id 为 key 存储。不管是内存 Map 还是 Redis都要在存储层强制带上这一层维度。我再提醒一个容易疏漏的细节摘要和检索索引也要按 session_id 划分。如果摘要混在一起模型接收到的就是多个人混杂的记忆回答自然牛头不对马嘴。在测试环境我一般会专门加一个“会话切换”按钮模拟不同用户场景快速切换检查是否存在上下文污染。这块不能省串号 bug 在代码 review 阶段往往靠肉眼发现不了。5.2 上下文缓存让模式切换不烧钱摘要模式和检索模式都有一个共同痛点重复计算。同一段历史可能因为用户多问了两句被反复做摘要同一个文档片段可能被几十次检索重复 embedding。这些都是白烧的钱。我会给摘要结果做一层缓存key 是历史消息区块的哈希值。只要区块内容没变直接取缓存摘要不再调模型。实测下来一个日活 1 万的客服机器人摘要缓存能把摘要模型的调用量降掉 60% 以上。还有一个更细的优化把“组装好的上下文”整体缓存。因为系统提示词和最近的截断片段如果没变组装结果就不会变。可以直接复用上一次的消息数组省去遍历和拼接。但这个缓存必须注意失效逻辑一旦新消息进来就要立即失效。5.3 后续扩展方向跨会话持久记忆、多模态 context把基础模式跑通之后还可以往更高级的方向扩展。一个值得做的方向是跨会话持久记忆。用户今天来聊过一次明天又来了系统可以把今天的摘要归档明天作为长期记忆注入。这个方向可以做但要注意隐私合规必须明确告知用户记忆存储规则并允许用户删除。另一个方向是多模态 context。现在的上下文不止是文字还可能是图片、语音、文档。例如用户发了一张截图问“这个报错怎么处理”那截图本身也是一条 context。多模态 context 的组织方式与传统文本不同需要考虑图片的压缩、优先层级以及和其他文本消息的混排顺序这也是一个很有意思的工程问题。还有些团队会接入中长期记忆系统把用户画像、偏好标签做成结构化的 context。比如“用户偏好深度推理、示例说明、引用来源”在系统提示词里以 profile 块存在。这些都属于 context-mode 的进阶玩法如果你的项目已经跑通了基础三种模式完全可以往这个方向探索。最后聊一点个人经验。如果有人刚接触 context-mode我会建议他别急着上最复杂的检索方案先用截断模式跑通全流程把日志和监控搭好。然后观察真实用户的使用习惯看平均轮次到底是多少、哪些信息丢了用户会抱怨再逐步引入摘要和检索。我自己就是在这个循序渐进的过程里把踩坑成本降到最低的。技术方案不在多在于适不适合你的业务场景先把基础打牢再往上加复杂度这才是最稳妥的路径。