深入浅出:langchain RAG 实战

8 阅读25分钟

引言:大型语言模型的“知”与“不知”——为何需要 RAG

大型语言模型(LLM)如 GPT,无疑是近年来最令人瞩目的技术突破。它们能写诗、写代码、翻译,展现出惊人的语言能力,仿佛拥有了某种“知”。

然而,这种“知”并非全知全能。它们的知识来源于训练数据,有明确的截止日期。对于训练之后的新事件、特定行业的最新报告,或是企业内部的私密文档,它们是“不知”的。更麻烦的是,面对未知,它们有时会自信满满地“胡说八道”,产生所谓的“幻觉”。

现实应用中,我们恰恰需要 LLM 回答那些关于特定、最新或私有数据的问题。如何弥合 LLM 的通用能力与特定知识之间的鸿沟?

答案便是 RAG(Retrieval Augmented Generation),检索增强生成。它赋予 LLM “查阅资料”的能力,让生成基于事实。

本文,我们就来深入浅出地理解 RAG 的核心原理,并借助 LangChain 这一强大框架,通过一个具体的实战案例,带你亲手构建一个 RAG 应用。

一、RAG 的核心原理:让 LLM 学会“查资料”

既然我们知道大型语言模型并非无所不知,尤其对新知识和特定领域知识存在盲区,那么一个自然的想法就是:我们能不能给它一本“参考书”,让它在回答问题前先去查阅呢?

这就是 RAG 的核心思想:检索增强生成(Retrieval Augmented Generation) 。顾名思义,它是在传统的文本生成(Generation)过程之前,增加了一个“检索”(Retrieval)步骤。

它的基本逻辑非常直观:

  1. 用户提出一个问题。
  2. 系统根据问题,去一个外部的知识库里查找最相关的资料片段
  3. 将找到的资料片段,连同用户的问题一起,作为上下文提供给大型语言模型。
  4. 大型语言模型根据这些上下文资料,生成最终的答案。

RAG 的优势,正是为了弥补 LLM 的不足而生:

  • 知识常新:  外部知识库可以随时更新,LLM 就能获取到最新的信息。
  • 减少幻觉:  回答基于真实的文档内容,大大降低了“胡说八道”的可能性。
  • 答案可追溯:  我们可以知道 LLM 的答案是基于哪些原始文档片段生成的,增加了可信度。
  • 处理私有数据:  可以轻松构建基于企业内部文档、个人笔记等私有数据的问答系统。

那么,RAG 具体是如何实现“查资料”这个过程的呢?我们可以将其拆解为六个主要步骤,就像一套精密的流水线:

  1. 加载 (Loading):  这是第一步,你需要把你想要 LLM 学习的知识(比如 PDF 文档、网页、数据库记录等)加载到系统中。
  2. 分割 (Splitting):  原始文档可能很长,但 LLM 的上下文窗口有限,也为了更精细地检索,我们需要把长文档切分成更小的、有意义的文本块(chunks)。
  3. 嵌入 (Embedding):  计算机不理解文字,但理解数字。这一步是将分割好的文本块转换成高维度的数字向量。这些向量捕捉了文本的语义信息,意思相近的文本,它们的向量在向量空间中距离也更近。
  4. 存储 (Storing):  将这些文本向量存储到一个专门的数据库中,通常是向量数据库(Vector Store) 。向量数据库能够高效地存储和检索海量向量。
  5. 检索 (Retrieval):  当用户提出问题时,同样将用户的问题转换成一个向量。然后,在向量数据库中查找与用户问题向量最相似(距离最近)的文本块向量。这些相似的文本块就是系统找到的“相关资料”。
  6. 生成 (Generation):  最后一步,将用户原始问题和检索到的相关文本块一起打包,发送给大型语言模型。LLM 阅读这些资料,并根据资料生成一个流畅、准确的答案。

理解了这六个步骤,你就把握了 RAG 的核心脉络。接下来,我们将看看 LangChain 这个工具如何帮助我们轻松地实现这套流程。

二、LangChain:构建 RAG 应用的“瑞士军刀”

