ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

context-mode 上下文管理实战:告别AI模型“失忆”的工程方案

context-mode 上下文管理实战:告别AI模型“失忆”的工程方案 做 AI 应用的人应该都有这种体会和模型对话超过几轮之后它就开始“失忆”——你明明在前面说过项目用的是 FastAPI后面它偏偏给你生成 Flask 的代码你刚告诉它某个接口的鉴权方式下一轮它又当成新问题来重新问。这个问题的根源往往不在模型本身而在于工具的 context-mode上下文模式没有做好。所谓 context-mode就是让 AI 工具在连续交互中主动维护、管理并注入上下文的一套机制它直接决定了模型“记得什么、忘掉什么、优先看什么”。这篇文章我会从零讲清楚 context-mode 是什么、为什么它比参数调优更影响使用体验以及我实测落地的一套完整方案适合正在做 AI 编程助手、智能客服、Agent 类产品的同学参考。1. 为什么需要 context-modeAI 工具的“失忆症”困局1.1 无状态对话的痛点很多人第一次接入大模型 API 时会写一个最简单的“发送问题→拿到回答”的函数。这种用法在小规模测试时没什么问题但一旦进入真实业务立刻就会露馅。最典型的场景是 AI 编程助手你让它“帮我看看这个报错”它确实给出了分析但你紧接着说“那按照你的方案改一下”它却完全不知道“你的方案”是什么。原因在于大模型本身没有记忆。每次 API 调用都是独立的模型看到的只是你当前传给它的一堆文本。所谓“记忆”无非是在每次请求里把历史对话、项目信息、用户偏好重新拼进去。而 context-mode 要解决的就是这套“拼进去”的工程问题拼什么、拼多少、按什么顺序拼、什么时候更新拼装好的内容。我见过不少团队在这上面栽跟头。有的把整个项目代码目录一股脑塞进 prompt结果因为 token 超限直接被 API 拒绝有的只塞最近两轮对话结果模型像金鱼一样七秒之后全忘光。这些本质上都是没有建立 context-mode 的“分级管理”意识。1.2 context-mode 到底是什么简单说context-mode 是一套“有状态”的上下文管理方案。它和普通的多轮对话补丁不同普通方案是“把历史消息全部倒回去”context-mode 是“建立上下文仓库按需检索、按优先级注入、按预算裁剪”。我习惯把 context-mode 类比成一个老中医的看诊记录。老中医不会每次看诊都让病人重新描述一遍全部病史而是有一本夹着过往方剂、体质备注、过敏史的病历本。每次问诊时他只看当前症状 翻出相关旧记录而不是把整本病历背下来。context-mode 干的就是这件事它维护“病历本”并在每次模型请求前决定“这次该翻哪几页”。这个机制通常包含四个环节采集收集对话、代码、配置等原始信息、存储用结构化方式保存、检索根据当前问题挑出相关内容、注入按 token 预算组装进模型请求。很多产品只做了存储和注入跳过了检索结果就是把所有信息一股脑塞进去既浪费 token 又稀释重点。1.3 适用场景与边界context-mode 不是所有场景的银弹。它在以下几类场景中收益最明显多轮对话型 AI 助手、AI 编程工具、Agent 类自动化任务、以及需要“记住用户偏好”的个性化产品。这些场景的共同特点是单次请求无法完成目标必须依赖跨越多次交互的状态累积。反过来如果是单轮问答比如“翻译这句话”“总结这段文字”context-mode 反而是负担——维护上下文有成本和延迟没有必要。另外如果是严格保密环境、不允许持久化任何用户数据的产品context-mode 的存储环节会受到很大限制这时候需要做“会话内临时上下文”而不是“跨会话持久上下文”两种模式的设计思路差别很大。2. context-mode 的核心设计如何把“记忆”装进系统2.1 上下文的分层模型我给 context-mode 做的第一件事是把上下文分成三个层级会话层、项目层、全局层。会话层对应“当前这次对话里发生的事情”比如用户刚说的一句话、模型上一个回答、用户对上个回答的修正。这一层的特征是时效性强、信息密度高但失效也快。实现上通常就是拿一个最近 N 轮的消息队列来存。项目层对应“用户当前正在做的事”比如一个代码仓库的目录结构、核心模块的职责说明、最近修改过的文件列表。这一层不会频繁变化但每次都可能需要参考。它的核心是“项目级记忆”也就是那些你希望模型跨对话还能记住的东西。全局层则对应“用户的长期偏好和固定约束”比如“代码里禁止使用第三方 UI 库”“回答要保持中文”“命名风格用下划线”。这一层最稳定是真正意义上的长期记忆。我踩过的第一个坑就是不分层。早期我把所有信息混在一个大字典里结果项目层的内容和会话层的内容互相覆盖用户刚说的一句话可能被旧的项目摘要冲掉。分层之后每一层有独立的更新节奏和优先级问题立刻缓解了。2.2 Token 预算与窗口管理上下文设计里最硬的约束是 token 预算。模型有上下文窗口限制——常见的 8K、32K、128K 等等——但这不意味着你可以全部用完。我实践下来的经验是把总窗口分成“系统提示词区、上下文注入区、对话历史区、当前请求区”四块并给每块设定上限。一个参考分配方案是这样的假设总窗口 32K token系统提示词占 2K当前用户请求预留 4K模型输出预留 4K那么真正给上下文注入区的只有 22K。这 22K 里再按比例分会话层占 50%项目层占 35%全局层占 15%。这个比例不是拍脑袋定的而是根据实测效果调出来的——项目层太多会把对话挤掉导致模型“只顾着看代码忘了你在说什么”。窗口管理还有一个关键动作溢出处理。当内容超出预算时不能直接截断而要“压缩 丢弃 摘要”三管齐下最老的低价值内容直接丢弃中等价值的内容压缩成摘要高价值的保留原文。这个策略会在后面的实现部分详细说。2.3 上下文的写入时机与更新策略什么时候把信息写进上下文仓库比怎么存更重要。我见过很多实现是“每次收到用户消息就全量更新”这在低频场景没问题但在高频交互中会造成大量无效写入——用户往往只是在重复确认并没有产生新信息。我的经验是采用“事件驱动 定期快照”结合的更新策略。事件驱动是指只有检测到关键事件才触发写入比如用户明确给出一个新指令、模型生成了最终答案、用户纠正了模型的错误、代码文件发生了变化。定期快照则是每隔一段时间比如每 30 分钟把当前状态压缩成一份摘要存档防止长时间运行后细节丢失。这里有一个很实用的细节用户纠正模型的内容优先级要高于模型原本的输出。比如模型第一次说“用 Redis 做缓存”用户纠正“用 Memcached”那么上下文仓库里应该记录“用户要求用 Memcached”而不是保留“推荐 Redis”这条过时信息。实现时可以做“覆盖标记”后写的纠正信息自动标记旧内容为失效。3. 从零搭建 context-mode一个可落地的实现方案3.1 数据结构设计选型上我没有引入重型数据库基于 JSON 文件 内存索引就够用了关键是要设计清楚数据结构。下面是我在实践中沉淀出来的核心结构。dataclass class ContextItem: item_id: str # 唯一标识 layer: str # session / project / global content: str # 实际内容 source: str # 来源user / model / system / file priority: int # 0-10越大越重要 created_at: float # 创建时间 updated_at: float # 最后更新时间 expires_at: float | None # 过期时间None 表示不过期 tags: list[str] # 标签用于检索 dataclass class ContextRepo: items: list[ContextItem] session_summary: str | None # 会话层压缩摘要 project_snapshot: dict # 项目层快照 global_profile: dict # 全局层配置这个设计的要点在于“分层 过期”。layer 字段决定了这条内容归属于哪一层expires_at 解决了上下文污染问题——会话层的临时细节比如“用户当前正在看 main.py 的第 30 行”过几分钟就该自动失效否则会干扰后续对话。3.2 检索与注入流程每次用户发起新请求时context-mode 的检索模块要回答一个问题从仓库里挑哪些内容注入这次请求我用的方法是“关键词召回 相似度排序 优先级加权”。def retrieve_context(query: str, repo: ContextRepo, max_tokens: int): # 1. 按标签和关键词做粗筛 candidates [item for item in repo.items if not is_expired(item)] # 2. 按 token 预算从高优先级开始注入 selected [] used 0 for item in sorted(candidates, keylambda x: (x.priority, x.updated_at), reverseTrue): item_tokens estimate_tokens(item.content) if used item_tokens max_tokens: continue selected.append(item) used item_tokens # 3. 组装成系统提示词的一部分 return format_selected_items(selected)这个流程看着简单但有几个细节容易踩坑。第一排序必须同时考虑优先级和时间否则新出现的低优先级内容永远排在后面模型看不到第二估算 token 不能靠 len()中文字符和英文单词消耗的 token 数完全不同需要用分词器估算第三注入顺序也很关键——会话层放在对话历史之前项目层放在用户请求之前这样模型读到的顺序符合“先背景、再历史、后当前问题”的自然逻辑。3.3 压缩与摘要策略context-mode 里最容易被低估的模块是摘要。当历史对话超过预算时不能简单丢最旧的因为最旧的部分往往包含项目的初始背景丢了之后模型就不知道“我们为什么要做这件事”。我的做法是“滚动摘要”每 20 轮对话触发一次摘要把旧的 20 轮压成一段 300-500 字的摘要替换掉原文。摘要要包含四个要素当前任务目标、已确定的方案、悬而未决的问题、用户明确表达的好恶。这一段摘要也就成了会话层的“锚点”后续所有对话都以它为基础。def summarize_old_messages(messages: list[dict]) - str: text \n.join([f{m[role]}: {m[content]} for m in messages]) prompt f请将以下对话压缩为摘要必须包含 1. 当前任务目标 2. 已确定的方案和结论 3. 尚未解决的问题 4. 用户的明确偏好或约束 对话内容 {text} return call_llm(prompt, max_tokens500)滚动摘要有个连带好处它能天然实现“遗忘”。AI 助手最怕的事情之一就是对话轮数太长、模型被一堆细枝末节绕晕摘要把琐碎的过程忘掉只保留决策和结论反而让模型更聚焦。3.4 一个可复用的最小实现光讲设计不落地是耍流氓。我写了一个约 200 行的最小实现框架核心逻辑其实就是三个类采集器、仓库、注入器。采集器负责监听对话事件并写入仓库仓库负责存储、过期清理和检索注入器负责把仓库内容按预算组装进系统提示词。这个框架跑起来之后AI 助手在“跨轮记忆”上的表现立刻上了一个台阶——第二天继续对话时它还记得昨天讨论的技术选型不需要用户重新解释一遍。如果你不想自己造轮子也可以用现成的 LangChain 里的 ConversationSummaryBufferMemory、LlamaIndex 的 ChatMemoryBuffer 做替代它们内置了 token 预算管理和摘要功能。但我的建议是不要直接拿来用——因为它们的检索能力比较弱和“上下文仓库”的核心思路有差距。自己实现一次你会对 context-mode 的理解深得多。4. 实测记录与效果对比开与不开完全是两个世界4.1 测试场景设计为了验证 context-mode 的实际效果我设计了三个测试场景。第一个是“跨轮纠错”前 5 轮让模型给出方案中间用户明确纠正两个关键决策第 10 轮再问它“当前方案是什么”看它是否记住了纠正内容。第二个是“项目记忆”上午让模型分析一个代码仓库下午不提供仓库信息继续问“刚才那个模块的入口函数叫什么”看它能否回答。第三个是“长对话稳定性”连续 50 轮对话每 5 轮埋一个“之前提过的关键信息”看模型之后能否回忆起来。这三个场景分别对应 context-mode 的会话层、项目层、全局层能力能比较全面地覆盖核心功能。4.2 实测表现与关键数据结果差距相当明显。在“跨轮纠错”场景中没有 context-mode 的版本在第 6 轮之后就忘了用户纠正过什么第 10 轮给出的方案又回到了被否决的 Redis 方案加上 context-mode 之后第 10 轮模型明确回答“根据您之前的纠正这里使用 Memcached”完全正确。在“长对话稳定性”场景里无 context-mode 的版本在 30 轮之后回忆准确率只有不到 20%基本靠猜有 context-mode 的版本维持在了 90% 左右主要失分点在于摘要压缩时丢失了一些细节数字比如端口号、版本号。这给我提了个醒摘要策略对“精确数字”类的信息不够友好需要在摘要提示词里额外强调“保留所有数字和专有名词”。另一个值得记录的数据是 token 消耗。很多人担心 context-mode 会让成本上升实测下来反而是下降的——因为滚动摘要把旧对话大幅压缩平均每轮请求的 token 数比不做管理时节省了约 30%。上下文管理不是多花钱是省钱。4.3 我踩过的坑与排查心得第一个坑是“过期时间设得太激进”。一开始我把会话层条目的过期时间设为 10 分钟结果用户隔了半小时回来问“刚才你说那个方案有问题具体是什么”模型已经完全不记得了。后来改成“活跃会话不主动过期只有会话结束才清理”问题才解决。第二个坑是摘要覆盖了关键决策。有次滚动摘要把“用户否决了 PostgreSQL改用 MySQL”这条信息压没掉了后面模型又开始推荐 PostgreSQL。排查之后发现是摘要提示词里没写“必须以原样保留用户明确否决过的选项”。现在我在摘要提示词里加了一行“用户否决/纠正的内容必须逐字保留”再没出现过这个问题。第三个坑是上下文污染。项目层快照更新时会把旧代码片段残留在仓库里检索时偶尔把已废弃的内容注入到新对话。解决方式是每次更新项目快照时先删除同一 source 下的旧条目再写入新条目保证“同一来源同一时刻只有一份有效数据”。5. 进阶优化让 context-mode 真正好用5.1 上下文优先级与衰减机制基础方案能跑之后我开始调优先级和衰减。一个常见问题是项目层里塞了几十个文件的信息其中有些文件已经三个月没改过了但它还是和今天刚改的文件一样被注入。这样不行。我现在给每条 ContextItem 加了“活跃度”概念每次文件被修改或引用时活跃度 1每次检索命中也 1同时按天衰减 5%。注入时排序用的不是静态 priority而是 priority × 活跃度的综合分。这样“最近在动的文件”自然排在前面“三个月没动的老文件”即使 priority 高综合分也会掉下去。这个机制实测下来对代码仓库场景非常有效模型给出的建议会更贴近当前状态。5.2 超大项目的上下文策略代码仓库超过一定规模后单靠“注入文件信息”是行不通的——文件太多token 预算根本不够。我的做法是引入“渐进式上下文加载”第一轮只注入仓库目录树和用户当前提到的文件如果模型回答中表现出“信息不足”比如它说“我不知道 X 文件的内容”下一轮再动态加载相关文件。实现核心是一个“按需加载器”它根据用户当前问题和模型的需求描述用关键词在仓库里搜索文件名、函数名、类名命中的文件才被加入上下文。对于特别大的 monorepo还要在加载前先看文件大小超过 5000 行的文件优先加载其函数签名列表而不是全文。这本质上是在 context-mode 之上再做了一层“文件级检索”两者配合才能覆盖大型项目。5.3 后续可以扩展的方向context-mode 这套思路再往下走有几个很自然的方向。第一个是“跨会话长期记忆”不只是记住当前项目的技术选型还能记住用户在不同项目间的统一偏好比如“所有 Python 项目都用 uv 管理依赖”这需要把全局层升级成持久化数据库并做统一的偏好归纳。第二个是“多 Agent 上下文共享”在一个 Agent 团队里不同 Agent 各自维护 context-mode但共享一个项目层仓库避免重复劳动。第三个是“基于评分的上下文淘汰”给每条上下文记录一个“最近有用度”分数定期用模型给历史会话打标签把从没被引用过的内容自动归档让仓库始终保持精简。从实际收益来看context-mode 是我在 AI 应用开发里投入产出比最高的一个模块。它不炫技不需要多么复杂的算法就是一套扎实的工程设计——分层、预算、检索、摘要、更新策略。但就是这套朴素的工程把 AI 工具从“金鱼记忆”变成了“靠谱助手”。最后再分享一个我自己的体会做 context-mode 别一开始就追求完美先用最简单的分层 滚动摘要跑通观察模型在长对话中的表现再逐步加检索和衰减机制。你会明显感受到每加一层设计模型的“记忆力”就扎实一分用户对它的信任也多一分。
RELATED READING

延伸阅读

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