从零开始搭建本地RAG系统:LangChain+Qwen3+BGE-M3实战指南

本文详细介绍了如何使用LangChain框架整合Qwen3大模型与BGE-M3嵌入模型,搭建本地运行的端到端RAG系统。通过4位量化技术降低硬件门槛至8GB显存,解决大模型知识时效性差、个性化能力弱及"幻觉"问题,实现数据安全的私有化问答系统。文章提供了从环境准备到代码实现的完整步骤,适合新手学习落地。

前排提示,文末有大模型AGI-优快云独家资料包哦!

在大模型这个时期,“私有化部署”以及“个性化问答”成了企业和开发者的关键需求。要是想让AI根据特定的文档(像公司手册、技术文档、学术论文等)来回答问题,并且还担忧数据隐私会被泄漏出去,那检索增强生成(RAG就是最棒的解决办法。

本文将带大家从零开始,用LangChain框架整合Qwen3大模型与BGE-M3嵌入模型,手戳一个可本地运行的端到端RAG系统。无需复杂云服务,只需一台带GPU的电脑,就能拥有专属的“文档问答机器人”。

为什么选择RAG?聊聊大模型的“知识痛点”

01

在正式动手前,我们先搞懂一个核心问题:为什么需要RAG?

大模型像GPT-4和Qwen这类的呢,虽然可以应对很多很多的通用问题,可是它们有两个特别要命的缺点:

1.知识时效性不太好:训练数据截止到某个特定的时间(就像Qwen3训练数据截止到2024年初那样),没办法获取最新的信息;

2.个性化能力弱:无法理解企业内部文档、个人笔记等私有数据;

3.易产生“幻觉”:对不确定的问题会编造看似合理的答案,无法溯源信息来源。

而RAG通过“检索生成”的组合完美解决这些问题:

  • 检索:从本地文档库中精准找到与问题相关的片段;

  • 生成:让大模型基于检索到的“事实依据”回答问题,确保答案准确、可溯源

直接讲RAG就像是给大模型安上了一个本地的知识仓库,既能让大模型具备语言理解的能力,又能把数据隐私和知识专属的问题给解决了。

技术选型:为什么是LangChain+Qwen3+BGEM3?

02

搭建RAG系统得有三大关键组件,分别是文档处理框架、大语言模型(LLM以及嵌入模型(Embedding)。我们在选择这些组件时,主要从“本地化、对中文友好、资源消耗低”这个方面去考虑的:

组件类型选型核心优势
文档处理框架LangChain一站式整合文档加载、分割、向量存储、检索链,降低开发门槛
大语言模型(LLM)Qwen3-7B-Instruct阿里达摩院开源模型,中文处理能力强,7B 参数支持 4 位量化,本地 GPU 可运行
嵌入模型(Embedding)BAAI/bge-m3中科院自动化所开源,中文嵌入效果顶尖,支持检索优化指令,精度高于传统模型
向量数据库Chroma轻量级本地向量库,无需复杂部署,支持持久化存储,适配 LangChain

除此之外,我们还用到BitsAndBytes量化技术,将Qwen3-7B模型压缩到4位精度,原本需要24GB显存的模型,现在8GB显存就能运行,大大降低硬件门槛。

手把手搭建:从环境准备到代码实现

03

接着开始实战部分啦,咱们把RAG系统搭建分成4步来进行,每一步都有着详细的讲解呢,就算是新手也能够很顺畅地跟得上哟。

3.1 环境准备:安装依赖库

先确定你的电脑已安装了Python3.8以及更高版本,与此同时拥有NVIDIAGPU(显存最好大于或等于8GB)。接着打开终端,接着执行以下这些命令来安装依赖:

# 基础依赖pip install langchain chromadb transformers torch# 文档加载与处理pip install sentence-transformers python-dotenv# 量化相关(4位量化需要)pip install bitsandbytes accelerate# 可选:如果需要处理PDF/Word文档,安装额外加载器pip install pypdf python-docx

3.2 核心代码解析:从配置到问答的全流程

我们的代码分为5个核心模块:配置类、RAG系统初始化、文档处理、向量库加载、问答交互。下面逐模块拆解,理解每个环节的作用。

  1. 配置类:统一管理参数,便于修改

先对Config类进行定义,把文档路径、模型名称、量化配置等这些参数都集中到一起进行管理,这样之后要是需要修改的话,就不用到处去寻找代码啦:

classConfig:    # 文档相关:指定文档存放目录、分块大小    DOCUMENTS_DIR = "documents"# 本地文档目录(需手动创建)    CHUNK_SIZE = 500             # 每个文本块的字符数(中文适配)    CHUNK_OVERLAP = 50           # 块间重叠字符数(避免分割丢失上下文)        # 模型相关:指定嵌入模型和LLM    EMBEDDING_MODEL_NAME = "BAAI/bge-m3"# 中文嵌入效果顶尖    LLM_MODEL_NAME = "Qwen/Qwen3-7B-Instruct"# 中文友好的7B模型        # 检索相关:向量库存储路径、检索数量    VECTOR_DB_DIR = "vector_db_qwen_bge_m3"# 向量库持久化目录    TOP_K = 3                               # 每次检索返回3个相关片段        # 量化配置:降低显存占用    USE_4BIT_QUANTIZATION = True  # 启用4位量化(8GB显存必备)
  1. RAG系统初始化:加载嵌入模型与LLM

RAGSystem类是核心,负责初始化嵌入模型和LLM,这是系统的“大脑”和“眼睛”:

. 嵌入模型BGEM3):将文本转化为向量数字),用于后续检索

  • LLM(Qwen3):基于检索到的文本片段生成自然语言回答