理解了 RAG 的原理和六个步骤后,你可能会想:要把这些步骤一步步自己实现,是不是很复杂?加载不同格式的文档、选择合适的文本分割策略、对接各种嵌入模型和向量数据库、最后还要把检索结果巧妙地喂给 LLM……听起来工作量不小。

幸运的是,我们有像 LangChain 这样的框架。

为什么说 LangChain 是构建 RAG 应用的利器?

RAG 的流程涉及多个独立的环节:数据加载、处理、存储、检索、再到最后的生成。LangChain 的设计哲学就是将这些环节模块化。它为 RAG 的每一个步骤都提供了抽象和实现,并且最重要的是,它让这些模块之间可以轻松地连接和协作,形成一个流畅的工作流,也就是所谓的“链”(Chain)。

你可以把 LangChain 的模块看作是乐高积木。每块积木都有特定的功能(加载文档、分割文本、存储向量等),而 LangChain 提供了各种连接件,让你能把这些积木按照 RAG 的流程组装起来。

让我们看看 LangChain 的核心模块如何对应 RAG 的六个步骤:

  • 加载 (Loading) -> DocumentLoaders:  LangChain 提供了大量的 DocumentLoaders,可以从各种来源加载数据,比如 PDF 文件、网页、CSV、数据库等等。这省去了你自己解析不同文件格式的麻烦。
  • 分割 (Splitting) -> TextSplitters:  处理长文本是 RAG 的关键一环。LangChain 提供了多种 TextSplitters,你可以根据不同的需求选择合适的策略来分割文本,确保分割后的文本块既不会太长超出 LLM 上下文,也不会太短丢失关键信息。
  • 嵌入 (Embedding) -> Embeddings:  将文本转换为向量是检索的基础。LangChain 抽象了嵌入模型的接口,你可以轻松切换使用 OpenAI 的嵌入模型,或是各种开源的本地模型。
  • 存储 (Storing) -> VectorStores:  存储和检索向量需要向量数据库。LangChain 集成了市面上主流的向量数据库,如 Chroma、FAISS、Pinecone、Weaviate 等。你可以选择一个,然后通过 LangChain 的统一接口进行操作。
  • 检索 (Retrieval) -> Retrievers:  这一层是对向量数据库检索逻辑的进一步封装。Retriever 知道如何根据用户查询去向量数据库里查找最相关的文档块。这是连接“检索”和“生成”的关键组件。
  • 生成 (Generation) -> LLMs & Chains:  LangChain 支持接入各种大型语言模型(LLMs)。而 Chains(或者更现代的 LCEL - LangChain Expression Language)则是将前面检索到的文档块和用户查询一起发送给 LLM,并指导 LLM 如何根据这些信息生成最终答案的“指挥官”。RetrievalQA Chain 就是一个专门用于 RAG 问答的预设链。

总而言之,LangChain 提供了一个结构化的方式来思考和实现 RAG 应用。它把复杂的底层细节封装起来,让你能更专注于业务逻辑和流程编排。

理论讲得再多,不如动手实践。接下来,我们就用 LangChain,一步步构建一个真实的 RAG 应用,让你亲身体验它的强大之处。

三、LangChain RAG 实战:构建一个技术文档问答机器人

理论终归要落地。现在,我们来卷起袖子,用 LangChain 构建一个实际的 RAG 应用。我们的目标是创建一个问答机器人,能够回答关于流行 Python 库 requests 的官方文档中的问题。

案例设定

requests 库是 Python 中用于发送 HTTP 请求的利器,其官方文档非常详尽。但当你急需查找某个函数的具体用法、某个参数的含义,或者某个特定场景(如上传文件、设置代理)的处理方法时,在浩瀚的文档中搜索有时并不高效。一个能够直接理解你的问题并从文档中提取答案的机器人,将大大提升效率。

选择技术文档作为案例,是因为它具有代表性:信息密集、专业术语多、结构化程度不一(代码示例、文字说明、表格等),是 RAG 技术非常适合处理的场景。

