Skip to content

治好大模型的"中间迷失":Agent 开发者的上下文工程实战

更新: 8/23/2026 字数: 0 字 时长: 0 分钟

开篇:你的 Agent 为什么"聊着聊着就忘了"

做过 Agent 应用的前端同学,大概都踩过这几个坑:

  • 多轮对话进行到第十几轮,用户在第 3 轮定的需求("用 TypeScript,不要用 any"),模型到后面全忘了;
  • 把一份 5 万字的 API 文档塞进上下文让它答题,它偏偏漏掉夹在中间那段最关键的接口说明;
  • RAG 应用里,明明检索到了正确的段落,但因为拼在了 prompt 中间,模型愣是没"看见"。

这些现象背后是同一个根因——"中间迷失"(Lost in the Middle)。研究发现,当关键信息位于长上下文的开头或结尾时,模型能很好地利用它;可一旦信息落在中间位置,模型的检索与利用能力会显著下降,呈现一条"两头高、中间塌"的 U 形曲线[1]

对我们做工程的人来说,重点不是去改模型的注意力机制(那是模型厂商的事),而是:在应用层,怎么通过上下文工程(context engineering)把关键信息"喂"到模型看得见的地方。 这篇文章就系统梳理这套实战方法论。

一、先搞懂:"中间迷失"到底是怎么发生的

中间迷失:记得头尾,忘了中间

用一个前端能秒懂的类比:模型的注意力,像是 CSS 里没设 object-fit 的图片——容器(上下文窗口)拉得再大,焦点始终糊在两端,中间被"拉伸"得模糊。

更技术一点说,Transformer 的注意力机制理论上对每个 token 都"看得到",但实际训练出来的模型存在两种位置偏好:

  • 首因效应(primacy):开头的内容(通常是 system prompt、任务指令)权重高;
  • 近因效应(recency):结尾的内容(最新的用户输入)权重高。

夹在中间的大段内容,就成了注意力的"低洼地带"。注意:上下文窗口能装 100 万 token,不代表这 100 万 token 被同等有效利用。 "容量"和"有效利用率"是两码事——这是所有优化手段的出发点。

既然模型的"生理结构"决定了它偏爱头尾,那么工程上的所有招数,归根结底就一个思路:要么让塞进去的东西更少更准,要么把重要的东西挪到头尾。 下面分五类手段展开。

二、五大技术手段详解

手段一:检索增强生成(RAG)——只喂最相关的,别塞全部

RAG:只抓最相关的,不塞全部

核心思想:与其把整份文档硬塞进上下文赌模型能找到答案,不如先用向量检索把最相关的几个片段捞出来,只把这几段喂给模型。上下文越短越聚焦,中间迷失的空间就越小。 这是对抗中间迷失最根本、最有效的手段。

原理流程:

JS/TS 实现思路(以 LangChain.js 为例):

typescript
import { MemoryVectorStore } from 'langchain/vectorstores/memory';
import { OpenAIEmbeddings } from '@langchain/openai';
import { RecursiveCharacterTextSplitter } from 'langchain/text_splitter';

// 1. 分块:块太大会稀释相关度,太小会切断语义,通常 500~1000 token + 重叠
const splitter = new RecursiveCharacterTextSplitter({
  chunkSize: 800,
  chunkOverlap: 150, // 重叠避免关键句被切在边界
});
const docs = await splitter.createDocuments([longText]);

// 2. 向量化入库
const store = await MemoryVectorStore.fromDocuments(docs, new OpenAIEmbeddings());

// 3. 检索 Top-K,只取最相关的几块
const relevant = await store.similaritySearch(userQuestion, 4);

// 4. 只把这 4 块 + 问题拼进 prompt(而非整份文档)
const context = relevant.map((d) => d.pageContent).join('\n---\n');
const prompt = `基于以下资料回答问题:\n${context}\n\n问题:${userQuestion}`;

关键工程点:分块策略(chunk size / overlap)直接决定效果。块太大,单块内部又会出现中间迷失;块太小,语义被切碎。对结构化文档,优先按标题/段落等语义边界切,而不是按固定字符数硬切。

手段二:重排序(Re-ranking)——把重点挪到模型眼皮底下

重排序:把重点放在头尾

这是直接针对 U 形曲线的"物理外挂"。既然模型偏爱头尾,那检索回来的片段就别随便排——把最相关的放头和尾,把次要的塞中间。

