Gemma + LangChain/LlamaIndex: Building RAG Applications
[Translation Pending]\n\n# 第 13 期 | Gemma + LangChain/LlamaIndex:RAG 应用构建
🎯 学习目标
- 理解检索增强生成 (RAG) 的核心概念、工作原理及其在提升 LLM 准确性方面的作用。
- 掌握使用 LangChain 框架构建 RAG 应用的端到端流程,包括文档加载、文本分割、向量嵌入和检索。
- 学会集成 Gemma 作为 RAG 应用的生成模型,并通过 Ollama 或 Hugging Face 部署。
- 实践利用 ChromaDB 存储和检索文档向量,并构建一个基本的对话式知识库问答系统。
📖 核心概念讲解
13.1 什么是检索增强生成 (RAG)?
大型语言模型 (LLM) 如 Gemma 在各种任务中表现出色,但它们也存在一些固有的局限性:
- 知识时效性:LLM 的知识截止于其训练数据,无法获取最新的信息。
- 幻觉 (Hallucination):LLM 有时会生成听起来合理但实际上不准确或捏造的信息。
- 特定领域知识缺乏:对于企业内部文档、专业报告或个人数据等特定领域知识,LLM 无法直接访问。
- 可追溯性差:LLM 生成的答案往往难以追溯其信息来源。
检索增强生成 (Retrieval-Augmented Generation, RAG) 是一种解决这些问题的强大范式。它通过将 LLM 与外部可信的知识库相结合,允许模型在生成答案之前,先从知识库中检索相关信息。
RAG 的核心思想: 当用户提出问题时,RAG 系统首先在外部知识库中查找与问题最相关的文档片段(检索),然后将这些检索到的信息与用户问题一起作为上下文提供给 LLM,让 LLM 基于这些增强的上下文生成最终答案。这极大地提高了 LLM 答案的准确性、可靠性和可追溯性。
13.2 RAG 工作流概述
RAG 应用通常分为两个主要阶段:数据摄取 (Ingestion) 阶段和 查询 (Query) 阶段。
13.2.1 数据摄取 (Ingestion) 阶段
这个阶段的目标是将非结构化或半结构化的文档转化为可供检索的格式。
原始文档 (PDF, TXT, DOCX, Web等)
│
▼
[1] 文档加载器 (Document Loader)
│
▼
[2] 文本分割器 (Text Splitter)
│
▼
[3] 嵌入模型 (Embedding Model)
│ (将文本块转换为高维向量)
▼
[4] 向量数据库 (Vector Database)
│ (存储文本块及其对应的向量,以及元数据)
▼
准备完成,等待查询
步骤详解:
- 文档加载器 (Document Loader):从各种来源(如本地文件系统、S3、网页、数据库等)加载原始文档。
- 文本分割器 (Text Splitter):将加载的文档分割成更小、更易于管理的文本块 (chunks)。这是因为 LLM 的上下文窗口有限,且较小的文本块有助于提高检索的相关性。分割策略包括固定大小分割、递归字符分割等。
- 嵌入模型 (Embedding Model):将每个文本块转换为一个高维的数值向量(也称为嵌入或 embedding)。这些向量捕获了文本块的语义信息,使得语义相似的文本块在向量空间中距离更近。
- 向量数据库 (Vector Database):存储文本块及其对应的嵌入向量。向量数据库能够高效地进行近似最近邻 (ANN) 搜索,即根据查询向量快速找到语义上最相似的文本块。常见的向量数据库有 ChromaDB, Pinecone, Weaviate, Milvus 等。
13.2.2 查询 (Query) 阶段
当用户提出问题时,RAG 系统会执行以下操作来生成答案:
用户查询 (User Query)
│
▼
[1] 嵌入模型 (Embedding Model)
│ (将查询转换为向量)
▼
[2] 向量数据库 (Vector Database)
│ (进行相似性搜索,检索相关文本块)
▼
[3] 检索器 (Retriever)
│ (获取与查询最相关的原始文本块)
▼
[4] 提示词构造器 (Prompt Builder)
│ (将用户查询、检索到的文本块、系统指令等组合成一个完整的提示词)
▼
[5] 大型语言模型 (LLM, e.g., Gemma)
│ (根据提示词生成答案)
▼
最终答案
步骤详解:
- 嵌入模型 (Embedding Model):与摄取阶段使用相同的嵌入模型,将用户的查询转换为一个查询向量。
- 向量数据库 (Vector Database):使用查询向量在向量数据库中执行相似性搜索,找到与查询语义最相似的 K 个文本块。
- 检索器 (Retriever):从向量数据库中获取这些相似的文本块(通常是原始文本内容)。
- 提示词构造器 (Prompt Builder):将用户的原始问题、检索到的相关文本块以及任何系统指令(例如“请根据以下信息回答问题:”)组合成一个结构化的提示词。
- 大型语言模型 (LLM):将构造好的提示词发送给 LLM(例如 Gemma),LLM 基于这些上下文信息生成最终答案。
13.3 LangChain/LlamaIndex 在 RAG 中的作用
LangChain 和 LlamaIndex 是两个流行的开源框架,旨在简化 LLM 应用程序的开发,特别是 RAG 系统。它们通过提供模块化的组件和抽象层,帮助开发者轻松地将各种 LLM、工具和数据源连接起来。
LangChain
LangChain 提供了一套全面的工具和抽象,用于构建复杂的 LLM 应用:
- 模型 (Models):统一的接口来与不同类型的 LLM(如 Gemma、OpenAI、Cohere 等)进行交互,包括聊天模型、文本模型、嵌入模型。
- 提示词 (Prompts):管理和优化提示词,包括提示词模板、输出解析器。
- 链 (Chains):将多个组件(如 LLM、提示词、检索器)组合成一个序列,实现特定任务。例如
RetrievalQA链、create_retrieval_chain等。 - 文档加载器 (Document Loaders):从各种数据源加载文档。
- 文本分割器 (Text Splitters):将文档分割成块。
- 向量存储 (Vector Stores):与各种向量数据库(如 ChromaDB)集成。
- 检索器 (Retrievers):从向量存储中检索相关文档。
- 记忆 (Memory):为对话式应用提供会话历史管理。
LlamaIndex
LlamaIndex (formerly GPT Index) 专注于将外部数据源连接到 LLM。它提供了一套数据连接器、索引结构和查询引擎:
- 数据连接器 (Data Connectors):加载各种格式和位置的数据。
- 数据索引 (Data Indexes):核心组件,用于构建可查询的索引,例如向量存储索引。
- 查询引擎 (Query Engines):定义如何查询索引并与 LLM 交互以生成答案。
- 聊天引擎 (Chat Engines):支持与索引进行多轮对话。
在本教程中,我们将主要使用 LangChain 来构建 RAG 应用,因为它在 RAG 链的构建和组件集成方面提供了非常灵活和强大的抽象。
13.4 文档处理与向量化
文档处理是 RAG 的第一步,它将原始数据转化为 LLM 可理解和检索的形式。
13.4.1 文档加载器 (Document Loaders)
LangChain 提供了大量的文档加载器。例如,TextLoader 用于加载纯文本文件,PyPDFLoader 用于 PDF 文件,WebBaseLoader 用于网页内容等。
from langchain_community.document_loaders import TextLoader
# 加载一个本地文本文件
loader = TextLoader("data/knowledge.txt")
documents = loader.load()
13.4.2 文本分割器 (Text Splitters)
选择合适的文本分割策略至关重要。RecursiveCharacterTextSplitter 是一个常用的选择,它会尝试按一系列字符(如 \n\n, \n, )递归地分割文本,直到块大小满足要求。
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=1000, # 每个文本块的最大字符数
chunk_overlap=200, # 文本块之间的重叠字符数,有助于保持上下文连贯性
length_function=len, # 计算长度的函数
add_start_index=True, # 添加每个文本块在原始文档中的起始索引
)
chunks = text_splitter.split_documents(documents)
13.4.3 嵌入模型 (Embedding Model)
嵌入模型将文本块转换为向量。Gemma 本身是一个生成模型,不直接提供嵌入功能。我们可以使用其他开源或商业的嵌入模型。常用的选择包括:
- HuggingFaceEmbeddings:可以加载来自 Hugging Face Hub 的各种开源嵌入模型,例如
BAAI/bge-small-en-v1.5、sentence-transformers/all-MiniLM-L6-v2等。 - OpenAIEmbeddings/CohereEmbeddings:如果使用商业服务,可以直接调用它们的 API。
from langchain_community.embeddings import HuggingFaceEmbeddings
# 使用 Hugging Face 上的 BGE Small 嵌入模型
# 首次运行时会自动下载模型
embeddings = HuggingFaceEmbeddings(
model_name="BAAI/bge-small-en-v1.5",
model_kwargs={'device': 'cpu'} # 如果有GPU,可以设置为 'cuda'
)
# 测试嵌入
# text_embedding = embeddings.embed_query("这是一个测试句子。")
# print(f"嵌入向量的维度: {len(text_embedding)}")
13.5 向量数据库:ChromaDB
ChromaDB 是一个开源的、轻量级的向量数据库,易于在本地部署和使用,非常适合开发和测试 RAG 应用。它支持存储文本、嵌入向量和元数据,并提供了高效的相似性搜索功能。
主要特点:
- 易用性:Python 客户端库简洁直观。
- 本地部署:无需复杂的服务器设置,可以直接作为 Python 库运行。
- 持久化:可以将数据保存到磁盘,以便下次加载。
- 过滤和元数据:支持基于元数据过滤搜索结果。
在 LangChain 中,我们可以直接通过 Chroma.from_documents 方法从文本块和嵌入模型创建并填充 ChromaDB 实例。
from langchain_community.vectorstores import Chroma
# 将文本块和嵌入模型存储到 ChromaDB
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory="./chroma_db" # 数据将持久化到这个目录
)
# 加载已存在的向量存储
# vectorstore = Chroma(persist_directory="./chroma_db", embedding_function=embeddings)
💻 实战演示
本节将通过一个端到端的示例,演示如何使用 Gemma、LangChain 和 ChromaDB 构建一个 RAG 问答系统。我们将使用 Ollama 来本地运行 Gemma 模型。
准备工作:安装依赖与 Ollama
首先,确保你的系统安装了 Python 3.9+。
安装必要的 Python 库:
pip install langchain langchain-community langchain-core chromadb sentence-transformers pypdf ollamalangchain,langchain-community,langchain-core: LangChain 核心库。chromadb: 向量数据库。sentence-transformers: 用于HuggingFaceEmbeddings下载和运行嵌入模型。pypdf: 如果你需要处理 PDF 文件。ollama: 用于与本地运行的 Gemma 模型交互。
安装并运行 Ollama: 访问 Ollama 官方网站 下载并安装适用于你操作系统的 Ollama。 安装完成后,在终端中拉取 Gemma 模型。我们推荐
gemma:2b或gemma:7b,它们在大多数消费级硬件上运行良好。ollama pull gemma:2b # 或 ollama pull gemma:7b确认 Ollama 服务正在运行。
创建知识库文件: 在你的项目目录下创建一个名为
data的文件夹,并在其中创建一个knowledge.txt文件。填充一些示例内容。data/knowledge.txtGoogle Gemma 是 Google DeepMind 开发的一系列轻量级、最先进的开放模型,基于与创建 Gemini 模型相同的研究和技术。Gemma 的发布旨在为开发者和研究人员提供强大的工具,以构建创新性的 AI 应用。 Gemma 模型家族目前包含两种尺寸:2B 和 7B 参数。这些模型在性能上都经过了优化,可以在各种硬件上高效运行,包括笔记本电脑、工作站和 Google Cloud。Gemma 支持多种部署方式,例如通过 Hugging Face Transformers 库、Keras 3.0、NVIDIA TensorRT-LLM,以及与 Google Cloud Vertex AI 集成。 Gemma 2B 模型非常适合在资源受限的环境中进行快速原型开发和部署。它在保持良好性能的同时,对计算资源的需求较低。Gemma 7B 模型则提供了更强的性能,适用于需要更高质量生成结果的应用场景,同时仍能保持相对高效的推理速度。 Gemma 模型的开源性质鼓励了社区的广泛参与和创新。开发者可以自由地使用、修改和分发 Gemma 模型,用于商业和研究目的。Google DeepMind 还提供了详细的文档和教程,帮助用户快速上手。 未来,Gemma 家族计划推出更多模型尺寸和功能,以满足不断进化的 AI 需求。Google 致力于推动负责任的 AI 发展,Gemma 模型也内置了安全指南和负责任的 AI 工具。
场景 1:数据摄取与向量存储 (Ingestion Pipeline)
此脚本将加载 knowledge.txt 文件,将其分割成块,使用 BGE Small 模型生成嵌入,并将这些嵌入存储到 ChromaDB。
# ingestion.py
import os
from langchain_community.document_loaders import TextLoader, PyPDFLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
# --- 配置参数 ---
DATA_DIR = "./data"
CHROMA_DB_DIR = "./chroma_db"
EMBEDDING_MODEL_NAME = "BAAI/bge-small-en-v1.5" # 推荐使用本地模型
CHUNK_SIZE = 1000
CHUNK_OVERLAP = 200
def load_documents(data_dir):
"""加载指定目录下的所有txt和pdf文档"""
documents = []
for root, _, files in os.walk(data_dir):
for file in files:
file_path = os.path.join(root, file)
if file.endswith(".txt"):
print(f"Loading .txt file: {file_path}")
loader = TextLoader(file_path, encoding="utf-8")
documents.extend(loader.load())
elif file.endswith(".pdf"):
print(f"Loading .pdf file: {file_path}")
loader = PyPDFLoader(file_path)
documents.extend(loader.load())
return documents
def create_vector_store():
"""创建并持久化向量存储"""
print("--- 1. 加载文档 ---")
documents = load_documents(DATA_DIR)
if not documents:
print(f"No documents found in {DATA_DIR}. Please add some files.")
return
print(f"Loaded {len(documents)} documents.")
print("\n--- 2. 分割文本 ---")
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=CHUNK_SIZE,
chunk_overlap=CHUNK_OVERLAP,
length_function=len,
add_start_index=True,
)
chunks = text_splitter.split_documents(documents)
print(f"Split into {len(chunks)} chunks.")
# print(f"First chunk example:\n{chunks[0].page_content[:200]}...")
print("\n--- 3. 初始化嵌入模型 ---")
# 注意: 如果你的机器没有CUDA GPU,最好指定device='cpu'
# 否则,sentence-transformers可能会尝试使用CUDA导致错误或性能问题
# 如果有GPU,可以设置为 'cuda'
embeddings = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL_NAME,
model_kwargs={'device': 'cpu'}
)
print(f"Embedding model '{EMBEDDING_MODEL_NAME}' initialized.")
print("\n--- 4. 创建并持久化 ChromaDB 向量存储 ---")
# 创建或加载向量存储
vectorstore = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
persist_directory=CHROMA_DB_DIR
)
# 持久化到磁盘
vectorstore.persist()
print(f"ChromaDB vector store created and persisted to '{CHROMA_DB_DIR}'.")
print("Ingestion pipeline completed successfully!")
if __name__ == "__main__":
create_vector_store()
运行 ingestion.py:
python ingestion.py
你将看到文档加载、分割、嵌入和 ChromaDB 创建的日志输出。完成后,./chroma_db 目录下会生成 ChromaDB 的数据文件。
场景 2:Gemma LLM 集成与 RAG 链构建 (Query Pipeline)
此脚本将加载之前创建的 ChromaDB,集成 Gemma 模型(通过 Ollama),并构建一个 RAG 问答链来回答问题。
# rag_query.py
import os
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.llms import Ollama
from langchain.prompts import ChatPromptTemplate, PromptTemplate
from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
# --- 配置参数 ---
CHROMA_DB_DIR = "./chroma_db"
EMBEDDING_MODEL_NAME = "BAAI/bge-small-en-v1.5"
OLLAMA_MODEL_NAME = "gemma:2b" # 确保已通过 ollama pull gemma:2b 拉取
def setup_rag_chain():
"""设置 RAG 问答链"""
print("--- 1. 初始化嵌入模型 ---")
embeddings = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL_NAME,
model_kwargs={'device': 'cpu'}
)
print("\n--- 2. 加载 ChromaDB 向量存储 ---")
if not os.path.exists(CHROMA_DB_DIR):
print(f"Error: ChromaDB directory '{CHROMA_DB_DIR}' not found.")
print("Please run 'python ingestion.py' first to create the vector store.")
return None
vectorstore = Chroma(
persist_directory=CHROMA_DB_DIR,
embedding_function=embeddings
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 检索最相关的3个文档块
print("ChromaDB vector store loaded and retriever initialized.")
print(f"\n--- 3. 初始化 Gemma LLM ({OLLAMA_MODEL_NAME}) ---")
# 使用 Ollama 客户端连接本地运行的 Gemma 模型
llm = Ollama(model=OLLAMA_MODEL_NAME, temperature=0.3)
print(f"Gemma LLM ({OLLAMA_MODEL_NAME}) initialized via Ollama.")
print("\n--- 4. 定义 RAG 提示词模板 ---")
# 这是一个用于向 LLM 提供上下文的模板
system_prompt_template = """你是一位专业的AI助手,请根据提供的上下文信息来回答问题。
如果上下文没有足够的信息,请说明你无法找到相关信息,不要编造答案。
上下文:
{context}
"""
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", system_prompt_template),
("human", "{input}"),
]
)
print("\n--- 5. 构建 RAG 链 ---")
# create_stuff_documents_chain 将检索到的文档“填充”到提示词中
document_chain = create_stuff_documents_chain(llm, qa_prompt)
# create_retrieval_chain 将检索器和文档链连接起来
retrieval_chain = create_retrieval_chain(retriever, document_chain)
print("RAG chain built.")
return retrieval_chain
def query_rag_system(rag_chain, query):
"""向 RAG 系统提问并打印答案"""
print(f"\n--- 提问: {query} ---")
response = rag_chain.invoke({"input": query})
print("\n--- 答案 ---")
print(response["answer"])
# 打印检索到的文档(可选)
print("\n--- 检索到的文档 ---")
for doc in response["context"]:
print(f"- {doc.metadata.get('source', 'Unknown Source')}: {doc.page_content[:150]}...")
if __name__ == "__main__":
rag_chain = setup_rag_chain()
if rag_chain:
# 示例查询
query1 = "Gemma 模型家族有哪些尺寸?它们有什么特点?"
query_rag_system(rag_chain, query1)
query2 = "Gemma 是由哪个公司开发的?"
query_rag_system(rag_chain, query2)
query3 = "Gemma 模型有什么优势?"
query_rag_system(rag_chain, query3)
query4 = "月球上有什么动物?" # 知识库中没有的信息
query_rag_system(rag_chain, query4)
运行 rag_query.py:
python rag_query.py
你将看到模型初始化、ChromaDB 加载以及对几个问题的回答。注意第四个问题,由于知识库中没有相关信息,Gemma 应该会根据提示词的指令说明无法回答。
场景 3:构建对话式 RAG (Conversational RAG)
在实际应用中,用户通常会进行多轮对话。对话式 RAG 能够理解上下文并根据之前的对话轮次检索相关信息。LangChain 提供了 create_history_aware_retriever 来处理这个问题。
# conversational_rag.py
import os
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import Chroma
from langchain_community.llms import Ollama
from langchain.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain.chains import create_retrieval_chain, create_history_aware_retriever
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain_core.messages import HumanMessage, AIMessage
# --- 配置参数 ---
CHROMA_DB_DIR = "./chroma_db"
EMBEDDING_MODEL_NAME = "BAAI/bge-small-en-v1.5"
OLLAMA_MODEL_NAME = "gemma:2b"
def setup_conversational_rag_chain():
"""设置对话式 RAG 问答链"""
print("--- 1. 初始化嵌入模型 ---")
embeddings = HuggingFaceEmbeddings(
model_name=EMBEDDING_MODEL_NAME,
model_kwargs={'device': 'cpu'}
)
print("\n--- 2. 加载 ChromaDB 向量存储 ---")
if not os.path.exists(CHROMA_DB_DIR):
print(f"Error: ChromaDB directory '{CHROMA_DB_DIR}' not found.")
print("Please run 'python ingestion.py' first to create the vector store.")
return None
vectorstore = Chroma(
persist_directory=CHROMA_DB_DIR,
embedding_function=embeddings
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
print("ChromaDB vector store loaded and retriever initialized.")
print(f"\n--- 3. 初始化 Gemma LLM ({OLLAMA_MODEL_NAME}) ---")
llm = Ollama(model=OLLAMA_MODEL_NAME, temperature=0.3)
print(f"Gemma LLM ({OLLAMA_MODEL_NAME}) initialized via Ollama.")
print("\n--- 4. 定义历史感知检索器提示词 ---")
# 这个提示词用于让LLM根据对话历史生成一个独立的检索查询
contextualize_q_prompt = ChatPromptTemplate.from_messages(
[
("system", """根据对话历史和用户最新的问题,如果需要,重新表述用户的问题,使其成为一个独立的搜索查询。
如果不需要,直接返回原始问题。"""),
MessagesPlaceholder("chat_history"), # 聊天历史的占位符
("human", "{input}"),
]
)
# 创建历史感知检索器
history_aware_retriever = create_history_aware_retriever(
llm, retriever, contextualize_q_prompt
)
print("History-aware retriever created.")
print("\n--- 5. 定义 RAG 问答提示词 ---")
qa_prompt = ChatPromptTemplate.from_messages(
[
("system", """你是一位专业的AI助手,请根据提供的上下文信息来回答问题。
如果上下文没有足够的信息,请说明你无法找到相关信息,不要编造答案。
上下文:
{context}"""),
MessagesPlaceholder("chat_history"),
("human", "{input}"),
]
)
print("\n--- 6. 构建对话式 RAG 链 ---")
document_chain = create_stuff_documents_chain(llm, qa_prompt)
# 将历史感知检索器和文档链组合成最终的对话式 RAG 链
conversational_rag_chain = create_retrieval_chain(history_aware_retriever, document_chain)
print("Conversational RAG chain built.")
return conversational_rag_chain
if __name__ == "__main__":
conversational_rag_chain = setup_conversational_rag_chain()
if conversational_rag_chain:
chat_history = [] # 用于存储对话历史
print("\n--- 开始对话 (输入 'exit' 退出) ---")
while True:
user_input = input("\n你: ")
if user_input.lower() == 'exit':
break
response = conversational_rag_chain.invoke({
"chat_history": chat_history,
"input": user_input
})
ai_response = response["answer"]
print(f"Gemma: {ai_response}")
# 更新对话历史
chat_history.append(HumanMessage(content=user_input))
chat_history.append(AIMessage(content=ai_response))
# 打印检索到的文档(可选)
# print("\