引言:当LLM遭遇“知识鸿沟”,RAG如何力挽狂澜?
嘿,各位技术爱好者! 随着大型语言模型(LLMs)的爆发式发展,我们惊喜地看到它们在文本生成、代码辅助、智能问答等领域展现出前所未有的能力。它们就像一个无所不知的“超级大脑”,能够流畅地进行对话,甚至创作诗歌和文章。然而,作为开发者和技术使用者,我们很快就发现了LLMs并非完美无缺,它们存在一些显著的局限性:
- 知识截止日期(Knowledge Cutoff):LLMs的知识来源于训练数据,这导致它们无法回答超出其训练数据截止日期之后发生的问题(例如,2023年后的新闻事件)。
- 幻觉(Hallucination):当LLM缺乏特定知识时,它可能会“一本正经地胡说八道”,生成看似合理实则错误或虚构的信息,这在关键业务场景中是不可接受的。
- 私有数据访问(Private Data Access):LLMs无法直接访问我们企业内部的文档、数据库或最新的业务报告,这意味着它们无法为我们提供基于私有知识的准确回答。
- 可解释性差(Lack of Explainability):我们很难知道LLM的回答是基于什么信息生成的,这让其在需要高度信任和溯源的场景中应用受限。
这些“知识鸿沟”和“幻觉”问题,常常让我们的LLM应用在面对真实世界的复杂性和时效性时显得力不从心。举个例子,如果我们问一个训练数据截止到2021年的LLM关于“最新的世界杯冠军”,它可能会给出错误答案或表示不知道:
# 伪代码:不使用RAG的LLM调用示例
# 假设我们有一个LLM客户端,其知识库截止到2021年
class MockLLMClient:
def chat_completion(self, messages):
query = messages[-1]["content"]
print(f"\
用户向LLM提问: {query}")
if "最新的世界杯冠军" in query:
return {"choices": [{"message": {"content": "我无法访问2022年之后的信息,因此无法告知最新的世界杯冠军。"}}]}
elif "RAG是什么" in query:
return {"choices": [{"message": {"content": "RAG(Retrieval Augmented Generation)是一种结合了检索和生成的技术,用于增强大型语言模型(LLM)的知识。"}}]}
else:
return {"choices": [{"message": {"content": "我是一个大型语言模型,能够回答各种问题。"}}]}
mock_client = MockLLMClient()
question_old_knowledge = "请告诉我2022年世界杯的冠军是哪个国家?" # 这是一个LLM可能不知道的问题
response = mock_client.chat_completion(messages=[{"role": "user", "content": question_old_knowledge}])
print(f"LLM的回答: {response['choices'][0]['message']['content']}")
# 预期输出:LLM表示无法回答,因为它缺乏这部分知识
question_generic = "RAG是什么?" # 这是一个LLM可能知道的问题
response = mock_client.chat_completion(messages=[{"role": "user", "content": question_generic}])
print(f"LLM的回答: {response['choices'][0]['message']['content']}")
# 预期输出:LLM能给出RAG的基本解释
为了解决这些痛点,检索增强生成(Retrieval Augmented Generation, RAG) 技术应运而生。RAG不再让LLM“凭空想象”,而是赋予它“查阅资料”的能力。它就像给LLM配备了一个“超级图书馆”和一位“图书馆管理员”,当LLM需要回答问题时,它会先让管理员从图书馆中找到最相关的资料,然后再根据这些资料来生成答案。本文将深入探讨RAG的原理、核心组件、实战应用,以及如何优化RAG系统,帮助你的LLM应用摆脱束缚,变得更加智能和可靠!
一、LLM的局限性:为什么我们需要RAG?
正如我们前面提到的,LLM的强大能力背后,隐藏着其固有的局限性。这些局限性使得直接使用LLM来构建某些应用变得困难,甚至是危险。
- 知识过时:LLM的训练是一个耗时耗力且成本高昂的过程,无法频繁更新。这意味着它们的知识总是带有滞后性。
- 幻觉问题:这是LLM最大的痛点之一。当被问到其训练数据中没有的信息时,LLM倾向于编造听起来合理但实际上是错误的内容。例如,它可以生成一篇关于“不存在的物理定律”的详细解释。
- 缺乏特定领域知识:通用LLM虽然知识广博,但在特定专业领域(如法律、医疗、企业内部规章)的深度和准确性往往不足。如果我们需要一个回答公司内部制度的AI,LLM无法直接做到。
- 可信度与可追溯性低:LLM给出的答案往往缺乏明确的来源,用户难以验证其真实性。在需要高度准确性和透明度的场景(如科研、金融分析)中,这是一个致命缺陷。
这些局限性促使我们寻找一种方法,既能利用LLM强大的语言理解和生成能力,又能弥补其知识上的不足。RAG正是这样一种巧妙的解决方案。
二、RAG核心组件解析:构建你的“知识雷达”
RAG系统的核心思想是,在LLM生成答案之前,先从一个外部的、实时的、或私有的知识库中检索出最相关的信息片段(context),然后将这些信息作为增强信息与用户查询一起提供给LLM。这个过程可以被分解为几个关键组件:
2.1 知识库与文档处理:数据准备是基础
我们的“图书馆”——知识库,可以包含各种形式的非结构化或半结构化数据,如PDF文档、Word文件、网页、数据库记录、API响应等。然而,这些原始数据不能直接喂给LLM或检索系统,我们需要对其进行预处理。
1. 文档加载(Document Loading)
将各种格式的原始数据读取并转换为统一的格式(通常是文本)。
2. 文本分块(Chunking)
这是RAG中的一个关键步骤。原始文档通常很长,超过了LLM的上下文窗口限制,也可能包含大量不相关信息。因此,我们需要将文档切割成更小、更具语义连贯性的“块”(chunks)。
-
为什么分块?
- 适应LLM上下文窗口:确保每个块都能完整传入LLM。
- 提高检索效率和相关性:更小的块意味着更聚焦的语义信息,避免检索到大段不相关的文本。
- 减少成本:LLM的调用费用通常与输入Token数量挂钩。
-
分块策略:
- 固定大小分块:简单,但可能切断语义。
- 递归字符分块(RecursiveCharacterTextSplitter):尝试按不同分隔符(如
\、``、.)递归分割,并保留一定的重叠(overlap),以保持上下文连贯性,这是目前最常用的策略。 - 语义分块:基于文本的语义相似性进行分块,更加智能。
让我们看看如何使用 langchain 进行文档加载和分块:
# 文档加载与分块示例 (使用LangChain)
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_core.documents import Document
import os
# 假设我们有一些长文本内容作为知识库
raw_text = """
RAG(Retrieval Augmented Generation)是一种结合了检索和生成的技术,用于增强大型语言模型(LLM)的知识。它的核心思想是,当LLM接收到用户查询时,首先从一个外部的、实时的、或私有的知识库中检索出相关的信息片段(chunks),然后将这些信息片段作为上下文与用户查询一起提供给LLM,从而让LLM能够生成更准确、更具体、更可靠的回答。这项技术有效地解决了LLM知识截止日期和幻觉等问题。
例如,如果LLM被问及最新的世界杯冠军,它可能因训练数据限制而无法回答。但如果RAG系统能从最新的新闻报道中检索到“最新的世界杯冠军是阿根廷队,他们在2022年的卡塔尔世界杯中夺冠”,LLM就能给出准确的答案。RAG不仅提升了LLM的实时性和准确性,还增强了其可解释性,因为它能够提供答案的来源文档。
RAG系统的主要组件包括:文档加载器、文本分块器、嵌入模型、向量数据库和大型语言模型。每个组件都扮演着至关重要的角色,共同协作以实现高效且准确的知识检索与生成。未来的RAG发展方向包括多模态RAG和Agentic RAG,它们将进一步拓宽RAG的应用边界。
"""
# 1. 加载文档 (这里模拟从文本加载,实际可以是PDF, Word等)
# loader = TextLoader("your_document.txt")
# documents = loader.load()
documents = [Document(page_content=raw_text, metadata={"source": "RAG_Introduction_Article", "page": 1})]
print(f"原始文档长度: {len(raw_text)} 字符")
# 2. 文本分块:使用递归字符分块器,并设置重叠,以保持语义连贯
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=200, # 每个块最大200字符
chunk_overlap=50, # 块之间重叠50字符,有助于保持上下文连贯性
length_function=len,
add_start_index=True # 添加块在原始文档中的起始位置,方便溯源
)
chunks = text_splitter.split_documents(documents)
print(f"生成了 {len(chunks)} 个文本块。")
for i, chunk in enumerate(chunks[:3]): # 打印前3个块的内容和元数据
print(f"\
--- 块 {i+1} (长度: {len(chunk.page_content)}): ---")
print(chunk.page_content)
print(f"元数据: {chunk.metadata}")
# 小贴士:合理的分块大小是RAG性能的关键,太小可能丢失上下文,太大可能引入噪声或超出LLM限制。需要根据实际数据和LLM模型进行调优。
2.2 嵌入模型(Embedding Model):文本到向量的转化
为了让计算机理解文本的语义,我们需要将文本转换为数值表示,这就是嵌入(Embedding)。嵌入模型将文本(分块后的chunks或用户查询)转化为一个高维的浮点数向量(Vector),这些向量能够捕捉文本的语义信息。语义相似的文本在向量空间中距离也更近。
常用的嵌入模型有:
- Hugging Face
Sentence Transformers:提供了多种开源的预训练模型,如paraphrase-MiniLM-L6-v2、BGE-Small等,适合本地部署或对成本敏感的场景。 - OpenAI
text-embedding-ada-002:OpenAI提供的强大Embedding服务,效果优秀,但需要API调用并产生费用。 - 各种闭源/SaaS服务:如Cohere、Google等。
# 文本嵌入 (Embedding) 示例
from sentence_transformers import SentenceTransformer
import numpy as np
# 尝试加载一个预训练的Embedding模型。首次运行会下载模型,可能需要一些时间。
# 如果网络或环境问题导致下载失败,我们提供一个Mock模型进行演示。
try:
embedding_model = SentenceTransformer('paraphrase-MiniLM-L6-v2')
print("SentenceTransformer模型加载成功!")
except Exception as e:
print(f"加载SentenceTransformer模型失败,请检查网络或安装:{e}")
print("尝试使用一个mock模型进行演示。这会生成随机向量,无法体现语义相似度。")
class MockEmbeddingModel:
def encode(self, texts, **kwargs):
print(f"Mocking embedding for {len(texts)} texts.")
return np.random.rand(len(texts), 384) # 模拟384维向量
embedding_model = MockEmbeddingModel()
sample_texts = [
"RAG是一种结合检索和生成的技术。",
"Retrieval Augmented Generation 增强了LLM的知识。",
"猫是一种可爱的宠物,喜欢晒太阳。",
"狗是人类最好的朋友,忠诚又活泼。"
]
# 将文本转换为向量
embeddings = embedding_model.encode(sample_texts)
print(f"\
Embedding 向量维度: {embeddings.shape[1]}")
print(f"第一个文本的Embedding向量 (前5个维度): {embeddings[0][:5]}")
# 计算向量之间的相似度 (余弦相似度)
# 语义越相似的文本,其向量的余弦相似度越高 (接近1)
from sklearn.metrics.pairwise import cosine_similarity
similarity_rag_1_2 = cosine_similarity([embeddings[0]], [embeddings[1]])[0][0]
similarity_rag_1_3 = cosine_similarity([embeddings[0]], [embeddings[2]])[0][0]
print(f"\
文本1 ('{sample_texts[0]}') 与 文本2 ('{sample_texts[1]}') 的相似度: {similarity_rag_1_2:.4f}")
print(f"文本1 ('{sample_texts[0]}') 与 文本3 ('{sample_texts[2]}') 的相似度: {similarity_rag_1_3:.4f}")
# 预期:RAG相关文本(1和2)的相似度会显著高于RAG与猫(1和3)的相似度。这说明Embedding成功捕捉了语义信息。
2.3 向量数据库(Vector Store):高效的“语义索引”
嵌入向量需要被存储起来,以便后续高效地进行相似性搜索。这就是向量数据库(Vector Store)的作用。它专门设计用于存储和查询高维向量,并能快速找出与给定查询向量最相似的Top-K个向量(即最近邻搜索)。
-
主流向量数据库:
- 开源本地:
FAISS(Facebook AI Similarity Search, 内存型)、Chroma(轻量级,可持久化)。 - 分布式/云服务:
Pinecone、Weaviate、Milvus、Qdrant等,适用于大规模生产环境。
- 开源本地:
# 向量数据库 (Vector Store) 示例 (使用Chroma)
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.documents import Document
import numpy as np
import os
# 假设我们已经有了一些经过分块的文本文档
chunks_for_db = [
Document(page_content="LLM的幻觉是一个严重的问题,它会导致模型生成不准确或虚假的信息。", metadata={"source": "TechBlog_001"}),
Document(page_content="RAG通过结合外部知识库,有效地缓解了LLM的幻觉问题,提高了答案的准确性。", metadata={"source": "TechBlog_001"}),
Document(page_content="向量数据库是RAG架构中的关键组件,用于高效存储和检索文本的嵌入向量。", metadata={"source": "Wiki_VectorDB"}),
Document(page_content="阿根廷国家足球队在2022年卡塔尔世界杯决赛中击败法国队,最终夺得了冠军奖杯。", metadata={"source": "SportsNews_2022"}),
Document(page_content="文本分块策略对RAG的检索效果至关重要,需要兼顾块的语义完整性和大小。", metadata={"source": "RAG_BestPractice"})
]
# 重新初始化嵌入模型 (这里确保与LangChain兼容)
try:
embedding_function = HuggingFaceEmbeddings(model_name="paraphrase-MiniLM-L6-v2")
print("HuggingFaceEmbeddings加载成功!")
except Exception as e:
print(f"加载HuggingFaceEmbeddings模型失败,请检查网络或安装:{e}")
print("尝试使用一个mock embedding function进行演示。")
class MockHuggingFaceEmbeddings:
def embed_documents(self, texts):
return [list(np.random.rand(384)) for _ in texts]
def embed_query(self, text):
return list(np.random.rand(384))
embedding_function = MockHuggingFaceEmbeddings()
# 1. 初始化并(可选)持久化向量数据库
# 为了演示,这里使用in-memory ChromaDB,实际应用中可以指定persist_directory
vectorstore = Chroma.from_documents(
documents=chunks_for_db,
embedding=embedding_function
)
print("向量数据库已创建 (in-memory)。")
# 2. 执行相似性搜索
query = "如何解决大型语言模型的幻觉问题?"
# 检索最相似的2个文档
retrieved_docs = vectorstore.similarity_search(query, k=2)
print(f"\
查询: '{query}'")
print("检索到的相关文档:")
for i, doc in enumerate(retrieved_docs):
print(f"--- 文档 {i+1} ---")
print(f"内容: {doc.page_content}")
print(f"来源: {doc.metadata.get('source', '未知')}")
# 预期:检索到关于LLM幻觉和RAG解决方法的文档。
# 再进行一个不同类型的查询
query_worldcup = "谁赢得了2022年的足球世界杯?"
retrieved_docs_wc = vectorstore.similarity_search(query_worldcup, k=1)
print(f"\
查询: '{query_worldcup}'")
print("检索到的相关文档:")
for i, doc in enumerate(retrieved_docs_wc):
print(f"--- 文档 {i+1} ---")
print(f"内容: {doc.page_content}")
print(f"来源: {doc.metadata.get('source', '未知')}")
# 预期:检索到关于阿根廷夺冠的新闻。
三、RAG工作流实战:构建你的第一个智能问答系统
理解了RAG的各个组件后,现在让我们将它们整合起来,构建一个完整的RAG工作流。整个流程就像一条生产线,将原始查询转化为LLM基于外部知识的精准回答。
RAG工作流图示:
用户查询 -> [检索器 (Retriever: 用户查询嵌入 -> 向量数据库相似搜索 -> 检索Top-K文档)] -> [上下文组装 (Context Builder)] -> [LLM (Generator: 接收查询+上下文 -> 生成答案)] -> 答案
我们使用 langchain 库来方便地构建这个流程。
# 完整的RAG工作流实战 (使用LangChain)
import os
from langchain_community.llms import OpenAI # 可选,用于旧版LLM
from langchain_community.chat_models import ChatOpenAI # 推荐用于OpenAI GPT系列
from langchain.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langchain_core.documents import Document
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
import numpy as np
# --- 0. 准备模拟组件 (为了能在没有API Key或下载大模型的情况下运行) ---
# 模拟Chat LLM
class MockChatLLM:
def init(self, name="MockLLM", temperature=0.0):
self.name = name
self.temperature = temperature
def invoke(self, prompt_messages):
# prompt_messages 是一个列表,包含像 {'role': 'user', 'content': '...'} 的字典
# 我们主要关注最后一个用户消息和上下文
system_prompt = ""
user_question = ""
context_info = ""
for msg in prompt_messages:
if isinstance(msg, dict) and msg.get('role') == 'system':
system_prompt = msg['content']
elif isinstance(msg, dict) and msg.get('role') == 'user':
# 用户消息通常包含上下文和问题
user_question = msg['content']
# 尝试从用户问题中提取上下文,这取决于我们prompt template的结构
if "上下文信息:" in user_question:
parts = user_question.split("\
\
用户问题:")
context_info = parts[0].replace("上下文信息:\
", "")
user_question = parts[1].strip()
elif "{context}" in user_question: # 如果直接是PromptTemplate输出的字符串
context_marker_start = user_question.find("上下文信息:\
")
question_marker_start = user_question.find("\
\
用户问题: ")
if context_marker_start != -1 and question_marker_start != -1:
context_info = user_question[context_marker_start + len("上下文信息:\
") : question_marker_start].strip()
user_question = user_question[question_marker_start + len("\
\
用户问题: ") :].strip()
print(f"\
--- MockLLM接收到Prompt ---")
# print(f"System: {system_prompt}")
# print(f"Context: {context_info[:100]}...") # 打印部分上下文
# print(f"Question: {user_question}")
# 模拟LLM基于上下文回答
if "2022年世界杯冠军" in user_question and "阿根廷" in context_info:
return "根据提供的上下文,2022年卡塔尔世界杯的冠军是阿根廷队。" # 模拟从上下文提取信息
elif "RAG" in user_question and "解决" in user_question and "幻觉" in context_info:
return "根据提供的上下文,RAG主要解决了LLM的幻觉、知识陈旧和无法访问私有数据等问题,通过结合外部知识库增强模型能力。"
elif "RAG" in user_question and "原理" in user_question and "检索和生成" in context_info:
return "根据提供的上下文,RAG的原理是先从外部知识库检索相关信息,再将这些信息作为上下文传递给LLM进行生成,从而提供更准确的答案。"
elif not context_info:
return f"我不知道如何回答 '{user_question}',因为没有提供足够的上下文信息。" # 模拟无上下文时不知道
else:
return f"我是一个大型语言模型,将基于提供的上下文回答您的问题:'{user_question}'。"
# 模拟HuggingFaceEmbeddings
class MockHuggingFaceEmbeddings:
def embed_documents(self, texts):
return [list(np.random.rand(384)) for _ in texts]
def embed_query(self, text):
return list(np.random.rand(384))
# --- 1. 准备知识库 (数据预处理、分块、嵌入、向量存储) ---
# 这里使用简化版的chunks_for_rag和in-memory Chroma来构建知识库
# 实际应用中,你可能需要加载大量文档,并持久化到文件系统或云端向量库
chunks_for_rag = [
Document(page_content="RAG(Retrieval Augmented Generation)是一种结合了检索和生成的技术,用于增强大型语言模型(LLM)的知识。", metadata={"source": "Doc_RAG_Intro"}),
Document(page_content="LLM的幻觉、知识陈旧和无法访问私有数据是RAG解决的主要问题,它极大地提升了LLM的可靠性和实用性。", metadata={"source": "Doc_LLM_Problems"}),
Document(page_content="RAG工作流通常包括文档加载、分块、嵌入、向量存储、检索和LLM生成。每个步骤都至关重要。", metadata={"source": "Doc_RAG_Workflow"}),
Document(page_content="2022年卡塔尔世界杯的冠军是阿根廷队,他们在决赛中以点球大战击败了法国队,梅西实现了他的世界杯梦想。", metadata={"source": "Doc_WorldCup_2022"}),
Document(page_content="文本分块(Chunking)策略需要平衡语义连贯性和块大小,常用的有递归字符分块器。", metadata={"source": "Doc_Chunking_Strategy"}),
Document(page_content="嵌入模型(Embedding Model)将文本转化为高维向量,捕捉语义信息,是向量搜索的基础。", metadata={"source": "Doc_Embedding"})
]
# 初始化嵌入模型 (使用Mock,或替换为HuggingFaceEmbeddings/OpenAIEmbeddings)
embedding_function = MockHuggingFaceEmbeddings()
# 使用Chroma作为向量数据库,并填充文档
vectorstore_rag = Chroma.from_documents(
documents=chunks_for_rag,
embedding=embedding_function
)
print("RAG知识库 (Chroma Vector Store) 已初始化并填充。")
# 创建检索器,用于从向量数据库中检索最相关的文档
retriever = vectorstore_rag.as_retriever(search_kwargs={"k": 2}) # 检索Top K=2的文档
print(f"检索器已配置,每次将检索 Top {retriever.search_kwargs['k']} 个文档。")
# --- 2. 初始化LLM (使用MockChatLLM,或替换为ChatOpenAI/HuggingFaceHub) ---
llm = MockChatLLM()
print(f"LLM已初始化: {llm.name}")
# --- 3. 定义提示模板 (Prompt Template) ---
# 这个模板指导LLM如何利用检索到的上下文来回答问题
template = """你是一个智能助手,请根据提供的上下文信息,简洁、准确地回答问题。不要编造信息。如果上下文中没有足够的信息,请明确说明你不知道。""
上下文信息:
{context}
用户问题: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
print("提示模板已定义。")
# --- 4. 构建RAG链 (使用LangChain表达式语言 LCEL) ---
# 这是一个辅助函数,用于将检索到的文档列表格式化成一个字符串,供LLM作为上下文使用
def format_docs(docs):
return "\
\
".join(doc.page_content for doc in docs)
# RAG链定义:
# 1. 接收用户的原始问题
# 2. "context" 部分:使用检索器检索相关文档,并通过 format_docs 函数格式化
# 3. "question" 部分:直接传递用户的原始问题
# 4. 将格式化后的上下文和问题传入提示模板
# 5. 将提示传入LLM进行生成
# 6. 使用 StrOutputParser 将LLM的输出解析为字符串 (如果LLM返回的是Message对象)
rag_chain = (
{"context": retriever | format_docs, "question": RunnablePassthrough()}
| prompt
| llm.invoke # 调用LLM的invoke方法
# | StrOutputParser() # MockLLM直接返回字符串,不需要解析
)
print("RAG链已构建完成!")
# --- 5. 执行查询并获取RAG增强的回答 ---
print("\
======== RAG查询示例 1: 2022年世界杯冠军 (知识更新) ========")
question_rag_1 = "请告诉我2022年世界杯的冠军是哪个国家?"
response_rag_1 = rag_chain.invoke(question_rag_1)
print(f"RAG的回答: {response_rag_1}")
# 预期:RAG系统能够通过检索到的最新知识,准确回答2022年世界杯冠军。
print("\
======== RAG查询示例 2: RAG的核心目标 (私有知识/准确性) ========")
question_rag_2 = "RAG主要解决了LLM的哪些问题?它有什么原理?"
response_rag_2 = rag_chain.invoke(question_rag_2)
print(f"RAG的回答: {response_rag_2}")
# 预期:RAG系统能够结合知识库中RAG的定义,给出详细和准确的回答。
print("\
======== RAG查询示例 3: LLM无知识时的表现 (如果没有匹配的上下文) ========")
# 注意:我们的MockLLM在没有匹配的上下文时会返回"我不知道",这里模拟没有相关文档被检索到的情况
# 为了演示,我们可以手动构造一个没有相关文档的查询,或者从知识库中排除所有相关文档
# 但目前知识库中包含RAG相关内容,因此这个例子仍会检索到。
# 真正的“不知道”情况需要向量数据库未能检索到任何相关文档。
# 模拟一个非常不相关的查询,期望检索器找不到或LLM说不知道
# 为了演示,我将修改知识库,让它无法回答一个不相关的问题。
# 这里假设 RAG链 依然会接收到不相关的上下文,但 LLM 应该能够判断是否能基于此回答。
# (为了演示无知情况,需要更精细的Mock,此处暂略,主要展示RAG成功案例)
# 在真实场景中,如果检索器返回空列表或低相关性文档,LLM会收到空或不相关上下文,从而触发“我不知道”的回答。
这段代码展示了一个完整的RAG pipeline。用户查询不再直接发送给LLM,而是首先通过检索器从你的自定义知识库中找到最相关的信息,然后这些信息被打包成上下文,与用户问题一起提交给LLM,最终生成一个基于事实的、可追溯的答案。这大大提升了LLM的实用性和可靠性!
四、RAG进阶优化与最佳实践:让你的RAG系统更“聪明”
构建了基础的RAG系统后,我们通常会面临检索效果不佳、上下文冗余、LLM输出不稳定等问题。以下是一些进阶的优化策略和最佳实践,可以帮助你的RAG系统变得更加“聪明”和高效。
4.1 检索增强技术:提高“图书馆管理员”的寻宝能力
检索是RAG的“生命线”,检索质量直接决定了最终答案的质量。我们可以通过多种方式来增强检索效果:
1. 查询重写(Query Rewriting)
用户原始查询可能模糊不清、过于简洁或包含代词,导致检索不准确。我们可以利用LLM来重写或扩展用户查询,使其更适合向量搜索。
# 查询重写示例 (概念代码)
# 假设有一个LLM或一个专门的Query Rewrite模型
def rewrite_query_with_llm(original_query, llm_for_rewrite=None):
print(f"原始查询: '{original_query}'")
# 模拟LLM将原始模糊查询重写为更精确的检索查询
if "RAG的问题" in original_query or "RAG解决了什么" in original_query:
rewritten_query = "RAG技术解决了大型语言模型(LLM)的哪些局限性,如幻觉、知识截止和私有数据访问问题?"
print(f" -> LLM重写后查询: '{rewritten_query}'")
return rewritten_query
elif "世界杯" in original_query and "赢家" in original_query:
rewritten_query = "2022年卡塔尔世界杯足球赛的冠军队伍是?"
print(f" -> LLM重写后查询: '{rewritten_query}'")
return rewritten_query
else:
print(" -> 查询无需重写,保持原样。")
return original_query # 保持不变
# 示例调用
rewritten_q1 = rewrite_query_with_llm("RAG解决了什么?")
# 此 rewritten_q1 将作为新的查询发送给检索器
rewritten_q2 = rewrite_query_with_llm("谁是2022年足球最大的赢家?")
# 此 rewritten_q2 将作为新的查询发送给检索器
2. 重排序(Re-ranking)
初始检索器(例如,向量相似性搜索)可能会返回一些相关但并非最优的文档。重排序阶段使用一个更小、更强大的交叉编码器(Cross-encoder)模型对初始检索结果进行二次排序,以提升顶部结果的准确性。
-
好的实践 vs 不好的实践:
- 不好的实践:直接将向量数据库检索到的Top-K文档不加筛选地传给LLM。这可能导致LLM接收到噪声或不那么相关的上下文。
- 好的实践:使用重排序器对Top-K文档进行再次打分,只选择得分最高的Top-N(N<K)文档作为最终上下文。这确保了LLM接收到的是最精华、最相关的片段。
# 重排序 (Re-ranking) 示例 (概念代码)
# 假设我们有初始检索到的文档列表,以及一个Query
from langchain_core.documents import Document
initial_retrieved_docs = [
Document(page_content="RAG增强了LLM的知识,通过外部检索弥补其知识不足。", metadata={"initial_score": 0.8}),
Document(page_content="LLM的幻觉是一个令人头疼的问题,模型有时会编造信息。", metadata={"initial_score": 0.7}),
Document(page_content="猫是一种可爱的宠物,喜欢睡觉和玩耍。", metadata={"initial_score": 0.5}), # 不相关文档
Document(page_content="RAG结合了检索和生成两个阶段,是增强LLM能力的关键技术。", metadata={"initial_score": 0.75})
]
query_for_rerank = "RAG如何改善LLM的缺陷,特别是幻觉问题?"\