当前位置: 首页 > news >正文

从零构建RAG系统:核心流程、代码实现与调优指南

1. 项目概述:从零构建一个RAG系统意味着什么?

最近在GitHub上看到一个挺有意思的项目,叫pguso/rag-from-scratch。光看名字,很多朋友可能就明白了,这是一个关于“从零开始构建RAG”的仓库。RAG,也就是检索增强生成,可以说是当前大语言模型应用落地最火热、最实用的范式之一。但市面上的教程和框架,要么是直接调用封装好的API,黑盒感太强;要么就是理论讲得天花乱坠,但一上手就不知道代码该怎么写。这个项目恰好戳中了这个痛点:它不依赖任何重量级的框架,试图用最基础的代码,把RAG的核心流程给串起来,让你能真正看清里面的“齿轮”是怎么转动的。

我自己在尝试将大模型接入内部知识库或者构建智能问答助手时,也经历过这个阶段。直接用LangChain或者LlamaIndex固然快,但一旦出了点奇怪的问题,比如检索结果总是不相关,或者生成的答案胡言乱语,排查起来就非常头疼,因为你不确定问题是出在向量化、检索器还是提示词工程上。rag-from-scratch项目的价值就在于“拆解”。它就像一份手把手的汽车发动机拆解手册,不是为了让你马上造一台车,而是让你理解发动机的进气、压缩、做功、排气四个冲程分别对应什么代码,每个活塞是怎么运动的。通过这个项目,你能获得对RAG技术栈最直接的掌控感,之后无论是用成熟框架还是自己魔改,心里都会有底。

简单来说,这个项目适合两类人:一是对RAG感兴趣,但被各种高层框架搞得有点晕的入门者;二是已经有一定经验,想深入原理,优化自家RAG系统效果的开发者。它不追求功能的大而全,而是追求逻辑的清晰与透明。接下来,我就结合这个项目的思路,以及我自己在实际操作中的经验,来拆解一下从零构建一个RAG系统,到底需要关注哪些核心环节,以及每个环节有哪些容易踩坑的细节。

2. 核心流程拆解:RAG的四大核心支柱

一个最基本的RAG系统,可以抽象为四个顺序执行的阶段:文档加载与处理、文本向量化、向量检索与排序、提示构建与生成。pguso/rag-from-scratch项目正是围绕这四个支柱来组织代码的。我们逐一来看,每个部分在“从零开始”的语境下,意味着要做什么样的技术选型和实现。

2.1 文档加载与分块:不只是读取文件那么简单

这是流水线的第一步,但往往被轻视。你的原始数据可能是PDF、Word、TXT、Markdown,甚至是网页。从零开始,意味着你需要自己处理这些格式的解析。

文本提取:对于简单的TXT和Markdown,Python内置的open函数就够了。但对于PDF,你可能需要用到PyPDF2pdfplumber;对于Word,python-docx是标准选择;网页则可以用BeautifulSoup。这里的“从零”不是让你自己写解析器,而是让你明确地引入并调用这些专门的库,理解它们输出的纯文本可能包含的噪音(如页眉页脚、无关链接)。

注意:不同解析库对同一份PDF的提取效果可能天差地别。PyPDF2对某些扫描版PDF提取效果很差,而pdfplumber在表格提取上更有优势。在实际项目中,往往需要根据文档类型混合使用多种工具。

文本分块:这是至关重要的一步,直接影响到后续检索的精度。你不能把一整本书作为一个向量存进去,那样检索毫无意义;也不能分得太碎,导致语义不完整。常见的策略有:

  • 固定大小分块:比如每256或512个字符一块。简单,但可能切断一个完整的句子或概念。
  • 按分隔符分块:比如按“\n\n”(段落)、句号、分号等。更符合语言结构。
  • 语义分块:使用模型判断句子间的语义连贯性,在语义边界处切割。效果最好,但实现最复杂。