环境准备

首先,确保你已经创建了一个名为 techdocs_bot 的项目目录,并在其中初始化了 uv 环境(如果需要帮助,请参考 深入浅出:langchain 快速上手(DeepSeek 版) 这篇文章关于环境配置的部分)。

我们需要安装以下依赖:

  • langchain: LangChain 核心库。
  • langchain_deepseek: 使用 DeepSeek 的 LLM 服务,目前 DeepSeek 没有 Embedding 的服务使用。
  • langchain_huggingface text2vec transformers[torch] sentence-transformers: 使用 huggingface 的本地模型 Embeddings,方便测试。如果你使用其他模型,需要安装对应的 LangChain 集成库以及配置 API。
  • langchain-community: 包含许多常用的组件,如文档加载器、文本分割器、向量存储等。
  • chromadb: 我们选择的向量数据库,轻量且易于使用。
  • bs4WebBaseLoader 用于解析 HTML 内容时需要。
  • python-dotenv: 用于加载 .env 文件中的 API 密钥等环境变量,这是一个好习惯。这次我们使用 DeepSeek,需要配置 DEEPSEEK_API_KEY

在 techdocs_bot 项目目录下,打开终端,运行以下命令安装依赖:

uv venv
source .venv/bin/activate
uv add langchain langchain-community langchain_huggingface  bs4 python-dotenv
uv add text2vec transformers[torch] sentence-transformers chromadb

实战步骤 1:获取并加载文档

我们的知识来源是 requests 的官方在线文档。LangChain 提供了 WebBaseLoader 来加载网页内容。我们可以指定几个关键页面的 URL。


import os
# 设置USER_AGENT(必须在导入WebBaseLoader之前)
os.environ['USER_AGENT'] = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'

from langchain_community.document_loaders import WebBaseLoader
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

# 定义要加载的文档 URL 列表
# 这里我们选择 requests 文档的几个核心页面
urls = [
    "https://requests.readthedocs.io/en/latest/", # 首页/快速开始
    "https://requests.readthedocs.io/en/latest/user/quickstart/", # 快速开始
    "https://requests.readthedocs.io/en/latest/user/advanced/", # 高级用法
    "https://requests.readthedocs.io/en/latest/api/" # API 参考 (部分)
]

# 使用 WebBaseLoader 加载文档
loader = WebBaseLoader(urls)
docs = loader.load()

print(f"成功加载了 {len(docs)} 个文档页面。")
# 可以打印第一个文档的内容看看
# print(docs[0].page_content[:500])

WebBaseLoader 会自动抓取网页内容并进行初步清理,将其转换为 LangChain 的 Document 对象列表。每个 Document 对象包含 page_content(文本内容)和 metadata(如来源 URL)。

实战步骤 2:处理和分割文本

加载进来的文档可能很长,直接喂给 LLM 会超出其上下文窗口。而且,为了更精确地检索,我们需要将长文档分割成更小的、有逻辑关联的文本块(chunks)。RecursiveCharacterTextSplitter 是一个常用的分割器,它会尝试按段落、句子等有意义的单位进行递归分割。

对于技术文档,chunk_size(每个文本块的最大长度)和 chunk_overlap(相邻文本块之间的重叠长度)的设置很重要。我们希望保留代码示例、函数签名、参数说明等关键信息的完整性。适当的重叠可以帮助保留跨越分割边界的信息。

# 导入文本分割器
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 初始化文本分割器
# chunk_size: 每个文本块的最大字符数
# chunk_overlap: 相邻文本块之间的重叠字符数
# 对于技术文档,chunk_size 不宜过小,以保留代码块或段落的完整性
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200)

# 分割文档
splits = text_splitter.split_documents(docs)

print(f"原始文档分割成了 {len(splits)} 个文本块。")
# 可以打印第一个分割块的内容看看
# print(splits[0].page_content)

通过分割,我们将原始的几个长网页变成了数百个更小的文本块,每个块都更适合进行嵌入和检索。

实战步骤 3:创建并存储向量

