资讯详情

资讯详情

大模型context-mode实战:三种上下文管理模式与调优

最近半年我身边的 AI 应用开发者几乎都在聊同一个词context-mode。这个词没有标准定义但大家实际指的都是同一件事——在调用大模型时怎么组织、裁剪、管理送进上下文窗口里的那堆内容。你可以把它理解成给模型配一个管家什么该记、什么该扔、什么该压缩、什么该去翻旧档案都由这个管家决定。它之所以在圈子里成为热词是因为上下文窗口越开越大大家却发现一个反直觉的现象塞进去的内容越多模型回答的质量反而可能越差。这篇文章我想从一线工程的角度把 context-mode 的全貌拆开讲清楚——包括三种主流实现形态、一个可以直接抄走的最小实现、我实践过程中踩过的几个典型坑以及实测有效的调优手段。无论你是在做聊天机器人、Agent 框架还是 AI 客服这套东西迟早都会碰上。1. 为什么 context-mode 会成为热词上下文不是越大越好1.1 大模型本质上没有记忆上下文管理就是重建状态先明确一个底层事实大模型本身是没有记忆的。它每次收到请求看到的都是你这次一次性扔进去的完整文本。所谓多轮对话本质上是把历史消息一遍又一遍地拼接进请求里。这个拼接进来的内容就是上下文。所以我们可以把 context-mode 看作是为无状态模型重建状态的工程手段。为什么要重建状态因为几乎所有像样的应用都离不开它。客服机器人要记住用户报过的订单号写代码助手要记得你正在改哪个文件、定义了哪些函数Agent 要记得自己已经执行到哪一步、拿到了什么中间结果。没有一套可靠的上下文管理方案这些应用全都只能聊一句忘一句产品体验会非常糟糕。1.2 窗口变大了成本和质量问题反而更突出早期的 GPT-3.5 时代上下文窗口只有 4K 左右那时大家天天琢磨怎么省 token。后来主流模型陆续开到 128K、200K甚至有些模型已经支持 1M 级别的窗口。照理说窗口变大了上下文管理应该不需要了吧实际恰恰相反。原因有两个层面。一是成本token 是计费的输入 token 的价格直接影响运营成本。你让用户聊了 50 轮每轮都把所有历史塞给模型一次请求可能就要几千甚至上万 token一个月下来账单会非常可观。二是质量模型对超长上下文的注意力是有限的。很多实测和论文都表明当上下文超过一定长度后模型对中间位置的信息利用率显著下降——这就是圈内常说的 lost in the middle 现象。把一万行日志塞进去模型很可能只盯着开头和结尾看中间的关键错误信息反而被忽略。1.3 热词背后的真实需求从塞得下到用得好所以 context-mode 这套东西火的根本原因其实是大家从怎么把内容塞进窗口转向了怎么让窗口里的内容高效服务于回答。这不只是工程优化而是直接决定了产品效果。我见过不少团队把检索结果一股脑全塞进提示词指望模型自己挑重点结果模型把不相干的文档信息也当成了依据一本正经地编出错误答案。context-mode 要解决的本质上就是这类上下文污染问题。其实这个问题在业内早就被注意到。比如吴恩达在 2024 年就提过 context rot上下文腐化的概念当上下文窗口里的信息越来越多、越来越杂时模型对早期关键信息的关注度会持续下降最终导致应用整体表现越来越差。这就是为什么有人说上下文管理是 RAG 之外的另一种检索——它是在时间维度上做检索和取舍核心始终是留什么、扔什么。2. 三种主流的 context-mode 实现形态别死磕一种任何 context-mode 的核心决策都是回答两个问题留什么、扔什么。不同实现方式对这两个问题的答案不同适用的场景也不同。2.1 滑动窗口模式最简单但记忆会断滑动窗口是最朴素的一种实现维护一个消息队列只保留最近 N 条消息超出部分直接丢弃。每次请求时把这个队列里最新的消息按顺序拼接给模型。滑动窗口的实现成本几乎为零一个数组加上入队时判断长度、超了就 shift 的逻辑就够了。它特别适合那些消息之间独立性较强、早期信息很少被再次引用的场景比如闲聊机器人、日志问答、一次性文档分析。但它的缺点也很明显早期关键信息会永久丢失。举我实际遇到过的一个例子做一个售后客服机器人用户在第三轮报了订单号客服处理过程中模型需要反复引用这个订单号。结果聊到第 15 轮订单号被滑出窗口模型开始一本正经地让用户再提供一下订单号体验非常出戏。所以滑动窗口适合的场景一定是对最近发生了什么敏感、对很久以前说过什么不敏感的场景。2.2 摘要压缩模式用信息密度换记忆长度摘要压缩模式的做法是当历史消息超过某个阈值时把较早的那部分消息交给模型生成一段摘要压缩成一小段文本放入上下文被压缩掉的原始消息就可以丢弃了。这个模式的关键在于什么时候触发压缩和摘要怎么和剩余原始消息共存。我的经验是触发点不要定得太晚——如果等窗口快满才压缩压缩必然阻塞在请求路径上用户能明显感觉到卡顿。更靠谱的做法是设置一个水位线当历史 token 超过总预算的 60%~70% 时就用异步任务把最早的一半消息整理成摘要等到下一轮请求时摘要已经就位。摘要压缩模式适合绝大多数产品型对话应用客服、陪伴类、角色扮演、写作助手等。因为这类对话中早期对话大部分是寒暄、铺垫、过程性内容真正需要长期记住的往往只是几个事实——用户的称呼、偏好、关键决定。一段 200 字的摘要足以替代几千 token 的原始对话。2.3 检索增强模式把上下文从内存搬到硬盘如果你做的应用允许超长会话或者需要跨多天、甚至跨用户会话复用信息那摘要压缩也会撑不住——摘要再压缩总量还是会涨。这时候就需要检索增强模式把历史消息全部落库向量库、关系库都行每次请求前根据当前问题检索出最相关的若干条历史消息拼进上下文。检索增强模式本质上和 RAG 是一套思路只是检索的对象从外部知识库变成了对话历史。实现上比前两种复杂需要做消息切片、向量化、维护索引还要设计检索的召回策略。但它换来的是记忆几乎无上限——理论上一个用户聊了一年你也能在每次请求时精准把最关键的那几轮对话捞回来。2.4 三种模式怎么选一张对比表维度滑动窗口摘要压缩检索增强实现成本极低中等高每次请求开销低中摘要本身占 token中检索耗时 额外请求长期记忆能力无中取决于摘要质量强信息失真风险低丢就丢了不编造中摘要可能漏关键事实中检索可能召回不相关内容适合场景短会话、独立性强的问答产品型多轮对话超长会话、跨会话记忆、Agent这里我得说句实话这三种模式不是互斥的。我见过做得好的系统基本都是复合形态——底层用向量库做检索中层用摘要维持连续对话感顶层用滑动窗口保证模型始终能拿到最近几轮的原话。后面第三部分我会给出一个把三者想法融合起来的最小实现。3. 手写一个最小可用的 context-mode核心代码与设计决策这部分我给出一套可以直接改造成自己项目的 Python 实现思路。它不一定适合所有场景但把最核心的决策点和代码骨架都覆盖了你可以按需调整。3.1 先把消息建模好而不是直接操作字符串任何 context-mode 的第一件事是把消息建模好而不是直接拼字符串。消息要承载的元信息比你想象的多角色、内容、消息 ID、时间戳、token 数、所属会话可能还需要给某些消息打标签。我一般用这样的结构from dataclasses import dataclass, field from typing import Optional dataclass class Message: role: str # system | user | assistant | tool content: str msg_id: str created_at: float token_count: int 0 # 用于检索增强模式这个消息属于哪个会话/任务 session_id: Optional[str] None meta: dict field(default_factorydict)每条消息记录 token 数很关键否则无法做预算管理。为了算 token我建议在写入时就调用 tiktoken 之类的工具算好存起来而不是每次请求时临时算——临时算在高并发下会白白浪费不少 CPU 时间。3.2 上下文预算分配钱花在刀刃上窗口再大也是有限的所以你一定要先给上下文预算做个分配。我的默认分配策略大概是这样的假设总预算 8000 token组成部分预算说明系统提示词1000放角色设定、输出格式要求尽量精简摘要块1000对早期对话的压缩摘要检索块800从历史中找回的相关消息最近对话原话4000直接保留的最近若干轮保证临场感响应预留1200给模型输出留空间这个分配不是固定的但它体现了两个原则一是最近的消息永远优先级最高因为绝大多数回答都基于最近几轮二是给输出留预算很多团队忘了这一点输入塞得太满模型输出经常被截断反而造成更差的体验。3.3 核心类 ContextManager 的实现下面是一个最小实现的核心逻辑我把滑动窗口、摘要压缩、检索三个部分都织进去import tiktoken class ContextManager: def __init__(self, token_budget8000, max_recent_messages12): self.token_budget token_budget self.max_recent_messages max_recent_messages self.messages: list[Message] [] self.summary # 压缩摘要 self.encoding tiktoken.encoding_for_model(gpt-4o) self.retriever None # 可以挂接任意向量检索实现 def add_message(self, message: Message): message.token_count len(self.encoding.encode(message.content)) self.messages.append(message) self._maybe_compress() def _count_tokens(self, blocks: list[str]) - int: return sum(len(self.encoding.encode(b)) for b in blocks) def _maybe_compress(self): # 当原始消息占用超过预算 60% 时触发异步压缩 recent_tokens sum(m.token_count for m in self.messages[-self.max_recent_messages:]) history_tokens sum(m.token_count for m in self.messages[:-self.max_recent_messages]) if history_tokens recent_tokens int(self.token_budget * 0.6): self._compress_async() def build_context(self, question: str) - list[dict]: # 1. 系统提示词 system_prompt {role: system, content: 你是...固定内容} # 2. 检索块按需从历史中捞 retrieval_block if self.retriever: hits self.retriever.search(question, top_k3) retrieval_block self._format_retrieval_hits(hits) # 3. 摘要块 summary_block {role: system, content: f以下是对更早对话的摘要{self.summary}} if self.summary else None # 4. 最近原话 recent_block self.messages[-self.max_recent_messages:] # 组装注意顺序system - summary - retrieval - recent - 当前问题 context [system_prompt] if summary_block: context.append(summary_block) if retrieval_block: context.append({role: system, content: retrieval_block}) context.extend({role: m.role, content: m.content} for m in recent_block) context.append({role: user, content: question}) return context这段代码里最值得注意的是 build_context 里块的组装顺序system 在最前紧接着摘要然后是检索块再是最近原话。为什么这么排因为很多模型对开头的注意力最强把最核心的角色设定和摘要放前面能保证它们被充分看到而最近的对话原话放到后面又能吃到模型对结尾区域的注意力红利——开头和结尾都照顾到了中间的检索块捡漏。3.4 摘要压缩的关键结构化四个字段我补上 _compress_async 的思路。压缩不能简单地说把前面的内容总结一下那样摘要很容易漏掉关键事实。我的做法是让摘要模板强制模型输出结构化内容COMPRESS_PROMPT 请把以下对话历史压缩为结构化摘要必须包含 1. facts: 用户明确提到的事实姓名、订单号、偏好、重要决定 2. decisions: 双方达成的结论 3. todos: 尚未完成的事项 4. narrative: 不超过50字的简要过程回顾 对话历史 {history} def _compress_async(self): history self.messages[:-self.max_recent_messages] # 异步交给模型生成摘要略 # 成功后self.summary generated_summary # 然后self.messages self.messages[-self.max_recent_messages:]把摘要分成 facts / decisions / todos / narrative 四个字段是我在所有 context-mode 实现里最想强调的经验。普通叙述性摘要会把用户说他的猫叫咪咪这种事实淹没在过程描述里结构化摘要则保证关键信息有固定位置、可被查询。你可以把 facts 部分看作是给模型的一个事实登记表它会在后续回答里反复引用这张表。提示结构化摘要是我在多个项目里反复验证过收益最大的一个细节。哪怕你暂时不做摘要压缩也建议把这套事实/决定/待办/叙述的分类方式用在你自己的上下文组织里。4. 我踩过的坑context-mode 翻车现场与修复方案再好的架构落地时都是在坑里爬过来的。下面这五个问题我都在真实项目里遇到过每一个都造成了实打实的线上影响。4.1 上下文污染检索把错误信息带进来了这是我遇到过最糟糕的问题。做一个企业知识库问答时我们用了混合检索把向量相似度和关键词匹配的分数做了加权。结果用户问请假流程检索系统召回了另一条关于离职流程的文档两段内容相似度很高模型把离职流程当成请假流程答了出来。用户当场发现不对直接投诉。这个坑的本质是检索召回的是相关信息不一定是正确信息。context-mode 把检索结果放进了上下文等于给了模型一个很高权威性的参考材料模型倾向于无条件相信它。我现在处理这个问题有两道防线第一硬性规则过滤——按文档标题、来源、时间做前置筛选第二在检索块前面加一句以下是可能相关的参考资料如果与事实矛盾请忽略给模型一个可以不信的出口。这不是万能的但确实能降低事故率。4.2 Token 黑洞系统提示词吃掉大半预算做过 Agent 的人应该都体会过系统提示词里塞了七八个工具的定义每个工具的 JSON Schema 几百字加起来轻松超过 2000 token。你再给摘要分 2000给检索分 1000真正留给最近对话的只剩 3000。用户问一个简单问题模型都要在这个巨大的工具说明书 上下文摘要里挣扎回答质量和速度双双下滑。我建议把系统提示词当作代码来对待分离角色设定与工具注册工具定义只保留当前任务真正会用到的。比如一个电商客服 Agent默认不注册改地址退换货工具等模型判断用户意图需要这个工具时通过动态工具注册把它注入到下一次请求。这样系统提示词从 2000 token 压到了 900 token 左右效果立竿见影。4.3 Lost in the middle中间位置的信息被忽略这个坑在超长上下文场景尤其明显。有一次我测试一个智能报表助手给它塞进了 30 轮对话历史其中第 12 轮用户提到只看华东区的数据不含港澳台。模型在前几轮回答得很正常但到第 20 轮左右再问它华东区数据是多少它居然把港澳台也算进去了。早先那个约束条件明明还在上下文里但模型就是看不见。针对这个问题我的处理方式有两层。第一层是重复关键约束——凡是用户明确说过的过滤条件、不可违背的规则在摘要的 facts 字段里记录然后在每次 build_context 时把 facts 放到摘要块最前面确保它出现在模型注意力最强的区域。第二层是结构化标记——在检索块和历史消息里把这类关键约束用醒目的标记包裹比如[[IMPORTANT]]只看华东区不含港澳台[[/IMPORTANT]]。实测下来这两层组合拳能把约束遗忘率降低一大截。4.4 压缩过度关键数字被摘要泛化掉了摘要压缩不是无损压缩。我踩过最典型的一个例子用户维护一个项目排期在早期对话里说了3 月 15 号上线后来在第 30 轮问我们什么时候上线。结果模型回答3 月 20 号——因为在压缩时摘要模型把不少具体日期做了泛化处理。这种问题的根源是摘要模板没有把精确事实和过程叙述分开对待。如果你用前面的四字段结构把 facts 单独拎出来就会好很多。更进一步我建议对数字类事实做特殊保护压缩前扫描历史消息把日期、金额、ID、URL 这类模式匹配出来直接追加到 facts 字段里不经过摘要模型的理解环节。机器直接搬运原始事实比让模型转述可靠得多。4.5 多轮压缩后的记忆叠层矛盾最后一个坑比较隐蔽但也最容易被真实长会话触发当压缩发生多次之后摘要里保留了旧摘要的内容旧摘要里又嵌套了更早的摘要形成记忆叠层。叠层本身没问题问题是各层摘要之间可能出现矛盾。比如第一轮压缩记录了用户偏好蓝色主题第二轮摘要模型可能基于后续对话推断用户现在喜欢深色模式了于是事实表里出现两条互相矛盾记录。模型不知道该信哪条回答就变得摇摆不定。我现在会在 facts 表里给每个事实加上时间戳和来源轮次并规定同一主题下以最新事实为准。当新的 facts 与旧的冲突时旧的被标记为 expired不再进入后续上下文。这相当于给记忆加了一个版本管理的机制。5. 实测调优经验监控指标、回归测试与细节技巧结构设计得再好不上线压测都是纸老虎。最后分享几个我实测下来真正有用的调优手段。5.1 盯紧三个监控指标做 context-mode 一定要上监控更重要的是知道该看哪些指标。我最常看的有三个上下文利用率单次请求输入 token 与窗口上限的比值。如果长期高于 80%说明预算分配太紧张回答容易截断而且用户请求的延迟会明显变高。压缩触发频率每小时触发压缩的次数。如果压缩太频繁说明会话长度远超预期要么上调窗口要么应该考虑接入检索增强模式。检索命中率检索模块返回的结果有多少被模型实际引用。这个比较难直接观测但可以用一个变通方案在检索块里要求模型回答时标注引用 ID日志里统计引用占比。低于 50% 说明召回质量不高得回去调整检索。5.2 建一套记忆回归测试集context-mode 这种逻辑靠人工点几轮根本测不出问题。我的做法是建一个专门的回归测试集每个用例模拟真实对话包含几个固定的考核点早期提到的关键事实、跨 20 轮后的约束条件、压缩后的数字准确性。每次改动 context-mode 逻辑就拿这套测试集跑一遍把记忆保持率当成核心指标。这个习惯帮我拦住过好几次差点上线的回归问题。5.3 几条实操层面很管用的优化最后补充几条实操层面很有效的技巧动态工具注册。把不常用的工具定义移出系统提示词等模型需要时再注入能省大量 token。会话内 Topic 分段。如果用户在一段长会话里聊了好几个话题别把消息按时间一字排开。按话题分组每个话题一个独立摘要回答当前话题时只带当前话题的摘要和检索块。这比单一流水账式的上下文组织方式效果好很多。定期上下文刷新。当检测到用户问题与最近几轮话题明显不同比如从聊订单切换到聊退款主动把当前摘要块替换为面向全局事实的版本而不是贴着旧话题继续累积。给模型装糊涂的权利。在系统提示词里明确写一句如果上下文信息不足或冲突直接说明并追问不要编造。这虽然在很多团队看来是推卸责任但实测能大幅减少一本正经的错误回答。我现在手头这个项目在接入了比较完整的 context-mode 之后印象最深的变化不是记住了多少事而是同样的功能平均每轮请求的输入 token 从 9000 降到了 4200 左右回答延迟低了 30%用户对模型失忆的投诉基本消失。这几个数字比任何架构理论都更能说服人。如果你也在做类似的东西建议不要一上来就追求最复杂的检索增强形态——先把摘要压缩做好把事实表管好很多问题就已经解决了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →