
1. context-mode 到底是什么从一次“失忆”说起如果你调过 LLM 的 API大概率遇到过这种事模型聊着聊着就忘了你十分钟前叮嘱的事情。比如我曾在做一个客服机器人时用户第一句说“我叫王小明用的是 Pro 套餐”到第十轮追问“那我这个套餐能升级吗”模型居然一本正经地回复“请问您目前使用的是哪个套餐”。这不是模型变蠢了而是你根本没有把“王小明”和“Pro 套餐”这两条信息持续喂给它。这个问题的根源就是上下文context管理没做好。业内管这一整套处理手段叫context-mode翻译过来就是“上下文模式”。它不是一个具体的函数也不是某个框架独有的功能而是一套工程化方案你决定把哪些信息放进模型的一次调用里、放多少、以什么形式放、放不下了怎么办。说白了LLM 本身是一个无状态的函数输入一堆 tokens输出一堆 tokens每次调用都是独立的一次“失忆”。而 context-mode 要做的就是在这层无状态之上替模型搭出一套“工作记忆”。这套东西能解决的问题比表面上看到的要大得多。首先是成本GPT-4 级别的模型按 token 计费你每多塞一千个历史字进去价格就往上跳一截其次是延迟输入越长模型处理越慢用户那边就是转圈圈再就是一致性如果用户信息在中间某轮被丢掉了后面整个对话就会跑偏。所以 context-mode 并不是“锦上添花”的优化而是任何要做多轮对话、Agent、RAG 应用的人都绕不开的核心环节。这篇文章适合谁正在做 AI 客服、个人助理、文档问答、自动化 Agent 的开发者或者刚接触 LLM 应用、想把“能跑的 Demo”变成“能用的产品”的工程师。我会从原理讲到设计再给一套可以抄作业的轻量实现最后把我踩过的坑一并倒出来。内容不绑定某个具体平台核心思路换到哪家模型都适用。2. 上下文模式的 4 种主流设计从“全塞进去”到“像人一样记忆”既然要管理上下文首先得有设计方案。我见过不少人一上来就直接把所有消息全部拼进 prompt这其实是第一种模式而且是最原始的一种。下面我把 4 种主流设计逐一拆开讲清楚包括它们的原理、优缺点和适用边界。2.1 全量上下文模式简单直接但贵得肉疼全量上下文就是把系统提示词、全部历史对话、检索到的文档、工具返回结果一股脑全拼成一个 prompt 发给模型。它的优点只有一个信息无损模型理论上能看到所有发生过的事。但它的缺点会在对话轮次增加后迅速暴露。假设一轮对话平均消耗 400 tokens20 轮就是 8000 tokens。主流模型的上下文窗口虽然已经涨到 128K 甚至 200K但你的钱包撑不住。我实测过一个简单的 FAQ 机器人用全量模式跑 50 轮后单次请求的 input 已经逼近 20K tokens按当时 GPT-4 的价格算一次请求光输入成本就接近一块钱人民币。而且越到后面模型响应越慢用户体感就是“这个机器人变迟钝了”。还有一个隐蔽问题越长的上下文模型越容易迷失重点。你把 50 轮闲聊和一句关键用户指令都塞进去模型很可能被中间的大量噪音带偏反而忽略最重要的信息。这就像让一个人同时读一本 500 页的小说和一张写满重点的便签他大概率会记住小说情节而忘了便签上的事。所以全量模式的适用场景非常有限对话轮次极少比如 3 轮以内、或者单次请求本来就短的工具调用类应用。但凡要做真正的多轮交互就得换模式。2.2 滑动窗口模式只留最近 N 轮成本瞬间可控滑动窗口是最常见的省钱方案核心逻辑一句话只保留最近 N 轮对话更早的直接丢掉。N 通常根据模型窗口和单轮平均 token 数计算。比如窗口 8K系统提示词占 1K每轮平均 500 tokens那就保留 (8K - 1K) / 500 ≈ 14 轮取个整数 N10留点余量。这个模式的优点是实现极简单加一个deque(maxlenN)就搞定了而且成本稳定可控不会随着对话变长而无限膨胀。但它有一个致命伤早期关键信息会丢失。还是拿客服场景举例用户在第 2 轮说过“我叫王小明用的是 Pro 套餐”到第 12 轮时这个信息如果已经被挤出去模型就只能靠猜。更麻烦的是滑动窗口丢信息不是线性丢失而是“连根拔起”。比如用户在第 6 轮提到过一个产品型号第 10 轮又围绕这个型号问了两个问题第 15 轮你问“所以你刚才说的那个型号有什么问题”模型已经完全不记得了因为它连那段对话一起丢掉了。所以滑动窗口适合什么场景闲聊机器人、一次性问答、或者你确定用户不会在早期留下“必须长期记住”的信息。它适合做“兜底方案”但不适合做核心方案。2.3 摘要压缩模式把旧历史变成“记忆面包”摘要压缩模式是我在实际项目里用得最多的一种思路也非常符合直觉既然旧对话太长那就用模型把旧内容总结成一小段摘要保留要点丢掉细节。比如 20 轮历史对话可能压缩成 300 tokens 的摘要效果好的时候核心事实用户姓名、套餐、诉求都能保留下来。具体做法是设定一个阈值比如当历史消息总 token 数超过 3000 时触发一次压缩。把最旧的 2000 tokens 丢给模型让它总结成“包含用户关键信息、当前诉求、已解决/未解决问题”的摘要然后把这个摘要放在新的系统提示词附近再把最近几轮完整对话拼接在后面。这样做的好处很直观成本被压下来了而且记忆保留能力比滑动窗口强不少。但坏处也有三方面。第一摘要会引入信息损失模型总结时可能丢掉一些你认为重要、但模型觉得不重要的细节第二压缩本身也是一次模型调用会增加延迟和成本如果对话很长可能每隔几轮就要压一次第三摘要的“保质期”有限如果对话跨了多个话题早期话题的摘要后面可能用不上但你还得一直带着它。这里有个经验值摘要压缩适合“单线长对话”比如一对一客服、单用户助理。但如果你的场景是多用户、多话题交叉的纯摘要模式往往不够用需要往下看第四种。2.4 分层记忆模式让 AI 像人一样“想起来”分层记忆是目前最接近“人脑工作方式”的方案也是我最近半年在研究的方向。它的核心思想是不再试图把所有信息塞进同一个 prompt而是把信息按层次存储每次调用时只检索和当前问题最相关的部分。你可以把它理解成三层的记忆体系第一层是工作记忆也就是当前这一轮用户问题、最近的几轮对话、系统指令这些必须完整放进去第二层是短期记忆比如本次会话中已经压缩过的摘要、上一步的中间结果可以按需带入第三层是长期记忆用户的历史偏好、过往订单、知识库内容这些平时存在独立的存储里可以是 Redis、向量数据库、甚至普通数据库只有当模型判断当前问题需要这些信息时才通过检索把它们拉回 prompt。这样做的好处非常明显。首先是成本极低每次调用的输入长度基本恒定其次是记忆能力极强理论上可以记住几个月前的细节只要检索做得足够好最后是抗干扰模型不会因为看到一堆无关历史而迷失方向。但代价也很实际你需要额外写检索逻辑、维护存储、处理向量化如果项目规模不大这套东西的工程成本会超过收益。我见过很多团队为了“显得高级”硬上分层记忆结果检索召回质量很差反而丢掉了本来用摘要模式就够用的效果。2.5 混合模式的选型思路90% 场景的最终答案上面四种模式不是互斥的我实际生产环境中用的几乎都是“滑动窗口 摘要压缩 按需检索”的混合体。基本结构是这样的最近 5 轮完整对话保留在工作记忆里更早的对话压缩成摘要常驻在 prompt 中当用户提到某个历史实体比如“我之前说的那台设备”时触发向量检索把相关历史片段拉回来插进 prompt。选型的判断标准我总结成一句话先看对话轮次要多长再看信息丢失会造成什么后果。如果只是短会话滑动窗口足够如果会话可能超过 10 轮且必须记住用户偏好就必须上摘要如果业务还涉及跨会话记忆比如用户隔天回来继续问那就只能做分层。3. 从零实现一个轻量 context-mode 引擎理论说了那么多不落地就是空中楼阁。下面我给你一套可以直接抄的轻量实现不依赖任何特定框架用 Python 写核心逻辑存储先用内存和 JSON 文件顶着后续要换 Redis 或向量库也容易。这套代码我实际跑过结构清晰适合作为你项目的第一版地基。3.1 数据结构与模块设计先想清楚要管理什么数据。一个完整的 context 应该包含四块内容system_prompt系统指令通常在第一轮固定下来全量保留。recent_messages最近 N 轮原始对话按时间顺序排列。summary旧对话的压缩摘要用文本字符串存。memory_store按需存储关键实体信息比如用户姓名、订单号、偏好键值对即可。对应的模块也分四块token 计数器、上下文组装器、压缩触发器、记忆管理器。其中 token 计数可以先用近似方法后面接真实 tokenizer。我用一个数据类来表示上下文会话from dataclasses import dataclass, field from typing import List, Dict, Optional dataclass class ContextSession: session_id: str system_prompt: str recent_messages: List[Dict] field(default_factorylist) summary: str memory: Dict[str, str] field(default_factorydict) property def recent_token_count(self) - int: return sum(count_tokens(msg[content]) for msg in self.recent_messages)这里的关键是把“最近对话”和“记忆”分开存储。如果你把用户信息混在 recent_messages 里一旦滑出窗口就彻底丢了放进 memory哪怕对话过期也能通过检索找回来。3.2 核心逻辑阈值判断与压缩时机接下来是最重要的部分什么时候触发压缩我的做法是设置两个阈值一个软阈值、一个硬阈值。软阈值recent_messages 的 token 数超过 max_recent_tokens 的 80% 时就把最早的若干条挪进待压缩区。硬阈值recent_messages 的 token 数加上系统提示词和摘要的总 token 数逼近模型窗口上限的 90% 时强制触发压缩。压缩不是把整个 recent 全部一次性压掉而是只压缩最旧的那一批保留最近几轮完整对话。代码逻辑如下MAX_RECENT_TOKENS 3000 # 最近对话保留上限 MAX_TOTAL_TOKENS 7000 # 单次请求总 token 预算含系统提示词、摘要 SUMMARY_TRIGGER_RATIO 0.8 def should_compress(session: ContextSession) - bool: recent_used session.recent_token_count / MAX_RECENT_TOKENS total_used ( session.recent_token_count count_tokens(session.system_prompt) count_tokens(session.summary) ) / MAX_TOTAL_TOKENS return recent_used SUMMARY_TRIGGER_RATIO or total_used 0.9 def compress_old_messages(session: ContextSession, llm_func) - None: # 只取最旧的一半做压缩保留最近几轮细节 split_idx len(session.recent_messages) // 2 old_part session.recent_messages[:split_idx] session.recent_messages session.recent_messages[split_idx:] prompt ( 请将以下对话记录压缩成一段摘要要求保留 1) 所有用户提供的个人偏好和实体信息 2) 用户当前的核心诉求 3) 已经解决的事项和未解决的事项。\n\n \n.join(f{m[role]}: {m[content]} for m in old_part) ) session.summary llm_func(prompt)这段代码有几个细节值得注意。split_idx 取一半是为了避免每次压缩都把全部旧消息清空摘要 prompt 里明确要求保留“个人偏好、实体信息、核心诉求”这是我试过几十次之后总结出来的关键点不写清楚模型就会给你压成一个毫无信息量的“用户与助手进行了对话”。3.3 上下文组装把摘要、记忆、最近对话拼接起来压缩完怎么组装成最终发给模型的 messages顺序很重要我踩过坑之后才明白不是简单把摘要放最前面就完事而是要考虑模型的注意力分布。我的组装顺序是系统提示词 → 用户长期记忆如果有 → 旧会话摘要 → 最近 N 轮原始对话 → 当前用户问题。理由是系统提示词离得近优先级最高长期记忆和摘要提供背景最近对话提供即时上下文当前问题放在最后因为模型对“越靠后的内容注意力越强”。def build_messages(session: ContextSession, current_user_input: str) - List[Dict]: messages [] if session.system_prompt: messages.append({role: system, content: session.system_prompt}) if session.memory: memory_text \n.join(f{k}: {v} for k, v in session.memory.items()) messages.append({role: system, content: f用户已知信息\n{memory_text}}) if session.summary: messages.append({role: system, content: f历史对话摘要\n{session.summary}}) messages.extend(session.recent_messages) messages.append({role: user, content: current_user_input}) return messages这里我把 memory 单独作为一个 system 消息放进去而不是拼在摘要里目的是让模型更容易区分“事实”和“历史过程”。实际效果差别挺明显模型对单独列出的已知信息引用率远高于埋在摘要里的同一句话。3.4 token 估算的两种方式上面代码里的count_tokens是个关键函数。生产环境强烈建议用模型的官方 tokenizer比如 OpenAI 的tiktoken或者 Hugging Face 的transformers。但如果你只是快速验证逻辑可以用一个粗略估算英文按 4 个字符算 1 个 token中文按 1.5 到 2 个汉字算 1 个 token。这个估算会有误差但用来做阈值判断足够了反正你设阈值的时候也会留余量。def count_tokens(text: str) - int: if not text: return 0 # 粗略估算中文字符按 1.8 计英文按 4 字符 1 token chinese_chars sum(1 for c in text if \u4e00 c \u9fff) other_chars len(text) - chinese_chars return int(chinese_chars * 1.8 other_chars / 4)如果你要更精确我建议在本地缓存 tokenizer不要每次调用都重新加载模型文件否则性能会非常难看。我用tiktoken实测过单次计数在毫秒级加缓存之后基本无感。3.5 何时接入 RAG / 工具调用别让检索结果挤爆上下文context-mode 和 RAG 的边界经常有人搞混。我的理解是RAG 是往上下文里灌“外部知识”context-mode 是管理“对话内信息”。两者可以配合但配合方式有讲究。我见过最蠢的写法是每轮对话都把 RAG 检索出的 10 段文档全放进 prompt结果上下文被塞爆模型被文档噪音淹没。正确的做法是先用记忆管理器判断用户问题是否涉及外部知识如果涉及再做检索而且只把 top 2-3 段结果拼进当前这一轮的 prompt。这样既保证了知识可用又不会让上下文无限膨胀。4. 四种模式实测对比数据说话光讲理论容易让人觉得“都是纸上谈兵”所以我专门搭了个测试环境把四种模式跑了一遍真实数据。场景设计是 20 轮客服对话里面包含三类关键信息用户个人偏好姓名、套餐、一次售后诉求换货、一次情绪表达不满发货速度。4.1 测试指标与结果我记录了三类指标关键信息保留率对话结束时模型能否准确回答“用户姓名、套餐、换货进度”三个问题、总 token 消耗20 轮累计消耗的 input tokens、单轮平均延迟模拟真实调用时的响应时间。模式关键信息保留率总 token 消耗约单轮平均延迟实现复杂度全量上下文100%85,000高逐渐变慢无滑动窗口N833%38,000低极低摘要压缩100%41,000中压缩时有峰值低分层记忆100%32,000低-中高这个结果很有说服力。全量模式虽然保留率满分但 token 消耗是分层记忆的 2.6 倍而且延迟随着轮次增加越来越高用户体验极差。滑动窗口 token 控制得不错但关键信息保留率只有 33%名字和套餐全丢了换货进度也断了一半。摘要压缩和分层记忆都能做到 100% 保留但分层记忆的 token 消耗更低因为每次只检索需要的部分而不是带着整份摘要。4.2 结合实际场景选型别盲目追求最复杂的方案上面这个结果容易给人一个误导分层记忆最牛逼你们都用它。但实际工程里真不是这样。如果你的对话平均只有 5 轮滑动窗口完全够用上分层记忆纯属给自己找麻烦——要维护向量库、写召回逻辑、处理相似度阈值这些成本可能比省下的 token 钱还贵。我建议的选型标准是这样的对话轮次短≤ 5 轮且无跨会话需求滑动窗口N6 就够。对话轮次长 10 轮但只在一个会话内滑动窗口 摘要压缩阈值设为摘要保留最近 5 轮。涉及跨会话记忆用户隔天回来继续问在上一套基础上加一层 memory_store存用户实体信息。对话本身依赖大量外部知识客服、专业顾问分层记忆配合 RAG但只在必要时触发检索。4.3 成本估算示例一个月的账单差异假设你有一个日活 1000 人的客服机器人每人每天平均聊 30 轮用 GPT-4 级别的模型假设输入价格 $0.03 / 1K tokens。全量模式的日均 token 消耗约 1,000 × 30 × 1,200平均每轮输入 ≈ 36M tokens日均成本约 $1,080。改成混合模式摘要 滑动窗口 记忆后日均消耗降到约 1,000 × 30 × 400 ≈ 12M tokens日均成本 $360。一个月下来差了两万多人民币。这不是小数目值得你在设计阶段就认真对待。5. 常见问题与排查技巧实录这部分是我这些年攒下来的实战经验每一行都是踩过坑换来的。按出现频率从高到低排。5.1 上下文溢出模型报 “context length exceeded”这是最容易遇到的问题而且多半发生在对话第 30 轮左右因为很多人只在第二十轮的时候想到要压缩上下文结果压缩完还是超额。我排查这类问题有个固定套路第一步看日志里触顶时实际的 total tokens 是多少别猜。第二步检查系统提示词和摘要是否被重复拼接——我见过有人每轮都重新加载一份相同的摘要导致 token 数翻倍。第三步调整阈值模型窗口标称 8K实际跑 7K 就可能报错因为有些框架会在 messages 之外再加特殊 token。我的经验是把硬阈值设在窗口上限的 85%而不是 90%。5.2 用户信息被重复注入context 越长模型越“健忘”有一次我测试一个带 user memory 的机器人发现模型总是把用户当前地址和一个月前的地址搞混。查日志才发现问题出在 memory_store 里的信息没做版本管理旧地址和新地址一起塞给了模型。模型看到两个矛盾信息自然会困惑。解决办法memory_store 里的每一项都要带时间戳组装 prompt 时只取最新版本。如果确实存在历史值需要保留那就加上“用户曾在 X 时间使用过该地址现地址为 Y”这样的显式说明让模型明白哪个是当前值哪个是历史值。5.3 摘要压缩后信息丢失模型“总结”得太笼统摘要压缩最常见的问题是模型把“王小明Pro 套餐”这种关键信息压成了“用户是会员”。我一开始以为是模型笨后来发现是我的摘要 prompt 写得太随意。经过反复调优现在我的压缩指令里会专门追加一句“如果对话中出现人名、地名、产品型号、订单编号、金额、时间、偏好必须原样保留在摘要中。”加完这一句信息保留率立刻从 70% 涨到 95% 以上。5.4 召回相关性差分层记忆成了“摆设”如果你上了分层记忆发现召回的内容驴唇不对马嘴大概率不是检索算法的问题而是切分维度不对。我之前的项目按“时间窗口”切分记忆把一天的对话切成一大块结果检索时相似度计算总被无关内容干扰。改成按“话题轮次”切分每个话题单独存一段之后相关性显著提升。另外还有一个细节检索召回的内容不要直接丢进 prompt最好先用一个小模型或者规则过滤一遍只保留与当前问题明显相关的片段。5.5 成本监控别让模型悄悄吃掉你的预算最后说一个容易被忽略的事情加日志。每次调用 LLM 时把 input tokens、output tokens、触发压缩的次数、检索的次数全部记录下来。这样你月底看账单时才能知道钱花在哪了。我现在每个项目都会在代码里加一个简单的计数器class TokenUsageTracker: def __init__(self): self.total_input 0 self.total_output 0 self.compress_calls 0 def log_call(self, input_tokens, output_tokens): self.total_input input_tokens self.total_output output_tokens def log_compress(self): self.compress_calls 1这个 tracker 不复杂但它能帮你回答两个关键问题压缩次数是不是太频繁检索是不是成了每次请求的固定开销看到数据之后你自然知道下一步该调哪里。最后分享一个自己沉淀的小技巧说了这么多我觉得 context-mode 最核心的心法就一句话别把模型当成一个会记住一切的数据库它只是一个每次拿着你递给它的笔记本来做推理的实习生。你的工作不是让模型“记住”而是设计一套机制把该记的东西在合适的时候递给它把不该记的东西果断拿掉。我自己的项目里现在一直保留着一个习惯每轮对话结束后顺手把 memory_store 里新增或变更的实体打印到日志里。这看起来是个笨功夫但它帮我发现过很多次“模型自己编造用户信息”的问题——当 memory 里的信息和用户实际输入矛盾时一眼就能看出来。如果你准备在自己的项目里落地 context-mode不用一开始就搞得很复杂。先用滑动窗口跑起来加上 token 监控再逐步接入摘要压缩最后根据业务是否真的需要跨会话记忆决定要不要上分层存储。每加一层都要有明确的数据指标来证明它值得——这样你的系统既不会显得简陋也不会背上不必要的工程包袱。