Lost in the middle 后续与 RAG 记忆系统设计
Lost in the Middle
Lost in the Middle 发布之后已经过了很久。
我前阵子写了这个:
Lost in the middle - how it affect LLM agent system
里面还有不少说法经不起推敲,因为来源不太可靠。
但可以定下来的是:
Lost in the Middle 前置概念
我会分成结论+证据的形式来写。
- 大模型在接受输入时会把当前用户输入加到整个历史上下文中然后进行推理,而不是仅仅对当前输入和提问进行推理。所以上下文越长,token 消耗费用越高,两者线性相关。
我说的这个情况是针对于 Stateless LLM, Gemini-2.5, OpenAI-5.2 等等常在 API 中使用的模型都是 Stateless LLM。它的特征是每次文本生成请求是独立且无状态。所以如果要实现连续对话都要手动创建一个列表把之前的历史对话加进去。(当然并不代表每次都要从 0 计算,具体参见下方)
While each text generation request is independent and stateless, you can still implement multi-turn conversations by providing additional messages as parameters to your text generation request. OpenAI Conversation State
当然,这里也许只能说明 API 调用时每次输入都需要完整输入,不能完全说明模型每次推理时一定要所有的输入。(因为这看上去是复用率很低的一个做法。)
但事实是,这正是 LLM 的底层计算逻辑,或者说由于底层的 Transformer,所有的 token ~ 会被作为 ~ 的计算条件。
The prompt phase takes the whole user prompt (𝑥1, . . . , 𝑥𝑛) as input and computes the probability of the first new token 𝑃 (𝑥𝑛+1 | 𝑥1, . . . , 𝑥𝑛).
Efficient Memory Management for Large Language Model Serving with PagedAttention
但实际上虽然模型每次输入都是整个上下文,但并不是每次都从 0 计算。 KV-cache 是很好的例子,它减少了重复计算带来的计算量。
At iteration 𝑡, the model takes one token 𝑥𝑛+𝑡 as input and computes the probability 𝑃 (𝑥𝑛+𝑡+1 | 𝑥1, . . . , 𝑥𝑛+𝑡) with the key vectors 𝑘1, . . . , 𝑘𝑛+𝑡 and value vectors𝑣1, . . . , 𝑣𝑛+𝑡 . Note that the key and value vectors at positions 1 to 𝑛 + 𝑡 − 1 are cached at previous iterations, only the new key and value vector 𝑘𝑛+𝑡 and 𝑣𝑛+𝑡 are computed at this iteration.
Efficient Memory Management for Large Language Model Serving with PagedAttention
- 计量上下文窗口上限的单位是 token,判断是否超出上限是看单次请求中模型能共同理解的输入+输出 token 总量上限,所以简单来说可以细分为输入超上限,和输出超上限,但两者表现出来的结果没什么。
上下文窗口(context window)是单次请求中模型能处理的 token 总量上限。
对 OpenAI 的一些模型/接口表述来说,这个上限明确包含:input tokens + output tokens + reasoning tokens。
The context window is the maximum number of tokens that can be used in a single request.
This max tokens number includes input, output, and reasoning tokens.
Conversation state
另外早期对于单次提示词输入的上限一般也有一些暴力约束比如单次输入 < 10000字。但大模型是并没有推理时一般并没有这方面的直接约束。
超过上下文窗口时,要么请求失败(API/模型直接报错),要么系统只保留/压缩/忽略一部分上下文,导致回复不再覆盖全部内容。
这里是 OpenAI API 在碰到超出上下文时给用户的truncation策略,可以自行选择。
auto: If the input to this Response exceeds the model's context window size, the model will truncate the response to fit the context window by dropping items from the beginning of the conversation.disabled(default): If the input size will exceed the context window size for a model, the request will fail with a 400 error.
OpenAI Responses
- 越接近上下文上限,模型的 U 形曲线特征越明显, Lost in the middle 越严重。
以及关于 Lost in the Middle 概念的一些详细内容我也曾写过。
Lost in the middle - how it affect LLM agent system
Lost in the Middle 是否依然存在于近期的语言模型
之后是目前真正关心的,是 Lost in the Middle 的现象,在最近的 LLM 中是否被解决。
当时我的推断是并未被解决,因为 LLM 的底层并没有变化。但目前需要来找些证据。
关于 Lost in the Middle (以下简称 LITM)的现象,我找到了这些相关内容:
- LongBench v2: 长语境、多任务下模型的推理理解能力评分 : Gemini-2.5 Flash, GLM-4.5 等等最近一年的无思维链推理的模型在 zero-shot without COT (零样本,无外接数据库或者搜索引擎;无思维链)的方式中上,由 Short -> Long 跳跃时,准确率掉了很多。
说明长文本对于目前模型的性能表现依然影响很大,虽然不能断言这个时由于 LITM 的现象,因为这里体现的是长度效应。而 LITM 则是位置效应。
另外这里比较好奇的一点是,具有思维链(CoT)的模型在文本变长时,有时性能不降反增,这很有意思。
经过查阅,得知 CoT 模型之所以在任务准确率上有提高是因为是会减少“猜测式”输出与格式错误。并不是根本上解决了长度效应(提高理解能力)。这就好像一场考试里, without CoT 的学生只有 120 分钟,且所有问题只做了一遍就交卷,以及它还有自己的草稿纸(CoT),可以分步拆解问题,而 CoT 的时间上更充裕,可以反复地检查自己是否有一些粗心犯错。
- # \inftyBench: Extending Long Context Evaluation Beyond 100K Tokens: 很多模型/系统“能吃进去”100K/200K tokens(标称窗口),但在真实任务里“用得起来”的信息量(有效上下文)明显更小;当长度继续拉长,性能会退化。
他们提出了一个有效窗口的概念,有的大模型即使声称可以接受 100/200k tokens 或者 1M tokens,但是实际的消融实验中,当上下文长度越长,性能表现越糟糕。
另外在 5.2 部分我看到他们提到了 Lost in the Middle。

image-20260102161101092
这里是他们做的位置效应的图:

image-20260102161154099
这里他们发现,对于不同产商的大模型,他们并没有在位置效应上呈现一致性。有的偏向首因,有的偏向中间。
这点看起来似乎否定了目前 Lost in the Middle 现象的存在。
但是我个人抱持的观点是,Lost in the Middle 中提到的 Transformer 预训练时存在的问题还未解决,只是在不同的应对方式下变成了不同的表现方式。
并且我在之后找到了可以支撑我观点的有效实验,即这个位置效应导致的注意力结构性偏差依然存在。
“strong attention scores towards initial tokens as a ‘sink’ even if they are not semantically important.” arXiv
翻译: 模型会对最初的 token给出很强的注意力分数,把它们当成“汇点(sink)”,即使这些 token 在语义上并不重要。
“attention sinks exist universally in LMs … even in small models.” arXiv
翻译: 注意力汇点在各种语言模型中普遍存在,甚至在小模型里也存在。
“attention sink is observed to emerge during the LM pre-training.” arXiv
翻译: 注意力汇点被观察到会在语言模型预训练过程中出现。
这里对模型的注意力结构性偏差进行了一些追踪和研究。并且发现了它在各种语言模型中普遍存在,且出现在预训练的过程中。
这可以说是 LITM 的一次溯源和解释,至少 Primacy Effect 是找到了。
“even when … trained … struggle to capture relevant information … in the middle.” arXiv
即使模型被专门训练来处理长输入上下文,仍然难以捕捉位于输入中间的相关信息。
“mitigate this positional bias through a calibration mechanism, found-in-the-middle.” arXiv
他们通过一种校准机制(found-in-the-middle)来缓解这种位置偏置。
这篇论文发现 LITM 始终存在,并且研究了一些缓解策略,当然这缓解策略肯定并不能治本,毕竟问题出现在更早的阶段。当然这也可能是为什么各大产商的位置效应表现得各不相同,缓解的方式不同。
如果有完全解决预训练阶段的注意力位置偏差的方法,那应当是一篇集体引用的颠覆性的论文,但目前是还未出现的。
记忆系统 RAG 调研
上面的所有工作总结起来就是: LITM 依然存在,且 LITM 目前在不同产商大模型中以不同的方式存在,不再只是呈现出 U 形。
那么,我们的记忆系统应该怎么做?(你可能会觉得有点突兀,但是我扯那么多确实是为了我的记忆系统。)
相比于 LITM 那边学术氛围会更重, RAG 这边更偏工程。所以对于论文的引用不会像之前那么多。因为比起论文的“我还有什么”,工程这边更在在意“我能用我现在的东西做什么”。
这里是我的一些灵感来源:
关于酒馆SillyTavern所代表的伴侣模型系统的一些小思考
这里是已有的发表的一些研究:
贪多嚼不烂,我们就先以 MemGPT 为重心,它提出来的相当多设计在我的系统中也依然使用着。
MemGPT(这块内容主要由 Chatgpt 5.2 Thinking 配合撰写)
它不是只提“向量检索 + 拼 prompt”,而是把 LLM 的上下文窗口当作 RAM,把外部存储当作 Disk,用“分页/中断/函数调用”做成一个真正意义上的 可运行的、可控的分层记忆系统。
这边就不引用原文了,因为不太需要区分证据什么的。
1.MemGPT 要解决的核心问题是什么
LLM 固定上下文窗口导致两类典型失败场景:
- 长期对话:跨会话一致性差、忘记用户偏好/事实、无法形成长期关系
- 超长文档分析:文档远超 context window,关键信息“进不来/留不住/用不上” MemGPT 的核心主张是:
不追求把所有历史都塞进 prompt,而是让模型“像操作系统一样”在有限窗口里 动态调度信息
另外,这里可以回忆下前面的内容:
- 超过上下文窗口时,要么请求失败(API/模型直接报错),要么系统只保留/压缩/忽略一部分上下文,导致回复不再覆盖全部内容。
- # \inftyBench: Extending Long Context Evaluation Beyond 100K Tokens: 很多模型/系统“能吃进去”100K/200K tokens(标称窗口),但在真实任务里“用得起来”的信息量(有效上下文)明显更小;当长度继续拉长,性能会退化。
所以 MemGPT 的核心问题现在也依然是核心,它仍然很具参考价值。
2. 架构总览:把 LLM 当“CPU”,把上下文当“主存”
MemGPT 由三块组成(你写作时可以配一张“OS类比图”):
- LLM Processor(处理器):真正生成 token 的模型
- Main Context(主上下文 / RAM):模型每次推理“看得到”的那点窗口
- External Context(外部上下文 / Disk):所有窗口外的长期存储(文本库/向量库/数据库)
LLM 的输出会被解析:要么直接 yield 回复用户;要么生成一个函数调用(读写记忆、检索、分页加载等),由系统执行后再把结果塞回主上下文继续推理。Readwise
这点非常关键:它把“记忆系统”从一个外挂检索器,升级成 LLM 自己能控制的内存管理循环(feedback loop)。Readwise
这篇论文发表在在 MCP 出来前,代码是很难写的,而且模型支持 Function Calling 的也不多,且格式没有规范化,不支持同意调用,代码复用率很低,现在会好写很多,且可以做到多模型统一格式调用。
3. Main Context 的精细切分:把“对话窗口”做成可治理的内存区
MemGPT 不把 main context 当成一锅粥,而是拆成 3 段(非常像 OS 的分区):
- System instructions(只读、常驻):告诉模型有哪些函数、如何管理内存、控制流规则等(相当于内核/系统调用说明)
- Conversational context(只读、FIFO 队列):近期对话事件;到上限会被截断或递归摘要压缩(类似页缓存/滑动窗口)
- Working context(可写的工作区):给 agent 当“草稿纸/工作记忆”,模型可通过函数直接 append/replace 修改它(类似 heap/working set)Readwise
这里其实还涉及一些上下文长度控制的问题,比如对于函数式调用的上下文,或是让模型考虑写入记忆的上下文,只要处理完成后就丢弃。另外这边的 Working Context 其实和后来的 CoT (Chain of Thinking) 思维链有异曲同工之妙。
可以说 MemGPT 的设计超前相当多。
4.External Context 的两类长期记忆:Recall vs Archival
MemGPT 把外部记忆分成两种,概念非常清晰:
- Recall storage(回忆存储):保存“完整事件历史”(本质上是未压缩的全量对话/事件日志),用来回看过去发生了什么
- Archival storage(档案存储):更通用的长期读写库,用来存“事实、偏好、经验”等可被检索的条目(相当于长期知识/档案)Readwise
同时它提供多种检索方式:按时间戳、按文本、按 embedding/向量相似度检索。Readwise
这其实就很接近 OpenAI 那篇里的情景记忆和工作记忆。
5.写入与读取:MemGPT 怎么“自己决定”存什么、取什么
5.1 自主写入(Self-directed editing)
MemGPT 会在 system preprompt 里明确教模型:什么时候该写、写到哪里、用什么函数写。它强调“记忆编辑和检索是由模型自主决定的”。Readwise
一个很典型的机制是 memory pressure 警告:当对话快到上限,系统发出提示,让模型把重要信息写入 working/archival 以免被裁剪掉。Readwise
5.2 受 token 约束的分页检索(Pagination)
它特别强调检索要“认知 token 限制”,并用分页避免一次检索结果把窗口撑爆。Readwise
这里实际上就是我理解中 MemGPT 最为超前的地方,因为模型可以自主写入,所以可以实现一些驱动式的自主学习。且学的是一个外接的数据库,这个数据库可以迁移到任何一个新的对话,新的模型当中。
要知道 Zero-shot 和 None-Zero-Shot 的大模型差距可谓天壤之别,所以在用支持深度研究的模型时会觉得和 Instant 差距巨大。但这份巨大差距主要体现在对客观事物的认识上,因为它可以随时调用搜索引擎来拓展所需。
但总有一些所需是拓展不到的,比如,用户的生日,偏好,用户的意图,用户的画像。这些主观的事物或者属性大模型是无法接触到的。以及,用户希望让大模型回答关于某部电影时不再是只会按照“权威式”来源,而是模仿自己喜欢的影评风格来写,抑或是写小说等等。
这实际上本质建构的是一个自动化的提示词工程,根据用户画像和意图来定制提示词。可以让大模型更加如臂指使,让用户更充分地调用大模型的能力来做自己真正要做的事情,而不再只是一个通用型助手。毕竟通用通用,也是统统不用。
总结
参考 5.2 中我最后提到的内容,那是我希望 RAG 记忆系统实际上能做的事情,而不是目前所用作的一次检索。
- 我希望记忆系统不是一次检索,而是一套可迁移的外部状态:把对话蒸馏成长期画像/偏好/风格资产,并在每次推理中被可控地调度回上下文。
- 这个系统至少要兑现三件事:一致性(跨会话稳定)、可控性(可审计/可回滚)、预算稳定(token 不失控)。
- 写入如何防污染、存储如何结构化、注入如何对抗位置效应、冲突如何处理以及何时需要用户确认。
当然这存在许多问题,每一个都不小,这都是需要一步步解决的,所以我说这仅仅只是希望,能够接近,已然很好。
- 如何避免错误的东西被写入数据库?仅仅依靠模型不够,不同模型能力不同,容易污染数据库,风险控制怎么做?
- 如何对写入的东西进行结构性的组织存储。大模型又怎么自己写自己取?
- 怎么重构提示词,并且放回上下文?要不要考虑位置效应?
- 如果重构的记忆提示词中存在冲突而造成幻觉怎么办?要不要把这个过程搞成透明化让用户可见?
暂时在这里搁笔,之后再补充一下 Mem0。