← Hub
DAY 02 / PHASE 1 · ENGINEERING

Context Engineering

Lost-in-the-Middle · 信息排布 · Chunk 设计 · Context Compaction

2026-05-22 · BigCat

「200K context」不等于「200K 有效注意力」。

前置概念 → ai-ml-daily Day 1: LLM 基础(Attention 机制、Tokenization、位置编码)

// WHY THIS MATTERS

所有大模型厂商都在卷 context window:Claude Sonnet 4.5 给了 1M、Gemini 2.5 给到 2M、GPT-5 也跟到 400K。于是大量人开始把整个 codebase / 全公司文档 / 三十轮历史对话一股脑塞进去,然后困惑:「为什么模型答不上来?我明明把答案放进去了。」答案是 context window 长 ≠ context 有效。注意力在长序列上是退化的、信息排布的位置决定模型能不能用、chunk 切的方式比换一个 embedding model 重要 10 倍。这一期讲四件事:长 context 真实有效长度的衡量、信息在 prompt 里的物理位置怎么放、为什么 90% 的 RAG 输给作者自己的 chunk 策略、以及当 history 超长时怎么做 compaction 而不丢关键信息。

// 01

Lost-in-the-Middle:长 context 的真实有效长度

论断:把关键信息放进 prompt 中段,就是把它埋进模型的视觉盲区。

背景与原理

Liu et al. 2023 的 Lost in the Middle: How Language Models Use Long Contexts(TACL 2024 收录)是这一切讨论的起点。他们让模型在一组文档里找答案,把 ground-truth 文档分别放在第 1、5、10、15、20 个位置,画出准确率曲线。结果在所有测试的模型(包括 GPT-3.5、Claude、Longchat)上都是 U 形:首尾位置准确率接近 75%,中段塌到 50% 以下——比关闭整个 retrieval 还差。

原因有两层。第一,预训练阶段,token 之间的依赖大多是局部的、或对齐到文档开头(causal mask 让 attention 天然偏向最近 token,RoPE 等位置编码的 long-tail 衰减也让远距离 attention score 变弱)。第二,post-training 的指令数据多数是「短 prompt → 答案」结构,模型对「指令在尾、关键事实在中段」的形态见过的样本少。

2024 年 NoLiMa(Modarressi et al.)进一步证明:当 question 和 answer 之间没有词面 overlap 时,所谓的 100K+ 长上下文模型在 32K 处就掉到基线以下。Chroma 2025 年的 Context Rot 报告复测了 18 个最新模型(含 Claude Sonnet 4、GPT-4.1、Gemini 2.5),结论一样:有效 context 远短于宣传的 max context,且和任务难度反向相关。

实战示例

跑一个最小 NIAH(Needle in a Haystack)测试,先量一下你常用模型的真实有效长度:

import anthropic, random, string

client = anthropic.Anthropic()
needle = "BigCat 的会议密码是 PURPLE-OWL-9182。"

def haystack(n_tokens, needle_pos):
    # 用 Paul Graham 文章拼到 n_tokens 长度
    filler = open("pg_essays.txt").read()
    chunks = filler.split("\n\n")
    random.shuffle(chunks)
    text = "\n\n".join(chunks)[:n_tokens * 4]  # 粗略 4 char/token
    pos = int(len(text) * needle_pos)
    return text[:pos] + "\n\n" + needle + "\n\n" + text[pos:]

for length in [8000, 32000, 100000, 180000]:
    for pos in [0.1, 0.5, 0.9]:
        ctx = haystack(length, pos)
        r = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=128,
            messages=[{"role":"user","content": ctx +
                "\n\nQ: BigCat 的会议密码是什么?只回答密码。"}])
        print(length, pos, r.content[0].text.strip())

跑完会得到一张 (length × position) 的命中表。你只能信任那张表里命中率 ≥ 95% 的格子——超过的部分,不管厂商宣传多少 K,都不该塞关键信息。

