
大概是半年前我接到一个需求给某内部工具加一个 AI 助手。第一版实现很简单——把用户最近几轮对话拼起来连同问题一起发给模型回答完直接返回。demo 演示的时候一切正常产品经理连连点头。结果上线第三天问题就来了。有用户连续问了二十几个问题之后助手开始答非所问确切地说是开始忘记自己十分钟前给出的结论。比如用户问过数据库连接串放在哪个配置项后面又问它支持加密吗助手居然回答我不太清楚你指的它是什么。后台一查日志问题很清楚我把上下文窗口填满了最前面的几轮关键对话被系统直接顶掉模型只能看到最近的三五条消息。这就是 context-mode——上下文模式——要解决的核心问题在大模型对话系统中如何管理模型能看到什么历史信息让它在长对话里既不丢失重点又不超出上下文窗口的限制。如果你正在做聊天机器人、客服系统、代码助手、文档问答这类应用context-mode 就是你绕不过去的一环。我当时被线上问题逼着把整个上下文管理模块重写了一遍期间查了不少资料、试了好几种策略最后沉淀下来一套比较实用的方案。这篇文章就把我的完整思路、代码实现和踩坑记录都写出来给同样在做类似项目的朋友一个参考。1. 当 AI 开始健忘context-mode 到底在解决什么问题1.1 一个让我深夜加班的对话场景先说那个翻车案例的细节。用户是一位内部业务同学他在连续提问的过程中先问过数据库连接串放在哪个配置项得到答复后又聊了十几个不相关的问题最后绕回来问它支持加密吗。我原本以为模型能靠自行推理理解它指的是什么结果模型直接懵了。查日志的时候我发现请求里只带了最近的 5 轮对话而数据库连接串那轮早被挤出了窗口。更尴尬的是这个方案上线前我也测过——只测了 3 轮短对话根本暴露不了问题。后来我把同样的对话模式拿去复现连续发 30 轮不相关的问题再回头追问失忆率几乎是百分之百。这个场景其实非常典型用户不会按你预设的剧本提问他们会岔开话题、会隔很久再回来追问、会在一次会话里处理多件事务。只要对话长度超过窗口容量纯拼接方案就一定会踩雷。所以我后来对 context-mode 的理解是它不是一个锦上添花的功能而是长对话场景下的刚需。1.2 上下文模式的定义与核心价值我这里的 context-mode 不是一个具体产品而是一类实现方案的总称。凡是需要把用户的历史交互信息组织起来喂给大模型的系统都会碰到这个问题。它不仅仅是简单地把消息拼起来至少包含四件事存量管理哪些历史信息该留哪些该丢压缩策略信息太多时如何用更低成本保留语义召回机制需要时如何快速找到被压缩掉的细节配额控制保证请求不超限、成本可预测说实话上下文管理做得好的系统用户是感知不到的做得不好的系统表现就是AI 变笨了AI 失忆了AI 回答自相矛盾。你可能觉得这些都是小问题但在真实项目里它们会直接变成用户投诉和工单。我当时被线上问题逼着把整个模块重写期间试过纯滑动窗口、摘要压缩、检索召回以及它们的组合。这篇文章就是把那段时间的完整思路、关键代码和踩坑经历分享出来。如果你正准备给自己的 AI 应用加上下文管理或者已经被AI 健忘问题折磨过下面这些内容应该能帮你少走不少弯路。2. 先搞懂 token上下文管理的最小计量单位2.1 token 不是字数是模型的思考颗粒做上下文管理之前第一个必须搞清楚的概念是 token。很多刚入门的朋友会把它当成字数这是最大的误解。token 是模型处理文本时的最小单位一个英文单词常常是 1 到 2 个 token而一个中文字符在主流模型的编码方案里大致是 1 到 2 个 token。举个例子同样是今天天气怎么样按字数算是 7 个字但在模型的 tokenizer 里可能是 12 个甚至更多的 token。所以我建议做上下文管理时永远用 token 作为计量单位而不是字符数或行数。原因很简单模型的上下文窗口、计费标准、输出上限全部以 token 为口径。你拿字符数去做预算控制误差会大到完全不可用。有个更直观的理解方式把 token 想象成模型的工作记忆容量。窗口越大模型一次能想起的内容越多但处理速度也会变慢、费用也会变高。你要做的就是在这个容量里精打细算让最值得被记住的信息占据最多的空间。2.2 上下文窗口的三种限制长度、成本、延迟每个模型都有上下文窗口上限常见的有 8K、32K、128K 等。但窗口够大不代表你就能随便塞。实际使用时你会碰到三方面限制长度限制请求里的 prompt token 加上要生成的 output token总和不能超过窗口上限超了要么报错、要么被静默截断。成本限制输入 token 都要计费不同模型单价不同长上下文会显著拉高单次调用的成本。延迟限制输入越长模型理解前置内容花费的时间越长首字返回速度明显变慢。我在实际项目中体会特别深的一点是上下文溢出往往不是某一次请求突然超限而是用户多聊几轮之后慢慢逼近。如果不做任何管理10 轮以内的对话可能问题不大到了 20 轮、30 轮几乎必然会出问题。而且这个问题通常是渐进的、不容易被测试发现的等用户反馈上来往往已经积累了一大批受影响会话。2.3 一个 1000 次对话的账单估算我们来算一笔账帮你建立对成本量级的直觉。假设每次调用的平均输入是 1,500 token一个用户每天对话 30 次那么一个用户大约 22 天就会消耗 100 万 token。如果是日均 100 个活跃用户的项目一天就是 450 万 token 的输入量。按现在主流模型的输入单价折算一个月下来单纯输入成本就相当可观。更关键的是如果加了 context-mode 之后能把每次请求的输入从 1,500 token 压到 600 token同样规模的业务输入成本能下降接近六成。这也是为什么我会在模块里专门做配额控制不只要保证请求不超限还要保证每月的模型账单在一个可控范围内。上线之后你会发现省下来的钱比想象中多得多。3. 三种主流上下文管理策略的对比与选型3.1 滑动窗口最简单但最粗暴滑动窗口的思路最直接只保留最近 N 条消息更早的直接丢弃。它实现成本极低不用额外的模型调用也不用接数据库适合对话轮次少、历史不太重要的场景。但它的缺点也极其明显一旦关键信息出现在窗口之外模型就彻底看不到了。我之前那个翻车案例就是纯滑动窗口的受害者。用户在第 5 轮提到的数据库连接串在第 23 轮还在被追问但窗口只保留最后 8 轮信息早就被挤出去了。而且你没法判断哪条消息重要——窗口内的未必都重要窗口外的未必都不重要一刀切就是赌博。3.2 摘要压缩用理解换记忆摘要压缩的做法是当历史消息将要超出预算时调用一次模型把旧消息压缩成一段摘要用摘要替代原始消息继续参与对话。这样既保留了宏观语义又大幅降低了 token 占用。代价是要多花一次摘要调用的费用而且摘要本身有信息损失。模型在压缩时可能漏掉用户强调过的细节比如某个具体的报错信息、某个精确的配置数值。所以我一般建议摘要只压缩背景类内容对于用户明确强调的关键信息要单独留着别压。这里还想多说一句摘要压缩不是简单的总结一下而是要给模型明确的压缩指令。比如告诉它保留所有数字、时间、命名实体删除寒暄和重复内容用不超过 200 token 输出。没有这些约束模型会用自己的理解改写历史信息失真会非常严重。3.3 检索增强让关键信息随用随取检索增强是我最终采用的核心方案之一。思路是把用户的历史消息按语义向量存入向量数据库每次新问题进来时先做一次相似度检索把与当前问题最相关的历史片段召回再拼进上下文。这种方式的优势在于历史总量可以很大但每次真正进入 prompt 的只有最相关的几百 token成本可控、信息也不容易丢。缺点是需要额外的向量化调用和存储设施工程复杂度明显高于前两种。而且检索质量本身是个新问题召回不准的时候模型反而会被无关的历史片段干扰。3.4 选型判断标准我做了个对比表格方便你快速判断自己的场景适合哪种方案维度滑动窗口摘要压缩检索增强实现成本极低中等较高历史容量小中大信息保真度窗口内高窗口外全丢整体保留但可能失真按需召回保真度高额外调用无每次压缩一次调用每次请求一次向量检索适用场景短对话、客服闲聊中长对话、背景铺垫长记忆、文档问答、个人助手实际项目中我不建议只选一种。更稳妥的是组合方案短对话用滑动窗口中等长度的旧消息用摘要压缩真正重要的关键信息用检索召回。这就是我最终实现的核心思路也是标题里 context-mode 的实际形态——一个按需组合、可配置的上下文管理模式。4. 手写一个轻量 context-mode 管理模块4.1 模块整体设计我先说一下这个模块的设计目标它要能被任何 LLM 应用复用不绑定具体模型供应商只需要一个标准的消息结构。模块内部维护一个消息队列对外暴露三个核心接口add_message把新消息写入队列build_prompt根据当前上下文预算组装最终发送给模型的 prompttrim当队列过长时按照策略压缩或丢弃旧消息我选 Python 来实现因为大部分 AI 应用的后端都是 Python示例代码最容易迁移。如果你用的是其他语言思路完全一样只是语法差异。4.2 核心代码实现先定义一个简单的消息结构from dataclasses import dataclass from typing import Optional dataclass class ChatMessage: role: str # system / user / assistant content: str timestamp: float msg_id: str importance: int 0 # 重要性标记后续压缩用 summary: Optional[str] None # 该消息被压缩后的摘要然后是一个 token 估算函数。注意这里我用的是估算而不是精确计算因为精确 token 数需要跑 tokenizer在每次请求前都完整跑一遍成本太高def estimate_tokens(text: str) - int: # 简单估算中文按 1.5 字符一个 token英文按 4 字符一个 token chinese_chars sum(1 for ch in text if \u4e00 ch \u9fff) other_chars len(text) - chinese_chars return int(chinese_chars * 1.5 other_chars / 4) 2这种估算方式比较粗略但对预算控制已经够用。如果你的项目对 token 精度要求高建议在模块里缓存 tokenizer 结果只对新增消息做精确计算兼顾精度和性能。核心的 context manager 类import time import uuid class ContextManager: def __init__(self, max_tokens: int 8000, reserve_output: int 1000, strategy: str hybrid): self.messages: list[ChatMessage] [] self.max_tokens max_tokens self.reserve_output reserve_output self.prompt_budget max_tokens - reserve_output self.strategy strategy # sliding / summary / hybrid def add_message(self, role: str, content: str): msg ChatMessage( rolerole, contentcontent, timestamptime.time(), msg_iduuid.uuid4().hex, ) self.messages.append(msg) self._maybe_trim() def build_prompt(self) - list[dict]: selected [] used 0 for msg in reversed(self.messages): text msg.summary or msg.content cost estimate_tokens(text) if used cost self.prompt_budget: break selected.append(msg) used cost # 保证 system 消息始终在最前面 system_msgs [m for m in reversed(selected) if m.role system] others [m for m in reversed(selected) if m.role ! system] return [ {role: m.role, content: m.summary or m.content} for m in system_msgs others ]这里有个细节很容易被忽略build_prompt 里我单独把 system 消息挑出来放到最前面。因为很多模型的 system 指令必须出现在 prompt 开头否则角色设定会失效。我第一次写的时候没注意顺序导致助手偶尔会用奇怪的口吻回答排查了很久才发现是消息顺序问题。4.3 trim 的压缩逻辑摘要和裁剪怎么配合接下来是关键的 trim 逻辑。我采用的是 hybrid 策略分两步def _maybe_trim(self): total sum( estimate_tokens(m.summary or m.content) for m in self.messages ) # 低于预算 80% 时不触发避免频繁调用模型 if total self.prompt_budget * 0.8: return # 第一步对最老的一半消息尝试摘要压缩 old_msgs self.messages[: len(self.messages) // 2] for msg in old_msgs: if msg.summary is None and msg.importance 0: msg.summary self._summarize(msg.content) # 第二步如果压缩后仍超限删除低价值且已被压缩的最老消息 after_summary sum( estimate_tokens(m.summary or m.content) for m in self.messages ) if after_summary self.prompt_budget: self.messages [ m for m in self.messages if not (m.importance 0 and m.summary is not None) ]_summarize 方法内部其实是一次带压缩指令的模型调用指令会明确要求保留数字、时间、命名实体等关键信息。实际运行时摘要压缩不会每次触发只有当总 token 超过预算的 80% 时才启动。这个阈值我调过几次80% 是比较平衡的太早压缩费钱太晚又可能偶发超标。接入 AI 接口的完整链路非常简洁用户输入 - add_message 记录 user 消息 - 调用 build_prompt 得到消息数组 - 调用模型 - 把返回结果 add_message 记录 assistant 消息 - 等下一轮。核心就是这样一个循环。你可以在 build_prompt 外面包一层日志把每次拼出来的 prompt 存下来后面排查问题会非常有用。5. 实测踩坑记录从看起来能用到真正能用5.1 截断方向错误模型看不到最新问题第一个坑是 build_prompt 的截断逻辑。我最初是从最旧的开始保留保留到预算耗尽就停。结果模型只能看到老的对话看不到用户的最新问题回答完全不在点上。那段时间线上反馈答非所问的比例反而更高了。后来我才意识到截断必须从最新往回选用户当前的问题永远是最高优先级越往后的历史消息越重要。这听起来像常识但写代码的时候很容易搞错方向。我建议你加一个单元测试构造 50 轮对话断言最新一条消息一定出现在 build_prompt 的结果里。这个测试成本极低能挡住最蠢的回归。5.2 摘要漂移模型越压越走样第二个坑是摘要压缩的质量问题。我第一次压缩时直接让模型总结上述对话结果模型把用户提到的具体数字、截止时间、排除条件全总结掉了。后续回答明显变得含糊因为模型记忆里只剩下模糊的语义。我的应对方案有两个。一是拆分压缩把长消息拆成若干小段分别压缩再拼接避免单次压缩信息丢失。二是重要性标记对 importance 为 1 的消息不压缩只压缩普通消息。通过对用户问题做简单的关键词判断把包含必须不要记住等强指令的内容标为高重要性宁可多留一点也不能漏掉关键约束。5.3 检索召回与摘要内容打架第三个坑出现在接入检索增强之后。有时候检索回来的历史片段和当前上下文里的摘要内容冲突比如摘要说用户选择了方案 A但某条原始片段里用户说的是方案 B 也可以考虑。模型看到矛盾信息后回答变得犹豫不决。我的处理方式是在 prompt 里给信息来源加标注检索片段标注来自早期对话记录摘要标注来自对话要点同时限制召回数量默认只取 top 3。加了标注之后模型遇到信息冲突时至少会主动向用户确认而不是自己瞎猜。这个改动的效果立竿见影冲突类投诉直接清零了。5.4 成本与延迟的实测数据最后给一组我实测到的数据。一个日活 500 左右的小项目纯滑动窗口方案的日均输入 token 约 380 万换成 hybrid 组合方案之后日均输入降到 160 万左右下降了接近六成。代价是每天多了几百次摘要压缩调用综合算下来总成本仍然是明显下降的。延迟方面由于进入 prompt 的 token 更少首字返回平均快了接近 30%。这两个数据让我彻底确信做好 context 管理不只是为了避免报错更是为了省钱和省时间。如果你的项目规模更大这个收益还会被进一步放大。6. 参数调优与我的经验值6.1 关键参数速查表我把自己最后沉淀下来的参数配置整理成表格你可以作为起点再根据你的模型和场景调整参数我的推荐值说明max_tokens模型窗口的 70%留出余量给输出和其他系统占用reserve_output窗口的 15%-20%太大浪费输入额度太小会截断回答trim 触发阈值预算的 80%太早压缩费钱太晚可能偶发超标摘要压缩批大小4-8 条消息一批太小费调用太大容易丢细节检索召回数量3 段越多越容易引入噪声重要性标记阈值含强指令词即标记宁可多留一点不可漏掉关键6.2 我个人推荐的冷启动配置如果你是第一次做 context 管理我建议不要一上来就搞大而全的检索增强。先跑通滑动窗口观察线上日志里的 token 消耗和报错情况然后加摘要压缩最后再加检索。每一步加上去之后都要用真实对话样本回放测试而不是只看单轮表现。我自己重写模块后的第一周专门写了一个回放脚本把线上真实的对话记录一条条喂给新模块比较它生成的 prompt 与旧方案的差异。这个脚本帮我发现了至少三个隐藏问题比如 system 消息顺序、时间戳重复、压缩后消息丢失 role 标记。强烈建议你也做同样的事——回放测试是验证上下文管理效果最直接的手段。6.3 最后的几个提醒代码层面的东西上面都说得差不多了最后分享几个我实操下来的体会。不要迷信大窗口。128K 的窗口确实诱人但窗口越大成本越高、延迟越高而且模型对中部长内容的注意力本来就容易衰减。与其盲目扩大窗口不如做好内容的取舍。我见过不少团队上了大窗口模型后彻底躺平不管理上下文结果账单翻了几倍效果反而没提升。摘要不是缓存它是理解。别想着压缩一次就永远能用用户的意图会变化压缩过的信息也可能需要再次确认。必要的时候要允许用户主动追加澄清信息而不是让模型基于一份可能失真的摘要硬猜。任何 context 管理方案都必须有观测手段。我给模块加了 token 消耗日志和 prompt 预览接口线上出了问题能直接看到模型到底看见了什么。这一条救了我很多次——没有观测能力的系统问题排查就是大海捞针。每次线上反馈 AI 回答不对我第一件事永远是打开日志看当时喂给模型的 prompt绝大多数问题当场就能定位。context-mode 不是一个能一劳永逸的功能它更像是一个持续调优的过程。你的模型会升级、用户的使用习惯会变化、成本预算也会调整所以把模块设计成可配置、可观测的结构比一次性写一个完美方案重要得多。真到了上线半年后再回头看你会发现当初的很多决策都还有优化空间而这恰恰说明这套体系在跟着业务一起成长。