恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

从零构建Python RAG系统:超越玩具项目的实战指南

  • 首页
  • 资讯中心
  • /
  • 从零构建Python RAG系统:超越玩具项目的实战指南

相关资讯

MaxFrame:基于Ray的分布式视频智能分析平台架构与实战 2026/8/14 10:40:16
微信聊天记录永久保存完全指南:WeChatMsg 数据留痕从入门到实战 2026/8/14 10:40:16
如何把微信聊天记录永久保存?WeChatMsg免费导出HTML、Word、CSV详细教程 2026/8/14 10:40:16

最新资讯

RPG Maker MV/MZ资源解密工具:3 种方式解锁加密游戏文件
LRC歌词批量下载实战:163MusicLyrics免费歌词工具完整上手教程
撤回的消息到底藏哪了?防撤回补丁如何在百MB的DLL里精准定位一行字节
Sileo包管理器全攻略:iOS越狱设备从第一个插件到个性化生态的完整路线
163MusicLyrics完整指南:如何免费批量获取网易云和QQ音乐歌词
C盘告急别硬扛:开源重复文件清理工具Czkawka,一次扫描轻松腾出几十GB

今日推荐

青岛煜鹏网站建设公司如何帮助传统企业实现数字化转型破局与增长路径
内蒙古生产建设兵团四师三十四团知青网站:承载岁月记忆与青春荣耀的精神家园
梅州市住房与城乡建设局官网:获取权威建筑信息、政策解读与民生服务的最佳平台入口

本周热门

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁
如何快速生成中国车牌图片:Python开源工具完整指南
当 LLM 遇见大文档:主流开源项目如何处理上下文超限

本月精选

如何用DamaiHelper实现演唱会门票的智能自动化抢购:完整技术解决方案指南
第4篇:59 倍性能差距的索引瓶颈定位——一次教科书级的全表扫描调优
终极歌词批量下载神器:5分钟解决离线音乐库歌词同步难题

从零构建Python RAG系统:超越玩具项目的实战指南

