§1.2.10

From RAG to Context Engineering(2025 末)的范式转变?

好的,我们来深入探讨从 RAG 到上下文工程(Context Engineering)的范式转变。这是一个非常前沿且在未来一两年内会成为面试热点的话题。

核心概念

检索增强生成(Retrieval-Augmented Generation, RAG)是一种将大型语言模型(LLM)与外部知识库相结合的技术范式。其核心思想是,在生成回答之前,首先从知识库(如向量数据库)中检索与用户问题相关的文本片段,然后将这些片段作为上下文(Context)连同原始问题一起提供给 LLM,以生成更准确、更具事实性的回答。

上下文工程(Context Engineering)是 RAG 的演进和泛化,它是一个更宏大的概念。它不仅仅关注“检索什么”,更关注如何“构建和呈现”整个上下文。上下文工程将上下文视为一个需要被精心设计、优化和组织的“产品”,其目标是在有限的上下文窗口内,最大化信噪比,以最低的成本引导 LLM 产生最期望的输出。它涵盖了检索、重排、压缩、格式化、以及与工具、多模态信息和多轮记忆的融合。

简而言之,如果说传统 RAG 的目标是“找到相关的知识”,那么上下文工程的目标是“为 LLM 精心准备一份完美的简报”。

原理与推导

RAG 的概率模型

从概率角度看,标准语言模型的生成过程是建模 P(yx)P(y|x),即给定输入 xx 生成输出 yy 的概率。

RAG 将这个过程分解,引入了隐变量 zz,代表从外部知识库 KK 中检索到的文档。其生成过程可以表示为:

P(yx)=zKtopkP(yx,z)P(zx)P(y|x) = \sum_{z \in K_{top-k}} P(y|x, z) \cdot P(z|x)

这个公式可以直观地解释为:

  • P(zx)P(z|x) (Retriever): 检索器部分。给定问题 xx,从知识库中检索到文档 zz 的概率。在实践中,这通常通过向量相似度(如余弦相似度)来近似。得分最高的 top-k 个文档被选中。
  • P(yx,z)P(y|x, z) (Generator): 生成器部分。给定问题 xx 和检索到的文档 zz,LLM 生成最终答案 yy 的概率。这就是将 xxzz 拼接后输入 LLM 的过程。

RAG 的核心在于通过引入 zz 来为模型提供其内部参数未包含的、即时的、可溯源的知识。

上下文工程的优化目标

上下文工程将 RAG 的思想推广为一个更复杂的优化问题。它不再仅仅是将检索到的 zz 简单拼接,而是构建一个最优的上下文 CoptC_{opt}

ff 为一个上下文工程函数,它接收原始问题 xx 和整个知识库 KK 作为输入,输出一个经过精心编排的上下文 CC。这个函数 ff 包含了一系列操作,如检索(R)、重排(Re-rank)、压缩(Compress)、结构化(Structure)等。

C=f(x,K)=Structure(Compress(Re-rank(Retrieve(x,K))))C = f(x, K) = \text{Structure}(\text{Compress}(\text{Re-rank}(\text{Retrieve}(x, K))))

上下文工程的目标是找到最优的工程函数 ff^*,使得 LLM 在给定 f(x,K)f^*(x, K) 的情况下,生成正确答案 yy^* 的概率最大化,同时满足上下文长度 L(C)L(C) 不超过模型限制 LmaxL_{max} 的约束。

f=argmaxfP(yf(x,K))s.t.L(f(x,K))Lmaxf^* = \arg\max_{f} P(y^* | f(x, K)) \quad \text{s.t.} \quad L(f(x, K)) \le L_{max}

信息论解释: 从信息论角度看,上下文工程的目标是最大化上下文 CC 和理想答案 YY 之间的互信息 I(C;Y)I(C; Y),同时最小化上下文本身的熵(即去除无关噪声)。一个好的上下文应该:

  1. 高相关性: 包含解答问题所需的所有关键信息。
  2. 低冗余度: 剔除重复、无关或次要的信息。
  3. 高可读性: 以一种 LLM 容易理解和解析的结构呈现信息,例如使用 Markdown、JSON 或 XML 格式,帮助模型区分不同来源或类型的信息。

这种从“填充”到“设计”的转变,是应对日益增长的上下文窗口(如 1M token)带来的“大海捞针”(Lost in the Middle)问题的关键。仅仅扩大窗口大小并不能保证性能提升,反而可能因为引入过多噪声而降低性能。上下文工程正是解决这个问题的核心方法论。

代码实现

下面的 Python 代码将使用 NumPy 模拟一个从“朴素 RAG”到“上下文工程”的演进过程,以展示其核心思想。