在“从零开始”的项目中,通常会先实现按段落或固定大小的分块。这里有一个关键技巧:重叠分块。比如块大小设为500字符,重叠设为50字符。这样可以避免一个关键信息恰好被切成两半而丢失,是提升召回率的有效手段。

# 一个简单的按固定大小+重叠分块的示例 def split_text_with_overlap(text, chunk_size=500, overlap=50): chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] chunks.append(chunk) start += chunk_size - overlap # 移动步长减去重叠部分 return chunks

2.2 文本向量化:把文字变成机器能“计算”的数字

这是RAG的“魔法”发生的地方。我们需要一个嵌入模型,将每一段文本(块)转换成一个高维度的向量(比如384维、768维或1024维)。这个向量就像是这段文本在语义空间中的“坐标”,语义相近的文本,其向量的“距离”也会很近。

模型选型:从零开始,你面临几个选择:

  1. 使用在线API:如OpenAI的text-embedding-ada-002,简单稳定,效果有保障,但会产生费用和网络延迟。
  2. 使用本地开源模型:如sentence-transformers库提供的模型(如all-MiniLM-L6-v2)。这是“从零开始”项目更常见的选择,因为它完全离线、免费,且能让你深入控制。
# 使用 sentence-transformers 生成嵌入向量 from sentence_transformers import SentenceTransformer model = SentenceTransformer('all-MiniLM-L6-v2') chunks = ["这是第一段文本。", "这是另一个语义相似的句子。"] embeddings = model.encode(chunks) # 得到一个 numpy 数组,形状为 (2, 384)

关键考量

  • 维度:模型输出的向量维度。维度越高,通常表征能力越强,但存储和计算成本也越高。all-MiniLM-L6-v2是384维,在效果和效率间取得了很好的平衡。
  • 序列长度:模型能处理的最大文本长度(token数)。超出部分会被截断。你需要确保你的分块大小在这个限制内。
  • 归一化:许多向量检索库(如FAISS)要求向量是归一化的(即模长为1)。sentence-transformersencode方法默认返回的就是归一化后的向量,非常方便。

2.3 向量存储与检索:建立并查询你的“语义记忆库”

生成向量后,我们需要把它们存储起来,并实现高效的相似度搜索。这就是向量数据库的核心功能。在“从零开始”的项目里,我们通常不会自己实现复杂的近似最近邻搜索算法,而是借助一个轻量级库。

FAISS - Facebook AI Similarity Search:这是最常用的选择之一。它是一个高效的向量相似度搜索和聚类的库,特别适合在内存中操作。

import faiss import numpy as np # 假设 embeddings 是一个 numpy 数组,形状为 (n_samples, embedding_dim) embedding_dim = embeddings.shape[1] # 1. 创建索引。这里使用最基础的 IndexFlatIP(内积索引),因为我们用了归一化向量,内积等于余弦相似度。 index = faiss.IndexFlatIP(embedding_dim) # 2. 添加向量到索引。FAISS 要求输入是 float32 类型。 embeddings = np.array(embeddings).astype('float32') index.add(embeddings) # 3. 检索:给定一个查询向量,返回最相似的k个结果 query_text = "用户提出的问题" query_embedding = model.encode([query_text]).astype('float32') k = 5 # 返回最相似的5个片段 distances, indices = index.search(query_embedding, k) # indices 是相似片段在原始列表中的位置,distances 是相似度分数(内积值,越大越相似)

检索过程解析

  1. 查询向量化:将用户的问题用同样的嵌入模型转化为向量。
  2. 相似度计算:在FAISS索引中,快速计算查询向量与库中所有向量的相似度(这里是余弦相似度,通过内积实现)。
  3. 排序与返回:按相似度从高到低排序,返回前k个最相关的文本片段及其索引。