现在,我们将这些文本块转换成向量,并存储到向量数据库中。我们将使用 HuggingFace 的本地 embedding 模型 (BAAI/bge-small-en-v1.5) 来生成向量,并使用 Chroma 作为向量数据库。 OpenAI 等一些平台也提供 embedding 服务,需要配置 API key来使用,这里我们使用的是本地模型,方便我们测试。

# 导入嵌入模型和向量数据库

from langchain_huggingface.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma

# 初始化嵌入模型
model_name = "BAAI/bge-small-en-v1.5"
model_kwargs = {'device': 'cpu'}
encode_kwargs = {'normalize_embeddings': True}

# 初始化向量数据库并从分割后的文本块创建
# directory 参数指定向量数据存储的路径,方便后续加载
persist_directory = './chroma_db'
vectorstore = Chroma.from_documents(documents=splits, embedding=embeddings, persist_directory=persist_directory)

print(f"文本块已嵌入并存储到向量数据库:{persist_directory}")

# 如果数据库已经存在,下次可以直接加载
# vectorstore = Chroma(persist_directory=persist_directory, embedding_function=embeddings)

这一步是 RAG 的核心基础设施。我们将所有文档的“语义指纹”(向量)存储起来,构建了一个可高效搜索的索引。

实战步骤 4:构建检索器

向量数据库存储了向量,但我们需要一个“检索器”来封装查询逻辑。检索器知道如何接收用户查询,将其转换为向量,然后在向量数据库中查找最相似的文本块。

我们可以直接从向量数据库实例创建一个检索器。as_retriever() 方法非常方便。我们可以设置 k 参数,指定检索器在每次查询时返回多少个最相关的文本块。

# 从向量数据库创建检索器
# search_kwargs={"k": 3} 表示检索最相似的 3 个文本块
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

print(f"已创建检索器,每次查询将返回最相似的 {retriever.search_kwargs['k']} 个文本块。")

# 可以测试一下检索效果
# query = "How to set a custom header in requests?"
# retrieved_docs = retriever.invoke(query)
# print(f"\n对查询 '{query}' 检索到 {len(retrieved_docs)} 个文档:")
# for i, doc in enumerate(retrieved_docs):
#     print(f"--- 文档 {i+1} (来源: {doc.metadata.get('source', '未知')}) ---")
#     print(doc.page_content[:200] + "...") # 打印前200字符

检索器是连接用户查询和知识库的桥梁。

实战步骤 5:组装 RAG Chain

现在我们有了检索器(能找到相关资料)和大型语言模型(能理解资料并生成答案)。我们需要将它们连接起来,形成一个完整的问答流程。LangChain 的 RetrievalQA Chain 就是为此设计的。它接收一个检索器和一个 LLM,自动处理“检索 -> 将检索结果和问题一起送给 LLM -> LLM 生成答案”的流程。

# 导入 LLM 和 RetrievalQA Chain
from langchain_deepseek import ChatDeepSeek
from langchain.chains import RetrievalQA

deepseek_api_key = os.getenv("DEEPSEEK_API_KEY") 
# 初始化大型语言模型
# temperature=0 表示希望模型回答更确定、更少创造性,适合问答任务
llm = ChatDeepSeek(model="deepseek-chat", api_key=deepseek_api_key, temperature=0)

# 创建 RetrievalQA Chain
# retriever: 使用我们之前创建的检索器
# llm: 使用我们初始化的 LLM
# return_source_documents=True: 设置为 True 可以让 Chain 返回检索到的原始文档,方便验证
qa_chain = RetrievalQA.from_chain_type(
    llm,
    chain_type="stuff", # "stuff" 链类型将所有检索到的文档填充到 LLM 的上下文
    retriever=retriever,
    return_source_documents=True
)

print("已成功组装 RetrievalQA Chain。")

chain_type="stuff" 是最简单的链类型,它将所有检索到的文档“填充”(stuff)到一个 Prompt 中,然后发送给 LLM。对于检索到的文档数量不多且总长度不超过 LLM 上下文窗口时,这种方式很有效。

