LLM RAG 检索增强生成原理与应用详解
本文详细解析了 LLM RAG(检索增强生成)的工作原理与实现路径。文章首先阐述了 RAG 解决大模型知识滞后、幻觉及领域适配问题的核心价值。随后深入技术细节,涵盖文档加载、文本分割策略、向量化模型选择、向量数据库存储及多种数据召回算法。文中还补充了混合检索、查询重写等优化方案,以及评估指标与延迟成本控制等工程实践要点,为构建高质量 RAG 应用提供了完整的技术参考。

本文详细解析了 LLM RAG(检索增强生成)的工作原理与实现路径。文章首先阐述了 RAG 解决大模型知识滞后、幻觉及领域适配问题的核心价值。随后深入技术细节,涵盖文档加载、文本分割策略、向量化模型选择、向量数据库存储及多种数据召回算法。文中还补充了混合检索、查询重写等优化方案,以及评估指标与延迟成本控制等工程实践要点,为构建高质量 RAG 应用提供了完整的技术参考。

2024 年随着大模型进一步增强升级,越来越多的大模型应用落地。经过初期的探索和研究,目前业界逐渐收敛聚焦于两个主要的应用方向:RAG(检索增强生成)和 Agents(智能体)。今天我们就来先聊聊这个 RAG。
RAG:全称 Retrieval-Augmented Generation,即检索增强生成。我们知道由 ChatGPT 掀起的 LLM 大模型浪潮,其核心就是 Generation(生成),而 Retrieval-augmented 就是指除了 LLM 本身已经学到的知识之外,通过外挂其他数据源的方式来增强 LLM 的能力。这其中就包括了外部向量数据库、外部知识图谱、文档数据、Web 数据等。
[图示:RAG 系统架构流程图]
如上图所示,经过 Doc Loader 加载各种数据源的数据,经过 Embedding 向量化后存储进向量数据库。这是 Retrieval-augmented 基础数据处理流程。用户通过 QA 向 LLM 提问,会通过 QA 问题向向量数据库召回相似度较高的上下文,通过 Prompt 提示词一起发给 LLM,LLM 通过问题与上下文一起生成答案返回给用户。
我们不禁会问,为什么大模型动不动就千亿参数级别,涵盖了 PB 级的数据,还需要自己外挂数据源?这里面主要有几方面的原因:
好,我们了解了 RAG 的基本概念,接下来我们就一起深入技术细节,了解 RAG 的实现原理。
RAG 首先要解决的问题是数据来源的问题。数据有多种来源,各种格式的数据,如 CSV、HTML、JSON、Markdown、PDF。所有的这些数据都需要有对应的 Document Loaders 来进行加工处理,将信息正确提取出来。
以 LangChain(LLM 应用框架)为例,目前 LangChain 社区中已经实现了多种文档加载器。例如 HTML 加载器:
from langchain_community.document_loaders import UnstructuredHTMLLoader
loader = UnstructuredHTMLLoader("example_data/fake-content.html")
data = loader.load()
可以看到目前 LangChain 社区涵盖了国内网诸多网站和平台的数据,如百度云盘、腾讯云文档,甚至包括了区块链信息。
加载完数据后,下一步通常需要将数据进行拆分,尤其是在处理长文本的情况下。如何将文本进行分割处理,听起来很简单,比如按 400 个字符直接切片就好了,但往往这样应用效果不甚理想。
我们通常希望能将语义相关的文本片段保留在一起。重点其实就在这个'语义相关'。比如中文,我们希望是句号为分割符;比如一段长代码,我们希望以编程语言特点来分割,比如 Python 中的 def、class。
以 LangChain 为例,LangChain 目前支持 HTML、字符、MarkdownHeader 和多种代码分割,甚至正在实验中的语义分割。
from langchain.text_splitter import MarkdownHeaderTextSplitter
markdown_document = "# Foo\n\n ## Bar\n\nHi this is Jim\n\nHi this is Joe\n\n ### Boo \n\n Hi this is Lance \n\n ## Baz\n\n Hi this is Molly"
headers_to_split_on = [
("#", "Header 1"),
("##", "Header 2"),
("###", "Header 3"),
]
markdown_splitter = MarkdownHeaderTextSplitter(headers_to_split_on=headers_to_split_on)
md_header_splits = markdown_splitter.split_text(markdown_document)
md_header_splits
from langchain_experimental.text_splitter import SemanticChunker
from langchain_openai.embeddings import OpenAIEmbeddings
# This is a long document we can split up.
with open("../../state_of_the_union.txt") as f:
state_of_the_union = f.read()
text_splitter = SemanticChunker(OpenAIEmbeddings())
docs = text_splitter.create_documents([state_of_the_union])
print(docs[0].page_content)
在进行文本分割的同时,我们还可以给分割的文本添加一下 Metadata 的数据,方便记录该文本段的一些基本信息,如文章来源、作者信息等。 一个是能在进行文本召回时可以作为过滤搜索,另一方面还在作为发给 LLM 的补充数据,让 LLM 生成的内容更为丰富。
metadatas = [{"document": 1}, {"document": 2}]
documents = text_splitter.create_documents(
[state_of_the_union, state_of_the_union], metadatas=metadatas
)
print(documents[0])
在进行文本分割时,我们还需要重点关注两个参数 chunk_size 和 chunk_overlap,这两个参数分别表示分割长度和两段分割文本重合长度。
在实际 RAG 应用中,chunk_size 需要结合向量数据库的来选择合适大小,比如腾讯云的向量数据库,一次只支持单块 512token(400 左右字符)的大小写入,那 chunk_size 就应该设置 400 多。chunk_overlap 的大小建议设置在 chunk_size 的 1/5 左右,在召回多段文本时,可以增加数据的丰富度。
实际情况请结合具体项目进行设置和测试验证。
在进行数据分割后,需要对文本数据段进行向量化。目前主流的中文向量化模型有:
| 模型 | 中文支持 |
|---|---|
| M3E | 是 |
| text2vec | 是 |
| OpenAIEmbeddings | 是 |
使用 OpenAIEmbeddings 向量化处理:
from langchain_openai import OpenAIEmbeddings
embeddings_model = OpenAIEmbeddings(openai_api_key="...")
embeddings = embeddings_model.embed_documents(
[
"Hi there!",
"Oh, hello!",
"What's your name?",
"My friends call me World",
"Hello World!"
]
)
len(embeddings), len(embeddings[0])
目前 LangChain 支持 37 种 embedding model,这些向量化模型核心功能就是将文本向量化,提供给向量数据库进行存储。
[图示:向量数据库存储示意图]
数据向量化后,就需要将向量数据存储进向量数据库。目前有很多开源向量数据库,如 ChromaDB、Faiss-CPU、LanceDB。云服务厂商也陆续推出了向量数据库服务,包括腾讯云、阿里云的向量数据库。
LanceDB 向量数据库使用:
from langchain_community.document_loaders import TextLoader
from langchain_openai import OpenAIEmbeddings
from langchain.text_splitter import CharacterTextSplitter
from langchain_community.vectorstores import LanceDB
import lancedb
db = lancedb.connect("/tmp/lancedb")
table = db.create_table(
"my_table",
data=[
{
"vector": embeddings.embed_query("Hello World"),
"text": "Hello World",
"id": "1",
}
],
mode="overwrite",
)
# Load the document, split it into chunks, embed each chunk and load it into the vector store.
raw_documents = TextLoader('../../../state_of_the_union.txt').load()
text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=0)
documents = text_splitter.split_documents(raw_documents)
db = LanceDB.from_documents(documents, OpenAIEmbeddings(), connection=table)
如以 LangChain VectorStore 为基础类,实现的支持腾讯云的向量数据库服务。 目前 LangChain 支持 47 种向量数据库接入,开发者也可以自行实现 VectorStore,定义自己的向量数据库。 主要实现以下三个抽象方法:
@abstractmethod
def add_texts(
self,
texts: Iterable[str],
metadatas: Optional[List[dict]] = None,
**kwargs: Any,
) -> List[str]:
"""Run more texts through the embeddings and add to the vectorstore."""
@abstractmethod
def similarity_search(
self, query: str, k: int = 4, **kwargs: Any
) -> List[Document]:
"""Return docs most similar to query."""
@classmethod
@abstractmethod
def from_texts(
cls: Type[VST],
texts: List[str],
embedding: Embeddings,
metadatas: Optional[List[dict]] = None,
**kwargs: Any,
) -> VST:
"""Return VectorStore initialized from texts and texts."""
add_texts: 将文本数据向量化,添加进向量数据库。similarity_search: 从向量数据库召回数据。from_texts: 类方法,实现将文本数据向量化,添加进向量数据库。在讲解完数据加载、数据处理、数据向量化和向量数据库后,我们开始进入数据召回的环节。数据召回是我们向 LLM 提问时,需要根据我们提问的问题向向量数据库召回相关的文档数据,并和问题加载进 Prompt 发送给 LLM。
比如下面这段提示词:
template = """Answer the question based only on the following context:
{context}
Question: {question}
"""
context 就是我们召回的上下文。
数据召回的方法有许多种,应用在不同应用场景。当前 LangChain 主流支持的 Retrievers 有以下 8 种:
| Name | Index Type | Uses an LLM | When to Use | Description |
|---|---|---|---|---|
| Vectorstore | Vectorstore | No | If you are just getting started and looking for something quick and easy. | This is the simplest method and the one that is easiest to get started with. It involves creating embeddings for each piece of text. |
| Vectorstore + Document Store | Vectorstore + Document Store | No | If your pages have lots of smaller pieces of distinct information that are best indexed by themselves, but best retrieved all together. | This involves indexing multiple chunks for each document. Then you find the chunks that are most similar in embedding space, but you retrieve the whole parent document and return that (rather than individual chunks). |
| Vectorstore + Document Store | Vectorstore + Document Store | Sometimes during indexing | If you are able to extract information from documents that you think is more relevant to index than the text itself. | This involves creating multiple vectors for each document. Each vector could be created in a myriad of ways - examples include summaries of the text and hypothetical questions. |
| Vectorstore | Vectorstore | Yes | If users are asking questions that are better answered by fetching documents based on metadata rather than similarity with the text. | This uses an LLM to transform user input into two things: (1) a string to look up semantically, (2) a metadata filer to go along with it. This is useful because oftentimes questions are about the METADATA of documents (not the content itself). |
| Any | Any | Sometimes | If you are finding that your retrieved documents contain too much irrelevant information and are distracting the LLM. | This puts a post-processing step on top of another retriever and extracts only the most relevant information from retrieved documents. This can be done with embeddings or an LLM. |
| Vectorstore | Vectorstore | No | If you have timestamps associated with your documents, and you want to retrieve the most recent ones | This fetches documents based on a combination of semantic similarity (as in normal vector retrieval) and recency (looking at timestamps of indexed documents) |
| Any | Any | Yes | If users are asking questions that are complex and require multiple pieces of distinct information to respond | This uses an LLM to generate multiple queries from the original one. This is useful when the original query needs pieces of information about multiple topics to be properly answered. By generating multiple queries, we can then fetch documents for each of them. |
| Any | Any | No | If you have multiple retrieval methods and want to try combining them. | This fetches documents from multiple retrievers and then combines them. |
这里简述一下不同 Retrievers 的主要应用场景,大家可以具体问题具体分析,再去查阅一下相关文档。
在数据召回中,目前业内有两种较为通用的召回算法。
COLLECTION_NAME = "state_of_the_union_test"
db = PGVector.from_documents(
embedding=embeddings,
documents=docs,
collection_name=COLLECTION_NAME,
connection_string=CONNECTION_STRING,
)
query = "What did the president say about Ketanji Brown Jackson"
docs_with_score = db.similarity_search_with_score(query)
for doc, score in docs_with_score:
print("-" * 80)
print("Score: ", score)
print(doc.page_content)
print("-" * 80)
docs_with_score = db.max_marginal_relevance_search_with_score(query)
for doc, score in docs_with_score:
print("-" * 80)
print("Score: ", score)
print(doc.page_content)
print("-" * 80)
算法 Python 实现:
def maximal_marginal_relevance(
query_embedding: np.ndarray,
embedding_list: list,
lambda_mult: float = 0.5,
k: int = 4,
) -> List[int]:
"""Calculate maximal marginal relevance."""
if min(k, len(embedding_list)) <= 0:
return []
if query_embedding.ndim == 1:
query_embedding = np.expand_dims(query_embedding, axis=0)
similarity_to_query = cosine_similarity(query_embedding, embedding_list)[0]
most_similar = int(np.argmax(similarity_to_query))
idxs = [most_similar]
selected = np.array([embedding_list[most_similar]])
while len(idxs) < min(k, len(embedding_list)):
best_score = -np.inf
idx_to_add = -1
similarity_to_selected = cosine_similarity(embedding_list, selected)
for i, query_score in enumerate(similarity_to_query):
if i in idxs:
continue
redundant_score = max(similarity_to_selected[i])
equation_score = (
lambda_mult * query_score - (1 - lambda_mult) * redundant_score
)
if equation_score > best_score:
best_score = equation_score
idx_to_add = i
idxs.append(idx_to_add)
selected = np.append(selected, [embedding_list[idx_to_add]], axis=0)
return idxs
我们可以在数据召回实践中,测试不同算法下的效果,来选择合适的算法。
在实际生产环境中部署 RAG 系统时,除了上述基础流程外,还需要关注以下几个关键挑战与优化点:
单纯的向量相似度搜索有时无法完全满足业务需求。为了提升检索质量,可以采用混合检索策略(Hybrid Search),结合关键词检索(BM25)和向量检索。关键词检索擅长处理专有名词和精确匹配,而向量检索擅长语义理解。两者加权融合通常能获得更好的召回率。
此外,查询重写(Query Rewriting)也是一种有效手段。当用户提问过于简短或模糊时,可以利用 LLM 将原始问题扩展为更详细的查询语句,再进行向量检索,从而提升上下文的相关性。
如何衡量 RAG 系统的效果至关重要。常用的评估指标包括:
可以通过构建测试集(Ground Truth),对比模型输出与标准答案的一致性来进行自动化评估。工具如 RAGAS 提供了端到端的评估框架。
RAG 涉及多次 API 调用(Embedding、Vector DB 查询、LLM 生成),这会导致响应延迟增加。优化措施包括:
同时,Token 消耗也是成本大头。通过优化 Prompt 设计,精简不必要的上下文信息,可以有效降低 Token 用量。
最后,我们对 RAG 进行一下总结。RAG 底层依赖 LLM 大模型和数据获取、数据存储等相关技术。在 RAG 技术层面基于底层技术,共实现了数据加载、数据处理、数据向量化、向量数据库和数据召回等五种核心技术。可以使用这 5 种技术,完成 RAG 应用实现。
综上所述,RAG 技术有效地解决了大模型的知识时效性和领域专业性不足的问题。通过合理的数据处理、高效的检索策略以及持续的优化评估,企业可以构建出既准确又实用的智能问答系统。随着技术的演进,RAG 将与 Agent 技术进一步融合,成为未来 AI 应用的核心基础设施之一。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online