实操心得IndexFlatIP是精确搜索,计算开销与数据量成正比。如果你的文档库非常大(比如超过10万条),构建索引和搜索会变慢。这时可以考虑使用IndexIVFFlat等量化索引,通过聚类来加速,但这会引入一点点精度损失。对于学习和中小型项目,IndexFlatIP完全够用。

2.4 提示工程与生成:让大模型基于“证据”说话

检索到相关的文本片段后,我们并不是直接把它们扔给用户,而是将它们作为“上下文”或“参考依据”,与大模型(LLM)的提示词结合起来,让模型生成最终答案。这是RAG区别于单纯搜索引擎的关键。

构建提示模板:一个典型的RAG提示词模板如下:

请基于以下提供的上下文信息,回答用户的问题。如果上下文信息不足以回答问题,请直接说“根据提供的信息,我无法回答这个问题”,不要编造信息。 上下文信息: {context} 用户问题:{question} 请给出专业、准确的回答:

这里,{context}就是我们检索到的、拼接起来的多个相关文本片段,{question}是用户原始问题。

调用大模型:和嵌入模型一样,你可以选择在线API(如OpenAI GPT, Anthropic Claude)或本地开源模型(如通过ollama运行的Llama 3,vllm部署的Qwen等)。

# 假设使用 OpenAI API(需安装openai库并设置API_KEY) from openai import OpenAI client = OpenAI() def generate_answer_with_context(question, context): prompt = f"""请基于以下提供的上下文信息,回答用户的问题。如果上下文信息不足以回答问题,请直接说“根据提供的信息,我无法回答这个问题”,不要编造信息。 上下文信息: {context} 用户问题:{question} 请给出专业、准确的回答:""" response = client.chat.completions.create( model="gpt-3.5-turbo", messages=[{"role": "user", "content": prompt}], temperature=0.1 # 温度调低,让输出更确定,更依赖上下文 ) return response.choices[0].message.content # 拼接检索到的上下文 retrieved_context = "\n\n".join([chunks[i] for i in indices[0]]) answer = generate_answer_with_context(query_text, retrieved_context)

关键技巧

  • 温度设置:在RAG中,通常将温度(temperature)设置得较低(如0.1),以鼓励模型严格遵循上下文,减少“自由发挥”和幻觉。
  • 上下文长度:大模型有上下文窗口限制。你需要确保检索到的所有片段拼接后,加上提示词模板,不超过这个限制。否则需要截断或进行二次精炼。
  • 引用来源:一个高级技巧是让模型在回答中注明依据来自哪个片段,这能极大增加可信度。可以在上下文中为每个片段添加一个简短的引用标识(如[1],[2]),并在提示词中要求模型引用这些标识。

3. 从零开始的完整实现链路与代码剖析

理解了四大支柱后,我们来看如何将它们串联成一个可运行的流水线。pguso/rag-from-scratch项目的代码结构通常就反映了这个流水线。下面我以一个处理多份PDF文档,并构建问答系统的简化示例,来展示每个环节的具体代码和思考。

3.1 环境准备与依赖安装

首先,我们需要一个干净的Python环境。建议使用condavenv创建虚拟环境。

# 创建并激活虚拟环境(以conda为例) conda create -n rag-scratch python=3.10 conda activate rag-scratch # 安装核心依赖 pip install pypdf2 sentence-transformers faiss-cpu openai # 如果使用GPU加速FAISS,可以安装 faiss-gpu # pip install faiss-gpu

这里我们选择了几个轻量级但核心的库:PyPDF2用于PDF解析,sentence-transformers用于本地向量化,faiss-cpu用于向量检索,openai作为生成模型的接口(你也可以替换为其他本地模型调用方式)。

3.2 文档处理模块实现

我们创建一个document_processor.py文件,封装文档加载和分块逻辑。

