RAG 系列(二十二):长上下文 vs RAG——要不要 RAG

0 阅读8分钟

一个看似合理的问题

Gemini 1.5 Pro 支持 100 万 token 上下文,Claude 3.5 支持 20 万 token,GPT-4 Turbo 12.8 万 token。一部小说大约 15 万字,约 20 万 token,直接塞进去就能问。有人问:RAG 还有必要吗?

这个问题值得认真回答,因为它背后藏着一个真实的决策:给一个生产系统,我应该用 RAG 还是长上下文?


先把数字摆出来

大语言模型的上下文窗口(2024–2025):

模型上下文窗口约合文本量
Gemini 1.5 Pro1,000,000 tokens~750,000 词,约 1500 页
Claude 3.5 Sonnet200,000 tokens~150,000 词,约 300 页
GPT-4 Turbo128,000 tokens~96,000 词,约 190 页
GPT-4o128,000 tokens~96,000 词,约 190 页

看起来很多。但一个企业知识库有多少内容?

  • 中等规模公司的内部文档:数千篇,数百万字
  • 大型代码库:数万个文件,十亿 token+
  • 新闻/研究数据库:数百万篇文章

所有这些都超出了任何模型的上下文窗口。这是长上下文能力的物理上限。


长上下文的实际代价

"窗口大"不等于"免费"。每次请求都要处理所有 token,代价是真实的。

代价一:钱

按 2024 年末的价格粗估(输入 token):

模型每百万 token 价格100 万 token 一次请求
Gemini 1.5 Pro$1.25$1.25
Claude 3.5 Sonnet$3.00$3.00
GPT-4 Turbo$10.00$10.00

对比 RAG 的成本:

  • 检索阶段:只调用 Embedding API(< $0.001)
  • 生成阶段:只发送 2,000–5,000 token 的检索结果 + 问题(< $0.05)

同样的问题,RAG 的成本可以比长上下文低 20–200 倍。

如果一天有 1,000 个用户查询企业知识库:

  • 长上下文(1M token):约 $1,250/天
  • RAG(3K token 上下文):约 $3–15/天

代价二:延迟

处理更多 token = 更慢的响应。首 token 延迟(TTFT)随输入长度线性增长:

100K token 输入 → TTFT ~2–5 秒
1M token 输入   → TTFT ~15–30 秒(视模型和基础设施)

对话类应用 30 秒才开始输出,用户体验基本无法接受。

代价三:中间丢失问题

2023 年 Stanford 的研究 "Lost in the Middle"(Liu et al.)发现:当相关信息放在长上下文的中间时,LLM 的召回表现显著下降。信息在开头或结尾时表现最好,在中间时表现最差。

位置 vs 召回率(近似趋势):
开头(0-10%)   ████████████████ 高
中间(40-60%)  ██████           低
结尾(90-100%) ████████████     较高

这意味着你把 100 篇文档全塞进去,模型不一定能找到放在 50 号位置的那篇。


RAG 的实际代价

RAG 不是没有代价的。

代价一:检索不完美

向量检索是近似匹配,会出错:

  • 漏检(False Negative):相关文档没被召回。用户问了一个问题,但对应的段落语义上和问题距离较远,被排在了 top-k 之外。
  • 误检(False Positive):无关文档被召回。LLM 拿到噪声上下文,可能产生混淆或幻觉。

这是 RAG 系列前几篇一直在解决的问题:混合检索、Rerank、HyDE……本质上都是在修补检索的不完美。

代价二:分块破坏上下文

分块(Chunking)把文档切碎,相关信息可能分散在不同 chunk。一篇 10 页的研究报告,结论依赖第 3 页的假设,但被切成了两个 chunk,检索时只拿到了结论那块,LLM 缺少背景信息。

代价三:系统复杂度

RAG 是一个完整的工程系统:向量库 + Embedding 模型 + 检索链路 + 更新机制 + 评估框架。相比"直接把文档发给 LLM",它的维护成本更高。


五个维度的对比

维度长上下文RAG
文档量上限~10–100 篇(受窗口和成本限制)无上限(向量库可扩展)
成本高(所有 token 每次都计费)低(只发相关片段)
延迟高(大输入慢)低(小输入快)
召回完整性完美(全部内容都在)不完整(依赖检索质量)
知识更新需要重新发送所有内容只更新变化的文档
工程复杂度低(直接调用 API)高(需要维护检索链路)
单文档理解强(跨全文的推理)弱(受分块影响)

没有哪一方全赢。


决策框架:用哪个?

四个维度定位你的场景:

维度一:文档量

