18
0
0

基于LangChain的RAG实现

2026-09-19
2026-09-19
基于LangChain的RAG实现
文章摘要
|

基于LangChain的RAG实现

本文使用最小四步构建RAG,最小可行系统采用数据准备、索引建立、检索优化和生成集成四步。下面就采用这个最小系统实现基于LangChain的RAG应用。

一、初始化设置

在这一步需要导入必要的Python库、加载环境变量以及加载嵌入模型。

import os
# os.environ['HF_ENDPOINT'] = 'https://hf-mirror.com'
from dotenv import load_dotenv
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_core.vectorstores import InMemoryVectorStore
from langchain_core.prompts import ChatPromptTemplate
from langchain_deepseek import ChatDeepSeek

# 加载环境变量
load_dotenv(override=True)

二、准备数据

  • 加载原始文档:

  1. 定义文档路径

  2. 加载文档

markdown_path = "./text.md"
loader = TextLoader(markdown_path, encoding="utf-8")
docs = loader.load()
  • 文本分块

为了便于后续的嵌入和检索,长文档被分割成较小的、可管理的文本块(chunks)。这里采用了递归字符分割策略,使用其默认参数进行分块。当不指定参数初始RecursiveCharacterTextSplitter()时,其默认行为旨在最大程度保留文本的语义结构:

  1. 默认分隔符与语义保留: 按顺序尝试使用一系列预设的分隔符 [“\n\n” (段落), “\n” (行), " " (空格), “” (字符)] 来递归分割文本。这种策略的目的是尽可能保持段落、句子和单词的完整性,因为它们通常是语义上最相关的文本单元,直到文本块达到目标大小。

  2. 保留分隔符: 默认情况下 (keep_separator=True),分隔符本身会被保留在分割后的文本块中。
    默认块大小与重叠: 使用其基类 TextSplitter 中定义的默认参数 chunk_size=4000(块大小)和 chunk_overlap=200(块重叠)。这些参数确保文本块符合预定的大小限制,并通过重叠来减少上下文信息的丢失。

text_splitter = RecursiveCharacterTextSplitter()
texts = text_splitter.split_documents(docs)

三、索引构建

数据准备完成之后,就是构建向量索引。

  • 初始化中文嵌入模型: 使用HuggingFaceEmbeddings加载之前在初始化设置中下载的中文嵌入模型。配置模型在CPU上运行,并启用嵌入归一化 (normalize_embeddings: True)。

embeddings = HuggingFaceEmbeddings(
    model_name="BAAI/bge-small-zh-v1.5",
    model_kwargs={'device': 'cpu'},
    encode_kwargs={'normalize_embeddings': True}
)
  • 构建向量存储: 将分割后的文本块 (texts) 通过初始化好的嵌入模型转换为向量表示,然后使用InMemoryVectorStore将这些向量及其对应的原始文本内容添加进去,从而在内存中构建出一个向量索引。

vectorstore = InMemoryVectorStore(embeddings)
vectorstore.add_documents(texts)

这个过程完成后,便构建了一个可供查询的知识索引。

四、查询与检索

构建向量之后,便可以争对加载的文档进行提问:

  • 定义问题

question = "强化学习是什么"
  • 查询文档
    使用向量存储的similarity_search方法,根据用户问题在索引中查找最相关的 k (此处示例中 k=3) 个文本块。

retrieved_docs = vectorstore.similarity_search(question, k=3)
  • 准备上下文
    将检索到的多个文本块的页面内容 (doc.page_content) 合并成一个单一的字符串,并使用双换行符 ("\n\n") 分隔各个块,形成最终的上下文信息 (docs_content) 供大语言模型参考。

docs_content = "\n\n".join(doc.page_content for doc in retrieved_docs)

使用 “\n\n” (双换行符) 而不是 “\n” (单换行符) 来连接不同的检索文档块,主要是为了在传递给大型语言模型(LLM)时,能够更清晰地在语义上区分这些独立的文本片段。双换行符通常代表段落的结束和新段落的开始,这种格式有助于LLM将每个块视为一个独立的上下文来源,从而更好地理解和利用这些信息来生成回答。

五、生成集成

最后一步就是将检索到的上下文与用户问题结合,利用大模型生成答案:

  • 构建提示词模板
    使用ChatPromptTemplate.from_template创建一个结构化的提示模板。此模板指导LLM根据提供的上下文 (context) 回答用户的问题 (question),并明确指出在信息不足时应如何回应。

prompt = ChatPromptTemplate.from_template("""请根据下面提供的上下文信息来回答问题。
请确保你的回答完全基于这些上下文。
如果上下文中没有足够的信息来回答问题,请直接告知:“抱歉,我无法根据提供的上下文找到相关信息来回答此问题。”
  
上下文:
{context}

问题: {question}

回答:"""
)
  • 配置大模型
    初始化 ChatDeepSeek 客户端,配置所用模型(deepseek-flash)、生成答案的温度参数(temperature=0.7)、最大Token数 (max_tokens=2048) 以及API密钥(从环境变量加载)和 url。

llm = ChatDeepSeek(
    model=os.getenv("DEEPSEEK_API_MODEL"),
    temperature=0.7,
    max_tokens=2048,
    api_key=os.getenv("DEEPSEEK_API_KEY"),
    base_url=os.getenv("DEEPSEEK_BASE_URL"),
)
  • 生成答案
    将用户问题 (question) 和先前准备好的上下文 (docs_content) 格式化到提示模板中,然后调用ChatDeepSeek的invoke方法获取生成的答案。

answer = llm.invoke(prompt.format(question=question, context=docs_content))
print(answer)

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!

评论