(1)初始化嵌入模型:给文本“编数字”

BGEM3有一个关键优化:支持查询指令,能让查询向量更精准。例如在生成查询向量时,添加“为这个句子生成表示以用于检索相关文章:”前缀提升检索命中率:

def_init_embeddings(self):    print(f"加载BGE-M3嵌入模型: {self.config.EMBEDDING_MODEL_NAME}")    # BGE-M3专属查询指令,优化中文检索效果    query_instruction = "为这个句子生成表示以用于检索相关文章:"    return HuggingFaceBgeEmbeddings(        model_name=self.config.EMBEDDING_MODEL_NAME,        model_kwargs={'device': 'cuda'if torch.cuda.is_available() else'cpu'},        encode_kwargs={'normalize_embeddings': True}, # 向量归一化,提升检索精度        query_instruction=query_instruction    )

(2)初始化LLM:给系统“装大脑”

Qwen3-7B模型默认需要24GB显存,我们通过4位量化将其压缩到8GB以内。这个时候Qwen有专属的提示词格式(<|im_start|>/<|im_end|>),需要自定义格式函数适配:

def_init_llm(self):    print(f"加载Qwen3模型: {self.config.LLM_MODEL_NAME}")        # 4位量化配置:关键优化,降低显存占用    quantization_config = None    if self.config.USE_4BIT_QUANTIZATION and torch.cuda.is_available():        quantization_config = BitsAndBytesConfig(            load_in_4bit=True,            bnb_4bit_use_double_quant=True, # 双重量化,进一步压缩            bnb_4bit_quant_type="nf4", # 适配大模型的量化类型            bnb_4bit_compute_dtype=torch.float16        )        # 加载tokenizer和模型(device_map="auto"自动分配GPU/CPU)    tokenizer = AutoTokenizer.from_pretrained(self.config.LLM_MODEL_NAME)    model = AutoModelForCausalLM.from_pretrained(        self.config.LLM_MODEL_NAME,        quantization_config=quantization_config,        device_map="auto",        torch_dtype=torch.float16,        trust_remote_code=True# 加载Qwen的自定义代码    )        # Qwen专属提示词格式:必须严格遵循,否则模型无法正常响应    defqwen_prompt_format(prompt):        returnf"<|im_start|>system\n你是一个 helpful 的助手,基于提供的上下文回答问题。<|im_end|>\n<|im_start|>user\n{prompt}<|im_end|>\n<|im_start|>assistant\n"        # 创建文本生成pipeline,包装成LangChain的LLM    pipe = pipeline(        "text-generation",        model=model,        tokenizer=tokenizer,        max_new_tokens=500, # 最大生成500个字符        temperature=0.7, # 随机性:0=严谨,1=灵活        repetition_penalty=1.1, # 避免重复生成        pad_token_id=tokenizer.pad_token_id,        eos_token_id=tokenizer.eos_token_id,        prompt_format_template=qwen_prompt_format    )        return HuggingFacePipeline(pipeline=pipe)
  1. 文档处理:从“原始文档”到“向量库”

文档处理是RAG的“地基”,直接影响检索精度。这一步分为3个关键操作:加载文档→分割文档→创建向量库。

(1) 加载文档:支持多格式(TXT/PDF/Word)

我们用DirectoryLoader批量加载documents目录下的文档,默认支持TXT格式,若需处理PDF/Word,只需替换loader\_clsPyPDFLoader/Docx2txtLoader

defload_and_process_documents(self):    start_time = time.time()        # 加载文档:glob="*.txt"指定只加载TXT文件    loader = DirectoryLoader(        self.config.DOCUMENTS_DIR,        glob="*.txt",        loader_cls=TextLoader,        loader_kwargs={"encoding": "utf-8"} # 解决中文乱码问题    )    documents = loader.load()        ifnot documents:        raise ValueError(f"请在 {self.config.DOCUMENTS_DIR} 目录中添加文档")    print(f"成功加载 {len(documents)} 个文档")

(2)分割文档:中文适配的“黄金分割点”

大模型有上下文长度限制(如Qwen3-7B支持8k tokens),若直接将长文档传入,会丢失上下文。我们用RecursiveCharacterTextSplitter按中文标点分割,避免将完整句子切散:

# 分割文档:按“段落→句子→标点”分层分割,保留中文语义text_splitter = RecursiveCharacterTextSplitter(    chunk_size=self.config.CHUNK_SIZE,    chunk_overlap=self.config.CHUNK_OVERLAP,    separators=["\n\n", "\n", "。", ",", ";", "、", " ", ""] # 中文优先分割符)texts = text_splitter.split_documents(documents)print(f"文档分割完成,得到 {len(texts)} 个文本块")