# document_processor.py import os from typing import List import PyPDF2 class DocumentProcessor: def __init__(self, chunk_size: int = 500, overlap: int = 50): self.chunk_size = chunk_size self.overlap = overlap def load_pdf(self, file_path: str) -> str: """从PDF文件中提取纯文本""" text = "" try: with open(file_path, 'rb') as file: reader = PyPDF2.PdfReader(file) for page_num in range(len(reader.pages)): page = reader.pages[page_num] text += page.extract_text() + "\n" except Exception as e: print(f"读取PDF文件 {file_path} 时出错: {e}") return text def split_text(self, text: str) -> List[str]: """使用滑动窗口进行固定大小+重叠分块""" if not text: return [] chunks = [] start = 0 text_length = len(text) while start < text_length: end = start + self.chunk_size # 确保不截断在一个单词中间(简单优化) while end < text_length and text[end] not in (' ', '\n', '.', '!', '?'): end -= 1 if end <= start: # 防止死循环 end = start + self.chunk_size chunk = text[start:end].strip() if chunk: # 忽略空块 chunks.append(chunk) start += self.chunk_size - self.overlap return chunks def process_directory(self, dir_path: str) -> List[str]: """处理一个目录下的所有PDF文件""" all_chunks = [] for filename in os.listdir(dir_path): if filename.endswith('.pdf'): full_path = os.path.join(dir_path, filename) print(f"正在处理: {filename}") raw_text = self.load_pdf(full_path) chunks = self.split_text(raw_text) all_chunks.extend(chunks) print(f" 生成 {len(chunks)} 个文本块") return all_chunks

这个类做了几件事:load_pdf方法用PyPDF2逐页提取文本;split_text方法实现了带简单边界优化的重叠分块;process_directory则批量处理一个文件夹。这里的分块逻辑还可以进一步优化,比如优先按段落(\n\n)分割,不足chunk_size再合并。

3.3 向量化与索引构建模块

接下来,我们创建vector_store.py,负责将文本块转化为向量,并构建FAISS索引。

# vector_store.py import numpy as np import faiss from sentence_transformers import SentenceTransformer from typing import List import pickle import os class VectorStore: def __init__(self, model_name: str = 'all-MiniLM-L6-v2'): self.model = SentenceTransformer(model_name) self.index = None self.chunks = [] # 存储原始文本块,用于检索后还原 self.embedding_dim = self.model.get_sentence_embedding_dimension() def create_index(self, chunks: List[str]): """为文本块列表创建向量索引""" if not chunks: raise ValueError("文本块列表不能为空") print("正在生成文本向量...") # 1. 生成嵌入向量。encode方法默认返回归一化的numpy数组。 embeddings = self.model.encode(chunks, show_progress_bar=True, convert_to_numpy=True) # 确保是float32类型,这是FAISS要求的 embeddings = np.array(embeddings).astype('float32') print(f"向量生成完毕,形状: {embeddings.shape}") # 2. 创建FAISS索引。使用内积索引,因为向量已归一化,内积=余弦相似度。 self.index = faiss.IndexFlatIP(self.embedding_dim) # 3. 将向量添加到索引 self.index.add(embeddings) # 保存原始文本块 self.chunks = chunks print(f"索引构建完成,共 {self.index.ntotal} 个向量。") def search(self, query: str, k: int = 5) -> List[tuple]: """检索与查询最相关的k个文本块""" if self.index is None: raise RuntimeError("索引尚未创建,请先调用 create_index 方法。") # 将查询文本向量化 query_embedding = self.model.encode([query], convert_to_numpy=True).astype('float32') # 执行搜索,返回相似度分数和索引 distances, indices = self.index.search(query_embedding, k) # 整理结果:[(相似度分数, 文本块), ...] results = [] for i, idx in enumerate(indices[0]): if idx != -1: # FAISS可能返回-1表示未找到足够结果 score = distances[0][i] text = self.chunks[idx] results.append((score, text)) return results def save(self, filepath: str): """保存索引和文本块到磁盘""" if self.index is None: raise RuntimeError("没有可保存的索引。") # 保存FAISS索引 faiss.write_index(self.index, filepath + '.index') # 保存文本块 with open(filepath + '.chunks.pkl', 'wb') as f: pickle.dump(self.chunks, f) print(f"索引已保存至 {filepath}.index") def load(self, filepath: str): """从磁盘加载索引和文本块""" # 加载FAISS索引 self.index = faiss.read_index(filepath + '.index') # 加载文本块 with open(filepath + '.chunks.pkl', 'rb') as f: self.chunks = pickle.load(f) self.embedding_dim = self.index.d print(f"索引已加载,共 {self.index.ntotal} 个向量。")

