1. 项目概述构建一个生产级的RAG生态系统如果你正在尝试将检索增强生成RAG从实验室的原型推向实际应用那么你很可能已经发现一个简单的“加载-切分-检索-生成”链条是远远不够的。用户的问题千奇百怪文档的结构复杂多样而一个“开箱即用”的向量检索其效果往往像在图书馆里蒙着眼睛找书——偶尔能撞对但更多时候会迷失方向。这正是我过去一年里在多个企业级AI项目中反复踩坑后得出的核心结论。于是我决定系统性地梳理和构建一个完整的、生产就绪的RAG生态系统这个项目就是rag-ecosystem。这个项目的目标不是提供一个“万能”的黑盒工具而是像一本开源的操作手册带你深入RAG的每一个核心组件。我们将从最基础的RAG流程开始逐步拆解那些让系统真正可靠、高效的关键技术如何让查询变得更聪明如何为不同的知识建立多层次的索引如何让系统具备自我评估和修正的能力以及最终如何客观地衡量整个系统的表现。我会结合LangChain这个强大的编排框架以及RAGAS、DeepEval等评估工具用代码和可视化的方式把每一个抽象的概念落到实处。无论你是想优化现有的问答机器人还是构建一个全新的企业知识库这个项目中的思路和实现都能为你提供直接的参考。2. 基础RAG系统从零搭建与核心原理剖析在深入高级技巧之前我们必须确保对基础RAG的每个环节都有扎实的理解。一个基础的RAG系统可以清晰地划分为索引、检索和生成三个阶段。很多人会直接调用现成的库但如果不明白背后的“为什么”当出现问题时你将无从下手调试。2.1 索引阶段不只是“切分和存储”索引是RAG系统的基石其质量直接决定了天花板的高度。很多人认为索引就是简单地把文档切成块然后扔进向量数据库这其实埋下了很多隐患。文档加载与清洗我们以 Lilian Weng 关于智能体的经典博客为例。直接加载整个网页会引入大量噪音导航栏、页脚、广告等。使用BeautifulSoup的SoupStrainer进行选择性解析是第一步关键优化。这里我指定只抓取class为 “post-content”, “post-title”, “post-header” 的HTML元素这能确保我们获取的是纯净的正文内容。import bs4 from langchain_community.document_loaders import WebBaseLoader loader WebBaseLoader( web_paths(https://lilianweng.github.io/posts/2023-06-23-agent/,), bs_kwargsdict( parse_onlybs4.SoupStrainer( class_(post-content, post-title, post-header) ) ), ) docs loader.load()分块策略的深度考量分块是索引中最容易被低估的环节。RecursiveCharacterTextSplitter是默认选择因为它能尽量保持段落和句子的完整性。但关键参数chunk_size和chunk_overlap需要根据你的文档类型和后续使用的嵌入模型来精心调整。chunk_size (块大小)这并非越大越好。如果块太大会包含过多无关信息稀释核心语义如果太小则可能无法提供一个完整的上下文来回答问题。对于通用文本1000个字符是一个不错的起点。但如果你处理的是技术文档如API参考其中包含大量代码片段可能需要更小的块如500字符来保持代码块的完整性。chunk_overlap (块重叠)重叠是为了防止一个完整的句子或概念被生硬地切断。200字符的重叠通常能保证上下文的连贯性。例如一个重要的定义如果恰好落在两个块的边界重叠部分能确保它在两个块中都出现提高了被检索到的概率。from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter(chunk_size1000, chunk_overlap200) splits text_splitter.split_documents(docs)嵌入模型的选择与向量化选择嵌入模型就像为你的知识库选择一种“语言”。OpenAI的text-embedding-ada-002是一个强大且通用的选择但它是一个API服务。如果你的数据敏感或需要离线处理可以考虑开源的sentence-transformers模型如all-MiniLM-L6-v2。在LangChain中切换嵌入模型通常只需更换一行代码。from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings # 使用OpenAI Embeddings vectorstore Chroma.from_documents( documentssplits, embeddingOpenAIEmbeddings() # 可替换为 HuggingFaceEmbeddings(model_nameall-MiniLM-L6-v2) )实操心得索引阶段的常见陷阱忽略文档结构对于PDF或Markdown先尝试用MarkdownHeaderTextSplitter等基于结构的拆分器能更好地保持章节逻辑。“一刀切”的分块混合长度的文档如既有长报告又有短消息需要更复杂的策略可以考虑先按标题分大块再递归分小块。嵌入模型的领域适配通用嵌入模型在法律、医疗等专业领域可能表现不佳。如果效果不理想可以考虑在该领域数据上微调一个开源嵌入模型。2.2 检索阶段扮演“智能图书管理员”检索器 (retriever) 是你的智能图书管理员。从向量库创建检索器很简单但默认的相似性搜索 (similarity_search) 可能不是最优解。retriever vectorstore.as_retriever() # 默认使用 similarity_search 返回前k个最相似的文档然而similarity_search计算的是查询向量和文档向量之间的余弦相似度。它假设“语义相似就等于答案相关”但这并不总是成立。例如查询“如何治疗感冒”可能检索到一篇详细描述感冒症状但未提及治疗的文档因为它们在语义上很接近。这就是为什么生产系统通常会引入重排序器 (Re-ranker)。重排序器如Cohere的rerank模型或BAAI/bge-reranker是一个更精细的、专门训练过的模型用于判断查询和文档之间的相关性而不仅仅是语义相似性。它可以将similarity_search返回的前20个文档重新排序把真正最相关的3-5个提到最前面。我们会在后续高级章节详细实现它。2.3 生成阶段用提示工程引导LLM检索到了上下文如何让LLM用好它提示模板是关键。直接从LangChain Hub拉取社区优化过的模板是个好习惯。from langchain import hub from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough prompt hub.pull(rlm/rag-prompt) llm ChatOpenAI(model_namegpt-3.5-turbo, temperature0) 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(What is Task Decomposition?) print(response)这个rlm/rag-prompt模板的核心指令是“使用检索到的上下文来回答问题。如果不知道就说不知道。答案最多三句话保持简洁。” 这是一个非常安全且有效的基线提示。但在生产环境中你可能需要定制它例如要求LLM引用来源的页码或ID。指示LLM当上下文冲突时如何取舍。为答案设定特定的格式如要点列表。注意事项生成阶段的幻觉与控制即使提供了上下文LLM仍可能产生“幻觉”编造信息。除了在提示中强调“基于上下文”还可以在链的后处理阶段加入事实核查步骤例如用另一个LLM调用判断生成的答案是否能在提供的上下文中找到支持。3. 高级查询转换让系统理解用户的“言外之意”基础RAG假设用户的查询是完美的但现实并非如此。查询可能过于模糊、过于具体或者使用了与知识库不同的术语。高级查询转换技术旨在解决这个问题通过改写、扩展或分解用户问题来提升检索质量。3.1 多查询生成多角度撒网单一查询可能因为术语不匹配而失败。多查询生成的核心思想是让LLM基于原始问题生成多个不同表述但同义的问题然后并行检索最后合并结果。from langchain.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser template 你是一个AI语言模型助手。你的任务是为给定的用户问题生成五个不同的版本以便从向量数据库中检索相关文档。通过生成用户问题的多个视角你的目标是帮助用户克服基于距离的相似性搜索的一些限制。请用换行分隔这些替代问题。原始问题{question} prompt_perspectives ChatPromptTemplate.from_template(template) generate_queries ( prompt_perspectives | ChatOpenAI(temperature0) | StrOutputParser() | (lambda x: x.split(\n)) ) question What is task decomposition for LLM agents? queries generate_queries.invoke({question: question}) # 输出可能是 # 1. How do LLM agents break down tasks? # 2. Explain the concept of task decomposition in AI agents. # 3. What methods are used for task decomposition by language model agents? # 4. Describe the process of dividing tasks for LLM-powered agents. # 5. Can you elaborate on task decomposition techniques for agents based on large language models?生成了多个查询后我们需要一个策略来合并检索结果。简单的并集 (union) 会引入重复和噪声。更优的策略是 ** Reciprocal Rank Fusion (RRF)**。RRF的基本思想是一个文档如果在多个查询的检索结果中都排名靠前那么它应该被赋予更高的最终排名。from langchain.load import dumps, loads def reciprocal_rank_fusion(results: list[list], k60): fused_scores {} for docs in results: # docs是单个查询的检索结果列表 for rank, doc in enumerate(docs): doc_str dumps(doc) if doc_str not in fused_scores: fused_scores[doc_str] 0 # RRF公式分数 1 / (rank k)。rank越小排名越前加分越多。 fused_scores[doc_str] 1 / (rank k) reranked_results [ (loads(doc), score) for doc, score in sorted(fused_scores.items(), keylambda x: x[1], reverseTrue) ] return reranked_results # 假设 retriever 已定义 retrieval_chain generate_queries | retriever.map() | reciprocal_rank_fusion参数k是一个平滑常数通常设为60用于防止排名非常靠后的文档对最终分数产生过大影响。3.2 查询分解化整为零有些问题是复合型的包含多个子问题。例如“LLM智能体的主要组成部分是什么它们如何交互” 直接检索可能找不到同时涵盖所有方面的单个文档。分解策略是让LLM先将复杂问题拆解成独立的子问题。template 你是一个有帮助的助手能根据输入问题生成多个子问题。目标是将其分解为可以独立回答的子问题/子查询。生成与以下内容相关的多个搜索查询{question} \n输出3个查询 prompt_decomposition ChatPromptTemplate.from_template(template) generate_sub_queries ( prompt_decomposition | ChatOpenAI(temperature0) | StrOutputParser() | (lambda x: x.split(\n)) ) complex_question What are the main components of an LLM-powered autonomous agent system and how do they interact? sub_questions generate_sub_queries.invoke({question: complex_question}) # 可能输出 # [1. What are the core components of an LLM-powered autonomous agent?, # 2. How does memory work in such an agent system?, # 3. How do the planning and action components interact within the agent?]接下来我们可以并行或串行地回答每个子问题然后将所有答案汇总作为最终答案的上下文。这种方法特别适合需要综合多个信息来源的复杂问答。3.3 Step-Back Prompting退一步海阔天空当用户的问题过于具体或依赖于某个特定实例时直接检索可能失败因为知识库中可能没有关于这个具体实例的记载但有关于其所属类别或一般原理的记载。Step-Back Prompting 教导LLM“退一步”提出一个更抽象、更一般化的问题。from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate examples [ {input: Could the members of The Police perform lawful arrests?, output: what are the powers and duties of police officers?}, {input: Jan Sindels was born in what country?, output: what is Jan Sindels personal history?}, ] example_prompt ChatPromptTemplate.from_messages([ (human, {input}), (ai, {output}) ]) few_shot_prompt FewShotChatMessagePromptTemplate( example_promptexample_prompt, examplesexamples, ) prompt ChatPromptTemplate.from_messages([ (system, You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:), few_shot_prompt, (user, {question}), ]) generate_step_back prompt | ChatOpenAI(temperature0) | StrOutputParser() original_q What is the architecture of GPT-4? step_back_q generate_step_back.invoke({question: original_q}) # 输出可能为”What are the common architectural components of large language models?“然后我们同时用原始问题original_q和退步问题step_back_q进行检索将两者的结果合并作为上下文。这样系统既能获取可能存在的具体信息也能从更一般的原理中推导出答案。3.4 HyDE用“假设的答案”来寻找真实的答案Hypothetical Document Embeddings (HyDE) 是一个极其巧妙的思路。它不直接拿用户的问题去搜索而是先让LLM根据问题“幻想”出一个理想的答案即使这个答案可能是错误的。然后用这个“假设文档”的嵌入向量去检索真实的文档。其逻辑是这个“假设答案”在语言风格、术语使用和内容结构上会与真实的正确答案高度相似。因此用它作为查询向量能更好地绕过词汇不匹配的问题找到语义上真正相关的文档。template 请撰写一段科学论文式的文本来回答以下问题。\n问题{question}\n文本 prompt_hyde ChatPromptTemplate.from_template(template) generate_hypothetical_doc ( prompt_hyde | ChatOpenAI(temperature0) | StrOutputParser() ) question Explain the transformer architecture in deep learning. hypo_doc generate_hypothetical_doc.invoke({question: question}) # hypo_doc 会是LLM生成的一段关于Transformer的假想论文段落。 # 关键步骤用生成的假想文档的嵌入向量进行检索 # 我们需要一个能接受原始文本并返回相似文档的检索器 # 一种实现方式是创建一个临时的向量存储只包含这个假想文档然后用它来查询相似性。 # 更简洁的方式是直接计算假想文档的嵌入向量然后用这个向量在原始向量库中进行相似性搜索。 from langchain_community.vectorstores import Chroma # 假设已有 vectorstore hypo_embedding embedding_model.embed_query(hypo_doc) # 计算假想文档的嵌入 relevant_docs vectorstore.similarity_search_by_vector(hypo_embedding, k4) # 用向量搜索实操心得查询转换技术的选型多查询生成适用于查询可能表述单一的场景成本是检索次数增加通常3-5倍。建议与RRF结合使用。查询分解适用于复杂的、多部分的复合问题。成本是生成和回答多个子问题的延迟。Step-Back Prompting适用于具体实例或专业术语的查询当知识库更偏向于通用知识时效果显著。HyDE在知识库文档专业性强、与用户查询用语差异大时特别有效。但它依赖于LLM生成高质量“假设”的能力且增加了额外的LLM调用开销。 在实际系统中可以根据查询的复杂度、对延迟的容忍度和成本预算动态选择或组合这些技术。4. 路由与智能查询构造为问题找到正确的路径在一个复杂的RAG系统中你的知识库可能不是单一的。你可能有一个产品手册向量库、一个技术论坛帖子向量库还有一个代码仓库的索引。或者有些问题需要调用计算器有些需要查询数据库。路由Routing就是决定一个问题应该由哪个“专家”特定的检索器或工具来处理的过程。4.1 逻辑路由基于规则的导航最简单的路由是基于规则的。例如如果问题中包含“价格”、“购买”、“套餐”等关键词就路由到“销售知识库”检索器如果包含“错误”、“bug”、“如何运行”就路由到“技术文档”检索器。在LangChain中可以用Conditional Runnable或RunnableBranch来实现。from langchain_core.runnables import RunnableBranch def route_by_keyword(query: str) - str: query_lower query.lower() if any(word in query_lower for word in [price, buy, cost, subscription]): return sales elif any(word in query_lower for word in [error, bug, how to, install]): return technical else: return general # 假设我们有三个不同的检索器 sales_retriever sales_vectorstore.as_retriever() tech_retriever tech_vectorstore.as_retriever() general_retriever general_vectorstore.as_retriever() branch RunnableBranch( (lambda x: route_by_keyword(x[question]) sales, sales_retriever), (lambda x: route_by_keyword(x[question]) technical, tech_retriever), general_retriever ) # 将路由分支集成到链中 chain { question: lambda x: x[question], context: branch | format_docs, # branch 返回的是检索器检索器返回文档 } | prompt | llm | StrOutputParser()逻辑路由简单直接但对于复杂或模糊的查询容易失效。4.2 语义路由让LLM做决策者更强大的方式是使用LLM本身作为路由器。我们训练或提示LLM让它根据问题的语义将其分类到预定义的类别中。from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser route_prompt ChatPromptTemplate.from_messages([ (system, 你是一个智能路由助手。请将用户问题分类到以下类别之一 - sales: 关于产品价格、购买、套餐、账单的问题。 - technical: 关于产品使用、错误排查、API、集成的问题。 - general: 关于公司历史、文化、新闻等一般性问题。 只输出类别名称不要输出其他任何文字。), (user, {question}) ]) classifier_chain route_prompt | ChatOpenAI(temperature0) | StrOutputParser() def semantic_router(query: str) - str: return classifier_chain.invoke({question: query}) # 在RunnableBranch中使用语义路由 branch RunnableBranch( (lambda x: semantic_router(x[question]) sales, sales_retriever), (lambda x: semantic_router(x[question]) technical, tech_retriever), general_retriever )语义路由更加灵活和智能但增加了LLM调用的延迟和成本。一种折中方案是缓存路由决策对于相似的问题直接使用缓存结果。4.3 查询结构化从自然语言到精准指令有些问题隐含了复杂的意图需要被解析成结构化的查询指令。例如“找出上个月销售额超过10万的所有产品” 需要被解析为对数据库的查询语句。这可以通过Pydantic模型和LangChain的with_structured_output功能来实现。from langchain_core.pydantic_v1 import BaseModel, Field from langchain_openai import ChatOpenAI class ProductQuery(BaseModel): 用于查询产品信息的结构化指令 time_frame: str Field(description查询的时间范围例如‘上个月’‘本季度’) metric: str Field(description需要查询的指标例如‘销售额’‘订单数’) threshold: float Field(description指标的阈值) product_category: str Field(description产品类别如‘电子产品’‘服装’。如未指定则为‘全部’) structured_llm ChatOpenAI(modelgpt-4, temperature0).with_structured_output(ProductQuery) query_parser_prompt ChatPromptTemplate.from_template( 将用户的自然语言问题解析为结构化的产品查询指令。 用户问题{question} ) parse_chain query_parser_prompt | structured_llm user_question 帮我看看上个月销售额超过10万的电子产品有哪些 parsed_query parse_chain.invoke({question: user_question}) # parsed_query 将是一个 ProductQuery 对象 # ProductQuery(time_frame上个月, metric销售额, threshold100000.0, product_category电子产品)得到结构化的parsed_query后你就可以编写一个函数将其转换为对数据库或API的具体调用从而获取精确的答案。这比单纯用向量检索模糊的文档片段要精准得多。5. 高级索引策略构建更智能的知识库基础的扁平向量索引在处理复杂、冗长或结构化的文档时力不从心。高级索引策略旨在构建一个层次化、多视角的知识库让检索更加精准和高效。5.1 多表征索引同一文档不同视角传统的RAG只使用一种嵌入模型为文档块创建向量。但同一个概念可以用不同的方式描述。多表征索引为每个文档块创建多种向量表示例如用不同的嵌入模型或对原文进行摘要后再嵌入在检索时同时查询这些不同的“视角”然后融合结果。# 伪代码示例展示思路 from langchain.embeddings import OpenAIEmbeddings, HuggingFaceEmbeddings from langchain.vectorstores import Chroma documents [...] # 你的文档块列表 # 使用两种不同的嵌入模型 embedding_model_1 OpenAIEmbeddings(modeltext-embedding-ada-002) embedding_model_2 HuggingFaceEmbeddings(model_nameall-MiniLM-L6-v2) # 创建两个独立的向量库 vectorstore_1 Chroma.from_documents(documents, embedding_model_1, collection_nameperspective_1) vectorstore_2 Chroma.from_documents(documents, embedding_model_2, collection_nameperspective_2) retriever_1 vectorstore_1.as_retriever(search_kwargs{k: 5}) retriever_2 vectorstore_2.as_retriever(search_kwargs{k: 5}) # 在检索时从两个检索器获取结果然后使用RRF或其他方法融合 def multi_representation_retrieve(query): docs_1 retriever_1.get_relevant_documents(query) docs_2 retriever_2.get_relevant_documents(query) fused_results reciprocal_rank_fusion([docs_1, docs_2]) return fused_results这种方法增加了索引的存储成本和检索时的计算开销但能显著提高召回率尤其是在处理专业术语或跨语言查询时。5.2 层次化索引RAPTOR自底向上的知识树对于书籍、长报告等具有层次结构的长文档扁平索引会丢失全局上下文。RAPTORRecursive Abstractive Processing for Tree-Organized Retrieval提出了一种构建“知识树”的方法。其核心思想是自底向上聚类将最底层的文档块叶子节点通过嵌入和聚类算法分组到不同的主题簇中。生成摘要节点对每个簇内的所有块用LLM生成一个概括性的摘要。这个摘要成为一个新的父节点。递归构建将这些摘要节点视为新的“文档”重复步骤1和2直到形成一个树状结构。检索时自上而下当查询到来时首先在顶层最抽象的节点中搜索找到最相关的顶层簇然后沿着树向下搜索到该簇的子节点最终到达最相关的原始文档块。这种方法能有效处理“全局理解”类的问题例如“这篇长报告的主要结论是什么”因为顶层摘要提供了全局视角。同时对于细节问题它也能通过树形导航快速定位到具体段落。实现RAPTOR较为复杂涉及聚类如UMAPHDBSCAN、摘要生成和树形检索逻辑。社区已有一些开源实现可供参考。5.3 词级精度检索ColBERT超越句向量像BERT这样的模型通常为整个句子或段落生成一个单一的“句向量”用于相似度计算。ColBERTContextualized Late Interaction over BERT采用了一种不同的思路它为查询和文档中的每个词token都生成一个上下文化的向量。在检索时它计算查询中每个词向量与文档中每个词向量的最大相似度然后对这些最大相似度求和作为最终的相关性分数。这种“迟交互”机制带来了两个巨大优势细粒度匹配即使查询和文档在整体语义上不相似但只要有一些关键术语匹配就能获得高分。这对于精确匹配名称、日期、代码片段等非常有效。可解释性你可以看到是哪些具体的词对匹配贡献了分数。ColBERT的检索速度比传统的向量相似度搜索慢因为它需要进行大量的向量比较。但它的检索精度尤其是对于事实性问题往往更高。现在有一些高效的实现如FlagEmbedding库中的BGE-M3模型也支持类似ColBERT的交互方式和专用向量数据库如Pinecone开始支持这种检索模式。索引策略选择指南策略适用场景优点缺点扁平向量索引文档较短、结构简单、问答直接实现简单检索速度快丢失长程依赖和文档结构对复杂问题召回率低多表征索引专业领域、术语多样、追求高召回率显著提高召回率应对词汇不匹配存储和计算成本倍增融合策略复杂层次化索引书籍、长报告、学术论文等结构化长文档兼顾全局概览和细节检索适合多粒度问答构建过程复杂耗时索引体积大词级精度检索事实性问答、精确匹配、代码检索检索精度高可解释性强检索延迟高对计算资源要求高6. 高级检索与生成让答案更精准、更可靠即使有了最好的索引和查询检索到的文档可能仍然包含不相关信息。此外LLM在生成时也可能偏离上下文或产生幻觉。这一部分我们聚焦于检索后和生成时的优化。6.1 专用重排序器从“相似”到“相关”如前所述向量相似度不等于答案相关性。一个专用的重排序器模型如Cohere rerankBAAI/bge-reranker-large就是为解决这个问题而生的。它通常是一个交叉编码器Cross-Encoder能够同时编码查询和文档并直接输出一个相关性分数。# 示例使用Cohere的重排序API (需要Cohere API Key) from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CohereRerank from langchain_community.vectorstores import Chroma # 首先一个基础的向量检索器返回较多结果比如20个 base_vectorstore Chroma(...) base_retriever base_vectorstore.as_retriever(search_kwargs{k: 20}) # 然后用CohereRerank对结果进行压缩和重排序 compressor CohereRerank(cohere_api_keyyour-key-here, top_n5) # 只保留最相关的5个 compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverbase_retriever ) # 现在使用 compression_retriever它返回的是重排序后的top_n个文档 compressed_docs compression_retriever.get_relevant_documents(你的问题)重排序器能显著提升最终答案的质量尤其是当你的向量检索返回的前几个结果中混入了不相关文档时。它是生产级RAG系统中性价比极高的一个组件。6.2 基于AI智能体的自我修正这是RAG系统走向“自治”的关键一步。我们可以构建一个智能体工作流让系统能够评估自己的输出并在发现问题时尝试自我修正。一个典型的自我修正流程如下初始回答RAG链生成一个初始答案。验证另一个LLM或同一个LLM扮演“验证者”根据检索到的上下文判断初始答案的事实正确性、相关性和完整性。它可以输出一个分数或“是/否”的判断。修正如果验证不通过例如答案不完整或与上下文矛盾则触发“修正者”LLM。修正者会收到原始问题、检索到的上下文、初始答案以及验证者的反馈然后生成一个修正后的答案。迭代这个过程可以迭代多次直到验证通过或达到最大尝试次数。# 概念性代码框架 from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser # 1. 初始RAG链 initial_rag_chain ... # 标准的RAG链 # 2. 验证链 verification_prompt ChatPromptTemplate.from_template( 你是一个严格的事实核查员。请根据以下上下文评估给定的答案。 上下文{context} 问题{question} 初始答案{initial_answer} 请从以下三个方面评估 1. 事实正确性答案中的所有事实是否都能在上下文中找到明确支持是/否 2. 相关性答案是否直接、完整地回应了问题是/否 3. 完整性答案是否涵盖了问题所要求的所有要点是/否 请以JSON格式输出包含三个键factual, relevant, complete值为布尔值。 ) verification_chain verification_prompt | ChatOpenAI(temperature0) | StrOutputParser() # 实际应用应解析为JSON # 3. 修正链 revision_prompt ChatPromptTemplate.from_template( 你是一个答案修正专家。之前的答案未能通过验证。请根据提供的上下文和反馈生成一个修正后的、更好的答案。 上下文{context} 问题{question} 初始答案{initial_answer} 验证反馈{feedback} 请输出修正后的答案 ) revision_chain revision_prompt | ChatOpenAI(temperature0) | StrOutputParser() # 工作流组装 def self_correcting_rag(question, max_attempts3): context retriever.get_relevant_documents(question) attempt 0 current_answer initial_rag_chain.invoke({question: question, context: context}) while attempt max_attempts: feedback verification_chain.invoke({ context: context, question: question, initial_answer: current_answer }) # 解析feedback判断是否通过 if feedback_passes(feedback): # 假设的解析判断函数 return current_answer else: current_answer revision_chain.invoke({ context: context, question: question, initial_answer: current_answer, feedback: feedback }) attempt 1 return current_answer # 返回最后一次修正的答案这种自我修正机制能大幅提升系统的可靠性和答案质量尤其适用于对准确性要求极高的场景但代价是增加了延迟和API调用成本。6.3 长上下文的影响与处理随着GPT-4 Turbo、Claude等模型支持128K甚至更长的上下文窗口一个自然的想法是是否可以直接将大量检索到的文档塞进上下文让LLM自己筛选这确实是一种方案被称为“检索后阅读”Retrieve-then-Read。优点简单避免了复杂的重排序和融合逻辑LLM可以自己权衡不同文档的重要性。缺点成本高输入长上下文会显著增加token消耗和API费用。性能下降有研究表明当相关信息被淹没在大量无关文本中时LLM的“大海捞针”能力会下降可能忽略关键信息。延迟增加处理长上下文需要更多计算时间。因此即使拥有长上下文窗口精选和压缩检索结果仍然是必要的。你可以先使用重排序器选出最相关的5-10个文档然后再将它们送入LLM。对于极长的文档还可以在送入LLM前用一个更小的模型如gpt-3.5-turbo先对每个文档进行摘要再将摘要送入最终生成模型以节省token。7. 端到端评估如何衡量RAG系统的好坏构建RAG系统是一个迭代过程。没有评估你就无法知道你的优化是让系统变得更好还是更糟。评估可以分为人工评估和自动评估这里我们重点介绍自动评估框架。7.1 核心评估指标我们到底要测什么一个完整的RAG评估体系通常涵盖以下几个方面检索质量命中率 (Hit Rate)对于一组问题正确答案所在的文档被检索到的比例至少出现在top-k结果中。平均排序倒数 (Mean Reciprocal Rank, MRR)衡量正确答案在检索结果中的排名。排名越靠前分数越高。生成质量事实一致性 (Faithfulness)生成的答案是否严格基于提供的上下文有没有“幻觉”出上下文不存在的信息这是RAG最重要的指标之一。答案相关性 (Answer Relevance)生成的答案是否直接回答了问题是否包含无关信息上下文相关性 (Context Relevance)检索到的上下文对于回答问题是否相关是否包含大量冗余或无关信息整体系统指标延迟 (Latency)从用户提问到收到答案的总时间。成本 (Cost)每次查询消耗的token费用。7.2 使用RAGAS进行自动化评估RAGAS (Retrieval-Augmented Generation Assessment) 是一个流行的开源框架专门用于评估RAG管道。它利用LLM作为评判员自动化地计算上述许多指标。# 安装 pip install ragas from ragas import evaluate from ragas.metrics import faithfulness, answer_relevance, context_recall, context_precision from datasets import Dataset import os # 1. 准备评估数据集 # 你需要一组样例问题 (question) 标准答案 (answer) 检索到的上下文 (contexts) 和RAG生成的答案 (generated_answer) eval_data { question: [什么是任务分解, LLM智能体的核心组件有哪些], answer: [任务分解是将复杂任务拆解为更小子目标的过程..., 核心组件包括规划、记忆和工具使用...], # ground truth contexts: [ [文档1关于任务分解的内容..., 文档2关于任务分解的内容...], [文档A关于智能体架构的内容..., 文档B关于记忆的内容...] ], generated_answer: [任务分解是让LLM将大任务拆小..., 智能体主要由规划、记忆等部分组成...] } dataset Dataset.from_dict(eval_data) # 2. 设置评估指标 metrics [ faithfulness, # 事实一致性 answer_relevance, # 答案相关性 context_recall, # 上下文召回率检索到的上下文是否包含标准答案 context_precision, # 上下文精确率检索到的上下文是否都与问题相关 ] # 3. 运行评估 (需要配置LLM如OpenAI) os.environ[OPENAI_API_KEY] your-key from langchain_openai import ChatOpenAI from ragas.llms import LangchainLLM langchain_llm LangchainLLM(ChatOpenAI(modelgpt-3.5-turbo)) # 注意RAGAS新版本可能直接支持OpenAI请参考最新文档 result evaluate( datasetdataset, metricsmetrics, llmlangchain_llm # 提供LLM用于评估 ) # 4. 查看结果 print(result) df result.to_pandas() # 转换为DataFrame便于分析RAGAS的评估依赖于LLM作为评判员其本身也有一定的波动性但对于快速迭代和对比不同配置如不同分块大小、不同重排序器的效果它是一个极其强大的工具。7.3 使用DeepEval进行快速评估DeepEval是另一个优秀的评估框架它提供了更丰富的开箱即用指标和易于集成的测试套件。# 安装 pip install deepeval from deepeval import evaluate from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric, ContextualRelevancyMetric from deepeval.test_case import LLMTestCase # 为每个测试样例创建一个 LLMTestCase test_case LLMTestCase( input什么是任务分解, actual_output任务分解是让LLM将大任务拆小..., # 你的RAG系统生成的答案 expected_output任务分解是将复杂任务拆解为更小子目标的过程..., # 标准答案 context[文档1关于任务分解的内容..., 文档2关于任务分解的内容...] # 检索到的上下文 ) # 定义要评估的指标 answer_relevancy_metric AnswerRelevancyMetric(threshold0.7) faithfulness_metric FaithfulnessMetric(threshold0.7) contextual_relevancy_metric ContextualRelevancyMetric(threshold0.7) # 运行评估 evaluate( test_cases[test_case], metrics[answer_relevancy_metric, faithfulness_metric, contextual_relevancy_metric] ) # 查看指标是否通过 print(fAnswer Relevancy: {answer_relevancy_metric.score}, Pass: {answer_relevancy_metric.is_successful()}) print(fFaithfulness: {faithfulness_metric.score}, Pass: {faithfulness_metric.is_successful()})DeepEval的优势在于它提供了明确的通过/失败阈值并且可以轻松集成到CI/CD管道中实现RAG系统的自动化测试。评估实践建议构建高质量的测试集这是评估的基石。测试集应包含多样化的、有代表性的问题并配有标准答案和相关的源文档引用。结合自动与人工评估自动评估如RAGAS适合快速迭代和回归测试。但对于关键场景或衡量答案的细微差别定期进行人工评估是必不可少的。进行A/B测试当你想引入一个新组件如一个新的重排序器时最好的方法是进行A/B测试。将流量的一部分导向新系统对比关键指标如答案好评率、用户停留时间的变化。监控生产环境除了离线评估还需要在生产环境监控真实用户的反馈、失败案例和延迟指标这能发现离线测试中无法预见的问题。构建一个生产级的RAG系统远不止是拼接几个API调用。它要求你对数据预处理、语义搜索、提示工程、LLM行为以及系统评估都有深入的理解。这个rag-ecosystem项目试图为你描绘出一张完整的地图从坚实的地基基础RAG到精密的架构高级索引、智能路由再到确保质量的监控体系评估。每一个环节的优化都可能带来显著的性能提升。在实际操作中我的体会是没有银弹你需要根据自己数据的特点、业务的要求和资源的约束从这张地图中选择合适的工具和技术进行组合与调优。最开始的简单实现能快速验证想法而后续的每一次迭代都应该是数据驱动和评估导向的。
构建生产级RAG系统:从基础原理到高级优化实战
发布时间:2026/5/15 15:12:33
1. 项目概述构建一个生产级的RAG生态系统如果你正在尝试将检索增强生成RAG从实验室的原型推向实际应用那么你很可能已经发现一个简单的“加载-切分-检索-生成”链条是远远不够的。用户的问题千奇百怪文档的结构复杂多样而一个“开箱即用”的向量检索其效果往往像在图书馆里蒙着眼睛找书——偶尔能撞对但更多时候会迷失方向。这正是我过去一年里在多个企业级AI项目中反复踩坑后得出的核心结论。于是我决定系统性地梳理和构建一个完整的、生产就绪的RAG生态系统这个项目就是rag-ecosystem。这个项目的目标不是提供一个“万能”的黑盒工具而是像一本开源的操作手册带你深入RAG的每一个核心组件。我们将从最基础的RAG流程开始逐步拆解那些让系统真正可靠、高效的关键技术如何让查询变得更聪明如何为不同的知识建立多层次的索引如何让系统具备自我评估和修正的能力以及最终如何客观地衡量整个系统的表现。我会结合LangChain这个强大的编排框架以及RAGAS、DeepEval等评估工具用代码和可视化的方式把每一个抽象的概念落到实处。无论你是想优化现有的问答机器人还是构建一个全新的企业知识库这个项目中的思路和实现都能为你提供直接的参考。2. 基础RAG系统从零搭建与核心原理剖析在深入高级技巧之前我们必须确保对基础RAG的每个环节都有扎实的理解。一个基础的RAG系统可以清晰地划分为索引、检索和生成三个阶段。很多人会直接调用现成的库但如果不明白背后的“为什么”当出现问题时你将无从下手调试。2.1 索引阶段不只是“切分和存储”索引是RAG系统的基石其质量直接决定了天花板的高度。很多人认为索引就是简单地把文档切成块然后扔进向量数据库这其实埋下了很多隐患。文档加载与清洗我们以 Lilian Weng 关于智能体的经典博客为例。直接加载整个网页会引入大量噪音导航栏、页脚、广告等。使用BeautifulSoup的SoupStrainer进行选择性解析是第一步关键优化。这里我指定只抓取class为 “post-content”, “post-title”, “post-header” 的HTML元素这能确保我们获取的是纯净的正文内容。import bs4 from langchain_community.document_loaders import WebBaseLoader loader WebBaseLoader( web_paths(https://lilianweng.github.io/posts/2023-06-23-agent/,), bs_kwargsdict( parse_onlybs4.SoupStrainer( class_(post-content, post-title, post-header) ) ), ) docs loader.load()分块策略的深度考量分块是索引中最容易被低估的环节。RecursiveCharacterTextSplitter是默认选择因为它能尽量保持段落和句子的完整性。但关键参数chunk_size和chunk_overlap需要根据你的文档类型和后续使用的嵌入模型来精心调整。chunk_size (块大小)这并非越大越好。如果块太大会包含过多无关信息稀释核心语义如果太小则可能无法提供一个完整的上下文来回答问题。对于通用文本1000个字符是一个不错的起点。但如果你处理的是技术文档如API参考其中包含大量代码片段可能需要更小的块如500字符来保持代码块的完整性。chunk_overlap (块重叠)重叠是为了防止一个完整的句子或概念被生硬地切断。200字符的重叠通常能保证上下文的连贯性。例如一个重要的定义如果恰好落在两个块的边界重叠部分能确保它在两个块中都出现提高了被检索到的概率。from langchain.text_splitter import RecursiveCharacterTextSplitter text_splitter RecursiveCharacterTextSplitter(chunk_size1000, chunk_overlap200) splits text_splitter.split_documents(docs)嵌入模型的选择与向量化选择嵌入模型就像为你的知识库选择一种“语言”。OpenAI的text-embedding-ada-002是一个强大且通用的选择但它是一个API服务。如果你的数据敏感或需要离线处理可以考虑开源的sentence-transformers模型如all-MiniLM-L6-v2。在LangChain中切换嵌入模型通常只需更换一行代码。from langchain_community.vectorstores import Chroma from langchain_openai import OpenAIEmbeddings # 使用OpenAI Embeddings vectorstore Chroma.from_documents( documentssplits, embeddingOpenAIEmbeddings() # 可替换为 HuggingFaceEmbeddings(model_nameall-MiniLM-L6-v2) )实操心得索引阶段的常见陷阱忽略文档结构对于PDF或Markdown先尝试用MarkdownHeaderTextSplitter等基于结构的拆分器能更好地保持章节逻辑。“一刀切”的分块混合长度的文档如既有长报告又有短消息需要更复杂的策略可以考虑先按标题分大块再递归分小块。嵌入模型的领域适配通用嵌入模型在法律、医疗等专业领域可能表现不佳。如果效果不理想可以考虑在该领域数据上微调一个开源嵌入模型。2.2 检索阶段扮演“智能图书管理员”检索器 (retriever) 是你的智能图书管理员。从向量库创建检索器很简单但默认的相似性搜索 (similarity_search) 可能不是最优解。retriever vectorstore.as_retriever() # 默认使用 similarity_search 返回前k个最相似的文档然而similarity_search计算的是查询向量和文档向量之间的余弦相似度。它假设“语义相似就等于答案相关”但这并不总是成立。例如查询“如何治疗感冒”可能检索到一篇详细描述感冒症状但未提及治疗的文档因为它们在语义上很接近。这就是为什么生产系统通常会引入重排序器 (Re-ranker)。重排序器如Cohere的rerank模型或BAAI/bge-reranker是一个更精细的、专门训练过的模型用于判断查询和文档之间的相关性而不仅仅是语义相似性。它可以将similarity_search返回的前20个文档重新排序把真正最相关的3-5个提到最前面。我们会在后续高级章节详细实现它。2.3 生成阶段用提示工程引导LLM检索到了上下文如何让LLM用好它提示模板是关键。直接从LangChain Hub拉取社区优化过的模板是个好习惯。from langchain import hub from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser from langchain_core.runnables import RunnablePassthrough prompt hub.pull(rlm/rag-prompt) llm ChatOpenAI(model_namegpt-3.5-turbo, temperature0) 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(What is Task Decomposition?) print(response)这个rlm/rag-prompt模板的核心指令是“使用检索到的上下文来回答问题。如果不知道就说不知道。答案最多三句话保持简洁。” 这是一个非常安全且有效的基线提示。但在生产环境中你可能需要定制它例如要求LLM引用来源的页码或ID。指示LLM当上下文冲突时如何取舍。为答案设定特定的格式如要点列表。注意事项生成阶段的幻觉与控制即使提供了上下文LLM仍可能产生“幻觉”编造信息。除了在提示中强调“基于上下文”还可以在链的后处理阶段加入事实核查步骤例如用另一个LLM调用判断生成的答案是否能在提供的上下文中找到支持。3. 高级查询转换让系统理解用户的“言外之意”基础RAG假设用户的查询是完美的但现实并非如此。查询可能过于模糊、过于具体或者使用了与知识库不同的术语。高级查询转换技术旨在解决这个问题通过改写、扩展或分解用户问题来提升检索质量。3.1 多查询生成多角度撒网单一查询可能因为术语不匹配而失败。多查询生成的核心思想是让LLM基于原始问题生成多个不同表述但同义的问题然后并行检索最后合并结果。from langchain.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI from langchain_core.output_parsers import StrOutputParser template 你是一个AI语言模型助手。你的任务是为给定的用户问题生成五个不同的版本以便从向量数据库中检索相关文档。通过生成用户问题的多个视角你的目标是帮助用户克服基于距离的相似性搜索的一些限制。请用换行分隔这些替代问题。原始问题{question} prompt_perspectives ChatPromptTemplate.from_template(template) generate_queries ( prompt_perspectives | ChatOpenAI(temperature0) | StrOutputParser() | (lambda x: x.split(\n)) ) question What is task decomposition for LLM agents? queries generate_queries.invoke({question: question}) # 输出可能是 # 1. How do LLM agents break down tasks? # 2. Explain the concept of task decomposition in AI agents. # 3. What methods are used for task decomposition by language model agents? # 4. Describe the process of dividing tasks for LLM-powered agents. # 5. Can you elaborate on task decomposition techniques for agents based on large language models?生成了多个查询后我们需要一个策略来合并检索结果。简单的并集 (union) 会引入重复和噪声。更优的策略是 ** Reciprocal Rank Fusion (RRF)**。RRF的基本思想是一个文档如果在多个查询的检索结果中都排名靠前那么它应该被赋予更高的最终排名。from langchain.load import dumps, loads def reciprocal_rank_fusion(results: list[list], k60): fused_scores {} for docs in results: # docs是单个查询的检索结果列表 for rank, doc in enumerate(docs): doc_str dumps(doc) if doc_str not in fused_scores: fused_scores[doc_str] 0 # RRF公式分数 1 / (rank k)。rank越小排名越前加分越多。 fused_scores[doc_str] 1 / (rank k) reranked_results [ (loads(doc), score) for doc, score in sorted(fused_scores.items(), keylambda x: x[1], reverseTrue) ] return reranked_results # 假设 retriever 已定义 retrieval_chain generate_queries | retriever.map() | reciprocal_rank_fusion参数k是一个平滑常数通常设为60用于防止排名非常靠后的文档对最终分数产生过大影响。3.2 查询分解化整为零有些问题是复合型的包含多个子问题。例如“LLM智能体的主要组成部分是什么它们如何交互” 直接检索可能找不到同时涵盖所有方面的单个文档。分解策略是让LLM先将复杂问题拆解成独立的子问题。template 你是一个有帮助的助手能根据输入问题生成多个子问题。目标是将其分解为可以独立回答的子问题/子查询。生成与以下内容相关的多个搜索查询{question} \n输出3个查询 prompt_decomposition ChatPromptTemplate.from_template(template) generate_sub_queries ( prompt_decomposition | ChatOpenAI(temperature0) | StrOutputParser() | (lambda x: x.split(\n)) ) complex_question What are the main components of an LLM-powered autonomous agent system and how do they interact? sub_questions generate_sub_queries.invoke({question: complex_question}) # 可能输出 # [1. What are the core components of an LLM-powered autonomous agent?, # 2. How does memory work in such an agent system?, # 3. How do the planning and action components interact within the agent?]接下来我们可以并行或串行地回答每个子问题然后将所有答案汇总作为最终答案的上下文。这种方法特别适合需要综合多个信息来源的复杂问答。3.3 Step-Back Prompting退一步海阔天空当用户的问题过于具体或依赖于某个特定实例时直接检索可能失败因为知识库中可能没有关于这个具体实例的记载但有关于其所属类别或一般原理的记载。Step-Back Prompting 教导LLM“退一步”提出一个更抽象、更一般化的问题。from langchain_core.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate examples [ {input: Could the members of The Police perform lawful arrests?, output: what are the powers and duties of police officers?}, {input: Jan Sindels was born in what country?, output: what is Jan Sindels personal history?}, ] example_prompt ChatPromptTemplate.from_messages([ (human, {input}), (ai, {output}) ]) few_shot_prompt FewShotChatMessagePromptTemplate( example_promptexample_prompt, examplesexamples, ) prompt ChatPromptTemplate.from_messages([ (system, You are an expert at world knowledge. Your task is to step back and paraphrase a question to a more generic step-back question, which is easier to answer. Here are a few examples:), few_shot_prompt, (user, {question}), ]) generate_step_back prompt | ChatOpenAI(temperature0) | StrOutputParser() original_q What is the architecture of GPT-4? step_back_q generate_step_back.invoke({question: original_q}) # 输出可能为”What are the common architectural components of large language models?“然后我们同时用原始问题original_q和退步问题step_back_q进行检索将两者的结果合并作为上下文。这样系统既能获取可能存在的具体信息也能从更一般的原理中推导出答案。3.4 HyDE用“假设的答案”来寻找真实的答案Hypothetical Document Embeddings (HyDE) 是一个极其巧妙的思路。它不直接拿用户的问题去搜索而是先让LLM根据问题“幻想”出一个理想的答案即使这个答案可能是错误的。然后用这个“假设文档”的嵌入向量去检索真实的文档。其逻辑是这个“假设答案”在语言风格、术语使用和内容结构上会与真实的正确答案高度相似。因此用它作为查询向量能更好地绕过词汇不匹配的问题找到语义上真正相关的文档。template 请撰写一段科学论文式的文本来回答以下问题。\n问题{question}\n文本 prompt_hyde ChatPromptTemplate.from_template(template) generate_hypothetical_doc ( prompt_hyde | ChatOpenAI(temperature0) | StrOutputParser() ) question Explain the transformer architecture in deep learning. hypo_doc generate_hypothetical_doc.invoke({question: question}) # hypo_doc 会是LLM生成的一段关于Transformer的假想论文段落。 # 关键步骤用生成的假想文档的嵌入向量进行检索 # 我们需要一个能接受原始文本并返回相似文档的检索器 # 一种实现方式是创建一个临时的向量存储只包含这个假想文档然后用它来查询相似性。 # 更简洁的方式是直接计算假想文档的嵌入向量然后用这个向量在原始向量库中进行相似性搜索。 from langchain_community.vectorstores import Chroma # 假设已有 vectorstore hypo_embedding embedding_model.embed_query(hypo_doc) # 计算假想文档的嵌入 relevant_docs vectorstore.similarity_search_by_vector(hypo_embedding, k4) # 用向量搜索实操心得查询转换技术的选型多查询生成适用于查询可能表述单一的场景成本是检索次数增加通常3-5倍。建议与RRF结合使用。查询分解适用于复杂的、多部分的复合问题。成本是生成和回答多个子问题的延迟。Step-Back Prompting适用于具体实例或专业术语的查询当知识库更偏向于通用知识时效果显著。HyDE在知识库文档专业性强、与用户查询用语差异大时特别有效。但它依赖于LLM生成高质量“假设”的能力且增加了额外的LLM调用开销。 在实际系统中可以根据查询的复杂度、对延迟的容忍度和成本预算动态选择或组合这些技术。4. 路由与智能查询构造为问题找到正确的路径在一个复杂的RAG系统中你的知识库可能不是单一的。你可能有一个产品手册向量库、一个技术论坛帖子向量库还有一个代码仓库的索引。或者有些问题需要调用计算器有些需要查询数据库。路由Routing就是决定一个问题应该由哪个“专家”特定的检索器或工具来处理的过程。4.1 逻辑路由基于规则的导航最简单的路由是基于规则的。例如如果问题中包含“价格”、“购买”、“套餐”等关键词就路由到“销售知识库”检索器如果包含“错误”、“bug”、“如何运行”就路由到“技术文档”检索器。在LangChain中可以用Conditional Runnable或RunnableBranch来实现。from langchain_core.runnables import RunnableBranch def route_by_keyword(query: str) - str: query_lower query.lower() if any(word in query_lower for word in [price, buy, cost, subscription]): return sales elif any(word in query_lower for word in [error, bug, how to, install]): return technical else: return general # 假设我们有三个不同的检索器 sales_retriever sales_vectorstore.as_retriever() tech_retriever tech_vectorstore.as_retriever() general_retriever general_vectorstore.as_retriever() branch RunnableBranch( (lambda x: route_by_keyword(x[question]) sales, sales_retriever), (lambda x: route_by_keyword(x[question]) technical, tech_retriever), general_retriever ) # 将路由分支集成到链中 chain { question: lambda x: x[question], context: branch | format_docs, # branch 返回的是检索器检索器返回文档 } | prompt | llm | StrOutputParser()逻辑路由简单直接但对于复杂或模糊的查询容易失效。4.2 语义路由让LLM做决策者更强大的方式是使用LLM本身作为路由器。我们训练或提示LLM让它根据问题的语义将其分类到预定义的类别中。from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser route_prompt ChatPromptTemplate.from_messages([ (system, 你是一个智能路由助手。请将用户问题分类到以下类别之一 - sales: 关于产品价格、购买、套餐、账单的问题。 - technical: 关于产品使用、错误排查、API、集成的问题。 - general: 关于公司历史、文化、新闻等一般性问题。 只输出类别名称不要输出其他任何文字。), (user, {question}) ]) classifier_chain route_prompt | ChatOpenAI(temperature0) | StrOutputParser() def semantic_router(query: str) - str: return classifier_chain.invoke({question: query}) # 在RunnableBranch中使用语义路由 branch RunnableBranch( (lambda x: semantic_router(x[question]) sales, sales_retriever), (lambda x: semantic_router(x[question]) technical, tech_retriever), general_retriever )语义路由更加灵活和智能但增加了LLM调用的延迟和成本。一种折中方案是缓存路由决策对于相似的问题直接使用缓存结果。4.3 查询结构化从自然语言到精准指令有些问题隐含了复杂的意图需要被解析成结构化的查询指令。例如“找出上个月销售额超过10万的所有产品” 需要被解析为对数据库的查询语句。这可以通过Pydantic模型和LangChain的with_structured_output功能来实现。from langchain_core.pydantic_v1 import BaseModel, Field from langchain_openai import ChatOpenAI class ProductQuery(BaseModel): 用于查询产品信息的结构化指令 time_frame: str Field(description查询的时间范围例如‘上个月’‘本季度’) metric: str Field(description需要查询的指标例如‘销售额’‘订单数’) threshold: float Field(description指标的阈值) product_category: str Field(description产品类别如‘电子产品’‘服装’。如未指定则为‘全部’) structured_llm ChatOpenAI(modelgpt-4, temperature0).with_structured_output(ProductQuery) query_parser_prompt ChatPromptTemplate.from_template( 将用户的自然语言问题解析为结构化的产品查询指令。 用户问题{question} ) parse_chain query_parser_prompt | structured_llm user_question 帮我看看上个月销售额超过10万的电子产品有哪些 parsed_query parse_chain.invoke({question: user_question}) # parsed_query 将是一个 ProductQuery 对象 # ProductQuery(time_frame上个月, metric销售额, threshold100000.0, product_category电子产品)得到结构化的parsed_query后你就可以编写一个函数将其转换为对数据库或API的具体调用从而获取精确的答案。这比单纯用向量检索模糊的文档片段要精准得多。5. 高级索引策略构建更智能的知识库基础的扁平向量索引在处理复杂、冗长或结构化的文档时力不从心。高级索引策略旨在构建一个层次化、多视角的知识库让检索更加精准和高效。5.1 多表征索引同一文档不同视角传统的RAG只使用一种嵌入模型为文档块创建向量。但同一个概念可以用不同的方式描述。多表征索引为每个文档块创建多种向量表示例如用不同的嵌入模型或对原文进行摘要后再嵌入在检索时同时查询这些不同的“视角”然后融合结果。# 伪代码示例展示思路 from langchain.embeddings import OpenAIEmbeddings, HuggingFaceEmbeddings from langchain.vectorstores import Chroma documents [...] # 你的文档块列表 # 使用两种不同的嵌入模型 embedding_model_1 OpenAIEmbeddings(modeltext-embedding-ada-002) embedding_model_2 HuggingFaceEmbeddings(model_nameall-MiniLM-L6-v2) # 创建两个独立的向量库 vectorstore_1 Chroma.from_documents(documents, embedding_model_1, collection_nameperspective_1) vectorstore_2 Chroma.from_documents(documents, embedding_model_2, collection_nameperspective_2) retriever_1 vectorstore_1.as_retriever(search_kwargs{k: 5}) retriever_2 vectorstore_2.as_retriever(search_kwargs{k: 5}) # 在检索时从两个检索器获取结果然后使用RRF或其他方法融合 def multi_representation_retrieve(query): docs_1 retriever_1.get_relevant_documents(query) docs_2 retriever_2.get_relevant_documents(query) fused_results reciprocal_rank_fusion([docs_1, docs_2]) return fused_results这种方法增加了索引的存储成本和检索时的计算开销但能显著提高召回率尤其是在处理专业术语或跨语言查询时。5.2 层次化索引RAPTOR自底向上的知识树对于书籍、长报告等具有层次结构的长文档扁平索引会丢失全局上下文。RAPTORRecursive Abstractive Processing for Tree-Organized Retrieval提出了一种构建“知识树”的方法。其核心思想是自底向上聚类将最底层的文档块叶子节点通过嵌入和聚类算法分组到不同的主题簇中。生成摘要节点对每个簇内的所有块用LLM生成一个概括性的摘要。这个摘要成为一个新的父节点。递归构建将这些摘要节点视为新的“文档”重复步骤1和2直到形成一个树状结构。检索时自上而下当查询到来时首先在顶层最抽象的节点中搜索找到最相关的顶层簇然后沿着树向下搜索到该簇的子节点最终到达最相关的原始文档块。这种方法能有效处理“全局理解”类的问题例如“这篇长报告的主要结论是什么”因为顶层摘要提供了全局视角。同时对于细节问题它也能通过树形导航快速定位到具体段落。实现RAPTOR较为复杂涉及聚类如UMAPHDBSCAN、摘要生成和树形检索逻辑。社区已有一些开源实现可供参考。5.3 词级精度检索ColBERT超越句向量像BERT这样的模型通常为整个句子或段落生成一个单一的“句向量”用于相似度计算。ColBERTContextualized Late Interaction over BERT采用了一种不同的思路它为查询和文档中的每个词token都生成一个上下文化的向量。在检索时它计算查询中每个词向量与文档中每个词向量的最大相似度然后对这些最大相似度求和作为最终的相关性分数。这种“迟交互”机制带来了两个巨大优势细粒度匹配即使查询和文档在整体语义上不相似但只要有一些关键术语匹配就能获得高分。这对于精确匹配名称、日期、代码片段等非常有效。可解释性你可以看到是哪些具体的词对匹配贡献了分数。ColBERT的检索速度比传统的向量相似度搜索慢因为它需要进行大量的向量比较。但它的检索精度尤其是对于事实性问题往往更高。现在有一些高效的实现如FlagEmbedding库中的BGE-M3模型也支持类似ColBERT的交互方式和专用向量数据库如Pinecone开始支持这种检索模式。索引策略选择指南策略适用场景优点缺点扁平向量索引文档较短、结构简单、问答直接实现简单检索速度快丢失长程依赖和文档结构对复杂问题召回率低多表征索引专业领域、术语多样、追求高召回率显著提高召回率应对词汇不匹配存储和计算成本倍增融合策略复杂层次化索引书籍、长报告、学术论文等结构化长文档兼顾全局概览和细节检索适合多粒度问答构建过程复杂耗时索引体积大词级精度检索事实性问答、精确匹配、代码检索检索精度高可解释性强检索延迟高对计算资源要求高6. 高级检索与生成让答案更精准、更可靠即使有了最好的索引和查询检索到的文档可能仍然包含不相关信息。此外LLM在生成时也可能偏离上下文或产生幻觉。这一部分我们聚焦于检索后和生成时的优化。6.1 专用重排序器从“相似”到“相关”如前所述向量相似度不等于答案相关性。一个专用的重排序器模型如Cohere rerankBAAI/bge-reranker-large就是为解决这个问题而生的。它通常是一个交叉编码器Cross-Encoder能够同时编码查询和文档并直接输出一个相关性分数。# 示例使用Cohere的重排序API (需要Cohere API Key) from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import CohereRerank from langchain_community.vectorstores import Chroma # 首先一个基础的向量检索器返回较多结果比如20个 base_vectorstore Chroma(...) base_retriever base_vectorstore.as_retriever(search_kwargs{k: 20}) # 然后用CohereRerank对结果进行压缩和重排序 compressor CohereRerank(cohere_api_keyyour-key-here, top_n5) # 只保留最相关的5个 compression_retriever ContextualCompressionRetriever( base_compressorcompressor, base_retrieverbase_retriever ) # 现在使用 compression_retriever它返回的是重排序后的top_n个文档 compressed_docs compression_retriever.get_relevant_documents(你的问题)重排序器能显著提升最终答案的质量尤其是当你的向量检索返回的前几个结果中混入了不相关文档时。它是生产级RAG系统中性价比极高的一个组件。6.2 基于AI智能体的自我修正这是RAG系统走向“自治”的关键一步。我们可以构建一个智能体工作流让系统能够评估自己的输出并在发现问题时尝试自我修正。一个典型的自我修正流程如下初始回答RAG链生成一个初始答案。验证另一个LLM或同一个LLM扮演“验证者”根据检索到的上下文判断初始答案的事实正确性、相关性和完整性。它可以输出一个分数或“是/否”的判断。修正如果验证不通过例如答案不完整或与上下文矛盾则触发“修正者”LLM。修正者会收到原始问题、检索到的上下文、初始答案以及验证者的反馈然后生成一个修正后的答案。迭代这个过程可以迭代多次直到验证通过或达到最大尝试次数。# 概念性代码框架 from langchain_core.prompts import ChatPromptTemplate from langchain_core.output_parsers import StrOutputParser # 1. 初始RAG链 initial_rag_chain ... # 标准的RAG链 # 2. 验证链 verification_prompt ChatPromptTemplate.from_template( 你是一个严格的事实核查员。请根据以下上下文评估给定的答案。 上下文{context} 问题{question} 初始答案{initial_answer} 请从以下三个方面评估 1. 事实正确性答案中的所有事实是否都能在上下文中找到明确支持是/否 2. 相关性答案是否直接、完整地回应了问题是/否 3. 完整性答案是否涵盖了问题所要求的所有要点是/否 请以JSON格式输出包含三个键factual, relevant, complete值为布尔值。 ) verification_chain verification_prompt | ChatOpenAI(temperature0) | StrOutputParser() # 实际应用应解析为JSON # 3. 修正链 revision_prompt ChatPromptTemplate.from_template( 你是一个答案修正专家。之前的答案未能通过验证。请根据提供的上下文和反馈生成一个修正后的、更好的答案。 上下文{context} 问题{question} 初始答案{initial_answer} 验证反馈{feedback} 请输出修正后的答案 ) revision_chain revision_prompt | ChatOpenAI(temperature0) | StrOutputParser() # 工作流组装 def self_correcting_rag(question, max_attempts3): context retriever.get_relevant_documents(question) attempt 0 current_answer initial_rag_chain.invoke({question: question, context: context}) while attempt max_attempts: feedback verification_chain.invoke({ context: context, question: question, initial_answer: current_answer }) # 解析feedback判断是否通过 if feedback_passes(feedback): # 假设的解析判断函数 return current_answer else: current_answer revision_chain.invoke({ context: context, question: question, initial_answer: current_answer, feedback: feedback }) attempt 1 return current_answer # 返回最后一次修正的答案这种自我修正机制能大幅提升系统的可靠性和答案质量尤其适用于对准确性要求极高的场景但代价是增加了延迟和API调用成本。6.3 长上下文的影响与处理随着GPT-4 Turbo、Claude等模型支持128K甚至更长的上下文窗口一个自然的想法是是否可以直接将大量检索到的文档塞进上下文让LLM自己筛选这确实是一种方案被称为“检索后阅读”Retrieve-then-Read。优点简单避免了复杂的重排序和融合逻辑LLM可以自己权衡不同文档的重要性。缺点成本高输入长上下文会显著增加token消耗和API费用。性能下降有研究表明当相关信息被淹没在大量无关文本中时LLM的“大海捞针”能力会下降可能忽略关键信息。延迟增加处理长上下文需要更多计算时间。因此即使拥有长上下文窗口精选和压缩检索结果仍然是必要的。你可以先使用重排序器选出最相关的5-10个文档然后再将它们送入LLM。对于极长的文档还可以在送入LLM前用一个更小的模型如gpt-3.5-turbo先对每个文档进行摘要再将摘要送入最终生成模型以节省token。7. 端到端评估如何衡量RAG系统的好坏构建RAG系统是一个迭代过程。没有评估你就无法知道你的优化是让系统变得更好还是更糟。评估可以分为人工评估和自动评估这里我们重点介绍自动评估框架。7.1 核心评估指标我们到底要测什么一个完整的RAG评估体系通常涵盖以下几个方面检索质量命中率 (Hit Rate)对于一组问题正确答案所在的文档被检索到的比例至少出现在top-k结果中。平均排序倒数 (Mean Reciprocal Rank, MRR)衡量正确答案在检索结果中的排名。排名越靠前分数越高。生成质量事实一致性 (Faithfulness)生成的答案是否严格基于提供的上下文有没有“幻觉”出上下文不存在的信息这是RAG最重要的指标之一。答案相关性 (Answer Relevance)生成的答案是否直接回答了问题是否包含无关信息上下文相关性 (Context Relevance)检索到的上下文对于回答问题是否相关是否包含大量冗余或无关信息整体系统指标延迟 (Latency)从用户提问到收到答案的总时间。成本 (Cost)每次查询消耗的token费用。7.2 使用RAGAS进行自动化评估RAGAS (Retrieval-Augmented Generation Assessment) 是一个流行的开源框架专门用于评估RAG管道。它利用LLM作为评判员自动化地计算上述许多指标。# 安装 pip install ragas from ragas import evaluate from ragas.metrics import faithfulness, answer_relevance, context_recall, context_precision from datasets import Dataset import os # 1. 准备评估数据集 # 你需要一组样例问题 (question) 标准答案 (answer) 检索到的上下文 (contexts) 和RAG生成的答案 (generated_answer) eval_data { question: [什么是任务分解, LLM智能体的核心组件有哪些], answer: [任务分解是将复杂任务拆解为更小子目标的过程..., 核心组件包括规划、记忆和工具使用...], # ground truth contexts: [ [文档1关于任务分解的内容..., 文档2关于任务分解的内容...], [文档A关于智能体架构的内容..., 文档B关于记忆的内容...] ], generated_answer: [任务分解是让LLM将大任务拆小..., 智能体主要由规划、记忆等部分组成...] } dataset Dataset.from_dict(eval_data) # 2. 设置评估指标 metrics [ faithfulness, # 事实一致性 answer_relevance, # 答案相关性 context_recall, # 上下文召回率检索到的上下文是否包含标准答案 context_precision, # 上下文精确率检索到的上下文是否都与问题相关 ] # 3. 运行评估 (需要配置LLM如OpenAI) os.environ[OPENAI_API_KEY] your-key from langchain_openai import ChatOpenAI from ragas.llms import LangchainLLM langchain_llm LangchainLLM(ChatOpenAI(modelgpt-3.5-turbo)) # 注意RAGAS新版本可能直接支持OpenAI请参考最新文档 result evaluate( datasetdataset, metricsmetrics, llmlangchain_llm # 提供LLM用于评估 ) # 4. 查看结果 print(result) df result.to_pandas() # 转换为DataFrame便于分析RAGAS的评估依赖于LLM作为评判员其本身也有一定的波动性但对于快速迭代和对比不同配置如不同分块大小、不同重排序器的效果它是一个极其强大的工具。7.3 使用DeepEval进行快速评估DeepEval是另一个优秀的评估框架它提供了更丰富的开箱即用指标和易于集成的测试套件。# 安装 pip install deepeval from deepeval import evaluate from deepeval.metrics import AnswerRelevancyMetric, FaithfulnessMetric, ContextualRelevancyMetric from deepeval.test_case import LLMTestCase # 为每个测试样例创建一个 LLMTestCase test_case LLMTestCase( input什么是任务分解, actual_output任务分解是让LLM将大任务拆小..., # 你的RAG系统生成的答案 expected_output任务分解是将复杂任务拆解为更小子目标的过程..., # 标准答案 context[文档1关于任务分解的内容..., 文档2关于任务分解的内容...] # 检索到的上下文 ) # 定义要评估的指标 answer_relevancy_metric AnswerRelevancyMetric(threshold0.7) faithfulness_metric FaithfulnessMetric(threshold0.7) contextual_relevancy_metric ContextualRelevancyMetric(threshold0.7) # 运行评估 evaluate( test_cases[test_case], metrics[answer_relevancy_metric, faithfulness_metric, contextual_relevancy_metric] ) # 查看指标是否通过 print(fAnswer Relevancy: {answer_relevancy_metric.score}, Pass: {answer_relevancy_metric.is_successful()}) print(fFaithfulness: {faithfulness_metric.score}, Pass: {faithfulness_metric.is_successful()})DeepEval的优势在于它提供了明确的通过/失败阈值并且可以轻松集成到CI/CD管道中实现RAG系统的自动化测试。评估实践建议构建高质量的测试集这是评估的基石。测试集应包含多样化的、有代表性的问题并配有标准答案和相关的源文档引用。结合自动与人工评估自动评估如RAGAS适合快速迭代和回归测试。但对于关键场景或衡量答案的细微差别定期进行人工评估是必不可少的。进行A/B测试当你想引入一个新组件如一个新的重排序器时最好的方法是进行A/B测试。将流量的一部分导向新系统对比关键指标如答案好评率、用户停留时间的变化。监控生产环境除了离线评估还需要在生产环境监控真实用户的反馈、失败案例和延迟指标这能发现离线测试中无法预见的问题。构建一个生产级的RAG系统远不止是拼接几个API调用。它要求你对数据预处理、语义搜索、提示工程、LLM行为以及系统评估都有深入的理解。这个rag-ecosystem项目试图为你描绘出一张完整的地图从坚实的地基基础RAG到精密的架构高级索引、智能路由再到确保质量的监控体系评估。每一个环节的优化都可能带来显著的性能提升。在实际操作中我的体会是没有银弹你需要根据自己数据的特点、业务的要求和资源的约束从这张地图中选择合适的工具和技术进行组合与调优。最开始的简单实现能快速验证想法而后续的每一次迭代都应该是数据驱动和评估导向的。