实战步骤 6:进行问答与验证

万事俱备,只欠提问!现在我们可以向构建好的 qa_chain 提出关于 requests 文档的问题了。

# 提出问题
query = "How to set a custom header in a requests GET request?"
# query = "What is the timeout parameter used for?"
# query = "How can I upload a file using requests?"

print(f"\n--- 提问: {query} ---")

# 运行 Chain 获取答案
response = qa_chain.invoke({"query": query})

# 打印答案
print("\n--- 答案 ---")
print(response["result"])

# 打印检索到的原始文档(因为 return_source_documents=True)
print("\n--- 答案来源 ---")
if "source_documents" in response:
    for i, doc in enumerate(response["source_documents"]):
        print(f"文档 {i+1} (来源: {doc.metadata.get('source', '未知')})")
        # print(doc.page_content[:300] + "...") # 可以打印部分内容查看
else:
    print("未返回来源文档。")

运行上面的代码,你会看到 LLM 基于从 requests 文档中检索到的相关片段,生成了一个关于如何设置自定义头部的答案。通过查看“答案来源”,你可以验证 LLM 的回答是否确实基于文档内容,这大大增强了回答的可信度。

至此,我们就成功地使用 LangChain 构建了一个简单的 RAG 应用,一个能够回答技术文档问题的机器人!

当然,这只是一个基础版本。在实际应用中,我们可能还需要考虑如何优化检索效果、处理更复杂的查询、提升用户体验等问题。这些,我们将在下一部分进行探讨。

四、优化与进阶:让你的 RAG 更聪明

通过第三部分的实战,我们已经成功地构建了一个基于 LangChain 的 RAG 应用,它能够从技术文档中检索信息并生成答案。这证明了 RAG 的基本流程是可行的。

然而,在现实世界中,文档的复杂性、用户查询的多样性,以及对回答质量更高的要求,意味着基础的 RAG 实现可能还不够。我们需要一些优化策略,让我们的 RAG 机器人变得更“聪明”。

优化 RAG,本质上是在优化其工作流中的各个环节:从数据处理到检索,再到最终的生成。

以下是一些关键的优化方向:

  1. 精细化文本分割策略:

    • 问题:  文本分割是 RAG 的第一步,也是非常关键的一步。如果分割得不好,一个完整的概念、一段代码示例、或者一个关键的参数说明可能被硬生生截断,导致后续的嵌入和检索效果大打折扣。
    • 优化:  RecursiveCharacterTextSplitter 已经比简单的固定长度分割要好,但我们可以进一步调整 chunk_size 和 chunk_overlap 参数。对于技术文档,可能需要更大的 chunk_size 来包含完整的代码块或段落。此外,LangChain 还提供了其他分割器,例如基于特定分隔符(如 Markdown 标题、代码块标记)的分割器,可以更好地尊重文档的结构。
    • 核心:  分割的目标是创建既包含足够上下文、又不过长的小块,并且尽量不破坏语义完整性。
  2. 选择更合适的嵌入模型:

    • 问题:  嵌入模型决定了文本块如何被转换为向量,直接影响向量数据库中相似度计算的准确性。不同的嵌入模型在处理不同类型的文本(通用文本、技术文本、代码等)时表现可能不同。
    • 优化:  除了 OpenAI 的 text-embedding-ada-002 或新的 third-gen-embedding 模型,社区还有许多优秀的开源嵌入模型(如 BGE, E5, Instructor-XL 等)。有些模型可能在技术领域或特定语言上表现更好。尝试不同的嵌入模型,并通过评估检索效果来选择最适合你知识库的模型。
    • 核心:  嵌入模型的质量是检索效果的基石。
  3. 改进检索方法:

    • 问题:  简单的向量相似度搜索(找到向量距离最近的 k 个文本块)可能存在问题。例如,检索到的 k 个文本块可能内容高度相似,缺乏多样性,或者虽然向量相似但语义上并非最优解。

    • 优化:

      • MMR (Maximal Marginal Relevance):  LangChain 的检索器支持 MMR 算法。它在选择 k 个文本块时,不仅考虑文本块与查询的相关性,还考虑文本块之间的相似性,从而返回既相关又多样化的结果。
      • 查询转换 (Query Transformation):  在进行向量搜索之前,可以使用 LLM 对用户原始查询进行改写、扩展,甚至生成一个“假设性”的答案(HyDE - Hypothetical Document Embeddings),然后用改写后的查询或假设性答案的向量进行检索。这有助于弥合用户查询和文档内容之间的词汇或语义差异。
    • 核心:  检索不仅仅是找到“相似”的,更是找到“最有用”的。

  4. 引入后处理/重排序 (Re-ranking):

    • 问题:  向量数据库检索到的初步结果(比如 top-k)可能包含一些相关性较低或冗余的文档。直接将这 k 个文档全部送给 LLM 可能引入噪声。
    • 优化:  在向量检索之后,但在发送给主 LLM 之前,增加一个后处理步骤。可以使用一个更小、更快的模型(例如一个专门的排序模型)或基于规则的方法,对检索到的文档进行二次排序或过滤,只选择最相关的子集发送给 LLM。
    • 核心:  给 LLM 提供更“干净”、更聚焦的上下文。
  5. 处理上下文窗口限制:

    • 问题:  LLM 的上下文窗口大小是有限的。如果检索到的相关文档块总长度超过了这个限制,就会出错或信息被截断。

    • 优化:

      • 调整 k 的值,减少检索到的文档数量。
      • 使用更先进的链类型,例如 map_reduce 或 refine,它们可以将文档分批处理或逐步提炼信息。
      • 对检索到的文档进行摘要,只将摘要发送给 LLM。
      • 选择具有更大上下文窗口的 LLM 模型。
    • 核心:  确保所有必要的上下文信息都能被 LLM 接收和处理。