失败模式:很多团队用 NIAH 单针测出「100% 通过 1M context」,就放心地把多份合同、邮件、聊天记录混进 prompt。NIAH 只测「字面查找」,而真实任务要的是多跳推理 + 整合,难度高 1-2 个数量级。BABILong、RULER、LongBench v2 才是更接近真实的 benchmark。
进阶资源 · Liu et al. 2023, arxiv.org/abs/2307.03172 · Chroma 2025 Context Rot, research.trychroma.com/context-rot · Greg Kamradt 的原版 NIAH 仓库,github.com/gkamradt/LLMTest_NeedleInAHaystack
// 02

信息排布原则:U 形分布 + Anchor + Restate

论断:prompt 不是一个 bag of tokens,它是一条有梯度的注意力管道

背景与原理

既然首尾权重最高,prompt 内部的物理排布就该按重要性梯度铺。Anthropic 官方文档给的 long-context 建议(claude.com/docs · Long context tips)非常直白:把待引用的长文档放在 prompt 的最上方,让 question 出现在 user turn 的末尾。这条规则在 Claude 上反复被测过,能把 long doc Q&A 的准确率提 10-20%。

实操有三个动作:

┌──────────────── Prompt 物理排布 ─────────────────┐ │ ↑ HIGH attention │ │ ┌──────────────────────────────────────────┐ │ │ │ SYSTEM: role + guardrails + tools schema│ │ ← cache-friendly │ ├──────────────────────────────────────────┤ │ │ │ REFERENCE DOCS (large, stable per task)│ │ ← put long stuff here │ │ <doc id="1">...</doc> │ │ │ │ <doc id="2">...</doc> │ │ │ ├──────────────────────────────────────────┤ │ │ │ … mid-context attention sag … │ │ ← 不要把关键信息放这 │ ├──────────────────────────────────────────┤ │ │ │ CRITICAL CONSTRAINTS (restate) │ │ │ │ CURRENT QUESTION (specific, last) │ │ ← 模型最先看到的 │ └──────────────────────────────────────────┘ │ │ ↑ HIGH attention │ └────────────────────────────────────────────────┘

实战示例

<documents>
  <document index="1">
    <source>contracts/acme-2024.pdf</source>
    <content>...8K tokens...</content>
  </document>
  <document index="2">...</document>
</documents>

Instructions (重申):
- 只引用 documents 中明确写出的条款,不要外推
- 先列出你将引用的原文片段,再给结论

Question:
我方在 Acme 合同里能否在第 18 个月单方面终止?请逐条列出依据。
失败模式:在 chat history 很长的会话里把新指令塞进 system,又指望模型「重新读一遍 system」。模型不会主动「再读」——它只是在每个 token 上做一次 attention。指令需要在最近的 user turn 里复述才会被注意到。
进阶资源 · Anthropic Long context tips, docs.claude.com/.../long-context-tips · Lilian Weng 的 LLM Powered Autonomous Agents 关于 working memory 的章节,lilianweng.github.io/posts/2023-06-23-agent
// 03

RAG 真正的瓶颈:Chunk 设计远比 Embedding Model 重要

论断:90% 的 RAG 写错了,错的不是 embedding,是切文档的方式。

背景与原理

团队上线 RAG 的标准流程:固定 512-token chunk + 50 overlap + OpenAI embedding + 简单 top-k。然后效果不好就开始换 embedding model、换向量库。换完发现差不多。这是因为:

真正决定 RAG 上限的三件事:chunk 边界(语义切分)、chunk 上下文(contextual prefix)、检索融合(hybrid + rerank)。embedding model 在合理选择下只是 ±5% 的差异。

实战示例

Anthropic 推荐的 Contextual Retrieval 流程(可直接搬到自己的项目):

# 步骤 1:为每个 chunk 生成上下文(用 Claude Haiku 批量做,配合 prompt caching)
context_prompt = """
<document>{whole_doc}</document>
Here is the chunk we want to situate within the whole document:
<chunk>{chunk}</chunk>
Please give a short succinct context to situate this chunk within
the overall document for improving search retrieval. Answer only
with the context, nothing else.
"""

# 步骤 2:把 context 拼到 chunk 前再做 embedding + BM25 索引
indexed_chunk = f"{generated_context}\n\n{original_chunk}"

