news 2026/9/11 9:44:39

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 就是一套白板管理制度:哪些内容必须长期贴在角落,哪些写完后可以擦掉,哪些需要压缩成一句话贴上去。

在代码层面,这种思维转换体现在几个地方:

  • 消息列表不再只是简单的Array<Message>,而是有分区的结构。
  • 系统提示词中要动态注入最新状态,而不是只写死一段初始说明。
  • 每次请求前需要对历史信息做一次“压缩、丢弃、保留”的决策。

从这以后,我的项目里开始出现了一个专门的模块,名字直接就叫context_mode,负责所有与上下文组装、切换、压缩相关的逻辑。接下来的部分,我会展开讲讲我在实际工程中借鉴和沉淀的几种落地形态。

2. context-mode在真实项目中的四种落地形态与取舍

具体做设计的时候,不能上来就套一个“万能方案”。不同的业务场景对上下文的要求差别很大,我根据自己的实践,把常见的 context-mode 分成四种形态。

2.1 会话级上下文模式:最轻量,但只在短期对话里有意义

会话级模式是最容易理解的一种:上下文只存在于一次会话中,会话结束就清空。适合客服一次性咨询、临时问答、以及不需要跨会话记忆的工具类应用。

实现这个模式时,我一般只做两件事:

  • 限制最大会话轮数。比如超过20轮后,强制把最早的对话归档。
  • 定期用摘要替代原始历史。每5轮对之前的对话生成一句话摘要,插入到上下文中。

这种模式的好处是简单可靠,出问题容易排查。但它不解决“跨会话”的问题——用户第二天回来接着聊,AI还是什么都不记得。

2.2 任务级上下文模式:以“完成一件事”为边界组织上下文

真实工作中,用户很少漫无目的地聊天,更多是围绕某个任务连续操作。比如“帮我分析这份销售数据”“然后对比上个月”“再把结论写到周报里”。任务级上下文模式,就是以“任务”为单位来管理记忆。

我是这样做的:

  1. 当检测到新的意图(比如用户上传了一个新文件),开启一个新的任务上下文块。
  2. 每个任务块内部保持完整的对话历史。
  3. 切换任务时,上一个任务块被压缩成“任务总结”,只保留结论、关键数字和待办事项。
  4. 最新任务的上下文完整保留,确保模型专注于当前目标。

这种模式的优点是:用户在不换任务时,体验很连续;一旦换任务,模型也不会被过去大量无关历史干扰。实现的关键是任务边界识别,我通常用意图分类模型或简单的关键词规则来完成。比如出现“换个话题”“先放一放”“看下另一个”这类标志性短语时,就会产生一个任务切换提示。

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_tokens=8000): 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_tokens=8000): 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(role=MessageRole.SUMMARY, content=summary_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 应用,我建议尽早把上下文管理层放到架构图里,而不是等项目出问题再补课。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/11 9:44:13

WorkBuddy自定义专家实操指南:从原理到配置与排错

最近好几个朋友在问我同一个事&#xff1a;WorkBuddy里那个“自己创建专家”到底怎么玩。说实话&#xff0c;这个功能属于典型的“入口好找、玩明白不容易”&#xff0c;很多人打开之后看到一堆配置项就懵了&#xff0c;填了个名字和描述&#xff0c;跑了两轮发现不对劲&#x…

作者头像 李华
网站建设 2026/9/11 9:43:54

关键节点识别与解决方案设计方法论

1. 项目背景与核心价值 "于星火交汇处&#xff0c;点燃一盏灯"这个标题蕴含着深刻的象征意义。作为一名从业十余年的内容创作者&#xff0c;我理解这个标题背后传递的是一种在关键时刻、关键节点提供指引和希望的理念。它让我联想到那些在黑暗中为他人指明方向的灯塔…

作者头像 李华
网站建设 2026/9/11 9:39:30

制造业飞书实施89天落地实战:从纸质单据到数字神经

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 9:39:22

MicroPython嵌入式日志模块uLogLite设计与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华