(3)创建向量库:将文本块“存入数据库”

用Chroma向量库存储文本块的向量,后续检索时,只需将问题转化为向量,与库中的向量计算相似度,就能快速找到相关片段:

# 创建并持久化向量库:下次运行可直接加载,无需重新处理self.vector_db = Chroma.from_documents(    documents=texts,    embedding=self.embeddings,    persist_directory=self.config.VECTOR_DB_DIR)self.vector_db.persist()# 创建检索链:将“检索”与“生成”串联self.qa_chain = RetrievalQA.from_chain_type(    llm=self.llm,    chain_type="stuff", # 简单高效:将所有相关片段传入LLM    retriever=self.vector_db.as_retriever(search_kwargs={"k": self.config.TOP_K}),    return_source_documents=True# 返回源文档,便于溯源)end_time = time.time()print(f"文档处理完成,耗时 {end_time - start_time:.2f} 秒")
  1. 问答交互:从“问题”到“答案”的闭环

最后,实现query方法,接收用户问题,调用检索链生成答案,并返回源文档片段(便于验证答案准确性):

defquery(self, question: str):    ifnot self.qa_chain:        raise ValueError("请先加载文档或向量库")        start_time = time.time()    print(f"处理查询: {question}")        # 执行检索增强生成:先检索相关片段,再生成答案    result = self.qa_chain({"query": question})        end_time = time.time()    print(f"查询处理完成,耗时 {end_time - start_time:.2f} 秒")        return result
  1. 主函数:一键运行系统

if name == "main":中,我们实现“自动创建示例文档→初始化系统→加载向量库→测试问答”的全流程:

if __name__ == "__main__":    os.environ["TOKENIZERS_PARALLELISM"] = "false"# 避免tokenizer并行警告        # 初始化配置与系统    config = Config()    rag_system = RAGSystem(config)        # 首次运行:创建示例文档(数据科学、LLM应用相关)    ifnot os.listdir(config.DOCUMENTS_DIR):        print("创建示例文档...")        create_sample_documents(config.DOCUMENTS_DIR)        # 加载向量库:存在则直接加载,不存在则重新处理文档    ifnot rag_system.load_existing_vector_db():        rag_system.load_and_process_documents()        # 测试中文问答    test_questions = [        "数据科学工作流包括哪些步骤?",        "大语言模型有哪些主要应用场景?"    ]        # 输出结果:包含答案和源文档片段    for question in test_questions:        print("\n" + "="*80)        print(f"问题: {question}")        result = rag_system.query(question)                print("\n回答:")        print(result["result"])                print("\n相关文档片段:")        for i, doc in enumerate(result["source_documents"], 1):            print(f"\n文档 {i}:")            print(f"来源: {doc.metadata['source']}")            print(f"内容: {doc.page_content[:200]}...")

运行测试:看看系统实际效果

04

将代码保存为rag\_qwe\_bge.py,执行以下命令运行:

python rag_qwen_bge.py

首次运行时,系统会自动:

  1. documents目录创建2个示例文档(数据科学、LLM应用);
  2. 下载BGE-M3和Qwen3-7B模型(约10GB,需耐心等待);
  3. 处理文档并创建向量库,存入vector*db*qwen*bge*m3目录。

运行成功后,会输出如下结果(以“数据科学工作流包括哪些步骤?”为例):

================================

问题: 数据科学工作流包括哪些步骤?处理查询: 数据科学工作流包括哪些步骤?回答:数据科学工作流通常包括以下9个步骤:问题定义:明确业务问题和分析目标,这是整个工作流的基础,确保后续工作围绕核心需求展开;数据收集:获取相关的结构化(如数据库表)和非结构化(如文本、图片)数据;数据清洗:处理数据中的缺失值、异常值和重复数据,保证数据质量,避免影响后续分析结果;探索性数据分析:通过统计方法和可视化工具,理解数据分布、变量关系等,挖掘数据潜在规律;特征工程:对原始数据进行转换、筛选或创建新特征,生成适合机器学习模型输入的特征;模型构建:根据问题类型(如分类、回归)选择适当的机器学习模型,并使用清洗后的数据集进行训练;模型评估:使用准确率、召回率、RMSE等指标评估模型性能,判断模型是否满足业务需求;结果部署:将训练好的模型集成到业务流程中(如API接口、业务系统),实现实际应用;监控与更新:持续监控模型在实际场景中的性能,当数据分布或业务需求变化时,定期更新模型。相关文档片段:文档 1:来源: documents/data_science.txt内容: 数据科学是一个跨学科领域,结合了统计学、机器学习、数据可视化和领域知识,用于从结构化和非结构化数据中提取有价值的 insights。数据科学工作流通常包括以下步骤:问题定义:明确业务问题和分析目标数据收集:获取相关的结构化和非结构化数据数据清洗:处理缺失值、异常值和重复数据探索性数据分析:理解数据分布和变量关系特征工程:创建适合建模的特征模型构建:选择和训练适当的机器学习模型模型评估:使用合适的指标评估模型性能结果部署:将模型集成到业务流程中监控与更新:持续监控模型性能并定期更新

