完整 RAG 系统:端到端实战#
经过前三章的学习,我们掌握了 RAG 系统的所有零件:
章节 |
技术 |
作用 |
|---|---|---|
第一章 |
文档加载与切块 |
把长文档切成合适大小的片段 |
第二章 |
Embedding |
把文本转成向量,捕获语义 |
第三章 |
ChromaDB |
持久化存储向量,高效检索 |
本章把这三项技术组合成完整的 RAG 系统:
文档语料库
│
▼ chunk_text()
文本片段(chunks)
│
▼ get_embeddings()
向量(embeddings)
│
▼ collection.add()
ChromaDB 向量库
│ ▲
│ │ retrieve(query)
▼ │
相关片段(context)
│
▼ rag_answer(question)
LLM 生成回答
完成本章后,你将拥有一个可直接用于生产的 RAG 骨架代码。
import chromadb
import litellm
litellm.drop_params = True
import json
import os
import re
import time
from dotenv import load_dotenv
load_dotenv()
MODEL = os.getenv("LLM_MODEL")
EMBED_MODEL = os.getenv("EMBED_MODEL", "openai/text-embedding-3-small")
print(f"LLM 模型: {MODEL}")
print(f"Embedding 模型: {EMBED_MODEL}")
# gpt-5/o系列不支持自定义temperature值,统一用安全wrapper
def _c(**kw):
_m = kw.get('model', MODEL)
if any(_m.startswith(p) for p in ('openai/gpt-5','openai/o1','openai/o3','openai/o4')):
kw.pop('temperature', None)
return litellm.completion(**kw)
LLM 模型: openai/gpt-5-mini
Embedding 模型: openai/text-embedding-3-small
核心辅助函数#
三个关键函数构成 RAG 的基础工具箱:
get_embedding:单条文本向量化get_embeddings:批量向量化(效率更高)chunk_text:带重叠的滑动窗口切块
为什么需要重叠(overlap)?
切块边界处的句子可能被截断,重叠区域确保边界信息不丢失。
例如 size=300、overlap=50 时,相邻两块共享 50 个字符。
def get_embedding(text: str) -> list[float]:
"""获取单条文本的 embedding 向量。"""
response = litellm.embedding(model=EMBED_MODEL, input=[text])
return response.data[0]["embedding"]
def get_embeddings(texts: list[str]) -> list[list[float]]:
"""批量获取 embedding,按 index 排序保证顺序一致。"""
response = litellm.embedding(model=EMBED_MODEL, input=texts)
items = sorted(response.data, key=lambda x: x["index"])
return [item["embedding"] for item in items]
def chunk_text(text: str, size: int = 300, overlap: int = 50) -> list[str]:
"""滑动窗口切块,带重叠区域。
Args:
text: 原始文本
size: 每块最大字符数
overlap: 相邻块重叠字符数
Returns:
切块列表
"""
if len(text) <= size:
return [text.strip()]
chunks = []
start = 0
step = size - overlap # 每次前进的步长
while start < len(text):
end = min(start + size, len(text))
chunk = text[start:end].strip()
if chunk:
chunks.append(chunk)
if end == len(text):
break
start += step
return chunks
# 验证 chunk_text
test_text = "A" * 650 # 650 个字符的测试文本
chunks = chunk_text(test_text, size=300, overlap=50)
print(f"原文长度: {len(test_text)} 字符")
print(f"切块数量: {len(chunks)}")
print(f"各块长度: {[len(c) for c in chunks]}")
print(f"重叠验证: 块0末尾50字符 == 块1开头50字符: {chunks[0][-50:] == chunks[1][:50]}")
原文长度: 650 字符
切块数量: 3
各块长度: [300, 300, 150]
重叠验证: 块0末尾50字符 == 块1开头50字符: True
第一节:构建知识库#
我们定义一个包含 5 篇中文文档的语料库,主题涵盖 LLM 核心概念。
构建流程:
对每篇文档调用
chunk_text切块批量获取所有块的 embedding
存入 ChromaDB,附带
doc_id、chunk_id、source元数据
# 定义语料库:5 篇关于 LLM 的中文文档
CORPUS = [
{
"doc_id": "doc_001",
"source": "LLM基础教材",
"text": (
"大语言模型(Large Language Model,LLM)是基于 Transformer 架构的深度学习模型,"
"通过在海量文本数据上进行预训练来学习语言规律。GPT 系列、Claude、Gemini 是当前最"
"具代表性的大语言模型。这些模型参数量从数十亿到数千亿不等,训练数据来自互联网、书"
"籍、代码等多种来源。预训练完成后,通过指令微调(Instruction Tuning)和人类反馈强"
"化学习(RLHF)使模型更好地遵循人类指令。大语言模型的核心能力包括文本生成、问答、"
"摘要、翻译、代码编写等,在众多自然语言处理任务上达到甚至超越人类水平。"
),
},
{
"doc_id": "doc_002",
"source": "RAG技术报告",
"text": (
"检索增强生成(Retrieval-Augmented Generation,RAG)是一种将信息检索与文本生成结"
"合的技术框架。RAG 的核心思想是:在回答问题时,先从外部知识库中检索相关信息,再将"
"检索到的内容作为上下文提供给语言模型,从而生成更准确、更有依据的回答。RAG 解决了"
"纯 LLM 的三大痛点:知识截止日期(训练数据有时效性)、幻觉问题(模型可能编造事实)"
"、领域知识不足(通用模型缺乏专业知识)。典型的 RAG 流程分为离线索引阶段和在线推理"
"阶段。离线阶段将文档切块、向量化并存入向量数据库;在线阶段将用户问题向量化,检索"
"最相关的文档块,拼接到提示词中让 LLM 生成回答。"
),
},
{
"doc_id": "doc_003",
"source": "Transformer论文解读",
"text": (
"Transformer 是 2017 年 Google 在论文《Attention is All You Need》中提出的神经"
"网络架构。其核心创新是自注意力机制(Self-Attention),允许模型在处理序列中的每个"
"位置时关注序列中所有其他位置,从而捕获长距离依赖关系。Transformer 由编码器"
"(Encoder)和解码器(Decoder)组成。编码器将输入序列映射到连续表示,解码器根据"
"编码器输出和已生成的词元逐步生成输出序列。多头注意力机制(Multi-Head Attention)"
"让模型从不同角度理解输入,前馈网络(FFN)负责非线性变换,残差连接和层归一化保证"
"训练稳定性。BERT 使用编码器结构,GPT 使用解码器结构,T5 使用完整的编解码器结构。"
),
},
{
"doc_id": "doc_004",
"source": "Embedding技术综述",
"text": (
"文本嵌入(Text Embedding)是将文本转换为高维向量的技术,使语义相近的文本在向量"
"空间中距离相近。现代 Embedding 模型通常基于预训练语言模型微调而来,如 OpenAI 的"
"text-embedding-3-small、Cohere 的 embed-multilingual 等。Embedding 的质量直接"
"影响 RAG 系统的检索效果。常用相似度指标有余弦相似度(Cosine Similarity)和欧氏"
"距离(Euclidean Distance)。余弦相似度关注向量方向而非长度,在文本检索中更常用。"
"Embedding 维度通常在 384 到 3072 之间,维度越高表达能力越强,但计算和存储开销"
"也越大。批量获取 Embedding 比逐条获取效率更高,生产环境应充分利用批量 API。"
),
},
{
"doc_id": "doc_005",
"source": "提示词工程指南",
"text": (
"提示词工程(Prompt Engineering)是设计和优化输入提示以引导大语言模型产生期望输出"
"的技术。良好的提示词设计对模型性能有显著影响。常见技巧包括:少样本提示(Few-Shot "
"Prompting)通过提供示例引导模型理解任务格式;思维链提示(Chain-of-Thought)让模"
"型逐步推理,提升复杂问题解决能力;角色扮演提示通过赋予模型特定角色改变回答风格。"
"在 RAG 场景中,系统提示词通常要求模型:仅根据提供的上下文回答,不要编造信息,无"
"法从上下文中找到答案时明确告知用户。结构化输出提示可以让模型返回 JSON、表格等"
"格式化数据,方便程序进一步处理。"
),
},
]
print(f"语料库文档数: {len(CORPUS)}")
for doc in CORPUS:
print(f" {doc['doc_id']} [{doc['source']}] - {len(doc['text'])} 字符")
语料库文档数: 5
doc_001 [LLM基础教材] - 271 字符
doc_002 [RAG技术报告] - 292 字符
doc_003 [Transformer论文解读] - 337 字符
doc_004 [Embedding技术综述] - 356 字符
doc_005 [提示词工程指南] - 285 字符
# 切块 + 向量化 + 存入 ChromaDB
kb_client = chromadb.Client()
kb_collection = kb_client.create_collection(
name="llm_knowledge_base",
metadata={"hnsw:space": "cosine"},
)
all_chunks = [] # 所有文本块
all_chunk_ids = [] # 每块的唯一 ID
all_metadatas = [] # 每块的元数据
print("── 切块结果 ──")
for doc in CORPUS:
chunks = chunk_text(doc["text"], size=200, overlap=40)
print(f" {doc['doc_id']}: {len(doc['text'])} 字 → {len(chunks)} 块")
for chunk_idx, chunk in enumerate(chunks):
chunk_id = f"{doc['doc_id']}_chunk_{chunk_idx:02d}"
all_chunks.append(chunk)
all_chunk_ids.append(chunk_id)
all_metadatas.append({
"doc_id": doc["doc_id"],
"chunk_id": chunk_idx,
"source": doc["source"],
})
print(f"\n总计 {len(all_chunks)} 个文本块")
# 批量获取 embedding
print("正在批量获取 embedding...")
t0 = time.perf_counter()
all_embeddings = get_embeddings(all_chunks)
t_embed = time.perf_counter() - t0
print(f"Embedding 完成,耗时 {t_embed:.2f}s")
# 存入 ChromaDB
kb_collection.add(
ids=all_chunk_ids,
embeddings=all_embeddings,
documents=all_chunks,
metadatas=all_metadatas,
)
print(f"知识库构建完成,共 {kb_collection.count()} 条记录")
── 切块结果 ──
doc_001: 271 字 → 2 块
doc_002: 292 字 → 2 块
doc_003: 337 字 → 2 块
doc_004: 356 字 → 2 块
doc_005: 285 字 → 2 块
总计 10 个文本块
正在批量获取 embedding...
Embedding 完成,耗时 0.55s
知识库构建完成,共 10 条记录
第二节:RAG 检索函数#
retrieve 函数是 RAG 系统的「大脑」:给定用户问题,从向量库中找出最相关的文档块。
返回格式统一为字典列表,每项包含:
text:文档块原文metadata:来源信息(doc_id、source 等)distance:余弦距离(越小越相关)
def retrieve(
query: str,
collection: chromadb.Collection,
top_k: int = 3,
where: dict | None = None,
) -> list[dict]:
"""从向量库中检索与 query 最相关的文档块。
Args:
query: 用户查询文本
collection: ChromaDB collection
top_k: 返回最相关的前 k 条
where: 可选的元数据过滤条件
Returns:
list of {text, metadata, distance}
"""
query_embedding = get_embedding(query)
kwargs = {
"query_embeddings": [query_embedding],
"n_results": min(top_k, collection.count()),
"include": ["documents", "metadatas", "distances"],
}
if where:
kwargs["where"] = where
results = collection.query(**kwargs)
return [
{
"text": doc,
"metadata": meta,
"distance": dist,
}
for doc, meta, dist in zip(
results["documents"][0],
results["metadatas"][0],
results["distances"][0],
)
]
# 示例检索
example_query = "Transformer 的自注意力机制是什么?"
retrieved = retrieve(example_query, kb_collection, top_k=3)
print(f"查询: \"{example_query}\"")
print("=" * 65)
for i, item in enumerate(retrieved, 1):
print(f"\n[{i}] 来源: {item['metadata']['source']} 距离: {item['distance']:.4f}")
print(f" {item['text'][:80]}...")
查询: "Transformer 的自注意力机制是什么?"
=================================================================
[1] 来源: Transformer论文解读 距离: 0.4184
Transformer 是 2017 年 Google 在论文《Attention is All You Need》中提出的神经网络架构。其核心创新是自注意力机...
[2] 来源: Transformer论文解读 距离: 0.5568
er)和解码器(Decoder)组成。编码器将输入序列映射到连续表示,解码器根据编码器输出和已生成的词元逐步生成输出序列。多头注意力机制(Multi-Head ...
[3] 来源: 提示词工程指南 距离: 0.6744
提示词工程(Prompt Engineering)是设计和优化输入提示以引导大语言模型产生期望输出的技术。良好的提示词设计对模型性能有显著影响。常见技巧包括:少...
第三节:RAG 生成函数#
rag_answer 函数串联检索与生成:
调用
retrieve获取相关文档块将文档块拼接成「上下文」
构建系统提示词,要求模型只根据上下文回答
调用 LLM 生成最终答案
系统提示词的关键设计:明确告知模型知识边界——上下文中没有的信息不要编造。
SYSTEM_PROMPT = """你是知识库助手,只根据提供的上下文回答用户问题。
规则:
1. 只使用「上下文」中的信息回答,不要添加上下文中没有的内容
2. 如果上下文中没有足够信息,直接回答「根据现有知识库,我无法回答这个问题」
3. 回答要简洁准确,引用具体信息时可以提及来源
4. 不要编造数据、引用或事实"""
def rag_answer(
question: str,
collection: chromadb.Collection,
top_k: int = 3,
verbose: bool = False,
) -> str:
"""完整的 RAG 问答流程:检索 → 构建提示 → 生成答案。
Args:
question: 用户问题
collection: 知识库 ChromaDB collection
top_k: 检索文档块数量
verbose: 是否打印完整提示词
Returns:
LLM 生成的回答
"""
# 步骤 1:检索相关文档块
chunks = retrieve(question, collection, top_k=top_k)
# 步骤 2:构建上下文
context_parts = []
for i, chunk in enumerate(chunks, 1):
source = chunk["metadata"]["source"]
context_parts.append(f"[文档{i} - 来源:{source}]\n{chunk['text']}")
context = "\n\n".join(context_parts)
# 步骤 3:构建用户消息
user_message = f"""上下文:
{context}
问题:{question}"""
if verbose:
print("── 完整提示词 ──")
print(f"[系统提示]\n{SYSTEM_PROMPT}")
print(f"\n[用户消息]\n{user_message}")
print("─" * 50)
# 步骤 4:调用 LLM
response = _c(
model=MODEL,
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
temperature=0.1, # 低温度确保回答稳定、忠实于上下文
)
return response.choices[0].message.content
# 演示:打印完整提示词
print("演示完整 RAG 提示词结构(第一次调用显示详情)")
demo_answer = rag_answer(
"RAG 解决了哪些 LLM 的痛点?",
kb_collection,
verbose=True,
)
print(f"\n── 模型回答 ──")
print(demo_answer)
演示完整 RAG 提示词结构(第一次调用显示详情)
── 完整提示词 ──
[系统提示]
你是知识库助手,只根据提供的上下文回答用户问题。
规则:
1. 只使用「上下文」中的信息回答,不要添加上下文中没有的内容
2. 如果上下文中没有足够信息,直接回答「根据现有知识库,我无法回答这个问题」
3. 回答要简洁准确,引用具体信息时可以提及来源
4. 不要编造数据、引用或事实
[用户消息]
上下文:
[文档1 - 来源:RAG技术报告]
练数据有时效性)、幻觉问题(模型可能编造事实)、领域知识不足(通用模型缺乏专业知识)。典型的 RAG 流程分为离线索引阶段和在线推理阶段。离线阶段将文档切块、向量化并存入向量数据库;在线阶段将用户问题向量化,检索最相关的文档块,拼接到提示词中让 LLM 生成回答。
[文档2 - 来源:RAG技术报告]
检索增强生成(Retrieval-Augmented Generation,RAG)是一种将信息检索与文本生成结合的技术框架。RAG 的核心思想是:在回答问题时,先从外部知识库中检索相关信息,再将检索到的内容作为上下文提供给语言模型,从而生成更准确、更有依据的回答。RAG 解决了纯 LLM 的三大痛点:知识截止日期(训练数据有时效性)、幻觉问题(模型可能编造事实)、领域知识不足(通用模型缺乏专业知
[文档3 - 来源:Embedding技术综述]
量直接影响 RAG 系统的检索效果。常用相似度指标有余弦相似度(Cosine Similarity)和欧氏距离(Euclidean Distance)。余弦相似度关注向量方向而非长度,在文本检索中更常用。Embedding 维度通常在 384 到 3072 之间,维度越高表达能力越强,但计算和存储开销也越大。批量获取 Embedding 比逐条获取效率更高,生产环境应充分利用批量 API。
问题:RAG 解决了哪些 LLM 的痛点?
──────────────────────────────────────────────────
── 模型回答 ──
RAG 解决三大痛点:知识截止日期(训练数据有时效性)、幻觉问题(模型可能编造事实)、领域知识不足(通用模型缺乏专业知识)。(来源:RAG技术报告,文档1/文档2)
第四节:完整问答演示#
用三类问题测试 RAG 系统的边界:
类型 |
预期行为 |
|---|---|
知识库中有明确答案 |
准确回答,引用相关文档 |
知识库中有部分相关信息 |
基于已有信息作答,不过度延伸 |
知识库中完全没有相关信息 |
明确告知无法回答,不编造 |
test_questions = [
{
"question": "Transformer 架构是由谁提出的?主要创新是什么?",
"type": "完全可回答",
"note": "知识库 doc_003 有详细介绍",
},
{
"question": "提示词工程在 RAG 中如何应用?",
"type": "部分可回答",
"note": "doc_005 有提示词工程,doc_002 有 RAG,但两者结合的内容有限",
},
{
"question": "2024 年诺贝尔物理学奖得主是谁?",
"type": "完全不可回答",
"note": "知识库完全没有诺贝尔奖相关内容",
},
]
for i, item in enumerate(test_questions, 1):
print(f"\n{'='*65}")
print(f"问题 {i} [{item['type']}]")
print(f"Q: {item['question']}")
print(f"备注: {item['note']}")
print("-" * 65)
answer = rag_answer(item["question"], kb_collection)
print(f"A: {answer}")
print(f"\n{'='*65}")
=================================================================
问题 1 [完全可回答]
Q: Transformer 架构是由谁提出的?主要创新是什么?
备注: 知识库 doc_003 有详细介绍
-----------------------------------------------------------------
A: Transformer 由 Google 在 2017 年的论文《Attention is All You Need》提出(见文档1)。其主要创新是自注意力机制(Self-Attention),允许模型在处理序列中每个位置时关注序列中所有其他位置,进而捕获长距离依赖关系(见文档1)。
=================================================================
问题 2 [部分可回答]
Q: 提示词工程在 RAG 中如何应用?
备注: doc_005 有提示词工程,doc_002 有 RAG,但两者结合的内容有限
-----------------------------------------------------------------
A: 简要回答(仅基于提供的上下文):
- 在 RAG 流程中,首先离线将文档切块并向量化,在线将用户问题向量化并检索最相关的文档块,然后将检索到的文档拼接到提示词中交给 LLM 生成回答(参见:文档2)。
- 提示词工程的常见手段(少样本提示、思维链提示、角色扮演提示等)用于设计和优化这些拼接后的提示,以引导模型按照期望的格式、推理步骤和风格输出(参见:文档1)。
- 结合以上,RAG 的核心是把外部检索到的信息作为上下文提供给模型,提示词工程则负责如何组织、示例化和指引这些上下文,从而帮助解决知识时效、幻觉和领域知识不足等问题(参见:文档3;文档2 提到的相关痛点)。
=================================================================
问题 3 [完全不可回答]
Q: 2024 年诺贝尔物理学奖得主是谁?
备注: 知识库完全没有诺贝尔奖相关内容
-----------------------------------------------------------------
A: 根据现有知识库,我无法回答这个问题
=================================================================
第五节:评估检索质量#
RAG 系统的质量取决于两个环节:检索质量和生成质量。
本节聚焦检索质量评估,使用 Recall@K 指标:
$$\text{Recall@K} = \frac{\text{Top-K 结果中包含正确文档的查询数}}{\text{总查询数}}$$
Recall@3 = 0.8 表示 80% 的查询,正确答案文档出现在前 3 个检索结果中。
def evaluate_retrieval(
test_cases: list[dict],
collection: chromadb.Collection,
top_k: int = 3,
) -> dict:
"""评估检索质量,计算 Recall@K。
Args:
test_cases: list of {question, expected_doc_id}
collection: 知识库 collection
top_k: 评估 Recall@K
Returns:
{recall_at_k, details}
"""
details = []
hit_count = 0
for case in test_cases:
question = case["question"]
expected_doc_id = case["expected_doc_id"]
retrieved = retrieve(question, collection, top_k=top_k)
# 检查期望的 doc_id 是否出现在结果中
retrieved_doc_ids = [item["metadata"]["doc_id"] for item in retrieved]
hit = expected_doc_id in retrieved_doc_ids
if hit:
hit_count += 1
hit_rank = retrieved_doc_ids.index(expected_doc_id) + 1
else:
hit_rank = None
details.append({
"question": question,
"expected": expected_doc_id,
"retrieved_ids": retrieved_doc_ids,
"hit": hit,
"hit_rank": hit_rank,
"top_distance": retrieved[0]["distance"] if retrieved else None,
})
recall_at_k = hit_count / len(test_cases)
return {"recall_at_k": recall_at_k, "k": top_k, "details": details}
# 5 个测试用例
eval_cases = [
{
"question": "大语言模型是如何训练的?",
"expected_doc_id": "doc_001",
},
{
"question": "RAG 的离线和在线阶段分别做什么?",
"expected_doc_id": "doc_002",
},
{
"question": "自注意力机制如何捕获长距离依赖?",
"expected_doc_id": "doc_003",
},
{
"question": "余弦相似度和欧氏距离的区别",
"expected_doc_id": "doc_004",
},
{
"question": "思维链提示词是什么技术?",
"expected_doc_id": "doc_005",
},
]
eval_result = evaluate_retrieval(eval_cases, kb_collection, top_k=3)
print(f"Recall@{eval_result['k']} = {eval_result['recall_at_k']:.2%}")
print()
print(f"{'#':<3} {'命中':<5} {'排名':<5} {'最小距离':<10} {'问题'}")
print("-" * 65)
for i, d in enumerate(eval_result["details"], 1):
hit_str = "✓" if d["hit"] else "✗"
rank_str = str(d["hit_rank"]) if d["hit_rank"] else "-"
dist_str = f"{d['top_distance']:.4f}" if d["top_distance"] is not None else "-"
print(f"{i:<3} {hit_str:<5} {rank_str:<5} {dist_str:<10} {d['question'][:30]}")
if not d["hit"]:
print(f" 期望: {d['expected']},实际检索到: {d['retrieved_ids']}")
Recall@3 = 100.00%
# 命中 排名 最小距离 问题
-----------------------------------------------------------------
1 ✓ 1 0.2957 大语言模型是如何训练的?
2 ✓ 1 0.4810 RAG 的离线和在线阶段分别做什么?
3 ✓ 1 0.5923 自注意力机制如何捕获长距离依赖?
4 ✓ 1 0.4899 余弦相似度和欧氏距离的区别
5 ✓ 1 0.5055 思维链提示词是什么技术?
第六节:RAG vs 直接问 LLM#
RAG 最重要的价值是事实基础:模型的回答有据可查,而非凭空生成。
本节对同一个问题进行两种方式的回答对比:
直接问 LLM:模型只能依赖训练数据,可能给出泛化或错误的答案
RAG 回答:模型基于知识库中的具体内容,回答更精准、可追溯
def direct_llm_answer(question: str) -> str:
"""直接调用 LLM,不提供任何上下文。"""
response = _c(
model=MODEL,
messages=[{"role": "user", "content": question}],
temperature=0.7,
)
return response.choices[0].message.content
# 选取一个在知识库中有具体信息的问题
comparison_question = "RAG 技术解决了大语言模型的哪三个具体痛点?请逐一说明。"
print(f"对比问题: {comparison_question}")
print("=" * 65)
print("\n【方式一:直接问 LLM(无上下文)】")
print("-" * 40)
direct_answer = direct_llm_answer(comparison_question)
print(direct_answer)
print("\n【方式二:RAG 回答(有知识库支撑)】")
print("-" * 40)
rag_ans = rag_answer(comparison_question, kb_collection)
print(rag_ans)
print("\n" + "=" * 65)
print("分析:")
print(" 直接 LLM:回答来自训练数据,内容可能正确,但无法追溯来源")
print(" RAG 回答:内容源自 'RAG技术报告',可追溯、可验证")
print(" RAG 的优势在事实密集型、专业领域、需要时效性信息时最为突出")
对比问题: RAG 技术解决了大语言模型的哪三个具体痛点?请逐一说明。
=================================================================
【方式一:直接问 LLM(无上下文)】
----------------------------------------
简短回答:RAG(Retrieval‑Augmented Generation)主要解决三类痛点——知识覆盖与时效性、生成内容的可验证性/幻觉(hallucination)、以及模型上下文/规模与成本限制。下面逐一说明每个痛点、RAG 如何缓解、并给出要点与注意事项。
1) 知识覆盖不足与时效性(knowledge cutoff / domain gaps)
- 痛点:纯靠预训练参数的 LLM 知识固定于训练时,无法及时包含最新信息或特定领域的细节(法律条文、公司内部文档、专利数据等)。
- RAG 解决方式:在生成前检索外部文档库(网页、数据库、内部知识库、向量索引等),把相关片段拼接进 prompt,模型基于这些检索到的上下文生成答案,从而“动态”利用最新或领域特定信息。
- 举例:需要回答“某公司最新季度财报要点”时,先检索最新财报段落,再基于这些段落生成摘要,避免依赖旧知识。
- 注意/最佳实践:检索库需要及时更新;检索质量直接决定最终答案的正确性;要设计好检索策略(布尔、向量、混合)与 chunking 大小。
2) 生成幻觉与可验证性不足(hallucination / traceability)
- 痛点:LLM 有时会编造事实或给出无法追溯的断言,尤其在细节或引用场景中不可接受。
- RAG 解决方式:模型在生成过程中直接引用检索到的文档片段(并可返回来源链接/段落 ID),为答案提供证据链,使输出更可验证、可审计,并降低无中生有的概率。
- 举例:法律咨询或学术问答中,RAG 可以在回答后附上检索到的法规条文或论文段落与页码,便于用户核验。
- 注意/最佳实践:模型仍可能“曲解”或错误引用检索文本,需要后处理(事实检验、置信度阈值、使用 reranker 或 extractive verification)并在界面上明确标注来源与可信度。
3) 上下文窗口/规模与成本限制(长文档、个性化与参数规模问题)
- 痛点:大规模知识库无法全部放入模型上下文窗口,训练或微调包含全部信息代价高昂且不可扩展;对个性化/多用户知识存储需求也难以直接用模型参数解决。
- RAG 解决方式:把知识库存放在外部索引(向量数据库/搜索引擎),按需检索相关片段拼入有限上下文,避免把大量信息写进模型参数或上下文,从而实现对海量文档的可扩展访问并降低推理成本。也便于为不同用户注入个性化上下文。
- 举例:要在客服场景中支持数百万条知识库条目,RAG 仅检索与当前对话相关的几条,既保持响应速度又支持大规模知识。
- 注意/最佳实践:需要设计高效的索引、缓存与检索策略以降低延迟;检索与生成分开会带来工程复杂度(同步、版本控制、权限管理);可结合轻量微调或提示工程提升生成对检索上下文的利用率。
补充几点限制与实务建议
- RAG 的效果高度依赖检索质量(检索错误会直接导致错误输出)。使用向量检索 + 精排(reranker)常能显著提高准确率。
- 仍有“模型忽视上下文”或“拼贴式引用导致语义错配”的问题,需要在 prompt 中明确指令(只基于检索文本回答/标注不确定项)。
- 考虑合规与隐私:外部检索库与日志需做好权限与审计。
- 常见改进:混合检索(sparse + dense)、文档分块优化、检索-生成迭代(retrieve → generate → verify → re-retrieve)以及返回证据与置信度分数。
如果需要,我可以基于你关注的具体场景(客服、法律、医学、企业内网等)给出更具体的 RAG 架构建议与检索/提示范例。
【方式二:RAG 回答(有知识库支撑)】
----------------------------------------
RAG 技术解决了三大痛点(来源:RAG 技术报告):
1. 知识截止日期(训练数据有时效性)
说明:训练得到的 LLM 可能只包含到某一时间点的知识,无法获得最新信息。RAG 在回答时先从外部知识库检索相关内容并提供给模型,利用检索到的最新文档作为上下文来弥补训练数据的时效性(见文档3)。
2. 幻觉问题(模型可能编造事实)
说明:纯生成式模型可能会“编造”事实。RAG 将检索到的真实文档片段拼接进提示词,令模型基于检索到的证据生成回答,从而降低幻觉发生的概率(见文档3、文档2 关于在线阶段检索并拼接文档的描述)。
3. 领域知识不足(通用模型缺乏专业知识)
说明:通用 LLM 在专业领域可能知识不足。RAG 通过从领域相关的知识库检索并提供专门文档作为上下文,使模型能利用外部领域资料生成更具专业性的回答(见文档3)。
=================================================================
分析:
直接 LLM:回答来自训练数据,内容可能正确,但无法追溯来源
RAG 回答:内容源自 'RAG技术报告',可追溯、可验证
RAG 的优势在事实密集型、专业领域、需要时效性信息时最为突出
总结#
完整 RAG 流水线图#
┌─────────────────────────────────────────────────────────────┐
│ 离线索引阶段(一次性) │
│ │
│ 原始文档 │
│ │ │
│ ▼ chunk_text(size=300, overlap=50) │
│ 文本块 [chunk_0, chunk_1, ..., chunk_N] │
│ │ │
│ ▼ get_embeddings(chunks) [批量调用 Embedding API] │
│ 向量 [emb_0, emb_1, ..., emb_N] │
│ │ │
│ ▼ collection.add(ids, embeddings, documents, metadata) │
│ ChromaDB 向量库(持久化存储) │
└─────────────────────────────────────────────────────────────┘
│
│ 每次查询时
▼
┌─────────────────────────────────────────────────────────────┐
│ 在线推理阶段(每次查询) │
│ │
│ 用户问题: "RAG 是什么?" │
│ │ │
│ ▼ get_embedding(question) │
│ 问题向量 │
│ │ │
│ ▼ collection.query(top_k=3) + 可选 where 过滤 │
│ 相关文档块 [chunk_a, chunk_b, chunk_c] + 距离分数 │
│ │ │
│ ▼ 构建提示词 │
│ System: 只根据上下文回答... │
│ User: 上下文: [chunk_a]\n[chunk_b]\n[chunk_c] │
│ 问题: RAG 是什么? │
│ │ │
│ ▼ litellm.completion(model, messages) │
│ 最终回答(有事实依据,可追溯来源) │
└─────────────────────────────────────────────────────────────┘
本章核心代码结构#
函数 |
作用 |
关键参数 |
|---|---|---|
|
文档切块 |
size=300, overlap=50 |
|
批量向量化 |
EMBED_MODEL |
|
语义检索 |
top_k=3, where 过滤 |
|
完整 RAG 问答 |
MODEL, SYSTEM_PROMPT |
|
检索质量评估 |
Recall@K |
下一步优化方向#
Reranker:在 Top-K 检索后,用交叉编码器重新排序,提升精度
混合检索:结合 BM25(关键词)和向量检索,优势互补
查询扩展:用 LLM 生成多个查询变体,提高召回率
多轮对话 RAG:结合对话历史,支持追问和上下文关联
流式输出:使用
litellm.completion(stream=True)实现打字机效果引用追踪:在回答中标注具体来自哪篇文档的哪一段