# 步骤 3:查询时做 hybrid (dense + BM25),取 top-150 → rerank 到 top-20
dense_hits = vector_store.search(query, k=150)
bm25_hits  = bm25_index.search(query, k=150)
fused = reciprocal_rank_fusion([dense_hits, bm25_hits])
top20 = cohere_rerank(query, fused[:150], top_n=20)

关键工程细节:第 1 步的 {whole_doc} 在 prompt 里复用同一个 system + document prefix,所有 chunk 共享 prompt cache,成本可以压到原 1/10。Anthropic 自己测下来一篇 100 页文档的 contextualization 成本大约 $1.02——比换 embedding model 便宜得多,效果好得多。

失败模式:把 contextual retrieval 当 silver bullet。它对「需要外部信息消歧」的 chunk 提升大;对「完全独立可读」的 chunk(如 API 文档每条 endpoint 独立)几乎无收益,反而增加成本。先做 baseline eval,再决定哪些 corpus 上用。
进阶资源 · Anthropic Introducing Contextual Retrieval 2024, anthropic.com/news/contextual-retrieval · LlamaIndex 的 chunking 实验,llamaindex.ai/blog · Jina AI Late Chunking 论文,arxiv.org/abs/2409.04701
// 04

Context Compaction:长对话的可控压缩

论断:长会话不是「窗口爆了」的问题,是「信号被噪声稀释」的问题。

背景与原理

Agent / 长聊会话跑到第 50 轮,accuracy 经常断崖下跌。原因不一定是 token 超了——很多时候离 max context 还远,但模型已经晕了。这就是 context rot:相关信号被无关历史稀释,加上 lost-in-the-middle,模型把注意力压到了无关 token 上。

处理 long history 有四种主流策略,工程选择对应不同 trade-off:

实战示例

给一个长跑的 coding agent 写一段最小可用的 compaction loop:

def maybe_compact(history, model="claude-sonnet-4-6", threshold=120_000):
    tok = count_tokens(history)
    if tok < threshold:
        return history
    # 保留最近 6 轮全文,之前的压成结构化摘要
    recent, old = history[-6:], history[:-6]
    summary = client.messages.create(model=model, max_tokens=2000,
        system="You compress agent histories. Output strict JSON.",
        messages=[{"role":"user","content": f"""
Compress the following turns into JSON with keys:
- goal: the user's overall objective
- decisions: list of decisions made (with rationale)
- artifacts: files/code created or modified (paths only)
- open_questions: anything explicitly deferred
- forbidden: constraints user said NOT to do
<turns>{serialize(old)}</turns>
"""}]).content[0].text
    return [{"role":"system","content":f"<prior_session>{summary}</prior_session>"}] + recent

关键经验:压缩的 schema 比压缩的算法重要。强制模型按固定 key 输出(goal / decisions / artifacts / open_questions / forbidden),可以让下一轮 prompt 直接引用 <prior_session>.forbidden,比让模型自由总结鲁棒得多。

失败模式:(1)压缩时把代码片段也总结掉,下次让 agent 改代码时它再要原文,反而多花 token。规则:code / file paths / exact identifiers 永远保留原文,自然语言才压。(2)每轮都 compact,把缓存彻底打掉,反向涨成本。设置 hysteresis:触发阈值 120K,恢复到 60K,期间不再触发。
进阶资源 · Anthropic Effective context engineering for AI agents 工程博客, anthropic.com/engineering/effective-context-engineering-for-ai-agents · MemGPT 论文, arxiv.org/abs/2310.08560 · Simon Willison Context engineering 词条整理, simonwillison.net/tags/context-engineering

// 综合实战 · 给个人 RAG 做一次端到端改造

把上面四点串成一个周末项目:拿你自己的 Notion / Obsidian 笔记当 corpus,做一个可日常用的 personal knowledge agent。

  1. :按 Markdown header 切(不要 fixed-size)。## 以上每一段为一个 chunk,附 frontmatter + breadcrumb(文件路径 + heading 链)。
  2. 上下文化:用 Haiku 4.5 给每个 chunk 生成 1-2 句 context prefix(cache 整个文档作为 prefix,单 chunk 增量 token 极少)。
  3. 检索:dense(voyage-3-large 或 BGE-M3)+ BM25 → RRF → Cohere rerank。
  4. 组装:把 top-K 文档放在 <documents> 段最前;指令 + question 放最末,且在末尾 restate 「只引用 documents 中明确出现的内容」。
  5. 长会话:超过 80K 触发摘要压缩,保留最近 6 轮 + structured summary。代码块永不压缩。
  6. Eval:自己写 30 道带 ground truth 的问题(自己笔记自己懂,不用对外标注),跑命中率 / 引用准确率。下一期讲怎么把这个 eval 自动化。

