什么是RAG?

简单来说,RAG就是让大模型在回答问题之前先"查资料"。传统的模型只能依靠训练时学到的知识,而RAG可以让模型实时检索外部知识库,获取最新、最准确的信息,然后基于这些信息生成答案。这样既解决了模型知识更新的问题,又能避免模型"胡编乱造"。

一、加载文档

from langchain_community.document_loaders import PyPDFLoader

loader = PyPDFLoader("file2.pdf")
docs = loader.load()
print(docs[0].page_content)

这是RAG流程的起点。使用PyPDFLoader加载了一个PDF文件,把文档内容读入内存。docs是一个包含多个Document对象的列表,每个Document对象都有page_content(文本内容)和metadata(元数据)两个属性。通过打印第一个文档的内容,我可以确认文件是否加载成功。

from langchain_community.document_loaders import WikipediaLoader
loader = WikipediaLoader(query="周杰伦", load_max_docs=3, lang="zh")
docs = loader.load()
# 查看第一个Document元素的文本内容
print(docs[1].page_content)

代码中注释掉的WikipediaLoader部分展示了另一个数据源——可以从维基百科加载"周杰伦"的相关信息。这说明RAG的数据源可以是多种多样的,本地文件、网页、百科都可以。

二、文本分割

from langchain_text_splitters import RecursiveCharacterTextSplitter

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
    separators=["\n\n", "\n", "。", "!", "?", ",", "、", ""]
)

texts = text_splitter.split_documents(docs)
print(texts[0].page_content)

加载完文档后,直接使用整个文档会有问题。大模型对输入长度有限制,而且检索时如果文档太大,可能会包含太多无关信息。所以需要对文档进行分割。

这里设置了chunk_size=500,意思是每个文本块大约500个字符。chunk_overlap=50让相邻的块有50个字符的重叠,这样可以避免在分割点处切断重要的语义信息。separators参数定义了分割的优先级,从段落、句子到词语,确保分割尽可能保持语义完整。

三、文本向量化

from langchain_openai import OpenAIEmbeddings
from langchain_community.embeddings import DashScopeEmbeddings

embeddings = DashScopeEmbeddings(
    model="text-embedding-v2",
    dashscope_api_key="sk********************"
)