这个类封装了核心的向量化与检索功能。create_index方法一次性处理所有文本块,生成向量并构建索引。search方法接受一个查询字符串,返回最相关的文本块及其相似度分数。saveload方法则提供了持久化能力,避免每次启动都重新计算向量,这对于生产环境至关重要。

3.4 问答生成模块集成

最后,我们创建一个主程序main.py,将整个流程串联起来,并集成大模型生成答案。

# main.py from document_processor import DocumentProcessor from vector_store import VectorStore import os from openai import OpenAI from dotenv import load_dotenv # 用于加载环境变量中的API KEY # 加载环境变量(假设你的OPENAI_API_KEY保存在.env文件中) load_dotenv() class RAGSystem: def __init__(self, data_dir: str, index_save_path: str = "my_rag_index"): self.data_dir = data_dir self.index_save_path = index_save_path self.vector_store = VectorStore() self.llm_client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) # 检查是否已有保存的索引 if os.path.exists(index_save_path + '.index'): print("检测到已保存的索引,正在加载...") self.vector_store.load(index_save_path) else: print("未找到索引,开始构建...") self._build_index() def _build_index(self): """构建向量索引的完整流程""" processor = DocumentProcessor(chunk_size=400, overlap=80) all_chunks = processor.process_directory(self.data_dir) print(f"总共处理得到 {len(all_chunks)} 个文本块。") self.vector_store.create_index(all_chunks) self.vector_store.save(self.index_save_path) def ask(self, question: str, top_k: int = 3) -> str: """核心问答接口""" # 1. 检索相关上下文 print(f"正在检索与『{question}』相关的信息...") search_results = self.vector_store.search(question, k=top_k) if not search_results: return "抱歉,在知识库中未找到相关信息。" # 2. 构建上下文字符串 context_parts = [] for i, (score, text) in enumerate(search_results): # 可以在这里加入引用标识 context_parts.append(f"[{i+1}] {text}") context = "\n\n".join(context_parts) print(f"检索到 {len(search_results)} 条相关片段。") # 3. 构建提示词 prompt = f"""你是一个专业的问答助手。请严格根据以下提供的上下文信息来回答问题。如果上下文信息中没有答案,请直接说“根据提供的信息,我无法回答这个问题”,不要编造任何信息。 上下文信息: {context} 用户问题:{question} 请根据上下文,给出准确、简洁的回答:""" # 4. 调用大模型生成答案 try: response = self.llm_client.chat.completions.create( model="gpt-3.5-turbo", # 或 "gpt-4" messages=[{"role": "user", "content": prompt}], temperature=0.1, max_tokens=500 ) answer = response.choices[0].message.content return answer except Exception as e: return f"调用模型时出错: {e}" # 使用示例 if __name__ == "__main__": # 假设你的PDF文档都放在 ./data 目录下 data_directory = "./data" rag = RAGSystem(data_directory, "my_knowledge_base") while True: user_question = input("\n请输入您的问题 (输入 'quit' 退出): ") if user_question.lower() == 'quit': break answer = rag.ask(user_question) print(f"\n【回答】\n{answer}\n")

这个RAGSystem类做了完整的封装。初始化时,它会检查是否有已保存的索引,有则直接加载,极大加快启动速度。ask方法集成了检索、提示词构建和LLM调用。现在,你只需要将PDF文件放入./data文件夹,运行python main.py,就可以与你的文档进行对话了。

