【本期目标】
- 理解 RAG 系统中常见的挑战(如幻觉、上下文冗余、检索不精确)及其原因。
- 掌握多种高级检索策略,覆盖检索前、检索中、检索后全流程,以提升召回率和相关性。
- 学习如何通过Prompt工程、输出解析等方法优化LLM的生成质量。
- 探讨RAG幻觉问题的成因与多种防范策略。
- 通过案例,实践这些高级优化方法,并观察其对RAG性能的影响。
引言:RAG的“痛点”与优化之路
即便构建了基本的RAG系统,你可能仍然会遇到以下问题 :
- 幻觉: LLM根据检索到的信息编造事实,或者生成与上下文不符的回答。
- 检索不精确: 检索器未能召回最相关或最完整的文档块,导致LLM无法获得足够信息。
- 上下文冗余: 检索到的文档块虽然相关,但包含了大量与问题不直接相关的细节,浪费LLM的Token。
- 生成质量不高: LLM的回答不够简洁、不符合用户期望格式,或引用不明确。
- 查询理解不足: 用户提问模糊、复杂,LLM难以准确理解其检索意图。
本期我们将重点解决这些痛点,让RAG系统更智能、更可靠,优化的核心思路,是围绕检索前、检索中和检索后三个阶段展开的系统性工程。
检索前优化 —— 从源头提升质量
在我们将问题喂给检索系统之前,可以做的优化是最基础、也最有效的。这部分主要包括两个方面:优化我们拥有的知识库数和优化用户提出的问题。
数据索引优化
问题: 基础RAG通常使用固定大小的文本块进行切割,这容易导致语义信息被割裂,或者相关的上下文分散在不同的块中。
a.智能分块
原理: 不同于按字符数或Token数切块,智能分块会根据文本的内在结构(如段落、标题)或语义完整性来切分文档。
语义分块: 尝试在语义的断点处进行分割,确保每个块包含一个完整的思想或概念单元。
句子窗口检索: 一种更精细的策略。索引时,以单个句子为单位创建嵌入。当检索到某个句子时,将该句子及其前后的句子一并作为上下文提供给LLM。这样既能保证检索的精准度,又能提供丰富的上下文。
优点:
最大程度地保留了每个文本块的语义完整性。
句子窗口策略能实现精确检索和充分上下文的平衡。
b.元数据与图谱构建
原理: 在索引时,为每个文档块添加丰富的元数据(如文档来源、创建日期、章节标题、作者等)。对于高度结构化的知识,甚至可以构建知识图谱(后面会专门写一篇关于知识图谱的文章)。
优点:
元数据可以用于检索时的精确过滤(例如,只检索2024年之后的文档)。LangChain的SelfQueryRetriever就擅长利用元数据。
知识图谱能处理多跳、复杂的关系查询,为RAG提供结构化的先验知识。
查询优化:更好地理解用户意图
用户的原始问题可能很模糊,或者包含多个意图。直接用它检索效果可能不佳。
a.查询扩展 : MultiQueryRetriever
- 问题: 用户原始的查询可能不够全面或精准,导致检索器遗漏相关文档。
- 原理: 使用LLM根据原始查询生成多个语义相近或不同角度的查询。 然后,用所有这些查询并行地执行检索,并将所有结果合并去重。
- 优点: 显著提升召回率,尤其适用于模糊或多义性查询。
- 缺点: 增加LLM调用次数和检索成本。
from langchain.retrievers import MultiQueryRetriever from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_chroma import Chroma import os from dotenv import load_dotenv; load_dotenv() llm = ChatOpenAI( model=os.environ.get("OPENAI_MODEL"), temperature=0.9, base_url=os.environ.get("OPENAI_BASE_URL"), openai_api_key=os.environ.get("OPENAI_API_KEY"), ) embeddings_model = OpenAIEmbeddings( model=os.environ.get("EMBEDDING_MODEL"), base_url=os.environ.get("EMBEDDING_BASE_URL"), openai_api_key=os.environ.get("EMBEDDING_API_KEY"), ) persist_directory = "./chroma_db_rag_basic" vectorstore = Chroma(persist_directory=persist_directory, embedding_function=embeddings_model) # 2. 创建 MultiQueryRetriever # 它需要一个 LLM 来生成多个查询 multiquery_retriever = MultiQueryRetriever.from_llm( retriever=vectorstore.as_retriever(search_kwargs={"k": 2}), # 基础检索器 llm=llm, # 用于生成新查询的LLM # prompt=ChatPromptTemplate.from_template("Generate 3 different ways to ask the question: {question}"), # 也可以自定义生成查询的Prompt # parser_key="text", # LLM输出中包含查询的键 ) print("\n--- MultiQueryRetriever 示例 ---") query_mq = "LangChain的优势是什么?" retrieved_docs_mq = multiquery_retriever.invoke(query_mq) print(f"对查询 '{query_mq}' 的 MultiQueryRetriever 检索结果 ({len(retrieved_docs_mq)} 个文档):") for i, doc in enumerate(retrieved_docs_mq): print(f"文档 {i+1} (来源: {doc.metadata.get('source', 'N/A')}):\n{doc.page_content}") print("-" * 30)b. RAG-Fusion
- 原理: 这是MultiQueryRetriever的一种演进。它同样会利用LLM生成多个相似的子查询并分别执行检索。关键区别在于合并结果时,它使用一种名为“倒数排序融合”(Reciprocal Rank Fusion, RFF)的算法,对所有检索结果进行智能的重新排序,选出在多次查询中都排名靠前的文档。
- 优点: 相比简单地合并结果,RFF算法能更有效地将最核心、最相关的文档提升到前排,提高结果质量。
c. 后退一步提示方法
- 原理: 面对一个非常具体的问题,直接检索可能找不到答案。此方法利用LLM,先从具体问题中抽象出一个更高层次、更宽泛的“后退一步”的问题。然后,同时对“原始问题”和“后退一步的问题”进行检索。
- 优点: 能同时召回包含高层概念的背景知识和针对具体问题的细节信息,为LLM提供更全面的视角。
d. 假设性文档嵌入
- 原理:首先让LLM根据用户问题生成一个假设性的、理想的回答。然后,使用这个理想的回答的嵌入去进行向量检索。
- 优点:因为这个假设性文档在语义上与理想的答案高度相关,所以它的嵌入能更有效地找到真正相关的原始文档。有些时候可以把问题和答案拼接一起检索效果可能更好些。
检索中优化 —— 融合多种检索优势
问题: 单一的向量搜索(语义搜索)虽然强大,但有时会忽略关键词的精确匹配,尤其是在处理专有名词、代码函数或特定术语时。
混合搜索(Hybrid Search)
不同向量库有不同实现方式,比如milvus内置BM25、关键词匹配、模糊查询匹配等
- 原理: 将传统的关键词搜索(如BM25算法,稀疏检索)与现代的向量搜索(密集检索)结合起来。关键词搜索确保专有名词等能被精确匹配,而向量搜索负责理解语义上的相似性。两者结合,取长补短。
- 优点: 显著提升检索的鲁棒性,在需要精确匹配和语义理解的场景下效果都很好。
- 缺点: 实现上需要同时维护两种索引(如BM25索引和向量索引),架构略微复杂。
LangChain 提供了 EnsembleRetriever 来实现此功能。它可以将多个不同的检索器组合在一起,并可以为各自的检索结果设置权重。
# 示例: 使用 EnsembleRetriever 实现混合搜索 from langchain.retrievers import EnsembleRetriever from langchain_chroma import Chroma from langchain_community.retrievers import BM25Retriever # all_splits 是你的所有文档块列表, vectorstore 也已创建,这些都可从前面或之前代码找到 # 1. 初始化关键词检索器 bm25_retriever = BM25Retriever.from_documents(all_splits) bm25_retriever.k = 3 # 2. 初始化向量检索器 vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 3. 初始化 EnsembleRetriever ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.5, 0.5] # 可以给不同检索器设置不同权重 ) # 4. 使用 query = "LangChain中的LCEL是什么?" retrieved_docs = ensemble_retriever.invoke(query) print(f"混合搜索召回了 {len(retrieved_docs)} 个文档。")检索后优化 —— 对检索结果精加工
从数据库检索到初步的文档列表后,直接将它们全部交给LLM并非最佳选择。精细的后处理是提升质量、降低成本的关键一步。
重排序 (Re-ranking): 提升相关性
- 原理: 检索器通常返回的文档是基于向量相似度的初步排序。 重排序模型是独立的机器学习模型,它们接收原始查询和每个检索到的文档对,然后输出一个更精细的相关性分数。
- 优点: 显著提升检索结果的相关性,尤其是在基础检索器表现不佳时。
- 缺点: 引入额外的模型调用和延迟。
- 实现方式: 通常作为 ContextualCompressionRetriever 的一部分,或独立作为一个后处理步骤。
这里为了方便找了使用Flashrank演示,工作中我们是使用vllm部署rerank模型使用
# 示例: 使用Flashrank 进行重排序 from langchain.retrievers.document_compressors import FlashrankRerank from langchain.retrievers import ContextualCompressionRetriever # 1. 定义一个重排序压缩器 # top_n 是重排序后返回多少个文档 rerank_compressor = FlashrankRerank( model="miniReranker_arabic_v1", top_n=3 ) # 2. 创建一个 ContextualCompressionRetriever, 使用 Flashrank 作为压缩器 rerank_retriever = ContextualCompressionRetriever( base_compressor=rerank_compressor, base_retriever=vectorstore.as_retriever(search_kwargs={"k": 5}) # 先检索5个再重排 ) # 3. 使用 print("\n--- Reranking (Flashrank) 示例 ---") query_rerank = "LangChain的最新功能是什么?" retrieved_reranked_docs = rerank_retriever.invoke(query_rerank) print(f"对查询 '{query_rerank}' 的重排序检索结果({len(retrieved_reranked_docs)} 个文档):") for i, doc in enumerate(retrieved_reranked_docs): print(f"文档 {i+1} (分数: {doc.metadata.get('relevance_score', 'N/A')}):\n{doc.page_content[:100]}...") print("-" * 30)上下文压缩: 减少冗余
- 问题: 检索到的文档块可能包含大量冗余信息,浪费Token,影响LLM的理解。
- 原理: 在检索到文档块之后,利用LLM或专门的压缩器对每个文档块进行精简,只保留与用户查询直接相关的部分。
- 优点: 减少LLM的输入Token数,降低成本,提高LLM对关键信息的聚焦能力。
- 缺点: 压缩过程可能损失少量上下文或引入偏差。
from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor from langchain_openai import ChatOpenAI # 1. 定义一个基础检索器(先多检索一些,再压缩) base_retriever_for_comp = vectorstore.as_retriever(search_kwargs={"k": 5}) # 2. 定义一个 LLMChainExtractor (压缩器) compressor = LLMChainExtractor.from_llm(llm=llm) # 3. 创建 ContextualCompressionRetriever compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=base_retriever_for_comp ) # 4. 使用 print("\n--- ContextualCompressionRetriever 示例 ---") query_comp = "LangChain的调试工具叫什么?它的主要作用是什么?" retrieved_compressed_docs = compression_retriever.invoke(query_comp) print(f"对查询 '{query_comp}' 的ContextualCompressionRetriever 检索结果:") for i, doc in enumerate(retrieved_compressed_docs): original_len = len(doc.metadata.get('original_content', doc.page_content)) compressed_len = len(doc.page_content) print(f"文档 {i+1}(原始长度: {original_len}, 压缩后长度: {compressed_len}):") print(doc.page_content) print("-" * 30)拐点法则: 减少冗余
问题: 检索到的文档块可能包含大量冗余信息,浪费Token,影响LLM的理解。
假设我们已经检索并重排序了20个文档,它们的分数从高到低排列。通常情况下,最前面的几个文档与查询高度相关,分数很高;但随着排名的下降,文档的相关性会急剧降低,然后趋于平缓(进入一个低相关性的“长尾”)。
“拐点”就是这个相关性分数急剧下降到平缓的转折点。这个点被认为是“高价值信息”和“噪音信息”的分界线。我们的目标就是自动找到这个点,这个过程通常在检索和重排序之后,进入LLM生成步骤之前进行。
原理:
- 获取排序文档和分数: 首先,从检索器(最好是经过重排序的)获取一个相对较长的文档列表(例如,top 20或top 30)以及它们对应的相关性分数。
- 寻找拐点: 有几种计算拐点的方式,其中一种经典的方法是:
- 画一条直线:在相关性分数图上,从第一个点(排名第1的文档)到最后一个点(排名第20的文档)画一条直线。
- 计算距离:计算中间每个点到这条直线的垂直距离。
- 找到最大距离点:距离这条直线最远的点,就是“弯曲”得最厉害的地方,即我们寻找的“拐点”。
- 动态截断: 假设通过计算,我们发现拐点出现在第4个文档上。那么,我们就只选择前4个文档作为最终的上下文传递给LLM,而忽略后面的16个文档。
优点:
- 动态、自适应的K值:这是最大的好处。对于一个简单、明确的问题,系统可能会发现有8个高度相关的文档(拐点在第8位);而对于一个模糊、小众的问题,可能只有2个相关文档(拐点在第2位)。系统可以自动适应,而不是僵化地使用固定的top_k=5。
- 有效降噪:能够动态地滤除那些相关性不高的“噪音”文档,防止它们干扰LLM的判断,从而降低产生“幻觉”的风险。
- 优化成本和延迟:通过向LLM提供更少、但更精炼的上下文,可以显著减少Token消耗,降低API调用成本,并可能加快最终答案的生成速度。
缺点与挑战:
- 依赖分数质量:该方法的效果高度依赖于重排序(Reranking)分数的质量和区分度。如果分数本身不能很好地反映相关性,那么找到的拐点也是无意义的。
- 可能不存在明显拐点:在某些情况下,相关性分数可能是平滑下降的,没有一个清晰的“拐点”,这会导致该方法失效或选择一个次优的截断点。
- 实现稍复杂:相比简单的top_k,它需要在检索后增加一个额外的计算步骤。
from typing import List, Tuple import numpy as np from langchain_core.documents import Document def find_elbow_point(scores: np.ndarray) -> int: """ 使用点到直线最大距离的纯几何方法。 返回的是拐点在原始列表中的索引。 """ n_points = len(scores) if n_points < 3: return n_points -1 # 返回最后一个点的索引 # 创建点坐标 (x, y),x是索引,y是分数 points = np.column_stack((np.arange(n_points), scores)) # 获取第一个点和最后一个点 first_point = points[0] last_point = points[-1] # 计算每个点到首末点连线的垂直距离 # 使用向量射影的方法 line_vec = last_point - first_point line_vec_normalized = line_vec / np.linalg.norm(line_vec) vec_from_first = points - first_point # scalar_product 是每个点向量在直线方向上的投影长度 scalar_product = np.dot(vec_from_first, line_vec_normalized) # vec_parallel 是投影向量 vec_parallel = np.outer(scalar_product, line_vec_normalized) # vec_perpendicular 是垂直向量,它的模长就是距离 vec_perpendicular = vec_from_first - vec_parallel dist_to_line = np.linalg.norm(vec_perpendicular, axis=1) # 找到距离最大的点的索引 elbow_index = np.argmax(dist_to_line) return elbow_index def truncate_with_elbow_method_final( reranked_docs: List[Tuple[float, Document]] ) -> List[Document]: if not reranked_docs or len(reranked_docs) < 3: print("文档数量不足3个,无法进行拐点检测,返回所有文档。") return [doc for _, doc in reranked_docs] scores = np.array([score for score, _ in reranked_docs]) docs = [doc for _, doc in reranked_docs] # 调用我们验证过有效的拐点检测函数 elbow_index = find_elbow_point(scores) # 我们需要包含拐点本身,所以截取到 elbow_index + 1 num_docs_to_keep = elbow_index + 1 final_docs = docs[:num_docs_to_keep] print(f"检测到分数拐点在第 {elbow_index + 1} 位。截断后返回 {len(final_docs)} 个文档。") return final_docs print("\n--- 拐点检测示例 ---") # 假设 reranked_docs 是你的输入数据 reranked_docs = [ (0.98, "文档1"), (0.95, "文档2"), (0.92, "文档3"), (0.75, "文档4"), (0.5, "文档5"), (0.48, "文档6") ] final_documents = truncate_with_elbow_method_final(reranked_docs) print(final_documents)QA对优化 —— 检索过程
将检索从“问题-文档”匹配,升级为“问题-问题”匹配。因为用户提出的问题,在语义上与另一个相似的问题,通常比与一个包含答案的长篇文档段落更接近。这大大降低了语义匹配的难度,从而提升了检索的精准度。
方法一:将QA对作为知识库直接索引
这是最直接的一种方式,特别适用于有现成FAQ文档的场景。
操作流程:
数据准备:将现有的知识库整理成一系列高质量的“一问一答”对。例如:
Q: “LangChain的LCEL是什么?”
A: “LCEL,全称LangChain Expression Language,是一种用于…”
索引构建:在构建向量索引时,只对问题部分进行嵌入。而将对应的答案作为元数据存储,与该问题向量绑定。
检索过程:当用户提出一个新问题时(例如:“请介绍一下LCEL”),对这个新问题进行嵌入。
相似度匹配:在向量数据库中搜索与新问题向量最相似的已索引问题向量。
返回答案:一旦找到最匹配的旧问题,系统直接从元数据中提取并返回其对应的答案。
优点:
- 极高的精度:由于是问题匹配问题,语义对齐非常精准,召回的结果相关性极高。
- 答案质量可控:返回的答案是预先编写和审核的,质量有保证,可以完全避免LLM的幻觉问题。
- 响应速度快:在某些情况下,如果匹配度足够高,甚至可以跳过最后调用LLM生成答案的步骤,直接返回预设答案,从而降低延迟和成本。
缺点:
- 知识范围有限:系统只能回答那些已经预设了QA对的问题。对于超出范围的新问题,它将无法回答。
- 制作成本高:需要人工或半自动地创建和维护高质量的QA对知识库,工作量较大。
# --- 1. 数据准备:高质量的QA对 --- qa_pairs = [ { "question": "LangChain的LCEL是什么,它有什么用?", "answer": "LCEL,全称LangChain Expression Language,是一种用于声明式地链式组合AI组件的语言。它简化了复杂链的构建,并原生支持流式处理、异步和并行执行等高级功能。" }, { "question": "什么是RAG系统中的“幻觉”问题?", "answer": "RAG系统中的“幻觉”指的是,大型语言模型在生成答案时,捏造了事实,或者生成了与提供的上下文不符的、看似有理有据的错误信息。" }, { "question": "如何提升RAG系统的检索精度?", "answer": "提升RAG检索精度的方法有很多,包括使用更先进的嵌入模型、对文档进行智能分块、采用混合搜索、以及在检索后进行重排序(Re-ranking)等。" } ] # --- 2. 索引构建:只对问题进行嵌入 --- # 创建一个文档列表,每个文档的内容是“问题”,元数据包含“答案” question_documents = [] for pair in qa_pairs: # 将答案作为元数据存储 metadata = {"answer": pair["answer"]} doc = Document(page_content=pair["question"], metadata=metadata) question_documents.append(doc) # 使用OpenAI的嵌入模型 embeddings_model = OpenAIEmbeddings( model=os.environ.get("EMBEDDING_MODEL"), base_url=os.environ.get("EMBEDDING_BASE_URL"), openai_api_key=os.environ.get("EMBEDDING_API_KEY") ) # 创建一个临时的Chroma向量数据库来存储问题向量 vectorstore_qa = Chroma.from_documents( documents=question_documents, embedding=embeddings_model ) # --- 3. 检索与问答 --- def answer_from_qa_pairs(user_question: str): """ 通过在QA对知识库中搜索最相似的问题来回答。 """ print(f"\n用户问题: '{user_question}'") # 在向量数据库中搜索与用户问题最相似的“已索引问题” similar_question_docs = vectorstore_qa.similarity_search(user_question,k=1) if similar_question_docs: # 提取最相似问题的预设答案 most_similar_question = similar_question_docs[0].page_content retrieved_answer = similar_question_docs[0].metadata['answer'] print(f"匹配到的最相似问题: '{most_similar_question}'") print(f"系统回答 (来自预设答案): {retrieved_answer}") else: print("抱歉,在知识库中没有找到相关问题。") # --- 测试 --- answer_from_qa_pairs("langchain的LCEL是做什么的?") answer_from_qa_pairs("大模型幻视是什么意思?")方法二:从文档中生成QA对,进行多路召回
这是一种更灵活的策略,它不是抛弃原始文档,而是用QA对为原始文档增加新的元数据。
操作流程:
文档分块:像往常一样,先将原始文档切分成块。
QA对生成:对每一个文档块,使用LLM来反向生成几个可能指向这个文档块的问题。
例如,对于一个讲述LCEL优点的文档块,LLM可能会生成:
“LCEL有哪些好处?”
“为什么应该使用LangChain表达式语言?”
“LCEL是如何支持流式处理的?”
混合索引:现在,我们有原始的文档块和新生成的问题。接下来有两种索引策略:
策略A(推荐):只索引生成的问题,但将它们全部链接到同一个原始文档块的ID。当任何一个问题被匹配到时,我们召回的是它们共同指向的那个原始文档块。
策略B:将每个生成的问题与文档块内容拼接后,再进行索引。
检索过程:用户提问时,他的问题现在有多条路径可以找到正确的信息:
路径1:直接与原始文档块的语义匹配。
路径2:与某个LLM生成的代理问题”相似,从而间接定位到原始文档块。
优点:
- 召回率极大提升:为单个知识点创建了多个不同的语义入口,即使用户的提问方式千奇百怪,只要能和其中一个代理问题对上,就能找到正确答案。
- 完美结合了两种模式的优点:既保留了原始文档的完整上下文,又利用了“问题匹配问题”的高精度优势。
- 自动化程度高:QA对的生成可以由LLM自动完成,减少了人工成本。
缺点:
- 索引成本增加:需要额外调用LLM来生成问题,并且索引的规模会变大。
- 对生成质量有要求:LLM生成的问题质量直接影响最终的检索效果。
from langchain.storage import InMemoryStore from langchain_core.documents import Document from langchain.retrievers.multi_vector import MultiVectorRetriever from langchain_core.output_parsers import StrOutputParser from langchain_core.prompts import ChatPromptTemplate # --- 1. 原始文档准备 --- original_docs = [ Document(page_content="LCEL,全称LangChain Expression Language,它通过操作符重载(如`|`符号)提供了一种声明式的、流畅的方式来构建AI链。它的关键优势包括:开箱即用的流式处理、异步和并行执行能力,以及对整个链的生命周期管理(如日志、调试)提供了极大的便利。", metadata={"doc_id": "lcel_intro"}), Document(page_content="混合搜索(Hybrid Search)结合了传统关键词搜索(如BM25)和现代向量搜索的优点。关键词搜索能精确匹配术语和缩写,而向量搜索擅长理解语义和意图。二者结合能显著提升检索的鲁棒性和准确性。", metadata={"doc_id": "hybrid_search_intro"}), ] # --- 2. 从文档生成“代理问题”的链 --- question_gen_prompt_str = ( "你是一位AI专家。请根据以下文档内容,生成3个用户可能会提出的、高度相关的问题。\n" "只返回问题列表,每个问题占一行,不要有其他前缀或编号。\n\n" "文档内容:\n" "----------\n" "{content}\n" "----------\n" ) question_gen_prompt = ChatPromptTemplate.from_template(question_gen_prompt_str) question_generator_chain = question_gen_prompt | llm | StrOutputParser() # --- 3. MultiVectorRetriever 设置 --- # a. 向量数据库 vectorstore_mv = Chroma(collection_name="multivector_retriever", embedding_function=embeddings_model) # b. 文档存储器:用于根据ID存储和查找原始文档 doc_store = InMemoryStore() # c. 生成的子文档(问题)列表 sub_docs = [] # d. 原始文档ID列表 doc_ids = [doc.metadata["doc_id"] for doc in original_docs] # 遍历每个原始文档,生成问题并存储 for i, doc in enumerate(original_docs): doc_id = doc_ids[i] # 生成问题 generated_questions = question_generator_chain.invoke({"content": doc.page_content}).split("\n") # 清理可能存在的空字符串 generated_questions = [q for q in generated_questions if q.strip()] # 将每个问题包装成一个Document,并链接到原始文档的ID for q in generated_questions: sub_docs.append(Document(page_content=q, metadata={"doc_id": doc_id})) # 将原始文档和生成的子文档(问题)都添加到存储中 doc_store.mset(list(zip(doc_ids, original_docs))) # 存储原始文档 vectorstore_mv.add_documents(sub_docs) # 只索引问题 # 初始化MultiVectorRetriever # - search_type="similarity": 表示用向量搜索来查找问题 # - a. 它会在vectorstore_mv中搜索最相似的问题 # - b. 然后根据问题的metadata['doc_id'],去doc_store中取回原始文档 retriever = MultiVectorRetriever( vectorstore=vectorstore_mv, docstore=doc_store, id_key="doc_id", search_type="similarity" ) # --- 4. 检索测试 --- user_query = "混合检索的好处是什么?" retrieved_results = retriever.invoke(user_query) print(f"\n用户问题: '{user_query}'") print("\n--- 检索到的原始文档 ---") print(retrieved_results[0].page_content)方法三:利用QA对微调嵌入模型
直接优化嵌入模型。
操作流程:
- 构建训练集:创建一个包含(查询, 正向文档, [负向文档])三元组的高质量数据集。这个数据集可以通过人工标注或使用现有QA对来构建。
- 模型微调:使用这个数据集对一个基础的嵌入模型进行微调。训练的目标是:让查询和正向文档的嵌入在向量空间中尽可能接近,同时让它们与“负向文档”的嵌入尽可能远离。
- 应用新模型:在整个RAG系统中使用这个经过微调、针对特定领域数据进行优化的嵌入模型。
优点:
- 根本性提升:直接从源头提升了嵌入的质量,使得模型能更好地理解您所在领域的特定术语和语义关系。
- 效果天花板高:对于特定领域的应用,一个精调的嵌入模型其效果通常会远超通用的预训练模型。
缺点:
- 技术门槛最高:需要高质量的标注数据,并且微调过程需要专业的机器学习知识和计算资源。
- 成本极高:数据标注和模型训练的成本非常昂贵。
以下是一个伪代码,只能作为参考,展示如何使用sentence-transformers库来微调一个嵌入模型。实际场景下需要一个规模大得多的高质量数据集。
import torch from sentence_transformers import SentenceTransformer, losses from sentence_transformers.readers import InputExample from torch.utils.data import DataLoader # --- 1. 准备微调数据集--- # 实际应用中,你需要成千上万这样的样本 # 每个样本是一个“查询”和“一个相关的正面段落” train_examples = [ InputExample(texts=["LCEL的优点有哪些?", "LCEL,全称LangChain Expression Language...提供了极大的便利。"]), InputExample(texts=["什么是混合搜索?", "混合搜索(Hybrid Search)结合了传统关键词搜索和现代向量搜索的优点..."]), InputExample(texts=["如何解决RAG幻觉", "RAG系统中的“幻觉”指的是...可以通过多种方式缓解,例如提高检索质量..."]), ] # --- 2. 定义模型和数据加载器 --- # 选择一个强大的预训练模型作为基础 model_name = 'moka-ai/m3e-base' model = SentenceTransformer(model_name) # 准备数据加载器 # MultipleNegativesRankingLoss 是一种非常适合此类任务的损失函数,它会智能地将一个batch内的其他“正向段落”作为当前查询的“负向样本” batch_size = 4 # 实际应用中可以更大,如32或64 train_dataloader = DataLoader(train_examples, shuffle=True, batch_size=batch_size) # --- 3. 定义损失函数 --- train_loss = losses.MultipleNegativesRankingLoss(model) # --- 4. 模型微调 --- num_epochs = 3 # 实际应用中可能需要更多轮次 warmup_steps = int(len(train_dataloader) * num_epochs * 0.1) # 10%的预热步数 print("开始微调嵌入模型...") model.fit( train_objectives=[(train_dataloader, train_loss)], epochs=num_epochs, warmup_steps=warmup_steps, output_path="./finetuned_embedding_model", # 微调后模型的保存路径 show_progress_bar=True ) print("\n微调完成!模型已保存至 './finetuned_embedding_model'")模型调优与Prompt工程——提升回答质量
即使检索到高质量的上下文,LLM的生成质量也依赖于优秀的Prompt工程。
清晰的指令与角色设定
- 在Prompt的 system 消息中,明确LLM的角色(如“你是一名专业的知识库助手”)、语气(如“友好且简洁”)。
- 明确回答的约束(“如果信息不足,请说明你不知道”)。
- 明确要求LLM引用其答案的来源(例如文档编号、文件名),这有助于用户验证信息,也是防范幻觉的有效手段。
强制结构化输出
- 原理:如果希望LLM输出特定格式(如JSON、列表),使用 llm.with_structured_output() 是最有效方法。强制LLM遵循定义的Pydantic模型输出格式。
- 优点:这对于后续的答案解析和整合非常重要,能保证输出的稳定可靠。
引用与来源
- 在Prompt中明确要求LLM引用其答案的来源(例如文档编号、文件名)。这有助于用户验证信息,也是防范幻觉的有效手段。
- 实现方式:通常在Prompt中添加“请在回答中引用你使用的上下文文档的编号或文件名”类似的指令。这需要你的 Document 块的 metadata 中包含这些信息。
from pydantic import BaseModel, Field from langchain_core.prompts import ChatPromptTemplate from langchain_core.runnables import RunnablePassthrough class AnswerFormat(BaseModel): answer: str = Field(description="针对问题的回答") references: list[str] = Field(description="答案引用的文档块ID或来源信息列表") confidence_score: float = Field(description="对答案的自信度评分,0.0到1.0之间") # 绑定 LLM, 强制其输出 AnswerFormat 结构 llm_structured_output = llm.with_structured_output(AnswerFormat) # 定义一个 Prompt,鼓励LLM输出结构化数据 structured_rag_prompt = ChatPromptTemplate.from_messages([ ("system", "你是一个知识问答机器人。请根据提供的上下文,以JSON格式回答问题。如果信息不足,请将answer字段设置为'不知道',confidence_score为0.0。"), ("user", "问题:{question}\n\n上下文:\n{context}") ]) # 创建一个只负责生成结构化答案的链 structured_answer_chain = ( {"question": RunnablePassthrough(), "context": vectorstore.as_retriever()} | structured_rag_prompt | llm_structured_output ) print("\n--- 结构化输出 RAG 示例 ---") query_structured = "LangChain的优势是什么??" response_structured = structured_answer_chain.invoke(query_structured) print(f"问题: {query_structured}") # pydantic模型可以方便地转为JSON print(f"回答:\n{response_structured}")RAG“幻觉”的防范策略
“幻觉”是LLM在没有足够信息时,或者错误地解释信息时,编造出不存在的事实。在RAG中,这尤其危险,因为它可能给出有理有据的错误信息。
防范策略是一个系统工程,它依赖于前面所有阶段的优化
优质的检索:这是最重要的。如果检索到的上下文本身就是不准确、不完整或不相关的,那么LLM就更容易产生幻觉。高级检索策略是解决这个问题的核心。
- 提高召回率: 确保能找到所有相关文如 MuiQueryRetriever)。
- 提高相关性: 确保召回的文档都是高度相关的 (如使用 Hybrid Search、Re-ranking)。
- 减少冗余: 确保上下文简洁明了 (如使用 Contextual Compression)。
严格的Prompt工程:
- 明确的指令: “只根据提供的上下文回答,如果不知道就说不知道,不要编造。”
- 负面约束: “不要包含个人意见”、“不要给出超出上下文范围的信息”。
- 引用要求: 要求LLM在回答中引用来源,强制其与提供的上下文关联。
答案验证/事实核查:
用于检查答案中的陈述是否都能在提供的上下文中找到证据。
- RAGAS 等评估工具: 后续章节会讲到专门的RAG评估框架。
- Post-processing : 在LLM生成答案后,可以设计一个额外的LLM链或传统NLP模块,
模型选择与微调:
- 某些LLM天生就比其他模型更不容易产生幻觉(通常是较新的、性能更好的模型)。
- 如果条件允许,可以针对特定领域数据对LLM进行微调,可以提高其在该领域的准确性和一致性,从而减少幻觉。但微调成本较高,通常是最后考虑的选项。
将优化策略整合到RAG链中
from langchain.chains import create_retrieval_chain from langchain.chains.combine_documents import create_stuff_documents_chain from langchain_core.prompts import ChatPromptTemplate from langchain.retrievers.document_compressors import FlashrankRerank from langchain.retrievers import ContextualCompressionRetriever # --- 1. 构建多阶段优化检索器 --- # 基础检索器: 混合搜索 bm25_retriever = BM25Retriever.from_documents(split_documents) bm25_retriever.k = 5 vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 5}) ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.5, 0.5] ) # 查询扩展: 将混合搜索作为基础 multi_query_hybrid_retriever = MultiQueryRetriever.from_llm( retriever=ensemble_retriever, llm=llm ) # 重排序: 将查询扩展和混合搜索的结果进行精排 rerank_compressor = FlashrankRerank( model="miniReranker_arabic_v1", top_n=3 ) # 创建一个 ContextualCompressionRetriever, 使用 Flashrank 作为压缩器 final_optimized_retriever = ContextualCompressionRetriever( base_compressor=rerank_compressor, base_retriever=vectorstore.as_retriever(search_kwargs={"k": 5}) # 先检索5个再重排 ) # --- 2. 构建最终的RAG链 --- # a. 定义RAG的Prompt optimized_rag_prompt = ChatPromptTemplate.from_messages([ ("system", "你是一名专业的知识库助手。请根据提供的上下文**简洁明了**地回答以下问题。\n**如果上下文没有足够信息,请明确说明你不知道,不要凭空捏造。**\n\n上下文:\n{context}"), ("user", "{input}") ]) # 创建文档处理链 optimized_document_chain = create_stuff_documents_chain(llm, optimized_rag_prompt) # 创建完整的检索链 optimized_retrieval_chain = create_retrieval_chain( final_optimized_retriever, optimized_document_chain ) # --- 3. 调用测试 --- print("\n--- 完整优化后的RAG链示例 ---") query_final = "LangChain的优势是什么?" response = optimized_retrieval_chain.invoke({"input": query_final}) print(f"用户: {query_final}") print(f"AI: {response['answer']}") # 查看检索到的上下文,验证其高质量 print("\n--- 检索到的上下文(Context) ---") for i, doc in enumerate(response['context']): print(f"文档 {i+1} (来源: {doc.metadata.get('source', 'N/A')}, 分数: {doc.metadata.get('relevance_score', 'N/A')}):") print(doc.page_content) print("-" * 20)代码解析:
- 我们将 EnsembleRetriever 作为 MultiQueryRetriever 的基础。
- 然后将 MultiQueryRetriever 的输出,再送入 ContextualCompressionRetriever 中,使用 FlashrankRerank (重排序) 进行最后的精简和排序。
- 这个 final_optimized_retriever 就是一个实现了“先扩展查询 -> 再混合搜索 -> 最后重排序结果”的检索器。
- 最终,我们将这个优化后的检索器整合到标准的 create_retrieval_chain 中,构建出一个非常强大的RAG应用。
注意: 如果你的 example.txt 内容较少,优化的效果可能不明显。它们在大规模、多样化的知库中表现更佳。
本期小结
本期教程中,我们可以掌握 RAG 系统的高级优化策略:
- 通过一个系统化的框架(检索前、中、后)来思考RAG优化。
- 掌握数据索引(智能分块)、查询优化(查询扩展、RAG-Fusion)、检索(混合搜索)和后处理(重排序、压缩)等多个维度的具体技术。
- 理解Prompt工程和结构化输出对生成质量的提升作用。
- 最重要的是:深入理解了RAG中“幻觉”问题的成因,并掌握了如何通过全流程的优化来系统性地防范它。
现在,我们的RAG系统已经不仅仅是能用,而是具备了迈向生产级应用的潜力。在下一期教程中,我们将探讨 LangChain 应用的调试、评估与部署,确保系统能够稳定运行、持续改进并最终上线!
代码仓库
https://github.com/lgy1027/ai-tutorial
普通人如何抓住AI大模型的风口?
领取方式在文末
为什么要学习大模型?
目前AI大模型的技术岗位与能力培养随着人工智能技术的迅速发展和应用 , 大模型作为其中的重要组成部分 , 正逐渐成为推动人工智能发展的重要引擎 。大模型以其强大的数据处理和模式识别能力, 广泛应用于自然语言处理 、计算机视觉 、 智能推荐等领域 ,为各行各业带来了革命性的改变和机遇 。
目前,开源人工智能大模型已应用于医疗、政务、法律、汽车、娱乐、金融、互联网、教育、制造业、企业服务等多个场景,其中,应用于金融、企业服务、制造业和法律领域的大模型在本次调研中占比超过30%。
随着AI大模型技术的迅速发展,相关岗位的需求也日益增加。大模型产业链催生了一批高薪新职业:
人工智能大潮已来,不加入就可能被淘汰。如果你是技术人,尤其是互联网从业者,现在就开始学习AI大模型技术,真的是给你的人生一个重要建议!
最后
只要你真心想学习AI大模型技术,这份精心整理的学习资料我愿意无偿分享给你,但是想学技术去乱搞的人别来找我!
在当前这个人工智能高速发展的时代,AI大模型正在深刻改变各行各业。我国对高水平AI人才的需求也日益增长,真正懂技术、能落地的人才依旧紧缺。我也希望通过这份资料,能够帮助更多有志于AI领域的朋友入门并深入学习。
真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发
【附赠一节免费的直播讲座,技术大佬带你学习大模型的相关知识、学习思路、就业前景以及怎么结合当前的工作发展方向等,欢迎大家~】
大模型全套学习资料展示
自我们与MoPaaS魔泊云合作以来,我们不断打磨课程体系与技术内容,在细节上精益求精,同时在技术层面也新增了许多前沿且实用的内容,力求为大家带来更系统、更实战、更落地的大模型学习体验。
希望这份系统、实用的大模型学习路径,能够帮助你从零入门,进阶到实战,真正掌握AI时代的核心技能!
01教学内容
从零到精通完整闭环:【基础理论 →RAG开发 → Agent设计 → 模型微调与私有化部署调→热门技术】5大模块,内容比传统教材更贴近企业实战!
大量真实项目案例:带你亲自上手搞数据清洗、模型调优这些硬核操作,把课本知识变成真本事!
02适学人群
应届毕业生:无工作经验但想要系统学习AI大模型技术,期待通过实战项目掌握核心技术。
零基础转型:非技术背景但关注AI应用场景,计划通过低代码工具实现“AI+行业”跨界。
业务赋能突破瓶颈:传统开发者(Java/前端等)学习Transformer架构与LangChain框架,向AI全栈工程师转型。
vx扫描下方二维码即可
【附赠一节免费的直播讲座,技术大佬带你学习大模型的相关知识、学习思路、就业前景以及怎么结合当前的工作发展方向等,欢迎大家~】
本教程比较珍贵,仅限大家自行学习,不要传播!更严禁商用!
03入门到进阶学习路线图
大模型学习路线图,整体分为5个大的阶段:
04视频和书籍PDF合集
从0到掌握主流大模型技术视频教程(涵盖模型训练、微调、RAG、LangChain、Agent开发等实战方向)
新手必备的大模型学习PDF书单来了!全是硬核知识,帮你少走弯路(不吹牛,真有用)
05行业报告+白皮书合集
收集70+报告与白皮书,了解行业最新动态!
0690+份面试题/经验
AI大模型岗位面试经验总结(谁学技术不是为了赚$呢,找个好的岗位很重要)
07 deepseek部署包+技巧大全
由于篇幅有限
只展示部分资料
并且还在持续更新中…
真诚无偿分享!!!
vx扫描下方二维码即可
加上后会一个个给大家发
【附赠一节免费的直播讲座,技术大佬带你学习大模型的相关知识、学习思路、就业前景以及怎么结合当前的工作发展方向等,欢迎大家~】