深入理解LLM与RAG结合应用:从原理到实战

38 阅读23分钟

引言:当LLM遭遇“知识鸿沟”,RAG如何力挽狂澜?

嘿,各位技术爱好者! 随着大型语言模型(LLMs)的爆发式发展,我们惊喜地看到它们在文本生成、代码辅助、智能问答等领域展现出前所未有的能力。它们就像一个无所不知的“超级大脑”,能够流畅地进行对话,甚至创作诗歌和文章。然而,作为开发者和技术使用者,我们很快就发现了LLMs并非完美无缺,它们存在一些显著的局限性:

  1. 知识截止日期(Knowledge Cutoff):LLMs的知识来源于训练数据,这导致它们无法回答超出其训练数据截止日期之后发生的问题(例如,2023年后的新闻事件)。
  2. 幻觉(Hallucination):当LLM缺乏特定知识时,它可能会“一本正经地胡说八道”,生成看似合理实则错误或虚构的信息,这在关键业务场景中是不可接受的。
  3. 私有数据访问(Private Data Access):LLMs无法直接访问我们企业内部的文档、数据库或最新的业务报告,这意味着它们无法为我们提供基于私有知识的准确回答。
  4. 可解释性差(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-v2BGE-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 (轻量级,可持久化)。
    • 分布式/云服务:PineconeWeaviateMilvusQdrant等,适用于大规模生产环境。
# 向量数据库 (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的缺陷,特别是幻觉问题?"\