发布时间:2026/8/14 10:40:16
从零构建Python RAG系统:超越玩具项目的实战指南 1. 从“玩具”到“工具”为什么你需要一个真正的RAG项目如果你最近在关注AI应用开发尤其是大语言模型LLM的落地那么“RAG”这个词一定在你眼前晃了无数次。RAG检索增强生成听起来像是一个解决LLM“幻觉”和知识过时问题的银弹。网上充斥着各种“5分钟搭建RAG”、“10行代码实现问答”的教程它们大多基于LangChain或LlamaIndex这样的高级框架用几行封装好的代码就能把一个PDF喂给模型然后得到一个看似能回答问题的系统。我最初也是从这些“玩具”项目入门的。但当我真的想用它来解决一个实际问题——比如让模型基于我们公司内部长达500页的技术文档来回答客户咨询时问题接踵而至。那个“5分钟搭建”的系统回答要么是胡言乱语要么就是“根据提供的信息我无法回答”。它成了一个精致的摆设。这就是“玩具”和“工具”的区别。一个真正的、可用的RAG系统远不止是调用几个API。它涉及到对原始文本的深度理解、对检索精度的苛刻要求、对生成结果的可控性设计。网上很多入门内容止步于“跑通Demo”却很少告诉你当你的文档超过10个当你的问题变得复杂当你的用户要求零错误时你该怎么办。这篇手册我想和你分享的就是如何跨过“玩具”的门槛动手搭建一个扎实的、以Python为核心的RAG项目骨架。我们不追求最快但追求每一步都知其所以然我们不依赖“魔法”框架而是从底层组件开始组装让你在遇到问题时有能力拆开它、调试它、优化它。你会发现抛开那些高级抽象后RAG的核心逻辑清晰而有力。2. 拆解RAG一个朴素的三段论工作流在引入任何库之前我们必须像设计一个普通软件模块一样理解RAG到底在干什么。抛开所有华丽的术语RAG的核心是一个“检索-增强-生成”的三段论管道。我们可以用一个图书馆管理员的比喻来理解它建立索引Indexing你有一屋子杂乱无章的书你的文档库。管理员索引系统需要把这些书的内容拆解成一个个有意义的章节或知识点文本切分/分块并为每个知识点制作一张精美的卡片向量化卡片上记录了知识点的核心摘要向量嵌入。最后所有卡片被按照某种规律向量相似度整齐地排列在卡片柜里。检索Retrieval当有读者用户来问一个问题时管理员首先理解这个问题将问题向量化然后拿着这个“问题卡片”快速地在卡片柜里寻找那些摘要最相似的几张卡片计算余弦相似度Top-K召回。这几张卡片对应的书页就是最相关的参考资料。生成Generation管理员不会直接把这几页纸扔给读者。他会结合问题仔细阅读这几页纸上的内容将检索到的文本作为上下文然后组织自己的语言给读者一个准确、完整、流畅的答案LLM生成。基于这个朴素模型一个最小可用的RAG系统只需要三个核心组件文本加载与切分器Loader Splitter负责读取各种格式TXT, PDF, MD, HTML的文档并将其切割成适合处理的片段。向量模型与数据库Embedding Model Vector Store负责将文本片段转化为数学向量嵌入并存储、索引这些向量支持快速相似度检索。大语言模型LLM负责理解“问题检索到的上下文”并生成最终答案。我们的实战就从亲手组装这三个组件开始。你会发现即使只用最基础的库你也能构建出比很多“玩具”项目更可控的系统。3. 环境奠基构建一个可复现的Python工作区在开始写第一行业务代码前一个隔离、干净、依赖明确的环境是专业项目的起点。我强烈建议你放弃直接使用系统Python或全局安装包的习惯。3.1 为什么是CondaPoetry虚拟环境工具很多我选择Conda管理Python解释器本身尤其是处理一些有C扩展依赖的包时更省心用Poetry管理项目依赖。Poetry的pyproject.toml能清晰地声明依赖、分组比如区分开发依赖和线上依赖、并锁定精确版本这比手写requirements.txt要优雅和可靠得多。# 1. 创建并激活一个Conda环境假设你已安装Miniconda conda create -n rag_workshop python3.10 -y conda activate rag_workshop # 2. 安装Poetry如果你还没有 pip install poetry # 3. 在你的项目目录初始化Poetry poetry init执行poetry init时它会交互式地引导你创建pyproject.toml文件。对于依赖我们暂时可以不填后面用add命令来加。3.2 核心依赖选型与安装接下来我们为RAG的三段论挑选具体的“武器”。这里的选择基于稳定性、社区活跃度和上手难度。# 进入项目目录后使用Poetry添加依赖 # 核心数据处理与向量计算 poetry add numpy pandas # 文本切分与处理 poetry add pypdf2 markdown beautifulsoup4 # 用于PDF、Markdown、HTML poetry add langchain-text-splitters # 使用LangChain的高质量切分器但不引入其全量框架 # 向量模型本地轻量首选 poetry add sentence-transformers # 向量数据库本地轻量首选 poetry add chromadb # LLM调用以OpenAI API为例也可替换为其他 poetry add openai # 可选用于更复杂的文本清理 poetry add tiktoken # OpenAI的Tokenizer用于精确计算长度 # 添加开发依赖组如代码格式化、类型检查等 poetry add --group dev black isort mypy pylint注意这里我们刻意只引入了langchain-text-splitters而不是整个LangChain。这能让我们聚焦于核心流程避免被框架的复杂性干扰初学时的理解。当你对底层了如指掌后再使用框架来提升开发效率才是明智的。执行poetry install后所有依赖都会被安装在一个独立的虚拟环境中。你可以通过poetry shell进入该环境或使用poetry run python your_script.py来运行脚本。4. 第一步文本加载与智能切分——质量决定上限这是最容易被轻视却对最终效果影响最大的环节。糟糕的切分会直接导致检索到无关信息俗称“垃圾进垃圾出”。4.1 文档加载处理多种格式我们需要一个统一的接口来加载不同格式的文档。这里我们写一个简单的工具函数import PyPDF2 from bs4 import BeautifulSoup import markdown def load_document(file_path: str) - str: 加载文本内容支持 .txt, .pdf, .md, .html text if file_path.endswith(.txt): with open(file_path, r, encodingutf-8) as f: text f.read() elif file_path.endswith(.pdf): with open(file_path, rb) as f: reader PyPDF2.PdfReader(f) for page in reader.pages: text page.extract_text() \n elif file_path.endswith(.md): with open(file_path, r, encodingutf-8) as f: md_text f.read() # 将markdown转为html再提取纯文本 html markdown.markdown(md_text) soup BeautifulSoup(html, html.parser) text soup.get_text() elif file_path.endswith(.html) or file_path.endswith(.htm): with open(file_path, r, encodingutf-8) as f: soup BeautifulSoup(f.read(), html.parser) text soup.get_text() else: raise ValueError(fUnsupported file format: {file_path}) # 基础清理合并多余空白字符 import re text re.sub(r\s, , text).strip() return text4.2 文本切分为什么不能简单按字数切很多新手会直接用text[i:ichunk_size]来切片这是灾难性的。它会粗暴地切断句子、甚至单词破坏语义的完整性。正确的做法是在自然的语义边界处进行切分比如句子、段落或者Markdown/HTML的标题。我们之前安装的langchain-text-splitters提供了非常优秀的RecursiveCharacterTextSplitter。from langchain_text_splitters import RecursiveCharacterTextSplitter def split_text(text: str, chunk_size: int 500, chunk_overlap: int 50) - list[str]: 使用递归字符切分器进行智能切分。 :param chunk_size: 每个文本块的目标最大字符数并非严格相等。 :param chunk_overlap: 块与块之间的重叠字符数防止上下文断裂。 # 它默认按 [\n\n, \n, , ] 的顺序尝试切分优先保持段落、句子完整。 splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionlen, # 使用简单的字符长度计算也可以用tiktoken计算token数 separators[\n\n, \n, 。, , , , , , ] # 针对中文调整分隔符 ) chunks splitter.split_text(text) return chunks4.3 关键参数调优与实战心得chunk_size块大小这是最重要的参数。太小如100会丢失上下文检索到的信息碎片化太大如2000可能包含过多无关信息稀释核心内容且可能超过LLM的上下文窗口限制。我的经验是对于通用文档500-800是一个不错的起点对于代码或技术规范可以更小一些300-500。chunk_overlap重叠度重叠是为了避免一个完整的句子或概念被硬生生切成两半导致任何一块都不完整。通常设置为chunk_size的10%-20%。重叠部分在后续向量化时会有重复计算但这是保证召回率必要的代价。separators分隔符对于中文文档一定要调整默认分隔符。我上面的例子加入了中文标点。更高级的做法可以尝试用spaCy或jieba进行句子分割但RecursiveCharacterTextSplitter在大多数场景下已经足够好。一个常见的坑是直接从PDF提取的文本可能包含大量的页眉、页脚、页码和换行符。在切分前最好写一些正则表达式进行清洗。例如移除形如“第 X 页”的字符串或者将因为PDF换行而断开的单词重新连接起来。5. 第二步向量化与存储——将文本映射到数学空间文本切分好后我们需要把它们变成计算机能高效计算相似度的东西——向量。5.1 嵌入模型选择本地还是云端OpenAItext-embedding-ada-002效果稳定简单易用但需要API调用有费用和延迟且数据需出境。sentence-transformers开源库提供大量预训练模型可在本地运行数据隐私有保障是入门和生产的首选。我们选择sentence-transformers并选用一个在中文上表现良好的模型例如paraphrase-multilingual-MiniLM-L12-v2它平衡了速度和效果。from sentence_transformers import SentenceTransformer import numpy as np class LocalEmbedder: def __init__(self, model_name: str paraphrase-multilingual-MiniLM-L12-v2): # 首次运行会下载模型请确保网络通畅 self.model SentenceTransformer(model_name) print(fLoaded embedding model: {model_name}) def embed_documents(self, texts: list[str]) - np.ndarray: 将一批文本转换为向量。 # 模型返回的是numpy数组 embeddings self.model.encode(texts, convert_to_numpyTrue, show_progress_barTrue, # 处理大量文本时显示进度 normalize_embeddingsTrue) # 归一化方便余弦相似度计算 return embeddings def embed_query(self, query: str) - np.ndarray: 将单个查询转换为向量。 return self.model.encode([query], convert_to_numpyTrue, normalize_embeddingsTrue)[0]5.2 向量数据库为什么需要它当你有几千、几万个文本块时用embed_query得到问题向量后难道要遍历计算和每一个块向量的相似度吗这效率太低了。向量数据库Vector Store就是为解决这个问题而生的。它使用近似最近邻ANN算法如HNSW、IVF在精度损失很小的前提下实现海量向量的毫秒级检索。我们使用ChromaDB因为它轻量、易用且完全本地化。import chromadb from chromadb.config import Settings class VectorStoreManager: def __init__(self, persist_directory: str ./chroma_db): # 配置ChromaDB设置持久化目录 self.client chromadb.PersistentClient( pathpersist_directory, settingsSettings(anonymized_telemetryFalse) # 禁用匿名数据收集 ) # 获取或创建一个集合类似于数据库中的表 self.collection self.client.get_or_create_collection( nameknowledge_base, metadata{hnsw:space: cosine} # 使用余弦相似度作为距离度量 ) def add_documents(self, chunks: list[str], embeddings: np.ndarray, metadatas: list[dict] None): 将文本块及其向量添加到集合中。 # 生成唯一ID ids [fdoc_{i} for i in range(len(chunks))] # 将numpy数组转换为列表ChromaDB接受的格式 embeddings_list embeddings.tolist() # 如果没有提供元数据则用空字典填充 if metadatas is None: metadatas [{} for _ in chunks] self.collection.add( documentschunks, embeddingsembeddings_list, metadatasmetadatas, idsids ) print(fAdded {len(chunks)} documents to vector store.) def search(self, query_embedding: np.ndarray, top_k: int 5) - list[tuple[str, float]]: 检索最相似的top_k个文本块返回(文本, 相似度得分)。 results self.collection.query( query_embeddings[query_embedding.tolist()], n_resultstop_k, include[documents, distances] # 返回文本和距离 ) # ChromaDB返回的距离是余弦距离1 - 余弦相似度值越小越相似 # 我们将其转换为相似度分数越大越相似 retrieved_docs [] for doc, distance in zip(results[documents][0], results[distances][0]): similarity_score 1 - distance # 近似余弦相似度 retrieved_docs.append((doc, similarity_score)) return retrieved_docs5.3 构建索引的完整流程现在我们可以将前两步串联起来构建一个完整的索引管道def build_knowledge_base(doc_paths: list[str], vector_store_dir: str ./chroma_db): 从原始文档构建向量知识库。 all_chunks [] all_metadatas [] # 1. 初始化组件 embedder LocalEmbedder() vector_store VectorStoreManager(persist_directoryvector_store_dir) for doc_path in doc_paths: print(fProcessing: {doc_path}) # 2. 加载文档 raw_text load_document(doc_path) # 3. 智能切分 chunks split_text(raw_text, chunk_size600, chunk_overlap80) # 4. 为每个块创建元数据例如记录来源文件 metadatas [{source: doc_path, chunk_index: i} for i in range(len(chunks))] all_chunks.extend(chunks) all_metadatas.extend(metadatas) print(fTotal chunks: {len(all_chunks)}) # 5. 批量生成向量比逐条生成效率高很多 print(Generating embeddings...) embeddings embedder.embed_documents(all_chunks) # 6. 存入向量数据库 print(Adding to vector store...) vector_store.add_documents(all_chunks, embeddings, all_metadatas) print(Knowledge base built successfully!) return embedder, vector_store6. 第三步检索与生成——组装最终答案索引建好后就进入了实时问答环节。6.1 检索环节不仅仅是相似度基础的相似度检索我们上面实现的已经能解决大部分问题。但在复杂场景下我们需要更智能的检索策略重排序Re-ranking初步检索出Top-K比如20个相关文档后使用一个更精细但更耗时的模型如BGE-reranker对它们进行重新打分和排序只保留最相关的Top-N比如5个给LLM。这能显著提升精度。混合检索Hybrid Search结合稠密向量检索我们正在做的和稀疏向量检索如BM25基于关键词匹配。前者语义理解好后者对精确术语召回强。ChromaDB也支持集成BM25。为了保持入门手册的简洁我们先实现基础版本。但你需要知道当效果遇到瓶颈时“重排序”通常是第一个应该考虑的优化点。6.2 提示工程如何让LLM更好地利用上下文检索到的上下文不会自动变成答案。我们需要精心设计一个提示Prompt来引导LLM。一个健壮的提示模板通常包含以下几个部分def build_prompt(query: str, contexts: list[str]) - str: 构建给LLM的提示。 context_str \n\n---\n\n.join([f[Context {i1}]: {ctx} for i, ctx in enumerate(contexts)]) prompt f你是一个专业的助手请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据已知信息无法回答该问题”不要编造信息。 相关上下文信息 {context_str} 问题{query} 请根据上述上下文回答。答案 return prompt这个模板明确了角色、指令、上下文和问题。清晰的指令能极大减少LLM的“幻觉”。6.3 调用LLM完成生成我们以OpenAI API为例你需要设置环境变量OPENAI_API_KEYimport os from openai import OpenAI class OpenAIGenerator: def __init__(self, model: str gpt-3.5-turbo): self.client OpenAI(api_keyos.getenv(OPENAI_API_KEY)) self.model model def generate(self, prompt: str, temperature: float 0.1) - str: 调用LLM生成答案。 try: response self.client.chat.completions.create( modelself.model, messages[ {role: user, content: prompt} ], temperaturetemperature, # 低温度使输出更确定、更忠于上下文 max_tokens1000 ) return response.choices[0].message.content.strip() except Exception as e: return fError generating answer: {e}6.4 串联成完整的RAG问答链最后我们把所有组件像流水线一样组装起来class SimpleRAGSystem: def __init__(self, embedder, vector_store, llm_generator): self.embedder embedder self.vector_store vector_store self.llm_generator llm_generator def ask(self, question: str, top_k: int 5) - dict: 核心问答流程。 # 1. 将问题向量化 print(fEmbedding question: {question[:50]}...) query_embedding self.embedder.embed_query(question) # 2. 检索相关文档 print(Searching vector store...) retrieved_items self.vector_store.search(query_embedding, top_ktop_k) contexts [item[0] for item in retrieved_items] print(fRetrieved {len(contexts)} contexts.) # 3. 构建提示 prompt build_prompt(question, contexts) # 调试时可打印prompt # print(--- Prompt ---\n, prompt[:500], \n---) # 4. 生成答案 print(Generating answer with LLM...) answer self.llm_generator.generate(prompt) # 5. 返回结果包含答案和用于解释的引用来源 return { question: question, answer: answer, source_documents: contexts, similarity_scores: [item[1] for item in retrieved_items] } # 使用示例 if __name__ __main__: # 假设你已经运行过 build_knowledge_base向量库已存在 embedder LocalEmbedder() vector_store VectorStoreManager() llm OpenAIGenerator(modelgpt-3.5-turbo) # 或使用其他本地模型接口 rag SimpleRAGSystem(embedder, vector_store, llm) while True: user_q input(\n请输入你的问题 (输入 quit 退出): ) if user_q.lower() quit: break result rag.ask(user_q) print(f\nAnswer: {result[answer]}) # 可选显示来源 # for i, ctx in enumerate(result[source_documents]): # print(f\n[Source {i1}, Score: {result[similarity_scores][i]:.3f}]: {ctx[:200]}...)7. 从“能用”到“好用”你必须面对的优化挑战一个能返回答案的系统只是起点。要让它在实际中可靠你必须关注以下问题7.1 效果评估你的RAG系统真的准吗没有评估优化就是盲人摸象。你需要一个评估集一组问题标准答案/相关文档。可以从以下几个维度评估检索相关性检索到的文档是否真的与问题相关可以人工打分或使用LLM-as-a-judge自动评分答案忠实度生成的答案是否严格基于检索到的上下文没有胡编乱造答案准确性基于上下文答案本身是否正确答案流畅性答案是否通顺、完整建立一个简单的评估脚本定期跑分是迭代优化的基础。7.2 常见问题与调优方向问题1答案不相关或胡编乱造。检查检索首先看检索到的source_documents是否相关。如果不相关问题出在前端调整chunk_size/overlap尝试不同的embedding模型或者引入重排序。检查提示如果检索结果相关但答案胡扯强化你的prompt指令比如加上“如果信息不足请明确说明”。降低LLM的temperature将其设为0.1或0减少随机性。问题2答案遗漏了关键信息。增加top_k让LLM看到更多的上下文。优化分块策略可能关键信息正好被切分在了两个块的边缘尝试增加chunk_overlap。尝试不同的分块方法对于结构化文档如Markdown可以尝试按标题MarkdownHeaderTextSplitter分块能更好地保持语义单元完整。问题3处理长文档或大量文档时速度慢。批量嵌入确保使用embed_documents进行批量处理而非循环调用单条嵌入。向量数据库索引ChromaDB默认使用HNSW对于千万级以下的数据量性能很好。如果数据量极大可以研究其持久化索引的配置参数。异步处理对于构建索引的过程可以考虑使用异步IO来并行处理多个文档。7.3 引入路由与元数据过滤更高级的RAG系统会引入“路由”概念。例如你的向量库存储了公司“产品手册”、“技术博客”、“客服QA”等多种文档。当用户问“如何退款”时系统应该优先在“客服QA”中搜索。这可以通过在存储时为每个文本块添加metadata如{doc_type: faq}并在检索时指定过滤条件来实现。# 在检索时增加元数据过滤 results self.collection.query( query_embeddings[query_embedding.tolist()], n_resultstop_k, where{doc_type: {$eq: faq}}, # 过滤条件 include[documents, distances] )8. 项目脚手架与后续演进至此你已经拥有了一个完全受控、可深度定制的RAG系统核心。我建议你将上述代码模块化组织成一个真正的项目my_rag_project/ ├── pyproject.toml # Poetry依赖管理 ├── README.md ├── src/ │ ├── __init__.py │ ├── document_processor.py # 加载、切分模块 │ ├── embedding.py # 嵌入模型封装 │ ├── vector_store.py # 向量数据库操作 │ ├── llm_client.py # LLM调用封装 │ ├── prompt_templates.py # 提示词模板 │ └── rag_pipeline.py # 核心流水线组装 ├── scripts/ │ ├── build_kb.py # 构建知识库脚本 │ └── query_cli.py # 命令行问答脚本 ├── data/ # 存放原始文档 └── chroma_db/ # ChromaDB持久化数据这个手工作坊式的项目是你理解RAG每一寸肌肤的最佳方式。当你对数据流、瓶颈、调参点都有了切身感受后你可以选择引入LangChain/LlamaIndex用它们来替换你手写的部分模块如更复杂的文档加载器、链式调用提升开发效率。这时你是在“驾驶”框架而不是被框架“裹挟”。探索高级模式如Agentic RAG让LLM主动决定何时、如何检索、Hypothetical Document Embeddings (HyDE)先让LLM生成一个假设答案再用这个答案去检索效果奇佳等。构建Web服务使用FastAPI将你的RAG系统包装成API供前端调用。持续迭代根据评估结果持续优化分块、嵌入模型、提示词甚至微调一个本地的小型重排序模型。RAG不是一个一蹴而就的框架调用而是一个需要持续观察、分析和调优的系统工程。这份手册给你的不是一辆现成的汽车而是一套完整的汽车零件和组装图纸。从拧第一个螺丝开始你才能真正掌握驾驶它的能力。当你下次再看到“五分钟搭建RAG”的标题时你心里会清楚那只是旅程的起点而真正的道路现在才刚刚在你脚下展开。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号