ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

context-mode:大模型上下文管理的核心工程实践

context-mode:大模型上下文管理的核心工程实践 前阵子在调一个企业知识库问答应用遇到一个特别典型的问题用户明明在前几轮对话里强调过“我是财务部门的预算口径按年度算”结果模型聊到后面开始用自然年口径回答甚至把另一个部门的数据也算了进来。翻日志一看根本不是模型能力不行而是最开始的约束早被挤出了上下文窗口。当时跟我一起排查的老同事丢过来一句你缺的其实是一个明确设计过的context-mode而不是一味地往提示词里塞更多背景。这句话点醒了我。过去大半年我一直在做基于大语言模型的应用开发从简单的聊天机器人到复杂的RAG管线踩过不少坑之后现在我可以负责任地说上下文模式context-mode不是一个花哨的概念而是决定AI应用能不能在真实场景里稳定工作的核心工程问题。这篇文章把我自己的理解、代码实现和踩坑记录整理出来希望能给正在做类似项目的朋友一些参考。1. 为什么要单独设计一个“上下文模式”日常开发里那些对话断篇的瞬间1.1 没有上下文管理的AI应用看似能聊实则失忆先看一个最简单的例子。你用messages数组直接往模型接口里传数据每一轮用户输入后把user消息和assistant回复追加进去。这看起来没问题但实际跑上几轮就会发现上下文越长模型越容易“抓不住重点”。我归纳过三种常见的失忆现象初期约束被淹没用户在第2轮说的规则到了第15轮已经被大量中间内容挤占模型开始按照最近几轮的局部信息作答。关键实体被遗忘人名、项目编号、日期口径等具体信息如果没有被持续强化很容易在长对话里丢失。回答风格漂移用户最初要求“用表格形式输出”过了一段时间模型开始用长段落回答。这些现象的根源都很一致我们默认上下文是一个“追加式”的列表但模型实际注意力是有限且非均匀分配的。当总量超过窗口的一定比例后早期内容虽然还在 tokens 里但有效权重已经很低了。1.2 context-mode的本质把“记住什么”从模型手里拿回来context-mode这个想法本质上就是改变“上下文完全由对话历史堆砌”的默认状态把“记住什么、忘记什么、怎么组织”这件事从模型隐式处理变成我们显式管理。你可以把上下文窗口想象成办公室里的白板。什么都不管的话白板上会被写满各种临时信息重要的事项反而被覆盖掉。context-mode 就是一套白板管理制度哪些内容必须长期贴在角落哪些写完后可以擦掉哪些需要压缩成一句话贴上去。在代码层面这种思维转换体现在几个地方消息列表不再只是简单的ArrayMessage而是有分区的结构。系统提示词中要动态注入最新状态而不是只写死一段初始说明。每次请求前需要对历史信息做一次“压缩、丢弃、保留”的决策。从这以后我的项目里开始出现了一个专门的模块名字直接就叫context_mode负责所有与上下文组装、切换、压缩相关的逻辑。接下来的部分我会展开讲讲我在实际工程中借鉴和沉淀的几种落地形态。2. context-mode在真实项目中的四种落地形态与取舍具体做设计的时候不能上来就套一个“万能方案”。不同的业务场景对上下文的要求差别很大我根据自己的实践把常见的 context-mode 分成四种形态。2.1 会话级上下文模式最轻量但只在短期对话里有意义会话级模式是最容易理解的一种上下文只存在于一次会话中会话结束就清空。适合客服一次性咨询、临时问答、以及不需要跨会话记忆的工具类应用。实现这个模式时我一般只做两件事限制最大会话轮数。比如超过20轮后强制把最早的对话归档。定期用摘要替代原始历史。每5轮对之前的对话生成一句话摘要插入到上下文中。这种模式的好处是简单可靠出问题容易排查。但它不解决“跨会话”的问题——用户第二天回来接着聊AI还是什么都不记得。2.2 任务级上下文模式以“完成一件事”为边界组织上下文真实工作中用户很少漫无目的地聊天更多是围绕某个任务连续操作。比如“帮我分析这份销售数据”“然后对比上个月”“再把结论写到周报里”。任务级上下文模式就是以“任务”为单位来管理记忆。我是这样做的当检测到新的意图比如用户上传了一个新文件开启一个新的任务上下文块。每个任务块内部保持完整的对话历史。切换任务时上一个任务块被压缩成“任务总结”只保留结论、关键数字和待办事项。最新任务的上下文完整保留确保模型专注于当前目标。这种模式的优点是用户在不换任务时体验很连续一旦换任务模型也不会被过去大量无关历史干扰。实现的关键是任务边界识别我通常用意图分类模型或简单的关键词规则来完成。比如出现“换个话题”“先放一放”“看下另一个”这类标志性短语时就会产生一个任务切换提示。2.3 知识库级上下文模式引入RAG后的上下文剪枝与召回排序做知识库问答的时候上下文模式面临更大挑战。因为除了对话历史还有检索回来的知识片段要一起塞进去。很多成熟的RAG应用很容易出现“对话历史太长挤走了检索片段”的问题。我在这个层面的策略是给不同来源的信息分优先级信息类型默认优先级原因系统指令与固定规范最高决定输出格式和边界当前问题对应的检索片段高回答的主要事实依据最近2-3轮对话历史中保持多轮追问的连贯性任务总结与状态信息中提供跨轮次的全局信息全部历史对话最低默认不完整保留只保留关键部分这个优先级表并不是固定的而是要动态调整。比如当问题明显在纠错“刚才的结论不对重算”那么检索片段的优先级就要暂时降低把最近对话的原文保留下来避免模型被新的检索结果带偏。2.4 记忆分层模式短期缓冲、中期总结、长期摘要的三层设计最接近“理想记忆”的方式是模仿人脑做三层记忆结构。这也是我现在在项目里验证最完整的一套方案短期缓冲层最近几轮对话原文每次请求都完整带上。中期总结层每 N 轮对话被压缩成结构化的中间摘要这部分是在用户暂停或转任务时异步生成的。长期摘要层整个会话的关键信息、用户偏好、已完成事项被整理成key-value或自然语言摘要每次请求都注入系统提示词保证不丢失。三层之间是流水线关系。举个直观例子用户第1轮说“我是HR部门的”第50轮说“请按我们部门口径统计”如果只有短期缓冲早期信息早就被冲掉了但如果长期摘要层里一直存着“user_department HR”模型随时能读到这个全局变量。长期摘要不是一次性生成的而是增量更新的——每轮对话结束我们都会检查摘要里是否有需要新增或修改的内容。3. 动手实现一个context-mode工程代码与关键设计点概念说再多不如直接看代码。下面我分享一下我目前实际使用的最小可运行实现。为了便于阅读我用 Python 风格写并做了简化但核心设计思路是完整的。3.1 数据结构如何用Message对象管理模态切换微信搜一搜先定义基础的Message对象。from dataclasses import dataclass from enum import Enum from typing import Optional class MessageRole(str, Enum): SYSTEM system USER user ASSISTANT assistant SUMMARY summary # 新增角色用于标记压缩后的摘要 TOOL_RESULT tool_result # 工具调用结果 dataclass class Message: role: MessageRole content: str metadata: Optional[dict] None def to_openai_format(self): return {role: self.role.value, content: self.content}这里的核心思路是把摘要信息独立成一种角色。把它和普通 assistant 消息混在一起模型很难通过角色差异来理解“这是历史摘要”但独立角色可以在组装上下文时灵活控制比如短期缓冲中保留最后5条消息无论它们是不是摘要长期信息则全部来自SUMMARY类型的消息。3.2 模式切换策略何时压缩、何时丢弃、何时保留原文只有数据结构还不够还需要一个决策函数。它根据当前上下文大小、业务规则和会话状态决定下一步动作。class ContextMode(str, Enum): FULL full # 完整保留所有消息 RECENT recent # 只保留最近N轮 SUMMARY summary # 用摘要替换早期内容 CUSTOM custom # 完全自定义规则 class ContextManager: def __init__(self, max_tokens8000): self.max_tokens max_tokens self.short_term_rounds 6 # 最近6轮完整保留 self.messages: list[Message] [] def decide_next_mode(self) - ContextMode: total_tokens self._estimate_tokens(self.messages) if total_tokens self.max_tokens * 0.6: return ContextMode.FULL elif total_tokens self.max_tokens * 0.85: return ContextMode.RECENT else: return ContextMode.SUMMARY def add_message(self, message: Message): self.messages.append(message) self._maybe_compress() def _maybe_compress(self): if self.decide_next_mode() ! ContextMode.SUMMARY: return # 触发压缩生成摘要替换短期缓冲区外的早期内容 self._summarize_and_compact()这段代码里藏着几个工程细节值得单独说一下阈值0.6和0.85不是拍脑袋定的。0.6 以下模型在生成长回答时有足够的余量不会突然截断0.85 以上继续增加上下文会导致回答生成长度受限还容易被 tokens 超限报错。这两个阈值是可以调的建议根据实际问题长度统计来决定。estimate_tokens不能简单用len(content)。中文、英文、代码混合文本的字符和token比例差别很大。我用的是tiktoken分词器开销不大但比估算准确得多。压缩是异步的不一定。如果应用是同步调用可以直接在add_message里同步触发压缩但对于大模型应用压缩本身也要调用一次LLM开销不小建议放到后台队列异步执行。3.3 一个基于长度感知的自动模式切换实现再往下我用一个重写过的ContextManager来说明“摘要生成”和“模式切换”怎么结合。class SmartContextManager(ContextManager): def __init__(self, llm_func, max_tokens8000): super().__init__(max_tokens) self.llm_func llm_func # 压缩摘要时需要调用的llm函数 def _summarize_and_compact(self): # 找出超过短期保留轮数的消息 overflow self.messages[:-self.short_term_rounds * 2] if not overflow: return base_text \n.join(m.content for m in overflow) system_prompt ( 你是一个上下文压缩器。将下面的对话历史压缩成一份结构化摘要 必须保留用户明确给出的个人偏好、约束条件、任务目标、关键数据。 不要编造原文中没有的信息。只输出摘要。 ) summary_content self.llm_func(system_prompt, base_text) # 新的消息列表摘要 最近N轮完整消息 keep_recent self.messages[-self.short_term_rounds * 2:] self.messages [ Message(roleMessageRole.SUMMARY, contentsummary_content, metadata{source: compressed}) ] keep_recent这里的llm_func是一个可注入的函数在实际项目里可能是openai.ChatCompletion.create或者别的模型接口。把LLM调用封装成函数传入的好处是压缩逻辑可以被单独测试和替换。3.4 上下文窗口适配给token预留多少安全边际即使是设计好的context-mode也不能把上下文用到极限。我给自己的经验值总窗口的80%以内是安全区超过80%必须触发压缩。回答预留空间如果你的模型输出上限是500 token那请求前上下文最好不超过窗口上限减500。同时准备失败降级方案即使触发了压缩如果某轮输入特别长仍然可能超限。这时我会做二级降级——只保留最后两轮对话并在系统提示中注明“早期信息已省略”。这类兜底逻辑往往决定一个应用在高峰期是否可用。我曾经吃过亏某次压测时用户一次性粘贴了2000行日志窗口直接爆掉应用丢出了一个404错误。加了上面这个二级降级之后再也没有出现过这种情况。4. 上线后真实踩过的坑上下文一致性、费用暴涨与用户错觉把 context-mode 从示例代码搬到线上会碰到很多“理论上没毛病”但实际很麻烦的问题。这里集中讲四个我印象最深的。4.1 模式切换时“忘了之前说过什么”状态迁移的一致性最头疼的一类问题出现在模式从FULL切换到RECENT再切换到SUMMARY的过程中。假设用户前10轮在讨论一个数据分析项目第10轮说“记住最终输出要带注释”然后继续聊。如果压缩过程没有把这条约束写进摘要下一次模型回答时可能就忘了带注释。问题根源是压缩器的 pass-through 能力不足。我后来在压缩器的系统提示词里明确加了清单式要求让它必须检查这几项用户的身份信息用户明确的约束条件尚未完成的任务已经得到的结论压缩之后我还会跑一个一致性校验把压缩摘要和原始对话同时给一个小模型问“摘要有没有违反或者遗漏原文中的关键信息”只输出 yes/no。虽然会增加一点延迟但大促场景里稳定大于速度。4.2 Token账单翻倍问题上下文重复注入另一个很隐蔽的坑是重复注入。有段时间我为了让模型始终记得当前时间、用户位置等动态信息把动态信息写在了 system prompt 的末尾。结果发现每次请求的 tokens 里这种动态信息出现了两次——一次来自 system prompt一次来自对话历史里的原始提及。账单数字翻倍还不是最致命的真正烦人的是模型会因此产生混淆。它看到同一个信息出现在两个位置且措辞不完全一致比如用户说的“下周”和系统解析出的“2025-07-01”容易选择后者导致时间口径错误。我的解决方案是建立“权威信息源”机制能放进 system prompt 的动态信息就不允许再出现在对话历史中。但这需要有配套的预处理逻辑比如在组装上下文前把历史消息里的时间类内容替换成占位符避免和权威信息源冲突。4.3 用户对“记忆”的错觉到底是AI记住了还是我们存了context-mode 做得越好用户越容易产生“AI真的记得我”的错觉。这会带出一个产品设计问题用户以为AI会主动回忆起所有事情但实际只是我们做了上下文压缩。有一次测试用户在第3轮说“我叫小林”第100轮问“我叫什么”系统答对了。用户兴高采烈地说“AI太智能了”。但实际上那是长期摘要层存了一个user_name小林的kv记录。这不叫智能叫工程。这种边界很重要。如果你是产品经理或技术负责人一定要区分“记忆”和“上下文管理”真正的记忆系统是能主动想起、主动关联的甚至能形成对用户的长期画像。context-mode 只是一种让上下文适配窗口的手段它的目标是“不遗忘”不是“理解用户”。把这两件事分清楚能够避免很多产品层面的过度承诺。在实现上我现在给摘要层加了source字段标记每一条摘要的来源是“用户主动说明”“系统推断”还是“历史摘要”这样既方便溯源也能让上层系统知道哪些信息是可靠的。4.4 评测方式如何验证上下文模式真的起作用很多人写完 context-mode 就直接上线这是很危险的。上下文管理的效果好坏必须有量化指标。我这段时间用的评测方案分享给你长对话召回测试人为设计一个20轮以上的对话在第5轮埋一个关键信息在第18轮提问。看模型能否正确使用这个信息。干扰项测试在对话中间插入大量无关内容看关键信息是否仍能被召回。压缩保真度测试把原文和压缩摘要给人类标注员打分看摘要是否保留了所有关键实体和数值。费用回归测试统计每次请求的 token 数和成本看 context-mode 是否真的把平均单次请求成本降下来了。有一次我发现压缩后模型的回答准确率明显下降仔细排查后发现是摘要生成时把“人民币”和“美元”搞混了。这种细节光靠人工看对话是发现不了的必须依赖结构化的评测数据集。我还做了一个简单的召回率评估脚本def evaluate_recall(conversation: list[Message], questions: list[tuple[str, str]]): correct 0 for question, expected_answer in questions: response run_model_with_context(conversation, question) if expected_answer in response: correct 1 return correct / len(questions) if questions else 0这个脚本的核心思路是让“上下文有没有用”变成可度量的数字。维护一套容易遗漏的黄金测试集比任何空泛的评价都管用。5. context-mode的下一步更聪明的上下文而不是更长的上下文现在再回头看最初的问题——模型忘记用户是财务部门的最直接的原因是上下文管理太粗放。光靠买更大窗口的模型或者堆更多提示词解决不了根本矛盾。context-mode 的价值在于它迫使你思考每一条信息在上下文中的位置、权重和生命周期。5.1 把context-mode做成可观测的内部状态工程上我很推荐把上下文模式做成可观测的状态机。每次组装请求时都打一条结构化日志记录当前模式FULL / RECENT / SUMMARY各层消息数预估token数触发压缩的原因有了这些日志用户可以投诉“AI忘了我说的话”时你能立刻定位到是压缩器丢信息还是检索召回范围不对而不是对着聊天记录瞎猜。我在项目里用一个非常轻量的方案把日志打到 JSONL 文件里每行一个请求快照。线上排查问题时用grep按会话ID捞出来看效率很高。如果要更精细可以接上指标监控系统但那是锦上添花不是必要前提。5.2 从手动模式扩展到自动路由根据意图自动切换现在的 context-mode 更多是开发者配置好规则程序自动执行。但更理想的状态是能够根据用户行为自动选择模式判断用户当前是在闲聊、做深度分析、还是查阅知识库然后动态调整上下文的组织策略。我自己在尝试的一个简单路径是在请求入口加一个intent_router用较小的模型或者规则先判断用户意图再选择不同的ContextManager配置。比如闲聊场景使用 RECENT 模式回复短而轻快。深度分析场景使用 SUMMARY 模式把历史结论和背景全部注入。检索问答场景使用 CUSTOM 模式把检索片段优先级调高。这个做法不需要对现有框架做大改只是把 context-mode 的配置点前移值得一试。5.3 后续可以这样扩展长期记忆与多会话复用如果已经拥有了良好的上下文管理模式下一步可以自然地延伸到“跨会话记忆”。具体做法是把每次会话结束时的SUMMARY消息存到数据库里下次新会话开始时作为初始摘要加载。这样用户隔天再回来AI还能记住“昨天讨论过 XX结论是 YY”。这已经接近一个轻量级用户记忆系统了仍然是在 context-mode 基础上的合理延展。从我自己的实践来看做 AI 应用开发的这两年多最大的心得是别把上下文静态拼接奉为不可变的前提把上下文当资源当状态当产品的一部分来看待很多棘手问题都会峰回路转。context-mode 在设计上并不高深但它带来的思维方式值得足够重视。如果你正在做一个有长期价值的 AI 应用我建议尽早把上下文管理层放到架构图里而不是等项目出问题再补课。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进