资讯动态

大模型 context-mode 工程实践:缓存、摘要与检索优化

发布时间:2026/10/15 23:12:42 来源:尧图企业网站定制
很多人第一次听到 context-mode 这个词多半是在把大模型接进业务系统的时候。我这边接手过一个客服问答项目最初的模型应答质量其实不错但一旦多轮对话超过三轮模型就开始犯迷糊要么把上一轮的用户意图记成当前轮次要么回答内容开始“跑偏”更麻烦的是每次请求都拿着完整的聊天记录去调用模型接口延迟越来越高token 成本也肉眼可见地涨。后来我把精力集中在 context-mode 上也就是把“上下文”当成一个独立模块来设计和运维而不是每次现拼一段历史消息塞给模型。做完之后会话准确率、响应速度、成本都同时有了改善。这篇博文把我踩过的坑、最终沉淀下来的方案、以及关键代码结构整理出来希望能给正在做类似功能的开发者省一些弯路。适用对象包括正在写聊天机器人、客服系统、Copilot 类应用或者任何需要让大模型“记住前面聊过什么”的开发者。1. 从无状态到有状态context-mode 到底在解决什么问题1.1 大模型的“失忆”根源不在模型而在方式大模型本身是无状态的。同样一个问题你单独发给它和放在第十轮对话里发给它只要拼接进去的内容一致模型产出的结果就一致。这个特性很干净也带来一个核心约束模型只能“看到”你本次请求给它什么。所谓记忆完全靠调用方安排。过去很多项目把历史消息一股脑拼进 system prompt 或 user message简单直接小规模试跑没问题。但一旦进入真实业务马上会遇到三个硬伤token 窗口很快被旧消息占满模型注意力被冗余信息稀释反而记不住最新意图不同会话之间如果需要隔离还得额外做标记和过滤。context-mode 就是为了解决这三件事而存在的它把上下文拆成“要保留什么”“从哪里取”“怎么把取到的内容组装回请求”三个步骤而不是每次从头把全部记录搬进 prompt。1.2 上下文不只是聊天记录做 context-mode 的第一件事是把“上下文”这个概念拆开。聊天记录只是其中的一部分还应该包括用户画像、当前会话的临时状态、检索到的参考知识、以及上一轮模型输出的摘要。把这么多信息揉成一个大字符串模型反而看不到重点。我习惯把 context-mode 拆成静态上下文和动态上下文。静态部分指不大变化的内容比如用户偏好、角色设定、业务规则这类可以组合后长时间复用。动态部分则是每一轮都在变化的比如最近几轮消息、当前追问的对象、阶段性结论。静态上下文适合放 system prompt动态上下文适合放在 user 侧的消息里。分工明确后组装成本会低很多后续排查的时候也更容易定位问题。1.3 先定一个基本原则不追求“完整记忆”追求“有效记忆”我做过一个对比实验。同一个问答场景一组把所有历史消息原封不动传给模型另一组只传最近三轮消息加一段“前面的结论摘要”结果后者在回答准确率和延迟上都更好。原因不复杂模型的注意力资源有限给的信息越多越容易“看花眼”。有效记忆的构建逻辑是确认模型在当前轮次回答时真正需要哪些前提条件。如果是连续追问“用户上一轮问过什么”是关键如果用户第 20 轮才提到一个第 2 轮的细节与其回溯原始消息不如在摘要里让相关信息浮现出来。所以 context-mode 要设计成一个对“记忆内容负责”的模块既不是无脑存全部也不是简单截断。2. context-mode 的三种落地路径缓存、摘要、检索2.1 基于会话 ID 的历史缓存与滑窗策略最简单的落地方式是给每个会话一个 session_id把每轮消息写入缓存。读取的时候按时间倒序取最近 N 轮加上超过 N 轮之前的摘要一起传给模型。这里的 N 不是拍脑袋定的需要根据业务实测。查询物流类场景用户往往在两三轮内就把问题说清楚了N 取 5 足够客服投诉场景用户情绪化表达多重要细节可能在前面几轮里我会把 N 放到 8同时强制开启摘要。滑窗策略的关键在裁剪时机。常见做法是当轮数超过 N 时把最前面一半消息动态压缩成一段摘要继续保留最近一半原始消息。每轮都做一次裁剪是浪费的更好的节点是“新消息写入后检查当前长度是否超过阈值”。阈值既看轮数也看字符数。基准项目里我会同时限定 20 轮或 6000 个汉字超过任一条件就触发摘要。这样长对话中模型始终能看到最近 20 轮以内的完整表达更早的则用摘要兜底。2.2 摘要压缩让模型自己决定哪些信息重要摘要不是把长句子改短那么简单而是要把“对后续回答仍然重要的事实”抽出来。我试过两种方式一种是用摘要模型单独跑一遍历史消息另一种是用硬编码规则取关键词。后者省成本但效果不稳定尤其是遇到隐含指代时规则会把“那个文件”这种指代词漏掉导致后续回答断线。在资源允许的情况下我推荐把摘要做成独立的 context context build 任务每天晚上或在长会话达到阈值时把之前的消息交给一个小模型做一次结构化摘要。摘要模板固定为“用户核心诉求 已经确认的信息 待解决事项 最近一次结论”。这样即使后面模型换掉摘要仍然能直接复用。摘要内容同样存在缓存中和原始消息分开存放方便审计。2.3 检索增强只在需要时拉取关键情报第三类方式适合知识库型场景比如产品手册问答、内部文档助手。这种情况下模型回答依赖的往往不是完整的聊天记录而是某一段文档内容。如果把整个文档都带进提示词窗口会爆掉不带又不够准确。检索增强的思路是每一轮请求前用当前问题去向量库里找最相关的 3 到 5 个片段连同最近对话一并组装成上下文体系。这里要特别关注检索器和对话模块之间的对齐。用户在第 1 轮问功耗第 2 轮说“那电池呢”如果只对第 2 轮的原始文本做检索很容易检索不到结果。我通常会把当前问题与上一轮主要概念拼接后再检索比如“电池 续航 功耗”——这类组合查询在很多向量库里能明显提升命中率。实践中我推荐至少保留最近 1 轮的组合关系而不是把全历史都用于检索否则延时会变得不可控。3. 工程化实现一个可复用的 context-mode 模块怎么搭3.1 数据结构的设定先定清楚再写逻辑落到代码之前把上下文的数据结构定清楚比任何优化都有效。我设计的核心结构里每个会话对象包含 session_id、messages原始消息数组、summary当前摘要、metadata用户画像、入口来源、业务字段、updated_at。messages 里的每条消息又包含 role、content、timestamp、msg_id。之所以把 timestamp 和 msg_id 独立出来是为了做幂等和做裁剪时方便。这里有个容易忽略的点不要把业务字段放进 content 里。比如用户选了哪个套餐、当前绑定的手机号这些适合进 metadata模型需要时由加载器从 metadata 读出来拼进上下文。这样做的好处是检索和摘要不受到无关业务字段的干扰后续改表单也不会污染历史记录。字段设计示例字段名类型作用session_idstring标记唯一会话关联缓存和日志messagesarray原始消息列表含角色、内容、时间summarystring旧消息压缩后的结构化描述metadatajson用户属性、来源、业务关键参数updated_atdatetime最近一次更新时间用于过期策略3.2 缓存读写Redis 还是内存大多数中小项目直接上 Redis 比较稳妥理由很简单会话数据需要共享给多个实例如果只用进程内存负载均衡后用户两次请求落到不同节点上下文就丢了。Redis 的 TTL 机制也适合做会话过期清理。我给会话设置了两种过期时间热会话 2 小时冷会话 24 小时。热会话指最近十分钟内有交互的会话滑动续期超过 2 小时没动静的降级为冷会话只保留摘要不保留完整消息。会话恢复时通常只重建摘要信息等用户下一轮真正提问时再加载详情。这个“延迟加载”策略省了不少内存开销。当前项目里我是按需拉取最近 N6 条原始消息加 summary 一起送给模型轮次再往上就交给摘要兜底。3.3 Prompt 组装模板与注入顺序组装 Prompt 的顺序会影响模型理解效果我按固定顺序拼接先 system 段包含静态上下文和业务规则再 summary 段承接旧消息摘要然后最近消息段按时间正序放入最后 N 条最后是当前用户输入。这样组装后的文本模型在结构上能清楚区分哪些是背景、哪些是事实、哪些是当前问题。注入顺序里有个小细节如果 current query 和最近消息有重复表述不需要去掉模型会自己判断硬去重反而会丢失口语里的情绪信号。摘要段我固定用一行“以下是对之前对话的总结”做引导实验里比直接放摘要的准确率高出不少。这种物理标记看似不起眼却能让模型在里程碑式的长对话里稳定输出。3.4 核心代码骨架参考下面的代码是一个 context-mode 模块的最小实现骨架用 Python 写的核心是 build_context 函数。每次请求进来先从缓存读会话再按需取消息和摘要最后拼装出模型输入。import json import time from typing import List, Dict, Optional class ContextMode: def __init__(self, redis_client, max_recent6, max_tokens4000): self.redis redis_client self.max_recent max_recent self.max_tokens max_tokens def get_session(self, session_id: str) - Dict: raw self.redis.get(fctx:{session_id}) if not raw: return {session_id: session_id, messages: [], summary: , metadata: {}} return json.loads(raw) def save_session(self, session_id: str, data: Dict): self.redis.setex(fctx:{session_id}, 7200, json.dumps(data)) def build_context(self, session_id: str, user_input: str, system_prompt: str ) - Dict[str, str]: sess self.get_session(session_id) messages sess.get(messages, []) recent messages[-self.max_recent:] summary sess.get(summary, ) parts [] if system_prompt: parts.append({role: system, content: system_prompt}) if summary: parts.append({ role: system, content: f以下是对之前对话的总结{summary} }) parts.extend(recent) parts.append({role: user, content: user_input}) # 更新会话缓存保留原始消息总量 messages.append({role: user, content: user_input, time: int(time.time())}) sess[messages] messages self.save_session(session_id, sess) return {messages: parts, session_id: session_id}实际使用时还需要在保存前把超过阈值的旧消息交给压缩器生成 summary。压缩器可以单独写成一个函数这里不展开。我的建议是先跑通上面的骨架再把压缩逻辑挂在“消息数超过 max_recent”那个节点上逐步迭代。3.5 更新与持久化的折中方案有些项目对响应速度要求极高Redis 每次读写都有网络开销。折中方案是本地内存缓存热会话Redis 做后台持久化。具体做法实例内存里保存 session_id 到上下文的映射TTL 设为 10 分钟同时异步把变化写回 Redis。这样用户连续提问时大概率直接命中内存不需要每次访问 Redis。代价是如果节点崩溃内存里未持久化的数据会丢。我的经验是把这个丢数据的概率控制在很低水平做法是每次写入 Redis 前在内存里做一个两分钟的小窗口超过两分钟才允许保留在 Redis。对客服、助手类场景丢失最后两分钟的上下文远比每次请求多 10ms 延迟更容易接受。不同业务的风险偏好不同这个需要按实际取舍。4. 常见问题排查与避坑记录4.1 会话串场的深层原因缓存 key 没带业务维度最开始我在公司内部做过一个联调环境用户反馈“A 用户能看到 B 用户的会话内容”排查了很久才发现不是模型问题而是缓存 key 只用了 session_id但前端生成的 session_id 在弱网环境下会被后端重新生成到相同值。后来我把 key 改成了业务线 ID 用户 ID session_id 三段拼接彻底解决了串场。这个坑让我总结出一个原则上下文缓存的 key 里永远不要只放一个短随机串至少带上能够唯一标识用户和业务入口的信息。否则上线后轻则数据错乱重则造成敏感信息越权。日志里也要把 context key 打出来方便快速复现问题。4.2 上下文漂移模型突然“忘了”前面的信息模型不是每次都稳定遵守摘要偶尔会出现“前面总结说过的事后面又茫然”的情况。这不是 context-mode 没传而是摘要本身写得过于精简模型把它当成弱参考。解决方式是给摘要加一点约束语义比如在摘要末尾固定写上“以上信息如需细节仍以最近消息为准”。加了这句话之后模型在需要细节时会更倾向于从最近消息中找线索而不是依赖摘要脑补。另一个更隐蔽的原因是摘要更新时机不对。如果新消息进来后没及时把摘要替换成“包含最新结论”的版本模型会把旧摘要和新消息里的矛盾内容当成冗余处理。所以我建议每次长消息压缩完成后把 summary 的版本号也存下来和 updated_at 一起展示在调试工具里。排障时一眼就能看出当前用的是哪一版的记忆。4.3 token 触顶与超时的处理策略即便做了裁剪长文档检索和超大摘要同时出现时仍可能触顶。我的策略是在组装之前对候选消息做一次 token 粗估。中文按一个汉字约 1.5 token 估算如果超过预算优先压缩摘要而不是直接丢弃最近消息。缩时先生成摘要再判断是否仍超如果仍超就从中间部分开始裁掉相对无关的辅助知识片段。核心原则是最近消息优先保留摘要兜底静态知识最后舍弃。超时问题有时候不是模型答得慢而是 context 组装时调用了检索接口而检索接口响应不稳定。我在代码里给检索调用加了 150ms 超时和降级逻辑超时后直接跳过检索只带最近消息和摘要请求模型。宁可答案准备不足也不让用户等太久。4.4 排查工具可视化审视每次 request 的真实内容context-mode 系统上线一段时间后最让我受益的是一个简单的 debug 接口。它会把每次发给模型的 messages 组装结果打到一个日志文件或独立存储里内容包括来源 session_id、各段字符长度、摘要版本、检索命中片段数。每次用户反馈“回答不对”我第一件事就是打开这段日志看模型到底“看见了什么”。很多时候发现问题根本不在于模型能力不足而是组装时把某个字段拼错了或者摘要没有更新到最新版。没有这个可视化工具单靠猜改提示词会浪费大量时间。我建议在小流量时期就埋好这个点日志保留至少一周比上线后出问题再补省力得多。5. 从模块到平台context-mode 可以继续长成什么样单点的 context-mode 解决当前业务没问题但如果团队要做多个大模型应用比如同时维护客服机器人和文档助手共享一套上下文基础设施会划算很多。把缓存、摘要、检索、组装模板四部分做成独立服务各业务线通过接口传入自己的系统提示和业务字段这样新项目接入成本会大幅下降。我这里踩过一个比较大的坑一开始把组装逻辑写在业务代码里导致客服线和文档线各维护一套 context 代码加上不同的业务字段后来改动一个通用逻辑要同步改两个项目。改成公共模块后业务线只传入配置和校验规则核心逻辑统一维护。除了省人力排障和监控也会简单很多因为所有日志格式一致埋点指标一致。如果你只是一个人维护一个小项目不建议一上来就做平台化先写一个五千行以内的模块化代码把功能跑通等第二个场景出现时再抽象也不迟。context-mode 的复杂度天然会跟着业务规模和团队协作方式走在规模未到时强行平台化只会增加成本和延迟。这套体系我从简单缓存逐步演进到“缓存 摘要 检索”组合模式前后改了四个版本。现在的体会是上下文设计不再是一个模型调用的附属品而是大模型应用工程质量的重要部分。你在设计新功能时可以先把“模型看到的上下文长什么样”画出来再开始写代码这样会少走很多弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