4. 性能调优与高级技巧探讨

一个能跑起来的RAG系统只是开始。要让它在实际应用中真正可靠、好用,还需要考虑很多优化点。这部分内容往往是“从零开始”项目不会深入涉及的,但却是工程实践中的重中之重。

4.1 提升检索质量的策略

检索是RAG的基石,检索结果不准,后面生成得再好也是空中楼阁。

1. 分块策略的精细化

  • 混合分块:不要只用固定大小。可以尝试先按段落分,对过长的段落再按句子或固定大小二次分割。这样能更好地保持语义完整性。
  • 基于语义的分块:使用更小的嵌入模型(如all-MiniLM-L6-v2本身)计算句子间的相似度,在语义变化大的地方进行切割。虽然计算成本高,但对复杂文档效果显著。
  • 元数据关联:为每个文本块附加元数据,如来源文件名、页码、章节标题等。在检索时,这些元数据可以作为过滤或加权条件。

2. 查询理解与改写

  • 查询扩展:用户的提问可能很短,缺乏细节。可以使用大模型对原始查询进行扩展或改写,生成多个相关的查询变体,然后对这些变体的检索结果进行融合(如取并集或重排序),这能有效提升召回率。
    # 一个简单的查询扩展示例(使用大模型) def expand_query(original_query): prompt = f"""请将以下用户问题,从不同角度或补充同义词,改写成3个不同的搜索查询,用于文档检索。直接输出改写后的查询,每行一个。

原始问题:{original_query}

