-
语义分块: 利用算法(或LLM)识别文本中语义的自然断点,如段落、标题或一个完整的概念单元,确保每个被索引的文本块都“言之有物”。 -
句子分块: 以完整的句子为单位进行切分,这是最细粒度的语义单元。
-
原理:
-
索引时:我们将一份文档切分成许多小的“子文档”(比如单个句子),并对这些“子文档”进行向量化。同时,我们保留一份完整的、较大的“父文档”(比如整个段落或页面)。 -
检索时:我们用用户查询去匹配那些精细的“子文档”。 -
返回时:一旦命中某个“子文档”,我们不返回这个小片段,而是返回它所属的那个完整的“父文档”作为上下文。
-
优点:兼具检索的精准度(匹配小块)和上下文的完整性(返回大块),效果拔群。 -
原理:
-
对每一个文档块,我们调用LLM,反向生成几个用户可能会提出的、能够被这个文档块回答的问题。 -
在构建索引时,我们只对这些新生成的“代理问题”进行向量化。 -
同时,我们将这些“代理问题”全部链接到它们所源自的那个原始文档块的ID。 -
当用户提问时,系统会在“代理问题”的向量库中进行搜索。一旦匹配成功,系统不会返回这个代理问题,而是通过ID找到并返回那个包含完整答案的原始文档块。
from langchain.storage import InMemoryStorefrom langchain_core.documents import Documentfrom langchain.retrievers.multi_vector import MultiVectorRetrieverfrom langchain_core.output_parsers import StrOutputParserfrom langchain_core.prompts import ChatPromptTemplateimport uuiddocs = [Document(page_content="RAG-Fusion通过生成多个查询变体并使用RRF算法智能排序来提升检索相关性。", metadata={"doc_id": str(uuid.uuid4())}),Document(page_content="假设性文档嵌入(HyDE)先让LLM生成一个理想答案,再用该答案的嵌入来检索真实文档。", metadata={"doc_id": str(uuid.uuid4())}),]doc_ids = [doc.metadata["doc_id"] for doc in docs]question_gen_prompt_str = ("你是一位AI专家。请根据以下文档内容,生成3个用户可能会提出的、高度相关的问题。n""只返回问题列表,每个问题占一行,不要有其他前缀或编号。nn""文档内容:n""----------n""{content}n""----------n")question_gen_prompt = ChatPromptTemplate.from_template(question_gen_prompt_str)question_generator_chain = question_gen_prompt | llm | StrOutputParser()sub_docs = []for i, doc in enumerate(docs):doc_id = doc_ids[i]generated_questions = question_generator_chain.invoke({"content": doc.page_content}).split("n")generated_questions = [q.strip() for q in generated_questions if q.strip()]for q in generated_questions:sub_docs.append(Document(page_content=q, metadata={"doc_id": doc_id}))vectorstore_qa = Chroma.from_documents(documents=sub_docs, embedding=embeddings)doc_store = InMemoryStore()doc_store.mset(list(zip(doc_ids, docs)))multivector_retriever = MultiVectorRetriever(vectorstore=vectorstore_qa,docstore=doc_store,id_key="doc_id",)user_query = "RAG-Fusion是怎么工作的?"retrieved_qa_docs = multivector_retriever.invoke(user_query)
-
元数据: 为每个文档块打上丰富的“标签”(如来源、日期、作者、章节等),这能让你在检索时进行精确过滤,是实现企业级知识管理的基础。 -
图RAG: 对于高度结构化、关系复杂的知识(如组织架构、产品依赖关系),构建知识图谱能让RAG处理“A和B有什么关系?”这类多跳查询。
-
原理: 让LLM扮演“头脑风暴师”,根据用户的原始问题,自动生成多个不同角度、但语义相似的子问题。然后用所有问题去“围剿”答案,最后合并结果。 -
优点: 大幅提升召回率,尤其擅长处理模糊和多义性查询。
from langchain.retrievers import MultiQueryRetriever# 1. 从LLM和向量数据库创建一个MultiQueryRetriever# 它会自动处理“生成查询 -> 检索 -> 合并去重”的整个流程multiquery_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(), # 使用我们创建的向量数据库作为基础检索器 llm=llm # 使用我们初始化的LLM来生成子查询)# 2. 使用原始查询进行调用user_query = "如何通过修改问题来改进检索效果?"retrieved_docs = multiquery_retriever.invoke(user_query)# 3. 打印结果print_docs(retrieved_docs, f"查询扩展 (MultiQuery) 对 '{user_query}' 的检索结果")# 深入了解它生成了哪些子查询import logginglogging.basicConfig()logging.getLogger("langchain.retrievers.multi_query").setLevel(logging.INFO)retrieved_docs = multiquery_retriever.invoke(user_query)
-
原理: 这是MultiQueryRetriever的进化版。它同样生成多个子查询,但在合并结果时,使用倒数排序融合 (RRF) 算法,能智能地将那些在多次不同查询中都排名靠前的“共识”文档,提升到最前面。 -
优点: 相比简单合并,能更有效地筛选出最核心、最相关的结果。
from langchain_core.prompts import ChatPromptTemplatefrom langchain_core.output_parsers import StrOutputParserfrom langchain_core.documents import Documentimport operator# 1. 定义一个用于生成子查询的链 (Chain)query_gen_prompt = ChatPromptTemplate.from_messages([("user", "你是一位AI研究员。请根据以下问题,生成3个不同角度的、语义相似的查询。n""每个查询占一行,不要有其他前缀或编号。nn""原始问题: {original_question}")])generate_queries_chain = query_gen_prompt | llm | StrOutputParser() | (lambda x: x.split("n"))# 2. 定义RRF算法函数def reciprocal_rank_fusion(retrieval_results: list[list[Document]], k=60):fused_scores = {}for doc_list in retrieval_results:for rank, doc in enumerate(doc_list):doc_id = doc.page_contentif doc_id not in fused_scores:fused_scores[doc_id] = 0fused_scores[doc_id] += 1 / (k + rank)reranked_results = [next((doc for doc_list in retrieval_results for doc in doc_list if doc.page_content == doc_id), None)for doc_id, score in sorted(fused_scores.items(), key=operator.itemgetter(1), reverse=True)]return [doc for doc in reranked_results if doc is not None]def rag_fusion_pipeline(original_question: str):# 生成多个查询generated_queries = generate_queries_chain.invoke({"original_question": original_question})all_queries = [original_question] + generated_queriesprint(f"生成的查询: {all_queries}")# 独立检索每个查询retriever = vectorstore.as_retriever()retrieval_results = [retriever.invoke(q) for q in all_queries]# 应用RRF算法对结果进行融合和重排final_docs = reciprocal_rank_fusion(retrieval_results)return final_docs# 4. 调用user_query = "如何处理用户提问太具体的情况?"fusion_docs = rag_fusion_pipeline(user_query)print(fusion_docs, f"RAG-Fusion 对 '{user_query}' 的检索结果")
-
原理: 当用户问得太具体时,先让LLM“后退一步”,提炼出一个更概括、更高层的问题。然后用“具体问题”+“概括问题”一起检索,从而同时捕获细节与背景。 -
优点: 为LLM提供更全面的视角,避免因问题太专而找不到信息。
from langchain_core.runnables import RunnableParallel, RunnablePassthrough# 1. 定义生成“后退一步”问题的Prompt和链step_back_prompt_template = ChatPromptTemplate.from_messages([ ("user", "你是一位善于提炼核心问题的专家。请将以下可能很具体的问题," "抽象成一个更通用、更高层次的“后退一步”的问题。nn" "例如:'LangChain的LCEL和Python的asyncio库是如何交互的?' -> 'LangChain LCEL的异步执行机制是怎样的?'nn" "原始问题: {original_question}")])step_back_chain = step_back_prompt_template | llm | StrOutputParser()retriever = vectorstore.as_retriever()chain = ( { # 第一个分支:对原始问题进行检索 "original_docs": RunnablePassthrough() | retriever, # 第二个分支:先生成后退问题,再用它进行检索 "step_back_docs": step_back_chain | retriever, } # 将两个分支的结果合并、去重 | (lambda x: remove_duplicates_by_id(x["original_docs"] + x["step_back_docs"])))# 辅助函数去重def remove_duplicates_by_id(documents): seen_ids = set() unique_docs = [] for doc in documents: # 假设 page_content 是唯一标识 if doc.page_content not in seen_ids: unique_docs.append(doc) seen_ids.add(doc.page_content) return unique_docsuser_query = "RAG-Fusion里那个RRF算法的平滑参数k有什么用?"step_back_docs = chain.invoke({"original_question": user_query})
-
原理: 先让LLM根据用户问题“凭空”生成一个理想的、完美的答案。然后,用这个“假想答案”的向量去检索真实文档。 -
优点: “假想答案”在语义上无限接近最终答案,因此它的向量可以作为一枚精准的“语义导弹”,高效地命中目标文档。
-
原理: 将现代的向量搜索与传统的关键词搜索(稀疏检索,如BM25)结合起来。一个负责理解“意思”,一个负责锁定“词语”,取长补短。 -
优点: 极大地提升了检索的鲁棒性,在需要精确匹配和语义理解的场景下都能表现出色。LangChain的EnsembleRetriever就是为此而生。
# 示例: 使用 EnsembleRetriever 实现混合搜索from langchain.retrievers import EnsembleRetrieverfrom langchain_community.retrievers import BM25Retrieverfrom langchain.retrievers import EnsembleRetriever# 假设 all_splits 和 vectorstore 已准备好# 1. 初始化关键词检索器 (Sparse Retriever)bm25_retriever = BM25Retriever.from_documents(all_splits)bm25_retriever.k = 3 # 检索3个结果# 2. 初始化向量检索器 (Dense Retriever)vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})# 3. 初始化 EnsembleRetriever,并设置权重# weights 参数决定了最终排序时,两种检索器结果的权重ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4, 0.6] # 稍微偏重向量搜索的语义理解能力)# 4. 使用query = "LangChain中的LCEL是什么?"retrieved_docs = ensemble_retriever.invoke(query)print(f"混合搜索召回了 {len(retrieved_docs)} 个文档。")
在本篇文章中,我们探讨了多种用于优化RAG系统的检索机制,包括索引构建的最佳实践、多样的查询转换策略以及混合搜索的实现。这些技术旨在从根本上提升检索的准确性与召回率。
然而,获取初步的文档列表只是整个流程的第一步。这些结果在相关性上可能仍然参差不齐,包含了与问题不直接相关的噪音信息。因此,下一步的关键任务,就是如何对这些初步结果进行有效的后处理与筛选。