这些优化策略并非相互独立,往往需要结合使用。例如,更好的文本分割会提升嵌入质量,进而改善检索效果。而改进检索方法和后处理则能确保发送给 LLM 的上下文是最优质的。

LangChain 为实现这些优化提供了相应的模块和接口,使得尝试和集成这些高级技术变得相对容易。在实际项目中,你需要根据你的具体知识库特点和应用需求,不断实验和调整这些参数和方法。

当然,RAG 也不是万能的,它依然面临一些固有的挑战。这些,我们将在下一部分进行探讨。

五、潜在问题与进一步探索:RAG 的边界在哪里?

通过前面的实战,我们看到了 RAG 如何有效地将 LLM 的生成能力与外部知识库结合起来。然而,就像任何技术一样,RAG 也不是万能的“银弹”,它在实际应用中仍然面临一些挑战和潜在问题。认识到这些,有助于我们更好地应用和改进 RAG 系统。

  1. 数据质量是基石:

    • RAG 的效果高度依赖于你提供的知识库质量。如果原始文档本身就错误百出、信息过时、结构混乱,或者包含大量不相关的内容,那么无论你的检索和生成做得多好,最终 LLM 生成的答案也很难令人满意。这印证了计算机科学中的一句老话:“Garbage in, garbage out”(垃圾进,垃圾出)。
    • 挑战:  清理、组织和维护一个高质量的知识库本身就是一项艰巨的任务。
  2. 检索的“盲点”与失败:

    • 向量检索是基于语义相似度。但有时候,用户的问题可能使用了与文档中完全不同的措辞,或者相关信息分散在文档的多个不相邻的段落中,导致简单的相似度搜索无法找到最相关的片段。
    • 挑战:  如何提高检索的鲁棒性,使其更能理解用户查询的真实意图,并能跨越文档结构找到分散的信息?
  3. LLM 的理解与生成质量:

    • 即使检索到了相关的文档片段,LLM 也可能未能充分理解这些信息,或者在整合多个片段的信息时出现逻辑错误。有时,LLM 仍然可能“偏离”检索到的事实,产生轻微的幻觉,或者未能充分利用提供的上下文。
    • 挑战:  如何设计更好的 Prompt 策略,或者使用更适合处理长上下文和复杂推理的 LLM 模型,确保 LLM 能够忠实且有效地利用检索到的信息?
  4. 上下文窗口的限制:

    • 虽然 LLM 的上下文窗口在不断增大,但它仍然是有限的。如果检索到的相关文档片段数量过多,总长度超过了 LLM 的处理上限,我们就无法将所有信息一次性喂给 LLM。
    • 挑战:  如何在检索到的信息量大时,智能地选择、摘要或分批处理这些信息,确保关键内容不丢失,同时不超过 LLM 的限制?

