最近我在做智能助手改造,被“机器不记事”这件事折磨得够呛,正好深入研究了一下 context-mode。这个词你在很多 AI 工具里能看到,但真正理解它的人不多。简单说,context-mode 就是让系统“带上脑子干活”——记住你刚才说过什么、现在在做什么、有哪些约束条件,而不是每轮都把背景重新交代一遍。这篇文章我会从实际体验出发,把 context-mode 的原理、实现思路、踩坑记录都拆开讲,适合正在做 AI 应用、智能助手、知识库产品的开发者,也想给那些被“对话失忆”折磨的用户一个解释:为什么有些工具用起来像傻子,有些像懂事的老同事。
1. context-mode 到底是什么
1.1 从一个让人抓狂的场景聊起
先想象一个真实场景:你让 AI 帮你写一份产品方案。第一轮你交代了团队背景、目标用户、预算范围大概是 5 万左右。AI 写得不错。第二轮你说“把第二部分再改得激进一点”,结果它把你的原意理解成“把预算改成 50 万”,后面全乱套。你开始一个字一个字地纠正,比自己做还累。这就是没有 context-mode 的日常。
反过来看真人助手是怎么工作的。你上午跟她说“小王下周请假”,下午跟她说“帮我订周三人少的餐厅”,她不会反问“小王是谁、下周三是什么意思”。因为她把每一次对话都放在同一个上下文里理解,甚至能主动关联你之前的偏好。context-mode 要做的事情就是这层东西:把散落的对话、环境信息、任务目标整合成一个可调用的“记忆场”,让 AI 的行为像连续的人,而不是每次都像第一次见面的陌生人。
1.2 三个核心特征
我做了几个不同形态的 context-mode 之后,发现真正值得做的,不是简单把聊天历史堆在一起,而是具备三个特征。
第一是连续性。系统要能跨轮次保持话题一致,用户不需要重复说“我之前提到的那个东西”。第二是感知性。它不只是被动接收历史,还能读取当前环境,比如当前打开的文件、当前所在的目录、当前正在处理的任务编号。第三是主动性。在合适的时机,它能自己把相关历史或资料捞出来塞进对话,而不是等用户手动触发。
对比一下会很清楚:
| 维度 | 普通对话模式 | context-mode |
|---|---|---|
| 跨轮记忆 | 基本没有,靠用户重复 | 有结构化记忆,自动延续 |
| 环境感知 | 不感知 | 读取当前文件、任务、上下文 |
| 历史调用 | 全量塞入或完全不塞 | 按相关性主动检索 |
| 用户体验 | 像找陌生人办事 | 像找老搭档配合 |
| 实现成本 | 低 | 中高,需要专门设计 |
说白了,context-mode 不是某一个开关,而是一整套围绕“上下文生命周期”的设计:上下文怎么写入、怎么存储、怎么检索、怎么更新、怎么过期。理解了这一层,后面所有技术选型才有意义。
2. 为什么要做 context-mode:三个真实痛点
2.1 每轮都在重复交代背景的崩溃
我自己用各种写作助手感触特别深。写长文章时,每次新建一个会话都要重新上传需求文档,再贴一遍品牌调性,再强调几次目标读者。有一次我连着创建了七八个会话,每个都重复同一段背景说明,最后自己都烦了,直接放弃那个工具。
这背后的代价很直观:用户每重复一次背景,就多一次流失的可能。你做一个客服机器人,用户问完一个订单问题,换一个话题再问一次,如果机器人完全忘了前面说过的内容,用户会觉得这工具“根本没法聊天”。做产品的人容易盯着准确率指标,但 context-mode 对体验的改善,往往比单轮回答准确率提升更明显。
2.2 长对话里前后矛盾的尴尬
第二个坑是长对话的一致性。模型本身没有持续记忆,它只能依靠当前请求里给它多少历史。如果你把 20 轮对话全塞进去,前面几轮的信息会被后面淹没,模型经常在关键决策上“失忆”。比如你前几轮确定了方案 A,第五轮它给你讲方案 B,还写得头头是道,因为第一轮的内容已经被挤出注意力范围了。
我遇到过最离谱的一次,是让 AI 帮我写项目排期。前面定的交付日期是 3 月 15 日,中间聊了很多细节,最后它写总结时变成了 3 月 25 日,我差点直接发给客户。这种问题不解决,context-mode 就算白做了。真正的记忆不应该只是“把文本堆进窗口”,而是要有一个可以被检索、被验证、被修正的结构化状态。
2.3 多任务并行时记忆串味儿
还有一类场景是“串味儿”。很多工具支持一个会话聊所有事,但你跟它先讨论产品设计,再让它帮忙写演讲稿,它很容易把两件事搅在一起。我实际测过,有些助手在回答演讲稿时还会一本正经地引用产品需求里的术语,读起来非常诡异。
context-mode 在这种情况下需要按任务域隔离上下文。同一个会话里可以切换多个上下文,或者用明确的 task_id 把不同任务的记忆分开。这就像你的桌面可以同时摊开几个文件夹,但每个文件夹里的资料不会自动互相污染。没有隔离,上下文越多反而越乱。
3. 核心原理拆解:context-mode 背后的关键拼图
3.1 上下文窗口和 token 预算
要谈 context-mode,绕不开上下文窗口(context window)和 token 预算。现在主流模型给到 8k、32k、甚至 128k 的窗口,听起来很大,但真用起来远没有想象中宽裕。一个中文 token 差不多对应 0.6 到 1.5 个字,128k token 也就十来万字,看着够用,可你不可能把窗口塞满,因为还要给模型留生成输出的空间。
我自己的经验公式是:
可用输入预算 = 模型窗口大小 - 系统提示词占用 - 当前用户输入长度 - 保留输出空间举一个例子。模型窗口 32k token,系统提示词吃了 2k,用户这次输入 1k,输出至少需要 4k,那你能拿来放历史上下文的只有 25k。如果你有 50k 的对话历史,硬塞肯定爆,这时候就必须有取舍策略:截断、滑动窗口、摘要压缩、检索召回。
这里面还有一个容易被忽略的点:预留输出空间不能太小。生成类任务如果输出空间不够,经常出现话说到一半被截断。我在接入 context-mode 时习惯预留总窗口的 15% 到 20% 给输出,宁可历史少放一点,也要保证回答完整。这个比例是我试了多次之后比较稳的经验值。
3.2 记忆分层:短期、工作、长期怎么配合
我见过很多失败的 context-mode 实现,问题都出在“把所有记忆平铺在一起”。最后 prompt 又臭又长,模型反而不知道该重点关注啥。做得好的方案,一定是有层次的记忆。
我把记忆分成三层:短期记忆、工作记忆、长期记忆。
- 短期记忆:当前会话最近几轮的对话,直接保留原文,按顺序放进 prompt。它解决“刚才说了什么”。
- 工作记忆:当前任务的关键信息,比如目标、约束、已确认决策、进度状态。它解决“现在要做到什么程度”,通常用一个结构化的摘要字段维护。
- 长期记忆:跨会话沉淀的用户偏好、历史项目资料、过往问答。它解决“这个用户长期关心什么”,通常要落到向量库里做检索。
三层记忆的配合方式也有讲究。短期记忆用滑动窗口管理,工作记忆每次请求前动态更新,长期记忆只在检索命中的时候才带进 prompt。这样既控制了 token 占用,又让模型始终能看到最该看的东西。
| 层级 | 存在哪 | 何时更新 | 典型例子 |
|---|---|---|---|
| 短期记忆 | 会话对象里 | 每次对话后追加 | 最近 8 轮对话原文 |
| 工作记忆 | 结构化字段 | 每轮提取关键信息 | 预算 5 万、截止 3 月 15 日 |
| 长期记忆 | 向量库 / 数据库 | 离线或批处理 | 用户偏好“喜欢简洁风格” |
3.3 检索增强(RAG)怎么配合 context-mode
很多人的第一反应是:既然要上下文,那把历史全丢给大模型不就行了?实际上这样做又贵又笨。历史越长,模型对早期内容的注意力越低,这就是所谓“lost in the middle”,中间部分信息最容易丢失。硬塞全量历史,等于把重要信息埋在垃圾堆里找。
这时候就要用到 RAG,检索增强生成。核心思路是:把历史对话切成小段,向量化后存进向量库;每次来新请求,先用当前问题去检索最相关的 Top-K 段内容,再拼进 prompt。这非常像人脑的记忆机制,不是把所有经历都完整回放,而是按需提取有线索关联的部分。
我的经验是,最近几轮用滑动窗口直接带,超过一定轮数就走 RAG 召回。具体阈值要看业务场景,我在实际项目中通常以 8 到 10 轮为分界线。窗口内的原文优先,窗口外的内容靠相关性决定是否出场。这样做的好处很直接:token 占用可控,模型注意力聚焦,历史调用也变得更聪明。
4. 从零实现一个轻量 context-mode:实操记录
4.1 整体架构和关键选型
我知道很多人看理论容易晕,直接上实操。我用一个轻量 Python 服务做了上下文模式的原型,核心就三个模块:会话状态管理、记忆维护、检索召回。
技术选型上我故意没用重东西。会话状态用内存里的数据结构存储,方便理解;向量检索直接用本地计算余弦相似度,没有引入向量数据库。真正放到生产环境时,可以换成 Redis 做会话缓存、Milvus 或 pgvector 做向量检索,但核心逻辑完全一样。
整体流程是这样的:
- 接收用户输入。
- 根据 session_id 读取会话状态。
- 调用检索模块,从长期记忆中召回相关内容。
- 构建带上下文信息的 prompt。
- 调用大模型接口获取回答。
- 把新的对话写入会话状态,并更新工作记忆摘要。
4.2 会话状态与滑动窗口实现
先定义会话对象,字段不多,但每个都有用。session_id 区分用户,messages 存对话原文,summary 存工作记忆摘要,task_id 标记任务域,max_rounds 控制滑动窗口大小。
from dataclasses import dataclass, field from typing import List @dataclass class Session: session_id: str task_id: str = "" summary: str = "" messages: List[dict] = field(default_factory=list) max_rounds: int = 8滑动窗口的逻辑很简单:超过 max_rounds 轮,就只保留最近的部分,把更早的交给摘要处理。这里要特别注意,messages 里存的是完整消息还是单轮消息会影响判断。我的实现里每条消息是一个 dict,包含 role 和 content,一来一回算两轮。
def trim_messages(session: Session) -> List[dict]: limit = session.max_rounds * 2 if len(session.messages) <= limit: return session.messages return session.messages[-limit:]这个实现看起来简单,但里面藏着经验。我一开始用 token 数量做阈值,后来发现 token 统计本身有开销,还得反复调,反而用“轮数”更直觉。不过轮数控制不能让超长单轮消息冲爆窗口,所以我还加了一道保险,在拼装 prompt 时统一检查总 token,超出就触发降级。
4.3 自动摘要压缩工作记忆
滑动窗口丢掉的旧对话不会直接消失,而是被“压缩”成工作记忆摘要。每过几轮,我会让模型把即将滑出窗口的旧消息总结成一段结构化描述,存进 session.summary。
摘要的 prompt 很关键,不能只说“请总结”,否则会丢关键信息。我用的模板长这样:
def summarize(old_messages: List[dict]) -> str: conversation = "\n".join( f"{m['role']}: {m['content']}" for m in old_messages ) prompt = ( "你是上下文摘要器。请把下面的对话压缩成中文摘要," "必须保留:用户目标、关键约束、已做出的决策、待办问题、重要数字。" "不要输出分析,不要输出建议,只输出摘要。\n\n" f"对话内容:\n{conversation}" ) result = call_llm(prompt, max_tokens=300) return result.strip()这里有几个细节容易踩坑。第一,摘要本身也要占用 token,不能无限累积,所以我固定给 300 token 的上限,超出会被截断。第二,摘要之后,旧消息我会直接丢掉,避免它和摘要内容重复,浪费预算。第三,工作记忆摘要每轮都要参与 prompt 拼装,因为它代表“这个任务走到哪了”。
我实测下来,摘要机制对长对话一致性的改善非常明显。它相当于给模型画了一张“进度地图”,模型知道你要去哪里、已经走了多远,自然不容易中途跑偏。
4.4 向量检索召回长期记忆
长期记忆这块,我先把每轮对话或者每段资料切成小块,做 embedding,存成向量。每次新的用户请求进来,也做一次 embedding,然后和库里的向量算相似度,取 Top-K。
为了演示清晰,我用了一个简单的余弦相似度实现:
import numpy as np memory_store = {} # session_id -> list of {"text": str, "vector": np.ndarray} def embed(text: str) -> np.ndarray: # 这里接入你用的 embedding 接口 return np.array(api_embed(text)) def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) def retrieve(session_id: str, query: str, top_k: int = 3, threshold: float = 0.7): q_vec = embed(query) scored = [] for item in memory_store.get(session_id, []): score = cosine_similarity(q_vec, item["vector"]) scored.append((score, item["text"])) scored.sort(key=lambda x: x[0], reverse=True) return [text for score, text in scored[:top_k] if score >= threshold]threshold 参数特别重要。我最早没设置阈值,结果不管相关不相关都塞给模型,反而引入噪声。后来加了 0.7 这个经验阈值,低于它宁可不召回,效果立刻好转。当然具体阈值要跟着 embedding 模型走,换一个模型就重新调一次。
切片大小也值得单独说。太长了定位不准,太短了上下文破碎。我一般按 300 到 500 字切一段,既保留语义完整性,也方便精准召回。
4.5 拼装 prompt 的完整规则
前面所有模块最终都要汇到 prompt 拼装这一步。我总结出一套固定格式,每次请求都按这个结构来:
def build_prompt(session: Session, user_input: str) -> str: recent = trim_messages(session) memory = retrieve(session.session_id, user_input, top_k=3) memory_text = "\n".join(memory) if memory else "无" recent_text = "\n".join( f"{m['role']}: {m['content']}" for m in recent ) prompt = f""" 【系统设定】 你是一个具备上下文感知能力的智能助手。请结合工作记忆、相关历史与最近对话回答用户问题。如果信息不足,直接说明不足,不要编造。 【工作记忆】 {session.summary or "暂无"} 【相关历史】 {memory_text} 【最近对话】 {recent_text or "暂无"} 【当前问题】 {user_input} """ return prompt这段代码里的分段顺序是我反复调过的。系统设定放最前,让模型理解自己的任务。工作记忆紧随其后,给它一张“任务全貌图”。相关历史居中,提供可参考的细节。最近对话放最后,因为它离用户当前问题最近,模型对这部分注意力最强。当前问题压轴,直接触发回答。
我踩过的一个坑是把相关历史放最前面,结果模型在长篇历史里绕不出来,反而忽略了用户最新问题。调整顺序之后,生成质量提升明显。这也印证了前面说的,不是有上下文就行,上下文的位置和优先级同样重要。
4.6 完整调用链演示
把上面几块串起来,完整的请求处理流程是这样的:
def handle_message(session_id: str, user_input: str) -> str: session = load_session(session_id) # 更新工作记忆:把用户输入里的关键信息提炼进摘要 session.summary = update_summary(session, user_input) # 构建带上下文的 prompt prompt = build_prompt(session, user_input) # 调用大模型接口 answer = call_llm(prompt, max_tokens=1024) # 记录新的对话消息 session.messages.append({"role": "user", "content": user_input}) session.messages.append({"role": "assistant", "content": answer}) # 超过窗口上限的旧对话做摘要后丢弃 if len(session.messages) > session.max_rounds * 2: old = session.messages[: -(session.max_rounds * 2)] session.summary = merge_summary(session.summary, summarize(old)) session.messages = trim_messages(session) save_session(session) return answerupdate_summary 和 merge_summary 这两个函数我简化了逻辑,本质上都是调用模型做提炼式改写。生产环境中还要考虑异步处理、失败重试、并发锁等问题,但核心骨架就是这样。我每次做新的 AI 应用,都会先按这个骨架搭一个可运行的 context-mode 原型,再根据业务调整细节。
5. 落地过程中踩过的坑与排查方案
5.1 token 爆炸:一次全量塞上下文的代价
我最早实现 context-mode 时图省事,直接把所有历史塞进 prompt。第一周没出问题,因为测试数据量小。等到真实用户上场,对话轮数一多,请求直接 400 报错,token 超限。
排查步骤也不难,先看错误信息是不是 context length exceeded,再去统计历史 token 总量。但根治还得靠预算控制。我现在每个请求都会算一个硬性 token 上限,超过就触发降级策略,优先保证最近对话和工作记忆,检索历史可以舍弃,也不能让请求失败。
5.2 上下文串味:旧任务干扰新任务
还有一次,用户先聊了产品方案,后来切到别的话题,AI 回答新问题时还带着旧话题的口吻。问题出在 session 里没有任务隔离,所有内容攒在一起,检索也全库搜。
后来我在 Session 里加了 task_id,写入记忆时打上标签,检索时按 task_id 过滤。同时在上下文切换时提供一个显式的“重置上下文”动作,把工作记忆清空,滑动窗口归零。这个方案上线后,串味问题基本消失了。
5.3 检索不到内容:阈值切片都可能是元凶
检索召回这块,最常见的问题是明明存了记忆,却永远召回不到。我踩过两个坑。第一个是切片太大,一段历史里有多个主题,query 只和其中一半相关,向量被不相关部分稀释,相似度就是上不去。第二个是阈值设得太高,导致召回率为零。
解决方法是切片缩小到 300 字左右,同时把阈值调低到 0.65 左右测试,先保证能召回,再慢慢调整平衡精度和召回率。还有一个技巧:存向量时把文本前面加上任务标签和日期,召回效果会更好,因为向量在编码时能捕捉到这些前缀信息。
5.4 隐私和成本:别把敏感信息裸奔
context-mode 做得越深,越要小心隐私。用户可能在对话里透露客户信息、内部数据,这些一旦落到向量库,删除和更新都非常麻烦。我在实现时加了几个约束:向量库里的记忆做脱敏处理,用户可以在设置里一键清空上下文,服务端对敏感字段做权限控制。
成本问题也不容忽视。embedding 调用和摘要调用都会额外产生 token 消耗。我统计过,开启完整 context-mode 后,单次请求的 token 成本比普通模式高 30% 到 50%。一个缓解方案是,摘要在对话轮数达到阈值时才触发,检索走缓存,embedding 结果存本地复用。钱要花在能提升体验的地方,别在看不见的角落白白烧掉。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 | 推荐处理 |
|---|---|---|---|
| 请求报 token 超限 | 历史全量塞入 | 查看 prompt 实际 token 数 | 加滑动窗口,设置硬性预算 |
| 回答前后矛盾 | 关键信息被挤出窗口 | 检查工作记忆摘要是否完整 | 用摘要保留决策和数字 |
| 上下文串味 | 缺少任务隔离 | 检查记忆是否有 task_id | 按任务域隔离存储与检索 |
| 相关历史总召不回 | 切片太大或阈值太高 | 打印召回相似度分数 | 缩小切片,调低阈值 |
| 成本明显上升 | 摘要和 embedding 过度调用 | 统计各环节 token 消耗 | 加缓存,延迟摘要触发 |
| 隐私隐患 | 敏感信息直接入库 | 审查存储内容 | 脱敏、权限控制、一键清空 |
6. 一些实在的优化经验
6.1 上下文不是越大越好
很多人一听到 128k 窗口就兴奋,觉得终于可以全量记忆了。但我实测下来,窗口越大,模型越容易注意力涣散,中间的信息真的会被“遗忘”。做完一轮测试后,我的结论是:上下文要给的恰好,而不是给得多。所谓恰好,就是只包含完成当前任务的最小必要信息。抓到 10 条相关历史,合并掉 7 条只留下 3 条精华,回答质量反而更高。
6.2 先做日志再做功能
这个建议我给所有做 context-mode 的人都提过:一定要先给每次请求打日志,记录“工作记忆是什么、召回了哪些片段、命中相似度多少、最终 prompt 长什么样”。如果没有这些日志,出问题只能盲猜。
有一次用户反馈回答质量下降,我靠日志发现是 embedding 接口切换后,指标空间变了,向量相似度整体漂移,召回结果全偏。没有日志,这种问题可能找一整天都定位不到。先有观测,再有优化,这是我在多次实战里最深刻的体会。
6.3 给用户一个“无痕切换”开关
最后是体验层面。context-mode 虽然好用,但不是所有用户都喜欢被记住。有些人只是临时问一个问题,不希望上个话题影响这次回答。我建议在界面上提供三个层级的控制:关闭上下文、单轮上下文、完整上下文。同时在用户明确切换任务时,提供一键清除上下文的入口。这个开关看着不起眼,却能让各种使用习惯的人都觉得工具“懂事”。
我在实现 context-mode 的过程中还有一个额外发现:真正让用户觉得“智能”的,不是模型每次回答得多么漂亮,而是它记得住你说过什么、不用你重复、还能在关键节点提醒你之前定的约束。工具再强大,如果它转身就忘,用起来依然像在跟一个刚入职的实习生打交道。把上下文这件“小事”做好,AI 应用的整体体验能上一个台阶。这就是我做完这个项目后最大的心得。