大型语言模型(LLMs)如 GPT-4 和 Claude 功能强大,但存在一个根本性的局限:它们的知识在训练时就被冻结了。它们无法访问你的内部文档、数据库或实时信息。检索增强生成(RAG) 通过将 LLM 的生成能力与从外部来源检索信息的能力相结合,完美地解决了这个问题。
问题:LLM 的局限性
在谈论 RAG 之前,重要的是要理解为什么我们需要它。
- 静态知识:LLM 只知道训练期间看到的内容。如果你询问其截止日期之后发生的事件,它无法回答。
- 幻觉:当 LLM 不知道答案时,它倾向于编造一个,生成看似合理但完全虚假的信息。
- 无法访问私有数据:通用 LLM 无法访问你公司的内部文档、工单或代码库。
RAG 通过在查询时从外部来源检索相关上下文来解决这三个问题。
什么是 RAG?
检索增强生成是一种架构,它使用从外部知识库检索的信息来丰富发送给 LLM 的提示。RAG 不是仅依赖模型的参数化知识,而是先搜索相关信息,然后将其注入到提示中,使模型能够生成准确、有据可依的回答。
RAG 的详细工作原理
RAG 架构由两个主要阶段组成:索引(离线)和 检索 + 生成(在线)。
阶段 1:索引(文档摄取)
索引阶段为语义搜索准备文档,由四个步骤组成。
1. 文档加载
文档可以来自任何来源:PDF 文件、网页、数据库、Markdown 文件、API。文档加载器读取这些文档并将其转换为结构化文本。
2. 文本分割(Chunking)
LLM 的上下文窗口有限,而文档可能很长。文本分割器将文档分成更小的片段,称为 chunks。分块的质量至关重要:太小的块会丢失上下文,太大的块会稀释相关性。
最常见的策略包括:
- 递归字符分割:使用
\n\n、\n、.等分隔符递归分割文本,尊重文档结构。 - 语义分割:使用嵌入来找到文本中的自然断点。
- 块重叠:在连续块之间包含重叠部分,以保留边界处的上下文。
3. 嵌入
每个块通过嵌入模型(如 OpenAI 的 text-embedding-3-small)转换为数值向量(嵌入)。这些向量捕获文本的语义含义:含义相似的句子在多维空间中的向量会很接近。
4. 向量数据库
向量存储在向量数据库中,如 ChromaDB、Pinecone、Weaviate 或 FAISS。该数据库针对相似性搜索进行了优化:给定查询,找到最相似的向量(因此也是最相关的文本块)。
阶段 2:检索 + 生成
当用户提出问题时:
- 使用相同的嵌入模型将问题转换为嵌入。
- 向量数据库通过相似性搜索(通常是余弦相似度或欧几里得距离)找到最相似的块。
- 检索到的块作为上下文插入到提示中。
- LLM 根据提供的上下文生成回答。
使用 LangChain 构建 RAG 管道
LangChain 是最流行的用于构建基于 LLM 应用的 Python(和 JavaScript)框架。它为 RAG 管道的每个组件提供高级抽象。
安装
pip install langchain langchain-openai langchain-community chromadb
步骤 1:加载文档
LangChain 为不同的数据源提供了数十个文档加载器。
from langchain_community.document_loaders import ( PyPDFLoader, WebBaseLoader, DirectoryLoader, TextLoader, ) # 加载 PDF pdf_loader = PyPDFLoader("docs/manual.pdf") pdf_docs = pdf_loader.load() # 加载网页 web_loader = WebBaseLoader("https://docs.example.com/guide") web_docs = web_loader.load() # 从目录加载所有 .md 文件 dir_loader = DirectoryLoader("./knowledge_base", glob="**/*.md", loader_cls=TextLoader) md_docs = dir_loader.load() all_docs = pdf_docs + web_docs + md_docs
步骤 2:将文档分割成块
from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=["\n\n", "\n", ". ", " ", ""], ) chunks = text_splitter.split_documents(all_docs) print(f"原始文档: {len(all_docs)}, 块: {len(chunks)}")
chunk_overlap 参数至关重要:它在连续块之间创建重叠,使上下文不会在边界处丢失。
步骤 3:创建嵌入和向量数据库
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma embedding_model = OpenAIEmbeddings(model="text-embedding-3-small") vectorstore = Chroma.from_documents( documents=chunks, embedding=embedding_model, persist_directory="./chroma_db", )
步骤 4:创建检索器
检索器是给定查询后从向量数据库中获取最相关块的组件。
retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 4}, ) relevant_docs = retriever.invoke("身份验证如何工作?") for doc in relevant_docs: print(doc.page_content[:200]) print("---")
步骤 5:构建 RAG 链
现在让我们将所有内容与 LLM 和提示模板组合在一起。
from langchain_openai import ChatOpenAI from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser llm = ChatOpenAI(model="gpt-4o", temperature=0) prompt = ChatPromptTemplate.from_template(""" 仅根据提供的上下文回答问题。 如果上下文中没有足够的信息,请说你不知道。 上下文: {context} 问题:{question} 回答: """) def format_docs(docs): return "\n\n".join(doc.page_content for doc in docs) rag_chain = ( {"context": retriever | format_docs, "question": RunnablePassthrough()} | prompt | llm | StrOutputParser() ) response = rag_chain.invoke("系统中的身份验证如何工作?") print(response)
高级 RAG 技术
基本管道运行良好,但有几种技术可以显著提高回答质量。
多查询检索
有时用户的查询含糊不清,或与文档中使用的语言不一致。多查询检索器自动生成原始问题的变体,以捕获多个视角。
from langchain.retrievers import MultiQueryRetriever multi_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), llm=llm, ) docs = multi_retriever.invoke("安全最佳实践是什么?")
上下文压缩
并非块中的所有内容都与查询相关。上下文压缩检索器使用 LLM 从每个检索到的块中仅提取相关部分。
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor compressor = LLMChainExtractor.from_llm(llm) compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=retriever, )
混合搜索
纯语义搜索并不总是最优的。混合搜索将语义搜索(嵌入)与词汇搜索(BM25、关键词匹配)相结合,以获得更好的结果。
from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever bm25_retriever = BM25Retriever.from_documents(chunks) bm25_retriever.k = 4 semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) hybrid_retriever = EnsembleRetriever( retrievers=[bm25_retriever, semantic_retriever], weights=[0.4, 0.6], )
对话式 RAG(带记忆)
要构建一个能记住对话上下文的 RAG 聊天机器人,需要添加一个记忆组件,根据对话历史重新表述用户的问题。
from langchain.chains import create_history_aware_retriever from langchain_core.prompts import MessagesPlaceholder contextualize_prompt = ChatPromptTemplate.from_messages([ ("system", "根据聊天历史和用户的最新问题," "重新表述问题使其在没有历史记录的情况下也能被理解。"), MessagesPlaceholder("chat_history"), ("human", "{input}"), ]) history_aware_retriever = create_history_aware_retriever( llm, retriever, contextualize_prompt )
最佳实践
- 选择正确的块大小:尝试不同的大小(500-1500 个标记)。较小的块用于精确回答,较大的块用于更广泛的上下文。
- 使用文档元数据:将来源、日期和类别作为元数据添加到块中。这允许在检索期间过滤结果。
- 评估质量:使用 RAGAS 等框架来衡量 faithfulness、relevancy 和 context precision 等指标。
- 处理文档更新:实施重新摄取管道,使向量数据库与数据源保持同步。
- 添加重新排序器:在初始检索后,使用重新排序模型(如 Cohere Rerank)根据实际相关性重新排序结果。
结论
RAG 已成为构建需要访问特定且最新知识的 AI 应用的标准架构。LangChain 通过为管道的每个组件提供抽象,极大地简化了实现过程。
下一步:
- 本地实验:从 ChromaDB 和少量文档开始,熟悉管道流程。
- 探索 LangSmith:使用 LangSmith 在生产中监控和调试你的链。
- 尝试不同的嵌入模型:比较
text-embedding-3-small、text-embedding-3-large等模型以及 Sentence Transformers 的开源模型。 - 查阅文档:LangChain 文档 是一个优秀且持续更新的资源。