python
1import numpy as np
2
3# --- 1. 模拟环境搭建 ---
4# 假设我们有一个文档库,已经被切分成 chunks 并向量化
5# 每个 chunk 是一个 (文本, 向量) 的元组
6document_chunks = [
7 ("太阳的核心温度约为1500万摄氏度,表面温度约为5500摄氏度。", np.random.rand(128)),
8 ("光从太阳表面传播到地球大约需要8分20秒。", np.random.rand(128)),
9 ("木星是太阳系中最大的行星,其体积是地球的1300多倍。", np.random.rand(128)),
10 ("太阳风是一种从太阳上层大气射出的超声速等离子体带电粒子流。", np.random.rand(128)),
11 ("尽管木星巨大,但它的密度较低,平均密度仅为水的1.33倍。", np.random.rand(128)),
12 ("太阳的主要成分是氢(约占74%)和氦(约占24%)。", np.random.rand(128)),
13 ("地球的近邻火星,因为其表面的氧化铁而呈现红色。", np.random.rand(128)),
14]
15
16# 模拟一个用户问题和它的向量表示
17user_query = "太阳的温度和构成成分是什么?"
18query_vector = np.random.rand(128)
19
20# 为了演示,我们手动让问题的向量与特定文档更相似
21# 使得与温度和成分相关的 chunk 获得更高的相似度分数
22document_chunks[0] = (document_chunks[0][0], query_vector * 0.9 + np.random.rand(128) * 0.1) # 核心温度
23document_chunks[5] = (document_chunks[5][0], query_vector * 0.8 + np.random.rand(128) * 0.2) # 成分
24document_chunks[3] = (document_chunks[3][0], query_vector * 0.5 + np.random.rand(128) * 0.5) # 太阳风,中等相关
25document_chunks[2] = (document_chunks[2][0], query_vector * 0.1 + np.random.rand(128) * 0.9) # 木星,不相关
26
27def cosine_similarity(v1, v2):
28 """计算两个向量的余弦相似度"""
29 return np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2))
30
31# --- 2. 朴素 RAG (Naive RAG) 实现 ---
32print("--- 朴素 RAG 流程 ---")
33
34# 步骤 2.1: 检索 (Retrieve)
35# 计算问题向量与所有文档块的相似度
36scores = [(chunk[0], cosine_similarity(query_vector, chunk[1])) for chunk in document_chunks]
37# 按相似度降序排序
38sorted_scores = sorted(scores, key=lambda x: x[1], reverse=True)
39
40# 检索 top-k (例如 k=3) 的文档
41top_k = 3
42retrieved_chunks = [chunk for chunk, score in sorted_scores[:top_k]]
43
44# 步骤 2.2: 构建上下文 (Context Building)
45# 简单地将检索到的文本拼接起来
46naive_context = "\n".join(retrieved_chunks)
47
48print(f"用户问题: {user_query}\n")
49print(f"【朴素 RAG】生成的上下文 (简单拼接 top-{top_k}):\n---\n{naive_context}\n---\n")
50
51# --- 3. 上下文工程 (Context Engineering) 实现 ---
52print("\n--- 上下文工程流程 ---")
53
54# 步骤 3.1: 检索 (Retrieve) - 与朴素 RAG 相同
55# 在这个例子中,我们检索更多的候选项,比如 top-k=5,为后续步骤提供更多素材
56k_for_rerank = 5
57retrieved_for_rerank = sorted_scores[:k_for_rerank]
58
59print(f"步骤 3.1: 初步检索 top-{k_for_rerank} 个候选项完成。")
60
61# 步骤 3.2: 重排 (Re-rank)
62# 为什么这样做: 向量相似度可能无法完全捕捉语义相关性。
63# 例如,一个文档可能包含查询的关键词,但只是顺带一提,并非核心内容。
64# 这里我们用一个模拟的 "Cross-Encoder" 模型打分,它会更精确地评估 "问题-文档" 对的相关性。
65def simulated_cross_encoder_rerank(query, chunks_with_scores):
66 reranked_chunks = []
67 for text, initial_score in chunks_with_scores:
68 # 模拟更精细的相关性评估
69 # 高度相关的关键词会得到加分
70 new_score = initial_score
71 if "温度" in text and "温度" in query: new_score += 0.2
72 if "成分" in text and "成分" in query: new_score += 0.2
73 if "核心" in text or "表面" in text: new_score += 0.1
74 # 不相关的词会得到轻微降分
75 if "木星" in text or "火星" in text: new_score -= 0.1
76 reranked_chunks.append((text, new_score))
77 return sorted(reranked_chunks, key=lambda x: x[1], reverse=True)
78
79reranked_chunks_with_scores = simulated_cross_encoder_rerank(user_query, retrieved_for_rerank)
80print(f"步骤 3.2: 重排完成。一个中等相关但排名靠前的文档(如太阳风)可能被排到后面。")
81
82# 步骤 3.3: 压缩 (Compress)
83# 为什么这样做: 即使是相关的文档,也可能包含大量与问题无关的句子。压缩旨在提取核心信息,减少噪声。
84# 我们只保留重排后 top-n (例如 n=2) 的文档进行压缩
85top_n_for_compress = 2
86final_chunks_to_process = reranked_chunks_with_scores[:top_n_for_compress]
87
88def simple_compressor(query, text):
89 """一个简单的基于关键词的摘要式压缩器"""
90 query_keywords = {"太阳", "温度", "构成", "成分"}
91 sentences = text.split(",")
92 relevant_sentences = [s for s in sentences if any(kw in s for kw in query_keywords)]
93 return ",".join(relevant_sentences) if relevant_sentences else text
94
95compressed_texts = []
96for text, score in final_chunks_to_process:
97 compressed_text = simple_compressor(user_query, text)
98 compressed_texts.append(compressed_text)
99
100print(f"步骤 3.3: 压缩完成。只保留每个文档中最相关的句子。")
101
102# 步骤 3.4: 结构化 (Structure)
103# 为什么这样做: 将信息以结构化方式呈现,可以帮助 LLM 更好地理解和利用上下文。
104# LLM 对格式很敏感,清晰的结构能引导其生成更有条理的回答。
105structured_context_parts = []
106structured_context_parts.append("### 相关知识摘要")
107structured_context_parts.append("根据提供的文档,以下是关于用户问题的核心信息:")
108for i, text in enumerate(compressed_texts):
109 structured_context_parts.append(f"\n**摘要 {i+1}:**\n- {text}")
110
111engineered_context = "\n".join(structured_context_parts)
112
113print(f"步骤 3.4: 结构化完成。")
114print(f"\n【上下文工程】生成的上下文 (经过重排、压缩、结构化):\n---\n{engineered_context}\n---")
115
116# --- 4. 最终对比 ---
117# 朴素 RAG 的上下文可能包含不那么相关的 "太阳风" 信息,且信息是平铺的。
118# 上下文工程的上下文更精炼、相关性更高,并且结构清晰,更能引导 LLM 直接回答问题。
119# 最终,将 user_query 和 engineered_context 组合成最终的 prompt 喂给 LLM。
120final_prompt_engineered = f"基于以下信息,请回答问题。\n\n{engineered_context}\n\n问题: {user_query}\n回答:"
121print(f"\n最终喂给 LLM 的 Prompt (工程化后):\n{final_prompt_engineered}")

