RAG实战:从Ollama本地部署到FAISS向量检索与Rerank重排的完整技术栈解析
1. 环境准备与模型部署打造你的本地AI工作站想自己动手搭建一个能“读懂”你公司文档、回答专业问题的AI助手吗听起来很高大上但其实用开源工具链一步步来完全可以在你自己的电脑上搞定。今天我就带你走一遍完整的流程从安装基础软件到调通整个系统我会把每一步的坑和技巧都讲清楚。我自己在搭建企业知识库时这套组合拳用下来既灵活又可控成本还低特别适合对数据隐私有要求或者想深度定制的团队。整个系统的基石是Ollama你可以把它理解成一个本地的“模型应用商店”兼“运行引擎”。它最大的好处是简化了大型语言模型的下载、管理和运行。你不用再去头疼地配置复杂的Python环境或CUDA驱动一条命令就能把模型跑起来。对于新手来说这是最快能体验到AI能力的方式。1.1 部署Ollama并优化存储路径Ollama的安装包默认会装在你的系统盘比如C盘。模型动辄几个GB很快就能把你的系统盘塞满。所以第一步我们就得给它“搬个家”。这里分享一个Windows和Linux/macOS都通用的思路改变Ollama的数据存储目录。在Windows上除了原始文章提到的mklink创建目录链接软连接的方法我更推荐直接修改Ollama的环境变量。首先正常安装Ollama。然后在它运行之前右键点击“此电脑”-“属性”-“高级系统设置”-“环境变量”在“系统变量”或“用户变量”中新建一个变量名称为OLLAMA_MODELS值设置为你想存放模型的新路径例如D:\AI_Models\Ollama。这样之后所有拉取的模型都会自动存到这个目录一劳永逸。对于Linux或macOS用户方法更简单。打开你的终端在启动Ollama服务之前先执行一行命令export OLLAMA_MODELS/path/to/your/custom/models如果想永久生效就把这行命令加到你的shell配置文件里比如~/.bashrc或~/.zshrc的末尾。然后运行source ~/.bashrc让它生效。之后再用ollama serve启动服务所有模型就都会乖乖存到你指定的地方了。这个技巧能帮你省去后续很多磁盘空间告急的麻烦。1.2 核心模型的选择与拉取模型选得好项目就成功了一半。在RAG系统里我们至少需要三类模型嵌入模型、重排序模型和大语言模型。原始文章提到了BGE-M3和DeepSeek-R1这都是非常优秀的选择。我根据自己的实战经验再给你补充一些选型思路和细节。嵌入模型负责把文本转换成数学向量Embedding它的质量直接决定了检索的准确性。BGE-M3是北京智源研究院开源的佼佼者支持多语言在中文任务上表现尤其出色而且它集成了密集检索、多向量检索和稀疏检索三种方式适应性很强。用Ollama拉取它非常简单ollama pull bge-m3:latest大语言模型是最终生成答案的“大脑”。DeepSeek-R1最近很火它的推理能力和工具调用功能确实强大特别适合处理复杂的、需要多步推理的问答。拉取命令类似ollama pull deepseek-r1:8b-qwen3-q4_K_M这里我用了带量化精度q4_K_M的版本它在保持不错效果的同时对显存和内存的要求大大降低更适合本地部署。如果你电脑配置一般可以从7B或更小的模型开始尝试。重排序模型是个“精益求精”的角色。当向量数据库初步检索出10个可能相关的文档片段后Rerank模型会基于问题和每个片段之间的精细语义关系重新打分和排序把最相关的那一两个挑出来。原始文章用的BGE-Reranker-v2-M3和BGE-M3是同一系列兼容性好。这个模型通常需要从Hugging Face或ModelScope魔塔社区下载。我以魔塔社区为例除了按照网页说明用git clone或下载工具更推荐在Python代码里用snapshot_download来下载这样更容易集成到你的项目里from modelscope import snapshot_download model_dir snapshot_download(Xorbits/bge-reranker-v2-m3, cache_dir./your_local_models)下载好后记住这个本地路径后面加载模型时会用到。1.3 安装Python依赖库配好你的工具箱模型准备好了我们还需要Python库这把“手术刀”来组装它们。创建一个独立的虚拟环境是专业开发的好习惯能避免库版本冲突。这里我列出一个更详细的requirements.txt文件并解释每个库的作用langchain0.1.0 langchain-community0.0.10 langchain-ollama0.1.0 faiss-cpu1.7.4 transformers4.37.0 torch2.1.0 sentence-transformers2.2.2 pypdf3.17.0 python-docx1.1.0 openpyxl3.1.2 Pillow10.1.0 pytesseract0.3.10 modelscope1.11.0langchain系列这是我们的核心框架。langchain是主库提供了构建AI应用链路的抽象langchain-community包含了许多社区贡献的集成工具比如我们后面要用到的FAISS向量库封装langchain-ollama则专门用于对接Ollama模型。faiss-cpuFacebook开源的向量相似性搜索库效率极高。我们安装CPU版本大部分本地场景够用了。如果你有高性能NVIDIA显卡且想加速可以后续研究faiss-gpu版本。transformers torchHugging Face的Transformer库和PyTorch是加载和使用Rerank等PyTorch模型的标配。sentence-transformers一个专门用于生成句子向量的库虽然我们主要用Ollama的嵌入模型但这个库也很有用可以作为备选或用于评估。pypdf, python-docx, openpyxl分别用于处理PDF、Word和Excel文档是文档读取的必备工具。Pillow pytesseract这对组合是用来处理扫描版PDF或图片中的文字的。Pillow处理图像pytesseract调用OCR引擎识别文字。安装时只需在终端进入项目目录执行pip install -r requirements.txt。如果遇到网络问题可以考虑使用国内的镜像源例如加上-i https://pypi.tuna.tsinghua.edu.cn/simple。2. 文档处理的艺术从原始文件到智能分块RAG系统就像一个记忆力超群且善于引经据典的学者。它的“记忆力”来自于我们喂给它的文档。但直接把整本书扔给它它也会“消化不良”。文档处理阶段就是要把原始的、杂乱的非结构化数据加工成易于AI理解和检索的“知识片段”。这一步做得好不好直接决定了后续问答的精准度。2.1 多格式文档读取一个都不能少企业里的知识文档格式五花八门PDF报告、Word方案、Excel表格、甚至扫描的图片。我们的系统需要有能力处理所有这些格式。对于可编辑的文档如txt, docx, pdf文本处理起来相对直接。以PDF为例使用pypdf库可以很方便地提取文字from pypdf import PdfReader def extract_text_from_pdf(pdf_path): reader PdfReader(pdf_path) text for page in reader.pages: text page.extract_text() \n return text但这里有个大坑很多PDF是扫描件本质上是图片。pypdf对这种文件无能为力提取出来的文字是空的。这时就需要我们的OCR组合拳上场了。你需要先用pdf2image之类的库将PDF每一页转换成图片然后用Pillow进行简单的图像预处理如灰度化、二值化去噪最后用pytesseract识别文字。这个过程参数调优很重要比如识别语言要设置为chi_simeng中英文混合否则中文会识别成乱码。我当初在这里踩过坑识别出来的内容牛头不对马嘴后来发现是没配置好Tesseract的安装路径和语言包。2.2 分块策略深度解析为何递归分块是折中优选文本分块是RAG的“灵魂手术”。原始文章对比了几种策略我的体会是没有绝对最好的分块方法只有最适合你数据特点的方法。固定大小分块会粗暴地切断句子可能把“因为……所以……”拆到两个块里语义完全丢失。基于句子的分块对长文档不友好一个复杂的法律条款可能就是一个超长句。语义分块听起来很美好它利用嵌入模型计算句子间的相似性把语义相近的句子聚在一起形成一个块。但这需要额外的计算并且“相似性阈值”这个参数很难调。设高了可能每个块都很大设低了可能把本该连贯的内容拆散。LLM分块更智能但成本高、速度慢不适合作为生产流水线的常规操作。因此对于大多数通用场景尤其是格式比较规范的文档如技术手册、产品说明书、公司制度递归分块是一个在简单性、效率和语义保持度之间取得很好平衡的选择。它的思想很像我们读文章先尝试按最大的逻辑单元分如两个空行分出来的块如果还太大就换更小的分隔符如句号层层递进直到每个块的大小落在我们设定的区间内。2.3 实现自适应中英文递归分块器原始文章给出了一个根据文本长度动态调整分块大小的代码非常实用。我在此基础上强化了它的语言自适应能力。因为中英文的标点符号和语言结构不同用同一套分隔符效果会打折扣。import re from langchain.text_splitter import RecursiveCharacterTextSplitter class SmartTextSplitter: def __init__(self): pass def _is_mostly_chinese(self, text: str, threshold: float 0.3) - bool: 判断文本是否主要为中文。对于混合文本中文占比超过阈值即视为中文文本。 # 匹配中文字符 chinese_chars re.findall(r[\u4e00-\u9fff], text) total_chars len(text) if total_chars 0: return False # 空文本按英文处理或自定义 return len(chinese_chars) / total_chars threshold def create_splitter(self, text: str) - RecursiveCharacterTextSplitter: 根据文本特征创建智能分块器 text_length len(text) # 1. 确定分隔符中文和英文使用不同的标点优先级 if self._is_mostly_chinese(text): # 中文分隔符优先级段落 句子结束符 换行 逗号/分号 空格 separators [\n\n, \n, 。, , , , , , ] else: # 英文分隔符优先级段落 句子结束符 换行 分号/逗号 空格 separators [\n\n, \n, ., ?, !, ;, ,, , ] # 2. 动态调整块大小和重叠区 # 重叠区chunk_overlap是为了防止上下文在边界处被硬生生切断保留一些连续性。 if text_length 1500: # 短文本如邮件、摘要 chunk_size 256 chunk_overlap 64 elif text_length 10000: # 中等文本如文章、报告章节 chunk_size 512 chunk_overlap 128 elif text_length 50000: # 长文本如白皮书、长报告 chunk_size 768 chunk_overlap 192 else: # 超长文本如书籍、手册 chunk_size 1024 chunk_overlap 256 # 3. 创建分块器 splitter RecursiveCharacterTextSplitter( separatorsseparators, chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionlen, is_separator_regexFalse ) return splitter # 使用示例 splitter_util SmartTextSplitter() my_text 这是一段中文技术文档。它包含多个句子。\n\n下一段从这里开始。 text_splitter splitter_util.create_splitter(my_text) chunks text_splitter.split_text(my_text) print(f分成了 {len(chunks)} 个块。) for i, chunk in enumerate(chunks): print(f块 {i1} (长度:{len(chunk)}): {chunk[:50]}...)这个改进版分块器不仅根据长度调整块大小还先判断文本语言选用最合适的分隔符序列。对于中英文混合的文档虽然判断逻辑以中文为主但分隔符列表中包含了中英文标点也能在一定程度上兼容。在实际使用中你可以根据自己文档的平均长度和语言特点微调chunk_size和chunk_overlap。我的经验是重叠部分通常设为块大小的10%-25%效果比较好。3. 向量化与检索构建系统的记忆核心文档被切成一个个规整的“知识块”后接下来就要让计算机能理解并快速找到它们。这就是嵌入和向量检索干的事。简单来说嵌入模型像是一个“翻译官”把人类语言文本翻译成计算机擅长处理的数学语言高维向量。向量数据库则像是一个拥有超强记忆力的“图书管理员”它把所有向量化后的文档块存储起来并能根据问题向量快速找到最相似的几个文档块。3.1 使用Ollama Embeddings生成向量我们选择Ollama来运行嵌入模型主要是图个方便和后续的大语言模型调用保持统一。LangChain已经提供了很好的封装。这里需要注意连接配置和错误处理。from langchain_ollama import OllamaEmbeddings from langchain_community.vectorstores import FAISS import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class VectorStoreManager: def __init__(self, model_name: str bge-m3:latest, base_url: str http://localhost:11434): 初始化向量存储管理器 Args: model_name: Ollama中嵌入模型的名称 base_url: Ollama服务的地址和端口 self.model_name model_name self.base_url base_url self.embeddings self._init_embeddings() self.vector_store None def _init_embeddings(self) - OllamaEmbeddings: 初始化嵌入模型。这里可以加入重试机制和健康检查。 try: # 尝试连接Ollama服务 embeddings OllamaEmbeddings( modelself.model_name, base_urlself.base_url, # 可选参数设置超时时间避免长时间等待 # request_timeout30.0 ) # 用一个简单的测试文本检查服务是否正常 test_vector embeddings.embed_query(测试连接) if test_vector and len(test_vector) 0: logger.info(f嵌入模型 {self.model_name} 初始化成功。) return embeddings else: raise ConnectionError(嵌入模型返回了空向量。) except Exception as e: logger.error(f初始化嵌入模型失败: {e}) logger.info(请确保Ollama服务已启动且模型 {self.model_name} 已通过 ollama pull 下载。) # 在实际项目中这里可以加入失败重试或回退到备用模型的逻辑 raise # ... 后续的FAISS操作函数这段代码比简单的调用更健壮。它尝试在初始化时进行一次嵌入查询确保Ollama服务以及指定的模型是可用的。如果失败会给出明确的错误提示这对于调试部署问题非常有帮助。3.2 FAISS向量数据库的构建与持久化FAISS的优势在于其惊人的检索速度即使在百万级别的向量中查找最近邻也能做到毫秒级响应。我们用LangChain封装的FAISS接口可以非常方便地进行创建、添加和保存。创建并保存向量库def create_and_save_faiss_index(self, documents, save_path./faiss_index): 从文档列表创建FAISS索引并保存到本地 Args: documents: 列表每个元素是一个Document对象包含page_content和metadata save_path: 索引保存的目录路径 if not documents: logger.warning(文档列表为空无法创建索引。) return logger.info(f开始为 {len(documents)} 个文档块创建向量索引...) # 关键步骤将文档和嵌入模型结合生成向量存储 self.vector_store FAISS.from_documents( documentsdocuments, embeddingself.embeddings ) logger.info(向量索引创建完成。) # 保存到本地磁盘 self.vector_store.save_local(save_path) logger.info(f向量索引已保存至: {save_path}) return self.vector_storeFAISS.from_documents这个方法内部做了很多事情它遍历所有文档调用嵌入模型为每个文档块生成向量然后用这些向量构建FAISS索引。保存后你会得到两个文件index.faiss索引数据和index.pkl文档的元数据映射。加载已有向量库并进行检索def load_and_search(self, load_path, query, k5): 加载本地向量库并进行相似性搜索 Args: load_path: 索引目录路径 query: 用户查询问题 k: 返回最相似结果的数量 # 加载时务必传入相同的嵌入模型否则向量空间不一致检索会出错 self.vector_store FAISS.load_local( folder_pathload_path, embeddingsself.embeddings, allow_dangerous_deserializationTrue # 加载本地文件需要此标志 ) logger.info(f已从 {load_path} 加载向量库。) # 执行相似性搜索 docs_and_scores self.vector_store.similarity_search_with_score(query, kk) logger.info(f针对查询 {query} 的检索结果:) results [] for i, (doc, score) in enumerate(docs_and_scores): # score是距离分数越小表示越相似。有时会转化为相似度分数 (1/(1score)) logger.info(f[结果 {i1}] 相似度分数: {score:.4f}) logger.info(f内容预览: {doc.page_content[:200]}...) logger.info(f元数据: {doc.metadata}\n) results.append({ content: doc.page_content, metadata: doc.metadata, score: score }) return results这里有几个关键点1.allow_dangerous_deserializationTrue参数是必须的因为加载本地文件可能存在安全风险但对我们可控的环境是安全的。2.similarity_search_with_score会返回文档和对应的距离分数。3. 检索数量k可以设置得比最终需要的结果多一些比如我们最终只要1个最佳答案但这里可以检索5-10个留给后面的重排序模型去精选。4. 重排序与答案生成从粗糙到精准的临门一脚经过向量检索我们拿到了几个“可能相关”的文档片段。但“可能相关”还不够我们需要“最相关”。这就是重排序模型的任务。它像一个严格的阅卷老师对初步筛选出来的答案进行二次精批找出得分最高的那一个。最后把这份精选的“参考资料”连同用户的问题一起交给大语言模型让它生成最终答案。4.1 集成Rerank模型进行结果精排原始文章使用了Transformers库直接加载模型。这里我给出一个更完整、封装更好的类包含模型加载、批处理和结果整合。import torch from transformers import AutoModelForSequenceClassification, AutoTokenizer from typing import List, Dict, Any import numpy as np class Reranker: def __init__(self, model_path: str): 初始化重排序模型 Args: model_path: 本地模型目录的路径从魔塔社区或Hugging Face下载后存放的路径 self.model_path model_path self.tokenizer None self.model None self._load_model() def _load_model(self): 加载模型和分词器 logger.info(f正在加载重排序模型从: {self.model_path}) # 使用本地路径加载 self.tokenizer AutoTokenizer.from_pretrained(self.model_path) self.model AutoModelForSequenceClassification.from_pretrained(self.model_path) # 将模型设置为评估模式关闭dropout等训练层 self.model.eval() logger.info(重排序模型加载完毕。) def rerank(self, query: str, candidates: List[Dict[str, Any]], top_k: int 1) - List[Dict[str, Any]]: 对候选文档进行重排序 Args: query: 用户查询 candidates: 候选文档列表每个元素是包含content和metadata的字典 top_k: 返回前K个最优结果 Returns: 重排序后的候选文档列表按相关性从高到低排序 if not candidates: return [] # 1. 准备查询文档对 pairs [[query, cand[content]] for cand in candidates] # 2. 分词和编码 with torch.no_grad(): # 禁用梯度计算加快推理速度 inputs self.tokenizer( pairs, paddingTrue, truncationTrue, return_tensorspt, max_length512, # 根据模型最大长度调整 ) # 3. 模型推理获取分数 outputs self.model(**inputs) # 对于分类模型我们取最后一个线性层的输出作为相关性分数 # BGE-Reranker的输出logits数值越大表示越相关 scores outputs.logits.view(-1,).float().numpy() # 4. 将分数与候选文档绑定并排序 for i, cand in enumerate(candidates): cand[rerank_score] scores[i] # 按分数降序排序分数越高越相关 sorted_candidates sorted(candidates, keylambda x: x[rerank_score], reverseTrue) # 5. 返回Top-K个结果 return sorted_candidates[:top_k] # 使用示例 # 假设你已经从FAISS检索到了初始结果 initial_results reranker Reranker(model_path./your_local_models/bge-reranker-v2-m3) top_doc reranker.rerank(query公司今年的年假政策是什么, candidatesinitial_results, top_k1)[0] print(f经过重排序最相关的文档是\n{top_doc[content][:300]}...)这个Reranker类做了几件重要的事一是将模型推理放在with torch.no_grad()上下文中节省内存和计算资源二是将重排序分数直接添加到候选文档字典中方便追踪三是返回排序后的完整列表而不仅仅是索引信息更完整。在实际应用中你可以观察rerank_score的分布如果最高分和次高分差距很大说明检索结果很明确如果分数很接近可能意味着问题比较模糊或者知识库中没有明确答案。4.2 调用Ollama LLM生成最终答案最后一步我们把经过重重筛选得到的最相关文档上下文和用户的问题一起构造成一个清晰的提示词发送给大语言模型让它来组织语言生成最终答案。from langchain_ollama import OllamaLLM from langchain.prompts import PromptTemplate from langchain.schema import StrOutputParser from langchain.schema.runnable import RunnablePassthrough class QAChain: def __init__(self, llm_model_name: str deepseek-r1:8b-qwen3-q4_K_M, base_url: str http://localhost:11434): self.llm OllamaLLM( modelllm_model_name, base_urlbase_url, temperature0.2, # 温度调低让答案更确定、更基于上下文 # top_p0.95, # 如果设置了top_p通常就不需要top_k了 # num_predict512 # 控制生成答案的最大长度 ) # 定义一个提示词模板 self.prompt_template PromptTemplate.from_template( 请严格根据以下提供的上下文信息来回答问题。如果上下文信息中没有明确答案请直接说“根据提供的信息我无法回答这个问题”不要编造信息。 上下文信息 {context} 问题{question} 请基于上下文给出准确、简洁的答案 ) def answer(self, question: str, context: str) - str: 基于给定的上下文回答问题 # 构建提示词 prompt self.prompt_template.format(contextcontext, questionquestion) # 调用模型 response self.llm.invoke(prompt) return response def answer_with_chain(self, question: str, context: str) - str: 使用LangChain Expression Language (LCEL) 构建一个简单的链更模块化 chain ( {context: RunnablePassthrough(), question: RunnablePassthrough()} | self.prompt_template | self.llm | StrOutputParser() ) result chain.invoke({context: context, question: question}) return result # 整合使用 # 1. 检索 vector_mgr VectorStoreManager() initial_docs vector_mgr.load_and_search(./faiss_index, 公司年假怎么计算, k5) # 2. 重排序 reranker Reranker(./models/bge-reranker-v2-m3) final_doc reranker.rerank(公司年假怎么计算, initial_docs, top_k1)[0] # 3. 生成答案 qa_bot QAChain() answer qa_bot.answer(question公司年假怎么计算, contextfinal_doc[content]) print(f最终答案\n{answer})这个流程就是RAG的核心检索-增强-生成。提示词模板的设计至关重要我强烈建议在模板中明确要求模型“严格根据上下文”并指示它在上下文不明确时承认无法回答。这能有效减少模型“幻觉”即编造信息。temperature参数设置为较低的值如0.2可以使生成的内容更加稳定和可预测更适合事实性问答。5. 避坑指南与性能优化实战把各个模块跑通只是第一步要让这个系统真正稳定、高效地运行起来还需要注意很多细节。下面是我在项目中实际踩过的一些坑和对应的解决方案。5.1 常见部署与运行问题排查问题一Ollama服务连接失败。这是最常见的问题。首先确保Ollama服务已经启动。在终端输入ollama serve启动服务它会默认监听11434端口。然后在另一个终端用curl http://localhost:11434/api/tags测试一下API是否可用。如果返回模型列表的JSON说明服务正常。如果是在Docker容器内或远程服务器上部署需要确保你的Python代码中base_url参数指向正确的IP和端口并且防火墙没有阻止该端口。问题二嵌入速度慢。首次为大量文档创建向量索引时可能会非常慢。除了升级硬件可以从以下方面优化1.批量处理不要一条条文本调用embed_query而是将文档列表批量传给embed_documents方法效率更高。2.调整参数有些嵌入模型支持batch_size参数可以适当调大。3.异步处理对于超大批量数据可以考虑使用异步库如asyncio,aiohttp来并发调用Ollama的嵌入接口但要注意不要压垮服务。问题三检索结果不相关。如果发现系统总是答非所问首先检查分块是否合理。过大的块会包含无关噪声过小的块会丢失关键上下文。回头调整chunk_size和chunk_overlap。其次检查嵌入模型是否适合你的领域。BGE-M3是通用模型如果你的文档是高度专业化的如法律、医学可以考虑在专业语料上微调一个嵌入模型或者尝试其他针对特定领域优化的开源模型。问题四Rerank模型加载失败或报错。最常见的原因是模型文件下载不完整或者PyTorch版本与模型不兼容。确保你从魔塔社区下载时网络稳定下载后检查文件大小是否与网页显示一致。另外使用transformers库加载本地模型时确保路径下包含config.json,pytorch_model.bin(或safetensors文件) 和tokenizer.json等必要文件。5.2 系统优化与扩展思路当基本流程跑通后你可以考虑以下优化来提升系统表现元数据过滤在将文档存入向量库时可以为每个块添加丰富的元数据如“文档标题”、“章节”、“作者”、“更新时间”等。在检索时除了语义相似度还可以先根据元数据进行过滤。例如当用户问“2024年的销售报告”你可以先过滤出metadata[year] 2024 and metadata[type] 销售报告的文档块再进行向量检索这样能极大提升准确率。FAISS支持结合元数据过滤进行检索。混合检索不要只依赖向量检索。可以结合关键词检索如BM25。例如先用关键词快速筛选出一批候选文档再在这批文档内部进行更精细的向量相似度计算和重排序。这种“粗排精排”的策略在保证相关性的同时有时能更快。缓存机制对于常见的、重复的用户问题可以将“问题-答案”对缓存起来可以用Redis或简单的内存缓存库cachetools。下次遇到相同或相似问题时直接返回缓存答案无需再走完整的检索和生成流程能显著降低响应延迟和计算开销。评估与迭代搭建一个简单的评估流程。准备一批标准问题Q和对应的标准答案A或参考文档Context。定期用你的RAG系统跑这批问题从检索相关性返回的文档是否包含答案和生成质量生成的答案是否准确、流畅两个维度打分。根据评估结果反过来调整分块策略、检索数量k值、提示词模板等参数。没有评估的优化都是盲目的。这套从Ollama部署到FAISS检索再到Rerank重排的本地RAG技术栈虽然涉及环节不少但每个环节都有成熟的开源工具支撑。最难的不是编码而是根据你自己的数据和需求耐心地调试每一个模块的参数和流程。我建议你先用一个小的、熟悉的文档集比如你团队的几篇技术博客跑通整个流程看到它从“胡言乱语”到“有模有样”的转变这个过程会给你带来巨大的成就感然后再逐步扩展到更复杂的场景。