ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

大模型上下文管理实战:四种模式与token预算优化

2026/10/7 14:02:19 拓冰建站 浏览量
大模型上下文管理实战:四种模式与token预算优化 1. context-mode是什么先搞清上下文管理的核心问题1.1 从一次失忆的对话说起如果你做过大模型应用开发大概率遇到过这个场景用户连续追问十几轮之后AI突然答非所问或者干脆表示我不记得你之前说过了。这时候懂行的人会告诉你——上下文窗口满了早期内容被挤掉了。这个问题的根子就是context-mode上下文模式。说白了它是决定每一轮请求里模型能看到哪些历史信息的管理策略。我在做AI客服、文档问答这类项目时几乎每天都要跟它打交道。可以这么说上下文管理做得怎么样直接决定了一个AI应用是看起来很聪明还是看起来像个智障。1.2 上下文模式的本质在有限窗口里做最优记忆分配理解context-mode之前得先弄清一个关键限制大模型的输入窗口是有限的。哪怕号称能处理200K token的模型真把这么多内容全部塞进去也会面临两个致命问题。第一个是费用。现在主流大模型API基本按token计费每多带一轮历史对话就要多付一份钱。一个日活一万的客服机器人如果每轮都带全部历史月底账单能让人心梗。第二个是注意力稀释。模型对输入中间部分的内容记忆最差越长的上下文越容易忘掉关键信息。我做过一个测试把一份50页的产品手册塞进上下文然后问具体参数模型给出的答案经常是错的因为它根本没注意到藏在第30页的那行小字。所以context-mode的核心不是简单地多带点历史或少带点历史而是在有限的窗口预算里决定保留什么、压缩什么、丢弃什么。这个决策逻辑就是整套上下文管理模式的设计重心。1.3 这个内容适合谁读如果你正在做这几类事情这篇文章会非常对胃口用API开发聊天机器人、做RAG知识库问答、写Agent类应用或者只是觉得我的prompt明明写得很好但AI用起来还是蠢。我会从模式原理讲到代码实现再分享一批实际踩坑记录帮你避掉那些文档里查不到的雷。2. 四种主流上下文模式拆解选型前先看懂原理2.1 全量上下文模式最省心的基础方案所谓全量上下文模式就是每轮对话都把全部历史消息组装好一股脑丢给模型。这是最简单、最直观的做法新手接入API时基本都会先这样写。它的优势非常明显实现成本低模型能看到完整的对话脉络对用户意图的理解最准确。在对话轮数少、历史总量可控的场景下比如10轮以内的闲聊机器人或单轮但输入较长的文档分析全量模式完全够用效果也最稳。但它有个硬伤不可扩展。假设每轮用户说200字、AI回复500字一轮对话大约消耗700 token20轮就是14000 token。如果你们的产品要求AI记住30轮以上或者每次请求还要附加固定的系统指令和参考资料窗口会很快被塞爆。更隐蔽的问题在于垃圾信息累积。用户中途说的今天天气不错、AI回应的是的呢请问还有什么可以帮您这类寒暄内容对当前问题的判断毫无帮助却实打实占着token预算。全量模式等于把这些噪声全部保留既不经济还可能干扰模型对真正关键信息的注意力。所以我的建议是全量模式适合项目原型期和低轮次场景但产品一旦要上线一定要切换到更精细的管理方式。2.2 滑动窗口模式控制成本与延迟的实用派滑动窗口Sliding Window的思路很直接只保留最近N轮对话更早的一律丢弃。它像一个永远在移动的取景框只关注眼前这一片。实现上就是在组装消息时对历史消息列表做一次裁剪def build_sliding_window_messages(messages, max_rounds10): # 假设每条消息是 {role: user/assistant, content: ...} # 至少保留system指令 system_messages [m for m in messages if m.get(role) system] history [m for m in messages if m.get(role) ! system] # 只取最近max_rounds轮的对话一轮算2条消息 recent history[-(max_rounds * 2):] return system_messages recent这个模式的优点很明显token消耗稳定可控请求延迟不会有大的波动代码实现也简单。但它有一个所有人都绕不开的痛点——切断记忆。用户20轮前提到的重要需求一旦窗口滑过模型就再也看不见了。我见过不少项目就是在这里翻车的。比如一个保险理赔助手用户在第3轮填了出生日期第18轮问按我之前的年龄算保额是多少滑动窗口模式下模型直接懵了。这种问题不是调prompt能解决的是数据结构上的缺失。所以滑动窗口模式适合什么场景呢对近期信息敏感、对远期信息不敏感的对话比如技术客服排障用户最近两步的操作记录最重要、闲聊陪伴类产品。如果你判断用户随时可能引用早期信息那就得考虑摘要模式或检索模式。2.3 摘要压缩模式让记忆提纯的关键路径摘要压缩模式Summarization Mode解决的是滑动窗口一刀切的问题我不直接删掉老对话而是把它们浓缩成一段摘要随请求一起传给模型。这样既保留了长期记忆又控制了token开销。它的流程分三步设定一个触发阈值比如历史消息超过10轮。把第1到第N轮对话发送给一个摘要模型要求它输出一段包含关键信息的摘要。后续请求组装的顺序变成系统指令 历史摘要 最近几轮完整对话。这里有个细节值得注意摘要不是只做一次而是滚动更新。每过几轮新对话就要把旧摘要和新对话合并再压缩成新摘要。我通常用二次摘要法第一次先把每5轮对话压缩成一小段第二次再把多段小摘要合并成一段话。这样能避免一次性压太多内容导致信息丢失。我自己实践下来的摘要提示词是你是对话摘要引擎。把下面的对话内容压缩为一段不超过200字的摘要。 要求 1. 保留所有具体数字、日期、姓名、地点等硬信息 2. 保留用户明确表达的偏好、需求、承诺与情绪倾向 3. 保留未解决的待办事项 4. 忽略寒暄、语气词、重复表达。摘要压缩模式最大的好处是让模型拥有真正意义上的长期记忆而且每次请求的token增长是缓慢的只增长摘要长度而不是增长全部历史。代价也很明显摘要过程本身有信息损耗而且需要额外调用一次摘要模型增加了延迟和费用。做这类模式时我强烈建议把摘要结果也持久化存储存到Redis或者数据库里而不是每次实时生成。否则每轮请求都要重新压缩一遍历史成本和速度都吃不消。2.4 检索增强模式把外部知识装进上下文检索增强模式也就是常说的RAGRetrieval-Augmented Generation是目前企业级应用里最热门的上下文管理方案。它的思路和前面几种完全不同不再保留对话历史而是从知识库里按需检索相关内容拼进当前请求里。以做AI客服为例。用户问你们的耳机支持蓝牙5.3吗传统的全量模式是把整个产品手册塞进上下文而检索增强模式是先把这个问题做向量化去知识库里捞出几段最相关的产品参数说明再把参数片段 最近几轮对话一起交给模型。这种模式最大的价值是打破了上下文窗口的物理限制。你不需要把几十万字的文档全部塞进每次请求而只需要找到当前这个问题最需要的那几百字。同时知识库可以随时更新不需要重新训练模型。但检索增强模式也不是银弹。最典型的翻车案例是检索不到导致幻觉知识库切得不够细、向量召回不准确模型找不到答案就开始一本正经地编。另外检索出来的片段之间如果存在逻辑断裂模型的回答质量也会明显下降。所以RAG对技术栈的要求更高。你需要搭向量数据库、选嵌入模型、设计切分策略、调召回阈值。这也是为什么很多团队最终选择混合模式核心业务知识用RAG对话记忆用摘要压缩近期对话用滑窗——几个模式协同工作各干各的活。我把这四种模式的取舍整理成一个对照表方便选型时快速参考模式长期记忆能力成本/延迟实现复杂度最适合场景全量模式一般受窗口限制随对话轮数线性增长极低原型、短对话滑动窗口差远古信息丢失恒定低强近期依赖的排障类摘要压缩强但存在损耗中需要额外摘要调用中需要长期记忆的陪伴/顾问类检索增强受知识库覆盖度影响中依赖检索链路较高知识密集型的问答/客服3. 实操从零实现一个可切换的context-mode管理器3.1 基础数据结构与token预算计算动手写代码之前先把底层的token预算算法搞定。我做上下文管理时第一步永远不是写消息组装函数而是先写一个预算计算器因为所有模式都是在一笔固定预算里做取舍。假设你的模型上下文窗口是8000 token你需要预留这几个部分系统指令固定消耗比如500 token输出空间给模型回复预留的通常设置max_tokens1000安全余量建议留15%到20%防止tokenizer估算误差导致超限剩余部分才是给对话历史和额外信息用的写成代码就是import tiktoken class ContextBudget: def __init__(self, max_window8000, max_output1000, system_prompt, reserve_ratio0.15): self.encoder tiktoken.get_encoding(cl100k_base) self.max_window max_window self.max_output max_output self.reserve_ratio reserve_ratio self.system_tokens len(self.encoder.encode(system_prompt)) def available_tokens(self): # 窗口总大小 - 系统指令 - 输出空间 - 安全余量 total self.max_window - self.system_tokens - self.max_output return int(total * (1 - self.reserve_ratio)) def count_tokens(self, messages): # 计算一组消息的token总量注意tiktoken的per-message格式 total 0 for msg in messages: total len(self.encoder.encode(msg.get(content, ))) total 4 # 每条消息的元数据开销role标记等 return total这里的核心思想是先算可用的、再往里装内容。很多开发者习惯先组装消息再截断结果系统指令或输出空间被压缩模型行为变得极其不稳定。我踩过这个坑有次把系统指令截断了一半模型直接开始乱答排查了半天才发现是token预算分配的问题。3.2 滑动窗口模式的代码实现基于预算计算器滑动窗口模式可以写成这样class SlidingWindowContext: def __init__(self, budget: ContextBudget, max_rounds12): self.budget budget self.max_rounds max_rounds def build(self, history: list, system_overrideNone): system system_override or history[:1] dialog [m for m in history if m[role] in (user, assistant)] # 方案A按轮数裁剪 trimmed dialog[-(self.max_rounds * 2):] # 方案B按token预算裁剪更稳 while len(trimmed) 2 and self.budget.count_tokens(trimmed) self.budget.available_tokens(): trimmed trimmed[2:] # 从头丢掉最早的一组问答 return system trimmed注意我在代码里写了两种裁剪策略。按轮数裁剪简单但不同用户输入的message长度差异极大——有人一句话20字有人一句话500字,固定轮数根本拦不住token溢出。所以我一般推荐先按轮数粗裁再按token预算精裁两头都卡住。这里还有一个隐患裁剪时只能成对丢弃userassistant不能只丢assistant回复而保留user问题。否则模型会面对一个没有回答半截的问题容易产生胡编乱造的倾向。成对丢弃虽然会多损失一点信息但能保住对话的因果完整性。3.3 摘要压缩模式的实现要点摘要模式的实现框架我也不藏着。它的核心是一个摘要管理器维护当前摘要 未压缩的最近对话并决定什么时候触发压缩class SummaryContext: def __init__(self, budget: ContextBudget, compress_after_rounds6): self.budget budget self.compress_after_rounds compress_after_rounds self.summary # 已经压缩过的历史摘要 self.recent_history [] # 尚未压缩的近期对话 def add_message(self, msg): self.recent_history.append(msg) if len(self.recent_history) self.compress_after_rounds * 2: self._compress() def _compress(self): # 旧摘要 新对话 → 新摘要 # 这里调用一次LLM完成压缩 combined f已有摘要{self.summary}\n\n新增对话{self.recent_history} new_summary call_summary_llm(combined, max_length200) self.summary new_summary self.recent_history [] def build(self): return [{role: system, content: f对话历史摘要{self.summary}}] self.recent_history注意这个实现和我在第2.3节说的二次摘要法有些差异——先简单滚动压缩如果摘要本身超过了200字再触发二次压缩。实际项目中建议把这两层都做上防止摘要越滚越长。摘要模式的姨妈期问题我自己给它起的名字在于摘要内容的时效性。如果用户在最近几轮里改口了之前说要黑色现在改成白色摘要里还留着用户喜欢黑色模型就容易犯矛盾。处理办法是在压缩提示词里加一句如果新对话与旧摘要存在冲突以新对话为准并在摘要中标注变更。3.4 检索增强模式的嵌入与召回做RAG模式的上下文管理核心就一句话把对话历史管理和知识检索结果分开组装。class RagContext: def __init__(self, budget: ContextBudget, vector_store, embed_fn): self.budget budget self.vector_store vector_store self.embed_fn embed_fn def build(self, query: str, history: list): # 1. 向量化用户当前问题 query_vec self.embed_fn(query) # 2. 从知识库召回topk相关片段 retrieved self.vector_store.search(query_vec, top_k3) # 3. 用预算分配知识片段优先历史对话次要 parts [] # 根据实际情况设置比例一般知识片段占大头 knowledge_limit int(self.budget.available_tokens() * 0.6) for doc in retrieved: if self.budget.count_tokens(parts, doc) knowledge_limit: parts.append(doc) # 4. 剩余预算给近期对话 history_limit self.budget.available_tokens() - knowledge_limit recent history[-4:] while recent and self.budget.count_tokens(recent) history_limit: recent.pop(0) return [ {role: system, content: 你是基于知识库回答问题的助手...}, {role: user, content: f参考信息\n{format_docs(parts)}\n\n历史对话\n{format_history(recent)}\n\n当前问题{query}} ]这里有个非常重要的经验知识片段必须带引用来源。也就是每段从知识库召回的内容后面要跟着出自哪个文档、哪个章节。这不仅能帮模型理清依据更能在后续排查时帮你定位模型答错了到底是知识库没收录还是召回的片段不对。召回数量也不是越多越好。我之前测试过top_k从3调到8回答质量不升反降——大量无关片段混进来反而干扰了模型对重点信息的注意力。现在业界比较共识的做法是先做粗召回取top20做rerank精排后只留top3效果比直接取top8好得多。4. 实践中的坑上下文模式切换与稳定性问题排查4.1 模式切换时的上下文断裂很多项目会遇到一个尴尬时刻全量模式跑得好好的为了省钱切到滑动窗口结果用户立刻发现客服失忆了。这不是切换本身的问题而是没有做切换缓冲。比如用户说之前你不是推荐过某某产品吗滑动窗口模式下这句历史已经被裁掉了模型当然答非所问。我的处理方案是渐进式切换新用户直接用滑动窗口模式老用户保留一份全局摘要用摘要模式生成对话前先注入摘要再叠加滑窗。这样既省token又不会让老用户体验断崖式下降。具体操作是在切换前跑一次离线脚本把每个活跃会话的历史批量压缩成摘要存起来。4.2 token估算不准导致的截断tiktoken和模型实际的tokenizer之间绝大多数情况下是一致或接近的但有一个特例某些模型的tokenizer版本更新后对代码、特殊符号的编码方式变了。如果你之前是在旧模型上用cl100k_base估算换到新模型后token预算计算就会产生偏差。排查这个问题我建议在日志里加一个字段每次请求记录估算token和实际token消耗。实际消耗可以从API返回的usage. total_tokens里拿到。连续跟踪几天你会发现偏差比例基本稳定把这个系数写进预算公式里就行。比如我的经验是中文对话场景下tiktoken的估算值通常比实际值高5%到8%这个余量可以适当从reserve_ratio里扣除。4.3 摘要失真与信息丢失摘要压缩模式最头疼的问题就是压没了关键信息。尤其用户提到的具体数字摘要模型经常觉得不重要就给丢了但下游任务恰恰需要这个数字。解决思路分两层。第一层是在摘要提示词里强调数字、编号、日期必须原样保留大部分时候有效。第二层是关键信息旁路在对话过程中单独把用户提到的实体信息电话、订单号、地址、偏好设置等抽取出来存成结构化的用户画像字段每次组装消息时直接注入不依赖摘要。这样即使摘要漏了画像字段也能兜底。4.4 不同模型对上下文格式的敏感度最后这个坑最隐蔽同一个context-mode代码换一个模型效果就变了。我踩过一次很典型的例子。GPT-4时代把历史摘要放在system字段里效果很好。但换了某个开源模型后同样把摘要放system里模型经常忽略摘要内容只盯着最后几句对话。后来排查发现这个模型的system指令遵循能力较弱对system字段里的长文本不敏感。后面我把摘要从system挪到user字段的最前端用对话历史摘要xxx的格式和当前问题拼在一起效果立刻正常了。所以结论是上下文模式不能只做一套就通用每个模型都要做单独的格式适配测试。低成本的做法是准备一个上下文体检集包含5类典型问题引用早期信息类、多跳推理类、指令遵循类、知识检索类、追问类换模型后先跑一遍体检再决定消息格式怎么组织。5. 一些实操感受写了这么多最后说点我自己的体会。context-mode这个东西听起来是个具体的功能其实是一整套预算思维。每次组装请求之前你都要先想清楚这笔token预算系统指令占多少、知识片段占多少、历史对话占多少、模型输出留多少。预算定好了模式选型才有意义不然就是瞎调。我个人的建议是新项目不要一上来就追求最复杂的RAG摘要滑窗混合架构。先用全量模式把业务逻辑验证清楚再根据用户真实对话中出现的问题成本失控、记忆丢失、知识不足一步步加对应模式。很多团队开局就上大而全的架构结果每个环节都跑不稳出了问题都不知道该查哪一环。另外不管用哪种模式日志记录一定要做两层一层记组装给模型的完整消息一层记模型的完整输出。这样将来用户投诉AI怎么胡说八道时你能立刻复盘出到底哪一步出了问题——是历史没带上是摘要压丢了信息还是知识库压根没这内容。省下的排查时间远大于记日志带来的那点存储成本。上下文管理这条路越往后做越觉得细节多。但只要把预算、模式、日志这三件事抓好大部分问题都能在可控范围内解决。希望这篇东西能帮你少踩几个坑。