从零搭建大模型上下文管理系统:context-mode实战指南

发布时间:2026/10/8 11:50:27
从零搭建大模型上下文管理系统:context-mode实战指南 如果你最近在折腾 AI 编程助手或者大模型应用肯定没少听到context-mode这个词。我最早被这个词搞晕是在连续两个项目里分别撞上同一个问题和 AI 聊了三十分钟之后它开始忘记最初的需求把一份项目文档喂进去之后它反而被无关的细节带偏。后来我意识到这些问题全都可以归结为一句话——上下文管理没做好。context-mode不是一个官方名词更像是一类设计思路的总称。它要解决的是大模型虽然有看似巨大的上下文窗口但我们在真实使用中不能把所有历史记录一股脑丢给它。到底保留什么、压缩什么、检索什么、按什么顺序注入这些组合起来就是一套上下文管理模式。这篇文章我会从工程视角把context-mode讲透包括它在模型侧和应用侧分别指什么、为什么非做不可、怎么从零搭一个能落地的上下文管理系统以及我实际操作中踩过的坑。无论你是做 AI 应用开发的、用 AI 写代码的还是打算给团队做个智能问答机器人应该都能从这里拿到一点可以直接用的东西。1. context-mode 到底是什么一次理清这个被用滥的词1.1 从一个让我抓狂的场景说起先说个我自己的真实经历。有次我用 AI 辅助重构一个老服务一开始聊得很顺AI 准确说出了某个模块的依赖关系还给了详细的改动方案。但四十多分钟聊下来我回头问它“你最开始不是说这个接口可以拆成三个吗后来怎么改成保留两个了”它的回答让我血压直接上来“我们之前没有讨论过拆成三个接口的方案。”那一刻我确认了两件事第一它确实“忘”了第二不是它的模型能力问题而是我的上下文管理方式有问题——我把关键信息埋在了大量无效对话里最终被系统截断或覆盖了。类似的场景在很多人身上都发生过长对话到后半段AI 开始用笼统话术回答在一个问答机器人里用户问出和半小时前相关的问题它完全答不上来给 AI 喂了一堆资料之后它反而只盯着其中一段无关内容猛输出。这些问题看起来五花八门本质都是同一件事我们没有显式地告诉 AI“哪些信息是重要的、当前任务是什么、哪些内容已经过期”。context-mode这个词就是在这种背景下被反复提起的。它不是一个按钮也不是某个具体模型参数而是一整套关于“如何在每次请求中组织上下文”的策略。做得好AI 像是一个记忆力极好且知道轻重缓急的助手做不好你花再多钱换更大窗口的模型也没用。1.2 两种最常见的 context-mode 理解我在不同社区里看到的讨论基本可以分成两种理解。第一种是模型侧的context window也就是上下文窗口本身。这指的是模型一次能处理的 token 数量上限比如常见的 8K、32K、128K、200K。这个值是在模型训练和部署时就确定的硬件级约束不是我们能随意改的。超出的部分要么被截断要么在服务端做隐式摘要。第二种是应用侧的context management mode也就是我们作为开发者在调用模型时如何选择、排列、更新那些即将进入上下文窗口的内容。这包括系统提示词怎么设计、历史对话保留多少轮、关键事实存在哪里、什么时候触发检索、什么时候做摘要压缩。这部分是我们真正能设计和优化的空间。打个比方模型上下文窗口像是一间会议室的容量会议室的墙和座位数是固定的。而 context-mode 就是会议主持人手中的那些权限安排谁坐第一排、谁的发言记录要保留、上轮讨论留下的白板内容要不要擦掉、什么时候从档案柜里调出新材料。同一个模型换了不同的主持人会议效率和参会者记忆完全不同。工程上做 context-mode本质上就是当好这个主持人。2. 为什么要做 context-mode模型记忆的三大真相2.1 上下文窗口不是内存很多第一次接触大模型开发的人都会有一个误解觉得模型跟人一样聊过的东西它会“记住”。真相是大模型在绝大多数应用形式下都是无状态的你发出去的每一条请求它都是独立处理的。之所以看起来像记得是因为我们把全部或部分历史对话重新放回了输入里。这个区别非常致命。如果说模型是一台计算器那上下文窗口就是这台计算器上临时摆放的数字纸条。计算器本身没有任何记忆纸条放多少它就处理多少放不下就翻车。context-mode要做的事情是在每次计算开始之前决定把哪几张纸条摆上台面、哪些收到抽屉里、哪些直接扔进废纸篓。理解了这一点你再看市面上那些号称“长记忆”的 AI 产品思路就清楚了。它们没有一个真的长记忆都是在每次请求前检索数据库、拼接历史、重放上下文。产品体验的差异全在检索准不准、拼接好不好、更新快不快这些工程细节上。2.2 token 既是成本也是约束为什么要费劲管理上下文而不是直接把窗口拉满最现实的原因是 token 真的很贵。不管是按输入输出计费的商业 API还是自己部署模型占用的算力每一轮请求的 token 消耗都在烧钱。举个例子一个常见大模型 API 每百万输入 token 的价格按几十元量级计算看起来不算夸张但如果你做一个每轮请求都要把 10 万 token 项目文档全量塞进去的问答机器人一次对话就是十几次请求一天的调用量上来之后成本会非常吓人。而且请求里塞的东西越多响应延迟越长。一个 200K 窗口的服务你每次都顶格输入首字返回时间可能翻两三倍用户根本等不起。所以 context-mode 的核心收益有两个省成本、降延迟。它不是让模型变聪明而是让模型把有限的预算花在真正重要的信息上。我自己的经验是把无用的长历史压缩掉、只在需要时检索相关文档同样质量的回答能把单轮 token 消耗降低 60% 以上响应速度提升也非常明显。2.3 信息衰减比想象中更快还有一个容易忽略的真相就算 token 没有超出窗口上限模型对上下文不同位置的关注度也不是均匀的。这个问题在圈内被讨论得很多通俗地说就是“中间内容更容易被忽略”。我做过一个小实验把一份 30 页的说明文档放在对话中间位置然后在后面的提问里特意引用了文档第 16 页上一条不起眼但关键的规则。AI 的回答经常忽略这条规则而当我把它放到系统提示词里或对话开头同样的回答就非常精准。后来我才了解到这个问题在论文里被叫做“lost in the middle”大规模语言模型对长上下文中间部分的信息利用率明显偏低。这给 context-mode 提出了一个硬性要求不能只保证信息在窗口内还要保证信息在合理的位置上。核心事实要放到开头或结尾这些模型关注度高的区域辅助信息可以放中间过期信息干脆移除。很多 AI 应用“该记住的记不住”不是模型不行而是工程师把重要信息塞进了最容易失忆的位置。3. 手把手构建一个可用的 context-mode 系统如果你只是用现成聊天工具前两章已经够你理解什么是 context-mode 了。但如果你是做开发的我更想把这一章写详细一点。我早期在内部实现过一个小工具代号就叫context-mode功能很简单给对话加上“分层记忆”和“动态注入”。下面把设计思路和核心代码拆开讲你可以直接照着改。3.1 决定架构的四个关键选择动手之前我先定了四个原则后面所有的代码和配置都是围绕它们展开的。第一是以会话为最小单位。不是说每次提问都临时拼上下文而是每个会话维护一个状态对象里面存着会话ID、目标描述、关键事实清单、最近对话轮次、相关文档引用。这样任何一次请求都能快速知道“我是谁、我在聊什么、什么最重要”。第二是三层记忆架构。我把信息分成工作记忆、压缩记忆、外部记忆三层。工作记忆是最近几次对话原文模型可以直接用压缩记忆是把更早的对话定期做成摘要保留主线逻辑外部记忆指文档库或知识库平时放向量检索里需要时把命中片段取出来注入。这三层对应不同的访问成本和信息精度。第三是注入顺序有讲究。我最终固定下来的拼接顺序是系统提示词在最前接着是当前的会话目标然后是被检索到的相关片段最后才是最近几轮对话。前面讲过模型对开头和结尾关注度高所以最重要的目标和最新指令绝对不放中间。第四是压缩要设触发条件。不是聊几句就摘要而是按照 token 预算阈值触发。比如预设 12K token 的软上限当最近对话原文超过这个值时就把最靠前的旧对话折叠成一段摘要并把其中提到的决定、日期、关键数字提取为结构化事实。3.2 项目结构先跑通再优化整个工具我保持得很轻核心就六个文件context-mode/ ├── config.py # 模型参数、token 预算、路径配置 ├── models.py # 数据结构定义会话、消息、片段 ├── session.py # 会话管理新增、保存、更新状态 ├── summarizer.py # 对话摘要与关键事实提取 ├── retriever.py # 向量检索从文档库召回相关片段 └── cli.py # 命令行入口对话、调试、查看上下文这个结构很朴素没有上什么重型框架就是为了方便把每一步都看明白。你如果做产品化完全可以把 retriever 换成 ES、Milvus、Redis 向量库之类的东西但核心逻辑不需要变。3.3 核心代码会话状态、压缩策略、检索注入先看models.py定义三个最基本的数据结构。from dataclasses import dataclass, field from typing import List, Optional, Dict dataclass class Message: role: str # system、user、assistant content: str timestamp: float # 时间戳用来判断新旧 extra: Dict[str, str] field(default_factorydict) # 元数据比如来源文件 dataclass class SessionState: session_id: str goal: str # 当前会话目标始终放在显眼位置 facts: List[str] # 提取出的关键事实例如“用户要求保留旧接口” work_memory: List[Message] # 最近的完整对话 compressed: List[str] # 早期对话的摘要列表 doc_refs: List[str] field(default_factorylist) # 关联的文档ID dataclass class RetrievedChunk: doc_id: str text: str score: float metadata: Dict[str, str]SessionState里有一个容易被忽略的字段facts。这个字段非常重要因为摘要本质上是模型的二次创作保不齐会丢掉细节。而 facts 是明文列出的关键结论比如“确认使用方案 B放弃方案 A”。每次注入时我把 facts 放在靠近尾部的位置代替一部分摘要模型回答时会优先参考。然后是session.py里负责压缩的方法from summarizer import summarize_old_messages, extract_facts class SessionManager: def __init__(self, max_work_tokens: int 12000): self.max_work_tokens max_work_tokens def push_message(self, session: SessionState, msg: Message): session.work_memory.append(msg) if self._estimate_tokens(session.work_memory) self.max_work_tokens: self._compress(session) def _compress(self, session: SessionState): # 只拿最靠前的 60% 消息做摘要留最近 40% 做原文 split_point int(len(session.work_memory) * 0.6) old_messages session.work_memory[:split_point] session.work_memory session.work_memory[split_point:] summary summarize_old_messages(old_messages) session.compressed.append(summary) new_facts extract_facts(old_messages) session.facts.extend(new_facts)_compress的逻辑有讲究。为什么是 60% 而不是全部因为模型对太遥远的信息利用率低但对刚才几轮对话还有较强记忆。只压缩旧的部分保住最近的原文这种滑窗式做法能在摘要损失和时效性之间取得平衡。我在实际跑下来的效果比整体摘要好不少推荐你直接抄这个比例。最后是cli.py里组装最终请求上下文的方法也是整个 context-mode 最关键的一步def build_request_messages(session: SessionState, user_query: str, retrieved: List[RetrievedChunk]) - List[Message]: system_prompt ( 你是一名严谨的工程助手。请优先依据下方提供的『会话目标』、 『关键事实』和『参考资料』回答如果参考资料与关键事实冲突 以关键事实为准。 ) messages [Message(rolesystem, contentsystem_prompt)] messages.append(Message(rolesystem, contentf会话目标{session.goal})) messages.append(Message(rolesystem, content关键事实 \n.join(session.facts))) if retrieved: ref_block \n---\n.join(f[{c.metadata.get(source,未知)}] {c.text} for c in retrieved) messages.append(Message(rolesystem, contentf参考资料\n{ref_block})) messages.extend(session.work_memory) messages.append(Message(roleuser, contentuser_query)) final_messages [] for msg in messages: text msg.content # 给不同来源的信息加上标记后续调试或日志可以直接溯源 final_messages.append(Message(rolemsg.role, contenttext, extra{src: msg.extra.get(src, state)})) return final_messages注意几个细节。第一目标、事实、参考资料都放在system角色里模型对 system 部分的指令遵守度一般高于普通对话内容。第二参考资料通过“来源”元数据标注这样出问题时能找到是哪份文档带偏了模型。第三work_memory放在参考文献之后、用户新提问之前保证最新的对话状态离模型更近减少中间丢失问题。整个工具的闭环是这样的用户提问 → 检索器召回片段 → 构建 request messages → 调用模型 → 把结果 push 回会话 → 若超预算则自动压缩。整个流程跑通之后AI 等于有了一个“能整理笔记、会翻档案柜”的助手失忆现象大幅减少。4. 实操中的常见问题与排坑记录4.1 token 预算失控第一个栽跟头的地方就是 token 预算。你以为压缩到 12K token 以内就稳了但模型返回的输出也在消耗 token如果输出特别长再加上 System Prompt 本身的长度极容易直接冲过窗口上限。更隐蔽的是不同模型分词器不一样同样的字符数在不同模型下 token 数可能差很多。我的解决方法是两件事第一给上下文预留 25% 缓冲比如窗口是 32K那我就把压缩阈值设在 21K 到 22K再往上就触发更强的压缩。因为系统提示词、事实列表、工具定义这些“隐藏消耗”加起来很容易超过你的直觉。第二调模型之前显式调用分词库做估算不要靠字符串长度猜。def _estimate_tokens(messages, encoding_namecl100k_base): enc tiktoken.get_encoding(encoding_name) # 简化估算按角色前缀 内容拼接后统计 raw .join(f{m.role}:{m.content} for m in messages) return len(enc.encode(raw))4.2 检索召回了错误的信息context-mode 依赖检索但检索这个环节特别容易出问题。最常见的现象是你问“用户登录失败如何处理”检索器从知识库召回了一段“登录日志字段说明”里面出现了“失败”这个词语义上却根本不相关。模型看到了这段内容反而被带到沟里给出一个答非所问的答案。我后来给检索环节加了两个增强一是混合检索不能只用向量相似度要配合关键词匹配和权重排序二是每个片段必须带元数据至少包含来源文件、标题、更新时间并在注入时标明。这样即使召回了一段不相关的内容模型也能在该部分打上“来自XX文档某章节”的标签回答时会更谨慎不会把不相关的资料当绝对事实。另外建议给检索结果设置分数下限——分数过低的片段宁可不注入也不硬塞。4.3 上下文污染AI 被旧信息带偏比“检索错误”更烦的是“上下文污染”。当你的工作记忆或摘要里保留了一段已经被推翻的结论模型会在之后几轮里反复参考这段过时信息。最典型的是改需求上午说用方案 A下午改成方案 B但摘要里还留着 A 的详细理由。模型重新读到摘要时会觉得 A 也是候选方案在两套方案之间摇摆。针对这个问题我在 facts 结构里加了状态标记每条关键事实要么是“active”要么是“superseded”并在压缩时保留一条“已被方案 B 取代”的显式记录。同时在push_message里增加了“事实更新”接口当检测到用户明确说“不要 X / 改成 Y”时把旧的 fact 标记为失效。这个逻辑看起来简单但它直接决定了多轮对话中模型对“最新决定”的敏感度。4.4 多轮对话中的角色崩坏还有一个很容易被忽略的坑当上下文里同时存在系统指令、文档片段、多轮历史时模型偶尔会“角色混乱”。比如你本来让它当客服助手结果它突然开始模仿某篇文档里的技术作者口吻或者用文档里的第二人称来称呼用户。这是因为拼接时不同来源的文本边界没有做清楚模型分不清“哪些话是对它说的”“哪些话是要理解的材料”。解决办法是在拼接时给不同来源的内容加显式标签我在代码里写入的“参考资料”和“关键事实”就是一种给模型做信号标记的方式。更彻底的做法是用 XML 风格的块标签比如context sourcedoc_123.../context让模型明确区分“指令”和“材料”两种语言。我实测下来加上这些边界标记之后角色错乱出现的频率明显下降。5. context-mode 的进阶玩法与实际场景复盘5.1 AI 编程场景下的 context-mode如果你主要用 AI 写代码会发现context-mode的思路同样适用。现在主流的 AI 编程工具里常见有三种工作模式纯对话模式、上下文模式、代理执行模式。纯对话模式只是聊代理执行模式让 AI 自己改文件跑命令而“上下文模式”则介于两者之间——它的重点不是写代码而是先帮你把代码库结构、关键文件、相关函数调用链读清楚再回答具体问题。我用下来最顺的方式是先让 AI 扫描项目目录生成一个结构清单然后针对要改的功能点明确告诉它“只关注 controller 层和 service 层不深入 model 层”。这其实就是手动管理最短上下文。而如果要让 AI 高效干活最好是给它一个“当前任务上下文”文件里面写清楚项目背景、技术栈、相关文件路径、测试命令并且每次会话开始时把这份文件注入到开头比现场让 AI 慢慢猜高效得多。5.2 团队知识库问答场景另一个我实际做过的是团队知识库问答机器人。这个场景下 context-mode 要额外考虑权限和时效性。同样的文档A 组能看B 组不能看检索时就不能只按关联度返回必须先过滤权限再进入候选集。时效性方面知识库里的制度文档经常更新如果摘要层缓存了旧内容用户会拿到过期答案。我当时的做法是给每份文档加一个版本号每次生成摘要时带上“当前版本”字段凡是版本不匹配的片段宁可重新检索也不使用缓存摘要。整个系统上线后有个很有意思的现象用户反馈“这个机器人好像分得清轻重”。比如同样一个问题在不同部门上下文里会给出不同侧重销售组偏向活动规则技术组偏向接口细节。这是因为我在注入时不是把命中的全部摘录塞进去而是在 system 部分写了“当前用户所属部门为技术部请优先关注开发相关内容”。一个简单的字段让同样的检索结果回答了不同的问题。5.3 几个配置模板最后分享三个可以直接抄作业的配置模板适用三种常见场景。轻量对话场景适合客服闲聊或简单问答系统提示词 当前会话目标 关键事实通常是用户身份、订单号、问题类型 最近 6-8 轮对话原文文档问答场景适合知识库/PDF 阅读系统提示词强调引用来源 检索到的 Top 5 片段每条带文件名与章节 当前用户原始问题代码助手场景适合项目仓库辅助项目背景说明 技术栈清单 相关文件路径列表按重要性排序 当前任务的约束条件“不要改动测试文件” 终端报错或最新执行结果 最近 3 轮对话这三个模板的共同点都是把“当前目标”放在最前、“原始材料”放中间、“最新反馈”放最后。你可以按自己的场景调比例但顺序最好不要乱。我遇到过很多人把用户问题放到中间结果模型回答时总是慢半拍调整顺序后立刻就好了。这个内容后续还能扩展的方向很多比如根据用户历史行为做个性化上下文权重、把 context-mode 和长期记忆数据库结合、在压缩时用更强的模型保留更多决策信息。但不管往哪个方向走核心思路都是同一句话不要试图让模型记住所有东西而是主动帮它决定记住什么。我自己的习惯是把上下文当成产品的一部分来设计而不是出了问题再修补。最开始我做 context-mode 只是为了解决 AI 失忆后来发现它直接改变了使用者对工具的信任度——一个记得住关键的 AI和一个总在反复询问的 AI用户在心理上完全当成两种东西。建议你从最小的会话状态压缩做起跑通之后再慢慢加检索、加权限、加记忆分层这条路是可以越走越顺的。