构建简单的 RAG 系统并集成 DeepSeek 大模型

484 阅读10分钟

RAG (Retrieval Augmented Generation) 是一种结合了检索(Retrieval)和生成(Generation)两大能力的技术。它允许大型语言模型(LLM)在生成回答之前,先从外部知识库中检索相关信息,从而提高回答的准确性、时效性,并减少“幻觉”现象。本文将详细介绍如何构建一个简单的 RAG 系统,并以 DeepSeek 大模型为例,展示如何将其集成到流程中。

RAG 的核心思想

RAG 的工作流程通常如下:

  1. 用户提问 (Query):用户提出一个问题。
  2. 信息检索 (Retrieval):系统将用户的问题在知识库中进行搜索,找出最相关的文档片段。
  3. 上下文增强 (Context Augmentation):将检索到的相关信息与用户的原始问题一起,构建成一个更丰富的提示(Prompt)。
  4. 答案生成 (Generation):将增强后的提示输入到大语言模型(如 DeepSeek)中,由模型生成最终的回答。

技术选型

为了实现一个简单的 RAG 系统,我们将使用以下工具:

  • Python: 主要编程语言。
  • LangChain: 一个强大的框架,用于简化 LLM 应用的开发,提供了文档加载、文本分割、向量存储、LLM 接口等模块。
  • Sentence Transformers: 用于生成文本嵌入(Embeddings),将文本转换为向量表示。
  • FAISS (Facebook AI Similarity Search): 一个高效的相似性搜索库,用于构建和查询向量数据库。
  • DeepSeek API: DeepSeek 提供的 LLM 服务接口。

实现步骤

1. 环境准备

首先,确保你安装了必要的 Python 库。

pip install langchain sentence-transformers faiss-cpu deepseek-llm openai python-dotenv
  • langchain: RAG 框架。
  • sentence-transformers: 文本嵌入模型。
  • faiss-cpu: 向量数据库 (CPU 版本,如果需要 GPU 加速,可以安装 faiss-gpu)。
  • deepseek-llm: DeepSeek 官方 Python SDK。
  • openai: LangChain 内部某些与 OpenAI API 兼容的接口(即使使用 DeepSeek,有时也会间接依赖)。
  • python-dotenv: 用于管理环境变量(如 API 密钥)。

接下来,你需要获取 DeepSeek API Key。访问 DeepSeek 开放平台 注册并获取你的 API Key。

建议将 API Key 存储在项目根目录下的 .env 文件中,以避免硬编码:

# .env 文件内容
DEEPSEEK_API_KEY="your_actual_api_key"

2. 准备知识库

创建一个名为 knowledge_base 的文件夹,并在其中放入一些文本文件作为你的知识库。例如:

knowledge_base/deepseek_info.txt:

DeepSeek是一家致力于研究通用人工智能(AGI)的公司。
DeepSeek的使命是“用AI改变世界”。
DeepSeek发布了多款强大的语言模型,包括DeepSeek Coder和DeepSeek LLM。
DeepSeek LLM 67B在多个基准测试中表现出色。

knowledge_base/rag_intro.txt:

RAG代表检索增强生成。
它结合了信息检索系统的能力和大型语言模型的生成能力。
RAG可以帮助减少LLM的幻觉,并提供基于特定文档的答案。

3. 代码实现

下面是完整的 Python 代码实现:

import os
from dotenv import load_dotenv

from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_deepseek import ChatDeepseek # langchain_community.chat_models for older versions
from langchain.chains import RetrievalQA
from langchain.prompts import PromptTemplate

# 1. 加载环境变量 (DeepSeek API Key)
load_dotenv()
api_key = os.getenv("DEEPSEEK_API_KEY")

if not api_key:
    raise ValueError("DEEPSEEK_API_KEY not found in .env file or environment variables.")

# --- 知识库处理 ---

# 2. 加载知识库文档
# 使用 TextLoader 加载单个文件,或 DirectoryLoader 加载整个目录
print("加载知识库文档...")
loader = DirectoryLoader('./knowledge_base/', glob="**/*.txt", loader_cls=TextLoader, loader_kwargs={'encoding': 'utf-8'})
documents = loader.load()
if not documents:
    print("未能加载任何文档,请检查 'knowledge_base' 文件夹和文件路径。")
    exit()
print(f"成功加载 {len(documents)} 个文档。")

# 3. 文本分割
# 将加载的文档分割成更小的块,以便嵌入和检索
print("分割文档...")
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=50)
texts = text_splitter.split_documents(documents)
if not texts:
    print("未能分割文档。")
    exit()
print(f"文档被分割成 {len(texts)} 个文本块。")

# 4. 文本嵌入
# 使用 HuggingFace 上的预训练模型将文本块转换为向量
# 'all-MiniLM-L6-v2' 是一个轻量级且效果不错的模型
print("生成文本嵌入...")
embeddings_model_name = "sentence-transformers/all-MiniLM-L6-v2"
embeddings = HuggingFaceEmbeddings(model_name=embeddings_model_name)