改写后的查询:""" # 调用LLM生成改写后的查询列表 # ... 调用代码 return expanded_queries_list ```

  • Hybrid Search(混合搜索):结合关键词搜索(如BM25)和向量搜索。关键词搜索擅长精确匹配术语,向量搜索擅长语义匹配。将两者的结果进行加权融合,可以兼顾精确度和语义相关性。你可以使用rank_bm25库实现简单的BM25,然后与向量搜索的分数进行线性加权。

4.2 优化生成答案的可靠性

即使检索到了正确内容,大模型也可能“视而不见”或“胡编乱造”。

1. 提示词工程进阶

  • 明确指令:在提示词中反复强调“严格依据上下文”、“不要使用外部知识”、“无法回答时明确告知”。
  • Few-Shot示例:在提示词中提供一两个正确回答的示例,引导模型遵循你期望的格式和风格。
  • 分步思考(Chain-of-Thought):要求模型先引用相关上下文中的句子,再进行推理和总结。这不仅能提高答案准确性,还能让回答过程更可解释。
    请按照以下步骤回答问题: 1. 从上下文中找出与问题直接相关的句子。 2. 基于这些句子,推理出问题的答案。 3. 用简洁的语言总结答案。 上下文:[...] 问题:[...]

2. 后处理与验证

  • 答案一致性验证:对于事实性问题,可以让模型从上下文中提取出支持答案的直接证据(引用),如果提取不到或证据矛盾,则判定为“无法回答”。
  • 多路径检索与生成:对于同一个问题,检索top-k个片段(k可以大一些,比如10),然后分别用不同的片段组合或提示词策略生成多个候选答案,最后通过投票或一致性检查选出最佳答案。

4.3 系统层面的考量

1. 索引更新:文档库不是一成不变的。当新增文档时,全量重建索引成本很高。FAISS支持向现有索引add新向量。你需要设计一个流程,将新文档分块、向量化后,增量添加到索引中,并同步更新存储的文本块列表。

2. 多模态支持:如果你的文档包含大量图片、表格,纯文本RAG会丢失信息。进阶方向是使用多模态模型(如CLIP)为图片生成向量,或使用专门的模型解析表格结构,将非文本内容也纳入检索范围。

3. 评估体系:如何知道你的RAG系统变好了还是变差了?需要建立评估指标。常见的包括:

  • 检索相关度:人工或使用模型判断检索到的片段与问题的相关程度。
  • 答案忠实度:生成的答案是否严格源自提供的上下文,有没有幻觉。
  • 答案有用性:答案是否准确、完整地解决了问题。

可以构建一个包含(问题, 相关文档片段, 理想答案)的小测试集,在每次优化后跑一遍,进行量化评估。

5. 常见问题排查与实战避坑指南

在实际搭建和运行rag-from-scratch这类项目时,你一定会遇到各种各样的问题。下面我整理了一些典型问题及其解决方案,这些都是我趟过的坑。

5.1 检索结果完全不相关

这是最常见也最令人沮丧的问题。

  • 检查嵌入模型:你用的嵌入模型是否适合你的文本领域?例如,all-MiniLM-L6-v2是一个通用模型,对于特别专业的领域(如生物医学、法律),使用在该领域微调过的嵌入模型(如bge系列、text2vec系列)效果会好得多。可以尝试在 Hugging Face 上搜索更合适的模型。
  • 检查文本预处理:你的文本在向量化前是否清洗干净?过多的换行符、乱码、URL、特殊字符可能会干扰模型。尝试进行简单的清洗:去除多余空白、统一编码等。
  • 调整分块大小:分块太大,包含过多无关信息;分块太小,语义不完整。尝试调整chunk_sizeoverlap参数。一个实用的方法是,用几个典型问题做测试,观察检索到的块是否包含了答案。
  • 确认相似度计算:如果你使用的是余弦相似度,确保向量是经过归一化的。FAISS的IndexFlatIP(内积)在向量归一化后等价于余弦相似度。如果你用了其他索引或自己计算,要确保算法一致。

5.2 模型生成答案时忽略上下文(幻觉)

模型总是自说自话,不理会你提供的参考信息。

  • 降低温度:这是最直接有效的方法。将temperature参数设为0.1甚至0,让模型的输出更确定、更可预测,从而更倾向于遵循上下文。
  • 强化提示词:在提示词的开头就用非常强硬、明确的指令,例如“你必须且只能使用以下上下文信息来回答问题,上下文之外的信息一概不知,也绝对不允许编造。”可以尝试不同的措辞,找到最有效的指令。
  • 提供更少的上下文:有时候,给模型过多的上下文(比如前5个相关片段),它反而会迷失。尝试只提供最相关的1-2个片段(top_k=1或2),看看答案的忠实度是否提高。
  • 使用更强的模型:如果条件允许,尝试从gpt-3.5-turbo升级到gpt-4claude-3。更强大的模型在遵循指令和减少幻觉方面通常表现更好。

5.3 处理速度慢

当文档数量上千时,构建索引和检索可能会变慢。

  • 使用GPUsentence-transformersfaiss-gpu都支持GPU加速。如果你的环境有CUDA,安装对应的GPU版本可以带来数十倍的编码和检索速度提升。
  • 选择合适的FAISS索引:对于海量数据(百万级以上),IndexFlatIP的线性扫描会成为瓶颈。研究并使用IndexIVFFlatIndexHNSWFlat这类近似搜索索引。它们通过聚类或图算法,用少量精度损失换取巨大的速度提升。FAISS官方文档有详细的索引选择指南。
  • 异步处理:在构建索引时,对于大批量文本的编码,可以尝试使用批处理并利用多线程/多进程。sentence-transformersencode方法本身支持show_progress_bar和批量处理,已经比较优化。

5.4 如何处理超长文档或复杂问题

用户的问题可能涉及多个文档的不同部分。

  • 迭代检索(Retrieval-Augmented Generation, RAG):这不是一次性检索。可以先检索到一些相关文档,然后让模型根据初步信息,提出需要进一步澄清的子问题,再进行第二轮检索。这需要更复杂的流程控制。
  • Map-Reduce:对于需要总结整篇长文档的问题,可以将文档分成多个块,让模型分别总结每个块(Map),然后再让另一个模型或同一模型对所有的块总结进行汇总(Reduce)。LangChain等框架内置了这种模式,自己在“从零开始”项目中实现也不复杂,核心是设计好两个阶段的提示词。

通过这个从零构建RAG的过程,你获得的最宝贵的东西不是代码本身,而是对RAG每一个组件、每一个数据流向的深刻理解。下次当你使用一个成熟的RAG框架时,你会清楚地知道,在VectorStoreRetriever背后,是FAISS在计算相似度;在TextSplitter背后,是分块策略在影响召回率。这种理解能让你从框架的使用者,变成问题的解决者和系统的调优者。

http://www.jsqmd.com/news/753777/

相关文章:

  • 蓝河工具箱下载6.6最新版
  • D2DX:暗黑破坏神2现代PC重生的终极解决方案
  • slot
  • 从Windows桌面到Raspberry Pi Zero W2:.NET 9跨架构边缘调试7大约束条件对照表,第4项已被微软标记为P0阻塞问题
  • 【新手必看】C语言二维数组实战:从栈损坏报错到彻底掌握(附VS2022排坑指南)
  • 全链路压测的环境复杂性:网络架构、应用架构与性能影响因素全解析
  • 【ISO/IEC 14882:2027草案第12.8节权威解读】:为什么你的noexcept函数仍在抛异常?3类隐式异常路径正在绕过你的防护
  • 5分钟快速上手d2s-editor:暗黑破坏神2存档修改完全指南
  • 告别模糊!用STM32F103C8T6驱动OV7670摄像头,实现稳定图像采集的完整流程
  • JTAG技术解析:从原理到嵌入式调试实践
  • 基于OpenClaw Starter快速构建Python多智能体系统:从原理到实践
  • 利用SAR图像相位信息的YOLOv10遥感舰船检测:从原理到实战完全指南
  • 【医疗数据安全红线】:PHP脱敏算法性能提升300%的5个核心优化技巧
  • 2026 活性炭箱厂家技术测评与行业优选解析 - 小艾信息发布
  • 爬虫进阶必学:彻底吃透 element.contents,手写动态内容解析与子节点精控
  • CVE-2026-3854深度剖析:GitHub Enterprise Server X-Stat注入漏洞,88%私有化实例面临全面接管风险
  • Windows HEIC缩略图插件:让你的电脑也能预览iPhone照片
  • 暗黑破坏神2存档编辑器:可视化编辑神器,轻松打造完美角色存档
  • OpenClaw中文教程:从零搭建开源机械爪的硬件组装与Arduino控制
  • 3步解锁Unity游戏无限可能:MelonLoader模组加载器完全指南
  • .NET 9 AOT编译终极调优:6个MSBuild参数+3个RuntimeConfig.json隐藏开关,让边缘设备CPU占用直降67%
  • 快马平台快速生成魔鬼面具主题网页原型,三分钟验证创意设计
  • PyTorch模型加载进阶:用load_state_dict实现预训练权重迁移和部分参数加载
  • 在Mac上解密QQ音乐加密音频:QMCDecode完全指南
  • 3.3V版LCD12864便宜10块,但真的香吗?实测对比5V版在Arduino+U8G2下的供电、背光与性能差异
  • 百度网盘Mac版SVIP功能解锁:终极免费提速方案
  • 告别复杂抠图!ComfyUI-BiRefNet-ZHO:5分钟实现专业级图像视频背景去除
  • 为什么你的Span<T>仍触发堆分配?C# 13内联数组编译器新规(/unsafe+ /optimize+)强制生效指南
  • Warcraft Helper终极指南:让魔兽争霸3在Win10/Win11上完美运行的3个关键步骤
  • 从Applied Intelligence高被引论文看2024年AI研究热点:CV、优化、异常检测