工程实践

  • 使用场景:

    • 复杂问答系统: 当答案需要综合多个文档来源,并且对准确性要求极高时(如金融、法律、医疗领域)。
    • 自主智能体 (Autonomous Agents): Agent 在执行多步任务时,需要不断更新其“工作记忆”(上下文)。上下文工程负责管理这个记忆,决定保留什么、遗忘什么、如何总结过去的步骤。
    • 代码生成/解释: 在大型代码库上进行问答或生成代码时,上下文工程可以检索相关的类定义、函数实现和文档,并以易于模型理解的结构(如代码块、依赖关系图)呈现。
    • 多模态 RAG: 上下文不仅可以是文本,还可以是图片、表格、图表的描述。上下文工程负责将这些异构信息统一编码到 LLM 能理解的格式中。
  • 超参数选择与权衡:

    • 检索数量 (k): 初次检索的 k 值应该偏大(如 20-50),以保证召回率,为后续的重排和压缩提供充足的候选。最终进入上下文的文档数量则应偏小(如 3-5),以保证精度。
    • 压缩率: 这是一个“信息保真度 vs. 上下文长度(成本)”的权衡。可以通过评估指标(如 faithfulness)来调整压缩算法的积极程度。
    • 重排模型: 使用 Cross-Encoder 重排效果好但延迟高。在实时性要求高的场景,可以使用更轻量的重排模型,或者完全依赖向量检索。这是一个典型的“效果 vs. 性能”的权衡。
    • 结构化格式: JSON、XML 还是 Markdown?这取决于 LLM 的“偏好”和任务的复杂性。对于需要严格字段提取的任务,JSON 可能更好。对于生成可读报告,Markdown 更合适。需要通过实验确定。
  • 性能 / 显存 / 吞吐 的权衡:

    • 整个上下文工程流程(特别是重排)增加了计算开销和延迟。
    • 但是,通过压缩和优化,最终送入 LLM 的 token 数量可能显著减少,从而降低了 LLM 的推理成本和延迟。
    • 权衡点: 前端处理(上下文工程)的延迟 vs. 后端 LLM 推理的成本和延迟。对于昂贵的大模型(如 GPT-4),精心的上下文工程可以显著降本增效。
  • 常见坑和调试技巧:

    • “中游迷失”: 流程中的某一步(如重排或压缩)错误地过滤掉了关键信息。需要对整个流程进行模块化评估,即单独评估检索的召回率、重排的准确率、压缩的信息保留率。
    • 调试方法: 构建一个“黄金上下文”评估集。对于每个问题,手动标注出理想的上下文应该包含哪些信息片段。然后检查你的上下文工程流程生成的上下文与这个黄金上下文的匹配程度。
    • 格式敏感性: LLM 可能对 prompt 模板的微小变化非常敏感。调试时,可以尝试不同的提示词、分隔符、标题,观察输出的变化。