# 5. 构建向量数据库
# 使用 FAISS 将嵌入向量化并存储,以便进行快速相似性搜索
print("构建向量数据库...")
# FAISS.from_documents 会自动处理嵌入和索引
vector_store = FAISS.from_documents(texts, embeddings)
print("向量数据库构建完成。")

# --- 与 DeepSeek 大模型集成 ---

# 6. 初始化 DeepSeek LLM
# 使用 DeepSeek 的聊天模型
# 注意:model_name 可能需要根据 DeepSeek 官方文档更新
# 常见的模型如 'deepseek-chat' 或 'deepseek-coder' (如果你用coder模型)
print("初始化 DeepSeek LLM...")
llm = ChatDeepseek(
    model="deepseek-chat", # 或者其他 DeepSeek 模型,如 "deepseek-coder"
    api_key=api_key,
    temperature=0.1 # 控制生成文本的随机性,较低的值使输出更确定
)
print("DeepSeek LLM 初始化完成。")

# 7. 创建 RAG 链 (RetrievalQA)
# RetrievalQA 链封装了检索、构建提示和调用 LLM 的整个过程

# 定义一个Prompt模板 (可选,但推荐用于更好地控制输出)
prompt_template = """
请根据以下提供的上下文信息来回答问题。
如果你在上下文中找不到答案,请说你不知道,不要试图编造答案。
保持答案简洁。

上下文:
{context}

问题: {question}

答案:
"""
PROMPT = PromptTemplate(
    template=prompt_template, input_variables=["context", "question"]
)

chain_type_kwargs = {"prompt": PROMPT}

print("创建 RAG 链...")
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff", # "stuff" 是最简单的方法,将所有检索到的文本块直接塞入上下文
    retriever=vector_store.as_retriever(search_kwargs={"k": 3}), # k=3 表示检索最相关的3个文档块
    chain_type_kwargs=chain_type_kwargs,
    return_source_documents=True # 同时返回源文档,方便溯源
)
print("RAG 链创建完成。")

# 8. 进行提问
print("\n--- 开始提问 ---")
while True:
    user_query = input("\n请输入你的问题 (输入 '退出' 来结束程序): ")
    if user_query.lower() == '退出':
        break
    if not user_query.strip():
        print("问题不能为空,请重新输入。")
        continue

    print(f"\n正在处理问题: {user_query}")
    try:
        result = qa_chain.invoke({"query": user_query}) # LangChain 0.1.0+ 使用 invoke

        print("\n模型回答:")
        print(result["result"])

        print("\n引用的源文档片段:")
        for i, source_doc in enumerate(result["source_documents"]):
            print(f"--- 片段 {i+1} (来自: {source_doc.metadata.get('source', '未知来源')}) ---")
            print(source_doc.page_content)
            print("-" * 20)

    except Exception as e:
        print(f"处理问题时发生错误: {e}")

print("\n程序已退出。")