总结:展望,一点建议

05

  1. 本文针对企业私有化、个性化问答需求,提供了基于LangChain+Qwen3+BGEM3的本地RAG系统搭建全流程,通过4位量化技术降低硬件门槛至8GB显存,新手也能落地。
  2. 这个方案通过“检索生成”这种方式,把大模型存在的知识会过时、容易产生“幻觉”以及没法适应私有数据这些难题给解决了,既保障了数据的安全,又保证了回答的准确性。
  3. 以后本地RAG会朝着更轻、更多种模式的方向去发展呢,给新手的建议是先从最基本的文本开始,把流程熟悉起来,等过了这一步之后呢,就可以去摸索参数的优化以及多种技术融合这样更高级的东西啦。

读者福利:倘若大家对大模型感兴趣,那么这套大模型学习资料一定对你有用。

针对0基础小白:

如果你是零基础小白,快速入门大模型是可行的。
大模型学习流程较短,学习内容全面,需要理论与实践结合
学习计划和方向能根据资料进行归纳总结

包括:大模型学习线路汇总、学习阶段,大模型实战案例,大模型学习视频,人工智能、机器学习、大模型书籍PDF。带你从零基础系统性的学好大模型!

😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

请添加图片描述

👉AI大模型学习路线汇总👈

大模型学习路线图,整体分为7个大的阶段:(全套教程文末领取哈)

第一阶段: 从大模型系统设计入手,讲解大模型的主要方法;

第二阶段: 在通过大模型提示词工程从Prompts角度入手更好发挥模型的作用;

第三阶段: 大模型平台应用开发借助阿里云PAI平台构建电商领域虚拟试衣系统;

第四阶段: 大模型知识库应用开发以LangChain框架为例,构建物流行业咨询智能问答系统;

第五阶段: 大模型微调开发借助以大健康、新零售、新媒体领域构建适合当前领域大模型;

第六阶段: 以SD多模态大模型为主,搭建了文生图小程序案例;

第七阶段: 以大模型平台应用与开发为主,通过星火大模型,文心大模型等成熟大模型构建大模型行业应用。

👉大模型实战案例👈

光学理论是没用的,要学会跟着一起做,要动手实操,才能将自己的所学运用到实际当中去,这时候可以搞点实战案例来学习。

在这里插入图片描述

👉大模型视频和PDF合集👈

这里我们能提供零基础学习书籍和视频。作为最快捷也是最有效的方式之一,跟着老师的思路,由浅入深,从理论到实操,其实大模型并不难

在这里插入图片描述

👉学会后的收获:👈

• 基于大模型全栈工程实现(前端、后端、产品经理、设计、数据分析等),通过这门课可获得不同能力;

• 能够利用大模型解决相关实际项目需求: 大数据时代,越来越多的企业和机构需要处理海量数据,利用大模型技术可以更好地处理这些数据,提高数据分析和决策的准确性。因此,掌握大模型应用开发技能,可以让程序员更好地应对实际项目需求;

• 基于大模型和企业数据AI应用开发,实现大模型理论、掌握GPU算力、硬件、LangChain开发框架和项目实战技能, 学会Fine-tuning垂直训练大模型(数据准备、数据蒸馏、大模型部署)一站式掌握;

• 能够完成时下热门大模型垂直领域模型训练能力,提高程序员的编码能力: 大模型应用开发需要掌握机器学习算法、深度学习框架等技术,这些技术的掌握可以提高程序员的编码能力和分析能力,让程序员更加熟练地编写高质量的代码。

👉获取方式:

😝有需要的小伙伴,可以保存图片到wx扫描二v码免费领取【保证100%免费】🆓

