news 2026/10/15 23:12:40

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型 context-mode 工程实践:缓存、摘要与检索优化

很多人第一次听到 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 小时没动静的,降级为冷会话,只保留摘要不保留完整消息。

会话恢复时,通常只重建摘要信息,等用户下一轮真正提问时再加载详情。这个“延迟加载”策略省了不少内存开销。当前项目里我是按需拉取最近 N=6 条原始消息加 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_recent=6, max_tokens=4000): 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(f"ctx:{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(f"ctx:{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 的复杂度天然会跟着业务规模和团队协作方式走,在规模未到时强行平台化,只会增加成本和延迟。

这套体系我从简单缓存逐步演进到“缓存 + 摘要 + 检索”组合模式,前后改了四个版本。现在的体会是:上下文设计不再是一个模型调用的附属品,而是大模型应用工程质量的重要部分。你在设计新功能时,可以先把“模型看到的上下文长什么样”画出来,再开始写代码,这样会少走很多弯路。

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

O2O优惠券核销预测:分段轻量化建模实战

1. 项目概述:一张优惠券背后的“行为预判力”“O2O优惠券使用预测”这八个字,乍看是电商或本地生活平台后台的一个常规模型任务,但真正把它从需求文档落到生产环境、跑出可解释的业务价值,我前后踩过三轮坑——第一轮以为只是调个…

作者头像 李华
网站建设 2026/10/15 23:11:24

中老年垂直社交应用架构:从低端机、弱网到信任安全的技术取舍

我前几天在一个家庭聚会上,帮我爸清理手机里的App,发现他手机上装了三四个社交软件,最后只留了一个。我问他为什么,他说:别的不是不好,就是“字太小”、“点了没反应”、“老是弹窗让我清理内存&#xff0c…

作者头像 李华
网站建设 2026/10/15 23:11:22

深度学习入门实操笔记:从模型原理到训练调优

深度学习这几年热度一直居高不下,身边越来越多朋友问我:到底该怎么入门?学哪些东西才算学会?网上教程铺天盖地,但大多是碎片化的知识点,今天讲个概念、明天跑个模型,很难形成体系。我把这段时间…

作者头像 李华
网站建设 2026/10/15 23:10:40

PyTorch底层原理:动态图、内存管理与CUDA执行机制解析

1. 这不是“学个框架”那么简单:PyTorch到底在解决什么问题? 很多人第一次听说PyTorch,是在某次技术分享里听到“动态图好调试”“API设计更Pythonic”,或者看到招聘JD上写着“熟悉PyTorch优先”。但如果你真去翻官方文档、跑几个…

作者头像 李华
网站建设 2026/10/15 23:10:15

TensorFlow底层原理:从计算图到部署契约的工程全链路解析

1. 这不是“学TensorFlow”,而是重建你对深度学习工程的认知很多人点开一篇叫“TensorFlow”的文章,下意识以为要学API怎么写、模型怎么搭、loss怎么定义——结果翻完发现全是tf.keras.Sequential()和model.fit()的复刻教程,跑通一个MNIST就宣…

作者头像 李华
网站建设 2026/10/15 23:08:57

面试必问:动态线程池如何确保任务执行完?五种方案详解

“面试官:如何确保动态线程池任务都执行完?”老实说,我第一次看到这道题的时候,内心是有那么一点不屑的——shutdown()再调一个awaitTermination(),这套优雅停机的组合拳谁不会啊。可真坐下来陪面试官把这道题聊深了&a…

作者头像 李华