常见误区与边界情况

  • 误区一:“上下文窗口越大,RAG/上下文工程就越不重要。”

    • 恰恰相反。 上下文窗口越大,“大海捞针”问题越严重。LLM 在长上下文中处理信息的能力并非线性增长,注意力会分散。上下文工程通过提纯和结构化信息,帮助 LLM 在巨大的上下文中聚焦于关键点,因此变得更加重要。此外,填充巨大的上下文窗口成本极高。
  • 误区二:“上下文工程就是复杂的 RAG。”

    • 不完全是。RAG 核心是“检索-生成”两阶段。上下文工程是一个更广义的框架,它可能包含 RAG 作为其一个组件。例如,一个 Agent 的上下文工程可能包括:从工具 API 获取的实时数据(非检索)、用户历史对话的摘要(记忆)、以及从向量数据库检索的知识(RAG)。它是一个关于“如何与 LLM 沟通”的整体方法论。
  • 边界情况与失败模式:

    • 检索失败 (Recall Failure): 如果知识库中根本不存在相关信息,或者检索器由于语义鸿沟未能召回相关文档,那么后续所有工程步骤都无济于事。这是整个系统的瓶颈。
    • 过度压缩 (Over-compression): 压缩算法过于激进,可能会删除掉回答问题所必需的“微妙”但关键的细节。
    • 上下文冲突 (Context Contradiction): 检索到的不同文档片段之间可能存在事实冲突。朴素 RAG 会将这些冲突信息直接喂给 LLM,可能导致其“精神分裂”。高级的上下文工程需要识别并处理这种冲突,例如,通过标注信息来源和置信度,让 LLM 自行判断。
  • 常见面试追问及回答要点:

    • 问:“如果你的 RAG 系统经常产生与上下文不符的幻觉,你会如何系统地解决?”

      • 回答要点:
        1. 溯源分析: 首先确认幻觉是源于模型自身,还是因为上下文信息不足/错误。
        2. 上下文质量提升 (即上下文工程):
          • 提高信噪比: 引入重排器,确保最高质量的文档排在最前面。
          • 信息压缩: 移除无关句子,减少模型分心的可能。
          • 明确指令: 在 prompt 中加强指令,如“请严格根据以下提供的上下文回答,如果信息不足,请明确指出”。
        3. 模型微调: 如果问题普遍存在,可以考虑在“忠实于上下文”的数据集上对 LLM 进行微调。
        4. 后处理: 设计一个验证模块,检查生成的答案中的事实性陈述是否能在原始上下文中找到支撑。
    • 问:“在设计一个处理百万级 token 上下文的应用时,你的上下文工程策略是什么?”

      • 回答要点:
        1. 分层与分块: 绝不会把 1M token 的原始文档直接丢进去。首先会将其分层、分块索引。
        2. 虚拟上下文 (Virtual Context): 初始检索可能在一个较小的摘要层或元数据层进行,快速定位到粗粒度的相关区域(比如哪个 PDF 的哪几章)。
        3. 动态加载与聚焦: 根据初步定位,只将最相关的几个大块(例如,每块 16k token)加载到“活跃”上下文中。
        4. 精细化工程: 在这个缩小的“活跃”上下文中,再执行重排、压缩、结构化等精细操作,生成最终的、高度优化的 prompt。
        5. 核心思想: 用多阶段的过滤和工程手段,将“在 1M token 的大海里捞针”的问题,转化为“在精心挑选的 32k token 的池塘里捞针”的问题。
相关题目