
1. 从上下文模式说起一个被低估的工程概念第一次看到context-mode这个词很多人会下意识地把它归类成某个框架里的配置项或者某个API里的枚举值。但如果你在一线写过几年代码、调过几年线上问题就会慢慢意识到上下文模式本质上是一种信息在系统里怎么流动、怎么被消费的约定。它决定了同一份数据在不同场景下以什么形态出现、被谁读取、被谁忽略。我最早接触这个概念是在处理一个多轮对话服务的时候。当时遇到一个很典型的问题同一个用户请求在单轮问答场景下表现正常一旦进入多轮连续对话模型就开始答非所问。排查了半天最后发现问题根本不在模型本身而在于上下文的组织模式——我们把历史消息一股脑全塞进去既没有做角色区分也没有做长度裁剪导致关键信息被淹没在一堆无关内容里。这件事让我彻底改变了对上下文的看法。上下文不是简单的把数据拼在一起它是一套有结构、有优先级、有生命周期的信息组织方式。而context-mode这个词恰好精准地概括了这套组织方式的核心——模式mode意味着它不是唯一的而是可以根据场景切换的。所以这篇博文我想围绕context-mode这个核心概念把我在实际项目里踩过的坑、总结的方法、以及可以直接抄作业的实现思路完整地分享出来。不管你是做对话系统、做Agent、做RAG检索增强还是做普通的业务状态管理只要涉及信息在多个环节之间传递这套思路都能用得上。文章会从设计思路讲到具体实现再到问题排查尽量做到看完就能动手。2. 上下文模式到底解决什么问题2.1 信息过载与信息缺失的两难做系统设计的人几乎都会遇到一个矛盾给下游传递的信息太少它做不出正确判断传递的信息太多它又会被噪声干扰。这个矛盾在上下文管理里体现得尤其明显。举个具体的例子。假设你在做一个客服机器人用户先问我的订单什么时候到然后追问那能改地址吗。第二句话里的那和省略的主语都依赖第一句话的上下文。如果你只把第二句话单独传给模型它根本不知道用户在说什么。但如果你把整个对话历史、用户画像、订单详情、商品信息全部塞进去模型又可能被大量无关信息带偏甚至超出上下文窗口限制。context-mode要解决的核心问题就是在这两者之间找到一个可控的平衡点。它不是简单地多给或少给而是根据当前所处的模式决定给什么、给多少、以什么优先级给。2.2 不同场景需要不同的上下文形态我总结下来实际项目里常见的上下文模式大致有这么几类每一类对应不同的信息组织策略模式类型典型场景上下文组织特点主要风险单轮无状态模式独立问答、分类任务只传当前输入不带历史无法处理指代和省略滑动窗口模式多轮对话、连续交互保留最近N轮超出则丢弃早期关键信息丢失摘要压缩模式长对话、长文档处理对历史做摘要保留要点摘要失真、细节丢失分层检索模式RAG、知识问答按相关性动态召回召回不准、排序偏差结构化槽位模式表单填写、任务型对话用固定字段承载状态字段设计僵化这张表不是理论分类而是我在不同项目里真实用过的几种模式。关键认知是没有一种模式是万能的context-mode的价值就在于让你能根据场景灵活切换而不是一套逻辑打天下。2.3 为什么模式这个词很关键很多人做上下文管理习惯写一个万能函数把所有可能用到的信息都拼进去然后靠下游自己去筛。这种做法在小项目里能跑通但一旦系统复杂起来就会变成维护噩梦——你永远不知道某个字段为什么被加进去也不敢随便删掉任何一个因为万一有用呢。用模式的思路就不一样了。模式意味着显式的契约当前处于哪种模式就明确知道该传哪些字段、该做哪些裁剪、该走哪条组装逻辑。这样带来的好处是可测试每种模式可以单独写单元测试验证输入输出是否符合预期。可演进新增场景时加一种新模式即可不用改动已有逻辑。可观测线上出问题时能快速定位是哪种模式、哪个环节出的错。可优化不同模式可以独立调参比如窗口大小、摘要粒度、召回数量。这四点里我觉得可观测是最容易被忽视但最重要的。上下文相关的问题往往很隐蔽模型输出不对你很难一眼看出是上下文的问题还是模型的问题。有了明确的模式划分排查效率会高很多。3. 核心设计如何构建一套可切换的上下文模式3.1 抽象出统一的上下文载体不管哪种模式最终都要产出一个上下文对象传给下游。我的做法是先定义一个统一的载体结构把上下文的各个维度都抽象出来。这个结构不需要很复杂但要覆盖常见的信息类型class ContextPayload: def __init__(self): self.system_prompt # 系统级指令 self.history [] # 历史消息列表 self.retrieved_docs [] # 检索召回内容 self.slots {} # 结构化槽位 self.metadata {} # 元信息时间、来源等 self.token_budget 4096 # 当前模式允许的token上限这个载体看起来平平无奇但它的价值在于统一了不同模式的输出接口。无论你用滑动窗口还是分层检索最终都往这个结构里填下游消费方只需要认这一个结构不用关心上游用了什么模式。提示token_budget这个字段非常关键。它让每种模式在组装上下文时就有了明确的预算意识而不是组装完再去做截断。预算前置能避免很多拼好了又删的浪费。3.2 模式注册与路由机制有了统一载体接下来要解决的是怎么根据场景选模式。我采用的是注册加路由的方式核心思路是把每种模式封装成一个独立的处理器然后根据请求特征路由到对应的处理器。class ContextModeRegistry: def __init__(self): self._modes {} def register(self, name, handler): self._modes[name] handler def resolve(self, request): # 根据请求特征决定用哪种模式 if request.get(session_id) and request.get(turn_count, 0) 1: return self._modes[sliding_window] if request.get(need_knowledge): return self._modes[retrieval] return self._modes[stateless]路由逻辑可以很简单也可以很复杂。简单版本就是根据几个标志位判断复杂版本可以引入规则引擎甚至小模型来做分类。我的建议是从简单规则开始等真正遇到规则覆盖不了的场景再升级不要一上来就搞得很重。3.3 模式之间的优先级与降级实际系统里模式不是孤立存在的经常需要组合。比如一个多轮对话场景既需要滑动窗口保留历史又需要检索补充知识。这时候就需要定义模式之间的优先级和组合规则。我的处理方式是引入基础模式加增强模式的概念基础模式负责核心的上下文组织比如滑动窗口增强模式负责补充额外信息比如检索召回。组装时先跑基础模式再让增强模式往载体里追加内容最后统一做预算裁剪。降级策略也很重要。当检索服务超时或者返回为空时不能让整个请求失败而应该降级到不带检索的基础模式保证核心功能可用。这种优雅降级的思路是线上系统稳定性的关键我在好几个项目里都靠它扛过了依赖服务的抖动。4. 实操实现从零搭一套上下文模式管理4.1 滑动窗口模式的完整实现滑动窗口是最常用的模式但要做好并不简单。核心难点在于怎么决定保留几轮、怎么处理超长单条消息、怎么保证不切断语义单元。先看基础实现def sliding_window_mode(history, max_turns10, max_tokens3000): # 从最近的消息往前取直到达到轮数或token上限 selected [] token_count 0 for msg in reversed(history): msg_tokens estimate_tokens(msg[content]) if len(selected) max_turns * 2: # 一问一答算两轮 break if token_count msg_tokens max_tokens: break selected.append(msg) token_count msg_tokens return list(reversed(selected))这段代码能跑但有几个坑要注意。第一个坑是token估算。不同模型的分词方式不一样用字符数除以2这种粗略估算在中文场景下误差可能达到30%以上。我的做法是维护一个针对目标模型的估算函数宁可估多不可估少。第二个坑是消息对的完整性。如果窗口刚好切在用户提问和助手回答之间只保留了提问没保留回答模型会以为上一轮没回答过可能重复回答。所以裁剪时要以对话轮次为单位而不是以单条消息为单位。第三个坑是系统消息的处理。系统提示词通常要始终保留不能参与窗口裁剪。我一般会把系统消息单独拎出来先占掉一部分预算剩下的预算再给历史消息用。4.2 摘要压缩模式的落地细节当对话轮次很多、窗口装不下时就需要摘要压缩。这个模式的核心是把久远的历史压缩成一段摘要近期的历史保持原文。实现上分两步。第一步是触发摘要的时机判断不能每轮都摘要那样开销太大。我的做法是当历史token超过阈值比如预算的70%时触发一次摘要把最老的一半消息压缩掉。def compress_history(history, keep_recent6): if len(history) keep_recent: return history, old_part history[:-keep_recent] recent_part history[-keep_recent:] summary summarize(old_part) # 调用摘要逻辑 return recent_part, summary第二步是摘要的生成。这里有个经验摘要不要追求完整复述而要追求保留决策相关信息。比如用户说过的偏好、确认过的关键事实、未解决的问题这些必须保留寒暄、重复确认这些可以丢掉。我在prompt里会明确告诉摘要模型只保留影响后续对话的关键信息效果比让它自由发挥好很多。注意摘要是有损的一旦压缩就回不去了。所以触发摘要前最好把原始历史落盘存档方便后续排查问题或者做离线分析。4.3 检索增强模式的组装逻辑检索增强模式RAG场景常用的上下文组装核心是召回加排序加裁剪。召回负责找候选排序负责定优先级裁剪负责塞进预算。def retrieval_mode(query, retriever, max_docs5, max_tokens2000): candidates retriever.search(query, top_k20) ranked rerank(query, candidates) # 精排 selected [] token_count 0 for doc in ranked: doc_tokens estimate_tokens(doc[content]) if token_count doc_tokens max_tokens: break selected.append(doc) token_count doc_tokens return selected这里的关键参数是召回数量top_k和最终保留数量max_docs的比例。我的经验是召回数量至少是保留数量的3到4倍给精排留出足够的筛选空间。如果召回只有5条、保留也是5条那精排就形同虚设了。另一个细节是文档之间的去重和互补。有时候召回的几篇文档内容高度重叠全塞进去既浪费预算又没增加信息量。我会在精排后加一步相似度去重把内容重复度超过阈值的文档合并或丢弃。4.4 结构化槽位模式的字段设计任务型对话里槽位模式特别有用。它的思路是把对话状态用固定字段维护而不是靠模型从历史里自己推断。SLOT_SCHEMA { order_id: {type: string, required: True}, address: {type: string, required: False}, time_preference: {type: string, required: False}, confirmed: {type: boolean, default: False} }字段设计的原则是只放真正影响后续决策的字段不要什么都往里塞。我见过有人把用户说的每句话都拆成槽位结果槽位表比对话历史还长完全失去了结构化的意义。槽位的更新逻辑也要小心。用户可能中途改主意比如先说送到公司后说还是送家里吧这时候要能正确覆盖旧值。我的做法是给每个槽位记录更新时间和来源轮次冲突时以最新的为准同时保留变更历史方便追溯。5. 参数调优那些文档里不会写的经验5.1 token预算怎么分配才合理预算分配是上下文模式里最需要经验的部分。我的分配原则是系统提示词优先、当前输入次之、历史再次、检索最后。具体比例上我一般这样分系统提示词占15%到20%当前用户输入占10%历史消息占40%到50%检索内容占20%到30%。这个比例不是固定的要根据场景调整。比如知识问答场景检索内容的比例要往上提闲聊场景历史的比例可以高一些。为什么要给系统提示词留固定比例因为系统提示词通常包含角色设定、输出格式要求、安全约束这些一旦被裁掉模型的行为可能完全跑偏。宁可少放几条历史也不能动系统提示词。5.2 窗口大小与响应质量的关系滑动窗口的轮数不是越多越好。我做过一组对比测试在同一个多轮对话任务上窗口从5轮加到20轮响应质量先升后降拐点大概在8到10轮。原因不难理解近几轮的信息和当前问题最相关再往前的信息相关性快速衰减但噪声却在累积。所以窗口开太大反而会稀释关键信息。我的建议是先用8轮作为默认值然后根据实际badcase调整。如果发现模型经常忘记较早的信息再往上加如果发现模型被无关历史带偏就往下减。不要凭感觉拍脑袋定一个很大的值。5.3 摘要粒度的取舍摘要粒度指的是多长的历史压缩成多长的摘要。压缩比太高细节丢失严重压缩比太低起不到节省预算的作用。我的经验压缩比在5比1到10比1之间比较合适。也就是说5000token的历史压缩成500到1000token的摘要。低于5比1省不了多少空间高于10比1关键信息容易丢。还有一个技巧是分层摘要。对于特别长的历史不要一次性压缩成一段而是先分段摘要再把分段摘要合并成总摘要。这样能减少单次摘要的信息损失。6. 常见问题与排查实录6.1 模型答非所问的排查思路这是最常见的问题排查时我一般按这个顺序走排查步骤检查内容常见原因第一步打印实际传给模型的完整上下文上下文组装出错第二步检查关键信息是否在上下文中裁剪过度或召回失败第三步检查信息顺序是否合理重要信息被放在末尾第四步检查是否有冲突信息新旧状态未正确覆盖第五步检查系统提示词是否完整被裁剪逻辑误伤第一步永远是打印实际上下文不要凭想象。我踩过太多次以为传进去了其实没传的坑。把真实上下文打出来很多问题一眼就能看出来。6.2 上下文超长的处理策略超长问题分两种一种是单次输入就超长另一种是累积历史超长。单次输入超长的处理我一般用分段加合并把长输入切成若干段分别处理后再合并结果。比如长文档问答先对每个段落做检索再把命中的段落拼起来。累积历史超长的处理就是前面说的摘要压缩。但要注意摘要不是万能的有些信息压缩后就找不回来了。所以对于关键状态比如订单号、用户明确确认的事实我会单独存到结构化槽位里不依赖摘要保留。6.3 多模式切换时的状态一致性当系统在多种模式之间切换时容易出现状态不一致。比如从滑动窗口切到检索模式历史里的槽位信息可能就丢了。解决思路是把状态和模式解耦。状态槽位、关键事实单独维护不随模式切换而丢失模式只决定这次请求怎么组装上下文不负责状态的持久化。这样切换模式时状态始终是连续的。提示状态持久化建议用独立的存储不要塞在上下文对象里。上下文对象是一次请求的临时产物状态是跨请求的持久数据两者生命周期不同混在一起容易出问题。6.4 性能与延迟的平衡上下文组装本身也是有开销的尤其是涉及检索和摘要的时候。我遇到过因为摘要调用太频繁导致整体延迟翻倍的情况。优化思路有几个一是异步化摘要和检索可以并行做不用串行等待二是缓存相同的历史前缀可以复用之前的摘要结果三是懒加载不是每次请求都需要检索只在真正需要时才触发。延迟和质量的平衡点需要根据业务容忍度来定。实时对话场景延迟优先摘要可以做得粗一些离线分析场景质量优先可以做得细一些。7. 一些实战中的个人体会做上下文管理这几年我最大的体会是这个领域没有银弹只有权衡。每一种模式都有它的适用场景和代价关键是要清楚自己在权衡什么。另一个体会是可观测性怎么强调都不过分。上下文相关的问题往往很隐蔽如果不能在出问题时快速看到实际传了什么排查就会变成玄学。我现在做任何上下文相关的功能第一件事就是把上下文快照打点做好这个投入回报率极高。还有一点是不要过早优化。我见过一些项目一上来就搞很复杂的多模式路由和动态调参结果大部分场景根本用不上反而增加了维护成本。正确的做法是先用最简单的模式跑通等真正遇到瓶颈了再针对性优化。最后分享一个我常用的小技巧给每种模式维护一组黄金测试用例。就是一组固定的输入和期望的上下文输出每次改动组装逻辑后跑一遍确保没有回归。这个习惯帮我避免了好几次线上事故强烈推荐。这套上下文模式的思路后续还可以往几个方向扩展。比如引入基于反馈的自适应调参让系统根据用户满意度自动调整窗口大小和召回数量或者把模式选择本身也交给一个小模型来做处理更复杂的路由场景。不过这些都是后话把基础的模式管理和可观测性做扎实才是最重要的。