
1. 从token燃烧聊起我为什么盯上了context-mode我第一回调大模型API做客服机器人的时候连 context-mode 这四个字都没听过。当时的思路特别朴素用户问什么就把什么拼到提示词里顺便把历史聊天记录全部带上生怕模型“失忆”。结果项目上线第二天后台日志里刷出一排400错误提示上下文超长。我盯着报错看了半天才反应过来一件事——我给模型塞的料已经超过它的上下文窗口了。那次事故让我花了整整一个下午去查文档、翻案例最后得到一个相当扎心的结论在生产和原型阶段模型能力只是决定效果的上半场下半场全看你怎么管理上下文。所谓context-mode简单说就是一套管理“模型每次回答前能看到哪些信息”的规则和策略。它决定哪些历史对话该保留、哪些该压缩、哪些该彻底丢掉、哪些该去外部存储里临时翻出来。这个主题适合谁看如果你正在做对话机器人、AI助手、文档问答这类应用或者你只是觉得“我的模型怎么聊着聊着就变笨了”那这篇文章应该能给你一些能直接落地的思路。我会把几种主流的context-mode策略拆开讲一遍再附上一套手写的混合模式实现和踩坑记录都是我自己在项目里试过的路子。先打个底大模型的上下文窗口可以理解成模型每次回答前摆在一张工作台上的材料。窗口越大能摆的材料越多但模型处理这么多材料的速度和成本也会上升。一个更关键的问题是——不是所有摆在台面上的材料都对回答有正贡献有些甚至会干扰模型。context-mode要解决的就是“台面上该放什么不该放什么”。2. 设计context-mode前先看清楚三种“信息失调”在写任何代码之前我建议你先搞清楚自己项目里的对话到底出了哪种问题。因为不同病因下context-mode的适配方向完全不同。我归纳下来最常见的情况有三类。2.1 长对话漂移模型越来越像“金鱼”这是我最常遇到的情况。用户和机器人连续聊上几十轮之后模型的回答质量会肉眼可见地下滑。它倒不算完全失忆只是把注意力全放在了最近几句话上早先确认过的用户偏好、关键约束、已经解答过的问题全被挤出了有效注意力范围。举个例子用户第一轮说“我现在人在上海想找一家能帮忙做公司注册的代理”机器人回答得很准确。聊到第十轮用户问“那他们支持异地办理吗”如果上下文里全是中间聊的无关琐碎内容模型可能还真就把“人在上海”这个前提给忘了给出一个泛泛而谈的答案。这种失谐的本质是全量上下文里高价值信息被低价值信息稀释了。context-mode在这个场景下要做的不是加窗口而是做“信息提纯”。2.2 超窗截断直接把模型的“长期记忆”删掉窗口毕竟有上限。拿常见的32K、128K窗口来说如果对话密集很快就能被塞满。早期有些项目图省事写一行代码把超出的部分直接截掉。这种做法在短期demo里看不出问题但在真实场景里往往会把最关键的内容丢掉。我见过一个做合规咨询的项目用户提交了一份长长的合同接近4万token又问“你帮我看看第27条第3款有没有问题”。结果上下文里只剩半个合同文本模型只能实话实说“没有找到第27条”。用户当场炸毛但这真不赖模型是你把材料事先给扔了。这种场景下context-mode的核心诉求是确保关键材料不会因为窗口溢出而消失。2.3 噪音干扰塞进去的越多模型越容易被带偏还有一种情况是上下文里塞进了大量和目标问题无关的内容比如客服场景中的寒暄、转接记录、机器人自己的错误日志。模型在这些噪音的包围下很容易过度关注无关细节甚至被某些措辞误导。我实测过一个简单对比在回答“退货政策是什么”这个问题时如果上下文里充满了用户和客服的对骂记录模型产出的回答语气都会变得带刺。它是在模仿上下文里的“情绪氛围”而不只是提取“事实信息”。这种干扰是隐性的api调用不报错回答也通顺但效果就是不对劲。现在你再回头看context-mode就会发现它本质上是一个信息治理机制判断不同信息在某个时机的重要性动态决定放行哪些、压缩哪些、拦截哪些。机制设计得好模型是在“开卷考试”中审题设计得不好模型就像在垃圾堆里翻线索。3. 五种常见context-mode的实现逻辑和适用边界搞清楚了问题下面进入正题。我结合自己做过的一些项目把业界常见的context-mode总结成五种。每种我都会给出核心思路、一个简易实现示例以及它适合和不适合的场景。3.1 固定窗口模式Fixed Window最简单也最容易出问题实现逻辑非常直白只保留最近的N轮对话更早的全都不要。N一般根据经验来定比如10轮或20轮。from collections import deque class FixedWindow: def __init__(self, max_rounds10): self.max_rounds max_rounds self.history deque(maxlenmax_rounds * 2) # 每条消息算一项一问一答算两项 def add(self, user_msg, assistant_msg): self.history.append((user, user_msg)) self.history.append((assistant, assistant_msg)) def build_context(self): return \n.join(f{role}: {content} for role, content in self.history)优点显而易见实现简单、token开销稳定、响应速度快。但它有个致命缺陷——它不理解“重要性”只理解“新旧”。如果用户在第3轮提过一个关键约束在第20轮才用到它固定窗口模式早就把这个约束丢掉了。所以它更适合低语境依赖的简易机器人比如查天气、查百科、做翻译助手这类每次提问相对独立的场景。3.2 摘要滚动模式Summarizing Window保留骨架丢掉血肉这种模式是对固定窗口的升级。窗口还是只能容纳最近N轮对话但每次窗口快要满的时候先把窗口里的内容压缩成一段摘要然后把摘要当作“记忆”继续携带在上下文里。举个例子窗口上限是3000个token每次满员后把最早的对话压缩成一段200token的摘要。这样新对话继续接近满员时整体上下文结构就是“摘要 最近对话”。class SummarizingWindow: def __init__(self, max_tokens3000, summarize_promptNone): self.max_tokens max_tokens self.summary self.recent_messages [] self.summarize_prompt summarize_prompt or ( 请将以下对话压缩为一段简洁摘要保留用户的关键需求、偏好、决定和待办事项 不要超过200字\n{content} ) def add_message(self, role, content): self.recent_messages.append((role, content)) # 如果超限触发摘要 if self.estimate_tokens(self.recent_messages) self.max_tokens * 0.7: self._rollup() def _rollup(self): old_messages self.recent_messages[:-6] # 留最近几轮 content \n.join(f{r}: {c} for r, c in old_messages) # 调用模型生成摘要 self.summary call_llm(self.summarize_prompt.format(contentcontent)) self.recent_messages self.recent_messages[-6:] def build_context(self): parts [] if self.summary: parts.append(f[此前对话摘要]\n{self.summary}) parts.extend(f{r}: {c} for r, c in self.recent_messages) return \n.join(parts)摘要滚动模式的好处是能在窗口有限的前提下尽量保留“长线记忆”。但坑也在这里摘要过程本身会丢失细节而丢掉的细节有可能就是后面某个问题的关键答案。比如用户中途提过一个具体的订单号摘要把它略过了后面再问这个订单的进度时模型完全不知道。所以这种模式我只推荐用在“对话主线明确、细节容忍度较高”的场景比如产品导购、流程指引、一般性咨询。如果是法律、医疗、金融这类对细节极其敏感的场景不能只依赖摘要滚动。3.3 向量检索模式Vector Retrieval Mode让模型学会“翻旧账”前面两种模式都是在有限窗口里做“减法”向量检索模式思路更激进对话全量写入外部向量数据库每次回答前只检索出最相关的若干条记录动态塞进上下文。相当于给模型配了个外部资料库它每次回答前会自己去查资料。这种模式对技术栈有要求需要部署一个embedding模型用来把文本转成向量一个向量数据库比如chroma、qdrant或云上的向量服务以及一套检索逻辑。核心流程是每轮对话按“用户消息回复”归档切块后做embedding写入向量库新一轮用户提问时把问题也做成embedding去向量库里做相似度检索取Top-K条最相关的历史记录拼接到系统提示词之后送给模型。我可以给你一个接近能跑的伪代码框架import chromadb from sentence_transformers import SentenceTransformer class VectorRetrieval: def __init__(self, embed_model_nameBAAI/bge-small-zh-v1.5): self.embedder SentenceTransformer(embed_model_name) self.client chromadb.Client() self.collection self.client.create_collection(chat_memories) def remember(self, record_id, text): vec self.embedder.encode(text).tolist() self.collection.add(ids[record_id], embeddings[vec], documents[text]) def recall(self, query, top_k5): qvec self.embedder.encode(query).tolist() results self.collection.query(query_embeddings[qvec], n_resultstop_k) return results[documents][0]这种模式的优点是能容纳极长的天量历史也不需要手动维护摘要。但它的问题同样不小检索到的Top-K只是“语义上相似”不代表“对这个问题的答案有用”。有时候用户问“我上次的反馈处理了吗”检索回来的可能是好几个相似句式但不同主题的反馈记录真正有用的那条反而因为关键词重合度不高被挤掉。另外embedding模型的质量对效果影响极大。在中文场景里我建议先拿业务数据跑一遍召回率测试再决定用哪个 embedding 模型不要盲从榜单。3.4 核心记忆模式Core Memory Mode把关键信息钉死在工作台上这种模式我和很多人聊下来发现大家都觉得“应该有”但很少被单独拎出来当一种模式。它的核心思路是维护一个结构化的“核心记忆区”专门存放那个用户/这个任务最要紧的、跨轮次必须保留的信息。比如客服机器人核心记忆区里可以放用户的姓名、会员等级、地域当前问题的工单状态用户已经明确表达的诉求和情绪业务规则里必须遵守的红线。这些信息单独维护不随滑动窗口滚动也不参与摘要压缩。每次调用模型时“核心记忆区”固定插入在系统提示词和对话历史之间占一块最显眼的注意力位置。我用JSON来存核心记忆轮次更新效果很直接。{ user_profile: { user_id: U12345, vip_level: gold, region: shanghai }, current_ticket: { issue_type: refund, ticket_id: T20240612-009, status: waiting_for_user_response, deadline: 2024-06-18 }, user_stated_preferences: [ 退款希望原路返回, 不接受换货方案 ], critical_constraints: [ 客服无权承诺超过500元的赔偿, 所有退款需附审核凭证 ] }然后构建context时把这段JSON直接贴进去def build_prompt(question, recent_history, core_memory): prompt 【用户核心档案始终保留】\n json.dumps(core_memory, ensure_asciiFalse) if recent_history: prompt \n【最近对话】\n recent_history prompt f\n【当前问题】\n{question} return prompt核心记忆模式最适合高频、强约束的业务场景比如银行客服、电商售后、医疗服务预约。它解决的是“模型老是忘掉关键事实”的问题。但要注意核心记忆区的结构设计本身就很重要字段设少了漏信息设多了又挤压对话空间需要业务和算法一起梳理。3.5 混合模式Hybrid Mode生产环境里的终极答案真正上线一跑就会发现单一模式根本扛不住真实对话的复杂度。用户聊着聊着就从售后跳到售前从问产品参数跳到问发票事宜。如果你只有摘要滚动细节会丢只有向量检索召回不一定准只有核心记忆又存不下复杂上下文。所以我在稳定项目里的做法是混合。大致结构是第一层核心记忆区存用户档案、关键约束、当前任务状态永不压缩第二层外部向量库存全量历史按检索召回第三层固定窗口放最近N轮未摘要的完整对话第四层摘要滚动把窗口早期内容定期压缩成摘要只在检索不到相关内容时作为兜底。调用模型之前先按优先级组装上下文。如果检索回来的信息与核心记忆冲突以核心记忆为准。这个分层逻辑虽然实现起来多了不少代码但效果和稳定性都强了一大截。4. 一个实战case我手写的混合context-mode完整记录有了上面的理论框架我把自己的一个真实项目摊开来讲。那是一个法律咨询机器人项目因为涉及合同审查和条文引用属于对信息完整性要求极高的场景。下面是我从零落地混合context-mode的完整记录包括系统设计、代码结构、实测数据和踩坑点。4.1 整体的上下文路由设计先明确需求用户会在对话中上传合同、提出法条问题、询问流程而且经常聊到一半回头追问之前提到的某个数字。对“数字”和“原文”的保真要求极高容不得摘要里的细节损耗。所以我的设计是窗口近期完整性保留最近8轮完整原文核心记忆区存咨询目标、已核实的关键事实如甲方名称、合同金额、管辖法院向量存储每轮对话、用户上传的合同片段都入库合同按段落切块存储摘要兜底只有当8轮前的信息既不在核心记忆、又检索不到时才使用月度摘要。整个组装逻辑用一段伪代码串起来class HybridContextEngine: def __init__(self): self.memory CoreMemory() # 核心记忆区 self.recent RecentBuffer(8) # 最近8轮完整对话 self.vector VectorRetrieval() # 全量历史向量检索 self.rollup SummarizingWindow() # 摘要兜底层 def build(self, user_query): # 先查核心记忆 blocks [(system, self.memory.to_prompt_block())] # 再向量检索找回Top-5相关历史 related self.vector.recall(user_query, top_k5) if related: blocks.append((related_history, \n.join(related))) # 再放最近对话 recent_text self.recent.get_text() if recent_text: blocks.append((recent, recent_text)) # 最后是兜底摘要 if self.rollup.need_fallback(): blocks.append((fallback_summary, self.rollup.summary_text)) final_prompt \n\n.join(f[{tag}]\n{text} for tag, text in blocks) final_prompt f\n\n[当前用户问题]\n{user_query} return final_prompt顺序是有讲究的。核心记忆离“当前问题”越远越容易被忽略但我刻意把系统提示词放在最前面、核心记忆紧跟其后——因为模型对开头部分的注意力权重普遍较高把“必须遵守的硬信息”放在显眼位置比放在中间或结尾更靠谱。向量检索结果放在中间作为参考资料。最近对话放在后面因为新对话的上下文风格直接影响模型回答的即时语气。4.2 合同文本的入库与检索策略法律场景有个特殊性合同原文一个词都不能错但向量检索是按块切分的切不好就把条文切断了。我一开始用固定长度切块256个token一刀切结果经常把“第27条第3款”切开导致检索时只命中半截条文模型只能猜。后面改成按条款切块先识别合同里的“第X条”标题然后以条款为边界整体切块。这样每条条文天然独立检索时只要命中条款开头就能拿到完整条文。切块粒度调整之后条文完整命中率从72%涨到了94%效果立竿见影。这里有个通用经验切块策略永远优先考虑“语义边界”而不是“字符数边界”无论哪种文档场景都适用。除非你要的是纯闲聊机器人否则别用固定窗口大小去切。4.3 参数选择我用了几组反复调出来的值调上下文管理参数其实没有标准答案完全是“看数据说话”。把我项目里调参过程列出几项供参考。参数初始值调整后调整依据最近对话保留轮数58少于5轮时回头追问细节命中率下降8轮以上token消耗变大但收益趋缓向量检索Top-K353条时偶发关键信息漏召回5条时准确率上升再多噪音增大核心记忆区最大token数8001200法律场景要多存条款序号和当事人信息小什么存不下摘要触发阈值窗口80%60%等80%才摘要往往在摘要过程中就超窗报错摘要最大长度200字500字200字对复杂咨询来说丢信息太严重调参过程中我发现一个很反直觉的点摘要触发阈值设得太高反而更容易超窗。因为摘要动作本身要先把旧对话塞进一次模型调用如果等到窗口快满才触发触发瞬间上下文还在增长后续组装阶段就极容易超限。所以我最终把阈值提前到60%宁可多点几次摘要调用也不让请求在被发送之前就撑爆。4.4 实测对比三类模式在同一批问题上的表现为了验证这个混合模式的价值我拿当时项目里的200条真实用户咨询做了个回放测试每条都跑了固定窗口、纯向量检索、混合模式三种方案让三个模型分别作答再用规则脚本检查关键信息命中率比如合同金额、日期、条款序号是否答对。结果如下方案关键信息命中率平均响应延时单次调用平均token消耗固定窗口(最近10轮)61%0.9s约2800纯向量检索(不保留最近对话)74%1.4s约3400混合模式(核心记忆向量最近窗口)93%1.3s约3100混合模式的关键信息命中率比固定窗口高了整整32个百分点。代价是代码复杂度上涨了不少部署时多了一个向量库、一个embedding计算服务调用链路也比原来长了一截。但就法律咨询这个场景而言这个代价我认为非常值得。对需求方来说用户问的是“那个违约金比例是多少”模型能给对数字比什么微妙的文风优化都重要。4.5 这个case里我最担心的事别看我晒了一堆漂亮数据这套方案隐患也不少。最大的隐患在于向量召回和核心记忆都有可能在维护过程中“静默失效”。比如用户刚更新了合同但核心记忆区的金额字段还是旧的模型两个信息都看到了它不会主动判断哪个新哪个旧。所以我在架构里补了一个强制流程任何涉及核心记忆字段更新的操作都必须走代码逻辑显式覆写不允许模型自行决定。具体做法是用户上传新合同时跑一遍“合同实体抽取”用抽取结果和核心记忆区做对比有差异就弹确认让用户选“以新文件为准”还是“保留原有记录”。这一步直接把数据矛盾的坑给填上了。5. 踩坑复盘context-mode上线前后最容易翻车的五个细节理论说完了case也给了这部分专门讲坑。这些坑我基本都踩过有些甚至反复踩了两三次才彻底想明白。5.1 摘要压缩的“信息幻灭”陷阱摘要本质上是损失压缩。模型在总结时天然倾向于“概括”而不是“列表”但业务场景恰恰需要后者。比如用户在前文说过“三个条件必须满足1、退款到原账户2、收到纸质合同3、盖公章”摘要压缩极容易写成“用户提出了退款和合同相关要求”条件细节全没了。我的应对方案是对关键信息类型做结构化提取而不是自由文本总结。在后端加一个信息提取环节把需要记住的数据点金额、日期、地点、条款号、客户要求用固定的JSON结构抽出来再放进核心记忆区。文本摘要去记录“语义脉络”结构化字段去保“硬事实”两者分工明确效果立刻不一样。5.2 检索召回的高分低质问题用向量检索时用户问“我上次说的那个文件你给我找找”embedding召回回来的往往是历史上所有和“文件”沾边的对话每条相似度都不低但真正指向用户要的那份文件的那条记录可能因为表述过于特殊向量距离反而更远。解决思路是我自己摸索出来的不要把“问题”直接作为唯一检索query可以先用模型从问题中抽出关键词列表再用关键词去做布尔过滤最后才用向量做语义排序。比如“上次说的那个文件”抽不出关键词就追一句“你指的可能是合同审查报告、发票扫描件还是律师函模板”——让用户选而不是让模型猜。5.3 上下文顺序的注意力偏差同一个事实放在提示词的开头、中间和末尾模型对它的利用程度是不一样的。我有一次做回归测试发现把“本公司不支持直接转账退款”这一条从核心记忆区挪到对话历史末尾之后模型回答同一个问题的合规性下降了20%。这个现象的背后是注意力分配机制模型对开头和结尾的信息敏感度远高于中间。所以我的上下文组装原则是最前面系统指令 硬性规则 核心记忆最后面当前问题 最近几轮对话中间检索回来的参考材料。最近几轮对话放最后还有一个额外好处模型的回答风格会和当前对话保持连贯不会显得“突然变得书面化”。5.4 模式不分任务一套方案到处套很多人拿一套context-mode去跑所有应用这是我在好几个项目里见过的共性问题。客服机器人需要的核心记忆区未必适合写作文助手代码助手最需要的是“当前打开文件的内容”而不是用户过去十轮的闲聊。我把常见场景和推荐模式整理了一张表应用场景推荐模式组合不建议做客服/售后核心记忆 最近窗口 向量检索只靠摘要滚动代码助手当前文件完整内容 最近对话 少量历史检索巨量历史对话全带上文档问答切块检索为主 原文引用不做切块直接大段拼接创意写作/闲聊摘要滚动 风格记忆核心记忆强约束数据报表助手结构化schema 当前页数据 最近操作历史直接拼接完整数据库schema窗口撑不住不要对着一个模式硬套先看你这个业务里“最不能错的信息”是什么反向推导出应该用哪种模式去保它。5.5 上线后没有监控模式退化成摆设最后这个坑最隐蔽。一个context-mode上线跑了两周之后你以为它还在好好干活其实核心记忆区可能已经没人更新了向量库里的数据可能因为接口报错早就不写入了摘要滚动因为某个配置错误一直没触发。模型还在回复但上下文管理早就形同虚设。我现在的做法是给上下文管理加上可观测性每次请求之后记录核心记忆区更新了吗检索命中了哪几条记录摘要最近一次是什么时候如果连续50次请求里检索平均命中数为0或者核心记忆区超过24小时没变过就触发告警。上下文管理本身也是系统的一部分必须有监控否则它就是你那天没发现的下一场事故。6. 进阶思路让context-mode根据场景自己切换基本的混合模式已经能覆盖大多数生产场景了但如果你还有余力可以进一步做动态context-mode切换。简单说就是让系统根据当前对话的状态自动选择最合适的模式组合而不是一套策略从头用到尾。我目前比较看好的一个思路是基于几个信号动态决策对话轮次。轮次少时直接全量保有最近对话不做摘要也不检索最大限度保留信息的原汁原味用户问题中是否出现“之前/刚才/上次”这类回指词。出现时立即把向量检索的权重调高而不是让最近窗口硬扛当前任务类型。通过分类器判断用户是在“咨询规则”“上传文件”还是“投诉催办”不同任务走不同的上下文组装路径。比如开头聊得浅系统只维护最近对话一旦用户说出“我之前说过这事”系统立刻走一遍向量检索把相关记忆捞回来等对话超过15轮再开始启用摘要滚动层。这种动态切换的好处是它能把每种模式的优势发挥在适合的时机同时避免长时间对话中模式与场景错配。眼下大模型的应用越做越深模型本身的窗口能力也在逐步增大但窗口变大不等于信息管理能力的自动提升。把更多语料塞进去反而更考验你对上下文的治理能力。context-mode不是一个可选的优化项而是所有认真做LLM应用的人迟早要面对的核心工程问题。我现在接新项目的时候已经把“上下文怎么管理”提到和“选哪个模型”同等重要的位置。毕竟选模型解决的是“上限”context-mode解决的是“下限”——它保证你在绝大多数时候都不会答得离谱。这个保底价值做久了自然能感受到。