
花了大半年时间做AI应用前后迭代了十几版最后发现真正决定产品体验上限的往往不是模型选得多强、Prompt写得有多花哨而是**上下文Context**这条暗线有没有理顺。项目代号“context-mode”说白了就是一套围绕大模型上下文的管理模式。这篇文章不聊空泛的概念直接把我在项目里落地的方案、踩过的坑、调参的经验全部掰开揉碎讲清楚适合正在做 AI Agent、智能客服、知识库问答或者任何涉及多轮对话场景的开发者参考。1. context-mode是什么一次关于“记忆”的架构选型1.1 从一次线上事故说起项目上线后的第三周我们收到了一堆用户投诉对话超过二十轮后AI开始“失忆”把用户两轮前提过的信息忘得一干二净甚至开始重复问同样的问题。排查了很久问题不复杂——我们把所有的对话历史一股脑塞进 PromptToken 超限后直接截断结果最前面的关键信息被丢掉了。这个问题的本质是没有一套明确的上下文管理模式。模型每请求一次都是“无状态”的它的记忆完全靠你在请求里带了什么。只带最近几轮它会忘掉开头用户交代的背景全量带上成本飙升、响应变慢还可能超出窗口导致报错。所以所谓“context-mode”就是你要显式地决策每轮请求里到底该让模型看到哪些信息用什么样的结构组织这些信息超限时又该怎么降级。从架构上讲这套模式可以分为四个核心模块上下文采集、上下文存储、上下文组装、上下文压缩。我在项目里把它们做成了一个独立的服务不耦合具体业务逻辑这样任何需要对话能力的模块都能接入。1.2 为什么单独为“上下文”立一个模式很多团队一开始都不重视上下文管理觉得“把history数组传进去不就好了”。但一旦产品进入真实使用你会发现几个躲不掉的问题。首先是Token预算问题。GPT-4级别的模型即便使用128K窗口版本真正高质量处理的输入也就是几十K Token。而一个活跃用户一天对话几百轮历史消息早就能撑爆窗口。再叠加系统提示词、检索到的参考文档、工具返回结果输入空间极其紧张。其次是信息层次问题。用户的对话历史里不是所有内容都值得让模型看到。寒暄、重复确认、无效指令这些信息不仅浪费Token还会干扰模型的注意力——上一轮用户随口说的“算了不重要”模型可能真的把它当作需求去执行。所以我们需要的不是“所有历史”而是“值得让模型知道的信息”。最后是成本与延迟问题。每多传一千个Token请求的延迟就会显著增加成本则成比例上升。如果你的应用日请求量是几十万这里随便优化一点省下的都是真金白银。把上下文管理从“随手处理”提升为“核心模式”本质上就是在为这些维度做工程化收敛。2. 五种上下文模式的设计与取舍2.1 零上下文模式最简单但最容易误用零上下文模式就是每次请求都完全独立不带任何历史只依靠单轮Prompt。它最适合那些“翻译一句话”“生成一段文案”“单张图片识别”这类天然无状态的任务。好处是无论多少轮响应质量和成本都完全可预测绝不出现上下文污染。但很多人误用了这个模式——在一个对话产品里用户明明说“刚才说的那个方案再帮我细化一下”结果系统没有任何历史模型完全不知道“那个方案”是什么。这种体验可以说是灾难级的。所以我的经验是零上下文模式只适用于“请求本身就包含全部必要信息”的场景比如通过接口传入完整参数而不是靠模型“记住”任何东西。在实现上零上下文模式意味着服务端不需要保存任何会话状态架构最清爽也最容易做水平扩展。如果你的业务允许尽量把更多的任务设计成无状态请求这是降本增效的第一步。2.2 全量上下文模式小规模场景的“暴力解法”全量模式就是把从会话开始到当前轮的所有对话消息全量拼入 Prompt 传给模型。它的实现最简单信息也最完整模型不会因为缺上下文而出错。我最早的原型就是这个方案确实省心。但这种模式有几道硬伤几乎无法回避Token 超限窗口是固定的对话是无限增长的总有一刻会爆掉。128K窗口看着大遇到频繁工具调用、大文档检索几轮就满了。成本非线性上涨每轮请求都带着全部历史随着对话增长单轮成本越来越高最终高到离谱。信噪比下降大量无关消息淹没了关键信息模型反而更容易“迷失重点”表现为回答偏离主题、重复提问。所以全量模式只适合内部调试、短会话不超过10轮或低并发场景不建议直接用在正式的多轮对话产品里。我甚至建议即使要用也一定要设一个硬性的轮数上限超过之后强制切换到摘要或滑窗模式。2.3 滑动窗口模式兼顾效率与实现的均衡解滑动窗口模式的核心思路很简单只保留最近N轮对话更早的一律丢弃。比如配置窗口大小为10轮那么第1轮到第10轮结束后第11轮的请求就只带第2轮到第11轮的消息始终保持最近10轮。这个模式的优点非常突出实现成本极低只维护一个固定长度的队列Token消耗稳定不会因为会话变长而无限增长模型始终能看到相对新鲜的上下文对多轮任务的连续性有基本保障。但它的代价也显而易见——窗口之外的信息被永久丢弃。用户的初始意图、几天前提过的偏好、某次操作的历史记录都会从模型视野中消失。所以在我的实现里滑动窗口模式永远不会单独使用而是作为基础层向上叠加摘要模式和检索模式。如果你的应用场景是“即时但不需要长期记忆”的对话比如一次售后会话滑窗是一个足够好的起点。2.4 摘要模式用“浓缩记忆”对抗窗口限制一旦对话超过窗口上限与其粗暴截断不如做信息蒸馏把早期对话通过模型生成一段结构化的摘要作为后续请求中的固定前缀。这样既保留了关键背景信息又控制了Token量。这是项目里最核心的压缩手段也是“context-mode”能真正在长会话场景落地的原因。具体实现是双层的结构长期摘要维护一份全局摘要每次触发压缩时把当前摘要和新增对话一起让模型重新生成一份更完整的摘要。滚动摘要当单次超限时用当前摘要替换最老的一部分消息保证整体仍在窗口内。实际写代码时摘要的触发条件不要只盯着“是否超限”还要考虑信息的有效期。比如用户昨天聊过的事情今天再提起来模型如果没有摘要就无法回答。所以更强的做法是引入时间衰减对每一条历史消息设定权重时间越近权重越高超过窗口时优先丢弃低权重的消息而高权重的关键信息即使较老也会被摘录取代而不是直接删除。2.5 结构化上下文模式让关键信息“持久化”下来在做的过程中我逐渐意识到对话历史只是上下文的一部分还有很多其他信息同等重要比如用户实名信息、偏好设置、正在操作的任务状态、业务规则等。于是我把上下文拆成了“会话历史、持久化用户画像、当前任务状态”三层结构。这也就是结构化上下文模式的核心思路不要把所有信息都塞进对话文本里而是把它们组织成结构化的数据结构在请求组装时动态注入。比如用户是会员这个信息在注册时就存在用户表里下次请求自动拼入“用户等级VIP”模型自然知道怎么处理。实现上我维护了一个ContextState对象分三个字段profile存用户属性task存当前任务执行状态history存对话记录。每次组装Prompt时按固定模板把这些字段填充进去。这个模式在接入Agent工具调用时价值尤其大——模型能通过状态对象明确知道“当前已经完成了哪几步、还差哪几步”而不是靠文本反复推理。3. 实操从零实现一个可用的上下文管理器3.1 整体架构与数据模型我最终采用的不是一个单一模式而是动态混合模式基础层用滑动窗口保证响应时效叠加摘要层完成长程记忆的沉淀再根据实时字段判断是否需要临时进入结构化模式。这样既能控制成本也能保证体验。下面给出核心的数据结构和类设计。from dataclasses import dataclass, field from typing import Optional, Callable import time dataclass class Message: role: str # user / assistant / system / tool content: str timestamp: float field(default_factorytime.time) dataclass class ContextState: profile: dict field(default_factorydict) # 用户画像 task_state: dict field(default_factorydict) # 当前任务状态 history: list field(default_factorylist) # 对话历史 class ContextManager: def __init__(self, max_tokens6000, history_limit20): self.max_tokens max_tokens self.history_limit history_limit self.summary # 长期摘要缓存 self.state ContextState() def add_message(self, role: str, content: str): self.state.history.append(Message(rolerole, contentcontent)) self._trim_history() def _trim_history(self): # 滑动窗口的基本裁剪 if len(self.state.history) self.history_limit: overflow len(self.state.history) - self.history_limit # 把超出的消息先交给摘要逻辑处理 self._condense_messages(self.state.history[:overflow]) self.state.history self.state.history[overflow:]这个类看起来很简单但它已经是整个模式最小的核心了。三个关键点值得解释max_tokens是组装Prompt时的硬上限history_limit控制滑动窗口的消息条数summary保存跨轮次的关键记忆。所有消息都带时间戳为后续的“时间衰减策略”留好了扩展位。3.2 上下文组装把“状态”渲染成模型能理解的Prompt组装阶段的任务是把ContextState翻译成模型输入。我用的模板大致如下先放系统指令再放用户画像和任务状态结构化上下文接着放长期摘要最后才是最近的会话历史。这样模型在生成每一步时背景信息永远在最前面不容易被后面的长文本冲淡。def build_prompt(self, system_prompt: str, user_input: str) - list[dict]: messages [{role: system, content: system_prompt}] if self.state.profile: messages.append({ role: system, content: 用户画像 self._dict_to_text(self.state.profile) }) if self.state.task_state: messages.append({ role: system, content: 任务状态 self._dict_to_text(self.state.task_state) }) if self.summary: messages.append({ role: system, content: 历史摘要 self.summary }) for msg in self.state.history: messages.append({role: msg.role, content: msg.content}) messages.append({role: user, content: user_input}) return messages注意这里一个细节summary我放在history之前是作为“全局背景”存在的而user_input永远放在最后保证模型最先关注最新的指令。消息数组的排列顺序直接影响注意力分布如果你在项目里发现模型“看不到”某些信息先检查它们是不是被淹没在了中段位置。3.3 Token估算与压缩阈值计算组装完Prompt后不能盲目发给模型必须先做Token估算。我一直用tiktoken库来做这件事比简单按字符数估算准确得多。import tiktoken encoding tiktoken.get_encoding(cl100k_base) def count_tokens(text: str) - int: return len(encoding.encode(text)) def is_exceeding_limit(messages: list[dict], max_tokens: int) - bool: total 0 for m in messages: # 每条消息额外4 tokenOpenAI对chat格式的固有开销 total count_tokens(m[content]) 4 total 2 # 会话层级的辅助token return total max_tokens估算的目的是触发压缩。我在每次附加新消息前都会估算一次如果超过max_tokens的88%就进入压缩流程。为什么是88%而不是100%因为要给模型回复预留空间——最大输出Token数如果设置为1024那总预算里就要先扣掉这部分否则容易出现“请求成功但响应被截断”的情况。这个比例是压了很多次线上问题后才定下来的。3.4 摘要压缩算法的完整实现摘要压缩的核心是一个_condense_messages方法。它接收一批旧消息调用模型生成摘要并把摘要合并到全局summary里。这里唯一需要注意的是摘要不能无限膨胀否则这就是把滑窗问题换了个形式重演。所以每次生成摘要后还要对summary本身做一次Token检查超过阈值就再压缩一次递归式摘要。def _condense_messages(self, messages: list[Message]): if not messages: return text \n.join(f{m.role}: {m.content} for m in messages) summarize_prompt ( 请把下面这段对话压缩成简洁的中文摘要保留所有关键信息 包括用户明确的偏好、任务要求、已确认的决策。不超过300字。\n\n text ) new_summary call_model([{role: user, content: summarize_prompt}]) if self.summary: self.summary call_model([{ role: user, content: f以下是已有摘要\n{self.summary}\n\n以下是最新补充对话的摘要\n{new_summary}\n\n请合并成一份连贯的总摘要。 }]) else: self.summary new_summary if count_tokens(self.summary) 500: # 摘要自身过长二次压缩 self.summary call_model([{ role: user, content: f请把以下摘要进一步压缩到200字以内只保留最关键的事实\n{self.summary} }])这个实现里有一个非常容易被忽略的技巧摘要合并时一定要把“已有摘要”和“新摘要”都传给模型而不是只传新摘要。最开始我图省事只对新对话做摘要再追加字符串拼接结果格式混乱、逻辑断层。让模型重新生成一份连贯的总摘要虽然多花一次调用但长期的准确性收益远大于成本。3.5 自动模式选择按场景切换上下文策略最后一个值得分享的实操点是我在项目中做的“模式自动路由”。不把所有逻辑都写死而是在每次请求时根据几个关键指标来动态决定用哪种模式如果会话轮数小于3直接全量模式不引入摘要噪声如果轮数在3到15之间滑窗模式结构化上下文保证新鲜度如果轮数超过15启用摘要模式并且把滑窗长度降低到10轮如果当前任务状态字段变化频繁比如连续多次工具调用优先保证任务状态的注入而非增加历史消息。def route_mode(self) - str: rounds len(self.state.history) if rounds 3: return full elif rounds 15: return sliding else: return summary这里的阈值不是拍脑袋定的而是根据模型的“遗忘曲线”测出来的。我在测试集里对比了不同窗口长度下模型的召回率发现超过15轮后即使把历史全部塞进去模型对早期信息的利用率也在明显下降。与其无效占用Token不如把这些信息压缩成摘要反而提升准确性。4. 常见问题与排查技巧实录4.1 上下文“串味”模型把旧对话当成当前指令这是多轮对话里最让人头疼的问题。用户第一轮说“帮我写一份合同”后面聊到别的十分钟后又回来说“改一下第三条”模型可能真的去改了第一轮的合同——如果你没有显式区分哪条信息是当前任务模型自己会乱猜。我的排查方法是在组装Prompt前打印当前ContextState中的task_state字段看看它是否被旧会话覆盖了。这个问题的根源往往是用户在对话中途换了主题而task_state没有跟着更新。解决思路是增加一个“意图漂移检测”当新输入与当前任务无关时清空任务状态开启新任务上下文而不是把新话题的内容继续叠加到旧任务上。4.2 Token超限但就是不报错有一种隐蔽的故障请求没有报错但模型回答质量断崖下降。打开日志一看才发现历史消息里混入了好几轮工具返回的长JSON单条消息就有几千Token窗口早就被撑爆了系统自动截断了末尾消息——可会话中间还有很多有价值的上下文直接全丢了。这个问题告诉我们两件事第一工具返回值必须单独做摘要不能原样塞进历史第二裁剪逻辑不能只从队首丢消息要按权重判断。我给每条消息加了一个importance字段工具调用返回的消息默认降低权重用户明确强调的信息手动抬高权重。裁剪时优先丢低权重消息再丢最老消息这样能将信息损失降到最低。4.3 摘要模式下的“事实漂移”摘要模式用久了我注意到一个现象模型对用户初始意图的记忆会随着摘要的不断压缩而逐渐扭曲。比如用户最初说“预算不超过5000”经两次摘要压缩后变成了“预算在5000左右”再到后来变成了“预算可以到5000以上”。这就是所谓的事实漂移。应对的方案是给关键约束做“固化”。在写入摘要时不允许模型自由改写这些数据型的事实。我会在摘要Prompt里强制规定“所有数字、日期、金额、名称必须原样保留不得修改如有不确定必须标注”。更进一步我会把这类硬约束同步存放一份在profile或task_state中摘要只是给模型看的文本真正的一致性校验靠结构化字段来兜底。4.4 Prompt长度与推理延迟的平衡很多人只关心窗口上限忽略了上下文长度对延迟的影响。实测下来当输入Token从2K涨到10K时在某些模型上首Token时间会翻倍以上。所以优化上下文模式不只是省钱也是在救响应速度。我在项目里加了一个“Token预算总控”它按照比例分配给四个部分系统提示词占15%、结构化上下文占25%、摘要占20%、近期历史占40%。超出预算时优先压缩近期历史把20轮压成10轮再去考虑减少结构化上下文里非必要的字段。这个预算比例不是固定的你可以根据自己场景调整但有了总控意识你的上下文管理才算真正闭环。4.5 多用户隔离与会话复用最后一个坑是多会话场景下的上下文串线。我在开发初期为了图方便用一个全局列表模拟了所有用户的对话记录测到第三个用户时就彻底乱了。后来把ContextState改成以session_id为Key的字典存储并加了一层超时清理机制才算稳定下来。class SessionStore: def __init__(self, ttl_seconds3600): self.sessions: dict[str, ContextState] {} self.ttl ttl_seconds def get_or_create(self, session_id: str) - ContextState: now time.time() if session_id not in self.sessions: self.sessions[session_id] ContextState() # 过期清理 self.sessions.pop(session_id, None) # placeholder return self.sessions[session_id] def cleanup(self): expired [sid for sid, state in self.sessions.items() if time.time() - state.history[-1].timestamp self.ttl] for sid in expired: del self.sessions[sid]这个清理函数很关键。不用的会话如果不释放内存占用会持续上涨存储成本也会让整体方案变得不可规模化。按会话存活时间做TTL清理是最简单的兜底策略。5. 模式评估什么样的场景该选什么模式为了让你对照自己的业务快速选型我把五种模式整理成了一张对比表模式实现成本上下文完整度长对话支持Token成本推荐场景零上下文低无差极低翻译、抽卡、单轮生成全量上下文极低高差高调试、短会话滑动窗口低中中稳定客服、短多轮对话摘要模式高中高好中长对话、AI助手结构化上下文中高好中高Agent、任务型对话从我的实战经验看大多数生产级应用最终都会走到“滑窗摘要结构化”的混合形态。单独一种模式很难同时满足“信息不丢、成本可控、响应快”这三个目标。混合模式的复杂度确实高一些但它换来的是你在用户体量增长后不需要推翻重来。很多团队一开始贪图简单全量模式上线做到几千用户就得重写对话模块这个迁移成本远比一开始搭建上下文体系要高。另一个很深的体会是上下文模式是产品形态驱动出来的不是算法驱动出来的。你需要先想清楚产品里用户是以什么样的方式和AI长期互动的是单次问答、连续对话还是一个持续数天的项目协作这个问题的答案直接决定了你的上下文管理策略。不要在没有明确用户行为画像时盲目追求“长上下文模型能搞定一切”128K窗口也是要成本的。6. 后续演进从上下文模式走向上下文工程项目做完了但我觉得这个方向其实还有很大的延展空间。“context-mode”只是第一步解决的是“怎么管”下一步要解决的是“怎么让模型真正用好”。首推的方向是自动摘要分层不只做一版摘要而是做多级摘要对话级摘要、主题级摘要、用户级摘要在组装时按需动态调用。比如用户问“我上周问过的那件事”系统先查主题级摘要定位到相关对话再取出那一段具体内容拼进Prompt——这比把所有摘要都塞进去要精准得多。另一个值得尝试的方向是上下文检索增强为历史对话建立向量索引只检索与当前输入最相关的几条历史消息动态加入Prompt。这个方案比摘要分层更进一步能处理超长周期的记忆但对基础设施的要求也更高需要维护向量数据库和Embedding流水线。就我自己而言下一步会先把多级摘要和向量检索融合起来先用摘要层级快速定位可能相关的对话区间再用向量检索精确命中关键内容最后把命中的原文片段和摘要一起注入Prompt。这套组合拳打下来理论上可以把模型的有效记忆窗口从“几十轮”拉到“几个月”而Token开销几乎不涨。最后再分享一个小的实际心得你在实现这些复杂功能之前先在本地把“组装后的Prompt”完整打印出来看几遍你就知道上下文管理哪里容易出问题了。每一次模型表现不对先Debug Prompt再Debug模型逻辑。上下文模式的本质不是玄学就是扎扎实实的工程问题理顺了它后面的Agent、复杂任务拆解都会顺很多。