1. 从零到一:构建你的第一个RAG智能体
如果你最近在折腾大语言模型应用,大概率听过RAG这个词。它全称是检索增强生成,听起来挺学术,但说白了就是一种让AI“更懂你”的技术。想象一下,你有一个无所不知的助手,但它只记得训练数据里的通用知识。当你问它“我们公司上周三的销售报告里,华东区的数据怎么样?”时,它大概率会懵掉。RAG要解决的,就是这个“最后一公里”的问题——让通用大模型能够访问并理解你私有的、最新的、特定领域的数据。
我花了不少时间研究GitHub上各种RAG项目,发现很多要么过于简单,就是个玩具Demo;要么过于复杂,堆砌了各种前沿论文里的技术,让人望而却步。直到我深度体验了bragai/bRAG-langchain这个项目,它提供了一个从入门到进阶的绝佳路径。这个项目不是简单地调用几个API,而是用一系列Jupyter Notebook,手把手带你拆解RAG的每一个核心组件,从最基础的文档加载、向量检索,一直讲到多查询、路由、重排序这些高级玩法。它更像是一本结构清晰的实战手册,而不是一堆散乱的代码。
所以,我决定结合这个项目的精华,以及我自己在搭建企业级问答系统时踩过的坑,写一篇详尽的指南。无论你是刚入门的新手,想理解RAG到底是怎么跑起来的;还是有一定经验的开发者,希望优化现有系统的召回效果和回答质量,这篇文章都会给你带来实实在在的收获。我们会从最核心的“检索-增强-生成”流程讲起,然后一步步深入到如何让这个流程变得更聪明、更精准。
2. RAG核心架构拆解:不只是“向量搜索+LLM”
很多人对RAG的第一印象就是“把文档切成块,变成向量存起来,用户提问时搜一下最相似的块,然后塞给大模型去生成答案”。这个理解没错,但它只描绘了最基础的骨架。一个健壮、可用的RAG系统,其内部远比这复杂。bRAG-langchain项目的结构就很好地体现了这一点,它没有一上来就扔给你一个“万能”的管道,而是引导你去思考每个环节的“为什么”。
2.1 检索(Retrieval):寻找相关信息的艺术
检索是整个RAG的基石,如果这一步找回来的信息是垃圾,那后面大模型再怎么“增强”,输出的也只能是精致的垃圾。基础的检索就是基于嵌入向量的相似度搜索,但这里有几个关键决策点直接决定了效果上限。
文档分块是门学问。你不能简单粗暴地按固定字符数切割。想象一下,你有一份PDF合同,如果切割点正好在某个关键条款的中间,那么检索到的片段就是残缺的,模型无法理解。常见的策略有:
- 固定大小重叠分块:这是最常用的方法。比如每块500个字符,块与块之间重叠50个字符。这能保证一定的上下文连贯性,但可能仍然会切断完整的句子或段落。
- 基于语义的分块:利用句子嵌入模型,在语义发生较大转变的地方进行切割。这更智能,但对模型有要求。
- 递归分块:先按较大的分隔符(如
\n\n)分,如果块还是太大,再用较小的分隔符(如\n,。,;)继续分。这种方法能更好地保持文本的结构。
在bRAG-langchain的[4]_rag_indexing_and_advanced_retrieval.ipynb中,作者提到了“多表征索引”的概念。这给了我很大启发:为什么只用一种方式表示文档?我们可以同时为文档存储多种形式的“摘要”或“表征”。例如:
- 原始文本块:用于最终生成答案时提供最详细的上下文。
- 摘要向量:为每个文本块生成一个简短的摘要,并对摘要进行向量化。检索时,先快速匹配摘要,再定位到原文。这相当于建立了一个“索引的索引”,能提升检索速度,尤其是当原文块很大时。
- 假设性问题:针对每个文本块,让LLM生成几个可能提出的问题,并对这些问题进行向量化。当用户提出一个真实问题时,我们是在匹配“可能被提出的问题”,这往往比直接匹配文档内容更贴近用户的查询意图。
这种“多表征”思路,本质上是在用空间换时间和精度,是工程上一种非常实用的优化策略。
2.2 增强(Augmentation):为模型准备“弹药”
检索到相关文档片段后,不能直接扔给LLM。我们需要把这些“弹药”进行整理和组装,形成一个有效的提示。这个过程就是增强。
最基础的增强方式就是简单的拼接:请根据以下上下文回答问题:\n[上下文1]\n[上下文2]\n问题:[用户问题]。但这远远不够。
上下文排序与过滤:检索系统可能返回10个相关片段,我们需要决定哪些放在前面,哪些可能因为相关性太低而直接丢弃。这里就引入了“重排序”技术。基础的重排序可以基于检索时的相似度分数,但更高级的做法是使用一个专门的、更精细的“重排序模型”。比如Cohere就提供了专门的rerank API,它比通用的嵌入模型更能理解问题和文档之间的细微相关性。在[5]_rag_retrieval_and_reranking.ipynb中,就演示了如何集成Cohere的重排序器,让最相关的信息优先出现在上下文中。
上下文压缩:LLM有上下文窗口限制,我们总想塞进去更多信息,但无脑堆砌会导致模型注意力分散,甚至忽略关键信息。上下文压缩就是在不丢失核心信息的前提下,精简检索到的内容。例如,可以让另一个LLM(或同一个LLM)对检索到的多个片段进行总结,只把总结后的精要信息放入最终提示。或者,使用更高级的“提取式”方法,直接从片段中摘取与问题最相关的句子。
注意:增强环节最容易犯的错误是“上下文污染”。如果你不小心把不相关甚至矛盾的文档片段塞给了LLM,它很可能会基于这些错误信息生成“一本正经的胡说八道”。因此,在拼接上下文前,一定要有严格的相关性过滤和排序机制。
2.3 生成(Generation):让模型“好好说话”
到了最后一步,似乎就是调用LLM API了。但这里也有讲究。你的提示词设计,直接决定了模型是复读机还是分析师。
指令设计:除了提供上下文和问题,你必须明确告诉模型应该以什么角色、什么格式、基于什么原则来回答。例如:
你是一个专业的客服助手。请严格根据提供的上下文信息来回答问题。 如果上下文中的信息不足以回答问题,请明确告知“根据现有资料,无法回答该问题”,不要编造信息。 请用清晰、有条理的列表或段落格式回复。 上下文: {context} 问题: {question}这个简单的指令,能极大地减少模型的幻觉(胡编乱造)倾向。
温度参数:对于事实性问答,通常建议将温度(temperature)设置得较低(如0.1或0),这样模型的输出更确定、更可预测,减少随机性。而对于创意写作,则可以调高。
在bRAG-langchain的进阶章节中,还提到了Self-RAG和CRAG等概念。这些属于更前沿的“自省式”生成。例如,Self-RAG会让模型在生成过程中,主动判断当前是否需要检索更多信息、检索到的信息是否相关、自己生成的这句话是否有依据等,并打上特殊的标记。这相当于让模型自己监督自己,进一步提升了回答的可靠性和可解释性。虽然实现起来更复杂,但代表了RAG系统走向更自主、更可靠的方向。
3. 手把手实战:用LangChain构建基础RAG管道
理论讲得再多,不如动手跑一遍。我们完全遵循bRAG-langchain项目中[1]_rag_setup_overview.ipynb的思路,但我会补充更多细节和避坑指南。假设我们的目标是为一份产品说明书搭建一个问答系统。
3.1 环境准备与依赖安装
首先,强烈建议使用Python 3.11+版本,这是很多AI库兼容性最好的版本。使用虚拟环境是必须的,它能避免包版本冲突。
# 1. 创建并进入项目目录 mkdir my_rag_project && cd my_rag_project # 2. 创建虚拟环境(以Python3.11为例) python3.11 -m venv venv # 3. 激活虚拟环境 # macOS/Linux: source venv/bin/activate # Windows: # venv\Scripts\activate # 4. 升级pip pip install --upgrade pip # 5. 安装核心依赖 pip install langchain langchain-openai langchain-community chromadb pypdf tiktoken这里解释一下几个关键包:
langchain: 框架本体,提供了构建链式应用的各种组件。langchain-openai: OpenAI模型的官方集成。langchain-community: 包含大量第三方集成,如文档加载器、向量数据库等。chromadb: 一个轻量级、开源的向量数据库,非常适合本地开发和实验。pypdf: 用于加载PDF文档。tiktoken: OpenAI用于计算token的工具,帮助我们控制输入长度。
3.2 文档加载与预处理
我们准备一个名为product_manual.pdf的说明书文件。
# 导入必要的模块 from langchain_community.document_loaders import PyPDFLoader from langchain.text_splitter import RecursiveCharacterTextSplitter # 1. 加载文档 loader = PyPDFLoader("./product_manual.pdf") documents = loader.load() print(f"加载了 {len(documents)} 页文档。") # 2. 分割文本 text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, # 每个块的最大字符数 chunk_overlap=50, # 块之间的重叠字符数 length_function=len, separators=["\n\n", "\n", "。", ";", ",", " ", ""] # 递归分割的分隔符优先级 ) chunks = text_splitter.split_documents(documents) print(f"将文档分割成了 {len(chunks)} 个文本块。") # 查看第一个块的内容和元数据(元数据里会保留页码等信息) print(chunks[0].page_content[:200]) # 打印前200个字符 print(chunks[0].metadata)实操心得:
chunk_size需要权衡。太小会丢失上下文,太大会降低检索精度并占用更多LLM上下文窗口。对于通用文档,500-1000是个不错的起点。你可以根据你的文档平均段落长度进行调整。chunk_overlap非常重要!它确保了语义的连贯性,避免一个完整的句子被硬生生切断。通常设置为chunk_size的10%-20%。- 加载后的
documents是一个列表,每个元素都是一个Document对象,包含page_content(文本内容)和metadata(元数据,如来源、页码)。在分割时,RecursiveCharacterTextSplitter会尽量把元数据继承给每个子块,这对于后续追溯答案来源至关重要。
3.3 向量化与存储
接下来,我们需要把文本块变成向量,并存入向量数据库。
from langchain_openai import OpenAIEmbeddings from langchain_community.vectorstores import Chroma import os # 设置你的OpenAI API Key (务必从环境变量读取,不要硬编码在代码里) os.environ["OPENAI_API_KEY"] = "你的-api-key" # 1. 初始化嵌入模型 # 使用OpenAI的 text-embedding-3-small 模型,性价比高 embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 2. 创建向量数据库并持久化 # persist_directory 指定数据库存储的本地路径 vectorstore = Chroma.from_documents( documents=chunks, embedding=embeddings, persist_directory="./chroma_db" # 数据将保存在这个目录 ) print("向量数据库创建并持久化完成。") # 3. 测试检索 query = "产品如何充电?" docs = vectorstore.similarity_search(query, k=3) # 检索最相似的3个块 print(f"针对问题 '{query}', 检索到 {len(docs)} 个相关文档块:") for i, doc in enumerate(docs): print(f"\n--- 结果 {i+1} (相关性分数: {doc.metadata.get('score', 'N/A')}) ---") print(doc.page_content[:300]) # 打印每个块的前300字符关键点解析:
- 嵌入模型选择:
text-embedding-3-small是OpenAI推出的新一代小尺寸模型,在效果和速度、成本上取得了很好的平衡,是大多数场景的首选。如果你对多语言支持有要求,可以考虑text-embedding-3-large或专门的多语言模型。 - 向量数据库:这里用了ChromaDB,它简单易用,支持本地持久化。在生产环境中,你可能会考虑更 scalable 的方案,如 Pinecone、Weaviate 或 Qdrant。
bRAG-langchain项目也演示了如何集成Pinecone。 similarity_search是最基础的检索方法,它计算查询向量与所有文档向量的余弦相似度,返回最相似的k个。返回的每个文档都包含了原始的page_content和metadata。
3.4 组装RAG链
现在,我们把检索器和语言模型组装起来,形成一个完整的问答链。
from langchain_openai import ChatOpenAI from langchain.chains import RetrievalQA from langchain.prompts import PromptTemplate # 1. 初始化LLM # 使用 gpt-3.5-turbo 模型,对于问答任务足够且成本较低 llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0) # 2. 从持久化的目录加载向量数据库 # 注意:这里我们重新加载,而不是重新创建,避免重复计算嵌入 vectorstore = Chroma( persist_directory="./chroma_db", embedding_function=embeddings ) # 3. 将向量数据库转换为检索器 # search_kwargs 可以控制检索的细节,比如返回数量 retriever = vectorstore.as_retriever(search_kwargs={"k": 4}) # 4. 定义自定义提示模板 # 这个模板明确要求模型基于上下文回答,并处理未知情况 prompt_template = """你是一个专业的产品支持助手。请严格根据以下提供的上下文信息来回答用户的问题。 如果你不知道答案,就诚实地回答你不知道,不要试图编造答案。 请用清晰、有条理的方式回答。 上下文: {context} 问题: {question} 请根据上下文给出答案:""" PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) # 5. 创建检索问答链 qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 最简单的方式,将所有检索到的上下文“塞”进提示 retriever=retriever, chain_type_kwargs={"prompt": PROMPT}, # 使用我们自定义的提示 return_source_documents=True # 非常重要!返回用于生成答案的源文档 ) # 6. 进行问答 result = qa_chain.invoke({"query": "这款产品的电池续航时间是多久?"}) print("答案:", result["result"]) print("\n--- 引用的源文档 ---") for i, doc in enumerate(result["source_documents"]): print(f"\n[文档 {i+1}], 来源:{doc.metadata.get('source', 'N/A')}, 页码:{doc.metadata.get('page', 'N/A')}") print(doc.page_content[:200])核心环节解析:
RetrievalQA是LangChain提供的一个高层抽象,它封装了“检索-增强-生成”的完整流程。chain_type="stuff"是最直接的方法,它把所有检索到的文档内容拼接起来,一次性发送给LLM。当文档总长度超过模型上下文窗口时,这种方法会失败。对于更长的上下文,可以考虑"map_reduce"或"refine"等链类型,它们会以更复杂的方式处理多文档。return_source_documents=True这个参数至关重要!它让我们能够追溯答案的来源,验证答案的准确性,这是构建可信RAG系统的关键。用户看到答案时,如果还能看到引用的原文出处,信任度会大大提升。- 自定义
PROMPT是控制模型行为的最有效手段。清晰的指令能显著减少幻觉。
至此,一个最基础但完全可用的RAG问答系统就搭建完成了。你可以运行代码,用你的产品说明书PDF进行测试。接下来,我们将探索如何让这个系统变得更强大。
4. 进阶优化实战:多查询、路由与重排序
基础RAG管道能跑通,但效果往往差强人意。bRAG-langchain项目后续的Notebook揭示了一系列提升效果的进阶技术。我们来逐一拆解并实现。
4.1 多查询检索:从多个角度寻找答案
用户的一个问题,可能对应文档中多种不同的表述。例如,“怎么开机?”也可能被表述为“如何启动设备?”、“电源按钮在哪里?”。单一查询可能无法覆盖所有相关片段。
思路:利用LLM,将用户的原始问题,扩展成多个语义相近但表述不同的问题,然后用所有问题去检索,最后合并结果。这被称为“多查询”或“查询扩展”。
from langchain.chains import LLMChain from langchain.prompts import PromptTemplate # 1. 定义一个生成多个查询的链 multi_query_prompt = PromptTemplate( input_variables=["question"], template="""你是一个专业的查询改写助手。针对用户的问题,生成三个不同的但语义相似的查询,用于在文档库中进行检索。每个查询应该从稍有不同的角度切入。 用户原始问题:{question} 请以JSON格式输出,包含一个名为”queries”的列表。 输出格式示例:{{"queries": ["查询1", "查询2", "查询3"]}} 只输出JSON,不要有其他内容。""" ) multi_query_llm = ChatOpenAI(model="gpt-3.5-turbo", temperature=0.7) # 温度可以稍高,鼓励多样性 multi_query_chain = LLMChain(llm=multi_query_llm, prompt=multi_query_prompt) # 2. 执行多查询生成 original_question = "产品出现蓝屏怎么办?" generated = multi_query_chain.run(original_question) import json try: queries = json.loads(generated)["queries"] print("生成的多个查询:", queries) except json.JSONDecodeError: # 如果LLM输出不标准,降级处理 queries = [original_question] print("解析失败,使用原始查询。") # 3. 用所有查询进行检索,并合并去重 all_docs = [] for q in queries: retriever = vectorstore.as_retriever(search_kwargs={"k": 2}) # 每个查询取前2个 docs = retriever.get_relevant_documents(q) all_docs.extend(docs) # 基于内容去重(简单方法:根据文本内容或元数据) unique_docs = [] seen_contents = set() for doc in all_docs: # 使用前100字符作为去重标识,更严谨的做法可以用哈希 content_id = doc.page_content[:100] + str(doc.metadata.get("page", "")) if content_id not in seen_contents: seen_contents.add(content_id) unique_docs.append(doc) print(f"合并去重后得到 {len(unique_docs)} 个唯一文档块。") # 4. 将合并后的文档作为上下文,调用LLM生成最终答案 context = "\n\n".join([doc.page_content for doc in unique_docs]) final_prompt = f"""基于以下上下文,回答用户问题。如果上下文信息不足,请说明。 上下文: {context} 问题: {original_question} 答案:""" final_answer = llm.invoke(final_prompt).content print("最终答案:", final_answer)这种方法能显著提高召回率,确保更多相关的文档片段被找到,尤其适用于文档表述多样或用户问题模糊的场景。
4.2 查询路由:将问题引导至正确的知识库
如果你的数据源不止一个(比如有产品手册、技术白皮书、客服对话记录等多个文档集合),盲目地在所有数据中检索不仅效率低,还可能引入噪声。查询路由就是根据问题的意图,将其自动引导到最相关的数据源。
bRAG-langchain的[3]_rag_routing_and_query_construction.ipynb提到了两种路由:基于函数/规则的路由和基于语义的路由。
基于语义的路由示例:假设我们有两个向量库,一个存数学知识,一个存物理知识。
# 假设我们已经创建了两个向量库:math_store 和 physics_store math_retriever = math_store.as_retriever() physics_retriever = physics_store.as_retriever() # 定义一个路由链 router_prompt = PromptTemplate( input_variables=["question"], template="""判断以下问题属于哪个学科领域。只输出一个单词:'math' 或 'physics'。 问题:{question} 领域:""" ) router_chain = LLMChain(llm=ChatOpenAI(temperature=0), prompt=router_prompt) def route_question(question: str): """根据问题路由到对应的检索器""" domain = router_chain.run(question).strip().lower() if "math" in domain: print(f"问题『{question}』被路由到数学知识库。") return math_retriever elif "physics" in domain: print(f"问题『{question}』被路由到物理知识库。") return physics_retriever else: print(f"无法确定领域,使用默认检索器。") return default_retriever # 一个全局检索器 # 使用路由 user_q = "牛顿第二定律的公式是什么?" target_retriever = route_question(user_q) relevant_docs = target_retriever.get_relevant_documents(user_q) # ... 后续用 retrieved_docs 生成答案这种方法构建了一个简单的“决策层”,让系统变得更智能。你可以将其扩展到更多的数据源和更复杂的决策逻辑上。
4.3 重排序:让最相关的信息排在前面
基础向量检索返回的结果是按余弦相似度排序的,但这个分数不一定完美反映“答案相关性”。一个专门训练过的“重排序模型”可以做得更好。我们以集成Cohere的重排序为例(需要Cohere API Key)。
# 首先,用基础检索器获取较多的候选文档(比如20个) base_retriever = vectorstore.as_retriever(search_kwargs={"k": 20}) candidate_docs = base_retriever.get_relevant_documents(query) # 假设我们有一个重排序函数(这里用Cohere API示例) def rerank_docs_with_cohere(query: str, docs: list, top_k: int = 5): """ 使用Cohere的重排序API对文档进行重新排序。 需要安装 cohere 库并设置 COHERE_API_KEY。 """ import cohere co = cohere.Client(os.environ.get("COHERE_API_KEY")) # 准备文档文本 doc_texts = [doc.page_content for doc in docs] # 调用重排序API rerank_results = co.rerank( model="rerank-english-v2.0", # 选择适合的模型 query=query, documents=doc_texts, top_n=top_k, ) # 根据新的排序,重新组织文档列表 reranked_docs = [] for result in rerank_results.results: original_doc_index = result.index reranked_docs.append(docs[original_doc_index]) # 你可以把新的相关性分数也存到文档元数据中 docs[original_doc_index].metadata["rerank_score"] = result.relevance_score return reranked_docs # 应用重排序 reranked_docs = rerank_docs_with_cohere(query, candidate_docs, top_k=5) print("重排序后的前5个文档:") for i, doc in enumerate(reranked_docs): print(f"{i+1}. Score:{doc.metadata.get('rerank_score', 'N/A')} - {doc.page_content[:100]}...") # 使用重排序后的top N个文档作为最终上下文 final_context_docs = reranked_docs[:3] # 取前3个重排序模型通常基于更复杂的交叉注意力机制,能更精细地衡量查询和文档之间的相关性,尤其擅长处理关键词匹配不直接但语义相关的情况。虽然多了一次API调用,但能显著提升最终答案的质量。
5. 生产环境部署与性能调优指南
当你完成了本地原型开发,下一步就是考虑如何将它部署成一个稳定、可用的服务,并应对真实场景中的挑战。bRAG-langchain项目更多侧重于算法原型,而生产部署涉及另一套工程实践。
5.1 架构设计:从笔记本到服务
你不能让用户直接运行Jupyter Notebook。一个典型的生产级RAG后端架构如下:
用户请求 -> [Web API (FastAPI/Flask)] -> [RAG应用核心] -> [向量数据库] & [LLM API] <- [JSON响应(答案+引用)] <-- Web框架:使用FastAPI或Flask构建RESTful API。FastAPI性能好,自动生成API文档,是当前主流选择。
- 应用核心:将你在Notebook中编写的RAG链(检索器、LLM、提示模板等)封装成可调用的类或函数。这里要特别注意异步编程,因为LLM API调用和向量检索可能是I/O密集型操作,使用
async/await可以大幅提升并发处理能力。 - 向量数据库:本地ChromaDB适合开发,生产环境建议使用Pinecone、Weaviate、Qdrant或Milvus等支持分布式、可扩展的云服务或自托管方案。它们提供更快的搜索速度、更大的存储容量和更丰富的过滤功能。
- 缓存层:对于相同或相似的问题,重复计算嵌入和调用LLM是巨大的浪费。引入缓存可以极大降低成本、提升响应速度。可以考虑:
- LLM响应缓存:使用
langchain.cache配合SQLiteCache或RedisCache。 - 向量检索缓存:对查询的嵌入向量进行哈希,缓存其检索结果。
- LLM响应缓存:使用
- 监控与日志:集成LangSmith(LangChain官方平台)或自定义日志,记录每一次问答的查询、检索到的文档、生成的答案、耗时和Token使用量。这对于调试、优化和成本核算至关重要。
5.2 性能瓶颈分析与优化
RAG系统的性能瓶颈通常出现在以下几个环节:
| 环节 | 潜在瓶颈 | 优化策略 |
|---|---|---|
| 文档处理 | 大量PDF/Word解析、分块、向量化耗时极长。 | 异步批处理:使用Celery或Django Q等任务队列,将文档预处理任务后台化。增量更新:只对新增或修改的文档进行处理,而不是全量重建索引。 |
| 向量检索 | 文档库巨大(百万级以上)时,相似度搜索变慢。 | 使用专业向量数据库:它们内置了HNSW、IVF-PQ等近似最近邻搜索算法,在精度和速度间取得平衡。索引优化:调整索引参数(如HNSW的ef_construction和M)。过滤:结合元数据过滤(如日期、类别)缩小搜索范围。 |
| LLM调用 | API网络延迟、Token消耗成本、速率限制。 | 缓存:如前所述。流式响应:对于长答案,使用流式传输让用户边生成边看到结果,提升体验。模型选择:在效果可接受的情况下,使用更小、更快的模型(如GPT-3.5-Turbo vs GPT-4)。批量处理:如有多个独立问题,可合并到一个请求中批量处理(需LLM支持)。 |
| 整体链路 | 串行执行导致总耗时等于各环节之和。 | 并行化:检索文档、生成多查询、调用重排序模型等步骤,如果彼此独立,可以并行执行。 |
一个简单的异步FastAPI服务示例:
# main.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List import asyncio from your_rag_module import YourRAGChain # 假设你把RAG逻辑封装在了这里 app = FastAPI() rag_chain = YourRAGChain() # 初始化你的RAG链 class QueryRequest(BaseModel): question: str top_k: int = 4 class SourceDoc(BaseModel): content: str source: str page: int class QueryResponse(BaseModel): answer: str sources: List[SourceDoc] latency: float @app.post("/ask", response_model=QueryResponse) async def ask_question(request: QueryRequest): import time start_time = time.time() try: # 异步调用你的RAG链 result = await rag_chain.aget_answer(request.question, request.top_k) latency = time.time() - start_time # 格式化源文档 sources = [] for doc in result["source_documents"]: sources.append(SourceDoc( content=doc.page_content[:500], # 只返回片段 source=doc.metadata.get("source", "unknown"), page=doc.metadata.get("page", 0) )) return QueryResponse( answer=result["answer"], sources=sources, latency=round(latency, 3) ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) # 运行: uvicorn main:app --reload5.3 效果评估与持续迭代
部署上线只是开始,你需要一套方法来评估和优化系统效果。
- 人工评估:构建一个包含各种类型问题的测试集,定期让人工评审员对答案的准确性、相关性、完整性和流畅性进行打分。这是最可靠但成本最高的方法。
- 自动评估指标:
- 检索召回率:对于有标准答案的问题,检查标准答案所在的文档块是否被检索到。
- 答案相似度:使用句子嵌入模型(如
text-embedding-3-small)计算生成答案与标准答案的余弦相似度。 - LLM-as-a-Judge:用另一个更强大的LLM(如GPT-4)来评判当前系统答案的质量。可以设计提示词让GPT-4从多个维度打分。
bRAG-langchain项目后续也可以引入这种评估循环。
- A/B测试:如果你尝试了新的分块策略、检索器或提示词,可以将其与旧版本进行A/B测试,通过实际用户反馈或上述指标来判断哪个更好。
- 反馈循环:在产品界面提供“答案是否有用?”的反馈按钮。收集到的负面反馈可以用于困难样本挖掘,找出系统薄弱环节,有针对性地补充数据或调整模型。
构建一个优秀的RAG系统是一个持续迭代的过程。从bRAG-langchain这样的优秀项目入手,理解每个模块的原理,然后结合具体的业务数据和场景进行打磨、优化和工程化,你才能真正打造出一个解决实际问题的AI应用。记住,没有一劳永逸的“最佳实践”,只有最适合你当前场景的“权衡之选”。多实验,多评估,数据驱动决策,是通往成功的不二法门。