# ======================================================= # 业务领域多模态RAG智能问答系统 (Business-RAG-MultiModal) # v2.1 - 最终稳定版 # ======================================================= # --- 核心依赖库导入 --- import os import hashlib import json import logging import base64 import pathlib import re import requests # 用于直接调用Ollama API import time # 新增:用于重试机制 from typing import List, Dict # --- LlamaIndex 核心导入 --- from llama_index.core import Settings from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, PromptTemplate, Document, StorageContext from llama_index.core.readers.base import BaseReader as LlamaBaseReader from llama_index.core.node_parser import SentenceSplitter from llama_index.core.schema import TextNode from llama_index.llms.ollama import Ollama from llama_index.core.postprocessor import SentenceTransformerRerank from llama_index.embeddings.huggingface import HuggingFaceEmbedding # --- Milvus 相关导入 --- from llama_index.vector_stores.milvus import MilvusVectorStore from pymilvus import utility, connections, Collection # --- 配置日志 --- logging.basicConfig(level=logging.INFO, format=&#39;%(asctime)s - %(levelname)s - %(message)s&#39;) logger = logging.getLogger(__name__) # ======================================================= # 1. 全局配置区 # ======================================================= CONFIG = { "knowledge_base_dir": "knowledge_base", "image_cache_file": "image_description_cache_offline.json", "embed_model_path": "D:/models/text2vec-base-chinese", "reranker_model_path": "D:/models/bge-reranker-v2-m3", "llm_model_name": "qwen3:8b", "mllm_model_name": "llava", "llm_request_timeout": 600.0, "chunk_size": 512, "chunk_overlap": 50, "retrieval_top_k": 10, "rerank_top_n": 3, "device": "cpu", # --- Milvus配置 --- "milvus_host": "127.0.0.1", # 确保与Docker容器端口一致 "milvus_port": "19530", # 默认端口 "milvus_collection": "law_rag_collection_v1", "vector_dim": 768 } # ======================================================= # 2. 核心功能函数区 # ======================================================= def load_image_cache(): if os.path.exists(CONFIG["image_cache_file"]): with open(CONFIG["image_cache_file"], &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: try: return json.load(f) except json.JSONDecodeError: return {} return {} def save_image_cache(cache): with open(CONFIG["image_cache_file"], &#39;w&#39;, encoding=&#39;utf-8&#39;) as f: json.dump(cache, f, ensure_ascii=False, indent=4) def get_image_description_from_mllm(image_bytes: bytes, local_mllm: Ollama, question="请详细描述这张图片的内容,如果图片中有文字也请一并识别出来。") -> str: """ 【修正版】直接调用Ollama API来获取图片描述,并使用正确的sha256。 """ image_cache = load_image_cache() image_hash = hashlib.sha256(image_bytes).hexdigest() if image_hash in image_cache: logger.info(f" - 发现已缓存的离线图片描述 (hash: {image_hash[:8]}...),从缓存加载。") return image_cache[image_hash] logger.info(f" - 未找到图片缓存,正在直接调用Ollama API (模型: {local_mllm.model})...") try: image_b64 = base64.b64encode(image_bytes).decode("utf-8") payload = { "model": local_mllm.model, "prompt": question, "images": [image_b64], "stream": False } response = requests.post( "http://localhost:11434/api/generate", json=payload, timeout=CONFIG["llm_request_timeout"] ) response.raise_for_status() response_data = response.json() description = response_data.get(&#39;response&#39;, &#39;[模型未返回有效描述]&#39;).strip() formatted_description = f"[图片描述]: {description}\n" image_cache[image_hash] = formatted_description save_image_cache(image_cache) return formatted_description except requests.exceptions.RequestException as e: logger.error(f"直接调用Ollama API处理图片时发生网络异常: {e}") return "[网络异常导致图片处理失败]\n" except Exception as e: logger.error(f"处理图片时发生未知异常: {e}") return "[未知异常导致图片处理失败]\n" class CustomMultimodalReader(LlamaBaseReader): def __init__(self, mllm_instance: Ollama): super().__init__() self.mllm = mllm_instance def load_data(self, file_path_obj: pathlib.Path, extra_info: Dict = None) -> List[Document]: file_path_str = str(file_path_obj) if file_path_str.endswith(".pdf"): return self._load_pdf(file_path_str, extra_info) elif file_path_str.endswith(".docx"): return self._load_docx(file_path_str, extra_info) else: # 为 .txt 文件添加一个基本的加载器 try: with open(file_path_str, &#39;r&#39;, encoding=&#39;utf-8&#39;) as f: text = f.read() return [Document(text=text, extra_info={**(extra_info or {}), "file_name": os.path.basename(file_path_str)})] except Exception as e: logger.error(f"处理TXT文件 &#39;{file_path_str}&#39; 时发生错误: {e}") return [] def _load_pdf(self, file_path: str, extra_info: Dict = None) -> List[Document]: documents = [] try: import pypdf with open(file_path, "rb") as fp: reader = pypdf.PdfReader(fp) if reader.is_encrypted: logger.warning(f"文件 {os.path.basename(file_path)} 是加密PDF,已跳过。") return [] for i, page in enumerate(reader.pages): page_info = {**(extra_info or {}), "page_label": str(i + 1), "file_name": os.path.basename(file_path)} if page_text := page.extract_text(): documents.append(Document(text=page_text.strip(), extra_info=page_info.copy())) for img_file_obj in page.images: if img_bytes := img_file_obj.data: image_description = get_image_description_from_mllm(img_bytes, self.mllm) documents.append(Document(text=image_description, extra_info={**page_info.copy(), "content_type": "image_description"})) except Exception as e: logger.error(f"处理PDF文件 &#39;{file_path}&#39; 时发生错误: {e}") return documents def _load_docx(self, file_path: str, extra_info: Dict = None) -> List[Document]: documents = [] try: import docx doc = docx.Document(file_path) file_info = {**(extra_info or {}), "file_name": os.path.basename(file_path)} for para in doc.paragraphs: if para.text.strip(): documents.append(Document(text=para.text.strip(), extra_info=file_info.copy())) for table in doc.tables: table_text = "\n".join([" | ".join([cell.text for cell in row.cells]) for row in table.rows]).strip() if table_text: documents.append(Document(text=f"[表格内容]:\n{table_text}", extra_info={**file_info.copy(), "content_type": "table_content"})) for rel in doc.part.rels.values(): if "image" in rel.target_ref: if image_bytes := rel.target_part.blob: image_description = get_image_description_from_mllm(image_bytes, self.mllm) documents.append(Document(text=image_description, extra_info={**file_info.copy(), "content_type": "image_description"})) except Exception as e: logger.error(f"处理DOCX文件 &#39;{file_path}&#39; 时发生错误: {e}") return documents # --- RAG核心流程函数 --- def setup_models_and_services(): """集中加载所有本地AI模型,并进行全局配置。""" logger.info("--- 步骤A: 加载所有本地AI模型 ---") embed_model = HuggingFaceEmbedding( model_name=CONFIG["embed_model_path"], device=CONFIG["device"] ) logger.info(f"成功加载本地嵌入模型: {CONFIG[&#39;embed_model_path&#39;]}") llm_model = Ollama(model=CONFIG["llm_model_name"], request_timeout=CONFIG["llm_request_timeout"]) logger.info(f"成功配置本地Ollama LLM (模型: {CONFIG[&#39;llm_model_name&#39;]})") mllm_for_parsing = Ollama(model=CONFIG["mllm_model_name"], request_timeout=300.0) logger.info(f"成功配置本地Ollama MLLM (模型: {CONFIG[&#39;mllm_model_name&#39;]})") reranker = SentenceTransformerRerank( model=CONFIG["reranker_model_path"], top_n=CONFIG["rerank_top_n"], device=CONFIG["device"] ) logger.info(f"成功加载本地重排模型: {CONFIG[&#39;reranker_model_path&#39;]}") # 将加载好的模型设置到全局 Settings 中,确保所有组件统一使用 Settings.embed_model = embed_model Settings.llm = llm_model logger.info("--- 已将embed_model和llm配置为全局默认 ---") logger.info("--- 所有AI模型加载完成 ---") return llm_model, embed_model, reranker, mllm_for_parsing def build_knowledge_index(mllm_for_parsing: Ollama, embed_model: HuggingFaceEmbedding): """ 【最终修正版】构建向量索引并将其持久化到Milvus数据库。 包含手动嵌入生成以绕过库的潜在bug。 """ logger.info("--- 步骤B: 连接Milvus并构建/加载知识库向量索引 ---") # ===== 新增:Milvus连接重试机制 ===== max_retries = 5 retry_delay = 5 # 秒 connected = False for attempt in range(max_retries): try: connections.connect(alias="default", host=CONFIG["milvus_host"], port=CONFIG["milvus_port"]) logger.info(f"成功连接到 Milvus 服务 at {CONFIG[&#39;milvus_host&#39;]}:{CONFIG[&#39;milvus_port&#39;]}") connected = True break except Exception as e: logger.warning(f"连接Milvus失败(尝试 {attempt+1}/{max_retries}): {e}") if attempt < max_retries - 1: logger.info(f"{retry_delay}秒后重试...") time.sleep(retry_delay) else: logger.error(f"无法连接到 Milvus 服务,已达到最大重试次数") raise ConnectionError(f"无法连接Milvus: {str(e)}") if not connected: raise RuntimeError("Milvus连接失败") vector_store = MilvusVectorStore( uri=f"http://{CONFIG[&#39;milvus_host&#39;]}:{CONFIG[&#39;milvus_port&#39;]}", collection_name=CONFIG["milvus_collection"], dim=CONFIG["vector_dim"], overwrite=False ) collection_exists_and_has_content = False if utility.has_collection(CONFIG["milvus_collection"]): collection = Collection(name=CONFIG["milvus_collection"]) collection.load() if collection.num_entities > 0: collection_exists_and_has_content = True if collection_exists_and_has_content: logger.info(f"在Milvus中已找到包含实体的集合,直接加载索引...") index = VectorStoreIndex.from_vector_store(vector_store) logger.info("从Milvus加载索引完成。") else: if utility.has_collection(CONFIG["milvus_collection"]): logger.info("在Milvus中找到空集合,开始处理并填充数据...") else: logger.info(f"在Milvus中未找到集合,开始完整的数据处理和索引构建流程...") # 步骤 1: 数据加载和切分 (此部分不变) reader = SimpleDirectoryReader( input_dir=CONFIG["knowledge_base_dir"], required_exts=[".pdf", ".docx", ".txt"], file_extractor={".pdf": CustomMultimodalReader(mllm_instance=mllm_for_parsing), ".docx": CustomMultimodalReader(mllm_instance=mllm_for_parsing)}, recursive=True ) documents = reader.load_data(show_progress=True) all_nodes = [] sentence_splitter = SentenceSplitter(chunk_size=CONFIG["chunk_size"], chunk_overlap=CONFIG["chunk_overlap"]) for doc in documents: filename = doc.metadata.get("file_name", "").lower() if doc.metadata.get("content_type") == "image_description": all_nodes.append(doc); continue if filename.endswith(".pdf"): article_pattern = r&#39;(第[一二三四五六七八九十百千万零〇\d]+条)&#39;; text_chunks = re.split(article_pattern, doc.text); i = 1 while i < len(text_chunks): article_title = text_chunks[i]; article_content = text_chunks[i+1] if (i + 1) < len(text_chunks) else "" full_article_text = (article_title + article_content).strip() if full_article_text: node = Document(text=full_article_text, extra_info=doc.metadata.copy()); all_nodes.append(node) i += 2 else: nodes = sentence_splitter.get_nodes_from_documents([doc]); all_nodes.extend(nodes) logger.info(f"文档条件化切分完毕,共生成 {len(all_nodes)} 个内容块 (Nodes)。") # --- 【核心修正】 --- # 步骤 2: 手动、显式地为所有节点生成向量嵌入 logger.info(f"正在为 {len(all_nodes)} 个节点手动生成向量嵌入...") for node in all_nodes: # node.get_content() 是获取节点文本最稳健的方法 node.embedding = embed_model.get_text_embedding(node.get_content()) logger.info("所有节点的向量嵌入已手动生成。") # --- 【核心修正结束】 --- # 步骤 3: 将已经带有向量的节点添加到Milvus logger.info(f"正在将 {len(all_nodes)} 个带有预生成向量的节点添加到Milvus...") vector_store.add(all_nodes) logger.info("节点已成功添加到Milvus。") # 步骤 4: 从已填充的向量存储创建索引对象 index = VectorStoreIndex.from_vector_store(vector_store) logger.info("索引对象创建完成。") connections.disconnect("default") logger.info("已断开与 Milvus 服务的连接。") return index def run_query_pipeline(index: VectorStoreIndex, llm_model: Ollama, reranker: SentenceTransformerRerank): """启动问答流程,循环处理预设的问题。""" logger.info("--- 步骤C: 开始RAG问答流程 ---") QA_PROMPT_TEMPLATE = PromptTemplate( "你是一个专业的业务问答助手,负责根据内部知识库提供精准、可靠的回答。\n\n" "**你的任务是:**\n" "1. 仔细阅读下面提供的“参考信息”。\n" "2. 根据“参考信息”直接回答“用户问题”,禁止进行任何形式的猜测、推理或使用你自己的知识。\n" "3. **引用来源**:在回答中,如果引用了某份文件的内容,必须在相关句子末尾用 `(文件名)` 的格式注明来源。\n" "4. **版本对比**:如果参考信息来自不同版本的文件(例如,文件名中包含年份),请对比说明它们之间的差异。\n" "5. **提供建议**:在回答的最后,根据回答内容,提供1-2条具体、可执行的业务建议。\n" "6. **未知问题**:如果“参考信息”中完全没有能回答问题的内容,你必须且只能回答:“根据提供的资料,无法回答该问题。”\n" "7. **格式要求**:回答的最后,必须附上一个“参考依据”列表,列出所有被引用的文件名。\n\n" "---------------------\n" "**参考信息:**\n{context_str}\n" "---------------------\n" "**用户问题:** {query_str}\n\n" "**你的回答:**\n" ) query_engine = index.as_query_engine( similarity_top_k=CONFIG["retrieval_top_k"], node_postprocessors=[reranker], text_qa_template=QA_PROMPT_TEMPLATE # llm会从全局Settings获取 ) questions = [ "根据附图1的技术架构图,究竟是哪个芯片独立负责生成刷新信号?", "数据出境安全评估申报流程图里,如果个人信息达到10万人规模该怎么办?", "我国公民的基本权力以及义务有哪些", "借钱不还怎么办?" ] for q in questions: logger.info(f"\n{&#39;=&#39;*70}\n--- 用户提问: {q} ---") try: response = query_engine.query(q) logger.info("\n--- 模型最终回答 ---\n" + str(response)) logger.info("\n--- 回答引用的参考信息 (经重排后) ---") for i, node_with_score in enumerate(response.source_nodes): logger.info(f"--- 来源 {i+1} (得分: {node_with_score.score:.4f}, 文件: {node_with_score.metadata.get(&#39;file_name&#39;, &#39;N/A&#39;)}) ---") node = node_with_score.node if hasattr(node, &#39;text&#39;) and node.text: logger.info(f"内容预览: {node.text[:150]}...\n") else: logger.info(f"内容预览: [这是一个非文本节点,类型为: {type(node).__name__}]\n") except Exception as e: logger.error(f"执行RAG查询时发生错误: {e}", exc_info=True) logger.info(f"{&#39;=&#39;*70}") # ======================================================= # 3. 主执行入口 # ======================================================= if __name__ == "__main__": logger.info("===== 启动业务领域多模态RAG智能问答系统 =====") try: llm, embed_model, reranker, mllm = setup_models_and_services() knowledge_index = build_knowledge_index(mllm, embed_model) run_query_pipeline(knowledge_index, llm, reranker) except Exception as e: logger.error(f"程序主流程发生致命错误,即将退出: {e}", exc_info=True) exit(1) logger.info("\n===== RAG系统执行完成 =====") 这是我的代码,(rag_project_env) PS D:\new_rag> & D:/miniconda/envs/rag_project_env/python.exe d:/new_rag/law_rag.py 2025-07-24 11:26:07,150 - INFO - ===== 启动业务领域多模态RAG智能问答系统 ===== 2025-07-24 11:26:07,150 - INFO - --- 步骤A: 加载所有本地AI模型 --- 2025-07-24 11:26:07,153 - INFO - Load pretrained SentenceTransformer: D:/models/text2vec-base-chinese 2025-07-24 11:26:08,791 - INFO - 成功加载本地嵌入模型: D:/models/text2vec-base-chinese 2025-07-24 11:26:08,791 - INFO - 成功配置本地Ollama LLM (模型: qwen3:8b) 2025-07-24 11:26:08,791 - INFO - 成功配置本地Ollama MLLM (模型: llava) 2025-07-24 11:26:09,586 - INFO - 成功加载本地重排模型: D:/models/bge-reranker-v2-m3 2025-07-24 11:26:09,586 - INFO - --- 已将embed_model和llm配置为全局默认 --- 2025-07-24 11:26:09,586 - INFO - --- 所有AI模型加载完成 --- 2025-07-24 11:26:09,586 - INFO - --- 步骤B: 连接Milvus并构建/加载知识库向量索引 --- 2025-07-24 11:26:19,712 - WARNING - 连接Milvus失败(尝试 1/5): <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> 2025-07-24 11:26:19,712 - INFO - 5秒后重试... 2025-07-24 11:26:34,849 - WARNING - 连接Milvus失败(尝试 2/5): <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> 2025-07-24 11:26:34,849 - INFO - 5秒后重试... 2025-07-24 11:26:49,974 - WARNING - 连接Milvus失败(尝试 3/5): <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> 2025-07-24 11:26:49,978 - INFO - 5秒后重试... 2025-07-24 11:27:05,102 - WARNING - 连接Milvus失败(尝试 4/5): <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> 2025-07-24 11:27:05,102 - INFO - 5秒后重试... 2025-07-24 11:27:20,192 - WARNING - 连接Milvus失败(尝试 5/5): <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> 2025-07-24 11:27:20,192 - ERROR - 无法连接到 Milvus 服务,已达到最大重试次数 2025-07-24 11:27:20,192 - ERROR - 程序主流程发生致命错误,即将退出: 无法连接Milvus: <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> Traceback (most recent call last): File "d:\new_rag\law_rag.py", line 230, in build_knowledge_index connections.connect(alias="default", host=CONFIG["milvus_host"], port=CONFIG["milvus_port"]) File "D:\miniconda\envs\rag_project_env\lib\site-packages\pymilvus\orm\connections.py", line 459, in connect connect_milvus(**kwargs, user=user, password=password, token=token, db_name=db_name) File "D:\miniconda\envs\rag_project_env\lib\site-packages\pymilvus\orm\connections.py", line 420, in connect_milvus raise e from e File "D:\miniconda\envs\rag_project_env\lib\site-packages\pymilvus\orm\connections.py", line 412, in connect_milvus gh._wait_for_channel_ready(timeout=timeout) File "D:\miniconda\envs\rag_project_env\lib\site-packages\pymilvus\client\grpc_handler.py", line 159, in _wait_for_channel_ready raise MilvusException( pymilvus.exceptions.MilvusException: <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)> During handling of the above exception, another exception occurred: Traceback (most recent call last): File "d:\new_rag\law_rag.py", line 374, in <module> knowledge_index = build_knowledge_index(mllm, embed_model) File "d:\new_rag\law_rag.py", line 241, in build_knowledge_index raise ConnectionError(f"无法连接Milvus: {str(e)}") ConnectionError: 无法连接Milvus: <MilvusException: (code=2, message=Fail connecting to server on 127.0.0.1:19530, illegal connection params or server unavailable)>这是我的报错,services: etcd: container_name: milvus-etcd image: quay.io/coreos/etcd:v3.5.5 environment: - ETCD_AUTO_COMPACTION_MODE=revision - ETCD_AUTO_COMPACTION_RETENTION=1000 - ETCD_QUOTA_BACKEND_BYTES=4294967296 - ETCD_SNAPSHOT_COUNT=50000 volumes: - ./volumes/etcd:/etcd command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd minio: container_name: milvus-minio image: quay.io/minio/minio:RELEASE.2023-03-20T20-16-18Z environment: - MINIO_ROOT_USER=minioadmin - MINIO_ROOT_PASSWORD=minioadmin volumes: - ./volumes/minio:/minio_data command: minio server /minio_data standalone: container_name: milvus-standalone image: milvusdb/milvus:v2.5.0 # <-- 修正点在这里 command: ["milvus", "run", "standalone"] environment: - ETCD_ENDPOINTS=etcd:2379 - MINIO_ADDRESS=minio:9000 volumes: - ./volumes/milvus:/var/lib/milvus ports: - "19530:19530" # Milvus port - "9091:9091" # Milvus metrics port depends_on: - "etcd" - "minio" volumes: etcd: minio: milvus:这是我的docker-compose.yml,请帮我解决我的报错,或者更改我的代码让我避开这个问题
07-25
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值