1. 项目概述一小时构建你的专属RAG系统如果你对AI应用开发感兴趣尤其是想快速搭建一个能“理解”你私人文档库的智能助手那么“在一小时内构建你自己的RAG栈”这个项目绝对值得你投入这六十分钟。RAG也就是检索增强生成它不是什么遥不可及的尖端科技而是当下让大语言模型变得更“接地气”、更“靠谱”的核心技术。简单来说它解决了大模型的两个核心痛点一是“幻觉”即一本正经地胡说八道二是知识“过时”模型训练数据截止后新知识它就不知道了。这个项目的核心目标就是让你绕过复杂的理论直接动手用最精简、最高效的工具链从零开始搭建一个能处理你自己文档比如PDF、Word、网页文章的问答系统。你不需要是机器学习专家只需要基础的Python编程能力和一台普通的电脑。一小时后你将拥有一个可以上传文档、自动解析、智能检索并根据文档内容准确回答问题的原型系统。这不仅是学习RAG技术的最佳实践更是你开启个性化AI应用开发的起点无论是用于个人知识管理、企业内部文档查询还是作为更复杂应用的基石这个“一小时项目”都能给你打下坚实的实操基础。2. 技术栈选型与快速部署策略要在短时间内完成一个可运行的RAG系统工具的选择至关重要。我们需要在功能、易用性和部署速度之间找到最佳平衡点。下面这套组合是我经过多次实践筛选出来的“黄金一小时套餐”它避开了那些需要复杂环境配置和大量依赖管理的重型框架。2.1 核心组件解析为什么是它们向量数据库ChromaDB在众多向量数据库中我首选ChromaDB。原因很简单它轻量、纯Python、且可以完全在内存中运行。对于这个一小时项目我们不需要考虑分布式、高可用这些生产级特性。ChromaDB的API设计非常直观几行代码就能完成集合创建、文档嵌入和相似性搜索极大地降低了入门门槛。它内置了与主流嵌入模型的集成让我们可以专注于应用逻辑而非底层存储细节。文本嵌入模型Sentence Transformers (all-MiniLM-L6-v2)嵌入模型负责将文本转换为计算机能理解的数字向量即嵌入。我选择sentence-transformers库中的all-MiniLM-L6-v2模型。这是一个在速度和效果上取得绝佳平衡的模型。它体积小约80MB在CPU上运行也足够快同时它在语义相似度任务上的表现经过了广泛验证。对于中文文本我们可以选择其多语言版本paraphrase-multilingual-MiniLM-L12-v2效果同样出色。使用这个库加载和运行模型只需两行代码。大语言模型Ollama Llama 3.1为了完全本地化、避免网络延迟和API费用我们使用Ollama。Ollama是一个强大的工具可以让你在本地一键拉取和运行诸如Llama 3、Mistral等开源大模型。我推荐使用Llama 3.1 8B版本。它在8B参数量级别上提供了优异的指令跟随和文本生成能力对硬件要求相对友好需要约8-10GB显存纯CPU也可运行但较慢。Ollama的管理命令极其简单ollama run llama3.1就能启动一个交互式会话。应用框架Gradio我们需要一个简单直观的界面来上传文件、输入问题和展示答案。Gradio是完成这个任务的不二之选。它是一个为机器学习模型快速构建Web界面的Python库用十几行代码就能生成一个包含文件上传、文本框和按钮的交互式应用。它支持实时反馈非常适合我们这种需要快速演示和迭代的原型。2.2 环境准备十分钟搞定一切假设你已经安装了Python3.8以上版本我们通过一个requirements.txt文件来一次性安装所有依赖。这是最节省时间的方法。首先创建一个项目目录例如my_rag_project然后在该目录下创建requirements.txt文件内容如下langchain0.1.0 langchain-community0.0.10 chromadb0.4.22 sentence-transformers2.2.2 gradio4.19.2 pypdf3.17.4 python-dotenv1.0.0注意这里引入了langchain和langchain-community。虽然LangChain有时被诟病“过度抽象”但对于快速原型而言它提供的文档加载器、文本分割器等组件能极大提升开发效率。我们只使用其最核心、最稳定的部分。接下来在终端中执行安装命令。强烈建议使用虚拟环境。# 创建并激活虚拟环境以venv为例 python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装依赖 pip install -r requirements.txt最后安装并启动Ollama。请访问Ollama官网下载对应操作系统的安装包。安装完成后打开一个新的终端窗口运行以下命令拉取并运行模型ollama pull llama3.1 ollama run llama3.1运行成功后你可以在这个终端里和Llama 3.1对话测试模型是否正常工作。保持这个终端运行我们的Python程序将通过本地API与它通信。至此所有环境在十分钟内准备就绪。接下来我们将进入核心的代码构建环节。3. 核心模块构建文档处理与向量化构建RAG系统的第一步是让你的文档变成机器可检索的格式。这个过程包括加载、分割和向量化我们称之为“索引”阶段。3.1 文档加载与智能文本分割文档加载器负责从不同格式的文件中提取纯文本。我们使用langchain_community.document_loaders中的组件。from langchain_community.document_loaders import PyPDFLoader, TextLoader, UnstructuredWordDocumentLoader import os def load_document(file_path): 根据文件后缀名选择合适的加载器 ext os.path.splitext(file_path)[-1].lower() if ext .pdf: loader PyPDFLoader(file_path) elif ext .docx or ext .doc: loader UnstructuredWordDocumentLoader(file_path) elif ext .txt: loader TextLoader(file_path, encodingutf-8) else: raise ValueError(fUnsupported file type: {ext}) documents loader.load() return documents加载后的文档是一个Document对象列表每个对象包含页面内容和元数据。接下来是最关键的一步文本分割。大模型有上下文长度限制我们不能把整本书塞给它。分割的目标是既要保持语义的完整性又要有适当的重叠以避免信息在边界处丢失。我推荐使用RecursiveCharacterTextSplitter它尝试按字符递归分割如段落、句子、单词是通用性最好的选择。from langchain.text_splitter import RecursiveCharacterTextSplitter def split_documents(documents, chunk_size500, chunk_overlap50): 分割文档。 chunk_size: 每个文本块的最大字符数。 chunk_overlap: 块之间的重叠字符数用于保持上下文连贯。 text_splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionlen, separators[\n\n, \n, 。, , , , , , ] ) splits text_splitter.split_documents(documents) print(f将 {len(documents)} 个文档分割为 {len(splits)} 个文本块。) return splits实操心得分割参数的艺术chunk_size和chunk_overlap的设置需要根据你的文档类型和模型上下文窗口调整。对于技术文档或论文chunk_size800可能更合适以保留完整的代码段或论证逻辑。对于新闻或社交媒体文本chunk_size300也许就够了。重叠部分通常设为chunk_size的10%-20%。一个重要的技巧是分割后务必随机检查几个文本块确保它们没有在句子中间或单词中间被生硬地切断。不合理的分割是后续检索效果差的主要原因之一。3.2 向量化存储构建你的“记忆库”文本分割后我们需要将它们转换为向量并存入ChromaDB。这里我们直接使用sentence-transformers模型创建嵌入。from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings # 初始化嵌入模型 embed_model SentenceTransformer(all-MiniLM-L6-v2) # 英文 # 对于中文使用embed_model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) # 初始化Chroma客户端持久化到磁盘 chroma_client chromadb.PersistentClient(path./chroma_db) # 创建或获取一个集合类似于数据库的表 collection chroma_client.get_or_create_collection( namemy_knowledge_base, metadata{hnsw:space: cosine} # 使用余弦相似度进行检索 ) def index_documents(splits): 将分割后的文本块向量化并存入ChromaDB texts [doc.page_content for doc in splits] metadatas [doc.metadata for doc in splits] # 生成嵌入向量 print(正在生成文本嵌入向量...) embeddings embed_model.encode(texts, show_progress_barTrue).tolist() # 为每个块生成唯一ID ids [fchunk_{i} for i in range(len(texts))] # 批量添加到集合 collection.add( embeddingsembeddings, documentstexts, metadatasmetadatas, idsids ) print(f已成功索引 {len(texts)} 个文本块到向量数据库。) return collection这段代码完成了核心的索引流程加载模型、连接数据库、将文本转换为向量并存储。chromadb.PersistentClient确保了数据被保存在本地./chroma_db目录下次启动无需重新索引。4. 检索与生成链路的实现索引构建好后就进入了RAG的“检索-增强-生成”核心循环。当用户提出一个问题时系统需要1将问题转换为向量2从向量库中找到最相关的文本块3将这些文本块作为上下文连同问题一起提交给大模型生成最终答案。4.1 相似性检索与上下文组装首先我们实现检索函数。它负责将用户查询向量化并从ChromaDB中找出最相关的K个文本块。def retrieve_relevant_chunks(query, collection, top_k3): 检索与查询最相关的文本块。 query: 用户问题 top_k: 返回最相关的K个结果 # 将查询语句转换为向量 query_embedding embed_model.encode([query]).tolist()[0] # 执行相似性搜索 results collection.query( query_embeddings[query_embedding], n_resultstop_k ) # 整理检索结果 retrieved_docs results[documents][0] # 取第一个查询的结果 retrieved_metadatas results[metadatas][0] retrieved_distances results[distances][0] print(f检索到 {len(retrieved_docs)} 个相关片段。) for i, (doc, meta, dist) in enumerate(zip(retrieved_docs, retrieved_metadatas, retrieved_distances)): print(f\n--- 结果 {i1} (距离: {dist:.4f}) ---) print(f来源: {meta.get(source, N/A)} - 页码: {meta.get(page, N/A)}) print(f内容预览: {doc[:150]}...) return retrieved_docs检索到的文本块需要被组装成一段连贯的“上下文”作为提示词的一部分送给大模型。组装策略直接影响生成质量。def build_context(retrieved_docs): 将检索到的文档片段组装成上下文提示 context for i, doc in enumerate(retrieved_docs): context f[相关段落 {i1}]:\n{doc}\n\n return context.strip()4.2 提示词工程与大模型调用这是连接检索系统与大模型的桥梁。一个精心设计的提示词Prompt能极大地提升答案的准确性和相关性。我们使用一个简单的模板明确指示模型基于给定的上下文回答问题。def build_prompt(query, context): 构建发送给LLM的最终提示词 prompt_template f请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据提供的资料我无法回答这个问题”不要编造信息。 上下文 {context} 问题{query} 请基于上下文给出准确、简洁的答案 return prompt_template接下来我们需要与本地运行的Ollama服务交互。Ollama提供了一个类OpenAI的API接口我们可以通过requests库直接调用。import requests import json def ask_llama(prompt, modelllama3.1, temperature0.1): 调用本地Ollama服务的LLM生成答案。 temperature: 控制随机性越低答案越确定。对于事实性问答建议设低0.1-0.3。 url http://localhost:11434/api/generate payload { model: model, prompt: prompt, stream: False, options: { temperature: temperature, num_predict: 512 # 限制生成的最大token数 } } try: response requests.post(url, jsonpayload) response.raise_for_status() result response.json() return result[response].strip() except requests.exceptions.ConnectionError: return 错误无法连接到Ollama服务。请确保Ollama正在运行在终端中输入 ollama run llama3.1。 except Exception as e: return f调用模型时发生错误{str(e)}现在我们可以将检索、提示构建和生成三个步骤串联起来形成一个完整的RAG问答函数。def rag_query(query, collection, top_k3): 完整的RAG问答流程 print(f\n用户提问: {query}) # 1. 检索 relevant_chunks retrieve_relevant_chunks(query, collection, top_k) if not relevant_chunks: return 未在知识库中找到相关信息。 # 2. 构建上下文和提示 context build_context(relevant_chunks) prompt build_prompt(query, context) # 3. 生成答案 print(正在生成答案...) answer ask_llama(prompt) return answer5. 集成与交互打造用户界面为了让整个系统易于使用我们使用Gradio快速构建一个Web界面。这个界面将允许用户上传文档、触发索引过程并进行问答交互。5.1 使用Gradio构建前端界面Gradio的BlocksAPI提供了高度的灵活性我们可以构建一个包含多个步骤的标签页界面。import gradio as gr import tempfile import shutil # 定义全局变量用于在Gradio回调间共享状态 global_collection None indexed_files [] def upload_and_index(files): 处理上传的文件进行加载、分割和索引 global global_collection, indexed_files splits_all [] file_paths [] for file in files: file_path file.name file_paths.append(file_path) print(f正在处理文件: {file_path}) # 加载文档 docs load_document(file_path) # 分割文档 splits split_documents(docs) splits_all.extend(splits) if splits_all: # 索引到向量数据库 global_collection index_documents(splits_all) indexed_files file_paths return f成功已索引 {len(splits_all)} 个文本块来自 {len(file_paths)} 个文件。\n文件列表{, .join([os.path.basename(f) for f in file_paths])} else: return 未成功加载任何文本内容请检查文件格式。 def answer_question(question, history): 处理用户提问调用RAG流程并管理对话历史 global global_collection if global_collection is None: return history [(question, 请先上传并索引文档。)], answer rag_query(question, global_collection) # 将本轮问答添加到历史中。Gradio的Chatbot组件期望历史记录格式为[(用户输入1, 助手回复1), (用户输入2, 助手回复2), ...] new_history history [(question, answer)] return new_history, # 返回更新后的历史并清空输入框 # 构建Gradio界面 with gr.Blocks(title我的RAG知识库助手, themegr.themes.Soft()) as demo: gr.Markdown(# 一小时构建你的专属RAG知识库) gr.Markdown(上传你的文档PDF/Word/TXT构建专属知识库然后开始智能问答。) with gr.Tab( 1. 上传与索引): file_input gr.File(label选择文档, file_countmultiple, file_types[.pdf, .docx, .doc, .txt]) index_button gr.Button(开始索引文档, variantprimary) index_output gr.Textbox(label索引状态, interactiveFalse) index_button.click(upload_and_index, inputs[file_input], outputs[index_output]) with gr.Tab( 2. 智能问答): chatbot gr.Chatbot(label对话历史, height400) msg gr.Textbox(label你的问题, placeholder请输入关于文档内容的问题...) clear_btn gr.Button(清空对话) def respond(message, chat_history): # 调用answer_question函数它返回更新后的历史记录和清空的消息 new_history, _ answer_question(message, chat_history) return new_history, # 当在文本框中按回车或点击发送时触发 msg.submit(respond, [msg, chatbot], [chatbot, msg]) # 也可以单独加一个发送按钮 send_btn gr.Button(发送) send_btn.click(respond, [msg, chatbot], [chatbot, msg]) clear_btn.click(lambda: None, None, chatbot, queueFalse) gr.Markdown(---) gr.Markdown(**技术栈**: ChromaDB | Sentence Transformers | Ollama (Llama 3.1) | Gradio) # 启动应用 if __name__ __main__: demo.launch(shareFalse, server_name0.0.0.0, server_port7860) # 在本地7860端口启动运行这个脚本Gradio会在本地启动一个Web服务器。打开浏览器访问http://localhost:7860你就能看到完整的应用界面。首先在“上传与索引”标签页上传你的文档并点击索引按钮然后在“智能问答”标签页开始提问。5.2 一键运行脚本与项目结构为了最简化操作你可以将上述所有代码整合到一个名为app.py的文件中。项目的最终目录结构如下my_rag_project/ ├── app.py # 主程序包含所有代码 ├── requirements.txt # 依赖列表 ├── chroma_db/ # ChromaDB自动生成的数据库目录 └── your_documents/ # 可选存放待上传文档的目录在终端激活虚拟环境后直接运行python app.py。首次运行时会下载sentence-transformers模型约80MB稍等片刻即可。看到“Running on local URL: http://0.0.0.0:7860”的输出后就可以在浏览器中使用了。6. 效果优化与常见问题排查一个能跑起来的系统只是第一步要让它的回答更准确、更可靠还需要一些调优技巧。以下是你在实际使用中一定会遇到的问题和解决方案。6.1 提升检索与回答质量的实用技巧1. 优化文本分割策略如果发现模型回答时经常遗漏关键信息或断章取义首先检查文本分割。尝试调整chunk_size和chunk_overlap。一个更高级的方法是使用“语义分割器”如SemanticChunkerLangChain提供它尝试在语义边界如段落结束进行分割但这需要额外的模型调用会减慢索引速度。2. 改进检索策略调整top_ktop_k决定了给模型喂多少上下文。太少可能信息不足太多可能引入噪声并超出模型上下文窗口。通常从3开始尝试根据答案的完整性和相关性调整。使用MMR最大边际相关性重排序简单的相似度搜索可能会返回内容高度重复的片段。MMR算法可以在保证相关性的同时增加结果的多样性。ChromaDB和LangChain都支持MMR。元数据过滤如果你的文档有清晰的元数据如章节标题、文档类型可以在检索时添加过滤条件。例如当用户问“第三章讲了什么”你可以让检索只返回metadata[chapter] 3的片段这能极大提升精度。3. 精炼提示词工程我们之前使用的提示词模板是基础版。你可以让它更强大明确指令加入“如果上下文不包含相关信息请明确告知用户无法回答”。指定格式要求模型“用分点列表的形式回答”或“先总结再详细说明”。引用来源要求模型在答案中注明信息出自哪个片段例如“根据[相关段落1]...”这增加了答案的可追溯性和可信度。一个增强版的提示词示例enhanced_prompt_template f你是一个专业的文档分析助手。请严格根据以下提供的上下文信息来回答用户的问题。 ### 上下文信息 {context} ### 用户问题 {query} ### 你的任务 1. 仔细分析上下文找出与问题直接相关的内容。 2. 如果上下文信息足以回答问题请组织一个清晰、准确、简洁的答案。**如果可能请指出你的答案主要基于哪个相关段落例如参考[段落1]。** 3. 如果上下文信息完全不足以回答该问题请直接说“根据提供的资料我无法回答这个问题。” 现在请开始你的回答6.2 常见问题与解决方案速查表在实际操作中你可能会遇到以下典型问题。这里提供一个快速排查指南。问题现象可能原因解决方案运行python app.py时报错提示缺少模块依赖未正确安装。1. 确认已激活虚拟环境。2. 在项目目录下执行pip install -r requirements.txt。索引文档时程序卡住或报编码错误1. 文档格式特殊或损坏。2. 非UTF-8编码的文本文件。1. 尝试用其他软件打开文档确认其完整性。2. 对于.txt文件尝试指定编码如TextLoader(file_path, encodinggb18030)针对中文GBK编码。Ollama连接失败提示“无法连接到服务”Ollama服务未启动或未在默认端口运行。1. 打开一个独立的终端运行ollama run llama3.1并保持运行。2. 确认app.py中调用API的URL是http://localhost:11434。问答响应速度非常慢1. 模型在CPU上运行。2. 检索的top_k值过大导致提示词过长。1. 如果有NVIDIA GPU确保已安装对应版本的PyTorch和CUDAOllama会自动利用GPU加速。2. 尝试减小top_k例如从5减到3。3. 检查sentence-transformers模型是否在首次运行时已下载完成。模型回答的内容与文档无关“幻觉”1. 检索到的片段不相关。2. 提示词指令不够强硬。3. 模型temperature参数过高。1. 检查检索结果代码中的print输出看返回的片段是否真的与问题相关。若不相关可能需要优化文档分割或尝试不同的嵌入模型。2. 强化提示词使用“严格根据上下文”、“禁止编造”等措辞。3. 降低ask_llama函数中的temperature参数如设为0.1。答案总是“根据提供的资料我无法回答这个问题”1. 检索确实未找到任何相关信息。2. 提示词中关于“无法回答”的条件过于严格或模糊。1. 检查你的问题是否确实在文档范围内。尝试问一个文档中明确存在答案的简单问题来测试。2. 调整提示词将“无法回答”的条件描述得更具体例如改为“如果上下文中完全没有提及问题中的核心关键词则告知无法回答”。Gradio界面无法上传文件或点击无反应浏览器兼容性问题或Gradio版本问题。1. 尝试使用Chrome或Edge浏览器。2. 确保Gradio版本为较新稳定版回退版本有时能解决问题pip install gradio4.19.2。6.3 性能与扩展性考量当前的一小时原型侧重于功能和速度。如果你希望将其用于更严肃的场景需要考虑以下几点嵌入模型升级all-MiniLM-L6-v2是速度和效果的折衷。对于更高精度的要求可以考虑all-mpnet-base-v2它更大也更慢但生成的向量质量更高。对于中文text2vec系列模型如text2vec-base-chinese是专门优化的选择。向量数据库升级如果数据量超过数万条或者需要持久化、远程访问可以考虑迁移到Qdrant、Weaviate或Milvus等支持生产部署的向量数据库。引入查询转换对于复杂问题可以先让大模型对原始查询进行改写或分解再进行检索。例如将“苹果公司最新手机的摄像头有什么特点”分解为“苹果公司最新手机型号”和“该型号摄像头特点”两个子查询分别检索后再综合答案这能显著提升复杂问答的效果。添加对话历史当前的实现是单轮问答。要实现多轮对话需要将历史问答也纳入上下文管理。一种简单的方法是将之前几轮的“问题-答案”对也作为文本块存入向量库或者在构建提示词时附上最近的对话历史。
一小时构建专属RAG系统:基于ChromaDB与Llama 3.1的本地化实践
发布时间:2026/5/31 10:40:07
1. 项目概述一小时构建你的专属RAG系统如果你对AI应用开发感兴趣尤其是想快速搭建一个能“理解”你私人文档库的智能助手那么“在一小时内构建你自己的RAG栈”这个项目绝对值得你投入这六十分钟。RAG也就是检索增强生成它不是什么遥不可及的尖端科技而是当下让大语言模型变得更“接地气”、更“靠谱”的核心技术。简单来说它解决了大模型的两个核心痛点一是“幻觉”即一本正经地胡说八道二是知识“过时”模型训练数据截止后新知识它就不知道了。这个项目的核心目标就是让你绕过复杂的理论直接动手用最精简、最高效的工具链从零开始搭建一个能处理你自己文档比如PDF、Word、网页文章的问答系统。你不需要是机器学习专家只需要基础的Python编程能力和一台普通的电脑。一小时后你将拥有一个可以上传文档、自动解析、智能检索并根据文档内容准确回答问题的原型系统。这不仅是学习RAG技术的最佳实践更是你开启个性化AI应用开发的起点无论是用于个人知识管理、企业内部文档查询还是作为更复杂应用的基石这个“一小时项目”都能给你打下坚实的实操基础。2. 技术栈选型与快速部署策略要在短时间内完成一个可运行的RAG系统工具的选择至关重要。我们需要在功能、易用性和部署速度之间找到最佳平衡点。下面这套组合是我经过多次实践筛选出来的“黄金一小时套餐”它避开了那些需要复杂环境配置和大量依赖管理的重型框架。2.1 核心组件解析为什么是它们向量数据库ChromaDB在众多向量数据库中我首选ChromaDB。原因很简单它轻量、纯Python、且可以完全在内存中运行。对于这个一小时项目我们不需要考虑分布式、高可用这些生产级特性。ChromaDB的API设计非常直观几行代码就能完成集合创建、文档嵌入和相似性搜索极大地降低了入门门槛。它内置了与主流嵌入模型的集成让我们可以专注于应用逻辑而非底层存储细节。文本嵌入模型Sentence Transformers (all-MiniLM-L6-v2)嵌入模型负责将文本转换为计算机能理解的数字向量即嵌入。我选择sentence-transformers库中的all-MiniLM-L6-v2模型。这是一个在速度和效果上取得绝佳平衡的模型。它体积小约80MB在CPU上运行也足够快同时它在语义相似度任务上的表现经过了广泛验证。对于中文文本我们可以选择其多语言版本paraphrase-multilingual-MiniLM-L12-v2效果同样出色。使用这个库加载和运行模型只需两行代码。大语言模型Ollama Llama 3.1为了完全本地化、避免网络延迟和API费用我们使用Ollama。Ollama是一个强大的工具可以让你在本地一键拉取和运行诸如Llama 3、Mistral等开源大模型。我推荐使用Llama 3.1 8B版本。它在8B参数量级别上提供了优异的指令跟随和文本生成能力对硬件要求相对友好需要约8-10GB显存纯CPU也可运行但较慢。Ollama的管理命令极其简单ollama run llama3.1就能启动一个交互式会话。应用框架Gradio我们需要一个简单直观的界面来上传文件、输入问题和展示答案。Gradio是完成这个任务的不二之选。它是一个为机器学习模型快速构建Web界面的Python库用十几行代码就能生成一个包含文件上传、文本框和按钮的交互式应用。它支持实时反馈非常适合我们这种需要快速演示和迭代的原型。2.2 环境准备十分钟搞定一切假设你已经安装了Python3.8以上版本我们通过一个requirements.txt文件来一次性安装所有依赖。这是最节省时间的方法。首先创建一个项目目录例如my_rag_project然后在该目录下创建requirements.txt文件内容如下langchain0.1.0 langchain-community0.0.10 chromadb0.4.22 sentence-transformers2.2.2 gradio4.19.2 pypdf3.17.4 python-dotenv1.0.0注意这里引入了langchain和langchain-community。虽然LangChain有时被诟病“过度抽象”但对于快速原型而言它提供的文档加载器、文本分割器等组件能极大提升开发效率。我们只使用其最核心、最稳定的部分。接下来在终端中执行安装命令。强烈建议使用虚拟环境。# 创建并激活虚拟环境以venv为例 python -m venv venv # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate # 安装依赖 pip install -r requirements.txt最后安装并启动Ollama。请访问Ollama官网下载对应操作系统的安装包。安装完成后打开一个新的终端窗口运行以下命令拉取并运行模型ollama pull llama3.1 ollama run llama3.1运行成功后你可以在这个终端里和Llama 3.1对话测试模型是否正常工作。保持这个终端运行我们的Python程序将通过本地API与它通信。至此所有环境在十分钟内准备就绪。接下来我们将进入核心的代码构建环节。3. 核心模块构建文档处理与向量化构建RAG系统的第一步是让你的文档变成机器可检索的格式。这个过程包括加载、分割和向量化我们称之为“索引”阶段。3.1 文档加载与智能文本分割文档加载器负责从不同格式的文件中提取纯文本。我们使用langchain_community.document_loaders中的组件。from langchain_community.document_loaders import PyPDFLoader, TextLoader, UnstructuredWordDocumentLoader import os def load_document(file_path): 根据文件后缀名选择合适的加载器 ext os.path.splitext(file_path)[-1].lower() if ext .pdf: loader PyPDFLoader(file_path) elif ext .docx or ext .doc: loader UnstructuredWordDocumentLoader(file_path) elif ext .txt: loader TextLoader(file_path, encodingutf-8) else: raise ValueError(fUnsupported file type: {ext}) documents loader.load() return documents加载后的文档是一个Document对象列表每个对象包含页面内容和元数据。接下来是最关键的一步文本分割。大模型有上下文长度限制我们不能把整本书塞给它。分割的目标是既要保持语义的完整性又要有适当的重叠以避免信息在边界处丢失。我推荐使用RecursiveCharacterTextSplitter它尝试按字符递归分割如段落、句子、单词是通用性最好的选择。from langchain.text_splitter import RecursiveCharacterTextSplitter def split_documents(documents, chunk_size500, chunk_overlap50): 分割文档。 chunk_size: 每个文本块的最大字符数。 chunk_overlap: 块之间的重叠字符数用于保持上下文连贯。 text_splitter RecursiveCharacterTextSplitter( chunk_sizechunk_size, chunk_overlapchunk_overlap, length_functionlen, separators[\n\n, \n, 。, , , , , , ] ) splits text_splitter.split_documents(documents) print(f将 {len(documents)} 个文档分割为 {len(splits)} 个文本块。) return splits实操心得分割参数的艺术chunk_size和chunk_overlap的设置需要根据你的文档类型和模型上下文窗口调整。对于技术文档或论文chunk_size800可能更合适以保留完整的代码段或论证逻辑。对于新闻或社交媒体文本chunk_size300也许就够了。重叠部分通常设为chunk_size的10%-20%。一个重要的技巧是分割后务必随机检查几个文本块确保它们没有在句子中间或单词中间被生硬地切断。不合理的分割是后续检索效果差的主要原因之一。3.2 向量化存储构建你的“记忆库”文本分割后我们需要将它们转换为向量并存入ChromaDB。这里我们直接使用sentence-transformers模型创建嵌入。from sentence_transformers import SentenceTransformer import chromadb from chromadb.config import Settings # 初始化嵌入模型 embed_model SentenceTransformer(all-MiniLM-L6-v2) # 英文 # 对于中文使用embed_model SentenceTransformer(paraphrase-multilingual-MiniLM-L12-v2) # 初始化Chroma客户端持久化到磁盘 chroma_client chromadb.PersistentClient(path./chroma_db) # 创建或获取一个集合类似于数据库的表 collection chroma_client.get_or_create_collection( namemy_knowledge_base, metadata{hnsw:space: cosine} # 使用余弦相似度进行检索 ) def index_documents(splits): 将分割后的文本块向量化并存入ChromaDB texts [doc.page_content for doc in splits] metadatas [doc.metadata for doc in splits] # 生成嵌入向量 print(正在生成文本嵌入向量...) embeddings embed_model.encode(texts, show_progress_barTrue).tolist() # 为每个块生成唯一ID ids [fchunk_{i} for i in range(len(texts))] # 批量添加到集合 collection.add( embeddingsembeddings, documentstexts, metadatasmetadatas, idsids ) print(f已成功索引 {len(texts)} 个文本块到向量数据库。) return collection这段代码完成了核心的索引流程加载模型、连接数据库、将文本转换为向量并存储。chromadb.PersistentClient确保了数据被保存在本地./chroma_db目录下次启动无需重新索引。4. 检索与生成链路的实现索引构建好后就进入了RAG的“检索-增强-生成”核心循环。当用户提出一个问题时系统需要1将问题转换为向量2从向量库中找到最相关的文本块3将这些文本块作为上下文连同问题一起提交给大模型生成最终答案。4.1 相似性检索与上下文组装首先我们实现检索函数。它负责将用户查询向量化并从ChromaDB中找出最相关的K个文本块。def retrieve_relevant_chunks(query, collection, top_k3): 检索与查询最相关的文本块。 query: 用户问题 top_k: 返回最相关的K个结果 # 将查询语句转换为向量 query_embedding embed_model.encode([query]).tolist()[0] # 执行相似性搜索 results collection.query( query_embeddings[query_embedding], n_resultstop_k ) # 整理检索结果 retrieved_docs results[documents][0] # 取第一个查询的结果 retrieved_metadatas results[metadatas][0] retrieved_distances results[distances][0] print(f检索到 {len(retrieved_docs)} 个相关片段。) for i, (doc, meta, dist) in enumerate(zip(retrieved_docs, retrieved_metadatas, retrieved_distances)): print(f\n--- 结果 {i1} (距离: {dist:.4f}) ---) print(f来源: {meta.get(source, N/A)} - 页码: {meta.get(page, N/A)}) print(f内容预览: {doc[:150]}...) return retrieved_docs检索到的文本块需要被组装成一段连贯的“上下文”作为提示词的一部分送给大模型。组装策略直接影响生成质量。def build_context(retrieved_docs): 将检索到的文档片段组装成上下文提示 context for i, doc in enumerate(retrieved_docs): context f[相关段落 {i1}]:\n{doc}\n\n return context.strip()4.2 提示词工程与大模型调用这是连接检索系统与大模型的桥梁。一个精心设计的提示词Prompt能极大地提升答案的准确性和相关性。我们使用一个简单的模板明确指示模型基于给定的上下文回答问题。def build_prompt(query, context): 构建发送给LLM的最终提示词 prompt_template f请严格根据以下提供的上下文信息来回答问题。如果上下文中的信息不足以回答问题请直接说“根据提供的资料我无法回答这个问题”不要编造信息。 上下文 {context} 问题{query} 请基于上下文给出准确、简洁的答案 return prompt_template接下来我们需要与本地运行的Ollama服务交互。Ollama提供了一个类OpenAI的API接口我们可以通过requests库直接调用。import requests import json def ask_llama(prompt, modelllama3.1, temperature0.1): 调用本地Ollama服务的LLM生成答案。 temperature: 控制随机性越低答案越确定。对于事实性问答建议设低0.1-0.3。 url http://localhost:11434/api/generate payload { model: model, prompt: prompt, stream: False, options: { temperature: temperature, num_predict: 512 # 限制生成的最大token数 } } try: response requests.post(url, jsonpayload) response.raise_for_status() result response.json() return result[response].strip() except requests.exceptions.ConnectionError: return 错误无法连接到Ollama服务。请确保Ollama正在运行在终端中输入 ollama run llama3.1。 except Exception as e: return f调用模型时发生错误{str(e)}现在我们可以将检索、提示构建和生成三个步骤串联起来形成一个完整的RAG问答函数。def rag_query(query, collection, top_k3): 完整的RAG问答流程 print(f\n用户提问: {query}) # 1. 检索 relevant_chunks retrieve_relevant_chunks(query, collection, top_k) if not relevant_chunks: return 未在知识库中找到相关信息。 # 2. 构建上下文和提示 context build_context(relevant_chunks) prompt build_prompt(query, context) # 3. 生成答案 print(正在生成答案...) answer ask_llama(prompt) return answer5. 集成与交互打造用户界面为了让整个系统易于使用我们使用Gradio快速构建一个Web界面。这个界面将允许用户上传文档、触发索引过程并进行问答交互。5.1 使用Gradio构建前端界面Gradio的BlocksAPI提供了高度的灵活性我们可以构建一个包含多个步骤的标签页界面。import gradio as gr import tempfile import shutil # 定义全局变量用于在Gradio回调间共享状态 global_collection None indexed_files [] def upload_and_index(files): 处理上传的文件进行加载、分割和索引 global global_collection, indexed_files splits_all [] file_paths [] for file in files: file_path file.name file_paths.append(file_path) print(f正在处理文件: {file_path}) # 加载文档 docs load_document(file_path) # 分割文档 splits split_documents(docs) splits_all.extend(splits) if splits_all: # 索引到向量数据库 global_collection index_documents(splits_all) indexed_files file_paths return f成功已索引 {len(splits_all)} 个文本块来自 {len(file_paths)} 个文件。\n文件列表{, .join([os.path.basename(f) for f in file_paths])} else: return 未成功加载任何文本内容请检查文件格式。 def answer_question(question, history): 处理用户提问调用RAG流程并管理对话历史 global global_collection if global_collection is None: return history [(question, 请先上传并索引文档。)], answer rag_query(question, global_collection) # 将本轮问答添加到历史中。Gradio的Chatbot组件期望历史记录格式为[(用户输入1, 助手回复1), (用户输入2, 助手回复2), ...] new_history history [(question, answer)] return new_history, # 返回更新后的历史并清空输入框 # 构建Gradio界面 with gr.Blocks(title我的RAG知识库助手, themegr.themes.Soft()) as demo: gr.Markdown(# 一小时构建你的专属RAG知识库) gr.Markdown(上传你的文档PDF/Word/TXT构建专属知识库然后开始智能问答。) with gr.Tab( 1. 上传与索引): file_input gr.File(label选择文档, file_countmultiple, file_types[.pdf, .docx, .doc, .txt]) index_button gr.Button(开始索引文档, variantprimary) index_output gr.Textbox(label索引状态, interactiveFalse) index_button.click(upload_and_index, inputs[file_input], outputs[index_output]) with gr.Tab( 2. 智能问答): chatbot gr.Chatbot(label对话历史, height400) msg gr.Textbox(label你的问题, placeholder请输入关于文档内容的问题...) clear_btn gr.Button(清空对话) def respond(message, chat_history): # 调用answer_question函数它返回更新后的历史记录和清空的消息 new_history, _ answer_question(message, chat_history) return new_history, # 当在文本框中按回车或点击发送时触发 msg.submit(respond, [msg, chatbot], [chatbot, msg]) # 也可以单独加一个发送按钮 send_btn gr.Button(发送) send_btn.click(respond, [msg, chatbot], [chatbot, msg]) clear_btn.click(lambda: None, None, chatbot, queueFalse) gr.Markdown(---) gr.Markdown(**技术栈**: ChromaDB | Sentence Transformers | Ollama (Llama 3.1) | Gradio) # 启动应用 if __name__ __main__: demo.launch(shareFalse, server_name0.0.0.0, server_port7860) # 在本地7860端口启动运行这个脚本Gradio会在本地启动一个Web服务器。打开浏览器访问http://localhost:7860你就能看到完整的应用界面。首先在“上传与索引”标签页上传你的文档并点击索引按钮然后在“智能问答”标签页开始提问。5.2 一键运行脚本与项目结构为了最简化操作你可以将上述所有代码整合到一个名为app.py的文件中。项目的最终目录结构如下my_rag_project/ ├── app.py # 主程序包含所有代码 ├── requirements.txt # 依赖列表 ├── chroma_db/ # ChromaDB自动生成的数据库目录 └── your_documents/ # 可选存放待上传文档的目录在终端激活虚拟环境后直接运行python app.py。首次运行时会下载sentence-transformers模型约80MB稍等片刻即可。看到“Running on local URL: http://0.0.0.0:7860”的输出后就可以在浏览器中使用了。6. 效果优化与常见问题排查一个能跑起来的系统只是第一步要让它的回答更准确、更可靠还需要一些调优技巧。以下是你在实际使用中一定会遇到的问题和解决方案。6.1 提升检索与回答质量的实用技巧1. 优化文本分割策略如果发现模型回答时经常遗漏关键信息或断章取义首先检查文本分割。尝试调整chunk_size和chunk_overlap。一个更高级的方法是使用“语义分割器”如SemanticChunkerLangChain提供它尝试在语义边界如段落结束进行分割但这需要额外的模型调用会减慢索引速度。2. 改进检索策略调整top_ktop_k决定了给模型喂多少上下文。太少可能信息不足太多可能引入噪声并超出模型上下文窗口。通常从3开始尝试根据答案的完整性和相关性调整。使用MMR最大边际相关性重排序简单的相似度搜索可能会返回内容高度重复的片段。MMR算法可以在保证相关性的同时增加结果的多样性。ChromaDB和LangChain都支持MMR。元数据过滤如果你的文档有清晰的元数据如章节标题、文档类型可以在检索时添加过滤条件。例如当用户问“第三章讲了什么”你可以让检索只返回metadata[chapter] 3的片段这能极大提升精度。3. 精炼提示词工程我们之前使用的提示词模板是基础版。你可以让它更强大明确指令加入“如果上下文不包含相关信息请明确告知用户无法回答”。指定格式要求模型“用分点列表的形式回答”或“先总结再详细说明”。引用来源要求模型在答案中注明信息出自哪个片段例如“根据[相关段落1]...”这增加了答案的可追溯性和可信度。一个增强版的提示词示例enhanced_prompt_template f你是一个专业的文档分析助手。请严格根据以下提供的上下文信息来回答用户的问题。 ### 上下文信息 {context} ### 用户问题 {query} ### 你的任务 1. 仔细分析上下文找出与问题直接相关的内容。 2. 如果上下文信息足以回答问题请组织一个清晰、准确、简洁的答案。**如果可能请指出你的答案主要基于哪个相关段落例如参考[段落1]。** 3. 如果上下文信息完全不足以回答该问题请直接说“根据提供的资料我无法回答这个问题。” 现在请开始你的回答6.2 常见问题与解决方案速查表在实际操作中你可能会遇到以下典型问题。这里提供一个快速排查指南。问题现象可能原因解决方案运行python app.py时报错提示缺少模块依赖未正确安装。1. 确认已激活虚拟环境。2. 在项目目录下执行pip install -r requirements.txt。索引文档时程序卡住或报编码错误1. 文档格式特殊或损坏。2. 非UTF-8编码的文本文件。1. 尝试用其他软件打开文档确认其完整性。2. 对于.txt文件尝试指定编码如TextLoader(file_path, encodinggb18030)针对中文GBK编码。Ollama连接失败提示“无法连接到服务”Ollama服务未启动或未在默认端口运行。1. 打开一个独立的终端运行ollama run llama3.1并保持运行。2. 确认app.py中调用API的URL是http://localhost:11434。问答响应速度非常慢1. 模型在CPU上运行。2. 检索的top_k值过大导致提示词过长。1. 如果有NVIDIA GPU确保已安装对应版本的PyTorch和CUDAOllama会自动利用GPU加速。2. 尝试减小top_k例如从5减到3。3. 检查sentence-transformers模型是否在首次运行时已下载完成。模型回答的内容与文档无关“幻觉”1. 检索到的片段不相关。2. 提示词指令不够强硬。3. 模型temperature参数过高。1. 检查检索结果代码中的print输出看返回的片段是否真的与问题相关。若不相关可能需要优化文档分割或尝试不同的嵌入模型。2. 强化提示词使用“严格根据上下文”、“禁止编造”等措辞。3. 降低ask_llama函数中的temperature参数如设为0.1。答案总是“根据提供的资料我无法回答这个问题”1. 检索确实未找到任何相关信息。2. 提示词中关于“无法回答”的条件过于严格或模糊。1. 检查你的问题是否确实在文档范围内。尝试问一个文档中明确存在答案的简单问题来测试。2. 调整提示词将“无法回答”的条件描述得更具体例如改为“如果上下文中完全没有提及问题中的核心关键词则告知无法回答”。Gradio界面无法上传文件或点击无反应浏览器兼容性问题或Gradio版本问题。1. 尝试使用Chrome或Edge浏览器。2. 确保Gradio版本为较新稳定版回退版本有时能解决问题pip install gradio4.19.2。6.3 性能与扩展性考量当前的一小时原型侧重于功能和速度。如果你希望将其用于更严肃的场景需要考虑以下几点嵌入模型升级all-MiniLM-L6-v2是速度和效果的折衷。对于更高精度的要求可以考虑all-mpnet-base-v2它更大也更慢但生成的向量质量更高。对于中文text2vec系列模型如text2vec-base-chinese是专门优化的选择。向量数据库升级如果数据量超过数万条或者需要持久化、远程访问可以考虑迁移到Qdrant、Weaviate或Milvus等支持生产部署的向量数据库。引入查询转换对于复杂问题可以先让大模型对原始查询进行改写或分解再进行检索。例如将“苹果公司最新手机的摄像头有什么特点”分解为“苹果公司最新手机型号”和“该型号摄像头特点”两个子查询分别检索后再综合答案这能显著提升复杂问答的效果。添加对话历史当前的实现是单轮问答。要实现多轮对话需要将历史问答也纳入上下文管理。一种简单的方法是将之前几轮的“问题-答案”对也作为文本块存入向量库或者在构建提示词时附上最近的对话历史。