RAG 的 Top-K 检索结果默认按相似度降序排(最相关的在最前)。但如果你把 K 个片段顺序拼接,那第 K/2 个片段就正好落在"迷失区"。解法有两种:

做法 A:相关性重排 + "两端优先"重新布局

typescript
// relevant 已按相似度降序(index 0 最相关)
function reorderForLLM<T>(chunks: T[]): T[] {
  const head: T[] = [];
  const tail: T[] = [];
  // 交替往两端塞:最相关的进头部,次相关的进尾部,越不相关越往中间
  chunks.forEach((c, i) => (i % 2 === 0 ? head.push(c) : tail.unshift(c)));
  return [...head, ...tail];
}

const reordered = reorderForLLM(relevant); // 最重要的两块分别在开头和结尾

这个"两端夹心"布局,正是 LangChain 里 LongContextReorder 的核心思路——专门为缓解中间迷失而设计。

做法 B:引入专门的 Reranker 模型

粗排(向量检索)召回 20 条,再用一个交叉编码器(Cross-Encoder,如 Cohere Rerank、bge-reranker)精排取 Top 4。粗排负责"快而全",精排负责"准",两级配合既省 token 又提相关度。

手段三:上下文压缩与记忆机制——把长历史"熬"成精华

压缩记忆:把长历史炼成精华

多轮对话场景里,历史消息会无限膨胀,越堆越长,老信息全埋进中间。解法是记忆管理——别把原始对话全塞进去,而是分层处理:

分层记忆架构(前端开发者可类比 缓存分级):

typescript
interface MemoryLayer {
  systemPrompt: string;        // 常驻头部:角色/约束,永远在最前
  summary: string;             // 中期记忆:早期对话的滚动摘要
  recentMessages: Message[];   // 短期记忆:最近 N 轮原文,放尾部
  facts: Record<string, any>;  // 结构化记忆:用户偏好等关键事实,单独提取
}

function buildContext(mem: MemoryLayer): string {
  return [
    mem.systemPrompt,                                   // 头部:高权重
    `【关键事实】${JSON.stringify(mem.facts)}`,          // 结构化,不怕丢
    `【历史摘要】${mem.summary}`,                        // 压缩后的中期记忆
    ...mem.recentMessages.map((m) => `${m.role}: ${m.content}`), // 尾部:高权重
  ].join('\n\n');
}

滚动摘要(rolling summary):当对话轮数超过阈值,就把较早的消息交给 LLM 压缩成一段摘要,替换掉原文。这样上下文长度稳定在可控范围,老信息以"精华"形式保留,而不是原始啰嗦地占着中间位置。

结构化记忆提取:像用户偏好("不要用 any")这种关键约束,别指望它埋在对话里模型能记住——主动用一次 LLM 调用把它抽成 facts 键值对,每轮都拼在头部。结构化 > 自然语言,不容易丢。

手段四:分块处理与 Map-Reduce——化整为零

当任务是"总结/分析一整份超长文档"、RAG 又不适用(需要通读全文)时,用分而治之:

typescript
// Map 阶段:每块独立处理(可并发),每块都短,无中间迷失
const mapResults = await Promise.all(
  chunks.map((chunk) =>
    llm.invoke(`提取这段内容的关键点:\n${chunk}`)
  )
);

// Reduce 阶段:汇总各块的"关键点"(已大幅缩短),再综合
const finalSummary = await llm.invoke(
  `基于以下各部分要点,写一份整体总结:\n${mapResults.join('\n')}`
);

每块都足够短,单块内部不会迷失;Reduce 阶段处理的是压缩后的要点,也很短。代价是多次 LLM 调用,需要在成本/延迟和质量间权衡,可用 Promise.all 并发 Map 阶段降低延迟。

手段五:提示工程层面的"位置优化"

不引入任何额外系统,纯靠 prompt 结构也能缓解:

  • 指令放两端:把最重要的任务指令同时放在开头和结尾(结尾复述一遍),命中双高权重区;
  • 关键信息前置/后置:回答依据的核心资料,别埋中间;
  • 显式引用编号:给每个片段编号,让模型"引用第 [3] 段回答",强制它扫描全部片段;
  • 结构化标记:用 Markdown 标题、XML 标签(如 <key_info>)包裹重点,给模型视觉锚点。
