├── model └── placeholder ├── .gitignore ├── data ├── images │ ├── Crag.webp │ ├── HyDE.webp │ ├── RSE.webp │ ├── Fusion.webp │ ├── Reranker.webp │ ├── Self RAG.webp │ ├── Multi Model.webp │ ├── Adaptive RAG.webp │ ├── Feedback Loop.webp │ ├── Knowledge Graph.webp │ ├── Document Augmentation.webp │ ├── Hierarchical Indices.webp │ ├── Query Transformation.webp │ ├── 1_7b5LkCrQ7XhUN9Ktu3Q1sQ.webp │ ├── 1_RtkAa3uP2XlaRs8uUAduwA.webp │ ├── 1_VL2fP0HuSoqc66GC1vbA7w.webp │ ├── Contextual Chunk Headers.webp │ └── Contextual Compression.webp ├── AI_Information.en.zh-CN.pdf ├── visa_rejection_insurance_2016_011.pdf └── val.json ├── src ├── README.md ├── full │ ├── AI_Information.en.zh-CN.pdf_detailed_store.pkl │ ├── AI_Information.en.zh-CN.pdf_summary_store.pkl │ ├── feedback_data.json │ └── 01_simple_rag.ipynb ├── core │ ├── 03_chunk_size_selector_core.py │ ├── 05_contextual_chunk_headers_rag_core.py │ ├── 06_doc_augmentation_rag_core.py │ ├── 04_context_enriched_rag_core.py │ ├── 02_semantic_chunking_core.py │ ├── 19_HyDE_rag_core.py │ ├── 16_fusion_rag_core.py │ ├── 14_propostion_chunking_core.py │ ├── 08_reranker_core.py │ ├── 17_graph_rag_core.py │ ├── 13_self_rag_core.py │ ├── 10_contextual_compression_core.py │ ├── 09_rse_core.py │ ├── 07_query_transform_core.py │ ├── 20_crag_core.py │ ├── 18_hierarchy_rag_core.py │ └── 11_feedback_loop_rag_core.py └── full_en │ └── 1_simple_rag_en.ipynb ├── .env.example ├── LICENSE └── README.md /model/placeholder: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .ipynb_checkpoints 3 | test1.py 4 | 21_rag_* -------------------------------------------------------------------------------- /data/images/Crag.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Crag.webp -------------------------------------------------------------------------------- /data/images/HyDE.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/HyDE.webp -------------------------------------------------------------------------------- /data/images/RSE.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/RSE.webp -------------------------------------------------------------------------------- /data/images/Fusion.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Fusion.webp -------------------------------------------------------------------------------- /data/images/Reranker.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Reranker.webp -------------------------------------------------------------------------------- /data/images/Self RAG.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Self RAG.webp -------------------------------------------------------------------------------- /data/images/Multi Model.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Multi Model.webp -------------------------------------------------------------------------------- /data/images/Adaptive RAG.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Adaptive RAG.webp -------------------------------------------------------------------------------- /data/images/Feedback Loop.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Feedback Loop.webp -------------------------------------------------------------------------------- /data/AI_Information.en.zh-CN.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/AI_Information.en.zh-CN.pdf -------------------------------------------------------------------------------- /data/images/Knowledge Graph.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Knowledge Graph.webp -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | 2 | # 目录 3 | 4 | - **core**: 各个RAG 核心函数 5 | - **full**: RAG实现过程,完整代码,包含注释与运行结果 6 | - **full_en**: RAG实现过程,英文版本 7 | -------------------------------------------------------------------------------- /data/images/Document Augmentation.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Document Augmentation.webp -------------------------------------------------------------------------------- /data/images/Hierarchical Indices.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Hierarchical Indices.webp -------------------------------------------------------------------------------- /data/images/Query Transformation.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Query Transformation.webp -------------------------------------------------------------------------------- /data/images/1_7b5LkCrQ7XhUN9Ktu3Q1sQ.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/1_7b5LkCrQ7XhUN9Ktu3Q1sQ.webp -------------------------------------------------------------------------------- /data/images/1_RtkAa3uP2XlaRs8uUAduwA.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/1_RtkAa3uP2XlaRs8uUAduwA.webp -------------------------------------------------------------------------------- /data/images/1_VL2fP0HuSoqc66GC1vbA7w.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/1_VL2fP0HuSoqc66GC1vbA7w.webp -------------------------------------------------------------------------------- /data/images/Contextual Chunk Headers.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Contextual Chunk Headers.webp -------------------------------------------------------------------------------- /data/images/Contextual Compression.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/images/Contextual Compression.webp -------------------------------------------------------------------------------- /data/visa_rejection_insurance_2016_011.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/data/visa_rejection_insurance_2016_011.pdf -------------------------------------------------------------------------------- /src/full/AI_Information.en.zh-CN.pdf_detailed_store.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/src/full/AI_Information.en.zh-CN.pdf_detailed_store.pkl -------------------------------------------------------------------------------- /src/full/AI_Information.en.zh-CN.pdf_summary_store.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liu673/rag-all-techniques/HEAD/src/full/AI_Information.en.zh-CN.pdf_summary_store.pkl -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | LLM_BASE_URL="https://ark.cn-beijing.volces.com/api/v3" 2 | LLM_API_KEY="your api key" 3 | LLM_MODEL_ID="select model id" 4 | EMBEDDING_MODEL_ID="select embedding model id" -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jensen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/full/feedback_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "query": "什么是神经网络以及它如何工作?", 3 | "response": "根据上下文内容,神经网络(尤其是**人工神经网络**)是受生物大脑结构和功能启发而设计的计算模型,属于深度学习的核心组成部分。以下是其定义和工作原理的详细说明:\n\n---\n\n### **1. 神经网络的定义**\n- **基本概念**:神经网络是由多层互连的“神经元”(节点)组成的系统,用于分析和学习数据中的复杂模式。它通过模拟人脑神经元之间的信号传递来处理信息。\n- **核心特点**:\n - **分层结构**:通常包含输入层、隐藏层(可能有多层)和输出层。\n - **自适应学习**:通过调整神经元之间的连接权重(参数)来优化模型性能。\n - **非线性处理**:激活函数(如ReLU、Sigmoid)引入非线性,使网络能拟合复杂数据。\n\n---\n\n### **2. 神经网络的工作原理**\n#### **(1)数据输入与前向传播**\n- **输入层**:接收原始数据(如图像像素、文本向量)。\n- **隐藏层**:每层神经元对输入进行加权求和,并通过激活函数生成输出。 \n - **示例**:在图像识别中,底层可能检测边缘,高层组合边缘形成物体轮廓。\n- **输出层**:生成最终预测(如分类标签、数值)。\n\n#### **(2)损失计算与反向传播**\n- **损失函数**:衡量预测值与真实值的误差(如交叉熵、均方误差)。\n- **反向传播**: \n - 从输出层反向计算误差对每个权重的梯度。 \n - 使用优化算法(如梯度下降)调整权重,逐步最小化误差。\n\n#### **(3)训练与优化**\n- **迭代学习**:重复前向传播和反向传播,直到模型收敛。\n- **正则化技术**:防止过拟合(如Dropout、L2正则化)。\n\n---\n\n### **3. 神经网络的类型(基于上下文)**\n- **卷积神经网络(CNN)**: \n - **用途**:处理图像/视频数据,通过卷积核自动提取局部特征(如边缘、纹理)。 \n - **应用**:医学图像分析、人脸识别(上下文提到的“物体检测”)。\n- **循环神经网络(RNN)**: \n - **用途**:处理序列数据(文本、时间序列),通过反馈连接记忆历史信息。 \n - **应用**:语言翻译、语音识别(上下文提到的“情感分析”)。\n\n---\n\n### **4. 神经网络的训练数据依赖**\n- **监督学习**:依赖标记数据(如图像分类中的“猫/狗”标签)。 \n- **无监督学习**:从无标记数据中发现模式(如聚类用户行为)。\n\n---\n\n### **5. 神经网络的突破性应用(基于上下文)**\n- **自然语言处理(NLP)**:如聊天机器人、文本摘要。 \n- **计算机视觉**:如自动驾驶中的环境感知(上下文提到的“监控系统”)。 \n- **医疗保健**:分析医学影像、辅助诊断(上下文提到的“预测患者预后”)。\n\n---\n\n### **6. 神经网络的挑战**\n- **黑箱问题**:决策过程难以解释(上下文提到“透明度和可解释性”)。 \n- **数据偏见**:训练数据偏差可能导致模型歧视(如种族或性别偏见)。 \n- **计算资源**:深度网络需要大量数据和算力。\n\n---\n\n### **总结**\n神经网络通过模拟生物神经元的交互,实现了从数据中自动学习复杂模式的能力。其核心在于分层特征提取和权重优化,广泛应用于图像、文本、医疗等领域。然而,伦理问题(如偏见)和技术限制(如可解释性)仍需持续研究(与上下文中“伦理影响”和“XAI可解释人工智能”相呼应)。", 4 | "relevance": 4, 5 | "quality": 4, 6 | "comments": "", 7 | "timestamp": "2025-04-25T14:43:14.020011" 8 | } 9 | -------------------------------------------------------------------------------- /src/core/03_chunk_size_selector_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | chunk不同大小 核心函数 3 | """ 4 | import fitz 5 | 6 | 7 | def extract_text_from_pdf(pdf_path): 8 | """ 9 | Extracts text from a PDF file. 10 | 11 | Args: 12 | pdf_path (str): Path to the PDF file. 13 | 14 | Returns: 15 | str: Extracted text from the PDF. 16 | """ 17 | # Open the PDF file 18 | mypdf = fitz.open(pdf_path) 19 | all_text = "" # Initialize an empty string to store the extracted text 20 | 21 | # Iterate through each page in the PDF 22 | for page in mypdf: 23 | # Extract text from the current page and add spacing 24 | all_text += page.get_text("text") + " " 25 | 26 | # Return the extracted text, stripped of leading/trailing whitespace 27 | return all_text.strip() 28 | 29 | 30 | def chunk_text(text, n, overlap): 31 | """ 32 | 将文本分割为重叠的块。 33 | 34 | Args: 35 | text (str): 要分割的文本 36 | n (int): 每个块的字符数 37 | overlap (int): 块之间的重叠字符数 38 | 39 | Returns: 40 | List[str]: 文本块列表 41 | """ 42 | chunks = [] # 43 | for i in range(0, len(text), n - overlap): 44 | # 添加从当前索引到索引 + 块大小的文本块 45 | chunks.append(text[i:i + n]) 46 | 47 | return chunks # Return the list of text chunks 48 | 49 | 50 | if __name__ == '__main__': 51 | 52 | pdf_path = "../../data/AI_Information.en.zh-CN.pdf" 53 | extracted_text = extract_text_from_pdf(pdf_path) 54 | # 定义要评估的不同块大小 55 | chunk_sizes = [128, 256, 512] 56 | 57 | # 创建一个字典,用于存储每个块大小对应的文本块 58 | text_chunks_dict = {size: chunk_text(extracted_text, size, size // 5) for size in chunk_sizes} 59 | 60 | # 打印每个块大小生成的块数量 61 | for size, chunks in text_chunks_dict.items(): 62 | print(f"Chunk Size: {size}, Number of Chunks: {len(chunks)}") 63 | -------------------------------------------------------------------------------- /src/core/05_contextual_chunk_headers_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 上下文标题块 核心函数 3 | """ 4 | import os 5 | from openai import OpenAI 6 | from dotenv import load_dotenv 7 | 8 | load_dotenv() 9 | 10 | client = OpenAI( 11 | base_url=os.getenv("LLM_BASE_URL"), 12 | api_key=os.getenv("LLM_API_KEY") 13 | ) 14 | llm_model = os.getenv("LLM_MODEL_ID") 15 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 16 | 17 | 18 | #################################### 19 | # 将文本切分成块并为每块生成标题 20 | #################################### 21 | def chunk_text_with_headers(text, n, overlap): 22 | """ 23 | 将文本分割为较小的片段,并生成标题。 24 | 25 | Args: 26 | text (str): 要分块的完整文本 27 | n (int): 每个块的字符数 28 | overlap (int): 块之间的重叠字符数 29 | 30 | Returns: 31 | List[dict]: 包含 'header' 和 'text' 键的字典列表 32 | """ 33 | chunks = [] 34 | 35 | # 按指定的块大小和重叠量遍历文本 36 | for i in range(0, len(text), n - overlap): 37 | chunk = text[i:i + n] 38 | header = generate_chunk_header(chunk) # 使用 LLM 为块生成标题 39 | chunks.append({"header": header, "text": chunk}) # 将标题和块添加到列表中 40 | 41 | return chunks 42 | 43 | 44 | #################################### 45 | # 使用LLM 为块生成标题 46 | #################################### 47 | def generate_chunk_header(chunk): 48 | """ 49 | 使用 LLM 为给定的文本块生成标题/页眉 50 | 51 | Args: 52 | chunk (str): T要总结为标题的文本块 53 | model (str): 用于生成标题的模型 54 | 55 | Returns: 56 | str: 生成的标题/页眉 57 | """ 58 | # 定义系统提示 59 | system_prompt = "为给定的文本生成一个简洁且信息丰富的标题。" 60 | 61 | # 根据系统提示和文本块生成 62 | response = client.chat.completions.create( 63 | model=llm_model, 64 | temperature=0, 65 | messages=[ 66 | {"role": "system", "content": system_prompt}, 67 | {"role": "user", "content": chunk} 68 | ] 69 | ) 70 | 71 | # 返回生成的标题/页眉,去除任何前导或尾随空格 72 | return response.choices[0].message.content.strip() 73 | -------------------------------------------------------------------------------- /src/core/06_doc_augmentation_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 文档增强:问题生成 核心函数 3 | """ 4 | import os 5 | import re 6 | from openai import OpenAI 7 | from dotenv import load_dotenv 8 | 9 | load_dotenv() 10 | 11 | client = OpenAI( 12 | base_url=os.getenv("LLM_BASE_URL"), 13 | api_key=os.getenv("LLM_API_KEY") 14 | ) 15 | llm_model = os.getenv("LLM_MODEL_ID") 16 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 17 | 18 | #################################### 19 | # 文档增强:问题生成,根据文本块语义生成相关问题 20 | #################################### 21 | def generate_questions(text_chunk, num_questions=5): 22 | """ 23 | 生成可以从给定文本块中回答的相关问题。 24 | 25 | Args: 26 | text_chunk (str): 要生成问题的文本块。 27 | num_questions (int): 要生成的问题数量。 28 | model (str): 用于生成问题的模型。 29 | 30 | Returns: 31 | List[str]: 生成的问题列表。 32 | """ 33 | # 定义系统提示 34 | system_prompt = "你是一个从文本中生成相关问题的专家。能够根据用户提供的文本生成可回答的简洁问题,重点聚焦核心信息和关键概念。" 35 | 36 | # 定义用户提示,包含文本块和要生成的问题数量 37 | # user_prompt = f""" 38 | # 根据以下文本,生成 {num_questions} 个不同的问题,这些问题只能通过此文本回答: 39 | # 40 | # {text_chunk} 41 | # 42 | # 请以编号列表的形式回复问题,且不要添加任何额外文本。 43 | # """ 44 | user_prompt = f""" 45 | 请根据以下文本内容生成{num_questions}个不同的、仅能通过该文本内容回答的问题: 46 | 47 | {text_chunk} 48 | 49 | 请严格按以下格式回复: 50 | 1. 带编号的问题列表 51 | 2. 仅包含问题 52 | 3. 不要添加任何其他内容 53 | """ 54 | 55 | # 使用 OpenAI API 生成问题 56 | response = client.chat.completions.create( 57 | model=llm_model, 58 | temperature=0.7, 59 | messages=[ 60 | {"role": "system", "content": system_prompt}, 61 | {"role": "user", "content": user_prompt} 62 | ] 63 | ) 64 | 65 | # 从响应中提取并清理问题 66 | questions_text = response.choices[0].message.content.strip() 67 | 68 | # 使用正则表达式模式匹配提取问题 69 | pattern = r'^\d+\.\s*(.*)' 70 | return [re.match(pattern, line).group(1) for line in questions_text.split('\n') if line.strip()] 71 | # 此处改变了原有的正则处理,避免生成的问题中没有问号以及中英文问号匹配的问题 72 | # questions = [] 73 | # for line in questions_text.split('\n'): 74 | # # 去除编号并清理空白 75 | # cleaned_line = re.sub(r'^\d+\.\s*', '', line.strip()) 76 | # # if cleaned_line and cleaned_line.endswith('?') or cleaned_line.endswith("?"): 77 | # # questions.append(cleaned_line) 78 | # 79 | # return questions 80 | 81 | 82 | 83 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /data/val.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "question": "什么是‘可解释人工智能’,为什么它被认为很重要?", 4 | "ideal_answer": "可解释人工智能(XAI)旨在使人工智能系统更加透明和易于理解,提供它们如何做出决策的见解。它之所以重要,是因为能够建立信任、问责制,并确保人工智能系统的公平性。", 5 | "reference": "第5章:人工智能的未来 - 可解释人工智能(XAI);第19章:人工智能与伦理", 6 | "has_answer": true, 7 | "reasoning": "文档直接定义并解释了XAI的重要性。" 8 | }, 9 | { 10 | "question": "人工智能可以用来预测地震吗?", 11 | "ideal_answer": "我没有足够的信息来回答这个问题。", 12 | "reference": "无", 13 | "has_answer": false, 14 | "reasoning": "文档未提到使用人工智能进行地震预测。" 15 | }, 16 | { 17 | "question": "与人工智能驱动的人脸识别相关的伦理问题有哪些?", 18 | "ideal_answer": "我没有足够的信息来回答这个问题。", 19 | "reference": "无,尽管相关内容出现在第4章(伦理和社会影响)和第2章(计算机视觉)中。", 20 | "has_answer": false, 21 | "reasoning": "虽然文档讨论了人工智能的伦理问题(总体而言),并提到了人脸识别作为一种技术,但并未具体讨论人脸识别的伦理问题。" 22 | }, 23 | { 24 | "question": "人工智能如何为个性化医疗做出贡献?", 25 | "ideal_answer": "人工智能通过分析个体患者数据、预测治疗反应以及根据特定需求定制干预措施,推动个性化医疗的发展。这提高了治疗效果并减少了不良反应。", 26 | "reference": "第11章:人工智能与医疗保健 - 个性化医疗", 27 | "has_answer": true, 28 | "reasoning": "文档直接解释了人工智能在个性化医疗中的作用。" 29 | }, 30 | { 31 | "question": "文档是否提到了任何开发人工智能技术的具体公司?", 32 | "ideal_answer": "我没有足够的信息来回答这个问题。", 33 | "reference": "无", 34 | "has_answer": false, 35 | "reasoning": "文档专注于人工智能的概念和应用,而非具体的公司。" 36 | }, 37 | { 38 | "question": "人工智能在智能电网中的作用是什么?", 39 | "ideal_answer": "人工智能通过实现实时监控、需求响应以及分布式能源资源的整合,优化了智能电网中的能源分配。这增强了电网的可靠性,减少了能源浪费,并支持可再生能源的利用。", 40 | "reference": "第5章:人工智能的未来 - 能源存储与电网管理 - 智能电网;第15章", 41 | "has_answer": true, 42 | "reasoning": "文档直接描述了人工智能在智能电网中的功能。" 43 | }, 44 | { 45 | "question": "人工智能能否写出一部完整的原创小说?", 46 | "ideal_answer": "我没有足够的信息来回答这个问题。", 47 | "reference": "第9章:人工智能、创造力与创新 - 人工智能在写作和内容创作中的应用(可能还包括第16章)", 48 | "has_answer": false, 49 | "reasoning": "文档提到人工智能被用于写作和内容创作,协助研究和编辑,但并未说明人工智能能够独立完成一部完整的原创小说。" 50 | }, 51 | { 52 | "question": "什么是‘协作机器人’?", 53 | "ideal_answer": "它提到了工业机器人中的协作设置(协作机器人)。", 54 | "reference": "第6章:人工智能与机器人 - 机器人类型 - 工业机器人", 55 | "has_answer": true, 56 | "reasoning": "文档定义了‘协作机器人’。" 57 | }, 58 | { 59 | "question": "直接空气捕获(DAC)技术的用途是什么?", 60 | "ideal_answer": "DAC技术直接从大气中去除二氧化碳。捕获的二氧化碳可以被储存或用于各种应用。", 61 | "reference": "第5章:人工智能的未来 - 碳捕获与利用 - 直接空气捕获;第15章", 62 | "has_answer": true, 63 | "reasoning": "文档直接解释了直接空气捕获的目的。" 64 | }, 65 | { 66 | "question": "人工智能目前是否被用于控制核武器系统?", 67 | "ideal_answer": "我没有足够的信息来回答这个问题。", 68 | "reference": "无(尽管第4章讨论了人工智能的武器化)", 69 | "has_answer": false, 70 | "reasoning": "文档讨论了将人工智能武器化的伦理问题,但并未说明人工智能是否目前被用于控制核武器。" 71 | } 72 | ] -------------------------------------------------------------------------------- /src/core/04_context_enriched_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 上下文丰富检索 核心函数 3 | """ 4 | import os 5 | import numpy as np 6 | from openai import OpenAI 7 | from dotenv import load_dotenv 8 | 9 | load_dotenv() 10 | 11 | client = OpenAI( 12 | base_url=os.getenv("LLM_BASE_URL"), 13 | api_key=os.getenv("LLM_API_KEY") 14 | ) 15 | llm_model = os.getenv("LLM_MODEL_ID") 16 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 17 | 18 | 19 | #################################### 20 | # 检索最相关文本块以及相邻上下文 21 | #################################### 22 | def context_enriched_search(query, text_chunks, embeddings, k=1, context_size=1): 23 | """ 24 | 检索最相关的文本块及其相邻的上下文块 25 | 26 | Args: 27 | query (str): 搜索查询 28 | text_chunks (List[str]): 文本块列表 29 | embeddings (List[dict]): 文本块嵌入列表 30 | k (int): 要检索的相关块数量 31 | context_size (int): 包含的相邻块数量 32 | 33 | Returns: 34 | List[str]: 包含上下文信息的相关文本块 35 | """ 36 | # 将查询转换为嵌入向量 37 | query_embedding = create_embeddings(query) 38 | similarity_scores = [] 39 | 40 | # 计算查询与每个文本块嵌入之间的相似度分数 41 | for i, chunk_embedding in enumerate(embeddings): 42 | # 计算查询嵌入与当前文本块嵌入之间的余弦相似度 43 | similarity_score = cosine_similarity(np.array(query_embedding), chunk_embedding) 44 | # 将索引和相似度分数存储为元组 45 | similarity_scores.append((i, similarity_score)) 46 | 47 | # 按相似度分数降序排序(相似度最高排在前面) 48 | similarity_scores.sort(key=lambda x: x[1], reverse=True) 49 | 50 | # 获取最相关块的索引 51 | # top_index = [index for index, _ in similarity_scores[:k]] 52 | top_index = similarity_scores[0][0] 53 | 54 | # similarities = [cosine_similarity(query_embedding, emb) for emb in embeddings] 55 | # top_indices = np.argsort(similarities)[-k:][::-1] 56 | 57 | # 定义上下文包含的范围 58 | # 确保不会超出 text_chunks 的边界 59 | start = max(0, top_index - context_size) 60 | end = min(len(text_chunks), top_index + context_size + 1) 61 | 62 | # 返回最相关的块及其相邻的上下文块 63 | return [text_chunks[i] for i in range(start, end)] 64 | 65 | 66 | def cosine_similarity(vec1, vec2): 67 | """ 68 | Computes cosine similarity between two vectors. 69 | 70 | Args: 71 | vec1 (np.ndarray): First vector. 72 | vec2 (np.ndarray): Second vector. 73 | 74 | Returns: 75 | float: Cosine similarity score. 76 | """ 77 | 78 | # Compute the dot product of the two vectors 79 | return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) 80 | 81 | 82 | def create_embeddings(texts): 83 | """ 84 | 为文本列表生成嵌入 85 | 86 | Args: 87 | texts (List[str]): 输入文本列表. 88 | 89 | Returns: 90 | List[np.ndarray]: List of numerical embeddings. 91 | """ 92 | # 确保每次调用不超过64条文本 93 | batch_size = 64 94 | embeddings = [] 95 | 96 | for i in range(0, len(texts), batch_size): 97 | batch = texts[i:i + batch_size] 98 | response = client.embeddings.create( 99 | model=os.getenv("EMBEDDING_MODEL_ID"), 100 | input=batch 101 | ) 102 | # 将响应转换为numpy数组列表并添加到embeddings列表中 103 | embeddings.extend([np.array(embedding.embedding) for embedding in response.data]) 104 | 105 | return embeddings 106 | -------------------------------------------------------------------------------- /src/core/02_semantic_chunking_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 语义块切分 核心函数 3 | """ 4 | import numpy as np 5 | 6 | #################################### 7 | # 根据相似度下降计算分块的断点:断点方法有三种:百分位、标准差和四分位距 8 | #################################### 9 | def compute_breakpoints(similarities, method="percentile", threshold=90): 10 | """ 11 | 根据相似度下降计算分块的断点。 12 | 13 | Args: 14 | similarities (List[float]): 句子之间的相似度分数列表。 15 | method (str): 'percentile'(百分位)、'standard_deviation'(标准差)或 'interquartile'(四分位距)。 16 | threshold (float): 阈值(对于 'percentile' 是百分位数,对于 'standard_deviation' 是标准差倍数)。 17 | 18 | Returns: 19 | List[int]: 分块的索引列表。 20 | """ 21 | # 根据选定的方法确定阈值 22 | if method == "percentile": 23 | # 计算相似度分数的第 X 百分位数 24 | threshold_value = np.percentile(similarities, threshold) 25 | elif method == "standard_deviation": 26 | # 计算相似度分数的均值和标准差。 27 | mean = np.mean(similarities) 28 | std_dev = np.std(similarities) 29 | # 将阈值设置为均值减去 X 倍的标准差 30 | threshold_value = mean - (threshold * std_dev) 31 | elif method == "interquartile": 32 | # 计算第一和第三四分位数(Q1 和 Q3)。 33 | q1, q3 = np.percentile(similarities, [25, 75]) 34 | # 使用 IQR 规则(四分位距规则)设置阈值 35 | threshold_value = q1 - 1.5 * (q3 - q1) 36 | else: 37 | # 如果提供了无效的方法,则抛出异常 38 | raise ValueError("Invalid method. Choose 'percentile', 'standard_deviation', or 'interquartile'.") 39 | 40 | # 找出相似度低于阈值的索引 41 | return [i for i, sim in enumerate(similarities) if sim < threshold_value] 42 | 43 | # # 使用百分位法计算断点,阈值为90 44 | # breakpoints = compute_breakpoints(similarities, method="percentile", threshold=90) 45 | # breakpoints 46 | 47 | #################################### 48 | # 根据断点分割文本,得到语义块 49 | #################################### 50 | def split_into_chunks(sentences, breakpoints): 51 | """ 52 | 将句子分割为语义块 53 | 54 | Args: 55 | sentences (List[str]): 句子列表 56 | breakpoints (List[int]): 进行分块的索引位置 57 | 58 | Returns: 59 | List[str]: 文本块列表 60 | """ 61 | chunks = [] # Initialize an empty list to store the chunks 62 | start = 0 # Initialize the start index 63 | 64 | # 遍历每个断点以创建块 65 | for bp in breakpoints: 66 | # 将从起始位置到当前断点的句子块追加到列表中 67 | chunks.append("。".join(sentences[start:bp + 1]) + "。") 68 | start = bp + 1 # 将起始索引更新为断点后的下一个句子 69 | 70 | # 将剩余的句子作为最后一个块追加 71 | chunks.append("。".join(sentences[start:])) 72 | return chunks # Return the list of chunks 73 | 74 | # # split_into_chunks 函数创建文本块 75 | # text_chunks = split_into_chunks(sentences, breakpoints) 76 | # 77 | # # Print the number of chunks created 78 | # print(f"Number of semantic chunks: {len(text_chunks)}") 79 | # 80 | # # Print the first chunk to verify the result 81 | # print("\nFirst text chunk:") 82 | # print(text_chunks[0]) 83 | def cosine_similarity(vec1, vec2): 84 | """ 85 | Computes cosine similarity between two vectors. 86 | 87 | Args: 88 | vec1 (np.ndarray): First vector. 89 | vec2 (np.ndarray): Second vector. 90 | 91 | Returns: 92 | float: Cosine similarity. 93 | """ 94 | return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) 95 | 96 | # Compute similarity between consecutive sentences 97 | # similarities = [cosine_similarity(embeddings[i], embeddings[i + 1]) for i in range(len(embeddings) - 1)] 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/core/19_HyDE_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 假设文档生成 核心函数 3 | """ 4 | import os 5 | from openai import OpenAI 6 | from dotenv import load_dotenv 7 | 8 | load_dotenv() 9 | 10 | client = OpenAI( 11 | base_url=os.getenv("LLM_BASE_URL"), 12 | api_key=os.getenv("LLM_API_KEY") 13 | ) 14 | llm_model = os.getenv("LLM_MODEL_ID") 15 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 16 | 17 | 18 | #################################### 19 | # 假设文档 RAG 完整流程 20 | #################################### 21 | def hyde_rag(query, vector_store, k=5, should_generate_response=True): 22 | """ 23 | 使用假设文档嵌入(Hypothetical Document Embedding)执行 RAG(检索增强生成)。 24 | 25 | 参数: 26 | query (str): 用户查询 27 | vector_store (SimpleVectorStore): 包含文档片段的向量存储 28 | k (int): 要检索的片段数量 29 | generate_response (bool): 是否生成最终响应 30 | 31 | 返回: 32 | Dict: 结果,包括假设文档和检索到的片段 33 | """ 34 | print(f"\n=== 使用 HyDE 处理查询: {query} ===\n") 35 | 36 | # 第 1 步:生成一个假设文档来回答查询 37 | print("生成假设文档...") 38 | hypothetical_doc = generate_hypothetical_document(query) 39 | print(f"生成了长度为 {len(hypothetical_doc)} 个字符的假设文档") 40 | 41 | # 第 2 步:为假设文档创建嵌入 42 | print("为假设文档创建嵌入...") 43 | hypothetical_embedding = create_embeddings([hypothetical_doc])[0] 44 | 45 | # 第 3 步:根据假设文档检索相似的片段 46 | print(f"检索 {k} 个最相似的片段...") 47 | retrieved_chunks = vector_store.similarity_search(hypothetical_embedding, k=k) 48 | 49 | # 准备结果字典 50 | results = { 51 | "query": query, 52 | "hypothetical_document": hypothetical_doc, 53 | "retrieved_chunks": retrieved_chunks 54 | } 55 | 56 | # 第 4 步:如果需要,生成最终响应 57 | if should_generate_response: 58 | print("生成最终响应...") 59 | response = generate_response(query, retrieved_chunks) 60 | results["response"] = response 61 | 62 | return results 63 | 64 | 65 | #################################### 66 | # 根据用户查询生成假设性文档 67 | #################################### 68 | def generate_hypothetical_document(query, desired_length=1000): 69 | """ 70 | 生成能够回答查询的假设文档 71 | 72 | Args: 73 | query (str): 用户查询内容 74 | desired_length (int): 目标文档长度(字符数) 75 | 76 | Returns: 77 | str: 生成的假设文档文本 78 | """ 79 | # 定义系统提示词以指导模型生成文档的方法 80 | system_prompt = f"""你是一位专业的文档创建专家。 81 | 给定一个问题,请生成一份能够直接解答该问题的详细文档。 82 | 文档长度应约为 {desired_length} 个字符,需提供深入且具有信息量的答案。 83 | 请以权威资料的口吻撰写,内容需包含具体细节、事实和解释。 84 | 不要提及这是假设性文档 - 直接输出内容即可。""" 85 | 86 | # 用查询定义用户提示词 87 | user_prompt = f"问题: {query}\n\n生成一份完整解答该问题的文档:" 88 | 89 | # 调用OpenAI API生成假设文档 90 | response = client.chat.completions.create( 91 | model=llm_model, # 指定使用的模型 92 | messages=[ 93 | {"role": "system", "content": system_prompt}, # 系统指令引导模型行为 94 | {"role": "user", "content": user_prompt} # 包含用户查询的输入 95 | ], 96 | temperature=0.1 # 控制输出随机性的温度参数 97 | ) 98 | 99 | # 返回生成的文档内容 100 | return response.choices[0].message.content 101 | 102 | 103 | def generate_response(query, relevant_chunks): 104 | """ 105 | 根据查询和相关文本块生成最终回答。 106 | 107 | Args: 108 | query (str): 用户查询 109 | relevant_chunks (List[Dict]): 检索到的相关文本块列表 110 | 111 | Returns: 112 | str: 生成的回答内容 113 | """ 114 | # 将多个文本块的内容拼接起来,形成上下文 115 | context = "\n\n".join([chunk["text"] for chunk in relevant_chunks]) 116 | 117 | # 使用 OpenAI API 生成回答 118 | response = client.chat.completions.create( 119 | model=llm_model, # 指定使用的模型 120 | messages=[ 121 | {"role": "system", "content": "你是一个有帮助的助手。请基于提供的上下文回答问题。"}, 122 | {"role": "user", "content": f"上下文内容:\n{context}\n\n问题:{query}"} 123 | ], 124 | temperature=0.5, # 控制生成内容的随机性 125 | max_tokens=500 # 最大生成 token 数量 126 | ) 127 | 128 | # 返回生成的回答内容 129 | return response.choices[0].message.content 130 | 131 | 132 | def create_embeddings(texts): 133 | """ 134 | 为给定文本创建嵌入向量。 135 | 136 | Args: 137 | texts (List[str]): 输入文本列表 138 | model (str): 嵌入模型名称 139 | 140 | Returns: 141 | List[List[float]]: 嵌入向量列表 142 | """ 143 | # 处理空输入的情况 144 | if not texts: 145 | return [] 146 | 147 | # 分批次处理(OpenAI API 的限制) 148 | batch_size = 100 149 | all_embeddings = [] 150 | 151 | # 遍历输入文本,按批次生成嵌入 152 | for i in range(0, len(texts), batch_size): 153 | batch = texts[i:i + batch_size] # 获取当前批次的文本 154 | 155 | # 调用 OpenAI 接口生成嵌入 156 | response = client.embeddings.create( 157 | model=embedding_model, 158 | input=batch 159 | ) 160 | 161 | # 提取当前批次的嵌入向量 162 | batch_embeddings = [item.embedding for item in response.data] 163 | all_embeddings.extend(batch_embeddings) # 将当前批次的嵌入向量加入总列表 164 | 165 | return all_embeddings # 返回所有嵌入向量 166 | -------------------------------------------------------------------------------- /src/core/16_fusion_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 混合检索核心函数 3 | """ 4 | import os 5 | import numpy as np 6 | import jieba 7 | from openai import OpenAI 8 | from dotenv import load_dotenv 9 | 10 | load_dotenv() 11 | 12 | client = OpenAI( 13 | base_url=os.getenv("LLM_BASE_URL"), 14 | api_key=os.getenv("LLM_API_KEY") 15 | ) 16 | llm_model = os.getenv("LLM_MODEL_ID") 17 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 18 | 19 | 20 | #################################### 21 | # 混合检索 22 | #################################### 23 | def fusion_retrieval(query, chunks, vector_store, bm25_index, k=5, alpha=0.5): 24 | """ 25 | 执行融合检索,结合基于向量和BM25的搜索。 26 | 27 | Args: 28 | query (str): 查询字符串 29 | chunks (List[Dict]): 原始文本块 30 | vector_store (SimpleVectorStore): 向量存储 31 | bm25_index (BM25Okapi): BM25 索引 32 | k (int): 返回的结果数量 33 | alpha (float): 向量分数的权重(0-1),其中 1-alpha 是 BM25 的权重 34 | 35 | Returns: 36 | List[Dict]: 基于综合分数的前 k 个结果 37 | """ 38 | print(f"正在为查询执行融合检索: {query}") 39 | 40 | # 定义一个小的 epsilon 来避免除以零 41 | epsilon = 1e-8 42 | 43 | # 获取向量搜索结果 44 | query_embedding = create_embeddings(query) # 为查询创建嵌入 45 | vector_results = vector_store.similarity_search_with_scores(query_embedding, k=len(chunks)) # 执行向量搜索 46 | 47 | # 获取 BM25 搜索结果 48 | bm25_results = bm25_search(bm25_index, chunks, query, k=len(chunks)) # 执行 BM25 搜索 49 | 50 | # 创建字典将文档索引映射到分数 51 | vector_scores_dict = {result["metadata"]["index"]: result["similarity"] for result in vector_results} 52 | bm25_scores_dict = {result["metadata"]["index"]: result["bm25_score"] for result in bm25_results} 53 | 54 | # 确保所有文档都有两种方法的分数 55 | all_docs = vector_store.get_all_documents() 56 | combined_results = [] 57 | 58 | for i, doc in enumerate(all_docs): 59 | vector_score = vector_scores_dict.get(i, 0.0) # 获取向量分数,如果未找到则为 0 60 | bm25_score = bm25_scores_dict.get(i, 0.0) # 获取 BM25 分数,如果未找到则为 0 61 | combined_results.append({ 62 | "text": doc["text"], 63 | "metadata": doc["metadata"], 64 | "vector_score": vector_score, 65 | "bm25_score": bm25_score, 66 | "index": i 67 | }) 68 | 69 | # 提取分数为数组 70 | vector_scores = np.array([doc["vector_score"] for doc in combined_results]) 71 | bm25_scores = np.array([doc["bm25_score"] for doc in combined_results]) 72 | 73 | # 归一化分数 74 | norm_vector_scores = (vector_scores - np.min(vector_scores)) / ( 75 | np.max(vector_scores) - np.min(vector_scores) + epsilon) 76 | norm_bm25_scores = (bm25_scores - np.min(bm25_scores)) / (np.max(bm25_scores) - np.min(bm25_scores) + epsilon) 77 | 78 | # 计算综合分数 79 | combined_scores = alpha * norm_vector_scores + (1 - alpha) * norm_bm25_scores 80 | 81 | # 将综合分数添加到结果中 82 | for i, score in enumerate(combined_scores): 83 | combined_results[i]["combined_score"] = float(score) 84 | 85 | # 按综合分数排序(降序) 86 | combined_results.sort(key=lambda x: x["combined_score"], reverse=True) 87 | 88 | # 返回前 k 个结果 89 | top_results = combined_results[:k] 90 | 91 | print(f"通过融合检索获取了 {len(top_results)} 份文档") 92 | return top_results 93 | 94 | 95 | #################################### 96 | # BM25 检索 97 | #################################### 98 | def bm25_search(bm25, chunks, query, k=5): 99 | """ 100 | 使用查询在 BM25 索引中进行搜索。 101 | 102 | Args: 103 | bm25 (BM25Okapi): BM25 索引 104 | chunks (List[Dict]): 文本块列表 105 | query (str): 查询字符串 106 | k (int): 返回的结果数量 107 | 108 | Returns: 109 | List[Dict]: 带有分数的前 k 个结果 110 | """ 111 | # 将查询按空格分割成单独的词 112 | # query_tokens = query.split() # 英文 113 | query_tokens = list(jieba.cut(query)) # 中文 114 | 115 | # 获取查询词对已索引文档的 BM25 分数 116 | scores = bm25.get_scores(query_tokens) 117 | 118 | # 初始化一个空列表,用于存储带有分数的结果 119 | results = [] 120 | 121 | # 遍历分数和对应的文本块 122 | for i, score in enumerate(scores): 123 | # 创建元数据的副本以避免修改原始数据 124 | metadata = chunks[i].get("metadata", {}).copy() 125 | # 向元数据中添加索引 126 | metadata["index"] = i 127 | 128 | results.append({ 129 | "text": chunks[i]["text"], # 文本内容 130 | "metadata": metadata, # 带索引的元数据 131 | "bm25_score": float(score) # BM25 分数 132 | }) 133 | 134 | # 按 BM25 分数降序排序结果 135 | results.sort(key=lambda x: x["bm25_score"], reverse=True) 136 | 137 | # 返回前 k 个结果 138 | return results[:k] 139 | 140 | 141 | def create_embeddings(texts): 142 | """ 143 | 为给定的文本创建嵌入向量。 144 | 145 | Args: 146 | texts (str 或 List[str]): 输入文本(可以是单个字符串或字符串列表) 147 | # model (str): 嵌入模型名称 148 | 149 | 返回: 150 | List[List[float]]: 嵌入向量列表 151 | """ 152 | # 处理字符串和列表类型的输入 153 | input_texts = texts if isinstance(texts, list) else [texts] 154 | 155 | # 如果需要,按批次处理(OpenAI API 有请求限制) 156 | batch_size = 100 157 | all_embeddings = [] 158 | 159 | # 按批次迭代输入文本 160 | for i in range(0, len(input_texts), batch_size): 161 | batch = input_texts[i:i + batch_size] # 获取当前批次的文本 162 | 163 | # 为当前批次创建嵌入向量 164 | response = client.embeddings.create( 165 | model=embedding_model, 166 | input=batch 167 | ) 168 | 169 | # 从响应中提取嵌入向量 170 | batch_embeddings = [item.embedding for item in response.data] 171 | all_embeddings.extend(batch_embeddings) # 将批次嵌入向量添加到总列表中 172 | 173 | # 如果输入是单个字符串,仅返回第一个嵌入向量 174 | if isinstance(texts, str): 175 | return all_embeddings[0] 176 | 177 | # 否则,返回所有嵌入向量 178 | return all_embeddings 179 | -------------------------------------------------------------------------------- /src/core/14_propostion_chunking_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 命题分块 核心函数 3 | """ 4 | import re 5 | import os 6 | import json 7 | import fitz 8 | from openai import OpenAI 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | client = OpenAI( 14 | base_url=os.getenv("LLM_BASE_URL"), 15 | api_key=os.getenv("LLM_API_KEY") 16 | ) 17 | llm_model = os.getenv("LLM_MODEL_ID") 18 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 19 | 20 | 21 | #################################### 22 | # 命题生成 完整的流程 23 | #################################### 24 | def process_document_into_propositions(pdf_path, chunk_size=800, chunk_overlap=100, quality_thresholds=None): 25 | """ 26 | 将文档处理为经过质量检查的命题。 27 | 28 | Args: 29 | pdf_path (str): PDF文件的路径 30 | chunk_size (int): 每个分块的字符大小 31 | chunk_overlap (int): 分块之间的重叠字符数 32 | quality_thresholds (Dict): 命题质量的阈值分数 33 | 34 | Returns: 35 | Tuple[List[Dict], List[Dict]]: 原始分块和命题分块 36 | """ 37 | # 如果未提供,则设置默认的质量阈值 38 | if quality_thresholds is None: 39 | quality_thresholds = { 40 | "accuracy": 7, # 准确性阈值 41 | "clarity": 7, # 清晰性阈值 42 | "completeness": 7, # 完整性阈值 43 | "conciseness": 7 # 简洁性阈值 44 | } 45 | 46 | # 从PDF文件中提取文本 47 | text = extract_text_from_pdf(pdf_path) 48 | 49 | # 从提取的文本创建分块 50 | chunks = chunk_text(text, chunk_size, chunk_overlap) 51 | 52 | # 初始化一个列表以存储所有命题 53 | all_propositions = [] 54 | 55 | print("从分块生成命题...") 56 | for i, chunk in enumerate(chunks): 57 | print(f"处理分块 {i + 1}/{len(chunks)}...") 58 | 59 | # 为当前分块生成命题 60 | chunk_propositions = generate_propositions(chunk) 61 | print(f"生成了 {len(chunk_propositions)} 个命题") 62 | 63 | # 处理每个生成的命题 64 | for prop in chunk_propositions: 65 | proposition_data = { 66 | "text": prop, # 命题文本 67 | "source_chunk_id": chunk["chunk_id"], # 来源分块ID 68 | "source_text": chunk["text"] # 来源分块文本 69 | } 70 | all_propositions.append(proposition_data) 71 | 72 | # 评估生成的命题质量 73 | print("\n评估命题质量...") 74 | quality_propositions = [] 75 | 76 | for i, prop in enumerate(all_propositions): 77 | if i % 10 == 0: # 每10个命题进行一次状态更新 78 | print(f"评估命题 {i + 1}/{len(all_propositions)}...") 79 | 80 | # 评估当前命题的质量 81 | scores = evaluate_proposition(prop["text"], prop["source_text"]) 82 | prop["quality_scores"] = scores 83 | 84 | # 检查命题是否通过质量阈值 85 | passes_quality = True 86 | for metric, threshold in quality_thresholds.items(): 87 | if scores.get(metric, 0) < threshold: 88 | passes_quality = False 89 | break 90 | 91 | if passes_quality: 92 | quality_propositions.append(prop) 93 | else: 94 | print(f"命题未通过质量检查: {prop['text'][:50]}...") 95 | 96 | print(f"\n在质量过滤后保留了 {len(quality_propositions)}/{len(all_propositions)} 个命题") 97 | 98 | return chunks, quality_propositions 99 | 100 | 101 | #################################### 102 | # 命题生成 103 | #################################### 104 | def generate_propositions(chunk): 105 | """ 106 | 从文本块中生成原子化、自包含的命题。 107 | 108 | Args: 109 | chunk (Dict): 包含内容和元数据的文本块 110 | 111 | Returns: 112 | List[str]: 生成的命题列表 113 | """ 114 | # 系统提示,指示AI如何生成命题 115 | system_prompt = """请将以下文本分解为简单的自包含命题。确保每个命题符合以下标准: 116 | 117 | 1. 表达单一事实:每个命题应陈述一个具体事实或主张 118 | 2. 独立可理解:命题应自成体系,无需额外上下文即可理解 119 | 3. 使用全称而非代词:避免使用代词或模糊指代,使用完整的实体名称 120 | 4. 包含相关日期/限定条件:如适用应包含必要日期、时间和限定条件以保持准确性 121 | 5. 保持单一主谓关系:聚焦单个主体及其对应动作或属性,避免连接词和多从句结构 122 | 123 | 请仅输出命题列表,不要包含任何额外文本或解释。 124 | """ 125 | # 用户提示,包含要转换为命题的文本块 126 | user_prompt = f"要转换为命题的文本:\n\n{chunk['text']}" 127 | 128 | # 从模型生成响应 129 | response = client.chat.completions.create( 130 | model=llm_model, # 使用更强的模型以准确生成命题 131 | messages=[ 132 | {"role": "system", "content": system_prompt}, 133 | {"role": "user", "content": user_prompt} 134 | ], 135 | temperature=0 136 | ) 137 | 138 | # 从响应中提取命题 139 | raw_propositions = response.choices[0].message.content.strip().split('\n') 140 | 141 | # 清理命题(移除编号、项目符号等) 142 | clean_propositions = [] 143 | for prop in raw_propositions: 144 | # 移除编号(如1., 2.等)和项目符号 145 | cleaned = re.sub(r'^\s*(\d+\.|\-|\*)\s*', '', prop).strip() 146 | if cleaned and len(cleaned) > 10: # 简单过滤空或过短的命题 147 | clean_propositions.append(cleaned) 148 | 149 | return clean_propositions 150 | 151 | 152 | #################################### 153 | # 评估命题质量 154 | #################################### 155 | def evaluate_proposition(proposition, original_text): 156 | """ 157 | 根据准确性、清晰性、完整性以及简洁性评估命题的质量。 158 | 159 | Args: 160 | proposition (str): 要评估的命题 161 | original_text (str): 用于比较的原文 162 | 163 | Returns: 164 | Dict: 每个评估维度的分数 165 | """ 166 | # 系统提示,指示AI如何评估命题 167 | system_prompt = """你是一位评估从文本中提取命题质量的专家。请根据以下标准对给定命题进行评分(1-10分): 168 | 169 | - 准确性(Accuracy):命题反映原文信息的准确程度 170 | - 清晰性(Clarity):不依赖额外上下文的情况下,命题是否易于理解 171 | - 完整性(Completeness):命题是否包含必要的细节(如日期、限定词等) 172 | - 简洁性(Conciseness):命题是否在保留关键信息前提下,表述精简程度 173 | 174 | 响应必须为有效的JSON格式,并包含每个标准的数值评分: 175 | {"accuracy": X, "clarity": X, "completeness": X, "conciseness": X} 176 | """ 177 | 178 | # 用户提示,包含命题和原文 179 | user_prompt = f"""命题: {proposition} 180 | 181 | 原文: {original_text} 182 | 183 | 请以JSON格式提供你的评分。""" 184 | 185 | # 从模型生成响应 186 | response = client.chat.completions.create( 187 | model=llm_model, 188 | messages=[ 189 | {"role": "system", "content": system_prompt}, 190 | {"role": "user", "content": user_prompt} 191 | ], 192 | response_format={"type": "json_object"}, 193 | temperature=0 194 | ) 195 | 196 | # 解析JSON响应 197 | try: 198 | scores = json.loads(response.choices[0].message.content.strip()) 199 | return scores 200 | except json.JSONDecodeError: 201 | # 如果JSON解析失败,使用默认值作为回退 202 | return { 203 | "accuracy": 5, 204 | "clarity": 5, 205 | "completeness": 5, 206 | "conciseness": 5 207 | } 208 | 209 | 210 | def extract_text_from_pdf(pdf_path): 211 | """ 212 | 从 PDF 文件中提取文本,并打印前 `num_chars` 个字符。 213 | 214 | Args: 215 | pdf_path (str): Path to the PDF file. 216 | 217 | Returns: 218 | str: Extracted text from the PDF. 219 | """ 220 | # 打开 PDF 文件 221 | mypdf = fitz.open(pdf_path) 222 | all_text = "" # 初始化一个空字符串以存储提取的文本 223 | 224 | # Iterate through each page in the PDF 225 | for page_num in range(mypdf.page_count): 226 | page = mypdf[page_num] 227 | text = page.get_text("text") # 从页面中提取文本 228 | all_text += text # 将提取的文本追加到 all_text 字符串中 229 | 230 | return all_text # 返回提取的文本 231 | 232 | 233 | def chunk_text(text, chunk_size=800, overlap=100): 234 | """ 235 | 将文本分割为重叠的块。 236 | 237 | Args: 238 | text (str): 要分割的输入文本 239 | chunk_size (int): 每个块的字符数 240 | overlap (int): 块之间的字符重叠数 241 | 242 | Returns: 243 | List[Dict]: 包含文本和元数据的块字典列表 244 | """ 245 | chunks = [] # 初始化一个空列表来存储块 246 | 247 | # 使用指定的块大小和重叠迭代文本 248 | for i in range(0, len(text), chunk_size - overlap): 249 | chunk = text[i:i + chunk_size] # 提取指定大小的块 250 | if chunk: # 确保不添加空块 251 | chunks.append({ 252 | "text": chunk, # 块文本 253 | "chunk_id": len(chunks) + 1, # 块的唯一ID 254 | "start_char": i, # 块的起始字符索引 255 | "end_char": i + len(chunk) # 块的结束字符索引 256 | }) 257 | 258 | print(f"创建了 {len(chunks)} 个文本块") # 打印创建的块数 259 | return chunks # 返回块列表 260 | -------------------------------------------------------------------------------- /src/core/08_reranker_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 重排(LLM重排、关键词重排) 核心函数 3 | """ 4 | import os 5 | import re 6 | from openai import OpenAI 7 | from dotenv import load_dotenv 8 | 9 | load_dotenv() 10 | 11 | client = OpenAI( 12 | base_url=os.getenv("LLM_BASE_URL"), 13 | api_key=os.getenv("LLM_API_KEY") 14 | ) 15 | llm_model = os.getenv("LLM_MODEL_ID") 16 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 17 | 18 | 19 | #################################### 20 | # 重排的完整流程 21 | #################################### 22 | def rag_with_reranking(query, vector_store, reranking_method="llm", top_n=3): 23 | """ 24 | 完整的RAG管道,包含重排序功能。 25 | 26 | Args: 27 | query (str): 用户查询 28 | vector_store (SimpleVectorStore): 向量存储 29 | reranking_method (str): 重排序方法 ('llm' 或 'keywords') 30 | top_n (int): 重排序后返回的结果数量 31 | 32 | Returns: 33 | Dict: 包括查询、上下文和响应的结果 34 | """ 35 | # 创建查询嵌入 36 | query_embedding = create_embeddings(query) 37 | 38 | # 初步检索(获取比重排序所需更多的结果) 39 | initial_results = vector_store.similarity_search(query_embedding, k=10) 40 | 41 | # 应用重排序 42 | if reranking_method == "llm": 43 | reranked_results = rerank_with_llm(query, initial_results, top_n=top_n) 44 | elif reranking_method == "keywords": 45 | reranked_results = rerank_with_keywords(query, initial_results, top_n=top_n) 46 | else: 47 | # 不进行重排序,直接使用初步检索的前几个结果 48 | reranked_results = initial_results[:top_n] 49 | 50 | # 将重排序结果的上下文合并 51 | context = "\n\n===\n\n".join([result["text"] for result in reranked_results]) 52 | 53 | # 根据上下文生成响应 54 | response = generate_response(query, context) 55 | 56 | return { 57 | "query": query, 58 | "reranking_method": reranking_method, 59 | "initial_results": initial_results[:top_n], 60 | "reranked_results": reranked_results, 61 | "context": context, 62 | "response": response 63 | } 64 | 65 | 66 | #################################### 67 | # 基于 LLM 的重排序 68 | #################################### 69 | def rerank_with_llm(query, results, top_n=3): 70 | """ 71 | 使用 LLM 相关性评分对搜索结果进行重排序。 72 | 73 | Args: 74 | query (str): 用户查询 75 | results (List[Dict]): 初始搜索结果 76 | top_n (int): 重排序后要返回的结果数量 77 | 78 | Returns: 79 | List[Dict]: 重排序后的结果 80 | """ 81 | print(f"正在重排序 {len(results)} 个文档...") # 打印要重排序的文档数量 82 | 83 | scored_results = [] # 初始化一个空列表以存储评分结果 84 | 85 | # 定义 LLM 的系统提示 86 | system_prompt = """ 87 | 您是文档相关性评估专家,擅长判断文档与搜索查询的匹配程度。您的任务是根据文档对给定查询的应答质量,给出0到10分的评分。 88 | 89 | 评分标准: 90 | 0-2分:文档完全无关 91 | 3-5分:文档含部分相关信息但未直接回答问题 92 | 6-8分:文档相关且能部分解答查询 93 | 9-10分:文档高度相关且直接准确回答问题 94 | 95 | 必须仅返回0到10之间的单个整数评分,不要包含任何其他内容。 96 | """ 97 | 98 | # 遍历每个结果 99 | for i, result in enumerate(results): 100 | # 每 5 个文档显示一次进度 101 | if i % 5 == 0: 102 | print(f"正在评分文档 {i + 1}/{len(results)}...") 103 | 104 | # 定义 LLM 的用户提示 105 | user_prompt = f""" 106 | 查询: {query} 107 | 108 | 文档: 109 | {result['text']} 110 | 111 | 请对文档的相关性进行评分,评分范围为 0 到 10, 并仅返回一个整数。 112 | """ 113 | 114 | # 获取 LLM 的响应 115 | response = client.chat.completions.create( 116 | model=llm_model, 117 | temperature=0, 118 | messages=[ 119 | {"role": "system", "content": system_prompt}, 120 | {"role": "user", "content": user_prompt} 121 | ] 122 | ) 123 | 124 | # 从 LLM 响应中提取评分 125 | score_text = response.choices[0].message.content.strip() 126 | 127 | # 使用正则表达式提取数值评分 128 | score_match = re.search(r'\b(10|[0-9])\b', score_text) 129 | if score_match: 130 | score = float(score_match.group(1)) 131 | else: 132 | # 如果评分提取失败,使用相似度评分作为备选 133 | print(f"警告:无法从响应中提取评分:'{score_text}',使用相似度评分代替") 134 | score = result["similarity"] * 10 135 | 136 | # 将评分结果添加到列表中 137 | scored_results.append({ 138 | "text": result["text"], 139 | "metadata": result["metadata"], 140 | "similarity": result["similarity"], 141 | "relevance_score": score 142 | }) 143 | 144 | # 按相关性评分降序对结果进行排序 145 | reranked_results = sorted(scored_results, key=lambda x: x["relevance_score"], reverse=True) 146 | 147 | # 返回前 top_n 个结果 148 | return reranked_results[:top_n] 149 | 150 | 151 | #################################### 152 | # 基于关键词的重排序 153 | #################################### 154 | def rerank_with_keywords(query, results, top_n=3): 155 | """ 156 | 基于关键词匹配次数和位置的简单重排序方法。 157 | 158 | Args: 159 | query (str): 用户查询 160 | results (List[Dict]): 初始搜索结果 161 | top_n (int): 重排序后返回的结果数量 162 | 163 | Returns: 164 | List[Dict]: 重排序后的结果 165 | """ 166 | # 从查询中提取重要关键词 167 | keywords = [word.lower() for word in query.split() if len(word) > 3] 168 | 169 | scored_results = [] # 初始化一个列表以存储评分结果 170 | 171 | for result in results: 172 | document_text = result["text"].lower() # 将文档文本转换为小写 173 | 174 | # 基础分数从向量相似度开始 175 | base_score = result["similarity"] * 0.5 176 | 177 | # 初始化关键词分数 178 | keyword_score = 0 179 | for keyword in keywords: 180 | if keyword in document_text: 181 | # 每找到一个关键词加一些分数 182 | keyword_score += 0.1 183 | 184 | # 如果关键词出现在文本开头部分,额外加分 185 | first_position = document_text.find(keyword) 186 | if first_position < len(document_text) / 4: # 在文本的前四分之一部分 187 | keyword_score += 0.1 188 | 189 | # 根据关键词出现的频率加分 190 | frequency = document_text.count(keyword) 191 | keyword_score += min(0.05 * frequency, 0.2) # 最大值限制为 0.2 192 | 193 | # 通过结合基础分数和关键词分数计算最终得分 194 | final_score = base_score + keyword_score 195 | 196 | # 将评分结果添加到列表中 197 | scored_results.append({ 198 | "text": result["text"], 199 | "metadata": result["metadata"], 200 | "similarity": result["similarity"], 201 | "relevance_score": final_score 202 | }) 203 | 204 | # 按最终相关性分数降序对结果进行排序 205 | reranked_results = sorted(scored_results, key=lambda x: x["relevance_score"], reverse=True) 206 | 207 | # 返回前 top_n 个结果 208 | return reranked_results[:top_n] 209 | 210 | 211 | def generate_response(query, context): 212 | """ 213 | 根据查询和上下文生成响应。 214 | 215 | Args: 216 | query (str): 用户查询 217 | context (str): 获取到的上下文 218 | 219 | Returns: 220 | str: 生成的响应 221 | """ 222 | # 定义系统提示以指导AI的行为 223 | system_prompt = "您是一个乐于助人的AI助手。请仅根据提供的上下文来回答用户的问题。如果在上下文中找不到答案,请直接说'没有足够的信息'。" 224 | 225 | # 通过组合上下文和查询创建用户提示 226 | user_prompt = f""" 227 | 上下文: 228 | {context} 229 | 230 | 问题: {query} 231 | 232 | 请基于上述上下文内容提供一个全面详尽的答案。 233 | """ 234 | 235 | # 使用指定的模型生成响应 236 | response = client.chat.completions.create( 237 | model=llm_model, 238 | temperature=0, 239 | messages=[ 240 | {"role": "system", "content": system_prompt}, 241 | {"role": "user", "content": user_prompt} 242 | ] 243 | ) 244 | 245 | # 返回生成的响应内容 246 | return response.choices[0].message.content 247 | 248 | 249 | def create_embeddings(text): 250 | """ 251 | 使用Embedding模型为给定文本创建嵌入向量。 252 | 253 | Args: 254 | text (str): 要创建嵌入向量的输入文本。 255 | 256 | Returns: 257 | List[float]: 嵌入向量。 258 | """ 259 | # 通过将字符串输入转换为列表来处理字符串和列表输入 260 | input_text = text if isinstance(text, list) else [text] 261 | 262 | # 使用指定的模型为输入文本创建嵌入向量 263 | response = client.embeddings.create( 264 | model=embedding_model, 265 | input=input_text 266 | ) 267 | 268 | # 如果输入是字符串,仅返回第一个嵌入向量 269 | if isinstance(text, str): 270 | return response.data[0].embedding 271 | 272 | # 否则,将所有嵌入向量作为向量列表返回 273 | return [item.embedding for item in response.data] 274 | -------------------------------------------------------------------------------- /src/core/17_graph_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Graph RAG 核心函数 3 | """ 4 | import json 5 | import re 6 | import os 7 | import numpy as np 8 | import networkx as nx 9 | import heapq 10 | from openai import OpenAI 11 | from dotenv import load_dotenv 12 | 13 | load_dotenv() 14 | 15 | client = OpenAI( 16 | base_url=os.getenv("LLM_BASE_URL"), 17 | api_key=os.getenv("LLM_API_KEY") 18 | ) 19 | llm_model = os.getenv("LLM_MODEL_ID") 20 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 21 | 22 | 23 | #################################### 24 | # 从文本块中提取关键概念 25 | #################################### 26 | def extract_concepts(text): 27 | """ 28 | 从文本中提取关键概念。 29 | 30 | Args: 31 | text (str): 需要提取概念的文本 32 | 33 | Returns: 34 | List[str]: 包含提取出的概念的列表 35 | """ 36 | # 系统消息,用于指导模型执行任务 37 | system_message = """从提供的文本中提取关键概念和实体。 38 | 只返回一个包含5到10个最重要的关键词、实体或概念的列表 39 | 以JSON字符串数组的格式返回结果。 40 | 41 | 结果格式为:{"concepts": [x, x, x]} 42 | 43 | """ 44 | 45 | # 调用OpenAI API进行请求 46 | response = client.chat.completions.create( 47 | model=llm_model, # 指定使用的模型 48 | messages=[ 49 | {"role": "system", "content": system_message}, # 系统消息 50 | {"role": "user", "content": f"从以下文本中提取关键概念:\n\n{text[:3000]}"} # 用户消息,限制文本长度以符合API要求 51 | ], 52 | temperature=0.0, # 设置生成温度为确定性结果 53 | response_format={"type": "json_object"} # 指定响应格式为JSON对象 54 | ) 55 | 56 | try: 57 | # 从响应中解析概念 58 | concepts_json = json.loads(response.choices[0].message.content.strip()) # 将响应内容解析为JSON 59 | concepts = concepts_json.get("concepts", []) # 获取"concepts"字段的值 60 | if not concepts and "concepts" not in concepts_json: 61 | # 如果未找到"concepts"字段,则尝试获取JSON中的任意列表 62 | for key, value in concepts_json.items(): 63 | if isinstance(value, list): 64 | concepts = value 65 | break 66 | return concepts # 返回提取出的概念列表 67 | except (json.JSONDecodeError, AttributeError): 68 | # 如果JSON解析失败,则进行回退处理 69 | content = response.choices[0].message.content # 获取原始响应内容 70 | # 尝试从响应内容中提取类似列表的部分 71 | matches = re.findall(r'\[(.*?)\]', content, re.DOTALL) # 查找方括号内的内容 72 | if matches: 73 | items = re.findall(r'"([^"]*)"', matches[0]) # 提取方括号内的字符串项 74 | return items 75 | return [] # 如果无法提取,则返回空列表 76 | 77 | 78 | #################################### 79 | # 构建知识图谱:节点、边 80 | #################################### 81 | def build_knowledge_graph(chunks): 82 | """ 83 | 从文本片段构建知识图谱。 84 | 85 | Args: 86 | chunks (List[Dict]): 包含元数据的文本片段列表 87 | 88 | Returns: 89 | Tuple[nx.Graph, List[np.ndarray]]: 知识图谱和片段嵌入 90 | """ 91 | print("正在构建知识图谱...") 92 | 93 | # 创建一个图 94 | graph = nx.Graph() 95 | 96 | # 提取片段文本 97 | texts = [chunk["text"] for chunk in chunks] 98 | 99 | # 为所有片段创建嵌入 100 | print("正在为片段创建嵌入...") 101 | embeddings = create_embeddings(texts) 102 | 103 | # 将节点添加到图中 104 | print("正在将节点添加到图中...") 105 | for i, chunk in enumerate(chunks): 106 | # 从片段中提取概念 107 | print(f"正在从片段 {i + 1}/{len(chunks)} 中提取概念...") 108 | concepts = extract_concepts(chunk["text"]) 109 | 110 | # 添加带有属性的节点 111 | graph.add_node(i, 112 | text=chunk["text"], 113 | concepts=concepts, 114 | embedding=embeddings[i]) 115 | 116 | # 根据共享概念连接节点 117 | print("正在在节点之间创建边...") 118 | for i in range(len(chunks)): 119 | node_concepts = set(graph.nodes[i]["concepts"]) 120 | 121 | for j in range(i + 1, len(chunks)): 122 | # 计算概念重叠 123 | other_concepts = set(graph.nodes[j]["concepts"]) 124 | shared_concepts = node_concepts.intersection(other_concepts) # 两个节点之间交集 125 | 126 | # 如果它们共享概念,则添加一条边 127 | if shared_concepts: 128 | # 使用嵌入计算语义相似性 129 | similarity = np.dot(embeddings[i], embeddings[j]) / ( 130 | np.linalg.norm(embeddings[i]) * np.linalg.norm(embeddings[j])) 131 | 132 | # 根据概念重叠和语义相似性计算边权重 133 | concept_score = len(shared_concepts) / min(len(node_concepts), len(other_concepts)) 134 | edge_weight = 0.7 * similarity + 0.3 * concept_score 135 | 136 | # 仅添加具有显著关系的边 137 | if edge_weight > 0.6: 138 | graph.add_edge(i, j, 139 | weight=edge_weight, 140 | similarity=similarity, 141 | shared_concepts=list(shared_concepts)) 142 | 143 | print(f"知识图谱已构建,包含 {graph.number_of_nodes()} 个节点和 {graph.number_of_edges()} 条边") 144 | return graph, embeddings 145 | 146 | 147 | #################################### 148 | # 遍历知识图谱以查找与查询相关的信息:相似度排序、广度优先搜索 149 | #################################### 150 | def traverse_graph(query, graph, embeddings, top_k=5, max_depth=3): 151 | """ 152 | 遍历知识图谱以查找与查询相关的信息。 153 | 154 | Args: 155 | query (str): 用户的问题 156 | graph (nx.Graph): 知识图谱 157 | embeddings (List): 节点嵌入列表 158 | top_k (int): 考虑的初始节点数量 159 | max_depth (int): 最大遍历深度 160 | 161 | Returns: 162 | List[Dict]: 图遍历得到的相关信息 163 | """ 164 | print(f"正在为查询遍历图: {query}") 165 | 166 | # 获取查询的嵌入 167 | query_embedding = create_embeddings(query) 168 | 169 | # 计算查询与所有节点之间的相似度 170 | similarities = [] 171 | for i, node_embedding in enumerate(embeddings): 172 | similarity = np.dot(query_embedding, node_embedding) / ( 173 | np.linalg.norm(query_embedding) * np.linalg.norm(node_embedding)) 174 | similarities.append((i, similarity)) 175 | 176 | # 按相似度排序(降序) 177 | similarities.sort(key=lambda x: x[1], reverse=True) 178 | 179 | # 获取最相似的前 top-k 个节点作为起点 180 | starting_nodes = [node for node, _ in similarities[:top_k]] 181 | print(f"从 {len(starting_nodes)} 个节点开始遍历") 182 | 183 | # 初始化遍历 184 | visited = set() # 用于跟踪已访问节点的集合 185 | traversal_path = [] # 存储遍历路径的列表 186 | results = [] # 存储结果的列表 187 | 188 | # 使用优先队列进行遍历 189 | queue = [] 190 | for node in starting_nodes: 191 | heapq.heappush(queue, (-similarities[node][1], node)) # 负号用于最大堆 192 | 193 | # 使用修改后的基于优先级的广度优先搜索遍历图 194 | while queue and len(results) < (top_k * 3): # 将结果限制为 top_k * 3 195 | _, node = heapq.heappop(queue) 196 | 197 | if node in visited: 198 | continue 199 | 200 | # 标记为已访问 201 | visited.add(node) 202 | traversal_path.append(node) 203 | 204 | # 将当前节点的文本添加到结果中 205 | results.append({ 206 | "text": graph.nodes[node]["text"], 207 | "concepts": graph.nodes[node]["concepts"], 208 | "node_id": node 209 | }) 210 | 211 | # 如果尚未达到最大深度,则探索邻居 212 | if len(traversal_path) < max_depth: 213 | neighbors = [(neighbor, graph[node][neighbor]["weight"]) 214 | for neighbor in graph.neighbors(node) 215 | if neighbor not in visited] 216 | 217 | # 根据边权重将邻居添加到队列中 218 | for neighbor, weight in sorted(neighbors, key=lambda x: x[1], reverse=True): 219 | heapq.heappush(queue, (-weight, neighbor)) 220 | 221 | print(f"图遍历找到了 {len(results)} 个相关片段") 222 | return results, traversal_path 223 | 224 | 225 | def create_embeddings(texts): 226 | """ 227 | 为给定文本创建嵌入向量。 228 | 229 | Args: 230 | texts (List[str]): 输入文本列表 231 | model (str): 嵌入模型名称 232 | 233 | Returns: 234 | List[List[float]]: 嵌入向量列表 235 | """ 236 | # 处理空输入的情况 237 | if not texts: 238 | return [] 239 | 240 | # 分批次处理(OpenAI API 的限制) 241 | batch_size = 100 242 | all_embeddings = [] 243 | 244 | # 遍历输入文本,按批次生成嵌入 245 | for i in range(0, len(texts), batch_size): 246 | batch = texts[i:i + batch_size] # 获取当前批次的文本 247 | 248 | # 调用 OpenAI 接口生成嵌入 249 | response = client.embeddings.create( 250 | model=embedding_model, 251 | input=batch 252 | ) 253 | 254 | # 提取当前批次的嵌入向量 255 | batch_embeddings = [item.embedding for item in response.data] 256 | all_embeddings.extend(batch_embeddings) # 将当前批次的嵌入向量加入总列表 257 | 258 | return all_embeddings # 返回所有嵌入向量 259 | -------------------------------------------------------------------------------- /src/core/13_self_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | Self-RAG 核心函数 3 | """ 4 | import os 5 | import re 6 | from openai import OpenAI 7 | from dotenv import load_dotenv 8 | 9 | load_dotenv() 10 | 11 | client = OpenAI( 12 | base_url=os.getenv("LLM_BASE_URL"), 13 | api_key=os.getenv("LLM_API_KEY") 14 | ) 15 | llm_model = os.getenv("LLM_MODEL_ID") 16 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 17 | 18 | 19 | #################################### 20 | # Self-RAG 完整流程 21 | #################################### 22 | def self_rag(query, vector_store, top_k=3): 23 | """ 24 | 实现完整的Self-RAG流程。 25 | 26 | Args: 27 | query (str): 用户查询 28 | vector_store (SimpleVectorStore): 包含文档块的向量存储 29 | top_k (int): 初始检索的文档数量 30 | 31 | Returns: 32 | dict: 包括查询、响应和Self-RAG流程指标的结果 33 | """ 34 | print(f"\n=== 开始Self-RAG处理查询: {query} ===\n") 35 | 36 | # 第1步:确定是否需要检索 37 | print("第1步:确定是否需要检索...") 38 | retrieval_needed = determine_if_retrieval_needed(query) # 调用函数判断是否需要检索 39 | print(f"是否需要检索: {retrieval_needed}") 40 | 41 | # 初始化指标以跟踪Self-RAG流程 42 | metrics = { 43 | "retrieval_needed": retrieval_needed, # 是否需要检索 44 | "documents_retrieved": 0, # 检索到的文档数量 45 | "relevant_documents": 0, # 相关文档数量 46 | "response_support_ratings": [], # 回答支持的评级列表 47 | "utility_ratings": [] # 回答实用性评级列表 48 | } 49 | 50 | best_response = None # 最佳响应初始化为None 51 | best_score = -1 # 最佳分数初始化为-1 52 | 53 | if retrieval_needed: 54 | # 第2步:检索文档 55 | print("\n第2步:检索相关文档...") 56 | query_embedding = create_embeddings(query) # 创建查询的嵌入向量 57 | results = vector_store.similarity_search(query_embedding, k=top_k) # 搜索相似文档 58 | metrics["documents_retrieved"] = len(results) # 更新检索到的文档数量 59 | print(f"检索到 {len(results)} 个文档") 60 | 61 | # 第3步:评估每个文档的相关性 62 | print("\n第3步:评估文档相关性...") 63 | relevant_contexts = [] # 初始化相关上下文列表 64 | 65 | for i, result in enumerate(results): 66 | context = result["text"] # 提取文档内容 67 | relevance = evaluate_relevance(query, context) # 评估文档与查询的相关性 68 | print(f"文档 {i + 1} 相关性: {relevance}") 69 | 70 | if relevance == "relevant": # 如果文档相关,则添加到相关上下文列表 71 | relevant_contexts.append(context) 72 | 73 | metrics["relevant_documents"] = len(relevant_contexts) # 更新相关文档数量 74 | print(f"找到 {len(relevant_contexts)} 个相关文档") 75 | 76 | if relevant_contexts: 77 | # 第4步:处理每个相关上下文 78 | print("\n第4步:处理相关上下文...") 79 | for i, context in enumerate(relevant_contexts): 80 | print(f"\n处理上下文 {i + 1}/{len(relevant_contexts)}...") 81 | 82 | # 根据上下文生成响应 83 | print("生成响应...") 84 | response = generate_response(query, context) # 根据上下文生成响应 85 | 86 | # 评估响应对上下文的支持程度 87 | print("评估支持程度...") 88 | support_rating = assess_support(response, context) # 评估支持程度 89 | print(f"支持评级: {support_rating}") 90 | metrics["response_support_ratings"].append(support_rating) # 添加支持评级 91 | 92 | # 评估响应的实用性 93 | print("评估实用性...") 94 | utility_rating = rate_utility(query, response) # 评估实用性 95 | print(f"实用性评级: {utility_rating}/5") 96 | metrics["utility_ratings"].append(utility_rating) # 添加实用性评级 97 | 98 | # 计算总体评分(支持和实用性越高,评分越高) 99 | support_score = { 100 | "fully supported": 3, # 完全支持得分为3 101 | "partially supported": 1, # 部分支持得分为1 102 | "no support": 0 # 无支持得分为0 103 | }.get(support_rating, 0) 104 | 105 | overall_score = support_score * 5 + utility_rating # 计算总体评分 106 | print(f"总体评分: {overall_score}") 107 | 108 | # 跟踪最佳响应 109 | if overall_score > best_score: # 如果当前评分高于最佳评分,则更新最佳响应和评分 110 | best_response = response 111 | best_score = overall_score 112 | print("找到新的最佳响应!") 113 | 114 | # 如果没有找到相关上下文或所有响应评分较低 115 | if not relevant_contexts or best_score <= 0: 116 | print("\n未找到合适的上下文或响应评分较差,直接生成响应而不进行检索...") 117 | best_response = generate_response(query) # 不使用检索直接生成响应 118 | else: 119 | # 不需要检索,直接生成响应 120 | print("\n不需要检索,直接生成响应...") 121 | best_response = generate_response(query) # 不使用检索直接生成响应 122 | 123 | # 最终指标 124 | metrics["best_score"] = best_score # 更新最佳评分 125 | metrics["used_retrieval"] = retrieval_needed and best_score > 0 # 更新是否使用了检索 126 | 127 | print("\n=== Self-RAG完成 ===") 128 | 129 | return { 130 | "query": query, # 查询 131 | "response": best_response, # 最佳响应 132 | "metrics": metrics # 指标 133 | } 134 | 135 | 136 | #################################### 137 | # 检索决策:判断给定查询是否需要检索,事实性查询检索,观点类查询不检索 138 | #################################### 139 | def determine_if_retrieval_needed(query): 140 | """ 141 | 判断给定查询是否需要检索。 142 | 143 | Args: 144 | query (str): 用户查询 145 | 146 | Returns: 147 | bool: 如果需要检索,返回True;否则返回False 148 | """ 149 | # 系统提示,指导AI如何判断是否需要检索 150 | system_prompt = """你是一个判断查询是否需要检索的AI助手。 151 | 针对事实性问题、具体信息请求或关于事件、人物、概念的查询,回答"Yes"。 152 | 对于观点类、假设性场景或常识性简单查询,回答"No"。 153 | 仅回答"Yes"或"No"。""" 154 | 155 | # 包含查询的用户提示 156 | user_prompt = f"查询: {query}\n\n准确回答此查询是否需要检索?" 157 | 158 | # 使用模型生成响应 159 | response = client.chat.completions.create( 160 | model=llm_model, 161 | messages=[ 162 | {"role": "system", "content": system_prompt}, 163 | {"role": "user", "content": user_prompt} 164 | ], 165 | temperature=0 166 | ) 167 | 168 | # 从模型响应中提取答案并转换为小写 169 | answer = response.choices[0].message.content.strip().lower() 170 | 171 | # 如果答案包含“yes”,返回True;否则返回False 172 | return "yes" in answer 173 | 174 | 175 | #################################### 176 | # 相关性评估:评估文本块与查询的相关性 177 | #################################### 178 | def evaluate_relevance(query, context): 179 | """ 180 | 评估上下文与查询的相关性。 181 | 182 | Args: 183 | query (str): 用户查询 184 | context (str): 上下文文本 185 | 186 | Returns: 187 | str: 'relevant'(相关)或 'irrelevant'(不相关) 188 | """ 189 | # 系统提示,指导AI如何判断文档是否与查询相关 190 | system_prompt = """你是一个AI助手,任务是判断文档是否与查询相关。 191 | 判断文档中是否包含有助于回答查询的信息。 192 | 仅回答“Relevant”或“Irrelevant”。""" 193 | 194 | # 如果上下文过长以避免超出标记限制,则截断上下文 195 | max_context_length = 2000 196 | if len(context) > max_context_length: 197 | context = context[:max_context_length] + "... [truncated]" 198 | 199 | # 包含查询和文档内容的用户提示 200 | user_prompt = f"""查询: {query} 201 | 文档内容: 202 | {context} 203 | 204 | 该文档与查询相关?仅回答“Relevant”或“Irrelevant”。 205 | """ 206 | 207 | # 使用模型生成响应 208 | response = client.chat.completions.create( 209 | model=llm_model, 210 | messages=[ 211 | {"role": "system", "content": system_prompt}, 212 | {"role": "user", "content": user_prompt} 213 | ], 214 | temperature=0 215 | ) 216 | 217 | # 从模型响应中提取答案并转换为小写 218 | answer = response.choices[0].message.content.strip().lower() 219 | 220 | return answer # 返回相关性评估结果 221 | 222 | 223 | #################################### 224 | # 支持性评估:评估响应是否基于给定的上下文 225 | #################################### 226 | def assess_support(response, context): 227 | """ 228 | 评估响应在多大程度上得到上下文的支持。 229 | 230 | Args: 231 | response (str): 生成的响应 232 | context (str): 上下文文本 233 | 234 | Returns: 235 | str: 'fully supported'(完全支持)、'partially supported'(部分支持)或 'no support'(无支持) 236 | """ 237 | # 系统提示,指导AI如何评估支持情况 238 | system_prompt = """你是一个AI助手,任务是判断回答是否基于给定的上下文。 239 | 评估响应中的事实、主张和信息是否由上下文支持。 240 | 仅回答以下三个选项之一: 241 | - "Fully supported"(完全支持):回答所有信息均可从上下文直接得出。 242 | - "Partially supported"(部分支持):回答中的部分信息由上下文支持,但部分不是。 243 | - "No support"(无支持):回答中包含大量未在上下文中找到、提及或与上下文矛盾的信息。 244 | """ 245 | 246 | # 如果上下文过长以避免超出标记限制,则截断上下文 247 | max_context_length = 2000 248 | if len(context) > max_context_length: 249 | context = context[:max_context_length] + "... [truncated]" 250 | 251 | # 包含上下文和要评估的响应的用户提示 252 | user_prompt = f"""上下文: 253 | {context} 254 | 255 | 回答: 256 | {response} 257 | 258 | 该回答与上下文的支持程度如何?仅回答 "Fully supported"、"Partially supported"或 "No support"。 259 | """ 260 | 261 | # 使用模型生成响应 262 | response = client.chat.completions.create( 263 | model=llm_model, 264 | messages=[ 265 | {"role": "system", "content": system_prompt}, 266 | {"role": "user", "content": user_prompt} 267 | ], 268 | temperature=0 269 | ) 270 | 271 | # 从模型响应中提取答案并转换为小写 272 | answer = response.choices[0].message.content.strip().lower() 273 | 274 | return answer # 返回支持评估结果 275 | 276 | 277 | #################################### 278 | # 效用评估:评估生成的回答是否对查询的有用,打出分数 279 | #################################### 280 | def rate_utility(query, response): 281 | """ 282 | 评估响应对查询的实用性。 283 | 284 | Args: 285 | query (str): 用户查询 286 | response (str): 生成的响应 287 | 288 | Returns: 289 | int: 实用性评分,范围为1到5 290 | """ 291 | # 系统提示,指导AI如何评估响应的实用性 292 | system_prompt = """你是一个AI助手,任务是评估一个回答对查询的实用性。 293 | 从回答准确性、完整性、正确性和帮助性进行综合评分。 294 | 使用1-5级评分标准: 295 | - 1:毫无用处 296 | - 2:稍微有用 297 | - 3:中等有用 298 | - 4:非常有用 299 | - 5:极其有用 300 | 仅回答一个从1到5的单个数字,不要过多解释。""" 301 | 302 | # 包含查询和要评分的响应的用户提示 303 | user_prompt = f"""查询: {query} 304 | 回答: 305 | {response} 306 | 307 | 请用1到5分的评分评估该回答的效用,仅用一个1-5的数字评分。""" 308 | 309 | # 使用OpenAI客户端生成实用性评分 310 | response = client.chat.completions.create( 311 | model=llm_model, 312 | messages=[ 313 | {"role": "system", "content": system_prompt}, 314 | {"role": "user", "content": user_prompt} 315 | ], 316 | temperature=0 317 | ) 318 | 319 | # 从模型响应中提取评分 320 | rating = response.choices[0].message.content.strip() 321 | 322 | # 提取评分中的数字 323 | rating_match = re.search(r'[1-5]', rating) 324 | if rating_match: 325 | return int(rating_match.group()) # 返回提取的评分作为整数 326 | 327 | return 3 # 如果解析失败,默认返回中间评分 328 | 329 | 330 | def create_embeddings(text): 331 | """ 332 | 使用Embedding模型为给定文本创建嵌入向量。 333 | 334 | Args: 335 | text (str): 要创建嵌入向量的输入文本。 336 | 337 | Returns: 338 | List[float]: 嵌入向量。 339 | """ 340 | # 通过将字符串输入转换为列表来处理字符串和列表输入 341 | input_text = text if isinstance(text, list) else [text] 342 | 343 | # 使用指定的模型为输入文本创建嵌入向量 344 | response = client.embeddings.create( 345 | model=embedding_model, 346 | input=input_text 347 | ) 348 | 349 | # 如果输入是字符串,仅返回第一个嵌入向量 350 | if isinstance(text, str): 351 | return response.data[0].embedding 352 | 353 | # 否则,将所有嵌入向量作为向量列表返回 354 | return [item.embedding for item in response.data] 355 | 356 | 357 | def generate_response(query, context=None): 358 | """ 359 | 根据查询和可选的上下文生成响应。 360 | 361 | Args: 362 | query (str): 用户查询 363 | context (str, 可选): 上下文文本 364 | 365 | Returns: 366 | str: 生成的响应 367 | """ 368 | # 系统提示,指导AI如何生成有用的响应 369 | system_prompt = """你是一个有帮助的AI助手。请针对查询提供清晰、准确且信息丰富的回答。""" 370 | 371 | # 根据是否提供了上下文创建用户提示 372 | if context: 373 | user_prompt = f"""上下文: 374 | {context} 375 | 376 | 查询: {query} 377 | 378 | 请基于提供的上下文回答该查询。 379 | """ 380 | else: 381 | user_prompt = f"""查询: {query} 382 | 383 | 请尽你所能回答该查询。""" 384 | 385 | # 使用OpenAI客户端生成响应 386 | response = client.chat.completions.create( 387 | model=llm_model, 388 | messages=[ 389 | {"role": "system", "content": system_prompt}, 390 | {"role": "user", "content": user_prompt} 391 | ], 392 | temperature=0.2 393 | ) 394 | 395 | # 返回生成的响应文本 396 | return response.choices[0].message.content.strip() 397 | -------------------------------------------------------------------------------- /src/core/10_contextual_compression_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 上下文情境压缩(Contextual Compression) 核心函数 3 | """ 4 | import os 5 | import fitz 6 | import numpy as np 7 | from openai import OpenAI 8 | from dotenv import load_dotenv 9 | 10 | load_dotenv() 11 | 12 | client = OpenAI( 13 | base_url=os.getenv("LLM_BASE_URL"), 14 | api_key=os.getenv("LLM_API_KEY") 15 | ) 16 | llm_model = os.getenv("LLM_MODEL_ID") 17 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 18 | 19 | 20 | #################################### 21 | # 上下文压缩RAG完整流程 22 | #################################### 23 | def rag_with_compression(pdf_path, query, k=10, compression_type="selective"): 24 | """ 25 | 完整的RAG管道,包含上下文压缩。 26 | 27 | Args: 28 | pdf_path (str): PDF文档的路径 29 | query (str): 用户查询 30 | k (int): 初始检索的块数量 31 | compression_type (str): 压缩类型 32 | 33 | Returns: 34 | dict: 包括查询、压缩块和响应的结果 35 | """ 36 | print("\n=== RAG WITH CONTEXTUAL COMPRESSION ===") 37 | print(f"Query: {query}") 38 | print(f"Compression type: {compression_type}") 39 | 40 | # 处理文档以提取文本、分块并创建嵌入 41 | vector_store = process_document(pdf_path) 42 | 43 | # 为查询创建嵌入 44 | query_embedding = create_embeddings(query) 45 | 46 | # 根据查询嵌入检索最相似的前k个块 47 | print(f"Retrieving top {k} chunks...") 48 | results = vector_store.similarity_search(query_embedding, k=k) 49 | retrieved_chunks = [result["text"] for result in results] 50 | 51 | # 对检索到的块应用压缩 52 | compressed_results = batch_compress_chunks(retrieved_chunks, query, compression_type) 53 | compressed_chunks = [result[0] for result in compressed_results] 54 | compression_ratios = [result[1] for result in compressed_results] 55 | 56 | # 过滤掉任何空的压缩块 57 | filtered_chunks = [(chunk, ratio) for chunk, ratio in zip(compressed_chunks, compression_ratios) if chunk.strip()] 58 | 59 | if not filtered_chunks: 60 | # 如果所有块都被压缩为空字符串,则使用原始块 61 | print("Warning: All chunks were compressed to empty strings. Using original chunks.") 62 | filtered_chunks = [(chunk, 0.0) for chunk in retrieved_chunks] 63 | else: 64 | compressed_chunks, compression_ratios = zip(*filtered_chunks) 65 | 66 | # 从压缩块生成上下文 67 | context = "\n\n---\n\n".join(compressed_chunks) 68 | 69 | # 基于压缩块生成响应 70 | print("Generating response based on compressed chunks...") 71 | response = generate_response(query, context) 72 | 73 | # 准备结果字典 74 | result = { 75 | "query": query, 76 | "original_chunks": retrieved_chunks, 77 | "compressed_chunks": compressed_chunks, 78 | "compression_ratios": compression_ratios, 79 | "context_length_reduction": f"{sum(compression_ratios) / len(compression_ratios):.2f}%", 80 | "response": response 81 | } 82 | 83 | print("\n=== RESPONSE ===") 84 | print(response) 85 | 86 | return result 87 | 88 | 89 | #################################### 90 | # 压缩检索到的文本块:selective、summary、extraction 91 | #################################### 92 | def compress_chunk(chunk, query, compression_type="selective"): 93 | """ 94 | 压缩检索到的文本块,仅保留与查询相关的内容。 95 | 96 | Args: 97 | chunk (str): 要压缩的文本块 98 | query (str): 用户查询 99 | compression_type (str): 压缩类型 ("selective", "summary" 或 "extraction") 100 | 101 | Returns: 102 | str: 压缩后的文本块 103 | """ 104 | # 为不同的压缩方法定义系统提示 105 | if compression_type == "selective": 106 | system_prompt = """您是专业信息过滤专家。 107 | 您的任务是分析文档块并仅提取与用户查询直接相关的句子或段落,移除所有无关内容。 108 | 109 | 输出要求: 110 | 1. 仅保留有助于回答查询的文本 111 | 2. 保持相关句子的原始措辞(禁止改写) 112 | 3. 维持文本的原始顺序 113 | 4. 包含所有相关文本(即使存在重复) 114 | 5. 排除任何与查询无关的文本 115 | 116 | 请以纯文本格式输出,不添加任何注释。""" 117 | 118 | elif compression_type == "summary": 119 | system_prompt = """您是专业摘要生成专家。 120 | 您的任务是创建文档块的简洁摘要,且仅聚焦与用户查询相关的信息。 121 | 122 | 输出要求: 123 | 1. 保持简明扼要但涵盖所有相关要素 124 | 2. 仅聚焦与查询直接相关的信息 125 | 3. 省略无关细节 126 | 4. 使用中立、客观的陈述语气 127 | 128 | 请以纯文本格式输出,不添加任何注释。""" 129 | 130 | else: # extraction 131 | system_prompt = """您是精准信息提取专家。 132 | 您的任务是从文档块中精确提取与用户查询相关的完整句子。 133 | 134 | 输出要求: 135 | 1. 仅包含原始文本中的直接引用 136 | 2. 严格保持原始文本的措辞(禁止修改) 137 | 3. 仅选择与查询直接相关的完整句子 138 | 4. 不同句子使用换行符分隔 139 | 5. 不添加任何解释性文字 140 | 141 | 请以纯文本格式输出,不添加任何注释。""" 142 | 143 | # 定义带有查询和文档块的用户提示 144 | user_prompt = f""" 145 | 查询: {query} 146 | 147 | 文档块: 148 | {chunk} 149 | 150 | 请严格提取与本查询相关的核心内容。 151 | """ 152 | 153 | # 使用 OpenAI API 生成响应 154 | response = client.chat.completions.create( 155 | model=llm_model, 156 | messages=[ 157 | {"role": "system", "content": system_prompt}, 158 | {"role": "user", "content": user_prompt} 159 | ], 160 | temperature=0 161 | ) 162 | 163 | # 从响应中提取压缩后的文本块 164 | compressed_chunk = response.choices[0].message.content.strip() 165 | 166 | # 计算压缩比率 167 | original_length = len(chunk) 168 | compressed_length = len(compressed_chunk) 169 | compression_ratio = (original_length - compressed_length) / original_length * 100 170 | 171 | return compressed_chunk, compression_ratio 172 | 173 | 174 | #################################### 175 | # 批量压缩检索到的文本块 176 | #################################### 177 | def batch_compress_chunks(chunks, query, compression_type="selective"): 178 | """ 179 | 逐个压缩多个文本块。 180 | 181 | Args: 182 | chunks (List[str]): 要压缩的文本块列表 183 | query (str): 用户查询 184 | compression_type (str): 压缩类型 ("selective", "summary", 或 "extraction") 185 | 186 | Returns: 187 | List[Tuple[str, float]]: 包含压缩比率的压缩文本块列表 188 | """ 189 | print(f"正在压缩 {len(chunks)} 个文本块...") # 打印将要压缩的文本块数量 190 | results = [] # 初始化一个空列表以存储结果 191 | total_original_length = 0 # 初始化变量以存储所有文本块的原始总长度 192 | total_compressed_length = 0 # 初始化变量以存储所有文本块的压缩后总长度 193 | 194 | # 遍历每个文本块 195 | for i, chunk in enumerate(chunks): 196 | print(f"正在压缩文本块 {i + 1}/{len(chunks)}...") # 打印压缩进度 197 | # 压缩文本块并获取压缩后的文本块和压缩比率 198 | compressed_chunk, compression_ratio = compress_chunk(chunk, query, compression_type) 199 | results.append((compressed_chunk, compression_ratio)) # 将结果添加到结果列表中 200 | 201 | total_original_length += len(chunk) # 将原始文本块的长度加到总原始长度中 202 | total_compressed_length += len(compressed_chunk) # 将压缩后文本块的长度加到总压缩长度中 203 | 204 | # 计算总体压缩比率 205 | overall_ratio = (total_original_length - total_compressed_length) / total_original_length * 100 206 | print(f"总体压缩比率: {overall_ratio:.2f}%") # 打印总体压缩比率 207 | 208 | return results # 返回包含压缩文本块和压缩比率的列表 209 | 210 | 211 | def generate_response(query, context): 212 | """ 213 | 根据查询和上下文生成响应。 214 | 215 | Args: 216 | query (str): 用户查询 217 | context (str): 从压缩块中提取的上下文文本 218 | 219 | Returns: 220 | str: 生成的响应 221 | """ 222 | # 定义系统提示以指导AI的行为 223 | system_prompt = "您是一个乐于助人的AI助手。请仅根据提供的上下文来回答用户的问题。如果在上下文中找不到答案,请直接说'没有足够的信息'。" 224 | 225 | # 通过组合上下文和查询创建用户提示 226 | user_prompt = f""" 227 | 上下文: 228 | {context} 229 | 230 | 问题: {query} 231 | 232 | 请基于上述上下文内容提供一个全面详尽的答案。 233 | """ 234 | 235 | # 使用OpenAI API生成响应 236 | response = client.chat.completions.create( 237 | model=llm_model, 238 | messages=[ 239 | {"role": "system", "content": system_prompt}, 240 | {"role": "user", "content": user_prompt} 241 | ], 242 | temperature=0 243 | ) 244 | 245 | # 返回生成的响应内容 246 | return response.choices[0].message.content 247 | 248 | 249 | def process_document(pdf_path, chunk_size=1000, chunk_overlap=200): 250 | """ 251 | 为RAG处理文档。 252 | 253 | Args: 254 | pdf_path (str): PDF文件的路径。 255 | chunk_size (int): 每个文本块的大小(以字符为单位)。 256 | chunk_overlap (int): 文本块之间的重叠大小(以字符为单位)。 257 | 258 | Returns: 259 | SimpleVectorStore: 包含文档文本块及其嵌入向量的向量存储。 260 | """ 261 | print("从PDF中提取文本...") 262 | extracted_text = extract_text_from_pdf(pdf_path) # 调用函数提取PDF中的文本 263 | 264 | print("分割文本...") 265 | chunks = chunk_text(extracted_text, chunk_size, chunk_overlap) # 将提取的文本分割为多个块 266 | print(f"创建了 {len(chunks)} 个文本块") 267 | 268 | print("为文本块创建嵌入向量...") 269 | # 为了提高效率,一次性为所有文本块创建嵌入向量 270 | chunk_embeddings = create_embeddings(chunks) 271 | 272 | # 创建向量存储 273 | store = SimpleVectorStore() 274 | 275 | # 将文本块添加到向量存储中 276 | for i, (chunk, embedding) in enumerate(zip(chunks, chunk_embeddings)): 277 | store.add_item( 278 | text=chunk, # 文本内容 279 | embedding=embedding, # 嵌入向量 280 | metadata={"index": i, "source": pdf_path} # 元数据,包括索引和源文件路径 281 | ) 282 | 283 | print(f"向向量存储中添加了 {len(chunks)} 个文本块") 284 | return store 285 | 286 | 287 | def create_embeddings(text): 288 | """ 289 | 使用Embedding模型为给定文本创建嵌入向量。 290 | 291 | Args: 292 | text (str): 要创建嵌入向量的输入文本。 293 | 294 | Returns: 295 | List[float]: 嵌入向量。 296 | """ 297 | # 通过将字符串输入转换为列表来处理字符串和列表输入 298 | input_text = text if isinstance(text, list) else [text] 299 | 300 | # 使用指定的模型为输入文本创建嵌入向量 301 | response = client.embeddings.create( 302 | model=embedding_model, 303 | input=input_text 304 | ) 305 | 306 | # 如果输入是字符串,仅返回第一个嵌入向量 307 | if isinstance(text, str): 308 | return response.data[0].embedding 309 | 310 | # 否则,将所有嵌入向量作为向量列表返回 311 | return [item.embedding for item in response.data] 312 | 313 | 314 | def extract_text_from_pdf(pdf_path): 315 | """ 316 | 从 PDF 文件中提取文本,并打印前 `num_chars` 个字符。 317 | 318 | Args: 319 | pdf_path (str): Path to the PDF file. 320 | 321 | Returns: 322 | str: Extracted text from the PDF. 323 | """ 324 | # 打开 PDF 文件 325 | mypdf = fitz.open(pdf_path) 326 | all_text = "" # 初始化一个空字符串以存储提取的文本 327 | 328 | # Iterate through each page in the PDF 329 | for page_num in range(mypdf.page_count): 330 | page = mypdf[page_num] 331 | text = page.get_text("text") # 从页面中提取文本 332 | all_text += text # 将提取的文本追加到 all_text 字符串中 333 | 334 | return all_text # 返回提取的文本 335 | 336 | 337 | def chunk_text(text, n, overlap): 338 | """ 339 | 将文本分割为重叠的块 340 | 341 | Args: 342 | text (str): 要分割的文本 343 | n (int): 每个块的字符数 344 | overlap (int): 块之间的重叠字符数 345 | 346 | Returns: 347 | List[str]: 文本块列表 348 | """ 349 | chunks = [] # 350 | for i in range(0, len(text), n - overlap): 351 | # 添加从当前索引到索引 + 块大小的文本块 352 | chunk = text[i:i + n] 353 | if chunk: 354 | chunks.append(chunk) 355 | 356 | return chunks # Return the list of text chunks 357 | 358 | 359 | class SimpleVectorStore: 360 | """ 361 | 使用NumPy实现的简单向量存储。 362 | """ 363 | 364 | def __init__(self): 365 | """ 366 | 初始化向量存储。 367 | """ 368 | self.vectors = [] # 用于存储嵌入向量的列表 369 | self.texts = [] # 用于存储原始文本的列表 370 | self.metadata = [] # 用于存储每个文本元数据的列表 371 | 372 | def add_item(self, text, embedding, metadata=None): 373 | """ 374 | 向向量存储中添加一个项目。 375 | 376 | Args: 377 | text (str): 原始文本。 378 | embedding (List[float]): 嵌入向量。 379 | metadata (dict, 可选): 额外的元数据。 380 | """ 381 | self.vectors.append(np.array(embedding)) # 将嵌入转换为numpy数组并添加到向量列表中 382 | self.texts.append(text) # 将原始文本添加到文本列表中 383 | self.metadata.append(metadata or {}) # 添加元数据到元数据列表中,如果没有提供则使用空字典 384 | 385 | def similarity_search(self, query_embedding, k=5): 386 | """ 387 | 查找与查询嵌入最相似的项目。 388 | 389 | Args: 390 | query_embedding (List[float]): 查询嵌入向量。 391 | k (int): 返回的结果数量。 392 | 393 | Returns: 394 | List[Dict]: 包含文本和元数据的前k个最相似项。 395 | """ 396 | if not self.vectors: 397 | return [] # 如果没有存储向量,则返回空列表 398 | 399 | # 将查询嵌入转换为numpy数组 400 | query_vector = np.array(query_embedding) 401 | 402 | # 使用余弦相似度计算相似度 403 | similarities = [] 404 | for i, vector in enumerate(self.vectors): 405 | # 计算查询向量与存储向量之间的余弦相似度 406 | similarity = np.dot(query_vector, vector) / (np.linalg.norm(query_vector) * np.linalg.norm(vector)) 407 | similarities.append((i, similarity)) # 添加索引和相似度分数 408 | 409 | # 按相似度排序(降序) 410 | similarities.sort(key=lambda x: x[1], reverse=True) 411 | 412 | # 返回前k个结果 413 | results = [] 414 | for i in range(min(k, len(similarities))): 415 | idx, score = similarities[i] 416 | results.append({ 417 | "text": self.texts[idx], # 添加对应的文本 418 | "metadata": self.metadata[idx], # 添加对应的元数据 419 | "similarity": score # 添加相似度分数 420 | }) 421 | 422 | return results # 返回前k个最相似项的列表 423 | -------------------------------------------------------------------------------- /src/core/09_rse_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 关联段落提取(RSE) 核心函数 3 | """ 4 | import os 5 | import fitz 6 | import numpy as np 7 | from openai import OpenAI 8 | from dotenv import load_dotenv 9 | 10 | load_dotenv() 11 | 12 | client = OpenAI( 13 | base_url=os.getenv("LLM_BASE_URL"), 14 | api_key=os.getenv("LLM_API_KEY") 15 | ) 16 | llm_model = os.getenv("LLM_MODEL_ID") 17 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 18 | 19 | 20 | #################################### 21 | # 关联段落提取(RSE)的完整流程 22 | #################################### 23 | def rag_with_rse(pdf_path, query, chunk_size=800, irrelevant_chunk_penalty=0.2): 24 | """ 25 | 完整的RAG管道,包含相关段落提取(Relevant Segment Extraction)。 26 | 27 | Args: 28 | pdf_path (str): 文档路径 29 | query (str): 用户查询 30 | chunk_size (int): 每个块的大小 31 | irrelevant_chunk_penalty (float): 对不相关块的惩罚值 32 | 33 | Returns: 34 | Dict: 包含查询、段落和响应的结果字典 35 | """ 36 | print("\n=== 开始带有相关段落提取的RAG ===") 37 | print(f"查询: {query}") 38 | 39 | # 处理文档以提取文本、分块并创建嵌入 40 | chunks, vector_store, doc_info = process_document(pdf_path, chunk_size) 41 | 42 | # 根据查询计算相关性分数和块值 43 | print("\n计算相关性分数和块值...") 44 | chunk_values = calculate_chunk_values(query, chunks, vector_store, irrelevant_chunk_penalty) 45 | 46 | # 根据块值找到最佳文本段落 47 | best_segments, scores = find_best_segments( 48 | chunk_values, 49 | max_segment_length=20, # 最大段落长度 50 | total_max_length=30, # 所有段落的最大总长度 51 | min_segment_value=0.2 # 考虑段落的最小值 52 | ) 53 | 54 | # 从最佳块中重建文本段落 55 | print("\n从块中重建文本段落...") 56 | segments = reconstruct_segments(chunks, best_segments) 57 | 58 | # 将段落格式化为语言模型的上下文字符串 59 | context = format_segments_for_context(segments) 60 | 61 | # 使用上下文从语言模型生成响应 62 | response = generate_response(query, context) 63 | 64 | # 将结果编译成字典 65 | result = { 66 | "query": query, # 用户查询 67 | "segments": segments, # 提取的段落 68 | "response": response # 模型生成的响应 69 | } 70 | 71 | print("\n=== 最终响应 ===") 72 | print(response) 73 | 74 | return result 75 | 76 | 77 | #################################### 78 | # 根据查询计算相关性分数和块值 79 | # 先获取所有带有相似度分数的块 80 | # 获取相关性分数,如果不在结果中则默认为0,同时应用惩罚以将不相关的块转换为负值 81 | #################################### 82 | def calculate_chunk_values(query, chunks, vector_store, irrelevant_chunk_penalty=0.2): 83 | """ 84 | 通过结合相关性和位置计算块的值。 85 | 86 | Args: 87 | query (str): 查询文本 88 | chunks (List[str]): 文档块列表 89 | vector_store (SimpleVectorStore): 包含块的向量存储 90 | irrelevant_chunk_penalty (float): 不相关块的惩罚值 91 | 92 | Returns: 93 | List[float]: 块值列表 94 | """ 95 | # 创建查询嵌入 96 | query_embedding = create_embeddings([query])[0] 97 | 98 | # 获取所有带有相似度分数的块 99 | num_chunks = len(chunks) 100 | results = vector_store.search(query_embedding, top_k=num_chunks) 101 | 102 | # 创建从块索引到相关性分数的映射 103 | relevance_scores = {result["metadata"]["chunk_index"]: result["score"] for result in results} 104 | 105 | # 计算块值(相关性分数减去惩罚) 106 | chunk_values = [] 107 | for i in range(num_chunks): 108 | # 获取相关性分数,如果不在结果中则默认为0 109 | score = relevance_scores.get(i, 0.0) 110 | # 应用惩罚以将不相关的块转换为负值 111 | value = score - irrelevant_chunk_penalty 112 | chunk_values.append(value) 113 | 114 | return chunk_values 115 | 116 | 117 | #################################### 118 | # 根据块值找到最佳文本块 119 | #################################### 120 | def find_best_segments(chunk_values, max_segment_length=20, total_max_length=30, min_segment_value=0.2): 121 | """ 122 | 使用最大子数组和算法的变体找到最佳段落。 123 | 124 | Args: 125 | chunk_values (List[float]): 每个块的值 126 | max_segment_length (int): 单个段落的最大长度 127 | total_max_length (int): 所有段落的最大总长度 128 | min_segment_value (float): 被考虑的段落的最小值 129 | 130 | Returns: 131 | List[Tuple[int, int]]: 最佳段落的(开始,结束)索引列表 132 | """ 133 | print("寻找最佳连续文本段落...") 134 | 135 | best_segments = [] 136 | segment_scores = [] 137 | total_included_chunks = 0 138 | 139 | # 继续寻找段落直到达到限制 140 | while total_included_chunks < total_max_length: 141 | best_score = min_segment_value # 段落的最低阈值 142 | best_segment = None 143 | 144 | # 尝试每个可能的起始位置 145 | for start in range(len(chunk_values)): 146 | # 如果该起始位置已经在选定的段落中,则跳过(重叠内容部分) 147 | if any(start >= s[0] and start < s[1] for s in best_segments): 148 | continue 149 | 150 | # 尝试每个可能的段落长度 151 | for length in range(1, min(max_segment_length, len(chunk_values) - start) + 1): 152 | end = start + length 153 | 154 | # 如果结束位置已经在选定的段落中,则跳过 155 | if any(end > s[0] and end <= s[1] for s in best_segments): 156 | continue 157 | 158 | # 计算段落值为块值的总和 159 | segment_value = sum(chunk_values[start:end]) 160 | 161 | # 如果这个段落更好,则更新最佳段落 162 | if segment_value > best_score: 163 | best_score = segment_value 164 | best_segment = (start, end) 165 | 166 | # 如果找到了一个好的段落,则添加它 167 | if best_segment: 168 | best_segments.append(best_segment) 169 | segment_scores.append(best_score) 170 | total_included_chunks += best_segment[1] - best_segment[0] 171 | print(f"找到段落 {best_segment},得分 {best_score:.4f}") 172 | else: 173 | # 没有更多的好段落可找 174 | break 175 | 176 | # 按段落的起始位置排序以便于阅读 177 | best_segments = sorted(best_segments, key=lambda x: x[0]) 178 | 179 | return best_segments, segment_scores 180 | 181 | 182 | #################################### 183 | # 从最佳块中重建文本段落 184 | #################################### 185 | def reconstruct_segments(chunks, best_segments): 186 | """ 187 | 基于块索引重建文本段落。 188 | 189 | Args: 190 | chunks (List[str]): 所有文档块的列表 191 | best_segments (List[Tuple[int, int]]): 段落的(开始,结束)索引列表 192 | 193 | Returns: 194 | List[Dict]: 重建的文本段落列表,每个段落包含文本和其范围 195 | """ 196 | reconstructed_segments = [] # 初始化一个空列表以存储重建的段落 197 | 198 | for start, end in best_segments: 199 | # 将此段落中的块连接起来以形成完整的段落文本 200 | segment_text = " ".join(chunks[start:end]) 201 | # 将段落文本及其范围追加到重建的段落列表中 202 | reconstructed_segments.append({ 203 | "text": segment_text, # 段落文本 204 | "segment_range": (start, end), # 段落范围 205 | }) 206 | 207 | return reconstructed_segments # 返回重建的文本段落列表 208 | 209 | 210 | #################################### 211 | # 将段落进行格式化 212 | #################################### 213 | def format_segments_for_context(segments): 214 | """ 215 | 将段落格式化为适用于LLM的上下文字符串。 216 | 217 | Args: 218 | segments (List[Dict]): 段落字典列表 219 | 220 | Returns: 221 | str: 格式化后的上下文文本 222 | """ 223 | context = [] # 初始化一个空列表以存储格式化后的上下文 224 | 225 | for i, segment in enumerate(segments): 226 | # 为每个段落创建一个包含索引和块范围的标题 227 | # segment_header = f"SEGMENT {i+1} (Chunks {segment['segment_range'][0]}-{segment['segment_range'][1]-1}):" 228 | segment_header = f"分段{i + 1}(包含文本块{segment['segment_range'][0]}至{segment['segment_range'][1] - 1}):" 229 | context.append(segment_header) # 将段落标题添加到上下文列表中 230 | context.append(segment['text']) # 将段落文本添加到上下文列表中 231 | context.append("-" * 80) # 添加分隔线以提高可读性 232 | 233 | # 将上下文列表中的所有元素用双换行符连接并返回结果 234 | return "\n\n".join(context) 235 | 236 | 237 | def generate_response(query, context): 238 | """ 239 | 根据查询和上下文生成响应。 240 | 241 | Args: 242 | query (str): 用户查询 243 | context (str): 来自相关段落的上下文文本 244 | 245 | Returns: 246 | str: 生成的响应 247 | """ 248 | print("正在使用相关段落作为上下文生成响应...") 249 | 250 | # 定义系统提示以引导AI的行为 251 | system_prompt = """ 252 | 您是基于上下文智能应答的AI助手,需根据提供的文档段落回答用户问题。 253 | 这些文档段落是通过相关性检索匹配到当前问题的上下文内容。 254 | 请严格依据以下要求执行: 255 | 1. 整合分析所有相关段落信息 256 | 2. 生成全面准确的综合回答 257 | 3. 当上下文不包含有效信息时,必须明确告知无法回答 258 | """ 259 | 260 | # 通过组合上下文和查询创建用户提示 261 | user_prompt = f""" 262 | 上下文内容: 263 | {context} 264 | 265 | 问题:{query} 266 | 267 | 请基于上述上下文内容提供专业可靠的回答。 268 | """ 269 | 270 | # 使用指定的模型生成响应 271 | response = client.chat.completions.create( 272 | model=llm_model, 273 | messages=[ 274 | {"role": "system", "content": system_prompt}, 275 | {"role": "user", "content": user_prompt} 276 | ], 277 | temperature=0 278 | ) 279 | 280 | # 返回生成的响应内容 281 | return response.choices[0].message.content 282 | 283 | 284 | def process_document(pdf_path, chunk_size=800): 285 | """ 286 | 处理文档以供 RSE 使用。 287 | 288 | Args: 289 | pdf_path (str): PDF 文档的路径 290 | chunk_size (int): 每个块的字符大小 291 | 292 | Returns: 293 | Tuple[List[str], SimpleVectorStore, Dict]: 块列表、向量存储和文档信息 294 | """ 295 | print("从文档中提取文本...") 296 | # 从 PDF 文件中提取文本 297 | text = extract_text_from_pdf(pdf_path) 298 | 299 | print("将文本切分为非重叠段落...") 300 | # 将提取的文本切分为非重叠段落 301 | chunks = chunk_text(text, n=chunk_size, overlap=0) 302 | print(f"创建了 {len(chunks)} 个块") 303 | 304 | print("为块生成嵌入向量...") 305 | # 为文本块生成嵌入向量 306 | chunk_embeddings = create_embeddings(chunks) 307 | 308 | # 创建 SimpleVectorStore 的实例 309 | vector_store = SimpleVectorStore() 310 | 311 | # 添加带有元数据的文档(包括块索引以便后续重建) 312 | metadata = [{"chunk_index": i, "source": pdf_path} for i in range(len(chunks))] 313 | vector_store.add_documents(chunks, chunk_embeddings, metadata) 314 | 315 | # 跟踪原始文档结构以便段落重建 316 | doc_info = { 317 | "chunks": chunks, 318 | "source": pdf_path, 319 | } 320 | 321 | return chunks, vector_store, doc_info 322 | 323 | 324 | def extract_text_from_pdf(pdf_path): 325 | """ 326 | 从 PDF 文件中提取文本,并打印前 `num_chars` 个字符。 327 | 328 | Args: 329 | pdf_path (str): Path to the PDF file. 330 | 331 | Returns: 332 | str: Extracted text from the PDF. 333 | """ 334 | # 打开 PDF 文件 335 | mypdf = fitz.open(pdf_path) 336 | all_text = "" # 初始化一个空字符串以存储提取的文本 337 | 338 | # Iterate through each page in the PDF 339 | for page_num in range(mypdf.page_count): 340 | page = mypdf[page_num] 341 | text = page.get_text("text") # 从页面中提取文本 342 | all_text += text # 将提取的文本追加到 all_text 字符串中 343 | 344 | return all_text # 返回提取的文本 345 | 346 | 347 | def chunk_text(text, n, overlap): 348 | """ 349 | 将文本分割为重叠的块 350 | 351 | Args: 352 | text (str): 要分割的文本 353 | n (int): 每个块的字符数 354 | overlap (int): 块之间的重叠字符数 355 | 356 | Returns: 357 | List[str]: 文本块列表 358 | """ 359 | chunks = [] # 360 | for i in range(0, len(text), n - overlap): 361 | # 添加从当前索引到索引 + 块大小的文本块 362 | chunk = text[i:i + n] 363 | if chunk: 364 | chunks.append(chunk) 365 | 366 | return chunks # Return the list of text chunks 367 | 368 | 369 | class SimpleVectorStore: 370 | """ 371 | 使用 NumPy 实现的轻量级向量存储。 372 | """ 373 | 374 | def __init__(self, dimension=1536): 375 | """ 376 | 初始化向量存储。 377 | 378 | Args: 379 | dimension (int): 嵌入向量的维度 380 | """ 381 | self.dimension = dimension # 嵌入向量的维度 382 | self.vectors = [] # 存储嵌入向量的列表 383 | self.documents = [] # 存储文档片段的列表 384 | self.metadata = [] # 存储元数据的列表 385 | 386 | def add_documents(self, documents, vectors=None, metadata=None): 387 | """ 388 | 向向量存储中添加文档。 389 | 390 | Args: 391 | documents (List[str]): 文档片段列表 392 | vectors (List[List[float]], 可选): 嵌入向量列表 393 | metadata (List[Dict], 可选): 元数据字典列表 394 | """ 395 | if vectors is None: # 如果未提供向量,则生成一个空列表 396 | vectors = [None] * len(documents) 397 | 398 | if metadata is None: # 如果未提供元数据,则生成一个空字典列表 399 | metadata = [{} for _ in range(len(documents))] 400 | 401 | for doc, vec, meta in zip(documents, vectors, metadata): # 遍历文档、向量和元数据 402 | self.documents.append(doc) # 将文档片段添加到列表中 403 | self.vectors.append(vec) # 将嵌入向量添加到列表中 404 | self.metadata.append(meta) # 将元数据添加到列表中 405 | 406 | def search(self, query_vector, top_k=5): 407 | """ 408 | 搜索最相似的文档。 409 | 410 | Args: 411 | query_vector (List[float]): 查询嵌入向量 412 | top_k (int): 返回的结果数量 413 | 414 | Returns: 415 | List[Dict]: 包含文档、分数和元数据的结果列表 416 | """ 417 | if not self.vectors or not self.documents: # 如果向量或文档为空,返回空列表 418 | return [] 419 | 420 | # 将查询向量转换为 NumPy 数组 421 | query_array = np.array(query_vector) 422 | 423 | # 计算相似度 424 | similarities = [] 425 | for i, vector in enumerate(self.vectors): # 遍历存储中的向量 426 | if vector is not None: # 如果向量不为空 427 | # 计算余弦相似度 428 | similarity = np.dot(query_array, vector) / ( 429 | np.linalg.norm(query_array) * np.linalg.norm(vector) 430 | ) 431 | similarities.append((i, similarity)) # 将索引和相似度添加到列表中 432 | 433 | # 按相似度降序排序 434 | similarities.sort(key=lambda x: x[1], reverse=True) 435 | 436 | # 获取前 top-k 结果 437 | results = [] 438 | for i, score in similarities[:top_k]: # 遍历前 top-k 的结果 439 | results.append({ 440 | "document": self.documents[i], # 文档片段 441 | "score": float(score), # 相似度分数 442 | "metadata": self.metadata[i] # 元数据 443 | }) 444 | 445 | return results # 返回结果列表 446 | 447 | 448 | def create_embeddings(texts): 449 | """ 450 | 为文本生成嵌入向量。 451 | 452 | Args: 453 | texts (List[str]): 要嵌入的文本列表 454 | 455 | Returns: 456 | List[List[float]]: 嵌入向量列表 457 | """ 458 | if not texts: # 如果没有提供文本,返回空列表 459 | return [] 460 | 461 | # 如果列表很长,则按批次处理 462 | batch_size = 100 # 根据API限制进行调整 463 | all_embeddings = [] # 初始化一个列表来存储所有嵌入向量 464 | 465 | for i in range(0, len(texts), batch_size): # 按批次处理文本 466 | batch = texts[i:i + batch_size] # 获取当前批次的文本 467 | 468 | # 使用指定的模型为当前批次生成嵌入向量 469 | response = client.embeddings.create( 470 | input=batch, # 输入文本批次 471 | model=embedding_model # 使用的模型 472 | ) 473 | 474 | # 从响应中提取嵌入向量 475 | batch_embeddings = [item.embedding for item in response.data] 476 | all_embeddings.extend(batch_embeddings) # 将批次嵌入向量添加到总列表中 477 | 478 | return all_embeddings # 返回所有嵌入向量的列表 479 | -------------------------------------------------------------------------------- /src/core/07_query_transform_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 查询转换 核心函数 3 | """ 4 | import os 5 | import re 6 | import fitz 7 | import numpy as np 8 | from openai import OpenAI 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | client = OpenAI( 14 | base_url=os.getenv("LLM_BASE_URL"), 15 | api_key=os.getenv("LLM_API_KEY") 16 | ) 17 | llm_model = os.getenv("LLM_MODEL_ID") 18 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 19 | 20 | 21 | #################################### 22 | # 查询转换完整流程 23 | #################################### 24 | def rag_with_query_transformation(pdf_path, query, transformation_type=None): 25 | """ 26 | 运行完整的RAG管道,并可选地进行查询转换。 27 | 28 | Args: 29 | pdf_path (str): PDF文档的路径 30 | query (str): 用户查询 31 | transformation_type (str): 转换类型(None、'rewrite'、'step_back' 或 'decompose') 32 | 33 | Returns: 34 | Dict: 包括原始查询、转换后的查询、上下文和回答的结果 35 | """ 36 | # 处理文档以创建向量存储:从PDF 中提取文本,分割文本块并创建向量存储 37 | vector_store = process_document(pdf_path) 38 | 39 | # 应用查询转换并搜索 40 | if transformation_type: 41 | # 使用转换后的查询进行搜索 42 | results = transformed_search(query, vector_store, transformation_type) 43 | else: 44 | # 不进行转换,执行常规搜索 45 | query_embedding = create_embeddings(query) 46 | results = vector_store.similarity_search(query_embedding, k=3) 47 | 48 | # 从搜索结果中组合上下文 49 | context = "\n\n".join([f"段落 {i + 1}:\n{result['text']}" for i, result in enumerate(results)]) 50 | 51 | # 根据查询和组合后的上下文生成响应 52 | response = generate_response(query, context) 53 | 54 | # 返回结果,包括原始查询、转换类型、上下文和响应 55 | return { 56 | "original_query": query, 57 | "transformation_type": transformation_type, 58 | "context": context, 59 | "response": response 60 | } 61 | 62 | 63 | #################################### 64 | # 使用查询转换进行文档检索 65 | #################################### 66 | def transformed_search(query, vector_store, transformation_type, top_k=3): 67 | """ 68 | 使用转换后的查询进行搜索。 69 | 70 | Args: 71 | query (str): 原始查询 72 | vector_store (SimpleVectorStore): 用于搜索的向量存储 73 | transformation_type (str): 转换类型 ('rewrite', 'step_back', 或 'decompose') 74 | top_k (int): 返回的结果数量 75 | 76 | Returns: 77 | List[Dict]: 搜索结果 78 | """ 79 | print(f"转换类型: {transformation_type}") 80 | print(f"原始查询: {query}") 81 | 82 | results = [] 83 | 84 | if transformation_type == "rewrite": 85 | # 查询重写 86 | transformed_query = rewrite_query(query) 87 | print(f"重写后的查询: {transformed_query}") 88 | 89 | # 为转换后的查询创建嵌入向量 90 | query_embedding = create_embeddings(transformed_query) 91 | 92 | # 使用重写后的查询进行搜索 93 | results = vector_store.similarity_search(query_embedding, k=top_k) 94 | 95 | elif transformation_type == "step_back": 96 | # 回退提示 97 | transformed_query = generate_step_back_query(query) 98 | print(f"后退查询: {transformed_query}") 99 | 100 | # 为转换后的查询创建嵌入向量 101 | query_embedding = create_embeddings(transformed_query) 102 | 103 | # 使用回退查询进行搜索 104 | results = vector_store.similarity_search(query_embedding, k=top_k) 105 | 106 | elif transformation_type == "decompose": 107 | # 子查询分解 108 | sub_queries = decompose_query(query) 109 | print("分解为子查询:") 110 | for i, sub_q in enumerate(sub_queries, 1): 111 | print(f"{i}. {sub_q}") 112 | 113 | # 为所有子查询创建嵌入向量 114 | sub_query_embeddings = create_embeddings(sub_queries) 115 | 116 | # 使用每个子查询进行搜索并合并结果 117 | all_results = [] 118 | for i, embedding in enumerate(sub_query_embeddings): 119 | sub_results = vector_store.similarity_search(embedding, k=2) # 每个子查询获取较少的结果 120 | all_results.extend(sub_results) 121 | 122 | # 去重(保留相似度最高的结果) 123 | seen_texts = {} 124 | for result in all_results: 125 | text = result["text"] 126 | if text not in seen_texts or result["similarity"] > seen_texts[text]["similarity"]: 127 | seen_texts[text] = result 128 | 129 | # 按相似度排序并取前 top_k 个结果 130 | results = sorted(seen_texts.values(), key=lambda x: x["similarity"], reverse=True)[:top_k] 131 | 132 | else: 133 | # 普通搜索(无转换) 134 | query_embedding = create_embeddings(query) 135 | results = vector_store.similarity_search(query_embedding, k=top_k) 136 | 137 | return results 138 | 139 | 140 | #################################### 141 | # 查询重写(Query Rewriting) 142 | #################################### 143 | def rewrite_query(original_query): 144 | """ 145 | 重写查询以使其更加具体和详细,从而提高检索效果。 146 | 147 | Args: 148 | original_query (str): 用户原始查询 149 | model (str): 用于查询重写的模型 150 | 151 | Returns: 152 | str: 重写后的查询 153 | """ 154 | # 定义系统提示,指导AI助手的行为 155 | system_prompt = "您是一个专注于优化搜索查询的AI助手。您的任务是通过重写用户查询,使其更加具体、详细,并提升检索相关信息的有效性。" 156 | 157 | # 定义用户提示,包含需要重写的原始查询 158 | user_prompt = f""" 159 | 请优化以下搜索查询,使其满足: 160 | 1. 增强查询的具体性和详细程度 161 | 2. 包含有助于获取准确信息的相关术语和核心概念 162 | 163 | 原始查询:{original_query} 164 | 165 | 优化后的查询: 166 | """ 167 | 168 | # 使用指定模型生成重写后的查询 169 | response = client.chat.completions.create( 170 | model=llm_model, 171 | temperature=0.0, # 确保输出的确定性 172 | messages=[ 173 | {"role": "system", "content": system_prompt}, 174 | {"role": "user", "content": user_prompt} 175 | ] 176 | ) 177 | 178 | # 返回重写后的查询,并去除首尾的空白字符 179 | return response.choices[0].message.content.strip() 180 | 181 | 182 | #################################### 183 | # 回退提示(Step-back Prompting) 184 | #################################### 185 | def generate_step_back_query(original_query): 186 | """ 187 | 生成一个更广泛的“回退”查询以检索更宽泛的上下文信息。 188 | 189 | Args: 190 | original_query (str): 原始用户查询 191 | model (str): 用于生成回退查询的模型 192 | 193 | Returns: 194 | str: 回退查询 195 | """ 196 | # 定义系统提示,以指导AI助手的行为 197 | system_prompt = "您是一个专注于搜索策略的AI助手。您的任务是将特定查询转化为更宽泛、更通用的版本,以帮助检索相关背景信息。" 198 | 199 | # 定义用户提示,包含要概括的原始查询 200 | user_prompt = f""" 201 | 请基于以下具体查询生成更通用的版本,要求: 202 | 1. 扩大查询范围以涵盖背景信息 203 | 2. 包含潜在相关领域的关键概念 204 | 3. 保持语义完整性 205 | 206 | 原始查询: {original_query} 207 | 208 | 通用化查询: 209 | """ 210 | 211 | # 使用指定的模型生成回退查询 212 | response = client.chat.completions.create( 213 | model=llm_model, 214 | temperature=0.1, # 稍微高点以增加多样性 215 | messages=[ 216 | {"role": "system", "content": system_prompt}, 217 | {"role": "user", "content": user_prompt} 218 | ] 219 | ) 220 | 221 | # 返回回退查询,去除任何前导/尾随空格 222 | return response.choices[0].message.content.strip() 223 | 224 | 225 | #################################### 226 | # 查询分解为子查询(Sub-query Decomposition) 227 | #################################### 228 | def decompose_query(original_query, num_subqueries=4): 229 | """ 230 | 将复杂查询分解为更简单的子查询。 231 | 232 | Args: 233 | original_query (str): 原始的复杂查询 234 | num_subqueries (int): 要生成的子查询数量 235 | model (str): 用于查询分解的模型 236 | 237 | Returns: 238 | List[str]: 更简单子查询的列表 239 | """ 240 | # 定义系统提示,指导AI助手的行为 241 | system_prompt = "您是一个专门负责分解复杂问题的AI助手。您的任务是将复杂的查询拆解成更简单的子问题,这些子问题的答案组合起来能够解决原始查询。" 242 | 243 | # 使用需要分解的原始查询定义用户提示 244 | user_prompt = f""" 245 | 将以下复杂查询分解为{num_subqueries}个更简单的子问题。每个子问题应聚焦原始问题的不同方面。 246 | 247 | 原始查询: {original_query} 248 | 249 | 请生成{num_subqueries}个子问题,每个问题单独一行,按以下格式: 250 | 1. [第一个子问题] 251 | 2. [第二个子问题] 252 | 依此类推... 253 | """ 254 | 255 | # 使用指定模型生成子查询 256 | response = client.chat.completions.create( 257 | model=llm_model, 258 | temperature=0.2, 259 | messages=[ 260 | {"role": "system", "content": system_prompt}, 261 | {"role": "user", "content": user_prompt} 262 | ] 263 | ) 264 | 265 | # 处理响应以提取子查询 266 | content = response.choices[0].message.content.strip() 267 | 268 | pattern = r'^\d+\.\s*(.*)' 269 | return [re.match(pattern, line).group(1) for line in content.split('\n') if line.strip()] 270 | 271 | # 使用简单解析提取编号的查询 272 | # lines = content.split("\n") 273 | # sub_queries = [] 274 | # for line in lines: 275 | # if line.strip() and any(line.strip().startswith(f"{i}.") for i in range(1, 10)): 276 | # # 移除编号和前导空格 277 | # query = line.strip() 278 | # query = query[query.find(".")+1:].strip() 279 | # sub_queries.append(query) 280 | # return sub_queries 281 | 282 | 283 | def generate_response(query, context): 284 | """ 285 | 根据查询和检索到的上下文生成响应。 286 | 287 | Args: 288 | query (str): 用户查询 289 | context (str): 检索到的上下文 290 | Returns: 291 | str: 生成的响应 292 | """ 293 | # 定义系统提示以指导AI助手的行为 294 | system_prompt = "您是一个乐于助人的AI助手。请仅根据提供的上下文来回答用户的问题。如果在上下文中找不到答案,请直接说'没有足够的信息'。" 295 | 296 | # 定义包含上下文和查询的用户提示 297 | user_prompt = f""" 298 | 上下文内容: 299 | {context} 300 | 301 | 问题: {query} 302 | 303 | 请基于上述上下文内容提供一个全面详尽的答案。 304 | """ 305 | 306 | # 使用指定的模型生成响应 307 | response = client.chat.completions.create( 308 | model=llm_model, 309 | temperature=0, # 低温度以获得确定性输出 310 | messages=[ 311 | {"role": "system", "content": system_prompt}, 312 | {"role": "user", "content": user_prompt} 313 | ] 314 | ) 315 | 316 | # 返回生成的响应,去除任何前导/尾随空格 317 | return response.choices[0].message.content.strip() 318 | 319 | 320 | def create_embeddings(text): 321 | """ 322 | 使用Embedding模型为给定文本创建嵌入向量。 323 | 324 | Args: 325 | text (str): 要创建嵌入向量的输入文本。 326 | 327 | Returns: 328 | List[float]: 嵌入向量。 329 | """ 330 | # 通过将字符串输入转换为列表来处理字符串和列表输入 331 | input_text = text if isinstance(text, list) else [text] 332 | 333 | # 使用指定的模型为输入文本创建嵌入向量 334 | response = client.embeddings.create( 335 | model=embedding_model, 336 | input=input_text 337 | ) 338 | 339 | # 如果输入是字符串,仅返回第一个嵌入向量 340 | if isinstance(text, str): 341 | return response.data[0].embedding 342 | 343 | # 否则,将所有嵌入向量作为向量列表返回 344 | return [item.embedding for item in response.data] 345 | 346 | 347 | def process_document(pdf_path, chunk_size=1000, chunk_overlap=200): 348 | """ 349 | 为RAG处理文档。 350 | 351 | Args: 352 | pdf_path (str): PDF文件的路径。 353 | chunk_size (int): 每个文本块的大小(以字符为单位)。 354 | chunk_overlap (int): 文本块之间的重叠大小(以字符为单位)。 355 | 356 | Returns: 357 | SimpleVectorStore: 包含文档文本块及其嵌入向量的向量存储。 358 | """ 359 | print("从PDF中提取文本...") 360 | extracted_text = extract_text_from_pdf(pdf_path) # 调用函数提取PDF中的文本 361 | 362 | print("分割文本...") 363 | chunks = chunk_text(extracted_text, chunk_size, chunk_overlap) # 将提取的文本分割为多个块 364 | print(f"创建了 {len(chunks)} 个文本块") 365 | 366 | print("为文本块创建嵌入向量...") 367 | # 为了提高效率,一次性为所有文本块创建嵌入向量 368 | chunk_embeddings = create_embeddings(chunks) 369 | 370 | # 创建向量存储 371 | store = SimpleVectorStore() 372 | 373 | # 将文本块添加到向量存储中 374 | for i, (chunk, embedding) in enumerate(zip(chunks, chunk_embeddings)): 375 | store.add_item( 376 | text=chunk, # 文本内容 377 | embedding=embedding, # 嵌入向量 378 | metadata={"index": i, "source": pdf_path} # 元数据,包括索引和源文件路径 379 | ) 380 | 381 | print(f"向向量存储中添加了 {len(chunks)} 个文本块") 382 | return store 383 | 384 | 385 | def extract_text_from_pdf(pdf_path): 386 | """ 387 | 从 PDF 文件中提取文本,并打印前 `num_chars` 个字符。 388 | 389 | Args: 390 | pdf_path (str): Path to the PDF file. 391 | 392 | Returns: 393 | str: Extracted text from the PDF. 394 | """ 395 | # 打开 PDF 文件 396 | mypdf = fitz.open(pdf_path) 397 | all_text = "" # 初始化一个空字符串以存储提取的文本 398 | 399 | # Iterate through each page in the PDF 400 | for page_num in range(mypdf.page_count): 401 | page = mypdf[page_num] 402 | text = page.get_text("text") # 从页面中提取文本 403 | all_text += text # 将提取的文本追加到 all_text 字符串中 404 | 405 | return all_text # 返回提取的文本 406 | 407 | 408 | def chunk_text(text, n, overlap): 409 | """ 410 | 将文本分割为重叠的块 411 | 412 | Args: 413 | text (str): 要分割的文本 414 | n (int): 每个块的字符数 415 | overlap (int): 块之间的重叠字符数 416 | 417 | Returns: 418 | List[str]: 文本块列表 419 | """ 420 | chunks = [] # 421 | for i in range(0, len(text), n - overlap): 422 | # 添加从当前索引到索引 + 块大小的文本块 423 | chunks.append(text[i:i + n]) 424 | 425 | return chunks # Return the list of text chunks 426 | 427 | 428 | class SimpleVectorStore: 429 | """ 430 | 使用NumPy实现的简单向量存储。 431 | """ 432 | 433 | def __init__(self): 434 | """ 435 | 初始化向量存储。 436 | """ 437 | self.vectors = [] # 用于存储嵌入向量的列表 438 | self.texts = [] # 用于存储原始文本的列表 439 | self.metadata = [] # 用于存储每个文本元数据的列表 440 | 441 | def add_item(self, text, embedding, metadata=None): 442 | """ 443 | 向向量存储中添加一个项目。 444 | 445 | Args: 446 | text (str): 原始文本。 447 | embedding (List[float]): 嵌入向量。 448 | metadata (dict, 可选): 额外的元数据。 449 | """ 450 | self.vectors.append(np.array(embedding)) # 将嵌入转换为numpy数组并添加到向量列表中 451 | self.texts.append(text) # 将原始文本添加到文本列表中 452 | self.metadata.append(metadata or {}) # 添加元数据到元数据列表中,如果没有提供则使用空字典 453 | 454 | def similarity_search(self, query_embedding, k=5): 455 | """ 456 | 查找与查询嵌入最相似的项目。 457 | 458 | Args: 459 | query_embedding (List[float]): 查询嵌入向量。 460 | k (int): 返回的结果数量。 461 | 462 | Returns: 463 | List[Dict]: 包含文本和元数据的前k个最相似项。 464 | """ 465 | if not self.vectors: 466 | return [] # 如果没有存储向量,则返回空列表 467 | 468 | # 将查询嵌入转换为numpy数组 469 | query_vector = np.array(query_embedding) 470 | 471 | # 使用余弦相似度计算相似度 472 | similarities = [] 473 | for i, vector in enumerate(self.vectors): 474 | # 计算查询向量与存储向量之间的余弦相似度 475 | similarity = np.dot(query_vector, vector) / (np.linalg.norm(query_vector) * np.linalg.norm(vector)) 476 | similarities.append((i, similarity)) # 添加索引和相似度分数 477 | 478 | # 按相似度排序(降序) 479 | similarities.sort(key=lambda x: x[1], reverse=True) 480 | 481 | # 返回前k个结果 482 | results = [] 483 | for i in range(min(k, len(similarities))): 484 | idx, score = similarities[i] 485 | results.append({ 486 | "text": self.texts[idx], # 添加对应的文本 487 | "metadata": self.metadata[idx], # 添加对应的元数据 488 | "similarity": score # 添加相似度分数 489 | }) 490 | 491 | return results # 返回前k个最相似项的列表 492 | -------------------------------------------------------------------------------- /src/core/20_crag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 纠错型检索 核心函数 3 | """ 4 | import os 5 | import re 6 | import requests 7 | from urllib.parse import quote_plus 8 | from openai import OpenAI 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | import requests 13 | 14 | # proxies = { 15 | # "https": "127.0.0.1:9090", 16 | # "http": "127.0.0.1:9090" 17 | # } 18 | 19 | 20 | client = OpenAI( 21 | base_url=os.getenv("LLM_BASE_URL"), 22 | api_key=os.getenv("LLM_API_KEY") 23 | ) 24 | llm_model = os.getenv("LLM_MODEL_ID") 25 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 26 | 27 | 28 | #################################### 29 | # CRAG(纠正型检索RAG) 完整流程 30 | #################################### 31 | def crag_process(query, vector_store, k=3): 32 | """ 33 | 执行“纠正性检索增强生成”(Corrective RAG)流程。 34 | 35 | Args: 36 | query (str): 用户查询内容 37 | vector_store (SimpleVectorStore): 包含文档块的向量存储 38 | k (int): 初始要检索的文档数量 39 | 40 | Returns: 41 | Dict: 处理结果,包括响应内容和调试信息 42 | """ 43 | print(f"\n=== 正在使用 CRAG 处理查询:{query} ===\n") 44 | 45 | # 步骤 1: 创建查询嵌入并检索文档 46 | print("正在检索初始文档...") 47 | query_embedding = create_embeddings(query) 48 | retrieved_docs = vector_store.similarity_search(query_embedding, k=k) 49 | 50 | # 步骤 2: 评估文档相关性 51 | print("正在评估文档的相关性...") 52 | relevance_scores = [] 53 | for doc in retrieved_docs: 54 | score = evaluate_document_relevance(query, doc["text"]) 55 | relevance_scores.append(score) 56 | doc["relevance"] = score 57 | print(f"文档得分为 {score:.2f} 的相关性") 58 | 59 | # 步骤 3: 根据最高相关性得分确定操作策略 60 | max_score = max(relevance_scores) if relevance_scores else 0 61 | best_doc_idx = relevance_scores.index(max_score) if relevance_scores else -1 62 | 63 | # 记录来源用于引用 64 | sources = [] 65 | final_knowledge = "" 66 | 67 | # 步骤 4: 根据情况执行相应的知识获取策略 68 | if max_score > 0.7: 69 | # 情况 1: 高相关性 - 直接使用文档内容 70 | print(f"高相关性 ({max_score:.2f}) - 直接使用文档内容") 71 | best_doc = retrieved_docs[best_doc_idx]["text"] 72 | final_knowledge = best_doc 73 | sources.append({ 74 | "title": "文档", 75 | "url": "" 76 | }) 77 | 78 | elif max_score < 0.3: 79 | # 情况 2: 低相关性 - 使用网络搜索 80 | print(f"低相关性 ({max_score:.2f}) - 进行网络搜索") 81 | web_results, web_sources = perform_web_search(query) 82 | final_knowledge = refine_knowledge(web_results) 83 | sources.extend(web_sources) 84 | 85 | else: 86 | # 情况 3: 中等相关性 - 结合文档与网络搜索结果 87 | print(f"中等相关性 ({max_score:.2f}) - 结合文档与网络搜索") 88 | best_doc = retrieved_docs[best_doc_idx]["text"] 89 | refined_doc = refine_knowledge(best_doc) 90 | 91 | # 获取网络搜索结果 92 | web_results, web_sources = perform_web_search(query) 93 | refined_web = refine_knowledge(web_results) 94 | 95 | # 合并知识 96 | final_knowledge = f"来自文档的内容:\n{refined_doc}\n\n来自网络搜索的内容:\n{refined_web}" 97 | 98 | # 添加来源 99 | sources.append({ 100 | "title": "文档", 101 | "url": "" 102 | }) 103 | sources.extend(web_sources) 104 | 105 | # 步骤 5: 生成最终响应 106 | print("正在生成最终响应...") 107 | response = generate_response(query, final_knowledge, sources) 108 | 109 | # 返回完整的处理结果 110 | return { 111 | "query": query, 112 | "response": response, 113 | "retrieved_docs": retrieved_docs, 114 | "relevance_scores": relevance_scores, 115 | "max_relevance": max_score, 116 | "final_knowledge": final_knowledge, 117 | "sources": sources 118 | } 119 | 120 | 121 | #################################### 122 | # 评估文档与查询的相关性:给出一个评估分数 123 | #################################### 124 | def evaluate_document_relevance(query, document): 125 | """ 126 | 评估文档与查询的相关性。 127 | 128 | Args: 129 | query (str): 用户查询 130 | document (str): 文档文本 131 | 132 | Returns: 133 | float: 相关性评分(0 到 1) 134 | """ 135 | # 定义系统提示语,指导模型如何评估相关性 136 | system_prompt = """ 137 | 你是一位评估文档相关性的专家。 138 | 请在 0 到 1 的范围内对给定文档与查询的相关性进行评分。 139 | 0 表示完全不相关,1 表示完全相关。 140 | 仅返回一个介于 0 和 1 之间的浮点数评分,不要过多解释与生成。 141 | """ 142 | 143 | # 构造用户提示语,包含查询和文档内容 144 | user_prompt = f"查询:{query}\n\n文档:{document}" 145 | 146 | try: 147 | # 调用 OpenAI API 进行相关性评分 148 | response = client.chat.completions.create( 149 | model=llm_model, # 使用的模型 150 | messages=[ 151 | {"role": "system", "content": system_prompt}, # 系统消息用于引导助手行为 152 | {"role": "user", "content": user_prompt} # 用户消息包含查询和文档 153 | ], 154 | temperature=0, # 设置生成温度为最低以保证一致性 155 | max_tokens=5 # 只需返回一个简短的分数 156 | ) 157 | 158 | # 提取评分结果 159 | score_text = response.choices[0].message.content.strip() 160 | # 使用正则表达式提取响应中的浮点数值 161 | score_match = re.search(r'(\d+(\.\d+)?)', score_text) 162 | if score_match: 163 | return float(score_match.group(1)) # 返回提取到的浮点型评分 164 | return 0.5 # 如果解析失败,默认返回中间值 165 | 166 | except Exception as e: 167 | # 捕获异常并打印错误信息,出错时返回默认值 168 | print(f"评估文档相关性时出错:{e}") 169 | return 0.5 # 出错时默认返回中等评分 170 | 171 | 172 | #################################### 173 | # 从文本中提取并精炼关键信息:使检索到的信息减少冗余 174 | #################################### 175 | def refine_knowledge(text): 176 | """ 177 | 从文本中提取并精炼关键信息。 178 | 179 | Args: 180 | text (str): 要精炼的输入文本 181 | 182 | Returns: 183 | str: 精炼后的关键要点 184 | """ 185 | # 定义系统提示,指导模型如何提取关键信息 186 | system_prompt = """ 187 | 请从以下文本中提取关键信息,并以清晰简洁的项目符号列表形式呈现。 188 | 重点关注最相关和最重要的事实与细节。 189 | 你的回答应格式化为一个项目符号列表,每一项以 "• " 开头,换行分隔。 190 | """ 191 | 192 | try: 193 | # 调用 OpenAI API 来精炼文本 194 | response = client.chat.completions.create( 195 | model=llm_model, # 使用的模型 196 | messages=[ 197 | {"role": "system", "content": system_prompt}, # 系统消息用于引导助手行为 198 | {"role": "user", "content": f"要提炼的文本内容:\n\n{text}"} # 用户消息包含待精炼的文本 199 | ], 200 | temperature=0.3 # 设置生成温度以控制输出随机性 201 | ) 202 | 203 | # 返回精炼后的关键要点(去除首尾空白) 204 | return response.choices[0].message.content.strip() 205 | 206 | except Exception as e: 207 | # 如果发生错误,打印错误信息并返回原始文本 208 | print(f"精炼知识时出错:{e}") 209 | return text # 出错时返回原始文本 210 | 211 | 212 | #################################### 213 | # 查询改写查询:对查询进行重写,以提高搜索效果 214 | #################################### 215 | def perform_web_search(query): 216 | """ 217 | 使用重写后的查询执行网络搜索。 218 | 219 | Args: 220 | query (str): 用户原始查询语句 221 | 222 | Returns: 223 | Tuple[str, List[Dict]]: 搜索结果文本 和 来源元数据列表 224 | """ 225 | # 重写查询以提升搜索效果 226 | rewritten_query = rewrite_search_query(query) 227 | print(f"重写后的搜索查询:{rewritten_query}") 228 | 229 | # 使用重写后的查询执行网络搜索 230 | results_text, sources = duck_duck_go_search(rewritten_query) 231 | 232 | # 返回搜索结果和来源信息 233 | return results_text, sources 234 | 235 | 236 | #################################### 237 | # 查询改写 238 | #################################### 239 | def rewrite_search_query(query): 240 | """ 241 | 将查询重写为更适合网络搜索的形式。 242 | 243 | Args: 244 | query (str): 原始查询语句 245 | 246 | Returns: 247 | str: 重写后的查询语句 248 | """ 249 | # 定义系统提示,指导模型如何重写查询 250 | system_prompt = """ 251 | 你是一位编写高效搜索查询的专家。 252 | 请将给定的查询重写为更适合搜索引擎的形式。 253 | 重点使用关键词和事实,去除不必要的词语,使查询更简洁明确。 254 | """ 255 | 256 | try: 257 | # 调用 OpenAI API 来重写查询 258 | response = client.chat.completions.create( 259 | model=llm_model, # 使用的模型 260 | messages=[ 261 | {"role": "system", "content": system_prompt}, # 系统提示用于引导助手行为 262 | {"role": "user", "content": f"原始查询:{query}\n\n重写后的查询:"} # 用户输入原始查询 263 | ], 264 | temperature=0.3, # 设置生成温度以控制输出随机性 265 | max_tokens=50 # 限制响应长度 266 | ) 267 | 268 | # 返回重写后的查询结果(去除首尾空白) 269 | return response.choices[0].message.content.strip() 270 | 271 | except Exception as e: 272 | # 如果发生错误,打印错误信息并返回原始查询 273 | print(f"重写搜索查询时出错:{e}") 274 | return query # 出错时返回原始查询 275 | 276 | 277 | #################################### 278 | # 使用 DuckDuckGo 进行网络搜索 279 | #################################### 280 | def duck_duck_go_search(query, num_results=3): 281 | """ 282 | 使用 DuckDuckGo 执行网络搜索。 283 | 284 | Args: 285 | query (str): 搜索查询语句 286 | num_results (int): 要返回的结果数量 287 | 288 | Returns: 289 | Tuple[str, List[Dict]]: 合并后的搜索结果文本 和 来源元数据 290 | """ 291 | # 对查询进行URL编码 292 | encoded_query = quote_plus(query) 293 | 294 | # DuckDuckGo 的非官方 API 接口地址 295 | url = f"https://api.duckduckgo.com/?q={encoded_query}&format=json" 296 | 297 | try: 298 | # 发送网络搜索请求 299 | headers = { 300 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" 301 | } 302 | proxies = { 303 | "http": "socks5://127.0.0.1:9090", 304 | "https": "socks5://127.0.0.1:9090" 305 | } 306 | response = requests.get(url, 307 | headers=headers, 308 | proxies=proxies 309 | ) 310 | print(response) 311 | 312 | data = response.json() 313 | # print(data) 314 | 315 | # 初始化变量用于存储搜索结果和来源信息 316 | results_text = "" 317 | sources = [] 318 | 319 | # 添加摘要内容(如果存在) 320 | if data.get("AbstractText"): 321 | results_text += f"{data['AbstractText']}\n\n" 322 | sources.append({ 323 | "title": data.get("AbstractSource", "Wikipedia"), 324 | "url": data.get("AbstractURL", "") 325 | }) 326 | 327 | # 添加相关主题搜索结果 328 | for topic in data.get("RelatedTopics", [])[:num_results]: 329 | if "Text" in topic and "FirstURL" in topic: 330 | results_text += f"{topic['Text']}\n\n" 331 | sources.append({ 332 | "title": topic.get("Text", "").split(" - ")[0], 333 | "url": topic.get("FirstURL", "") 334 | }) 335 | 336 | return results_text, sources 337 | 338 | except Exception as e: 339 | # 如果主搜索失败,打印错误信息 340 | print(f"执行网络搜索时出错:{e}") 341 | 342 | # 尝试使用备份的搜索API(如SerpAPI) 343 | try: 344 | backup_url = f"https://serpapi.com/search.json?q={encoded_query}&engine=duckduckgo" 345 | response = requests.get(backup_url) 346 | data = response.json() 347 | 348 | # 初始化变量 349 | results_text = "" 350 | sources = [] 351 | 352 | # 从备份API提取结果 353 | for result in data.get("organic_results", [])[:num_results]: 354 | results_text += f"{result.get('title', '')}: {result.get('snippet', '')}\n\n" 355 | sources.append({ 356 | "title": result.get("title", ""), 357 | "url": result.get("link", "") 358 | }) 359 | 360 | return results_text, sources 361 | except Exception as backup_error: 362 | # 如果备份搜索也失败,打印错误并返回空结果 363 | print(f"备用搜索也失败了:{backup_error}") 364 | return "无法获取搜索结果。", [] 365 | 366 | 367 | def create_embeddings(texts): 368 | """ 369 | 为文本输入创建向量嵌入。 370 | 371 | 嵌入是文本的密集向量表示,能够捕捉语义含义,便于进行相似性比较。 372 | 在 RAG 系统中,嵌入对于将查询与相关文档块进行匹配非常关键。 373 | 374 | Args: 375 | texts (str 或 List[str]): 要嵌入的输入文本。可以是单个字符串或字符串列表。 376 | model (str): 要使用的嵌入模型名称。默认为 "text-embedding-3-small"。 377 | 378 | Returns: 379 | List[List[float]]: 如果输入是列表,返回每个文本对应的嵌入向量列表; 380 | 如果输入是单个字符串,返回一个嵌入向量。 381 | """ 382 | # 处理单个字符串和列表两种输入形式:统一转为列表处理 383 | input_texts = texts if isinstance(texts, list) else [texts] 384 | 385 | # 分批次处理以避免 API 速率限制和请求体大小限制 386 | batch_size = 100 387 | all_embeddings = [] 388 | 389 | # 遍历每一批文本 390 | for i in range(0, len(input_texts), batch_size): 391 | # 提取当前批次的文本 392 | batch = input_texts[i:i + batch_size] 393 | 394 | # 调用 API 生成当前批次的嵌入 395 | response = client.embeddings.create( 396 | model=embedding_model, 397 | input=batch 398 | ) 399 | 400 | # 从响应中提取嵌入向量并加入总结果中 401 | batch_embeddings = [item.embedding for item in response.data] 402 | all_embeddings.extend(batch_embeddings) 403 | 404 | # 如果原始输入是单个字符串,则只返回第一个嵌入 405 | if isinstance(texts, str): 406 | return all_embeddings[0] 407 | 408 | # 否则返回所有嵌入组成的列表 409 | return all_embeddings 410 | 411 | 412 | def generate_response(query, knowledge, sources): 413 | """ 414 | 根据查询内容和提供的知识生成回答。 415 | 416 | Args: 417 | query (str): 用户的查询内容 418 | knowledge (str): 用于生成回答的知识内容 419 | sources (List[Dict]): 来源列表,每个来源包含标题和URL 420 | 421 | Returns: 422 | str: 生成的回答文本 423 | """ 424 | 425 | # 将来源格式化为可用于提示的内容 426 | sources_text = "" 427 | for source in sources: 428 | title = source.get("title", "未知来源") 429 | url = source.get("url", "") 430 | if url: 431 | sources_text += f"- {title}: {url}\n" 432 | else: 433 | sources_text += f"- {title}\n" 434 | 435 | # 定义系统指令(system prompt),指导模型如何生成回答 436 | system_prompt = """ 437 | 你是一个乐于助人的AI助手。请根据提供的知识内容,生成一个全面且有信息量的回答。 438 | 在回答中包含所有相关信息,同时保持语言清晰简洁。 439 | 如果知识内容不能完全回答问题,请指出这一限制。 440 | 最后在回答末尾注明引用来源。 441 | """ 442 | 443 | # 构建用户提示(user prompt),包含用户的查询、知识内容和来源信息 444 | user_prompt = f""" 445 | 查询内容:{query} 446 | 447 | 知识内容: 448 | {knowledge} 449 | 450 | 引用来源: 451 | {sources_text} 452 | 453 | 请根据以上信息,提供一个有帮助的回答,并在最后列出引用来源。 454 | """ 455 | 456 | try: 457 | # 调用 OpenAI API 生成回答 458 | response = client.chat.completions.create( 459 | model=llm_model, # 使用模型以获得高质量回答 460 | messages=[ 461 | {"role": "system", "content": system_prompt}, 462 | {"role": "user", "content": user_prompt} 463 | ], 464 | temperature=0.2 # 控制生成内容的随机性(较低值更稳定) 465 | ) 466 | 467 | # 返回生成的回答内容,并去除首尾空格 468 | return response.choices[0].message.content.strip() 469 | 470 | except Exception as e: 471 | # 捕获异常并返回错误信息 472 | print(f"生成回答时出错: {e}") 473 | return f"抱歉,在尝试回答您的问题“{query}”时遇到了错误。错误信息为:{str(e)}" 474 | 475 | 476 | if __name__ == '__main__': 477 | from duckduckgo_search import DDGS 478 | query = "机器学习 vs 传统编程 区别" 479 | # ddgs = DDGS(proxy="127.0.0.1:9090", timeout=20) # "tb" is an alias for "socks5://127.0.0.1:9150" 480 | # results = ddgs.text(query, max_results=5) 481 | # print(results) 482 | res_text, source = duck_duck_go_search(query) 483 | print(res_text) -------------------------------------------------------------------------------- /src/core/18_hierarchy_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 分级检索 核心函数 3 | """ 4 | import os 5 | import pickle 6 | import fitz 7 | import numpy as np 8 | from openai import OpenAI 9 | from dotenv import load_dotenv 10 | 11 | load_dotenv() 12 | 13 | client = OpenAI( 14 | base_url=os.getenv("LLM_BASE_URL"), 15 | api_key=os.getenv("LLM_API_KEY") 16 | ) 17 | llm_model = os.getenv("LLM_MODEL_ID") 18 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 19 | 20 | 21 | #################################### 22 | # 分级索引 RAG 流程 23 | #################################### 24 | def hierarchical_rag(query, pdf_path, chunk_size=1000, chunk_overlap=200, 25 | k_summaries=3, k_chunks=5, regenerate=False): 26 | """ 27 | 完整的分级 RAG 管道。 28 | 29 | Args: 30 | query (str): 用户查询 31 | pdf_path (str): PDF 文档的路径 32 | chunk_size (int): 每个详细块的大小 33 | chunk_overlap (int): 块之间的重叠 34 | k_summaries (int): 要检索的摘要数量 35 | k_chunks (int): 每个摘要要检索的块数量 36 | regenerate (bool): 是否重新生成向量存储 37 | 38 | Returns: 39 | Dict: 包括响应和检索到的块的结果 40 | """ 41 | # 创建用于缓存的存储文件名 42 | summary_store_file = f"{os.path.basename(pdf_path)}_summary_store.pkl" 43 | detailed_store_file = f"{os.path.basename(pdf_path)}_detailed_store.pkl" 44 | 45 | # 如果需要,处理文档并创建存储 46 | if regenerate or not os.path.exists(summary_store_file) or not os.path.exists(detailed_store_file): 47 | print("处理文档并创建向量存储...") 48 | # 处理文档以创建分层索引和向量存储 49 | summary_store, detailed_store = process_document_hierarchically( 50 | pdf_path, chunk_size, chunk_overlap 51 | ) 52 | 53 | # 将摘要存储保存到文件以供将来使用 54 | with open(summary_store_file, 'wb') as f: 55 | pickle.dump(summary_store, f) 56 | 57 | # 将详细存储保存到文件以供将来使用 58 | with open(detailed_store_file, 'wb') as f: 59 | pickle.dump(detailed_store, f) 60 | else: 61 | # 从文件加载现有的摘要存储 62 | print("加载现有的向量存储...") 63 | with open(summary_store_file, 'rb') as f: 64 | summary_store = pickle.load(f) 65 | 66 | # 从文件加载现有的详细存储 67 | with open(detailed_store_file, 'rb') as f: 68 | detailed_store = pickle.load(f) 69 | 70 | # 使用查询分层检索相关块 71 | retrieved_chunks = retrieve_hierarchically( 72 | query, summary_store, detailed_store, k_summaries, k_chunks 73 | ) 74 | 75 | # 根据检索到的块生成响应 76 | response = generate_response(query, retrieved_chunks) 77 | 78 | # 返回结果,包括查询、响应、检索到的块以及摘要和详细块的数量 79 | return { 80 | "query": query, 81 | "response": response, 82 | "retrieved_chunks": retrieved_chunks, 83 | "summary_count": len(summary_store.texts), 84 | "detailed_count": len(detailed_store.texts) 85 | } 86 | 87 | 88 | #################################### 89 | # 将文档处理为分级索引:页面摘要、详细块 90 | #################################### 91 | def process_document_hierarchically(pdf_path, chunk_size=1000, chunk_overlap=200): 92 | """ 93 | 将文档处理为分层索引。 94 | 95 | Args: 96 | pdf_path (str): PDF 文件的路径 97 | chunk_size (int): 每个详细块的大小 98 | chunk_overlap (int): 块之间的重叠量 99 | 100 | Returns: 101 | Tuple[SimpleVectorStore, SimpleVectorStore]: 摘要和详细向量存储 102 | """ 103 | # 从 PDF 中提取页面 104 | pages = extract_text_from_pdf(pdf_path) 105 | 106 | # 为每一页创建摘要 107 | print("生成页面摘要...") 108 | summaries = [] 109 | for i, page in enumerate(pages): 110 | print(f"正在摘要第 {i + 1}/{len(pages)} 页...") 111 | summary_text = generate_page_summary(page["text"]) 112 | 113 | # 创建摘要元数据 114 | summary_metadata = page["metadata"].copy() 115 | summary_metadata.update({"is_summary": True}) 116 | 117 | # 将摘要文本和元数据添加到摘要列表中 118 | summaries.append({ 119 | "text": summary_text, 120 | "metadata": summary_metadata 121 | }) 122 | 123 | # 为每一页创建详细块 124 | detailed_chunks = [] 125 | for page in pages: 126 | # 将页面的文本切分为块 127 | page_chunks = chunk_text( 128 | page["text"], 129 | page["metadata"], 130 | chunk_size, 131 | chunk_overlap 132 | ) 133 | # 使用当前页面的块扩展 detailed_chunks 列表 134 | detailed_chunks.extend(page_chunks) 135 | 136 | print(f"已创建 {len(detailed_chunks)} 个详细块") 137 | 138 | # 为摘要创建嵌入 139 | print("正在为摘要创建嵌入...") 140 | summary_texts = [summary["text"] for summary in summaries] 141 | summary_embeddings = create_embeddings(summary_texts) 142 | 143 | # 为详细块创建嵌入 144 | print("正在为详细块创建嵌入...") 145 | chunk_texts = [chunk["text"] for chunk in detailed_chunks] 146 | chunk_embeddings = create_embeddings(chunk_texts) 147 | 148 | # 创建向量存储 149 | summary_store = SimpleVectorStore() 150 | detailed_store = SimpleVectorStore() 151 | 152 | # 将摘要添加到摘要存储中 153 | for i, summary in enumerate(summaries): 154 | summary_store.add_item( 155 | text=summary["text"], 156 | embedding=summary_embeddings[i], 157 | metadata=summary["metadata"] 158 | ) 159 | 160 | # 将块添加到详细存储中 161 | for i, chunk in enumerate(detailed_chunks): 162 | detailed_store.add_item( 163 | text=chunk["text"], 164 | embedding=chunk_embeddings[i], 165 | metadata=chunk["metadata"] 166 | ) 167 | 168 | print(f"已创建包含 {len(summaries)} 个摘要和 {len(detailed_chunks)} 个块的向量存储") 169 | return summary_store, detailed_store 170 | 171 | 172 | #################################### 173 | # 分级索引检索信息:先搜索摘要,再搜索块 174 | #################################### 175 | def retrieve_hierarchically(query, summary_store, detailed_store, k_summaries=3, k_chunks=5): 176 | """ 177 | 使用分级索引检索信息。 178 | 179 | Args: 180 | query (str): 用户查询 181 | summary_store (SimpleVectorStore): 文档摘要存储 182 | detailed_store (SimpleVectorStore): 详细块存储 183 | k_summaries (int): 要检索的摘要数量 184 | k_chunks (int): 每个摘要要检索的块数量 185 | 186 | Returns: 187 | List[Dict]: 检索到的带有相关性分数的块 188 | """ 189 | print(f"正在为查询执行分层检索: {query}") 190 | 191 | # 创建查询嵌入 192 | query_embedding = create_embeddings(query) 193 | 194 | # 首先,检索相关的摘要 195 | summary_results = summary_store.similarity_search( 196 | query_embedding, 197 | k=k_summaries 198 | ) 199 | 200 | print(f"检索到 {len(summary_results)} 个相关摘要") 201 | 202 | # 收集来自相关摘要的页面 203 | relevant_pages = [result["metadata"]["page"] for result in summary_results] 204 | 205 | # 创建一个过滤函数,仅保留来自相关页面的块 206 | def page_filter(metadata): 207 | return metadata["page"] in relevant_pages 208 | 209 | # 然后,仅从这些相关页面检索详细块 210 | detailed_results = detailed_store.similarity_search( 211 | query_embedding, 212 | k=k_chunks * len(relevant_pages), 213 | filter_func=page_filter 214 | ) 215 | 216 | print(f"从相关页面检索到 {len(detailed_results)} 个详细块") 217 | 218 | # 对于每个结果,添加它来自哪个摘要/页面 219 | for result in detailed_results: 220 | page = result["metadata"]["page"] 221 | matching_summaries = [s for s in summary_results if s["metadata"]["page"] == page] 222 | if matching_summaries: 223 | result["summary"] = matching_summaries[0]["text"] 224 | 225 | return detailed_results 226 | 227 | 228 | #################################### 229 | # 分级索引检索信息:先搜索摘要,再搜索块 230 | #################################### 231 | def generate_page_summary(page_text): 232 | """ 233 | 生成页面的简洁摘要。 234 | 235 | Args: 236 | page_text (str): 页面的文本内容 237 | 238 | Returns: 239 | str: 生成的摘要 240 | """ 241 | # 定义系统提示,指导摘要模型如何生成摘要 242 | system_prompt = """你是一个专业的摘要生成系统。 243 | 请对提供的文本创建一个详细的摘要。 244 | 重点捕捉主要内容、关键信息和重要事实。 245 | 你的摘要应足够全面,能够让人理解该页面包含的内容, 246 | 但要比原文更简洁。""" 247 | 248 | # 如果输入文本超过最大令牌限制,则截断 249 | max_tokens = 6000 250 | truncated_text = page_text[:max_tokens] if len(page_text) > max_tokens else page_text 251 | 252 | # 向OpenAI API发出请求以生成摘要 253 | response = client.chat.completions.create( 254 | model=llm_model, # 指定要使用的模型 255 | messages=[ 256 | {"role": "system", "content": system_prompt}, # 系统消息以引导助手 257 | {"role": "user", "content": f"请总结以下文本:\n\n{truncated_text}"} # 用户消息,包含要总结的文本 258 | ], 259 | temperature=0.3 # 设置响应生成的温度 260 | ) 261 | 262 | # 返回生成的摘要内容 263 | return response.choices[0].message.content 264 | 265 | 266 | def create_embeddings(texts): 267 | """ 268 | 为给定文本创建嵌入向量。 269 | 270 | Args: 271 | texts (List[str]): 输入文本列表 272 | model (str): 嵌入模型名称 273 | 274 | Returns: 275 | List[List[float]]: 嵌入向量列表 276 | """ 277 | # 处理空输入的情况 278 | if not texts: 279 | return [] 280 | 281 | # 分批次处理(OpenAI API 的限制) 282 | batch_size = 100 283 | all_embeddings = [] 284 | 285 | # 遍历输入文本,按批次生成嵌入 286 | for i in range(0, len(texts), batch_size): 287 | batch = texts[i:i + batch_size] # 获取当前批次的文本 288 | 289 | # 调用 OpenAI 接口生成嵌入 290 | response = client.embeddings.create( 291 | model=embedding_model, 292 | input=batch 293 | ) 294 | 295 | # 提取当前批次的嵌入向量 296 | batch_embeddings = [item.embedding for item in response.data] 297 | all_embeddings.extend(batch_embeddings) # 将当前批次的嵌入向量加入总列表 298 | 299 | return all_embeddings # 返回所有嵌入向量 300 | 301 | 302 | def extract_text_from_pdf(pdf_path): 303 | """ 304 | 从PDF文件中提取文本内容,并按页分离。 305 | 306 | Args: 307 | pdf_path (str): PDF文件的路径 308 | 309 | Returns: 310 | List[Dict]: 包含文本内容和元数据的页面列表 311 | """ 312 | print(f"正在提取文本 {pdf_path}...") # 打印正在处理的PDF路径 313 | pdf = fitz.open(pdf_path) # 使用PyMuPDF打开PDF文件 314 | pages = [] # 初始化一个空列表,用于存储包含文本内容的页面 315 | 316 | # 遍历PDF中的每一页 317 | for page_num in range(len(pdf)): 318 | page = pdf[page_num] # 获取当前页 319 | text = page.get_text() # 从当前页提取文本 320 | 321 | # 跳过文本非常少的页面(少于50个字符) 322 | if len(text.strip()) > 50: 323 | # 将页面文本和元数据添加到列表中 324 | pages.append({ 325 | "text": text, 326 | "metadata": { 327 | "source": pdf_path, # 源文件路径 328 | "page": page_num + 1 # 页面编号(从1开始) 329 | } 330 | }) 331 | 332 | print(f"已提取 {len(pages)} 页的内容") # 打印已提取的页面数量 333 | return pages # 返回包含文本内容和元数据的页面列表 334 | 335 | 336 | def chunk_text(text, metadata, chunk_size=1000, overlap=200): 337 | """ 338 | 将文本分割为重叠的块,同时保留元数据。 339 | 340 | Args: 341 | text (str): 要分割的输入文本 342 | metadata (Dict): 要保留的元数据 343 | chunk_size (int): 每个块的大小(以字符为单位) 344 | overlap (int): 块之间的重叠大小(以字符为单位) 345 | 346 | Returns: 347 | List[Dict]: 包含元数据的文本块列表 348 | """ 349 | chunks = [] # 初始化一个空列表,用于存储块 350 | 351 | # 按指定的块大小和重叠量遍历文本 352 | for i in range(0, len(text), chunk_size - overlap): 353 | chunk_text = text[i:i + chunk_size] # 提取文本块 354 | 355 | # 跳过非常小的块(少于50个字符) 356 | if chunk_text and len(chunk_text.strip()) > 50: 357 | # 创建元数据的副本,并添加块特定的信息 358 | chunk_metadata = metadata.copy() 359 | chunk_metadata.update({ 360 | "chunk_index": len(chunks), # 块的索引 361 | "start_char": i, # 块的起始字符索引 362 | "end_char": i + len(chunk_text), # 块的结束字符索引 363 | "is_summary": False # 标志,表示这不是摘要 364 | }) 365 | 366 | # 将带有元数据的块添加到列表中 367 | chunks.append({ 368 | "text": chunk_text, 369 | "metadata": chunk_metadata 370 | }) 371 | 372 | return chunks # 返回带有元数据的块列表 373 | 374 | 375 | class SimpleVectorStore: 376 | """ 377 | 使用NumPy实现的简单向量存储。 378 | """ 379 | 380 | def __init__(self): 381 | """ 382 | 初始化向量存储。 383 | """ 384 | self.vectors = [] # 用于存储嵌入向量的列表 385 | self.texts = [] # 用于存储原始文本的列表 386 | self.metadata = [] # 用于存储每个文本元数据的列表 387 | 388 | def add_item(self, text, embedding, metadata=None): 389 | """ 390 | 向向量存储中添加一个项目。 391 | 392 | Args: 393 | text (str): 原始文本。 394 | embedding (List[float]): 嵌入向量。 395 | metadata (dict, optional): 额外的元数据。 396 | """ 397 | self.vectors.append(np.array(embedding)) # 将嵌入转换为numpy数组并添加到向量列表中 398 | self.texts.append(text) # 将原始文本添加到文本列表中 399 | self.metadata.append(metadata or {}) # 添加元数据到元数据列表中,如果没有提供则使用空字典 400 | 401 | def similarity_search(self, query_embedding, k=5, filter_func=None): 402 | """ 403 | 查找与查询嵌入最相似的项目。 404 | 405 | Args: 406 | query_embedding (List[float]): 查询嵌入向量。 407 | k (int): 返回的结果数量。 408 | 409 | Returns: 410 | List[Dict]: 包含文本和元数据的前k个最相似项。 411 | """ 412 | if not self.vectors: 413 | return [] # 如果没有存储向量,则返回空列表 414 | 415 | # 将查询嵌入转换为numpy数组 416 | query_vector = np.array(query_embedding) 417 | 418 | # 使用余弦相似度计算相似度 419 | similarities = [] 420 | for i, vector in enumerate(self.vectors): 421 | # 如果存在过滤函数且该元数据不符合条件,则跳过该项 422 | if filter_func and not filter_func(self.metadata[i]): 423 | continue 424 | # 计算查询向量与存储向量之间的余弦相似度 425 | similarity = np.dot(query_vector, vector) / (np.linalg.norm(query_vector) * np.linalg.norm(vector)) 426 | similarities.append((i, similarity)) # 添加索引和相似度分数 427 | 428 | # 按相似度排序(降序) 429 | similarities.sort(key=lambda x: x[1], reverse=True) 430 | 431 | # 返回前k个结果 432 | results = [] 433 | for i in range(min(k, len(similarities))): 434 | idx, score = similarities[i] 435 | results.append({ 436 | "text": self.texts[idx], # 添加对应的文本 437 | "metadata": self.metadata[idx], # 添加对应的元数据 438 | "similarity": score # 添加相似度分数 439 | }) 440 | 441 | return results # 返回前k个最相似项的列表 442 | 443 | 444 | def generate_response(query, retrieved_chunks): 445 | """ 446 | 根据查询和检索到的块生成响应。 447 | 448 | Args: 449 | query (str): 用户查询 450 | retrieved_chunks (List[Dict]): 从分层搜索中检索到的块 451 | 452 | Returns: 453 | str: 生成的响应 454 | """ 455 | # 从块中提取文本并准备上下文部分 456 | context_parts = [] 457 | 458 | for i, chunk in enumerate(retrieved_chunks): 459 | page_num = chunk["metadata"]["page"] # 从元数据中获取页码 460 | context_parts.append(f"[Page {page_num}]: {chunk['text']}") # 使用页码格式化块文本 461 | 462 | # 将所有上下文部分合并为一个上下文字符串 463 | context = "\n\n".join(context_parts) 464 | 465 | # 定义系统消息以指导AI助手 466 | system_message = """你是一个乐于助人的AI助手,根据提供的上下文回答问题。 467 | 请准确利用上下文中的信息来回答用户的问题。 468 | 如果上下文中不包含相关信息,请予以说明。 469 | 引用具体信息时请注明页码。""" 470 | 471 | # 使用OpenAI API生成响应 472 | response = client.chat.completions.create( 473 | model=llm_model, # 指定要使用的模型 474 | messages=[ 475 | {"role": "system", "content": system_message}, # 系统消息以指导助手 476 | {"role": "user", "content": f"上下文内容:\n\n{context}\n\n查询问题: {query}"} # 包含上下文和查询的用户消息 477 | ], 478 | temperature=0.2 # 设置用于响应生成的温度 479 | ) 480 | 481 | # 返回生成的响应内容 482 | return response.choices[0].message.content 483 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RAG All Techniques(ZH) 2 | 3 | > ✨ 此仓库为中文的实现过程,并将每一步中文注释,且都跑通实现,目前基于文本进行实现,没有实现多模态的RAG。详细的内容可以去看 [RESOURCES](#RESOURCES) 哦。 4 | 5 | 该仓库采用清晰、动手实践的方法来处理检索增强生成(RAG),将高级技术分解为简单易懂的实现。这里不依赖于像 `LangChain` 或 `FAISS` 这样的框架,而是使用熟悉的 Python 库 `openai` 、 `numpy` 、 `fitz(pymupdf)` 以及其他一些库来实现。 6 | 7 | 目标很简单:提供易于阅读、修改和学习的代码。通过关注基础,这个项目有助于揭开 RAG 的神秘面纱,使人们更容易理解它真正的工作原理。 8 | 9 | | Notebook | Description | 10 | | ------------------------------------------------------------ | ------------------------------------------------------------ | 11 | | [01. Simple RAG ](#Simple-RAG) | 简单的 RAG:基本的 RAG 实现 | 12 | | [02. Semantic Chunking](#Semantic-Chunking) | 语义切分:根据语义相似性分割文本,以形成更有意义的块。 | 13 | | [03. Context Enriched Retrieval RAG](#Context-Enriched-Retrieval) | 上下文增强检索:获取相邻块以提供更多上下文。 | 14 | | [04. Contextual Chunk Headers](#Contextual-Chunk-Headers) | 上下文分块标题:在嵌入之前,为每个片段添加描述性标题。 | 15 | | [05. Document Augmentation RAG](#Document-Augmentation) | 文档增强:从文本片段生成问题以增强检索过程。 | 16 | | [06. Query Transformation](#Query-Transformation) | 查询转换:重新编写、扩展或分解查询以提高检索效果。包括回退提示和子查询分解。 | 17 | | [07. Reranker](#Reranker) | 重新排序器:使用 LLM 对最初检索到的结果进行重排,以获得更好的相关性。 | 18 | | [08. RSE](#RSE) | 相关段落提取:识别并重建连续的文本段落,保留上下文。 | 19 | | [09. Contextual Compression](#Contextual-Compression) | 上下文压缩:实现上下文压缩以过滤和压缩检索到的块,最大化相关信息。 | 20 | | [10. Feedback Loop](#Feedback-Loop) | 反馈循环:随时间推移,通过用户反馈学习并改进 RAG 系统。 | 21 | | [11. Adaptive RAG](#Adaptive-RAG) | 适应性 RAG:根据查询类型动态选择最佳检索策略。 | 22 | | [12. Self RAG](#Self-RAG) | Self-RAG:动态决定何时以及如何检索,评估相关性,并评估支持和效用。 | 23 | | [13. Fusion RAG](#Fusion-RAG) | 融合RAG:结合向量搜索和基于关键词(BM25)的检索,以改善结果。 | 24 | | [14. Graph RAG](#Graph-RAG) | 将知识组织为图,使相关概念能够遍历。 | 25 | | [15. Hierarchy RAG](#Hierarchical-Indices) | 构建层次索引(摘要+详细片段),以实现高效检索。 | 26 | | [16. HyDE RAG](#HyDE) | 使用假设文档嵌入来提高语义匹配。 | 27 | | [17. CRAG](#CRAG) | 纠错型RAG:动态评估检索质量,并使用网络搜索作为后备 | 28 | | **分块技术** | | 29 | | [Proposition Chunking 命题分块](#命题分块技术) | 将文档分解为原子事实陈述,以实现精确检索。 | 30 | | [Chunk Size Selector 块大小选择器](#不同块大小) | 探讨不同块大小对检索性能的影响。 | 31 | 32 | ## Simple RAG 33 | 34 | 检索增强生成(RAG)是一种混合方法,它结合了信息检索与生成模型。通过结合外部知识,它增强了语言模型的表现,提高了准确性和事实的正确性。 35 | 36 | ----- 37 | 实现步骤: 38 | 39 | - **Data Ingestion(数据采集)**: 加载和预处理文本数据。 40 | - **Chunking(分块处理)**: 将数据分割成更小的块以提高检索性能。 41 | - **Embedding Creation(嵌入创建)**: 使用嵌入模型将文本块转换为数值表示。 42 | - **Semantic Search(语义搜索)**: 根据用户查询检索相关块。 43 | - **Response Generation(响应生成)**:使用语言模型根据检索到的文本生成响应。 44 | 45 | ![](data/images/1_VL2fP0HuSoqc66GC1vbA7w.webp) 46 | 47 | ---- 48 | 49 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/01_simple_rag.ipynb) 50 | 51 | ## Semantic Chunking 52 | 53 | 文本块切分是检索增强生成(RAG)中的关键步骤,其中将大文本体分割成有意义的段落以提高检索准确性。与固定长度块切分不同,语义块切分是根据句子之间的内容相似性来分割文本的。 54 | 55 | ------ 56 | 实现步骤: 57 | 58 | - 从PDF文件中提取文本:按句子进行切分 59 | - 提取的文本创建语义分块: 60 | - 将前后两个相邻的句子计算相似度 61 | - 根据相似度下降计算分块的断点,断点方法有三种:百分位、标准差和四分位距 62 | - 然后根据断点分割文本,得到语义块 63 | - 创建嵌入 64 | - 根据查询并检索文档 65 | - 将检索出来的文本用于模型生成回答 66 | 67 | ![](data/images/1_7b5LkCrQ7XhUN9Ktu3Q1sQ.webp) 68 | 69 | ---- 70 | 71 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/02_semantic_chunking.ipynb) 72 | - 核心函数,请查看,[点击跳转🖱️](src/core/02_semantic_chunking_core.py) 73 | 74 | ## Context Enriched Retrieval 75 | 76 | 检索增强生成(RAG)通过从外部来源检索相关知识来增强 AI 的响应。传统的检索方法返回孤立的文本片段,这可能导致答案不完整。 77 | 为了解决这个问题,我们引入了上下文增强检索,确保检索到的信息包括相邻的片段,以实现更好的连贯性。 78 | 79 | ------ 80 | 实现步骤: 81 | 82 | - 数据采集:从 PDF 中提取文本 83 | - 重叠上下文分块:将文本分割成重叠的块以保留上下文 84 | - 嵌入创建:将文本块转换为数值表示 85 | - 上下文感知检索:检索相关块及其邻居以获得更好的完整性 86 | - 回答生成:使用语言模型根据检索到的上下文生成回答。 87 | - 评估:使用评估数据集评估模型性能。 88 | 89 | ![](data/images/1_RtkAa3uP2XlaRs8uUAduwA.webp) 90 | 91 | ---- 92 | 93 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/04_context_enriched_rag.ipynb) 94 | - 核心函数,请查看,[点击跳转🖱️](src/core/04_context_enriched_rag_core.py) 95 | 96 | ## Contextual Chunk Headers 97 | 98 | 通过在生成响应之前检索相关外部知识,检索增强生成(RAG)提高了语言模型的事实准确性。然而,标准的分块往往丢失重要的上下文,使得检索效果不佳。 99 | 上下文块标题(CCH)通过在每个块嵌入之前添加高级上下文(如文档标题或部分标题)来增强 RAG。这提高了检索质量,并防止了不相关的响应。 100 | 101 | ------ 102 | 实现步骤: 103 | 104 | - 数据采集:从 PDF 中提取文本 105 | - **带上下文标题的块分割:提取章节标题(或使用模型为块生成标题)并将其添加到块的开头。** 106 | - 嵌入创建:将文本块转换为数值表示 107 | - 语义搜索:根据用户查询检索相关块 108 | - 回答生成:使用语言模型根据检索到的上下文生成回答。 109 | - 评估:使用评估数据集评估模型性能。 110 | 111 | ![](data/images/Contextual Chunk Headers.webp) 112 | 113 | ---- 114 | 115 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/05_contextual_chunk_headers_rag.ipynb) 116 | - 核心函数,请查看,[点击跳转🖱️](src/core/05_contextual_chunk_headers_rag_core.py) 117 | 118 | ## Document Augmentation 119 | 120 | 增强的 RAG 方法,通过文档增强和问题生成来改进。通过为每个文本块生成相关的问题,我们提高了检索过程,从而使得语言模型能够提供更好的响应。 121 | 122 | ------ 123 | 实现步骤: 124 | 125 | - 数据采集:从 PDF 文件中提取文本。 126 | - 分块处理:将文本分割成可管理的块 127 | - **生成问题:为每个块生成相关的问题。** 128 | - 创建嵌入:为块和生成的问题创建嵌入。 129 | - 向量存储创建:使用 NumPy 构建简单的向量存储。 130 | - 语义搜索:检索与用户查询相关的片段和问题。 131 | - 响应生成:根据检索到的内容生成答案。 132 | - 评估:评估生成响应的质量。 133 | 134 | ![](data/images/Document Augmentation.webp) 135 | 136 | ---- 137 | 138 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/06_doc_augmentation_rag.ipynb) 139 | - 核心函数,请查看,[点击跳转🖱️](src/core/06_doc_augmentation_rag_core.py) 140 | 141 | ## Query Transformation 142 | 143 | 通过修改用户查询,我们可以显著提高检索信息的关联性和全面性。 144 | 145 | 实现了三种查询转换技术,以在不依赖 LangChain 等专用库的情况下增强 RAG 系统中的检索性能。 146 | 147 | ------ 148 | 核心概念 149 | 150 | 1. 查询重写:使查询更加具体和详细,从而提高搜索精度。 151 | 152 | 2. 回退提示:生成更广泛的查询,以检索有用的上下文信息。 153 | 154 | 3. 子查询分解:将复杂查询拆分为更简单的组件,以实现全面检索。 155 | 156 | ------ 157 | 实现步骤: 158 | 159 | - 处理文档以创建向量存储:从PDF 中提取文本,分割文本块并创建向量存储 160 | - 应用查询转换技术: 161 | - 查询重写(Query Rewriting):通过使查询更加具体和详细,从而提高检索的准确性 162 | - 回退提示(Step-back Prompting):生成更广泛的查询以检索上下文背景信息 163 | - 子查询分解(Sub-query Decomposition):将复杂查询拆分为更简单的组成部分,以实现全面检索 164 | - 通过上面的查询转换,创建新查询嵌入并检索文档 165 | - 根据检索到的内容生成回答 166 | 167 | ![](data/images/Query Transformation.webp) 168 | 169 | ---- 170 | 171 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/07_query_transform.ipynb) 172 | - 核心函数,请查看,[点击跳转🖱️](src/core/07_query_transform_core.py) 173 | 174 | ## Reranker 175 | 176 | 实现重排序技术以提高 RAG 系统中的检索质量。重排序作为初始检索后的第二步过滤,确保使用最相关的内容进行响应生成。 177 | 178 | ------ 179 | 核心概念 180 | 181 | 1. **初始检索**:第一轮使用基础相似性搜索(速度较快但准确性较低) 182 | 2. **文档评分**:评估每个检索到的文档与查询的相关性 183 | 3. **重新排序**:按相关性分数对文档进行排序 184 | 4. **选择**:仅使用最相关的文档进行响应生成 185 | 186 | ------ 187 | 实现步骤: 188 | 189 | - 处理文档以创建向量存储:从PDF 中提取文本,分割文本块并创建向量存储 190 | - 创建查询嵌入并检索文档:初步查询,需要召回较多文档 191 | - 应用重排: 192 | - 基于 LLM 的重排序:使用 LLM 相关性评分对搜索结果进行重排序 193 | - 基于关键词的重排序:关键词匹配的次数和关键词所在位置的简单重排序方法 194 | - 利用重排序组合成的上下文生成回答 195 | 196 | ![](data/images/Reranker.webp) 197 | 198 | ----- 199 | 200 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/08_reranker.ipynb) 201 | - 核心函数,请查看,[点击跳转🖱️](src/core/08_reranker_core.py) 202 | 203 | ## RSE 204 | 205 | 关联段落提取(Relevant Segment Extraction,RSE)技术,以提高RAG系统的上下文质量。RSE不仅仅检索一组孤立的片段,而是识别并重建提供更好上下文的连续文本段落,从而为语言模型提供更好的支持。 206 | 207 | ----- 208 | 核心概念: 209 | 210 | 相关的片段往往会在文档中聚集成簇。通过识别这些簇并保持其连续性,RSE为大型语言模型提供了更加连贯的上下文。 211 | 212 | ----- 213 | 实现步骤: 214 | 215 | - 处理文档以创建向量存储:从PDF 中提取文本,分割文本块(0重叠)并创建向量存储 216 | - 根据查询计算相关性分数和块值: 217 | - 先获取所有带有相似度分数的块; 218 | - 获取相关性分数,如果不在结果中则默认为0,同时应用惩罚以将不相关的块转换为负值 219 | - 根据块值找到最佳文本块:使用最大子数组和算法的变体找到最佳 220 | - 从最佳块中重建文本段落:基于块索引重建文本段落 221 | - 利用将段落格式化为上下文生成回答 222 | 223 | ![](data/images/RSE.webp) 224 | 225 | ---- 226 | 227 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/09_rse.ipynb) 228 | - 核心函数,请查看,[点击跳转🖱️](src/core/09_rse_core.py) 229 | 230 | ## Contextual Compression 231 | 232 | 上下文情境压缩技术(Contextual Compression),以提高 RAG 系统的效率。过滤并压缩检索到的文本块,只保留最相关的内容,从而减少噪声并提高响应质量。 233 | 234 | 在为 RAG 检索文档时,经常得到包含相关和不相关信息的块。上下文压缩可以帮助我们: 235 | 236 | - 删除无关的句子和段落 237 | - 仅关注与查询相关的信息 238 | - 在上下文窗口中最大化有用信号 239 | 240 | 本文提供了三种方法: 241 | 242 | 1. 过滤(selective):分析文档块并仅提取与用户查询直接相关的句子或段落,移除所有无关内容。 243 | 2. 摘要(summary):创建文档块的简洁摘要,且仅聚焦与用户查询相关的信息。 244 | 3. 抽取(extraction):从文档块中精确提取与用户查询相关的完整句子。 245 | 246 | ----- 247 | 实现步骤: 248 | 249 | - 处理文档以创建向量存储:从PDF 中提取文本,分割文本块并创建向量存储 250 | - 创建查询嵌入并检索文档,检索最相似的前k个块 251 | - 对检索到的块应用压缩: 252 | - 过滤(selective):分析文档块并仅提取与用户查询直接相关的句子或段落,移除所有无关内容。 253 | - 摘要(summary):创建文档块的简洁摘要,且仅聚焦与用户查询相关的信息 254 | - 抽取(extraction):从文档块中精确提取与用户查询相关的完整句子 255 | - 过滤掉任何空的压缩块 256 | - 基于压缩块形成上下文内容,然后生成回答 257 | 258 | ![](data/images/Contextual Compression.webp) 259 | 260 | ---- 261 | 262 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/10_contextual_compression.ipynb) 263 | - 核心函数,请查看,[点击跳转🖱️](src/core/10_contextual_compression_core.py) 264 | 265 | ## Feedback Loop 266 | 267 | 实现一个具备反馈循环机制的RAG系统,通过持续学习实现性能迭代优化。系统将收集并整合用户反馈数据,使每次交互都能提升回答的相关性与质量。 268 | 269 | ----- 270 | 传统RAG系统采用静态检索模式,仅依赖嵌入相似性获取信息。而本系统通过反馈循环构建动态优化框架,实现: 271 | 272 | - 记忆有效/无效的交互模式 273 | - 动态调整文档相关性权重 274 | - 将优质问答对整合至知识库 275 | - 通过用户互动持续增强智能水平 276 | 277 | ----- 278 | 实现步骤: 279 | 280 | - 加载历史反馈数据集 281 | - 文档预处理与分块处理 282 | - 可选基于历史反馈微调向量索引 283 | - 基于反馈修正的相关性评分执行检索与生成 284 | - 收集新用户反馈数据用于后续优化 285 | - 持久化存储反馈数据支撑系统持续学习能力 286 | 287 | ![](data/images/Feedback Loop.webp) 288 | 289 | ---- 290 | 291 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/11_feedback_loop_rag.ipynb) 292 | - 核心函数,请查看,[点击跳转🖱️](src/core/11_feedback_loop_rag_core.py) 293 | 294 | ## Adaptive RAG 295 | 296 | 自适应检索(Adaptive Retrieval)系统可根据查询类型动态选择最优检索策略,该方法显著提升RAG系统在多样化问题场景下的响应准确性与相关性。 297 | 298 | ----- 299 | 不同查询类型需匹配差异化检索策略,本系统实现四阶优化流程: 300 | 301 | 1. 查询类型分类(事实性/分析性/意见性/上下文型) 302 | 2. 自适应选择检索策略 303 | 3. 执行专门的检索技术 304 | 4. 生成定制化响应 305 | 306 | ----- 307 | 实现步骤: 308 | 309 | - 处理文档以提取文本,将其分块,并创建嵌入向量 310 | - 对查询进行分类以确定其类型:查询分为事实性(Factual)、分析性(Analytical)、意见性(Opinion)或上下文相关性(Contextual)。 311 | - 根据查询类型使用自适应检索策略检索文档 312 | - 根据查询、检索到的文档和查询类型生成回答 313 | 314 | ![](data/images/Adaptive RAG.webp) 315 | 316 | ---- 317 | 318 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/12_adaptive_rag.ipynb) 319 | - 核心函数,请查看,[点击跳转🖱️](src/core/12_adaptive_rag_core.py) 320 | 321 | ## Self RAG 322 | 323 | Self-RAG——这是一个先进的 RAG 系统,能够动态决定何时以及如何使用检索到的信息。与传统 RAG 方法不同,Self-RAG 324 | 在整个检索和生成过程中引入了反思点,从而产生更高品质和更可靠的回答。 325 | 326 | ----- 327 | Self-RAG 核心组件 328 | 329 | 1. **检索决策**:判断特定查询是否需要执行检索 330 | 2. **文档检索**:在需要时获取潜在相关文档 331 | 3. **相关性评估**:评估每个检索文档的相关程度 332 | 4. **响应生成**:基于相关上下文构建回答 333 | 5. **支持性评估**:验证回答是否基于上下文正确锚定 334 | 6. **效用评估**:对生成回答的整体实用性进行评分 335 | 336 | ----- 337 | 实现步骤: 338 | 339 | - 确定是否需要检索,检索决策:LLM判断是否需要检索,事实性的查询需要检索,观点类的查询不需要检索 340 | - 检索文档:创建查询的嵌入向量然后检索文档 341 | - 评估文档相关性:用模型评估每个文档块的相关性,如果相关,将其加入上下文列表中 342 | - 处理每个相关上下文,用模型进行支持性评估和效用评估 343 | - 找出最佳答案:对生成的答案进行效用评估,并选择最佳答案 344 | 345 | ![](data/images/Self RAG.webp) 346 | 347 | ---- 348 | 349 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/13_self_rag.ipynb) 350 | - 核心函数,请查看,[点击跳转🖱️](src/core/13_self_rag_core.py) 351 | 352 | ## Fusion RAG 353 | 354 | 实现一个融合检索系统,将语义向量搜索与基于关键词的BM25检索优势相结合。这种方法通过同时捕获概念相似性和精确关键词匹配,提升了检索质量。 355 | 356 | ----- 357 | 为什么融合检索很重要? 358 | 359 | 传统的RAG系统通常仅依赖向量搜索,但这存在局限性: 360 | 361 | - 向量搜索擅长捕捉语义相似性,但可能遗漏精确关键词匹配 362 | - 关键词搜索适合特定术语检索,但缺乏语义理解能力 363 | - 不同类型的查询在不同检索方法中表现差异显著 364 | 365 | ----- 366 | 融合检索通过以下方式实现优势互补: 367 | 368 | - 并行执行基于向量和基于关键词的检索 369 | - 对两种方法的得分进行标准化处理 370 | - 通过加权公式组合两者结果 371 | - 基于综合得分对文档进行重新排序 372 | 373 | ----- 374 | 实现步骤: 375 | 376 | - 从 PDF 文件中提取文本 377 | - 使用 jieba 分词器对文本进行分词,并创建向量存储 378 | - 使用 BM25 算法对查询进行关键词匹配 379 | - 使用向量搜索对查询进行语义匹配 380 | - 将两种方法的结果进行加权组合,并重新排序 381 | - 返回最终的搜索结果 382 | 383 | ![](data/images/Fusion.webp) 384 | 385 | ---- 386 | 387 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/16_fusion_rag.ipynb) 388 | - 核心函数,请查看,[点击跳转🖱️](src/core/16_fusion_rag_core.py) 389 | 390 | ## Graph RAG 391 | 392 | Graph RAG - 一种通过将知识组织为连接图来增强传统RAG系统的技巧,而不是将知识作为平面文档集合。这使得系统能够导航相关概念,并找到比标准向量相似性方法更符合上下文的相关信息。 393 | 394 | ------ 395 | Graph RAG的关键优势 396 | 397 | - 保留信息之间的关系 398 | - 通过连接的概念启用上下文的遍历 399 | - 提高处理复杂、多部分查询的能力 400 | - 通过可视化知识路径提供更好的解释性 401 | 402 | ------ 403 | 实现步骤: 404 | 405 | - 从PDF中提取文本 406 | - 将提取的文本分割成重叠的块 407 | - 从文本块构建知识图谱,用模型从文本块中总结出关键概念,然后利用关键概念构建知识图谱节点,根据概念重叠和语义相似性计算边权重,设置边,从而创建知识图谱 408 | - 遍历知识图谱以找到与查询相关的信息,计算查询与所有节点之间的相似度,按相似度排序(降序),获取最相似的前 top-k 409 | 个节点作为起点,使用优先队列进行广度遍历,查找出与查询最相关的信息 410 | - 根据查询和相关块生成响应 411 | 412 | ![](data/images/Knowledge Graph.webp) 413 | 414 | ---- 415 | 416 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/17_graph_rag.ipynb) 417 | - 核心函数,请查看,[点击跳转🖱️](src/core/17_graph_rag_core.py) 418 | 419 | ## Hierarchical Indices 420 | 421 | 实现一种用于RAG系统的分级索引方法(Hierarchical Indices)。这种技术通过使用两级搜索方法来提高检索效果:首先通过摘要识别相关的文档部分,然后从这些部分中检索具体细节。 422 | 423 | ----- 424 | 传统的RAG方法将所有文本块一视同仁,这可能导致: 425 | 426 | - 当文本块过小时,上下文信息丢失 427 | - 当文档集合较大时,检索结果无关 428 | - 在整个语料库中搜索效率低下 429 | 430 | ----- 431 | 分级检索解决了这些问题,具体方式如下: 432 | 433 | - 为较大的文档部分创建简洁的摘要 434 | - 首先搜索这些摘要以确定相关部分 435 | - 然后仅从这些部分中检索详细信息 436 | - 在保留具体细节的同时保持上下文信息 437 | 438 | ----- 439 | 实现步骤: 440 | 441 | - 从 PDF 中提取页面 442 | - 为每一页创建摘要,将摘要文本和元数据添加到摘要列表中 443 | - 为每一页创建详细块,将页面的文本切分为块 444 | - 为以上两个创建嵌入,并行其存入向量存储中 445 | - 使用查询分层检索相关块:先检索相关的摘要,收集来自相关摘要的页面,然后过滤掉不是相关页面的块,从这些相关页面检索详细块 446 | - 根据检索到的块生成回答 447 | 448 | ![](data/images/Hierarchical Indices.webp) 449 | 450 | ---- 451 | 452 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/18_hierarchy_rag.ipynb) 453 | - 核心函数,请查看,[点击跳转🖱️](src/core/18_hierarchy_rag_core.py) 454 | 455 | ## HyDE 456 | 457 | 实现 HyDE(Hypothetical Document Embedding,假设文档嵌入)——这是一种创新的检索技术,它将用户查询转化为假设性的回答文档后再进行检索。这种方法弥补了短查询与长文档之间的语义差距。 458 | 459 | ------ 460 | 传统的 RAG(Retrieval-Augmented Generation,检索增强生成)系统是直接对用户的简短查询进行嵌入处理,但这种方式往往无法捕捉到最佳检索所需的丰富语义信息。HyDE 461 | 通过以下方式解决了这一问题: 462 | 463 | - **生成一个假设性文档来回答该查询** 464 | - **对该扩展后的文档进行嵌入,而非原始查询** 465 | - **检索与该假设文档相似的文档** 466 | - **从而生成更加上下文相关的回答** 467 | 468 | ------ 469 | 实现步骤: 470 | 471 | - 从PDF文件中提取文本内容 472 | - 分割文本为块 473 | - 创建一个向量存储,将文本块和元数据存储为向量 474 | - 根据用户查询,利用模型回答用户的查询(生成假设性文档) 475 | - 为假设文档创建嵌入 476 | - 根据假设文档检索相似的片段 477 | - 然后利用检索到的片段构成上下文,生成回答 478 | 479 | ![](data/images/HyDE.webp) 480 | 481 | ---- 482 | 483 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/19_HyDE_rag.ipynb) 484 | - 核心函数,请查看,[点击跳转🖱️](src/core/19_HyDE_rag_core.py) 485 | 486 | ## Crag 487 | 488 | 实现**纠错型RAG(Corrective RAG)**——一种先进的方法,能够动态评估检索到的信息,并在必要时对检索过程进行修正,使用网络搜索作为备选方案。 489 | 490 | ----- 491 | CRAG 在传统 RAG 的基础上进行了以下改进: 492 | 493 | - 在使用前对检索到的内容进行评估 494 | - 根据相关性动态切换不同的知识源 495 | - 当本地知识不足以回答问题时,通过网络搜索修正检索结果 496 | - 在适当时合并多个来源的信息 497 | 498 | ----- 499 | 实现步骤: 500 | 501 | - 处理文档并创建向量数据库 502 | - 创建查询嵌入并检索文档 503 | - 评估文档相关性:对检索到的内容进行评估。 504 | - 根据情况执行相应的知识获取策略:高相关性(评估分数>0.7),直接使用文档内容;低相关性(评估分数< 505 | 0.3)使用网络搜索;中等相关性(0.3-0.7)结合文档与网络搜索结果,并将文档结果与网络搜索结果进行合并。在混合搜索中,需要将搜索出来的内容,进行模型提炼,避免内容重复冗余。 506 | - 生成最终回答 507 | 508 | ![](data/images/Crag.webp) 509 | 510 | ---- 511 | 512 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/20_crag.ipynb) 513 | - 核心函数,请查看,[点击跳转🖱️](src/core/20_crag_core.py) 514 | 515 | ## 分块技术 516 | 517 | ### 不同块大小 518 | 519 | 选择合适的块大小对于提高检索增强生成(RAG)管道的检索准确性至关重要。目标是平衡检索性能与响应质量。 520 | 521 | ----- 522 | 以下方式评估不同的块大小: 523 | 524 | - 从 PDF 中提取文本 525 | - 将文本分割成不同大小的块 526 | - 为每个块创建嵌入 527 | - 为查询检索相关块 528 | - 使用检索到的块生成响应 529 | - 评估响应质量 530 | - 比较不同块大小的结果 531 | 532 | ----- 533 | 实现步骤: 534 | 535 | - 从 PDF 中提取文本:按页获取页面文本 536 | - 将文本分割成不同大小的块,为每个块创建嵌入 537 | - 根据查询检索相关块 538 | - 使用检索到的文本块用模型生成回答 539 | - 评估不同大小块的检索回答质量 540 | 541 | ---- 542 | 543 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/03_chunk_size_selector.ipynb) 544 | - 核心函数,请查看,[点击跳转🖱️](src/core/03_chunk_size_selector_core.py) 545 | 546 | ### 命题分块技术 547 | 548 | 命题分块技术(Proposition Chunking)——这是一种通过将文档分解为原子级事实陈述来实现更精准检索的先进方法。与传统仅按字符数分割文本的分块方式不同,命题分块能保持单个事实的语义完整性。 549 | 550 | ----- 551 | 命题分块通过以下方式实现更精确的检索: 552 | 553 | 1. 将内容分解为原子化的、自包含的事实陈述 554 | 2. 创建更小、更细粒度的检索单元 555 | 3. 实现查询与相关内容间更精确的匹配 556 | 4. 过滤掉低质量或不完整的命题 557 | 558 | ----- 559 | 实现步骤: 560 | 561 | - 从PDF文件中提取文本 562 | - 从提取的文本创建分块 563 | - 分块生成命题 564 | - 为每个命题进行评估 565 | 566 | ---- 567 | 568 | - 具体代码实现过程与分析,请查看,[点击跳转🖱️](src/full/14_proposition_chunking.ipynb) 569 | - 核心函数,请查看,[点击跳转🖱️](src/core/14_propostion_chunking_core.py) 570 | 571 | ## RESOURCES: 572 | - https://levelup.gitconnected.com/testing-18-rag-techniques-to-find-the-best-094d166af27f 573 | - https://github.com/FareedKhan-dev/all-rag-techniques -------------------------------------------------------------------------------- /src/core/11_feedback_loop_rag_core.py: -------------------------------------------------------------------------------- 1 | """ 2 | 反馈循环机制(Feedback Loop) 核心函数 3 | """ 4 | import os 5 | import json 6 | import fitz 7 | import numpy as np 8 | from openai import OpenAI 9 | from dotenv import load_dotenv 10 | from datetime import datetime 11 | 12 | load_dotenv() 13 | 14 | client = OpenAI( 15 | base_url=os.getenv("LLM_BASE_URL"), 16 | api_key=os.getenv("LLM_API_KEY") 17 | ) 18 | llm_model = os.getenv("LLM_MODEL_ID") 19 | embedding_model = os.getenv("EMBEDDING_MODEL_ID") 20 | 21 | 22 | #################################### 23 | # 反馈循环机制的RAG完整流程 24 | #################################### 25 | def full_rag_workflow(pdf_path, query, feedback_data=None, feedback_file="feedback_data.json", fine_tune=False): 26 | """ 27 | 协调执行完整的RAG工作流,集成反馈机制实现持续优化提升。 28 | 29 | 本函数系统化执行检索增强生成(RAG)全流程: 30 | 1. 加载历史反馈数据集 31 | 2. 文档预处理与分块处理 32 | 3. 可选基于历史反馈微调向量索引 33 | 4. 基于反馈修正的相关性评分执行检索与生成 34 | 5. 收集新用户反馈数据用于后续优化 35 | 6. 持久化存储反馈数据支撑系统持续学习能力 36 | 37 | Args: 38 | pdf_path (str): 要处理的PDF文档路径 39 | query (str): 用户的自然语言查询 40 | feedback_data (List[Dict], optional): 预加载的反馈数据,如果为None则从文件加载 41 | feedback_file (str): 存储反馈历史的JSON文件路径 42 | fine_tune (bool): 是否通过成功的过往问答对来增强索引 43 | 44 | Returns: 45 | Dict: 包含响应和检索元数据的结果 46 | """ 47 | # 第1步:如果未明确提供,则加载历史反馈数据以进行相关性调整 48 | if feedback_data is None: 49 | feedback_data = load_feedback_data(feedback_file) 50 | print(f"从 {feedback_file} 加载了 {len(feedback_data)} 条反馈记录") 51 | 52 | # 第2步:通过提取、分块和嵌入管道处理文档 53 | chunks, vector_store = process_document(pdf_path) 54 | 55 | # 第3步:通过结合高质量的过往交互微调向量索引 56 | # 这将从成功的问答对中创建增强的可检索内容 57 | if fine_tune and feedback_data: 58 | vector_store = fine_tune_index(vector_store, chunks, feedback_data) 59 | 60 | # 第4步:执行核心RAG并使用反馈感知检索 61 | # 注意:这依赖于rag_with_feedback_loop函数,应在其他地方定义 62 | result = rag_with_feedback_loop(query, vector_store, feedback_data) 63 | 64 | # 第5步:收集用户反馈以改进未来的表现 65 | print("\n=== 您是否愿意对这个响应提供反馈? ===") 66 | print("评分相关性(1-5,5表示最相关):") 67 | relevance = input() 68 | 69 | print("评分质量(1-5,5表示最高质量):") 70 | quality = input() 71 | 72 | print("有任何评论吗?(可选,按Enter跳过)") 73 | comments = input() 74 | 75 | # 第6步:将反馈格式化为结构化数据 76 | feedback = get_user_feedback( 77 | query=query, 78 | response=result["response"], 79 | relevance=int(relevance), 80 | quality=int(quality), 81 | comments=comments 82 | ) 83 | 84 | # 第7步:持久化反馈以实现系统的持续学习 85 | store_feedback(feedback, feedback_file) 86 | print("反馈已记录。感谢您的参与!") 87 | 88 | return result 89 | 90 | 91 | #################################### 92 | # 完整的RAG 93 | #################################### 94 | def rag_with_feedback_loop(query, vector_store, feedback_data, k=5): 95 | """ 96 | 完整的RAG管道,包含反馈循环。 97 | 98 | Args: 99 | query (str): 用户查询 100 | vector_store (SimpleVectorStore): 包含文档块的向量存储 101 | feedback_data (List[Dict]): 反馈历史 102 | k (int): 检索的文档数量 103 | 104 | Returns: 105 | Dict: 包括查询、检索到的文档和响应的结果 106 | """ 107 | print(f"\n=== 使用反馈增强型RAG处理查询 ===") 108 | print(f"查询: {query}") 109 | 110 | # 第1步:创建查询嵌入 111 | query_embedding = create_embeddings(query) 112 | 113 | # 第2步:基于查询嵌入执行初始检索 114 | results = vector_store.similarity_search(query_embedding, k=k) 115 | 116 | # 第3步:根据反馈调整检索到的文档的相关性分数 117 | adjusted_results = adjust_relevance_scores(query, results, feedback_data) 118 | 119 | # 第4步:从调整后的结果中提取文本以构建上下文 120 | retrieved_texts = [result["text"] for result in adjusted_results] 121 | 122 | # 第5步:通过连接检索到的文本构建用于生成响应的上下文 123 | context = "\n\n---\n\n".join(retrieved_texts) 124 | 125 | # 第6步:使用上下文和查询生成响应 126 | print("正在生成响应...") 127 | response = generate_response(query, context) 128 | 129 | # 第7步:编译最终结果 130 | result = { 131 | "query": query, 132 | "retrieved_documents": adjusted_results, 133 | "response": response 134 | } 135 | 136 | print("\n=== 回答 ===") 137 | print(response) 138 | 139 | return result 140 | 141 | 142 | #################################### 143 | # 调用LLM评估每个历史反馈项的相关性 144 | #################################### 145 | def assess_feedback_relevance(query, doc_text, feedback): 146 | """ 147 | 调用大语言模型(LLM)判定历史反馈条目与当前查询及文档的关联性。 148 | 149 | 该函数通过向LLM提交以下内容实现智能判定: 150 | 1. 当前查询语句 151 | 2. 历史查询及对应反馈数据 152 | 3. 关联文档内容 153 | 最终确定哪些历史反馈应影响当前检索优化。 154 | 155 | Args: 156 | query (str): 当前需要信息检索的用户查询 157 | doc_text (str): 正在评估的文档文本内容 158 | feedback (Dict): 包含 'query' 和 'response' 键的过去反馈数据 159 | 160 | Returns: 161 | bool: 如果反馈被认为与当前查询/文档相关,则返回True,否则返回False 162 | """ 163 | # 定义系统提示,指示LLM仅进行二元相关性判断 164 | system_prompt = """您是专门用于判断历史反馈与当前查询及文档相关性的AI系统。 165 | 请仅回答 'yes' 或 'no'。您的任务是严格判断相关性,无需提供任何解释。""" 166 | 167 | # 构造用户提示,包含当前查询、过去的反馈数据以及截断[truncated]的文档内容 168 | user_prompt = f""" 169 | 当前查询: {query} 170 | 收到反馈的历史查询: {feedback['query']} 171 | 文档内容: {doc_text[:500]}... [截断] 172 | 收到反馈的历史响应: {feedback['response'][:500]}... [truncated] 173 | 174 | 该历史反馈是否与当前查询及文档相关?(yes/no) 175 | """ 176 | 177 | # 调用LLM API,设置温度为0以获得确定性输出 178 | response = client.chat.completions.create( 179 | model=llm_model, 180 | messages=[ 181 | {"role": "system", "content": system_prompt}, 182 | {"role": "user", "content": user_prompt} 183 | ], 184 | temperature=0 # 使用温度=0以确保一致性和确定性响应 185 | ) 186 | 187 | # 提取并规范化响应以确定相关性 188 | answer = response.choices[0].message.content.strip().lower() 189 | return 'yes' in answer # 如果答案中包含 'yes',则返回True 190 | 191 | 192 | #################################### 193 | # 根据反馈调整检索到的文档的相关性分数 194 | #################################### 195 | def adjust_relevance_scores(query, results, feedback_data): 196 | """ 197 | 基于历史反馈数据动态调整文档关联分数以优化检索质量。 198 | 199 | 本函数通过分析历史用户反馈实现以下优化流程: 200 | 1. 识别与当前查询上下文相关的历史反馈 201 | 2. 根据关联度分数(相关性评分)计算分数修正因子 202 | 3. 基于修正结果重排序检索文档 203 | 204 | Args: 205 | query (str): 当前用户查询 206 | results (List[Dict]): 检索到的文档及其原始相似度分数 207 | feedback_data (List[Dict]): 包含用户评分的历史反馈 208 | 209 | Returns: 210 | List[Dict]: 调整后的相关性分数结果,按新分数排序 211 | """ 212 | # 如果没有反馈数据,则返回原始结果不变 213 | if not feedback_data: 214 | return results 215 | 216 | print("基于反馈历史调整相关性分数...") 217 | 218 | # 处理每个检索到的文档 219 | for i, result in enumerate(results): 220 | document_text = result["text"] 221 | relevant_feedback = [] 222 | 223 | # 查找与此特定文档和查询组合相关的反馈 224 | # 通过调用LLM评估每个历史反馈项的相关性 225 | for feedback in feedback_data: 226 | is_relevant = assess_feedback_relevance(query, document_text, feedback) 227 | if is_relevant: 228 | relevant_feedback.append(feedback) 229 | 230 | # 如果存在相关反馈,则应用分数调整 231 | if relevant_feedback: 232 | # 计算所有适用反馈条目的平均相关性评分 233 | # 反馈相关性为1-5分(1=不相关,5=高度相关) 234 | avg_relevance = sum(f['relevance'] for f in relevant_feedback) / len(relevant_feedback) 235 | 236 | # 将平均相关性转换为范围在0.5-1.5的分数调整因子 237 | # - 低于3/5的分数将降低原始相似度(调整因子 < 1.0) 238 | # - 高于3/5的分数将增加原始相似度(调整因子 > 1.0) 239 | modifier = 0.5 + (avg_relevance / 5.0) 240 | 241 | # 将调整因子应用于原始相似度分数 242 | original_score = result["similarity"] 243 | adjusted_score = original_score * modifier 244 | 245 | # 更新结果字典中的新分数和反馈元数据 246 | result["original_similarity"] = original_score # 保留原始分数 247 | result["similarity"] = adjusted_score # 更新主分数 248 | result["relevance_score"] = adjusted_score # 更新相关性分数 249 | result["feedback_applied"] = True # 标记反馈已应用 250 | result["feedback_count"] = len(relevant_feedback) # 使用的反馈条目数量 251 | 252 | # 记录调整细节 253 | print( 254 | f" 文档 {i + 1}: 基于 {len(relevant_feedback)} 条反馈,分数从 {original_score:.4f} 调整为 {adjusted_score:.4f}") 255 | 256 | # 按调整后的分数重新排序结果,确保更高匹配质量的结果优先显示 257 | results.sort(key=lambda x: x["similarity"], reverse=True) 258 | 259 | return results 260 | 261 | 262 | #################################### 263 | # 通过结合高质量的过往交互微调向量索引 264 | # 这将从成功的问答对中创建增强的可检索内容 265 | #################################### 266 | def fine_tune_index(current_store, chunks, feedback_data): 267 | """ 268 | 通过高质量反馈数据增强向量存储,实现检索质量的持续优化。 269 | 270 | 本函数通过以下机制实现持续学习流程: 271 | 1. 筛选优质反馈数据(高评分问答对) 272 | 2. 将成功交互案例转化为检索条目 273 | 3. 为新增条目配置强化关联权重并注入向量库 274 | 275 | Args: 276 | current_store (SimpleVectorStore): 当前包含原始文档块的向量存储 277 | chunks (List[str]): 原始文档文本块 278 | feedback_data (List[Dict]): 用户的历史反馈数据,包含相关性和质量评分 279 | 280 | Returns: 281 | SimpleVectorStore: 增强后的向量存储,包含原始块和基于反馈生成的内容 282 | """ 283 | print("使用高质量反馈微调索引...") 284 | 285 | # 筛选出高质量反馈(相关性和质量评分均达到4或5) 286 | # 这确保我们仅从最成功的交互中学习 287 | good_feedback = [f for f in feedback_data if f['relevance'] >= 4 and f['quality'] >= 4] 288 | 289 | if not good_feedback: 290 | print("未找到可用于微调的高质量反馈。") 291 | return current_store # 如果没有高质量反馈,则返回原始存储不变 292 | 293 | # 初始化一个新的存储,它将包含原始内容和增强内容 294 | new_store = SimpleVectorStore() 295 | 296 | # 首先将所有原始文档块及其现有元数据转移到新存储中 297 | for i in range(len(current_store.texts)): 298 | new_store.add_item( 299 | text=current_store.texts[i], # 原始文本 300 | embedding=current_store.vectors[i], # 对应的嵌入向量 301 | metadata=current_store.metadata[i].copy() # 使用副本防止引用问题 302 | ) 303 | 304 | # 根据高质量反馈创建并添加增强内容 305 | for feedback in good_feedback: 306 | # 将问题和高质量答案组合成新的文档格式 307 | # 这样可以创建直接解决用户查询的可检索内容 308 | enhanced_text = f"Question: {feedback['query']}\nAnswer: {feedback['response']}" 309 | 310 | # 为这个新的合成文档生成嵌入向量 311 | embedding = create_embeddings(enhanced_text) 312 | 313 | # 将其添加到向量存储中,并附带特殊元数据以标识其来源和重要性 314 | new_store.add_item( 315 | text=enhanced_text, 316 | embedding=embedding, 317 | metadata={ 318 | "type": "feedback_enhanced", # 标记为来自反馈生成 319 | "query": feedback["query"], # 保存原始查询以供参考 320 | "relevance_score": 1.2, # 提高初始相关性以优先处理这些项 321 | "feedback_count": 1, # 跟踪反馈整合情况 322 | "original_feedback": feedback # 保存完整的反馈记录 323 | } 324 | ) 325 | 326 | print(f"已添加来自反馈的增强内容: {feedback['query'][:50]}...") 327 | 328 | # 记录关于增强的汇总统计信息 329 | print(f"微调后的索引现在有 {len(new_store.texts)} 个项目 (原始: {len(chunks)})") 330 | return new_store 331 | 332 | 333 | #################################### 334 | # 反馈检索组件函数 335 | #################################### 336 | def get_user_feedback(query, response, relevance, quality, comments=""): 337 | """ 338 | 将用户反馈格式化为字典。 339 | 340 | Args: 341 | query (str): 用户的查询 342 | response (str): 系统的回答 343 | relevance (int): 相关性评分 (1-5) 344 | quality (int): 质量评分 (1-5) 345 | comments (str): 可选的反馈评论 346 | 347 | Returns: 348 | Dict: 格式化的反馈 349 | """ 350 | return { 351 | "query": query, 352 | "response": response, 353 | "relevance": int(relevance), 354 | "quality": int(quality), 355 | "comments": comments, 356 | "timestamp": datetime.now().isoformat() # 当前时间戳 357 | } 358 | 359 | 360 | def store_feedback(feedback, feedback_file="feedback_data.json"): 361 | """ 362 | 将反馈存储在JSON文件中。 363 | 364 | Args: 365 | feedback (Dict): 反馈数据 366 | feedback_file (str): 反馈文件的路径 367 | """ 368 | with open(feedback_file, "a", encoding="utf-8") as f: 369 | json.dump(feedback, f, ensure_ascii=False, indent=4) 370 | f.write("\n") 371 | 372 | 373 | def load_feedback_data(feedback_file="feedback_data.json"): 374 | """ 375 | 从文件中加载反馈数据。 376 | 377 | Args: 378 | feedback_file (str): 反馈文件的路径 379 | 380 | Returns: 381 | List[Dict]: 反馈条目的列表 382 | """ 383 | feedback_data = [] 384 | try: 385 | with open(feedback_file, "r", encoding="utf-8") as f: 386 | for line in f: 387 | if line.strip(): 388 | feedback_data.append(json.loads(line.strip())) 389 | except FileNotFoundError: 390 | print("未找到反馈数据文件。将以空反馈开始。") 391 | # print("No feedback data file found. Starting with empty feedback.") 392 | 393 | return feedback_data 394 | 395 | 396 | ##### 397 | def process_document(pdf_path, chunk_size=1000, chunk_overlap=200): 398 | """ 399 | 为带有反馈循环的RAG(检索增强生成)处理文档。 400 | 该函数处理完整的文档处理管道: 401 | 1. 从PDF中提取文本 402 | 2. 带有重叠的文本分块 403 | 3. 为每个文本块创建向量嵌入 404 | 4. 在向量数据库中存储带有元数据的块 405 | 406 | Args: 407 | pdf_path (str): 要处理的PDF文件路径。 408 | chunk_size (int): 每个文本块的字符数。 409 | chunk_overlap (int): 相邻块之间的重叠字符数。 410 | 411 | Returns: 412 | Tuple[List[str], SimpleVectorStore]: 包含以下内容的元组: 413 | - 文档块列表 414 | - 填充了嵌入和元数据的向量存储 415 | """ 416 | # 第一步:从PDF文档中提取原始文本内容 417 | print("从PDF中提取文本...") 418 | extracted_text = extract_text_from_pdf(pdf_path) 419 | 420 | # 第二步:将文本分成可管理的、带有重叠的块,以便更好地保存上下文 421 | print("对文本进行分块...") 422 | chunks = chunk_text(extracted_text, chunk_size, chunk_overlap) 423 | print(f"创建了 {len(chunks)} 个文本块") 424 | 425 | # 第三步:为每个文本块生成向量嵌入 426 | print("为文本块创建嵌入...") 427 | chunk_embeddings = create_embeddings(chunks) 428 | 429 | # 第四步:初始化向量数据库以存储块及其嵌入 430 | store = SimpleVectorStore() 431 | 432 | # 第五步:将每个块及其嵌入添加到向量存储中 433 | # 包含用于基于反馈改进的元数据 434 | for i, (chunk, embedding) in enumerate(zip(chunks, chunk_embeddings)): 435 | store.add_item( 436 | text=chunk, 437 | embedding=embedding, 438 | metadata={ 439 | "index": i, # 在原始文档中的位置 440 | "source": pdf_path, # 源文档路径 441 | "relevance_score": 1.0, # 初始相关性分数(将通过反馈更新) 442 | "feedback_count": 0 # 接收到此块反馈的计数器 443 | } 444 | ) 445 | 446 | print(f"已将 {len(chunks)} 个块添加到向量存储中") 447 | return chunks, store 448 | 449 | 450 | def generate_response(query, context): 451 | """ 452 | 根据查询和上下文生成响应。 453 | 454 | Args: 455 | query (str): 用户查询 456 | context (str): 从检索文档中提取的上下文文本 457 | 458 | Returns: 459 | str: 生成的响应 460 | """ 461 | # 定义系统提示以指导AI的行为 462 | system_prompt = "您是一个乐于助人的AI助手。请仅根据提供的上下文来回答用户的问题。如果在上下文中找不到答案,请直接说'没有足够的信息'。" 463 | 464 | # 通过结合上下文和查询创建用户提示 465 | user_prompt = f""" 466 | 上下文: 467 | {context} 468 | 469 | 问题: {query} 470 | 471 | 请基于上述上下文内容提供一个全面详尽的答案。 472 | """ 473 | 474 | # 调用OpenAI API,根据系统提示和用户提示生成响应 475 | response = client.chat.completions.create( 476 | model=llm_model, 477 | messages=[ 478 | {"role": "system", "content": system_prompt}, 479 | {"role": "user", "content": user_prompt} 480 | ], 481 | temperature=0 # 使用temperature=0以获得一致且确定性的响应 482 | ) 483 | 484 | # 返回生成的响应内容 485 | return response.choices[0].message.content 486 | 487 | 488 | def extract_text_from_pdf(pdf_path): 489 | """ 490 | 从 PDF 文件中提取文本,并打印前 `num_chars` 个字符。 491 | 492 | Args: 493 | pdf_path (str): Path to the PDF file. 494 | 495 | Returns: 496 | str: Extracted text from the PDF. 497 | """ 498 | # 打开 PDF 文件 499 | mypdf = fitz.open(pdf_path) 500 | all_text = "" # 初始化一个空字符串以存储提取的文本 501 | 502 | # Iterate through each page in the PDF 503 | for page_num in range(mypdf.page_count): 504 | page = mypdf[page_num] 505 | text = page.get_text("text") # 从页面中提取文本 506 | all_text += text # 将提取的文本追加到 all_text 字符串中 507 | 508 | return all_text # 返回提取的文本 509 | 510 | 511 | def chunk_text(text, n, overlap): 512 | """ 513 | 将文本分割为重叠的块 514 | 515 | Args: 516 | text (str): 要分割的文本 517 | n (int): 每个块的字符数 518 | overlap (int): 块之间的重叠字符数 519 | 520 | Returns: 521 | List[str]: 文本块列表 522 | """ 523 | chunks = [] # 524 | for i in range(0, len(text), n - overlap): 525 | # 添加从当前索引到索引 + 块大小的文本块 526 | chunk = text[i:i + n] 527 | if chunk: 528 | chunks.append(chunk) 529 | 530 | return chunks # Return the list of text chunks 531 | 532 | 533 | class SimpleVectorStore: 534 | """ 535 | 使用NumPy实现的简单向量存储。 536 | """ 537 | 538 | def __init__(self): 539 | """ 540 | 初始化向量存储。 541 | """ 542 | self.vectors = [] # 用于存储嵌入向量的列表 543 | self.texts = [] # 用于存储原始文本的列表 544 | self.metadata = [] # 用于存储每个文本元数据的列表 545 | 546 | def add_item(self, text, embedding, metadata=None): 547 | """ 548 | 向向量存储中添加一个项目。 549 | 550 | Args: 551 | text (str): 原始文本。 552 | embedding (List[float]): 嵌入向量。 553 | metadata (dict, 可选): 额外的元数据。 554 | """ 555 | self.vectors.append(np.array(embedding)) # 将嵌入转换为numpy数组并添加到向量列表中 556 | self.texts.append(text) # 将原始文本添加到文本列表中 557 | self.metadata.append(metadata or {}) # 添加元数据到元数据列表中,如果没有提供则使用空字典 558 | 559 | def similarity_search(self, query_embedding, k=5): 560 | """ 561 | 查找与查询嵌入最相似的项目。 562 | 563 | Args: 564 | query_embedding (List[float]): 查询嵌入向量。 565 | k (int): 返回的结果数量。 566 | 567 | Returns: 568 | List[Dict]: 包含文本和元数据的前k个最相似项。 569 | """ 570 | if not self.vectors: 571 | return [] # 如果没有存储向量,则返回空列表 572 | 573 | # 将查询嵌入转换为numpy数组 574 | query_vector = np.array(query_embedding) 575 | 576 | # 使用余弦相似度计算相似度 577 | similarities = [] 578 | for i, vector in enumerate(self.vectors): 579 | # 计算查询向量与存储向量之间的余弦相似度 580 | similarity = np.dot(query_vector, vector) / (np.linalg.norm(query_vector) * np.linalg.norm(vector)) 581 | similarities.append((i, similarity)) # 添加索引和相似度分数 582 | 583 | # 按相似度排序(降序) 584 | similarities.sort(key=lambda x: x[1], reverse=True) 585 | 586 | # 返回前k个结果 587 | results = [] 588 | for i in range(min(k, len(similarities))): 589 | idx, score = similarities[i] 590 | results.append({ 591 | "text": self.texts[idx], # 添加对应的文本 592 | "metadata": self.metadata[idx], # 添加对应的元数据 593 | "similarity": score # 添加相似度分数 594 | }) 595 | 596 | return results # 返回前k个最相似项的列表 597 | 598 | 599 | def create_embeddings(text): 600 | """ 601 | 使用Embedding模型为给定文本创建嵌入向量。 602 | 603 | Args: 604 | text (str): 要创建嵌入向量的输入文本。 605 | 606 | Returns: 607 | List[float]: 嵌入向量。 608 | """ 609 | # 通过将字符串输入转换为列表来处理字符串和列表输入 610 | input_text = text if isinstance(text, list) else [text] 611 | 612 | # 使用指定的模型为输入文本创建嵌入向量 613 | response = client.embeddings.create( 614 | model=embedding_model, 615 | input=input_text 616 | ) 617 | 618 | # 如果输入是字符串,仅返回第一个嵌入向量 619 | if isinstance(text, str): 620 | return response.data[0].embedding 621 | 622 | # 否则,将所有嵌入向量作为向量列表返回 623 | return [item.embedding for item in response.data] 624 | -------------------------------------------------------------------------------- /src/full_en/1_simple_rag_en.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "vscode": { 7 | "languageId": "markdown" 8 | } 9 | }, 10 | "source": [ 11 | "# Introduction to Simple RAG\n", 12 | "\n", 13 | "Retrieval-Augmented Generation (RAG) is a hybrid approach that combines information retrieval with generative models. It enhances the performance of language models by incorporating external knowledge, which improves accuracy and factual correctness.\n", 14 | "\n", 15 | "In a Simple RAG setup, we follow these steps:\n", 16 | "\n", 17 | "1. **Data Ingestion**: Load and preprocess the text data.\n", 18 | "2. **Chunking**: Break the data into smaller chunks to improve retrieval performance.\n", 19 | "3. **Embedding Creation**: Convert the text chunks into numerical representations using an embedding model.\n", 20 | "4. **Semantic Search**: Retrieve relevant chunks based on a user query.\n", 21 | "5. **Response Generation**: Use a language model to generate a response based on retrieved text.\n", 22 | "\n", 23 | "This notebook implements a Simple RAG approach, evaluates the model’s response, and explores various improvements." 24 | ] 25 | }, 26 | { 27 | "cell_type": "markdown", 28 | "metadata": {}, 29 | "source": [ 30 | "## Setting Up the Environment\n", 31 | "We begin by importing necessary libraries." 32 | ] 33 | }, 34 | { 35 | "cell_type": "code", 36 | "execution_count": 1, 37 | "metadata": {}, 38 | "outputs": [], 39 | "source": [ 40 | "import fitz\n", 41 | "import os\n", 42 | "import numpy as np\n", 43 | "import json\n", 44 | "from openai import OpenAI" 45 | ] 46 | }, 47 | { 48 | "cell_type": "markdown", 49 | "metadata": {}, 50 | "source": [ 51 | "## Extracting Text from a PDF File\n", 52 | "To implement RAG, we first need a source of textual data. In this case, we extract text from a PDF file using the PyMuPDF library." 53 | ] 54 | }, 55 | { 56 | "cell_type": "code", 57 | "execution_count": 2, 58 | "metadata": {}, 59 | "outputs": [], 60 | "source": [ 61 | "def extract_text_from_pdf(pdf_path):\n", 62 | " \"\"\"\n", 63 | " Extracts text from a PDF file and prints the first `num_chars` characters.\n", 64 | "\n", 65 | " Args:\n", 66 | " pdf_path (str): Path to the PDF file.\n", 67 | "\n", 68 | " Returns:\n", 69 | " str: Extracted text from the PDF.\n", 70 | " \"\"\"\n", 71 | " # Open the PDF file\n", 72 | " mypdf = fitz.open(pdf_path)\n", 73 | " all_text = \"\" # Initialize an empty string to store the extracted text\n", 74 | "\n", 75 | " # Iterate through each page in the PDF\n", 76 | " for page_num in range(mypdf.page_count):\n", 77 | " page = mypdf[page_num] # Get the page\n", 78 | " text = page.get_text(\"text\") # Extract text from the page\n", 79 | " all_text += text # Append the extracted text to the all_text string\n", 80 | "\n", 81 | " return all_text # Return the extracted text" 82 | ] 83 | }, 84 | { 85 | "cell_type": "markdown", 86 | "metadata": {}, 87 | "source": [ 88 | "## Chunking the Extracted Text\n", 89 | "Once we have the extracted text, we divide it into smaller, overlapping chunks to improve retrieval accuracy." 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 3, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "def chunk_text(text, n, overlap):\n", 99 | " \"\"\"\n", 100 | " Chunks the given text into segments of n characters with overlap.\n", 101 | "\n", 102 | " Args:\n", 103 | " text (str): The text to be chunked.\n", 104 | " n (int): The number of characters in each chunk.\n", 105 | " overlap (int): The number of overlapping characters between chunks.\n", 106 | "\n", 107 | " Returns:\n", 108 | " List[str]: A list of text chunks.\n", 109 | " \"\"\"\n", 110 | " chunks = [] # Initialize an empty list to store the chunks\n", 111 | " \n", 112 | " # Loop through the text with a step size of (n - overlap)\n", 113 | " for i in range(0, len(text), n - overlap):\n", 114 | " # Append a chunk of text from index i to i + n to the chunks list\n", 115 | " chunks.append(text[i:i + n])\n", 116 | "\n", 117 | " return chunks # Return the list of text chunks" 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "metadata": {}, 123 | "source": [ 124 | "## Setting Up the OpenAI API Client\n", 125 | "We initialize the OpenAI client to generate embeddings and responses." 126 | ] 127 | }, 128 | { 129 | "cell_type": "code", 130 | "execution_count": null, 131 | "metadata": {}, 132 | "outputs": [], 133 | "source": [ 134 | "# Initialize the OpenAI client with the base URL and API key\n", 135 | "client = OpenAI(\n", 136 | " base_url=\"https://api.studio.nebius.com/v1/\",\n", 137 | " api_key=os.getenv(\"OPENAI_API_KEY\") # Retrieve the API key from environment variables\n", 138 | ")" 139 | ] 140 | }, 141 | { 142 | "cell_type": "markdown", 143 | "metadata": {}, 144 | "source": [ 145 | "## Extracting and Chunking Text from a PDF File\n", 146 | "Now, we load the PDF, extract text, and split it into chunks." 147 | ] 148 | }, 149 | { 150 | "cell_type": "code", 151 | "execution_count": 5, 152 | "metadata": {}, 153 | "outputs": [ 154 | { 155 | "name": "stdout", 156 | "output_type": "stream", 157 | "text": [ 158 | "Number of text chunks: 42\n", 159 | "\n", 160 | "First text chunk:\n", 161 | "Understanding Artificial Intelligence \n", 162 | "Chapter 1: Introduction to Artificial Intelligence \n", 163 | "Artificial intelligence (AI) refers to the ability of a digital computer or computer-controlled robot \n", 164 | "to perform tasks commonly associated with intelligent beings. The term is frequently applied to \n", 165 | "the project of developing systems endowed with the intellectual processes characteristic of \n", 166 | "humans, such as the ability to reason, discover meaning, generalize, or learn from past \n", 167 | "experience. Over the past few decades, advancements in computing power and data availability \n", 168 | "have significantly accelerated the development and deployment of AI. \n", 169 | "Historical Context \n", 170 | "The idea of artificial intelligence has existed for centuries, often depicted in myths and fiction. \n", 171 | "However, the formal field of AI research began in the mid-20th century. The Dartmouth Workshop \n", 172 | "in 1956 is widely considered the birthplace of AI. Early AI research focused on problem-solving \n", 173 | "and symbolic methods. The 1980s saw a rise in exp\n" 174 | ] 175 | } 176 | ], 177 | "source": [ 178 | "# Define the path to the PDF file\n", 179 | "pdf_path = \"data/AI_Information.pdf\"\n", 180 | "\n", 181 | "# Extract text from the PDF file\n", 182 | "extracted_text = extract_text_from_pdf(pdf_path)\n", 183 | "\n", 184 | "# Chunk the extracted text into segments of 1000 characters with an overlap of 200 characters\n", 185 | "text_chunks = chunk_text(extracted_text, 1000, 200)\n", 186 | "\n", 187 | "# Print the number of text chunks created\n", 188 | "print(\"Number of text chunks:\", len(text_chunks))\n", 189 | "\n", 190 | "# Print the first text chunk\n", 191 | "print(\"\\nFirst text chunk:\")\n", 192 | "print(text_chunks[0])" 193 | ] 194 | }, 195 | { 196 | "cell_type": "markdown", 197 | "metadata": {}, 198 | "source": [ 199 | "## Creating Embeddings for Text Chunks\n", 200 | "Embeddings transform text into numerical vectors, which allow for efficient similarity search." 201 | ] 202 | }, 203 | { 204 | "cell_type": "code", 205 | "execution_count": 6, 206 | "metadata": {}, 207 | "outputs": [], 208 | "source": [ 209 | "def create_embeddings(text, model=\"BAAI/bge-en-icl\"):\n", 210 | " \"\"\"\n", 211 | " Creates embeddings for the given text using the specified OpenAI model.\n", 212 | "\n", 213 | " Args:\n", 214 | " text (str): The input text for which embeddings are to be created.\n", 215 | " model (str): The model to be used for creating embeddings. Default is \"BAAI/bge-en-icl\".\n", 216 | "\n", 217 | " Returns:\n", 218 | " dict: The response from the OpenAI API containing the embeddings.\n", 219 | " \"\"\"\n", 220 | " # Create embeddings for the input text using the specified model\n", 221 | " response = client.embeddings.create(\n", 222 | " model=model,\n", 223 | " input=text\n", 224 | " )\n", 225 | "\n", 226 | " return response # Return the response containing the embeddings\n", 227 | "\n", 228 | "# Create embeddings for the text chunks\n", 229 | "response = create_embeddings(text_chunks)" 230 | ] 231 | }, 232 | { 233 | "cell_type": "markdown", 234 | "metadata": {}, 235 | "source": [ 236 | "## Performing Semantic Search\n", 237 | "We implement cosine similarity to find the most relevant text chunks for a user query." 238 | ] 239 | }, 240 | { 241 | "cell_type": "code", 242 | "execution_count": 7, 243 | "metadata": {}, 244 | "outputs": [], 245 | "source": [ 246 | "def cosine_similarity(vec1, vec2):\n", 247 | " \"\"\"\n", 248 | " Calculates the cosine similarity between two vectors.\n", 249 | "\n", 250 | " Args:\n", 251 | " vec1 (np.ndarray): The first vector.\n", 252 | " vec2 (np.ndarray): The second vector.\n", 253 | "\n", 254 | " Returns:\n", 255 | " float: The cosine similarity between the two vectors.\n", 256 | " \"\"\"\n", 257 | " # Compute the dot product of the two vectors and divide by the product of their norms\n", 258 | " return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))" 259 | ] 260 | }, 261 | { 262 | "cell_type": "code", 263 | "execution_count": 8, 264 | "metadata": {}, 265 | "outputs": [], 266 | "source": [ 267 | "def semantic_search(query, text_chunks, embeddings, k=5):\n", 268 | " \"\"\"\n", 269 | " Performs semantic search on the text chunks using the given query and embeddings.\n", 270 | "\n", 271 | " Args:\n", 272 | " query (str): The query for the semantic search.\n", 273 | " text_chunks (List[str]): A list of text chunks to search through.\n", 274 | " embeddings (List[dict]): A list of embeddings for the text chunks.\n", 275 | " k (int): The number of top relevant text chunks to return. Default is 5.\n", 276 | "\n", 277 | " Returns:\n", 278 | " List[str]: A list of the top k most relevant text chunks based on the query.\n", 279 | " \"\"\"\n", 280 | " # Create an embedding for the query\n", 281 | " query_embedding = create_embeddings(query).data[0].embedding\n", 282 | " similarity_scores = [] # Initialize a list to store similarity scores\n", 283 | "\n", 284 | " # Calculate similarity scores between the query embedding and each text chunk embedding\n", 285 | " for i, chunk_embedding in enumerate(embeddings):\n", 286 | " similarity_score = cosine_similarity(np.array(query_embedding), np.array(chunk_embedding.embedding))\n", 287 | " similarity_scores.append((i, similarity_score)) # Append the index and similarity score\n", 288 | "\n", 289 | " # Sort the similarity scores in descending order\n", 290 | " similarity_scores.sort(key=lambda x: x[1], reverse=True)\n", 291 | " # Get the indices of the top k most similar text chunks\n", 292 | " top_indices = [index for index, _ in similarity_scores[:k]]\n", 293 | " # Return the top k most relevant text chunks\n", 294 | " return [text_chunks[index] for index in top_indices]\n" 295 | ] 296 | }, 297 | { 298 | "cell_type": "markdown", 299 | "metadata": {}, 300 | "source": [ 301 | "## Running a Query on Extracted Chunks" 302 | ] 303 | }, 304 | { 305 | "cell_type": "code", 306 | "execution_count": 9, 307 | "metadata": {}, 308 | "outputs": [ 309 | { 310 | "name": "stdout", 311 | "output_type": "stream", 312 | "text": [ 313 | "Query: What is 'Explainable AI' and why is it considered important?\n", 314 | "Context 1:\n", 315 | "systems. Explainable AI (XAI) \n", 316 | "techniques aim to make AI decisions more understandable, enabling users to assess their \n", 317 | "fairness and accuracy. \n", 318 | "Privacy and Data Protection \n", 319 | "AI systems often rely on large amounts of data, raising concerns about privacy and data \n", 320 | "protection. Ensuring responsible data handling, implementing privacy-preserving techniques, \n", 321 | "and complying with data protection regulations are crucial. \n", 322 | "Accountability and Responsibility \n", 323 | "Establishing accountability and responsibility for AI systems is essential for addressing potential \n", 324 | "harms and ensuring ethical behavior. This includes defining roles and responsibilities for \n", 325 | "developers, deployers, and users of AI systems. \n", 326 | "Chapter 20: Building Trust in AI \n", 327 | "Transparency and Explainability \n", 328 | "Transparency and explainability are key to building trust in AI. Making AI systems understandable \n", 329 | "and providing insights into their decision-making processes helps users assess their reliability \n", 330 | "and fairness. \n", 331 | "Robustness and Reliability \n", 332 | "\n", 333 | "=====================================\n", 334 | "Context 2:\n", 335 | " incidents. \n", 336 | "Environmental Monitoring \n", 337 | "AI-powered environmental monitoring systems track air and water quality, detect pollution, and \n", 338 | "support environmental protection efforts. These systems provide real-time data, identify \n", 339 | "pollution sources, and inform environmental policies. \n", 340 | "Chapter 15: The Future of AI Research \n", 341 | "Advancements in Deep Learning \n", 342 | "Continued advancements in deep learning are expected to drive further breakthroughs in AI. \n", 343 | "Research is focused on developing more efficient and interpretable deep learning models, as well \n", 344 | "as exploring new architectures and training techniques. \n", 345 | "Explainable AI (XAI) \n", 346 | "Explainable AI (XAI) aims to make AI systems more transparent and understandable. Research in \n", 347 | "XAI focuses on developing methods for explaining AI decisions, enhancing trust, and improving \n", 348 | "accountability. \n", 349 | "AI and Neuroscience \n", 350 | "The intersection of AI and neuroscience is a promising area of research. Understanding the \n", 351 | "human brain can inspire new AI algorithms and architectures, \n", 352 | "=====================================\n" 353 | ] 354 | } 355 | ], 356 | "source": [ 357 | "# Load the validation data from a JSON file\n", 358 | "with open('data/val.json') as f:\n", 359 | " data = json.load(f)\n", 360 | "\n", 361 | "# Extract the first query from the validation data\n", 362 | "query = data[0]['question']\n", 363 | "\n", 364 | "# Perform semantic search to find the top 2 most relevant text chunks for the query\n", 365 | "top_chunks = semantic_search(query, text_chunks, response.data, k=2)\n", 366 | "\n", 367 | "# Print the query\n", 368 | "print(\"Query:\", query)\n", 369 | "\n", 370 | "# Print the top 2 most relevant text chunks\n", 371 | "for i, chunk in enumerate(top_chunks):\n", 372 | " print(f\"Context {i + 1}:\\n{chunk}\\n=====================================\")" 373 | ] 374 | }, 375 | { 376 | "cell_type": "markdown", 377 | "metadata": {}, 378 | "source": [ 379 | "## Generating a Response Based on Retrieved Chunks" 380 | ] 381 | }, 382 | { 383 | "cell_type": "code", 384 | "execution_count": 10, 385 | "metadata": {}, 386 | "outputs": [], 387 | "source": [ 388 | "# Define the system prompt for the AI assistant\n", 389 | "system_prompt = \"You are an AI assistant that strictly answers based on the given context. If the answer cannot be derived directly from the provided context, respond with: 'I do not have enough information to answer that.'\"\n", 390 | "\n", 391 | "def generate_response(system_prompt, user_message, model=\"meta-llama/Llama-3.2-3B-Instruct\"):\n", 392 | " \"\"\"\n", 393 | " Generates a response from the AI model based on the system prompt and user message.\n", 394 | "\n", 395 | " Args:\n", 396 | " system_prompt (str): The system prompt to guide the AI's behavior.\n", 397 | " user_message (str): The user's message or query.\n", 398 | " model (str): The model to be used for generating the response. Default is \"meta-llama/Llama-2-7B-chat-hf\".\n", 399 | "\n", 400 | " Returns:\n", 401 | " dict: The response from the AI model.\n", 402 | " \"\"\"\n", 403 | " response = client.chat.completions.create(\n", 404 | " model=model,\n", 405 | " temperature=0,\n", 406 | " messages=[\n", 407 | " {\"role\": \"system\", \"content\": system_prompt},\n", 408 | " {\"role\": \"user\", \"content\": user_message}\n", 409 | " ]\n", 410 | " )\n", 411 | " return response\n", 412 | "\n", 413 | "# Create the user prompt based on the top chunks\n", 414 | "user_prompt = \"\\n\".join([f\"Context {i + 1}:\\n{chunk}\\n=====================================\\n\" for i, chunk in enumerate(top_chunks)])\n", 415 | "user_prompt = f\"{user_prompt}\\nQuestion: {query}\"\n", 416 | "\n", 417 | "# Generate AI response\n", 418 | "ai_response = generate_response(system_prompt, user_prompt)" 419 | ] 420 | }, 421 | { 422 | "cell_type": "markdown", 423 | "metadata": {}, 424 | "source": [ 425 | "## Evaluating the AI Response\n", 426 | "We compare the AI response with the expected answer and assign a score." 427 | ] 428 | }, 429 | { 430 | "cell_type": "code", 431 | "execution_count": 11, 432 | "metadata": {}, 433 | "outputs": [ 434 | { 435 | "name": "stdout", 436 | "output_type": "stream", 437 | "text": [ 438 | "Based on the evaluation criteria, I would assign a score of 0.8 to the AI assistant's response.\n", 439 | "\n", 440 | "The AI assistant's response is very close to the true response, but there are some minor differences. The true response mentions \"transparency\" and \"accountability\" explicitly, which are not mentioned in the AI assistant's response. However, the overall meaning and content of the response are identical, and the AI assistant's response effectively conveys the importance of Explainable AI in building trust and ensuring fairness in AI systems.\n", 441 | "\n", 442 | "Therefore, the score of 0.8 reflects the AI assistant's response being very close to the true response, but not perfectly aligned.\n" 443 | ] 444 | } 445 | ], 446 | "source": [ 447 | "# Define the system prompt for the evaluation system\n", 448 | "evaluate_system_prompt = \"You are an intelligent evaluation system tasked with assessing the AI assistant's responses. If the AI assistant's response is very close to the true response, assign a score of 1. If the response is incorrect or unsatisfactory in relation to the true response, assign a score of 0. If the response is partially aligned with the true response, assign a score of 0.5.\"\n", 449 | "\n", 450 | "# Create the evaluation prompt by combining the user query, AI response, true response, and evaluation system prompt\n", 451 | "evaluation_prompt = f\"User Query: {query}\\nAI Response:\\n{ai_response.choices[0].message.content}\\nTrue Response: {data[0]['ideal_answer']}\\n{evaluate_system_prompt}\"\n", 452 | "\n", 453 | "# Generate the evaluation response using the evaluation system prompt and evaluation prompt\n", 454 | "evaluation_response = generate_response(evaluate_system_prompt, evaluation_prompt)\n", 455 | "\n", 456 | "# Print the evaluation response\n", 457 | "print(evaluation_response.choices[0].message.content)" 458 | ] 459 | } 460 | ], 461 | "metadata": { 462 | "kernelspec": { 463 | "display_name": ".venv-new-specific-rag", 464 | "language": "python", 465 | "name": "python3" 466 | }, 467 | "language_info": { 468 | "codemirror_mode": { 469 | "name": "ipython", 470 | "version": 3 471 | }, 472 | "file_extension": ".py", 473 | "mimetype": "text/x-python", 474 | "name": "python", 475 | "nbconvert_exporter": "python", 476 | "pygments_lexer": "ipython3", 477 | "version": "3.9.0" 478 | } 479 | }, 480 | "nbformat": 4, 481 | "nbformat_minor": 2 482 | } 483 | -------------------------------------------------------------------------------- /src/full/01_simple_rag.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "d72026c2ead5ee86", 6 | "metadata": {}, 7 | "source": [ 8 | "# RAG 入门" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "d905010bf6edd686", 14 | "metadata": {}, 15 | "source": [ 16 | "检索增强生成(RAG)是一种混合方法,它结合了信息检索与生成模型。通过结合外部知识,它增强了语言模型的表现,提高了准确性和事实的正确性。\n", 17 | "\n", 18 | "-----\n", 19 | "实现步骤:\n", 20 | "- **Data Ingestion(数据采集)**: 加载和预处理文本数据。\n", 21 | "- **Chunking(分块处理)**: 将数据分割成更小的块以提高检索性能。\n", 22 | "- **Embedding Creation(嵌入创建)**: 使用嵌入模型将文本块转换为数值表示。\n", 23 | "- **Semantic Search(语义搜索)**: 根据用户查询检索相关块。\n", 24 | "- **Response Generation(响应生成)**:使用语言模型根据检索到的文本生成响应。\n" 25 | ] 26 | }, 27 | { 28 | "metadata": {}, 29 | "cell_type": "markdown", 30 | "source": "# 设置环境", 31 | "id": "b8522c2eb03cc571" 32 | }, 33 | { 34 | "metadata": { 35 | "ExecuteTime": { 36 | "end_time": "2025-04-23T05:30:05.153135Z", 37 | "start_time": "2025-04-23T05:30:03.495677Z" 38 | } 39 | }, 40 | "cell_type": "code", 41 | "source": [ 42 | "import fitz\n", 43 | "import os\n", 44 | "import numpy as np\n", 45 | "import json\n", 46 | "from openai import OpenAI\n", 47 | "from dotenv import load_dotenv\n", 48 | "\n", 49 | "load_dotenv()" 50 | ], 51 | "id": "fda658ebdedf1ad8", 52 | "outputs": [ 53 | { 54 | "data": { 55 | "text/plain": [ 56 | "True" 57 | ] 58 | }, 59 | "execution_count": 1, 60 | "metadata": {}, 61 | "output_type": "execute_result" 62 | } 63 | ], 64 | "execution_count": 1 65 | }, 66 | { 67 | "metadata": {}, 68 | "cell_type": "markdown", 69 | "source": [ 70 | "# 从 PDF 文件中提取文本\n", 71 | "\n", 72 | "使用 PyMuPDF 库从 PDF 文件中提取文本" 73 | ], 74 | "id": "5b57bf95ccf33077" 75 | }, 76 | { 77 | "metadata": { 78 | "ExecuteTime": { 79 | "end_time": "2025-04-23T05:30:06.652632Z", 80 | "start_time": "2025-04-23T05:30:06.647167Z" 81 | } 82 | }, 83 | "cell_type": "code", 84 | "source": [ 85 | "def extract_text_from_pdf(pdf_path):\n", 86 | " \"\"\"\n", 87 | " Extracts text from a PDF file and prints the first `num_chars` characters.\n", 88 | "\n", 89 | " Args:\n", 90 | " pdf_path (str): Path to the PDF file.\n", 91 | "\n", 92 | " Returns:\n", 93 | " str: Extracted text from the PDF.\n", 94 | " \"\"\"\n", 95 | " # Open the PDF file\n", 96 | " mypdf = fitz.open(pdf_path)\n", 97 | " all_text = \"\" # Initialize an empty string to store the extracted text\n", 98 | "\n", 99 | " # Iterate through each page in the PDF\n", 100 | " for page_num in range(mypdf.page_count):\n", 101 | " page = mypdf[page_num] # Get the page\n", 102 | " text = page.get_text(\"text\") # Extract text from the page\n", 103 | " all_text += text # Append the extracted text to the all_text string\n", 104 | "\n", 105 | " return all_text # Return the extracted text" 106 | ], 107 | "id": "f7ef2488923ee4da", 108 | "outputs": [], 109 | "execution_count": 2 110 | }, 111 | { 112 | "metadata": {}, 113 | "cell_type": "markdown", 114 | "source": [ 115 | "# 对提取的文本进行分块\n", 116 | "\n", 117 | "将文本切分成更小的、重叠的块以提高检索准确性" 118 | ], 119 | "id": "ea76730a9166ee1a" 120 | }, 121 | { 122 | "metadata": { 123 | "ExecuteTime": { 124 | "end_time": "2025-04-23T05:30:11.594138Z", 125 | "start_time": "2025-04-23T05:30:11.589760Z" 126 | } 127 | }, 128 | "cell_type": "code", 129 | "source": [ 130 | "def chunk_text(text, n, overlap):\n", 131 | " \"\"\"\n", 132 | " Chunks the given text into segments of n characters with overlap.\n", 133 | "\n", 134 | " Args:\n", 135 | " text (str): 文本\n", 136 | " n (int): 块长度\n", 137 | " overlap (int): 重叠度\n", 138 | "\n", 139 | " Returns:\n", 140 | " List[str]: A list of text chunks.\n", 141 | " \"\"\"\n", 142 | " chunks = [] # Initialize an empty list to store the chunks\n", 143 | "\n", 144 | " # Loop through the text with a step size of (n - overlap)\n", 145 | " for i in range(0, len(text), n - overlap):\n", 146 | " # Append a chunk of text from index i to i + n to the chunks list\n", 147 | " chunks.append(text[i:i + n])\n", 148 | "\n", 149 | " return chunks" 150 | ], 151 | "id": "662cf7929fb76a21", 152 | "outputs": [], 153 | "execution_count": 3 154 | }, 155 | { 156 | "metadata": {}, 157 | "cell_type": "markdown", 158 | "source": [ 159 | "# 设置 OpenAI API 客户端\n", 160 | "\n", 161 | "初始化 OpenAI 客户端以生成嵌入和响应" 162 | ], 163 | "id": "17657c31e434c1fc" 164 | }, 165 | { 166 | "metadata": { 167 | "ExecuteTime": { 168 | "end_time": "2025-04-23T05:30:14.531508Z", 169 | "start_time": "2025-04-23T05:30:14.194070Z" 170 | } 171 | }, 172 | "cell_type": "code", 173 | "source": [ 174 | "# 国内支持类OpenAI的API都可,我用的是火山引擎的,需要配置对应的base_url和api_key\n", 175 | "\n", 176 | "client = OpenAI(\n", 177 | " base_url=os.getenv(\"LLM_BASE_URL\"),\n", 178 | " api_key=os.getenv(\"LLM_API_KEY\")\n", 179 | ")" 180 | ], 181 | "id": "44e6e847004b7a79", 182 | "outputs": [], 183 | "execution_count": 4 184 | }, 185 | { 186 | "metadata": {}, 187 | "cell_type": "markdown", 188 | "source": [ 189 | "# 从 PDF 文件中提取和分块文本\n", 190 | "\n", 191 | "加载 PDF,提取文本并将其分割成块" 192 | ], 193 | "id": "68836556534acb04" 194 | }, 195 | { 196 | "metadata": { 197 | "ExecuteTime": { 198 | "end_time": "2025-04-23T05:30:16.195953Z", 199 | "start_time": "2025-04-23T05:30:16.152111Z" 200 | } 201 | }, 202 | "cell_type": "code", 203 | "source": [ 204 | "# PDF file\n", 205 | "pdf_path = \"../../data/AI_Information.en.zh-CN.pdf\"\n", 206 | "\n", 207 | "# 提取文本\n", 208 | "extracted_text = extract_text_from_pdf(pdf_path)\n", 209 | "\n", 210 | "# 切分文本块,块长度为300,重叠度为50\n", 211 | "text_chunks = chunk_text(extracted_text, 500, 100)\n", 212 | "\n", 213 | "# 文本块的数量\n", 214 | "print(\"Number of text chunks:\", len(text_chunks))\n", 215 | "\n", 216 | "# 第一个文本块\n", 217 | "print(\"\\nFirst text chunk:\")\n", 218 | "print(text_chunks[0])" 219 | ], 220 | "id": "8771e8dc3dd3daef", 221 | "outputs": [ 222 | { 223 | "name": "stdout", 224 | "output_type": "stream", 225 | "text": [ 226 | "Number of text chunks: 26\n", 227 | "\n", 228 | "First text chunk:\n", 229 | "理解⼈⼯智能\n", 230 | "第⼀章:⼈⼯智能简介\n", 231 | "⼈⼯智能 (AI) 是指数字计算机或计算机控制的机器⼈执⾏通常与智能⽣物相关的任务的能⼒。该术\n", 232 | "语通常⽤于开发具有⼈类特有的智⼒过程的系统,例如推理、发现意义、概括或从过往经验中学习\n", 233 | "的能⼒。在过去的⼏⼗年中,计算能⼒和数据可⽤性的进步显著加速了⼈⼯智能的开发和部署。\n", 234 | "历史背景\n", 235 | "⼈⼯智能的概念已存在数个世纪,经常出现在神话和⼩说中。然⽽,⼈⼯智能研究的正式领域始于\n", 236 | "20世纪中叶。1956年的达特茅斯研讨会被⼴泛认为是⼈⼯智能的发源地。早期的⼈⼯智能研究侧\n", 237 | "重于问题解决和符号⽅法。20世纪80年代专家系统兴起,⽽20世纪90年代和21世纪初,机器学习\n", 238 | "和神经⽹络取得了进步。深度学习的最新突破彻底改变了这⼀领域。\n", 239 | "现代观察\n", 240 | "现代⼈⼯智能系统在⽇常⽣活中⽇益普及。从 Siri 和 Alexa 等虚拟助⼿,到流媒体服务和社交媒体\n", 241 | "上的推荐算法,⼈⼯智能正在影响我们的⽣活、⼯作和互动⽅式。⾃动驾驶汽⻋、先进的医疗诊断\n", 242 | "技术以及复杂的⾦融建模⼯具的发展,彰显了⼈⼯智能应⽤的⼴泛性和持续增⻓。此外,⼈们对其\n", 243 | "伦理影响、偏⻅和失业的担忧也⽇益凸显。\n", 244 | "第⼆章:⼈⼯智能\n" 245 | ] 246 | } 247 | ], 248 | "execution_count": 5 249 | }, 250 | { 251 | "metadata": {}, 252 | "cell_type": "markdown", 253 | "source": [ 254 | "# 文本块创建嵌入\n", 255 | "\n", 256 | "嵌入将文本转换为数值向量,这允许进行高效的相似性搜索" 257 | ], 258 | "id": "1c53d7c8ce9ed98e" 259 | }, 260 | { 261 | "metadata": { 262 | "ExecuteTime": { 263 | "end_time": "2025-04-23T05:30:39.169313Z", 264 | "start_time": "2025-04-23T05:30:37.914231Z" 265 | } 266 | }, 267 | "cell_type": "code", 268 | "source": [ 269 | "# from sentence_transformers import SentenceTransformer, util\n", 270 | "# from typing import List\n", 271 | "# from pathlib import Path\n", 272 | "#\n", 273 | "#\n", 274 | "# def create_embeddings(text: List[str], model_path: str = \"../rag_naive/model/gte-base-zh\") -> List[List[float]]:\n", 275 | "# \"\"\"\n", 276 | "# Creates embeddings for the given text using the local-embedding model.\n", 277 | "# eg: modelscope gte-base-zh\n", 278 | "# \"\"\"\n", 279 | "# # Create embeddings for the input text using the specified model\n", 280 | "#\n", 281 | "# st_model = SentenceTransformer(model_name_or_path=model_path)\n", 282 | "# st_embeddings = st_model.encode(text, normalize_embeddings=True)\n", 283 | "# response = [embedding.tolist() for embedding in st_embeddings]\n", 284 | "#\n", 285 | "# return response\n", 286 | "\n", 287 | "def create_embeddings(text):\n", 288 | " # Create embeddings for the input text using the specified model\n", 289 | " response = client.embeddings.create(\n", 290 | " model=os.getenv(\"EMBEDDING_MODEL_ID\"),\n", 291 | " input=text\n", 292 | " )\n", 293 | "\n", 294 | " return response # Return the response containing the embeddings\n", 295 | "\n", 296 | "\n", 297 | "# 文本块的嵌入向量\n", 298 | "response = create_embeddings(text_chunks)\n" 299 | ], 300 | "id": "7415587211f6d5c6", 301 | "outputs": [], 302 | "execution_count": 6 303 | }, 304 | { 305 | "metadata": {}, 306 | "cell_type": "markdown", 307 | "source": [ 308 | "# 语义搜索\n", 309 | "\n", 310 | "实现余弦相似度来找到与用户查询最相关的文本片段" 311 | ], 312 | "id": "fa28f1e9569c420a" 313 | }, 314 | { 315 | "metadata": { 316 | "ExecuteTime": { 317 | "end_time": "2025-04-23T05:30:41.587816Z", 318 | "start_time": "2025-04-23T05:30:41.583283Z" 319 | } 320 | }, 321 | "cell_type": "code", 322 | "source": [ 323 | "def cosine_similarity(vec1, vec2):\n", 324 | " \"\"\"\n", 325 | " Calculates the cosine similarity between two vectors.\n", 326 | "\n", 327 | " Args:\n", 328 | " vec1 (np.ndarray): The first vector.\n", 329 | " vec2 (np.ndarray): The second vector.\n", 330 | "\n", 331 | " Returns:\n", 332 | " float: The cosine similarity between the two vectors.\n", 333 | " \"\"\"\n", 334 | " # Compute the dot product of the two vectors and divide by the product of their norms\n", 335 | " return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))" 336 | ], 337 | "id": "564e872b70c6e0af", 338 | "outputs": [], 339 | "execution_count": 7 340 | }, 341 | { 342 | "metadata": { 343 | "ExecuteTime": { 344 | "end_time": "2025-04-23T05:30:46.798170Z", 345 | "start_time": "2025-04-23T05:30:46.789497Z" 346 | } 347 | }, 348 | "cell_type": "code", 349 | "source": [ 350 | "def semantic_search(query, text_chunks, embeddings, k=5):\n", 351 | " \"\"\"\n", 352 | " Performs semantic search on the text chunks using the given query and embeddings.\n", 353 | "\n", 354 | " Args:\n", 355 | " query (str): The query for the semantic search.\n", 356 | " text_chunks (List[str]): A list of text chunks to search through.\n", 357 | " embeddings (List[dict]): A list of embeddings for the text chunks.\n", 358 | " k (int): The number of top relevant text chunks to return. Default is 5.\n", 359 | "\n", 360 | " Returns:\n", 361 | " List[str]: A list of the top k most relevant text chunks based on the query.\n", 362 | " \"\"\"\n", 363 | " # Create an embedding for the query\n", 364 | " query_embedding = create_embeddings(query).data[0].embedding\n", 365 | " similarity_scores = [] # Initialize a list to store similarity scores\n", 366 | "\n", 367 | " # Calculate similarity scores between the query embedding and each text chunk embedding\n", 368 | " for i, chunk_embedding in enumerate(embeddings):\n", 369 | " similarity_score = cosine_similarity(np.array(query_embedding), np.array(chunk_embedding.embedding))\n", 370 | " similarity_scores.append((i, similarity_score)) # Append the index and similarity score\n", 371 | "\n", 372 | " # Sort the similarity scores in descending order\n", 373 | " similarity_scores.sort(key=lambda x: x[1], reverse=True)\n", 374 | " # Get the indices of the top k most similar text chunks\n", 375 | " top_indices = [index for index, _ in similarity_scores[:k]]\n", 376 | " # Return the top k most relevant text chunks\n", 377 | " return [text_chunks[index] for index in top_indices]" 378 | ], 379 | "id": "8aa7d8a5425a5e63", 380 | "outputs": [], 381 | "execution_count": 8 382 | }, 383 | { 384 | "metadata": {}, 385 | "cell_type": "markdown", 386 | "source": [ 387 | "# 在提取的文本块上进行语义搜索\n", 388 | "\n" 389 | ], 390 | "id": "721439c3c3474cb8" 391 | }, 392 | { 393 | "metadata": { 394 | "ExecuteTime": { 395 | "end_time": "2025-04-23T05:30:50.936598Z", 396 | "start_time": "2025-04-23T05:30:50.364722Z" 397 | } 398 | }, 399 | "cell_type": "code", 400 | "source": [ 401 | "# Load the validation data from a JSON file\n", 402 | "with open('../../data/val.json', encoding=\"utf-8\") as f:\n", 403 | " data = json.load(f)\n", 404 | "\n", 405 | "# Extract the first query from the validation data\n", 406 | "query = data[0]['question']\n", 407 | "\n", 408 | "# Perform semantic search to find the top 2 most relevant text chunks for the query\n", 409 | "top_chunks = semantic_search(query, text_chunks, response.data, k=2)\n", 410 | "\n", 411 | "# Print the query\n", 412 | "print(\"Query:\", query)\n", 413 | "\n", 414 | "# Print the top 2 most relevant text chunks\n", 415 | "for i, chunk in enumerate(top_chunks):\n", 416 | " print(f\"Context {i + 1}:\\n{chunk}\\n=====================================\")" 417 | ], 418 | "id": "2fe4a5f35c53eb5a", 419 | "outputs": [ 420 | { 421 | "name": "stdout", 422 | "output_type": "stream", 423 | "text": [ 424 | "Query: 什么是‘可解释人工智能’,为什么它被认为很重要?\n", 425 | "Context 1:\n", 426 | "透明、负责且有\n", 427 | "益于社会。关键原则包括尊重⼈权、隐私、不歧视和仁慈。\n", 428 | "解决⼈⼯智能中的偏⻅\n", 429 | "⼈⼯智能系统可能会继承并放⼤其训练数据中存在的偏⻅,从⽽导致不公平或歧视性的结果。解决\n", 430 | "偏⻅需要谨慎的数据收集、算法设计以及持续的监测和评估。\n", 431 | "透明度和可解释性\n", 432 | "透明度和可解释性对于建⽴对⼈⼯智能系统的信任⾄关重要。可解释⼈⼯智能 (XAI) 技术旨在使⼈\n", 433 | "⼯智能决策更易于理解,使⽤⼾能够评估其公平性和准确性。\n", 434 | "隐私和数据保护\n", 435 | "⼈⼯智能系统通常依赖⼤量数据,这引发了⼈们对隐私和数据保护的担忧。确保负责任的数据处\n", 436 | "理、实施隐私保护技术以及遵守数据保护法规⾄关重要。\n", 437 | "问责与责任\n", 438 | "建⽴⼈⼯智能系统的问责制和责任制,对于应对潜在危害和确保道德⾏为⾄关重要。这包括明确⼈\n", 439 | "⼯智能系统开发者、部署者和⽤⼾的⻆⾊和职责。\n", 440 | "第 20 章:建⽴对⼈⼯智能的信任\n", 441 | "透明度和可解释性\n", 442 | "透明度和可解释性是建⽴⼈⼯智能信任的关键。让⼈⼯智能系统易于理解,并深⼊了解其决策过\n", 443 | "程,有助于⽤⼾评估其可靠性和公平性。\n", 444 | "稳健性和可靠性\n", 445 | "确保⼈⼯智能系统的稳健可靠对于建⽴信任⾄关重要。这包括测试和验证⼈⼯智能模型、监控其性\n", 446 | "能以及解决潜\n", 447 | "=====================================\n", 448 | "Context 2:\n", 449 | "。让⼈⼯智能系统易于理解,并深⼊了解其决策过\n", 450 | "程,有助于⽤⼾评估其可靠性和公平性。\n", 451 | "稳健性和可靠性\n", 452 | "确保⼈⼯智能系统的稳健可靠对于建⽴信任⾄关重要。这包括测试和验证⼈⼯智能模型、监控其性\n", 453 | "能以及解决潜在的漏洞。\n", 454 | "⽤⼾控制和代理\n", 455 | "赋予⽤⼾对AI系统的控制权,并赋予他们与AI交互的⾃主权,可以增强信任。这包括允许⽤⼾⾃定\n", 456 | "义AI设置、了解其数据的使⽤⽅式,以及选择退出AI驱动的功能。\n", 457 | "道德设计与发展\n", 458 | "将伦理考量纳⼊⼈⼯智能系统的设计和开发对于建⽴信任⾄关重要。这包括进⾏伦理影响评估、与\n", 459 | "利益相关者沟通,以及遵守伦理准则和标准。\n", 460 | "公众参与和教育\n", 461 | "让公众参与⼈⼯智能的讨论,并教育他们了解其能⼒、局限性和伦理影响,有助于建⽴信任。公众\n", 462 | "意识宣传活动、教育计划和开放式对话有助于促进公众对⼈⼯智能的理解和接受。\n", 463 | "第 21 章:⼈⼯智能的前进之路\n", 464 | "持续研究与创新\n", 465 | "持续的研究和创新对于提升⼈⼯智能能⼒、应对挑战并充分发挥其潜⼒⾄关重要。这包括投资基础\n", 466 | "研究、应⽤研究以及新型⼈⼯智能技术和应⽤的开发。\n", 467 | "负责任的开发和部署\n", 468 | "负责任地开发和部署⼈⼯智能对于确保其效益得到⼴泛共享并降低其⻛险⾄关重要。这涉及遵守\n", 469 | "=====================================\n" 470 | ] 471 | } 472 | ], 473 | "execution_count": 9 474 | }, 475 | { 476 | "metadata": {}, 477 | "cell_type": "markdown", 478 | "source": "# 基于检索到的片段生成响应", 479 | "id": "737c1edcc6274c5e" 480 | }, 481 | { 482 | "metadata": { 483 | "ExecuteTime": { 484 | "end_time": "2025-04-23T05:31:03.789208Z", 485 | "start_time": "2025-04-23T05:30:56.429071Z" 486 | } 487 | }, 488 | "cell_type": "code", 489 | "source": [ 490 | "# Define the system prompt for the AI assistant\n", 491 | "system_prompt = \"你是一个AI助手,严格根据给定的上下文进行回答。如果无法直接从提供的上下文中得出答案,请回复:'我没有足够的信息来回答这个问题。'\"\n", 492 | "\n", 493 | "def generate_response(system_prompt, user_message):\n", 494 | " \"\"\"\n", 495 | " Generates a response from the AI model based on the system prompt and user message.\n", 496 | "\n", 497 | " Args:\n", 498 | " system_prompt (str): The system prompt to guide the AI's behavior.\n", 499 | " user_message (str): The user's message or query.\n", 500 | "\n", 501 | " Returns:\n", 502 | " dict: The response from the AI model.\n", 503 | " \"\"\"\n", 504 | " response = client.chat.completions.create(\n", 505 | " model=os.getenv(\"LLM_MODEL_ID\"),\n", 506 | " messages=[\n", 507 | " {\"role\": \"system\", \"content\": system_prompt},\n", 508 | " {\"role\": \"user\", \"content\": user_message}\n", 509 | " ],\n", 510 | " temperature=0.1,\n", 511 | " top_p=0.8,\n", 512 | " presence_penalty=1.05,\n", 513 | " max_tokens=4096,\n", 514 | " )\n", 515 | " return response.choices[0].message.content\n", 516 | "\n", 517 | "# Create the user prompt based on the top chunks\n", 518 | "user_prompt = \"\\n\".join([f\"上下文内容 {i + 1}:\\n{chunk}\\n=====================================\\n\" for i, chunk in enumerate(top_chunks)])\n", 519 | "user_prompt = f\"{user_prompt}\\n问题: {query}\"\n", 520 | "\n", 521 | "# Generate AI response\n", 522 | "ai_response = generate_response(system_prompt, user_prompt)\n", 523 | "print(ai_response)" 524 | ], 525 | "id": "99320ba034a35632", 526 | "outputs": [ 527 | { 528 | "name": "stdout", 529 | "output_type": "stream", 530 | "text": [ 531 | "**可解释人工智能(XAI)**是指那些能够使其决策过程和结果易于理解和解释的人工智能系统。它旨在让用户、开发者和其他利益相关者能够清晰地了解AI系统是如何做出特定决策的。\n", 532 | "\n", 533 | "**为什么可解释人工智能很重要?**\n", 534 | "\n", 535 | "1. **建立信任**:透明度和可解释性是建立对人工智能系统信任的关键。当用户能够理解AI的决策过程时,他们更有可能信任其可靠性和公平性。\n", 536 | "\n", 537 | "2. **评估公平性和准确性**:可解释人工智能使用户能够评估系统的决策是否公平、准确,从而避免潜在的偏见和歧视。\n", 538 | "\n", 539 | "3. **解决偏见**:AI系统可能会继承并放大其训练数据中存在的偏见。可解释性有助于识别和纠正这些偏见,确保结果更加公正。\n", 540 | "\n", 541 | "4. **问责与责任**:明确AI系统的决策过程有助于建立问责制和责任制,使开发者、部署者和用户都能明确各自的角色和职责。\n", 542 | "\n", 543 | "5. **遵守法规**:在某些领域,法律和法规要求决策过程必须是透明的。可解释人工智能有助于满足这些要求。\n", 544 | "\n", 545 | "综上所述,可解释人工智能对于确保AI系统的透明度、公平性、可靠性和合规性至关重要,从而促进其广泛接受和信任。\n" 546 | ] 547 | } 548 | ], 549 | "execution_count": 10 550 | }, 551 | { 552 | "metadata": {}, 553 | "cell_type": "markdown", 554 | "source": [ 555 | "# 评估响应质量\n", 556 | "\n" 557 | ], 558 | "id": "c1bb700b447e0bf1" 559 | }, 560 | { 561 | "metadata": { 562 | "ExecuteTime": { 563 | "end_time": "2025-04-23T05:31:12.803299Z", 564 | "start_time": "2025-04-23T05:31:08.996179Z" 565 | } 566 | }, 567 | "cell_type": "code", 568 | "source": [ 569 | "# Define the system prompt for the evaluation system\n", 570 | "evaluate_system_prompt = \"你是一个智能评估系统,负责评估AI助手的回答。如果AI助手的回答与真实答案非常接近,则评分为1。如果回答错误或与真实答案不符,则评分为0。如果回答部分符合真实答案,则评分为0.5。\"\n", 571 | "\n", 572 | "# Create the evaluation prompt by combining the user query, AI response, true response, and evaluation system prompt\n", 573 | "evaluation_prompt = f\"用户问题: {query}\\nAI回答:\\n{ai_response}\\nTrue Response: {data[0]['ideal_answer']}\\n{evaluate_system_prompt}\"\n", 574 | "\n", 575 | "# Generate the evaluation response using the evaluation system prompt and evaluation prompt\n", 576 | "evaluation_response = generate_response(evaluate_system_prompt, evaluation_prompt)\n", 577 | "print(evaluation_response)" 578 | ], 579 | "id": "c874f4d9c04dbcec", 580 | "outputs": [ 581 | { 582 | "name": "stdout", 583 | "output_type": "stream", 584 | "text": [ 585 | "根据提供的用户问题、AI回答和真实答案,AI助手的回答与真实答案非常接近,涵盖了可解释人工智能(XAI)的定义及其重要性的多个方面,包括建立信任、评估公平性和准确性、解决偏见、问责与责任以及遵守法规。这些内容与真实答案中提到的建立信任、问责制和确保公平性高度一致。\n", 586 | "\n", 587 | "因此,AI助手的回答可以被认为是全面且准确的,与真实答案非常接近。\n", 588 | "\n", 589 | "**评分:1**\n" 590 | ] 591 | } 592 | ], 593 | "execution_count": 11 594 | } 595 | ], 596 | "metadata": { 597 | "kernelspec": { 598 | "display_name": "Python 3 (ipykernel)", 599 | "language": "python", 600 | "name": "python3" 601 | }, 602 | "language_info": { 603 | "codemirror_mode": { 604 | "name": "ipython", 605 | "version": 3 606 | }, 607 | "file_extension": ".py", 608 | "mimetype": "text/x-python", 609 | "name": "python", 610 | "nbconvert_exporter": "python", 611 | "pygments_lexer": "ipython3", 612 | "version": "3.12.9" 613 | } 614 | }, 615 | "nbformat": 4, 616 | "nbformat_minor": 5 617 | } 618 | --------------------------------------------------------------------------------