这些挑战促使研究者和开发者不断探索更复杂的 RAG 架构和技术,将 RAG 推向更智能的阶段。一些进一步探索的方向包括:

  • 查询转换 (Query Transformation):  在检索之前,使用 LLM 或其他技术对用户原始查询进行改写、扩展,生成多个可能的查询版本,或者生成一个假设性的答案(HyDE),然后用这些转换后的查询进行检索,以提高召回率。
  • 多跳检索 (Multi-hop Retrieval):  对于需要多步推理的问题(例如,“A 的老板 B 的年龄是多少?”),系统需要先找到 A 的老板是 B,再根据 B 的信息找到其年龄。这需要更复杂的检索逻辑,可能涉及多次检索和中间推理。
  • Agentic RAG:  将 RAG 集成到更广泛的 Agent 框架中。Agent 可以根据用户查询,自主决定是直接调用 RAG、还是使用其他工具(如搜索引擎、代码解释器),甚至可以决定如何优化检索过程(例如,先进行关键词搜索,再进行向量搜索)。
  • 混合检索 (Hybrid Search):  结合传统的关键词搜索(如 BM25)和向量搜索的优势,以提高检索的准确性和召回率。
  • RAG 与 Fine-tuning 的结合:  在某些场景下,可以先使用 RAG 提供领域知识,然后对 LLM 进行微调,使其更好地理解特定领域的术语和推理模式。

RAG 仍然是一个快速发展的领域,新的技术和优化方法层出不穷。我们今天构建的只是一个基础但功能完整的 RAG 系统。理解其局限性,并关注这些进阶方向,将帮助我们构建更强大、更鲁棒的 LLM 应用。

总结:RAG——连接 LLM 与现实知识的桥梁

走到这里,我们已经完整地走过了 RAG 的旅程。从认识到大型语言模型在知识上的局限性,到理解 RAG 如何通过“检索”来增强“生成”,再到借助 LangChain 这一强大框架,亲手构建了一个能够回答技术文档问题的机器人。

我们看到了 RAG 的核心价值:它不再让 LLM 仅仅依赖于其静态的训练数据,而是赋予了它连接外部、实时、甚至私有知识的能力。这就像是给一个博览群书但记忆停留在过去的学者,提供了一个可以随时查阅最新文献和特定资料的图书馆。

LangChain 在这个过程中扮演了关键角色。它将 RAG 复杂的流程分解为可管理的模块,并提供了灵活的连接方式,极大地降低了构建这类应用的门槛。从文档加载、文本分割、向量嵌入与存储,到最后的检索与生成,LangChain 的各个组件就像精心设计的齿轮,协同工作,让整个 RAG 流程顺畅运转。

通过构建技术文档问答机器人的实战案例,我们不仅验证了 RAG 的可行性,也体会到了它在解决实际问题中的潜力——让庞杂的文档变得触手可及,让信息获取更加高效。

当然,我们也探讨了 RAG 当前面临的一些挑战,比如数据质量、检索精度、上下文限制等,并展望了一些正在发展中的高级技术,这些都指明了 RAG 未来优化的方向。

总而言之,RAG 是当前连接大型语言模型与现实世界知识最有效、最主流的技术方案之一。它让 LLM 的应用场景从通用领域拓展到特定行业和个性化需求。而 LangChain,则是帮助我们快速搭建这座“知识桥梁”的得力助手。

希望通过本文,你不仅理解了 RAG 的原理,更能动手实践,开启构建属于你自己的智能问答应用之旅