text = "This is a test document."
query_result = embeddings.embed_query(text)
print("文本向量长度:", len(query_result

计算机无法直接理解文本,需要把文字转换成数字——这就是向量化。这段代码使用阿里云DashScope的嵌入模型,把文本转换成向量。

关键代码解析:

  • embed_query():将单个查询文本转为向量

  • embed_documents():将多个文档批量转为向量

运行结果会显示向量的维度,比如1536维。text-embedding-v3模型支持指定向量维度,这在存储和计算效率上会更有优势。

四、构建向量库和检索

from langchain_community.vectorstores import FAISS

db = FAISS.from_documents(texts, embeddings_model)
retriever = db.as_retriever()
retrieved_docs = retriever.invoke("是否可以使用闪光灯")
print(retrieved_docs[0].page_content)

这是RAG的核心步骤。我使用FAISS(Facebook AI Similarity Search)创建了一个向量数据库,把之前分割好的文档块和对应的向量存入其中。

当用户提问时,系统会:

  • 把问题向量化
  • 在FAISS中搜索最相似的文档向量
  • 返回最相关的文档块

在这个例子中,我询问"是否可以使用闪光灯",检索器从《卢浮宫.pdf》中找出了相关内容,打印出最相关的那段文本。

五、带记忆的对话系统

更进阶RAG,带记忆功能的对话式RAG系统。这就像是给我们的RAG系统装上了"大脑",让它能够记住对话的上下文,实现真正的多轮对话。

1. 导入必要的模块

from langchain.chains import ConversationalRetrievalChain  # 对话检索链,RAG的核心
from langchain.memory import ConversationBufferMemory      # 记忆组件,存储对话历史
from langchain_community.document_loaders import PyPDFLoader  # PDF加载器
from langchain_community.vectorstores import FAISS         # 向量数据库
from langchain_openai import ChatOpenAI                    # 大模型接口
from langchain_community.embeddings import DashScopeEmbeddings  # 阿里云向量模型
from langchain_text_splitters import RecursiveCharacterTextSplitter  # 文本分割器

这里导入了构建RAG系统所需的所有组件。特别注意ConversationalRetrievalChain是专门为对话设计的检索增强生成链,它整合了检索、记忆和生成三大功能。

2. 加载PDF文档

loader = PyPDFLoader("./卢浮宫.pdf")
docs = loader.load()

使用PyPDFLoader加载本地的"卢浮宫.pdf"文件。load()方法返回一个Document对象列表,每个Document包含page_content(文本内容)和metadata(元数据,如页码等)。

3. 文本分割

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,        # 每个文本块大小500字符
    chunk_overlap=50,      # 相邻块重叠50字符,避免切断语义
    separators=["\n", "。", "!", "?", ",", "、", ""]  # 分割优先级
)
texts = text_splitter.split_documents(docs)

将长文档分割成适合检索的小块。为什么需要重叠?假设一句话被切在两块之间,重叠能保证这句话至少完整地出现在某一个块中。

4. 创建向量数据库

embeddings_model = DashScopeEmbeddings(
    model="text-embedding-v2",
    dashscope_api_key="sk-450489d77b3c46f28ebc4f9ac8403e19",
)
db = FAISS.from_documents(texts, embeddings_model)  # 灌库
retriever = db.as_retriever()
  • DashScopeEmbeddings:使用阿里云的向量模型,将文本转换成向量

  • FAISS.from_documents:创建向量数据库,把文档块和对应的向量存入FAISS索引

  • as_retriever():将向量数据库包装成检索器,方便后续调用

5. 初始化大模型

model = ChatOpenAI(
    model="qwen3.5-plus",
    openai_api_key="sk-********************",
    openai_api_base="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

这里有个巧妙之处:虽然用的是ChatOpenAI类,但实际上调用的是阿里云通义千问的API。因为阿里云提供了兼容OpenAI接口的服务,所以可以用同一套代码调用不同的模型。

6. 设置记忆组件

memory = ConversationBufferMemory(
    return_messages=True,      # 以消息列表形式返回历史
    memory_key='chat_history', # 在prompt中使用的变量名
    output_key='answer'        # 指定哪个输出需要保存
)

记忆组件是对话系统的灵魂。它会在每次对话后自动保存问题和答案,在下一次对话时把这些历史信息加入到prompt中,让模型理解上下文。

7. 构建对话检索链

qa = ConversationalRetrievalChain.from_llm(
    llm=model,
    retriever=retriever,
    memory=memory
)

将三大核心组件组装在一起:

  • llm:负责生成回答

  • retriever:负责检索相关文档

  • memory:负责管理对话历史

8. 开始多轮对话

question = "卢浮宫这个名字怎么来的?"
print(qa.invoke({"chat_history": memory, "question": question}))

question = "对应的拉丁语是什么呢?"
print(qa.invoke({"chat_history": memory, "question": question}))

这就是带记忆的RAG最神奇的地方!当问"对应的拉丁语是什么呢?",系统会自动理解这里的"对应的"指的是上一个问题中讨论的"卢浮宫"这个名字。因为记忆组件已经把第一轮对话的内容传递给模型了。

注意看invoke的参数:虽然传入了"chat_history": memory,但实际上ConversationalRetrievalChain会自动从memory对象中获取历史消息。这是LangChain的一个特性。

9. 带来源显示的版本

qa = ConversationalRetrievalChain.from_llm(
    llm=model,
    retriever=retriever,
    memory=memory,
    return_source_documents=True  # 显示回答参考的原片段
)

question = "卢浮宫博物馆位于什么地方?"
print(qa.invoke({"chat_history": memory, "question": question}))

设置return_source_documents=True后,返回结果中会包含source_documents字段,告诉我们这个回答是基于哪些原始文档块生成的。这在需要验证答案准确性时特别有用。

通过这个代码示例,我们看到了一个完整的、带记忆功能的RAG系统是如何工作的。它不再是简单的"检索+生成",而是"检索+记忆+生成"的有机结合。正是这个记忆组件,让机器能够像人一样理解对话的上下文,实现真正自然的交流。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