typescript
const prompt = `
# 任务(重要)
${task}

<key_constraints>
${constraints}  // 关键约束,用标签突出
</key_constraints>

# 参考资料
${numberedChunks}  // 每段带 [1][2][3] 编号

# 再次强调任务
${task}  // 结尾复述,命中近因效应
`;

三、场景应用:如何组合这些手段

场景 1:企业知识库问答机器人

痛点:文档几百篇,不可能全塞进上下文。 方案:RAG(手段一)为主 + Reranker(手段二)精排。 要点:向量粗排召回 20 条 → Cross-Encoder 精排取 4 条 → LongContextReorder 两端布局。

typescript
const candidates = await store.similaritySearch(q, 20); // 粗排召回
const ranked = await reranker.rerank(q, candidates, { topN: 4 }); // 精排
const context = reorderForLLM(ranked); // 两端夹心布局

场景 2:长程 Coding Agent(多轮任务)

痛点:任务跨几十轮,早期定的技术约束被遗忘。 方案:分层记忆(手段三)。System prompt + 结构化 facts(技术栈约束)常驻头部,滚动摘要压缩中期历史,最近 5 轮原文放尾部。 要点:约束一定要抽成结构化 facts,每轮重新注入,别靠模型"记忆"。

场景 3:超长文档总结(如财报、论文)

痛点:需要通读全文,RAG 会漏信息。 方案:Map-Reduce 分块(手段四)。 要点:按语义边界(章节)切块而非固定长度;Map 阶段 Promise.all 并发。

场景 4:实时长对话客服

痛点:对话持续几小时,又要低延迟。 方案:滑动窗口 + 摘要(手段三)+ 关键事实结构化提取。 要点:窗口只保留最近 N 轮,超出部分异步摘要;用户身份/订单号等抽成 facts 常驻。

场景 5:RAG 命中了却答错

痛点:检索对了,但片段拼在中间被忽略。 方案:纯用重排序(手段二)+ 编号引用(手段五)。这是最典型的"中间迷失"现场,重排布局往往立竿见影。

四、拓展知识

模型层面的进展:除了应用层手段,模型侧也在改进——更好的位置编码(如 RoPE 的各种外推方案)、专门针对长文本检索能力的训练,都在拉平那条 U 形曲线。选型时,可以关注模型在长上下文检索基准(如 "Needle in a Haystack" 大海捞针测试)上的表现,而不只看标称的上下文窗口大小。

前端环境的性能考量:

  • Token 成本:上下文越长,费用和首字延迟(TTFT)越高。RAG/压缩不仅治迷失,也直接省钱。
  • 流式输出:长上下文首字延迟高,务必用 SSE / ReadableStream 做流式渲染,改善体感。
  • 客户端缓存:Embedding 结果、摘要可缓存(IndexedDB / 服务端 Redis),避免重复计算。
  • 边缘轻量向量库:小规模场景可用纯 JS 的向量库(如 voyhnswlib-node)甚至浏览器端向量检索,省去后端往返。

五、诊断决策指南与工具推荐

先诊断,再下药:

决策速查表:

场景特征首选手段
海量文档,只需局部答案RAG + Reranker
检索对了但答错重排序(两端布局)
多轮对话,历史膨胀分层记忆 + 滚动摘要
单份超长文档需通读Map-Reduce 分块
关键约束总被遗忘结构化记忆 + 头部注入
不想动架构,快速见效提示工程位置优化

实用工具:

  • 编排框架:LangChain.js / LlamaIndex.TS —— 内置 LongContextReorder、记忆模块、Map-Reduce chain,开箱即用。
  • 向量库:Pinecone、Weaviate(托管);Chroma、hnswlib-nodevoy(轻量/本地)。
  • Reranker:Cohere Rerank API、bge-reranker(可自部署)。

一句话总结:"中间迷失"是模型注意力"两头高、中间塌"的固有特性,应用层治不了它的"病根",但能绕开它——核心策略无非两条:用 RAG 和压缩让上下文"更短更准",用重排序和提示工程把重点"挪到头尾"。 先诊断问题出在检索、位置还是长度,再对症组合上面的手段,就能显著改善你的 Agent 在长上下文下的表现。

References

  1. Lost in the Middle: How Language Models Use Long Contexts