< 50 篇,总计 < 100K token    → 考虑长上下文
50–1000 篇                   → 评估成本后决定
> 1000 篇,或总量 > 1M token  → RAG

维度二:更新频率

静态内容(月级更新以上)         → 长上下文可接受
动态内容(日级/小时级更新)       → RAG(增量索引成本低)
实时数据                        → RAG(或直接 API 集成)

维度三:查询次数

一次性分析(研究、报告生成)       长上下文
低频查询(< 100 次/天)           两者都可以
高频查询(> 1000 次/天)          RAG(成本差异会累积到不可忽视)

维度四:延迟要求

交互式问答(< 3 秒响应)         → RAG
报告生成、离线分析               → 长上下文可接受

综合判断

场景匹配表:

用例                    文档量    更新      查询     建议
────────────────────────────────────────────────────────
法律合同审查(单份)     小        无        一次     长上下文
企业知识库问答          大        频繁      高频     RAG
PDF 财务报告分析        中        无        一次     长上下文
产品文档问答系统        大        中频      高频     RAG
代码库理解              极大      频繁      高频     RAG
会议纪要摘要(单次)     小        无        一次     长上下文

混合策略:两者兼用

长上下文和 RAG 并不互斥,有时候最好的选择是组合:

策略一:RAG 选文档,长上下文读全文

# 第一步:用 RAG 找到最相关的 3 篇文档
relevant_docs = retriever.invoke(query)  # top-3 文档

# 第二步:把完整文档(而不是 chunk)发给 LLM
full_docs = [load_full_doc(doc.metadata["source"]) for doc in relevant_docs]
full_context = "\n\n".join([doc.page_content for doc in full_docs])

# 第三步:LLM 基于完整文档回答
answer = llm.invoke(f"基于以下文档回答:{full_context}\n\n问题:{query}")

适用场景:文档数量大(不能全发),但每篇文档内部有复杂的跨段推理需求。

策略二:粗粒度 RAG + 大块上下文

传统 RAG 的 chunk 大小是 512–1024 tokens。现在窗口大了,可以用 3000–10000 token 的大块,保留更多上下文,同时仍然做检索过滤。

# 分块时用大块(保留更多上下文)
splitter = RecursiveCharacterTextSplitter(
    chunk_size=4000,    # 传统 512 → 现在可以用 4000
    chunk_overlap=400,
)

# 检索时 top-k 相应减小
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# 3 × 4000 = 12,000 tokens,足够精准,比传统 RAG 上下文丰富得多

策略三:摘要缓存 + 精准检索

对大型文档库,先用 LLM 生成每篇文档的结构化摘要,存入向量库;检索时先找摘要,再按需拉取原文对应段落。

# 预处理:生成摘要(一次性)
for doc in all_documents:
    summary = llm.invoke(f"总结这篇文档的核心要点(200字以内):{doc.page_content}")
    # 把摘要作为检索单元
    summary_doc = Document(page_content=summary, metadata={
        "source": doc.metadata["source"],
        "original": doc.page_content,
    })
    summary_vectorstore.add_documents([summary_doc])

# 查询时:检索摘要,返回原文段落
def query_with_summary(question):
    summaries = summary_vectorstore.similarity_search(question, k=5)
    # 从原文中精确提取相关段落
    relevant_chunks = [
        extract_relevant_passage(s.metadata["original"], question)
        for s in summaries
    ]
    return llm.invoke(build_prompt(question, relevant_chunks))

现实中的选择

大窗口模型的出现确实改变了一些决策:

以前需要 RAG,现在可以不用的场景

  • 50 页以内的文档理解(直接塞进去,更简单)
  • 一次性的文档分析任务(不值得搭 RAG 系统)
  • 原型验证阶段(快速验证想法,不需要生产级 RAG)

仍然需要 RAG 的场景(大多数生产系统):

  • 知识库 > 1000 篇文档
  • 需要实时/频繁更新
  • 高并发,成本敏感
  • 需要引用溯源(RAG 天然知道答案来自哪篇文档)

大窗口模型让"简单场景不用 RAG"变得合理了。但它没有让 RAG 过时——它只是让 RAG 的适用场景变得更清晰:当文档量、更新频率、或成本让"全量上下文"不可行时,RAG 是无可替代的。


小结

长上下文RAG
核心优势完整上下文,跨文档推理可扩展,成本低,实时更新
核心局限成本高,延迟大,文档量有上限检索不完美,工程复杂
最适合小量文档的一次性深度分析大规模生产系统
趋势窗口继续变大,成本继续降低检索质量继续提升

两者不是竞争关系,而是互补的工具箱。理解各自的代价,选对了工具,才是工程判断力。


参考资料