详细解释

  1. 加载环境变量 (load_dotenv, os.getenv):

    • .env 文件安全地加载 DEEPSEEK_API_KEY。这是保护敏感信息的良好实践。
  2. 加载知识库文档 (DirectoryLoader, TextLoader):

    • DirectoryLoader 用于从指定目录加载所有匹配 glob 模式(这里是 *.txt)的文件。
    • loader_cls=TextLoader 指定用 TextLoader 来处理每个找到的文件。
    • loader_kwargs={'encoding': 'utf-8'} 确保以 UTF-8 编码读取文件,避免中文乱码。
  3. 文本分割 (RecursiveCharacterTextSplitter):

    • LLM 的上下文窗口长度有限,且对较短、集中的文本块进行嵌入效果更好。
    • RecursiveCharacterTextSplitter 会尝试按特定字符(如 \n\n, \n, )递归地分割文本。
    • chunk_size=500: 每个文本块的最大字符数。
    • chunk_overlap=50: 块之间的重叠字符数,有助于保持语义的连续性,避免重要信息在分割点被切断。
  4. 文本嵌入 (HuggingFaceEmbeddings):

    • 嵌入是将文本转换为高维向量的过程,使得语义相似的文本在向量空间中也相近。
    • HuggingFaceEmbeddings 使用 sentence-transformers 库从 Hugging Face Hub 下载并运行指定的嵌入模型(这里是 sentence-transformers/all-MiniLM-L6-v2,一个流行且高效的模型)。这些嵌入是在本地计算的。
    • 备选方案:DeepSeek 可能也提供自己的嵌入 API。如果使用其 API,则需要替换此步骤,调用 DeepSeek 的嵌入接口,这可能带来与 DeepSeek LLM 更一致的语义理解。
  5. 构建向量数据库 (FAISS):

    • FAISS 是一个用于高效相似性搜索和稠密向量聚类的库。
    • FAISS.from_documents(texts, embeddings) 这个便捷方法会:
      • 对所有分割后的 texts(文档块)使用提供的 embeddings 模型生成向量。
      • 将这些向量和原始文本块一起存储在 FAISS 索引中。
    • 现在,我们可以用一个新的查询向量来快速找到数据库中最相似的(即最相关的)文本块。
  6. 初始化 DeepSeek LLM (ChatDeepseek):

    • ChatDeepseek 是 LangChain 中与 DeepSeek 聊天模型交互的类。
    • model="deepseek-chat": 指定要使用的 DeepSeek 模型。请查阅 DeepSeek 官方文档获取最新的可用模型名称 (如 deepseek-chat, deepseek-coder 等)。
    • api_key=api_key: 传入之前加载的 API Key。
    • temperature=0.1: 控制模型输出的创造性/随机性。较低的温度(如0.1-0.3)使输出更具确定性和事实性,适合 RAG 场景。较高的温度(如0.7-1.0)则更具创造性。
  7. 创建 RAG 链 (RetrievalQA):

    • RetrievalQA 是 LangChain 提供的一个标准链,它将检索器(Retriever)和 LLM 组合在一起。
    • llm=llm: 指定我们初始化的 DeepSeek LLM。
    • chain_type="stuff": 这是最直接的链类型。它获取所有检索到的文档,将它们的内容“塞入”(stuff)到提示中,然后将这个组合的提示传递给 LLM。其他链类型如 map_reduce, refine, map_rerank 用于处理大量文档或需要更复杂处理的场景。
    • retriever=vector_store.as_retriever(search_kwargs={"k": 3}):
      • vector_store.as_retriever(): 将我们的 FAISS 向量数据库转换为一个 LangChain Retriever 对象。
      • search_kwargs={"k": 3}: 配置检索器在每次查询时返回最相关的 k=3 个文档块。可以根据需求调整 k 的值。
    • prompt_templatePROMPT: 定义了一个自定义的提示模板。这非常重要,因为它指导 LLM 如何利用提供的上下文来回答问题,并指示其在信息不足时如何回应(例如,“如果你在上下文中找不到答案,请说你不知道”)。{context}{question} 是占位符,RetrievalQA 链会自动填充它们。
    • chain_type_kwargs={"prompt": PROMPT}: 将自定义提示应用到 stuff 链。
    • return_source_documents=True: 使链在返回 LLM 生成的答案(result)的同时,也返回检索到的源文档片段(source_documents)。这对于调试和验证答案来源非常有用。
  8. 进行提问 (qa_chain.invoke):

    • 在一个循环中接收用户输入。
    • qa_chain.invoke({"query": user_query}) (在 LangChain 0.1.0+ 版本中,旧版为 qa_chain({"query": user_query})qa_chain.run(user_query)):
      1. 用户的 user_query 首先被传递给 retriever
      2. retriever 使用嵌入模型将 user_query 转换为向量,并在 FAISS 向量数据库中执行相似性搜索,找到 k=3 个最相关的文档块。
      3. 这些文档块的内容(作为 context)和原始 user_query(作为 question)被填充到 PROMPT 模板中。
      4. 最终形成的完整提示被发送给 DeepSeek LLM (llm)。
      5. LLM 根据提供的上下文和问题生成答案。
    • 打印 LLM 的回答 (result["result"]) 和引用的源文档 (result["source_documents"])。

如何运行

  1. 将上述 Python 代码保存为 simple_rag.py
  2. 确保你的 .env 文件和 knowledge_base 文件夹与 simple_rag.py 在同一目录下。
  3. 在终端中运行脚本: python simple_rag.py
  4. 程序会加载文档、构建索引,然后提示你输入问题。

进阶与优化方向

这个简单的 RAG 系统是一个很好的起点,但还有许多可以优化和扩展的地方:

  1. 更优的文本分割策略: 探索不同的 chunk_sizechunk_overlap,或使用基于语义的分割器。
  2. 更强的嵌入模型: all-MiniLM-L6-v2 是轻量级的,对于复杂任务,可以考虑更大更强的嵌入模型,或者 DeepSeek 官方提供的嵌入服务(如果可用)。
  3. 混合搜索 (Hybrid Search): 结合关键词搜索(如 BM25)和向量搜索,可能会提高检索效果。
  4. 重排 (Re-ranking): 在初步检索后,使用一个更复杂的模型(如 Cross-Encoder)对检索到的文档进行重排序,以提高最顶部结果的质量。
  5. 提示工程 (Prompt Engineering): 进一步优化提示模板,引导 LLM 更好地利用上下文。
  6. 上下文管理: 对于需要多轮对话的场景,需要管理对话历史和上下文。
  7. 处理找不到答案的情况: 更优雅地处理知识库中没有相关信息的情况。
  8. 异步处理与流式输出: 对于生产环境,异步处理和流式输出可以改善用户体验。
  9. 评估: 建立评估 RAG 系统性能的指标和流程(如 RAGAS 框架)。
  10. 用户界面: 使用 Streamlit 或 Gradio 为 RAG 系统创建一个简单的 Web UI。

总结

通过 LangChain、Sentence Transformers、FAISS 和 DeepSeek API,我们成功构建了一个基础的 RAG 系统。这个系统能够从本地知识库中检索信息,并利用 DeepSeek 大模型的强大能力生成基于上下文的回答。RAG 是增强 LLM 应用能力的关键技术,希望本文能为你探索更高级的 AI 应用打下坚实的基础。