这一套跑下来,你会发现 chunking + contextualization + rerank 这三步带来的提升,比把 embedding model 换 5 次都大。

// ENGLISH GLOSSARY

Context Window
模型一次推理能处理的最大 token 数(输入 + 输出)。1M context ≠ 1M 有效注意力。
Lost-in-the-Middle
关键信息位于 prompt 中段时,模型准确率呈 U 形塌陷。Liu et al. 2023。
NIAH (Needle in a Haystack)
把一个事实「针」埋进长 context「干草堆」测召回率的基准。Greg Kamradt 开源版本最常用。
Context Rot
有效注意力随 context 增长退化的现象。Chroma 2025 研究术语。
Chunking
把长文档切成可索引片段的过程。固定大小切是 baseline,语义切 / late chunking 是进阶。
Contextual Retrieval
给每个 chunk 加一段「在原文档里的上下文」前缀,再索引。Anthropic 2024 提出。
Hybrid Search
BM25(词面)+ dense embedding(语义)召回结合,常用 RRF (Reciprocal Rank Fusion) 融合。
Reranker
对召回结果用交叉编码器重排,比单独检索精度高很多(Cohere / Jina / BGE-reranker)。
Context Compaction
长对话超过阈值时把旧 turns 压成结构化摘要的过程。Claude Code 的 auto-compact 就是。
Hierarchical Memory
working / episodic / semantic 三层记忆架构。MemGPT 是代表实现。

// 深入思考

Lost-in-the-Middle 是 transformer 架构的固有缺陷,还是训练数据偏见?未来模型会消失这问题吗?
两者都有。架构层:positional encoding 在长 context 下精度下降,attention 在中段被稀释。训练层:大多数 instruction tuning 数据是短文本,模型没学到「中段也要看」。新模型(GPT-4o long、Claude 3.5 200k)通过 needle-in-haystack 训练有改善,但 Liu et al. 2024 显示真实任务上仍有 20%+ gap。短期不会消失。
RAG 切 chunk 时,按固定字符切 vs 按 semantic boundary 切,差距多大?为什么很多 framework 默认前者?
Semantic boundary 通常 recall 高 10-30%——embedding 是按语义单元训的,半截句子 embedding 是噪声。Framework 默认固定字符是因为简单 + 不依赖文档解析。Production 应该 hybrid:按 paragraph/heading 切,再用 max_tokens 兜底。
U 形排布把关键信息放头尾。如果只有一个关键信息,放头还是放尾?
放尾。Recency bias 更强(attention 对最近 tokens 权重最高),且大多数任务里「最后再说一遍」比「最前说一遍」收益高。但有例外:role/persona 必须放头(影响整段输出风格),任务指令放尾。这就是为什么 system prompt 模板里 role 在最上面,user query 在最下面。
Context compaction 用 LLM 做 summarization 是常见做法,但会引入幻觉。怎么设计才能既压缩又不失真?
三个机制:1) 结构化压缩(让 LLM 输出 JSON 而非自然语言,限制 schema);2) 双 pass 验证(compaction 后用另一个 prompt 验证「原文是否真有 X」);3) 保留 reference(compacted 内容引用原始 chunk id,需要细节时回查)。Claude Code 的 conversation compaction 用前两种。
Chunk 大小该 200 token 还是 800 token?取决于什么变量?
三个变量:1) 问题粒度(细节问题需小 chunk,宏观需大);2) embedding model 训练长度(OpenAI text-embedding-3 训在 ~256 token,超过会被 mean-pool 失真);3) retrieval k(k=1 时大 chunk 保信息,k=10 时小 chunk 保多样)。Production 常见 200-500 + overlap 50。

// 延伸阅读