基础RAG实现,最佳入门选择(四)

RAG中的上下文丰富检索,检索增强生成(RAG)通过从外部来源检索相关知识来增强AI响应。传统的检索方法返回孤立的文本块,这可能导致答案不完整。为了解决这个问题,引入了上下文丰富检索,它确保检索到的信息包括相邻的块以获得更好的一致性。 -数据摄取:从PDF中提取文本。 -带有重叠上下文的分块:将文本拆分为重叠的块以保留上下文。 -嵌入创建:将文本块转换为数字表示。 -上下文感知检索:检索相关块及其邻居以获得更好的完整性。 -响应生成:使用语言模型根据检索到的上下文生成响应。 -评估:评估模型的响应准确性。

下述方法从 PDF 提取文本、生成带重叠内容的文本块、创建文本嵌入向量、结合邻居块的上下文感知检索、基于检索内容生成响应,以及评估答案准确性。

设置环境

python 复制代码
import fitz  # PyMuPDF用于PDF处理
import os
import numpy as np
import json
from dashscope import TextEmbedding, Generation
from typing import List, Dict, Tuple, Optional
import logging
from config import Config

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

初始化模型和Key信息

python 复制代码
    def __init__(self, api_key: str = None):
        """
        初始化RAG系统

        Args:
            api_key (str): 阿里云API密钥,如果为None则使用配置文件中的设置
        """
        # 使用配置文件中的设置或传入的参数
        self.api_key = api_key or Config.API_KEY

        # 存储文档块和嵌入
        self.text_chunks = []
        self.embeddings = []

        # 从配置文件加载参数
        self.chunk_size = Config.CHUNK_SIZE
        self.chunk_overlap = Config.CHUNK_OVERLAP
        self.context_size = Config.CONTEXT_SIZE
        self.top_k = Config.TOP_K

        logger.info("上下文增强RAG系统初始化完成")
        logger.info(f"使用嵌入模型: {Config.EMBEDDING_MODEL}")
        logger.info(f"使用语言模型: {Config.LLM_MODEL}")

从PDF文件中提取文本内容

python 复制代码
    def extract_text_from_pdf(self, pdf_path: str) -> str:
        """
        从PDF文件中提取文本内容

        Args:
            pdf_path (str): PDF文件路径

        Returns:
            str: 提取的文本内容

        Raises:
            FileNotFoundError: 当PDF文件不存在时
            Exception: 当PDF处理出现错误时
        """
        try:
            logger.info(f"开始从PDF文件提取文本: {pdf_path}")

            # 检查文件是否存在
            if not os.path.exists(pdf_path):
                raise FileNotFoundError(f"PDF文件不存在: {pdf_path}")

            # 打开PDF文件
            pdf_document = fitz.open(pdf_path)
            all_text = ""

            # 遍历每一页并提取文本
            for page_num in range(pdf_document.page_count):
                page = pdf_document[page_num]
                text = page.get_text("text")
                all_text += text + "\n"  # 添加换行符分隔页面

            pdf_document.close()

            logger.info(f"成功提取文本,总长度: {len(all_text)} 字符")
            return all_text

        except Exception as e:
            logger.error(f"PDF文本提取失败: {str(e)}")
            raise

文本分割

python 复制代码
    def chunk_text(self, text: str, chunk_size: int = None, overlap: int = None) -> List[str]:
        """
        将文本分割成重叠的块

        Args:
            text (str): 要分割的文本
            chunk_size (int): 每个块的大小,默认使用实例变量
            overlap (int): 块间重叠大小,默认使用实例变量

        Returns:
            List[str]: 文本块列表
        """
        if chunk_size is None:
            chunk_size = self.chunk_size
        if overlap is None:
            overlap = self.chunk_overlap

        logger.info(f"开始文本分块,块大小: {chunk_size}, 重叠: {overlap}")

        chunks = []
        step_size = chunk_size - overlap

        # 使用滑动窗口进行分块
        for i in range(0, len(text), step_size):
            chunk = text[i:i + chunk_size]
            if chunk.strip():  # 只添加非空块
                chunks.append(chunk)

        logger.info(f"文本分块完成,共生成 {len(chunks)} 个块")
        return chunks

文本列表创建嵌入向量

python 复制代码
    def create_embeddings(self, texts: List[str], model: str = None, batch_size: int = 20) -> List[Dict]:
        """
        为文本列表创建嵌入向量

        Args:
            texts (List[str]): 要嵌入的文本列表
            model (str): 使用的嵌入模型名称,默认使用配置文件中的设置
            batch_size (int): 批处理大小,默认20(阿里云限制最多25个)

        Returns:
            List[Dict]: 包含嵌入向量的响应列表
        """
        if model is None:
            model = Config.EMBEDDING_MODEL

        try:
            logger.info(f"开始创建嵌入向量,使用模型: {model}")
            logger.info(f"总文本数量: {len(texts)}, 批处理大小: {batch_size}")

            all_embeddings = []
            total_batches = (len(texts) + batch_size - 1) // batch_size

            for i in range(0, len(texts), batch_size):
                batch_texts = texts[i:i + batch_size]
                batch_num = i // batch_size + 1
                
                logger.info(f"处理批次 {batch_num}/{total_batches},包含 {len(batch_texts)} 个文本")

                # 使用阿里云dashscope创建嵌入
                response = TextEmbedding.call(
                    model=model,
                    input=batch_texts,
                    api_key=self.api_key
                )

                if response.status_code == 200:
                    batch_embeddings = response.output.get('embeddings', [])
                    all_embeddings.extend(batch_embeddings)
                    logger.info(f"批次 {batch_num} 完成,获取到 {len(batch_embeddings)} 个嵌入向量")
                else:
                    raise Exception(f"批次 {batch_num} 嵌入API调用失败: {response.message}")

            logger.info(f"嵌入向量创建完成,共 {len(all_embeddings)} 个向量")
            return all_embeddings

        except Exception as e:
            logger.error(f"嵌入向量创建失败: {str(e)}")
            raise

计算两个向量之间的余弦相似度

python 复制代码
    def cosine_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:
        """
        计算两个向量之间的余弦相似度

        Args:
            vec1 (np.ndarray): 第一个向量
            vec2 (np.ndarray): 第二个向量

        Returns:
            float: 余弦相似度值 (0-1之间)
        """
        # 计算向量的点积除以向量范数的乘积
        dot_product = np.dot(vec1, vec2)
        norm_product = np.linalg.norm(vec1) * np.linalg.norm(vec2)

        # 避免除零错误
        if norm_product == 0:
            return 0.0

        return dot_product / norm_product

执行上下文增强的语义搜索

python 复制代码
    def context_enriched_search(self, query: str, k: int = None, context_size: int = None) -> List[str]:
        """
        执行上下文增强的语义搜索

        Args:
            query (str): 查询文本
            k (int): 返回的相关块数量,默认使用实例变量
            context_size (int): 包含的上下文块数量,默认使用实例变量

        Returns:
            List[str]: 相关文本块及其上下文
        """
        if k is None:
            k = self.top_k
        if context_size is None:
            context_size = self.context_size

        logger.info(f"开始上下文增强搜索,查询: {query[:50]}...")

        try:
            # 为查询创建嵌入向量
            query_embedding_response = self.create_embeddings([query])
            query_embedding = np.array(query_embedding_response[0].get('embedding', []))

            similarity_scores = []

            # 计算查询与每个文档块的相似度
            for i, chunk_embedding in enumerate(self.embeddings):
                chunk_vector = np.array(chunk_embedding.get('embedding', []))
                similarity = self.cosine_similarity(query_embedding, chunk_vector)
                similarity_scores.append((i, similarity))

            # 按相似度降序排序
            similarity_scores.sort(key=lambda x: x[1], reverse=True)

            # 获取最相关的块索引
            top_index = similarity_scores[0][0]

            # 计算上下文范围
            start_idx = max(0, top_index - context_size)
            end_idx = min(len(self.text_chunks), top_index + context_size + 1)

            # 返回相关块及其上下文
            result_chunks = [self.text_chunks[i] for i in range(start_idx, end_idx)]

            logger.info(f"搜索完成,返回 {len(result_chunks)} 个上下文块")
            return result_chunks

        except Exception as e:
            logger.error(f"上下文增强搜索失败: {str(e)}")
            raise

基于检索的上下文生成回答

python 复制代码
def generate_response(self, query: str, context_chunks: List[str],
                          model: str = None) -> str:
        """
        基于检索的上下文生成回答

        Args:
            query (str): 用户查询
            context_chunks (List[str]): 检索到的上下文块
            model (str): 使用的语言模型,默认使用配置文件中的设置

        Returns:
            str: 生成的回答
        """
        if model is None:
            model = Config.LLM_MODEL

        try:
            logger.info("开始生成回答")

            # 构建系统提示
            system_prompt = """你是一个专业的AI助手,请严格基于提供的上下文信息来回答问题。
如果无法从给定上下文中得出答案,请回复:"我无法从提供的信息中找到答案。" """

            # 构建用户消息
            context_text = "\n\n".join([f"上下文 {i + 1}:\n{chunk}" for i, chunk in enumerate(context_chunks)])
            user_message = f"{context_text}\n\n问题: {query}"

            logger.info(f"使用模型: {model}")
            logger.info(f"用户消息长度: {len(user_message)} 字符")

            # 调用语言模型生成回答
            response = Generation.call(
                model=model,
                messages=[
                    {'role': 'system', 'content': system_prompt},
                    {'role': 'user', 'content': user_message}
                ],
                api_key=self.api_key
            )

            logger.info(f"API响应状态码: {response.status_code}")
            logger.info(f"API响应类型: {type(response)}")

            if response is None:
                raise Exception("API响应为空")

            if response.status_code == 200:
                if hasattr(response, 'output') and response.output is not None:
                    # 阿里云GenerationResponse的格式
                    if hasattr(response.output, 'text'):
                        answer = response.output.text
                        logger.info("回答生成完成")
                        return answer
                    elif hasattr(response.output, 'choices') and response.output.choices:
                        # 兼容OpenAI格式
                        answer = response.output.choices[0].message.content
                        logger.info("回答生成完成")
                        return answer
                    else:
                        logger.error(f"响应格式不支持: {response.output}")
                        raise Exception("API响应格式不支持")
                else:
                    logger.error(f"响应中没有output: {response}")
                    raise Exception("API响应格式错误:没有output")
            else:
                error_msg = response.message if hasattr(response, 'message') else '未知错误'
                raise Exception(f"生成API调用失败: {error_msg}")

        except Exception as e:
            logger.error(f"回答生成失败: {str(e)}")
            raise

评估AI回答的质量

python 复制代码
    def evaluate_response(self, query: str, ai_response: str, true_answer: str) -> Dict:
        """
        评估AI回答的质量

        Args:
            query (str): 原始查询
            ai_response (str): AI生成的回答
            true_answer (str): 标准答案

        Returns:
            Dict: 包含评分和评估结果的字典
        """
        try:
            logger.info("开始评估回答质量")

            # 构建评估提示
            evaluation_prompt = f"""你是一个智能评估系统,请评估AI助手的回答质量。

评估标准:
- 1.0分:回答完全正确且详细
- 0.8分:回答基本正确,略有不足
- 0.5分:回答部分正确
- 0.0分:回答错误或无关

用户问题: {query}
AI回答: {ai_response}
标准答案: {true_answer}

请给出评分(0.0-1.0)和简要理由:"""

            logger.info(f"评估提示长度: {len(evaluation_prompt)} 字符")

            # 调用模型进行评估
            response = Generation.call(
                model=Config.LLM_MODEL,
                messages=[
                    {'role': 'system', 'content': '你是一个专业的评估专家。'},
                    {'role': 'user', 'content': evaluation_prompt}
                ],
                api_key=self.api_key
            )

            logger.info(f"评估API响应状态码: {response.status_code}")

            if response is None:
                raise Exception("评估API响应为空")

            if response.status_code == 200:
                if hasattr(response, 'output') and response.output is not None:
                    # 阿里云GenerationResponse的格式
                    if hasattr(response.output, 'text'):
                        evaluation_result = response.output.text
                        logger.info("评估完成")
                        return {
                            "evaluation": evaluation_result,
                            "query": query,
                            "ai_response": ai_response,
                            "true_answer": true_answer
                        }
                    elif hasattr(response.output, 'choices') and response.output.choices:
                        # 兼容OpenAI格式
                        evaluation_result = response.output.choices[0].message.content
                        logger.info("评估完成")
                        return {
                            "evaluation": evaluation_result,
                            "query": query,
                            "ai_response": ai_response,
                            "true_answer": true_answer
                        }
                    else:
                        logger.error(f"评估响应格式不支持: {response.output}")
                        raise Exception("评估API响应格式不支持")
                else:
                    logger.error(f"评估响应中没有output: {response}")
                    raise Exception("评估API响应格式错误:没有output")
            else:
                error_msg = response.message if hasattr(response, 'message') else '未知错误'
                raise Exception(f"评估API调用失败: {error_msg}")

        except Exception as e:
            logger.error(f"回答评估失败: {str(e)}")
            raise

执行效果展示

python 复制代码
============================================================
上下文增强RAG系统演示
============================================================

1. 初始化RAG系统...

2. 加载PDF文档...
2025-06-18 17:14:08,837 - INFO - 上下文增强RAG系统初始化完成
2025-06-18 17:14:08,837 - INFO - 使用嵌入模型: text-embedding-v2
2025-06-18 17:14:08,837 - INFO - 使用语言模型: qwen-max
2025-06-18 17:14:08,837 - INFO - 开始加载文档: 2888年Java程序员找工作最新场景题.pdf
2025-06-18 17:14:08,837 - INFO - 开始从PDF文件提取文本: 2888年Java程序员找工作最新场景题.pdf
2025-06-18 17:14:09,089 - INFO - 成功提取文本,总长度: 271055 字符
2025-06-18 17:14:09,089 - INFO - 开始文本分块,块大小: 1000, 重叠: 200
2025-06-18 17:14:09,089 - INFO - 文本分块完成,共生成 339 个块
2025-06-18 17:14:09,089 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:14:09,089 - INFO - 总文本数量: 339, 批处理大小: 20
2025-06-18 17:14:09,089 - INFO - 处理批次 1/17,包含 20 个文本
2025-06-18 17:14:10,574 - INFO - 批次 1 完成,获取到 20 个嵌入向量
2025-06-18 17:14:10,574 - INFO - 处理批次 2/17,包含 20 个文本
2025-06-18 17:14:11,931 - INFO - 批次 2 完成,获取到 20 个嵌入向量
2025-06-18 17:14:11,931 - INFO - 处理批次 3/17,包含 20 个文本
2025-06-18 17:14:13,121 - INFO - 批次 3 完成,获取到 20 个嵌入向量
2025-06-18 17:14:13,121 - INFO - 处理批次 4/17,包含 20 个文本
2025-06-18 17:14:14,568 - INFO - 批次 4 完成,获取到 20 个嵌入向量
2025-06-18 17:14:14,568 - INFO - 处理批次 5/17,包含 20 个文本
2025-06-18 17:14:15,768 - INFO - 批次 5 完成,获取到 20 个嵌入向量
2025-06-18 17:14:15,768 - INFO - 处理批次 6/17,包含 20 个文本
2025-06-18 17:14:16,912 - INFO - 批次 6 完成,获取到 20 个嵌入向量
2025-06-18 17:14:16,912 - INFO - 处理批次 7/17,包含 20 个文本
2025-06-18 17:14:18,274 - INFO - 批次 7 完成,获取到 20 个嵌入向量
2025-06-18 17:14:18,274 - INFO - 处理批次 8/17,包含 20 个文本
2025-06-18 17:14:19,394 - INFO - 批次 8 完成,获取到 20 个嵌入向量
2025-06-18 17:14:19,394 - INFO - 处理批次 9/17,包含 20 个文本
2025-06-18 17:14:20,483 - INFO - 批次 9 完成,获取到 20 个嵌入向量
2025-06-18 17:14:20,483 - INFO - 处理批次 10/17,包含 20 个文本
2025-06-18 17:14:21,613 - INFO - 批次 10 完成,获取到 20 个嵌入向量
2025-06-18 17:14:21,613 - INFO - 处理批次 11/17,包含 20 个文本
2025-06-18 17:14:22,799 - INFO - 批次 11 完成,获取到 20 个嵌入向量
2025-06-18 17:14:22,799 - INFO - 处理批次 12/17,包含 20 个文本
2025-06-18 17:14:24,005 - INFO - 批次 12 完成,获取到 20 个嵌入向量
2025-06-18 17:14:24,005 - INFO - 处理批次 13/17,包含 20 个文本
2025-06-18 17:14:25,368 - INFO - 批次 13 完成,获取到 20 个嵌入向量
2025-06-18 17:14:25,368 - INFO - 处理批次 14/17,包含 20 个文本
2025-06-18 17:14:26,475 - INFO - 批次 14 完成,获取到 20 个嵌入向量
2025-06-18 17:14:26,475 - INFO - 处理批次 15/17,包含 20 个文本
2025-06-18 17:14:27,596 - INFO - 批次 15 完成,获取到 20 个嵌入向量
2025-06-18 17:14:27,596 - INFO - 处理批次 16/17,包含 20 个文本
2025-06-18 17:14:28,746 - INFO - 批次 16 完成,获取到 20 个嵌入向量
2025-06-18 17:14:28,746 - INFO - 处理批次 17/17,包含 19 个文本
2025-06-18 17:14:29,803 - INFO - 批次 17 完成,获取到 19 个嵌入向量
2025-06-18 17:14:29,803 - INFO - 嵌入向量创建完成,共 339 个向量
2025-06-18 17:14:29,803 - INFO - 文档加载和处理完成
2025-06-18 17:14:29,804 - INFO - 加载验证数据: ../data/java_val.json
2025-06-18 17:14:29,804 - INFO - 验证数据加载完成,共 20 条记录
2025-06-18 17:14:29,804 - INFO - 开始处理查询: Java程序员面试中常见的技术问题有哪些?
2025-06-18 17:14:29,804 - INFO - 开始上下文增强搜索,查询: Java程序员面试中常见的技术问题有哪些?...
2025-06-18 17:14:29,804 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:14:29,804 - INFO - 总文本数量: 1, 批处理大小: 20
2025-06-18 17:14:29,804 - INFO - 处理批次 1/1,包含 1 个文本

3. 加载验证数据...

4. 运行查询示例...
查询: Java程序员面试中常见的技术问题有哪些?
2025-06-18 17:14:30,093 - INFO - 批次 1 完成,获取到 1 个嵌入向量
2025-06-18 17:14:30,093 - INFO - 嵌入向量创建完成,共 1 个向量
2025-06-18 17:14:30,113 - INFO - 搜索完成,返回 2 个上下文块
2025-06-18 17:14:30,113 - INFO - 开始生成回答
2025-06-18 17:14:30,113 - INFO - 使用模型: qwen-max
2025-06-18 17:14:30,113 - INFO - 用户消息长度: 2043 字符
回答: 根据提供的上下文信息,没有具体列出2024年Java程序员面试中常见的技术问题。不过,文中提到现在的面试趋势是从"常规八股文"转向了以"项目、场景问题、技术深度思考为主",这意味着面试官更倾向于提出需要候选人基于自身理解和实践经验来解答的问题。这类问题可能包括但不限于:

- 与候选人过往参与过的项目相关的问题。
- 在特定情境下如何应用Java及其生态系统中的工具和技术(如微服务架构、云原生技术)。
- 对某些Java特性的深入理解,比如并发编程、垃圾回收机制等。
- 如何解决实际开发过程中遇到的技术难题。

为了更好地准备这样的面试,建议求职者不仅要掌握扎实的基础知识,还要能够清晰地表达自己的思考过程和解决方案,并准备好讨论自己在以往工作中遇到的具体案例。
使用的上下文块数量: 2

5. 运行评估测试...
2025-06-18 17:14:38,486 - INFO - API响应状态码: 200
2025-06-18 17:14:38,487 - INFO - API响应类型: <class 'dashscope.api_entities.dashscope_response.GenerationResponse'>
2025-06-18 17:14:38,487 - INFO - 回答生成完成
2025-06-18 17:14:38,487 - INFO - 查询处理完成
2025-06-18 17:14:38,487 - INFO - 开始运行评估测试,样本数量: 5
2025-06-18 17:14:38,487 - INFO - 处理样本 1/5
2025-06-18 17:14:38,487 - INFO - 开始处理查询: Java程序员面试中常见的技术问题有哪些?
2025-06-18 17:14:38,487 - INFO - 开始上下文增强搜索,查询: Java程序员面试中常见的技术问题有哪些?...
2025-06-18 17:14:38,487 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:14:38,487 - INFO - 总文本数量: 1, 批处理大小: 20
2025-06-18 17:14:38,487 - INFO - 处理批次 1/1,包含 1 个文本
2025-06-18 17:14:38,776 - INFO - 批次 1 完成,获取到 1 个嵌入向量
2025-06-18 17:14:38,777 - INFO - 嵌入向量创建完成,共 1 个向量
2025-06-18 17:14:38,793 - INFO - 搜索完成,返回 2 个上下文块
2025-06-18 17:14:38,794 - INFO - 开始生成回答
2025-06-18 17:14:38,794 - INFO - 使用模型: qwen-max
2025-06-18 17:14:38,794 - INFO - 用户消息长度: 2043 字符
2025-06-18 17:14:43,857 - INFO - API响应状态码: 200
2025-06-18 17:14:43,857 - INFO - API响应类型: <class 'dashscope.api_entities.dashscope_response.GenerationResponse'>
2025-06-18 17:14:43,857 - INFO - 回答生成完成
2025-06-18 17:14:43,857 - INFO - 查询处理完成
2025-06-18 17:14:43,857 - INFO - 开始评估回答质量
2025-06-18 17:14:43,857 - INFO - 评估提示长度: 540 字符
2025-06-18 17:14:48,374 - INFO - 评估API响应状态码: 200
2025-06-18 17:14:48,374 - INFO - 评估完成
2025-06-18 17:14:48,374 - INFO - 处理样本 2/5
2025-06-18 17:14:48,375 - INFO - 开始处理查询: Spring框架在Java开发中的重要性是什么?
2025-06-18 17:14:48,375 - INFO - 开始上下文增强搜索,查询: Spring框架在Java开发中的重要性是什么?...
2025-06-18 17:14:48,375 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:14:48,375 - INFO - 总文本数量: 1, 批处理大小: 20
2025-06-18 17:14:48,375 - INFO - 处理批次 1/1,包含 1 个文本
2025-06-18 17:14:48,568 - INFO - 批次 1 完成,获取到 1 个嵌入向量
2025-06-18 17:14:48,568 - INFO - 嵌入向量创建完成,共 1 个向量
2025-06-18 17:14:48,583 - INFO - 搜索完成,返回 3 个上下文块
2025-06-18 17:14:48,583 - INFO - 开始生成回答
2025-06-18 17:14:48,583 - INFO - 使用模型: qwen-max
2025-06-18 17:14:48,583 - INFO - 用户消息长度: 3055 字符
2025-06-18 17:14:49,771 - INFO - API响应状态码: 200
2025-06-18 17:14:49,771 - INFO - API响应类型: <class 'dashscope.api_entities.dashscope_response.GenerationResponse'>
2025-06-18 17:14:49,771 - INFO - 回答生成完成
2025-06-18 17:14:49,771 - INFO - 查询处理完成
2025-06-18 17:14:49,771 - INFO - 开始评估回答质量
2025-06-18 17:14:49,771 - INFO - 评估提示长度: 317 字符
2025-06-18 17:14:53,326 - INFO - 评估API响应状态码: 200
2025-06-18 17:14:53,326 - INFO - 评估完成
2025-06-18 17:14:53,326 - INFO - 处理样本 3/5
2025-06-18 17:14:53,326 - INFO - 开始处理查询: JVM调优有哪些常用的方法?
2025-06-18 17:14:53,326 - INFO - 开始上下文增强搜索,查询: JVM调优有哪些常用的方法?...
2025-06-18 17:14:53,326 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:14:53,326 - INFO - 总文本数量: 1, 批处理大小: 20
2025-06-18 17:14:53,326 - INFO - 处理批次 1/1,包含 1 个文本
2025-06-18 17:14:53,562 - INFO - 批次 1 完成,获取到 1 个嵌入向量
2025-06-18 17:14:53,563 - INFO - 嵌入向量创建完成,共 1 个向量
2025-06-18 17:14:53,577 - INFO - 搜索完成,返回 3 个上下文块
2025-06-18 17:14:53,577 - INFO - 开始生成回答
2025-06-18 17:14:53,577 - INFO - 使用模型: qwen-max
2025-06-18 17:14:53,577 - INFO - 用户消息长度: 3045 字符
2025-06-18 17:15:09,663 - INFO - API响应状态码: 200
2025-06-18 17:15:09,663 - INFO - API响应类型: <class 'dashscope.api_entities.dashscope_response.GenerationResponse'>
2025-06-18 17:15:09,663 - INFO - 回答生成完成
2025-06-18 17:15:09,663 - INFO - 查询处理完成
2025-06-18 17:15:09,663 - INFO - 开始评估回答质量
2025-06-18 17:15:09,663 - INFO - 评估提示长度: 938 字符
2025-06-18 17:15:14,954 - INFO - 评估API响应状态码: 200
2025-06-18 17:15:14,954 - INFO - 评估完成
2025-06-18 17:15:14,954 - INFO - 处理样本 4/5
2025-06-18 17:15:14,954 - INFO - 开始处理查询: Java多线程编程中需要注意什么?
2025-06-18 17:15:14,954 - INFO - 开始上下文增强搜索,查询: Java多线程编程中需要注意什么?...
2025-06-18 17:15:14,954 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:15:14,954 - INFO - 总文本数量: 1, 批处理大小: 20
2025-06-18 17:15:14,954 - INFO - 处理批次 1/1,包含 1 个文本
2025-06-18 17:15:15,174 - INFO - 批次 1 完成,获取到 1 个嵌入向量
2025-06-18 17:15:15,174 - INFO - 嵌入向量创建完成,共 1 个向量
2025-06-18 17:15:15,190 - INFO - 搜索完成,返回 3 个上下文块
2025-06-18 17:15:15,190 - INFO - 开始生成回答
2025-06-18 17:15:15,190 - INFO - 使用模型: qwen-max
2025-06-18 17:15:15,190 - INFO - 用户消息长度: 3048 字符
2025-06-18 17:15:20,634 - INFO - API响应状态码: 200
2025-06-18 17:15:20,634 - INFO - API响应类型: <class 'dashscope.api_entities.dashscope_response.GenerationResponse'>
2025-06-18 17:15:20,634 - INFO - 回答生成完成
2025-06-18 17:15:20,634 - INFO - 查询处理完成
2025-06-18 17:15:20,634 - INFO - 开始评估回答质量
2025-06-18 17:15:20,634 - INFO - 评估提示长度: 471 字符
2025-06-18 17:15:26,280 - INFO - 评估API响应状态码: 200
2025-06-18 17:15:26,280 - INFO - 评估完成
2025-06-18 17:15:26,280 - INFO - 处理样本 5/5
2025-06-18 17:15:26,280 - INFO - 开始处理查询: 数据库优化在Java项目中的作用是什么?
2025-06-18 17:15:26,280 - INFO - 开始上下文增强搜索,查询: 数据库优化在Java项目中的作用是什么?...
2025-06-18 17:15:26,280 - INFO - 开始创建嵌入向量,使用模型: text-embedding-v2
2025-06-18 17:15:26,280 - INFO - 总文本数量: 1, 批处理大小: 20
2025-06-18 17:15:26,280 - INFO - 处理批次 1/1,包含 1 个文本
2025-06-18 17:15:26,581 - INFO - 批次 1 完成,获取到 1 个嵌入向量
2025-06-18 17:15:26,581 - INFO - 嵌入向量创建完成,共 1 个向量
2025-06-18 17:15:26,596 - INFO - 搜索完成,返回 3 个上下文块
2025-06-18 17:15:26,596 - INFO - 开始生成回答
2025-06-18 17:15:26,596 - INFO - 使用模型: qwen-max
2025-06-18 17:15:26,596 - INFO - 用户消息长度: 3051 字符
2025-06-18 17:15:29,829 - INFO - API响应状态码: 200
2025-06-18 17:15:29,829 - INFO - API响应类型: <class 'dashscope.api_entities.dashscope_response.GenerationResponse'>
2025-06-18 17:15:29,829 - INFO - 回答生成完成
2025-06-18 17:15:29,829 - INFO - 查询处理完成
2025-06-18 17:15:29,829 - INFO - 开始评估回答质量
2025-06-18 17:15:29,829 - INFO - 评估提示长度: 384 字符
2025-06-18 17:15:37,631 - INFO - 评估API响应状态码: 200
2025-06-18 17:15:37,631 - INFO - 评估完成
2025-06-18 17:15:37,631 - INFO - 评估测试完成,成功处理 5 个样本

============================================================
评估结果汇总
============================================================

样本 1:
问题: Java程序员面试中常见的技术问题有哪些?
AI回答: 根据提供的信息,文档中并没有直接列出2024年Java程序员面试中具体会遇到哪些常见的技术问题。它主要讨论了面试准备的整体策略、简历写作技巧以及强调了现今的面试更倾向于考察候选人对项目的理解、场景化问...
标准答案: Java程序员面试中常见的技术问题包括:Java基础(面向对象、集合框架、异常处理)、多线程和并发编程、JVM原理和调优、Spring框架(IOC、AOP、事务管理)、数据库(SQL优化、索引、事务)...
评估结果: 评分:0.5分

理由:AI的回答指出了面试过程中可能考察的重点方向,如基于项目经验的实际问题解决能力和技术深度思考等,但没有直接回答用户关于"常见的技术问题"的具体询问。标准答案提供了更详细且直接相关的信息,列举了具体的Java技术领域和技术点,这正是用户所寻求的答案。因此,AI的回答部分正确,因为它虽然触及到了面试中可能会关注的方面,却没有给出用户想要的具体技术问题列表。
----------------------------------------

样本 2:
问题: Spring框架在Java开发中的重要性是什么?
AI回答: 我无法从提供的信息中找到答案。...
标准答案: Spring框架在Java开发中具有重要地位,主要体现在:1)IOC容器管理对象依赖,降低代码耦合度;2)AOP面向切面编程,实现横切关注点的模块化;3)提供统一的事务管理机制;4)集成各种数据访问技...
评估结果: 评分:0.0

理由:AI助手的回答表明它未能提供任何关于Spring框架在Java开发中重要性的信息,而直接表示无法找到答案。这与用户的问题完全不符,也未尝试基于已有知识给出任何形式的回答或解释。根据提供的标准答案来看,正确的回答应该包括Spring框架如何通过其特性(如IOC容器、AOP等)来提高Java项目的开发效率和质量等方面的信息。因此,按照评估标准,此回答应得分为0.0分。
----------------------------------------

样本 3:
问题: JVM调优有哪些常用的方法?
AI回答: 根据提供的信息,文本中并没有直接提到JVM调优的具体方法。但是,它提到了一些可以帮助理解和诊断JVM性能问题的工具和命令,这些间接地对于JVM调优是有帮助的。例如:

- **Arthas**:提供了...
标准答案: JVM调优的常用方法包括:1)调整堆内存大小(-Xms、-Xmx);2)设置新生代和老年代比例(-XX:NewRatio);3)选择垃圾收集器(G1、CMS、ParallelGC);4)调整GC参数(...
评估结果: 评分:0.5

理由:AI的回答虽然提到了一些与JVM性能监控和故障排除相关的工具(如Arthas、jps、jmap等),并且说明了这些工具如何间接支持JVM调优工作,但是没有直接回答用户关于"JVM调优有哪些常用的方法"的具体询问。标准答案中列举了几种具体的JVM调优措施,比如调整堆内存大小、选择垃圾收集器类型及参数设置等,这些都是更直接针对问题核心的内容。因此,AI的回答部分正确但缺乏针对性,未能充分满足用户的查询需求。
----------------------------------------

样本 4:
问题: Java多线程编程中需要注意什么?
AI回答: 我无法从提供的信息中找到答案。 

提供的上下文主要讨论了如何发现和优化Java系统的性能瓶颈,包括减少编码、减少序列化、Java极致优化以及并发读优化等方面,并没有直接提到在Java多线程编程时需要...
标准答案: Java多线程编程需要注意:1)线程安全问题,使用synchronized、volatile、Lock等机制;2)避免死锁,合理设计锁的获取顺序;3)使用线程池管理线程资源;4)注意内存可见性问题;5...
评估结果: 评分:0.0

理由:AI助手的回答表明它未能从提供的上下文中找到关于Java多线程编程需要注意事项的答案,但实际上这个问题并不依赖于特定的上下文信息来回答。标准答案中提到了几个关键点,如线程安全、避免死锁、使用线程池等,这些都是在进行Java多线程开发时非常基础且重要的注意事项。因此,AI助手应该能够直接根据问题本身提供相关信息而不是指出无法基于给定的信息作答。这显示了AI助手在处理该问题上的不足。
----------------------------------------

样本 5:
问题: 数据库优化在Java项目中的作用是什么?
AI回答: 我无法从提供的信息中找到答案。给定的上下文主要讨论了Java系统性能优化的一些方法,包括减少编码、减少序列化、Java极致优化以及并发读优化等,并没有直接提到数据库优化及其在Java项目中的作用。...
标准答案: 数据库优化在Java项目中起到关键作用:1)提高查询性能,减少响应时间;2)优化索引设计,提升查询效率;3)合理设计表结构,避免冗余;4)使用连接池管理数据库连接;5)优化SQL语句,避免全表扫描;6...
评估结果: 评分:0.0

理由:AI助手的回答未能提供关于数据库优化在Java项目中作用的具体信息,而是表示无法从给定的信息中找到答案。实际上,用户的问题是直接询问数据库优化的作用,不需要依赖于任何特定的上下文信息来回答。标准答案列举了多个方面说明数据库优化的重要性,而AI助手的回答没有包含这些关键点或任何形式的相关信息,因此被认为是错误或无关的。
----------------------------------------

RAG系统演示完成!

进程已结束,退出代码为 0

附录

完整代码示例

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
上下文增强检索增强生成 (Context-Enriched RAG) 系统
================================================

本模块实现了一个上下文增强的RAG系统,通过检索相关文档片段及其上下文信息
来生成更准确和连贯的回答。

主要功能:
1. PDF文档文本提取和分块
2. 文本嵌入向量化
3. 上下文感知的语义搜索
4. 基于检索结果的回答生成
5. 回答质量评估

作者: RAG系统开发团队
版本: 1.0
"""

import fitz  # PyMuPDF用于PDF处理
import os
import numpy as np
import json
from dashscope import TextEmbedding, Generation
from typing import List, Dict, Tuple, Optional
import logging
from config import Config

# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


class ContextEnrichedRAG:
    """
    上下文增强检索增强生成系统

    该类实现了完整的RAG流程,包括文档处理、嵌入生成、上下文感知检索
    和回答生成等功能。
    """

    def __init__(self, api_key: str = None):
        """
        初始化RAG系统

        Args:
            api_key (str): 阿里云API密钥,如果为None则使用配置文件中的设置
        """
        # 使用配置文件中的设置或传入的参数
        self.api_key = api_key or Config.API_KEY

        # 存储文档块和嵌入
        self.text_chunks = []
        self.embeddings = []

        # 从配置文件加载参数
        self.chunk_size = Config.CHUNK_SIZE
        self.chunk_overlap = Config.CHUNK_OVERLAP
        self.context_size = Config.CONTEXT_SIZE
        self.top_k = Config.TOP_K

        logger.info("上下文增强RAG系统初始化完成")
        logger.info(f"使用嵌入模型: {Config.EMBEDDING_MODEL}")
        logger.info(f"使用语言模型: {Config.LLM_MODEL}")

    def extract_text_from_pdf(self, pdf_path: str) -> str:
        """
        从PDF文件中提取文本内容

        Args:
            pdf_path (str): PDF文件路径

        Returns:
            str: 提取的文本内容

        Raises:
            FileNotFoundError: 当PDF文件不存在时
            Exception: 当PDF处理出现错误时
        """
        try:
            logger.info(f"开始从PDF文件提取文本: {pdf_path}")

            # 检查文件是否存在
            if not os.path.exists(pdf_path):
                raise FileNotFoundError(f"PDF文件不存在: {pdf_path}")

            # 打开PDF文件
            pdf_document = fitz.open(pdf_path)
            all_text = ""

            # 遍历每一页并提取文本
            for page_num in range(pdf_document.page_count):
                page = pdf_document[page_num]
                text = page.get_text("text")
                all_text += text + "\n"  # 添加换行符分隔页面

            pdf_document.close()

            logger.info(f"成功提取文本,总长度: {len(all_text)} 字符")
            return all_text

        except Exception as e:
            logger.error(f"PDF文本提取失败: {str(e)}")
            raise

    def chunk_text(self, text: str, chunk_size: int = None, overlap: int = None) -> List[str]:
        """
        将文本分割成重叠的块

        Args:
            text (str): 要分割的文本
            chunk_size (int): 每个块的大小,默认使用实例变量
            overlap (int): 块间重叠大小,默认使用实例变量

        Returns:
            List[str]: 文本块列表
        """
        if chunk_size is None:
            chunk_size = self.chunk_size
        if overlap is None:
            overlap = self.chunk_overlap

        logger.info(f"开始文本分块,块大小: {chunk_size}, 重叠: {overlap}")

        chunks = []
        step_size = chunk_size - overlap

        # 使用滑动窗口进行分块
        for i in range(0, len(text), step_size):
            chunk = text[i:i + chunk_size]
            if chunk.strip():  # 只添加非空块
                chunks.append(chunk)

        logger.info(f"文本分块完成,共生成 {len(chunks)} 个块")
        return chunks

    def create_embeddings(self, texts: List[str], model: str = None, batch_size: int = 20) -> List[Dict]:
        """
        为文本列表创建嵌入向量

        Args:
            texts (List[str]): 要嵌入的文本列表
            model (str): 使用的嵌入模型名称,默认使用配置文件中的设置
            batch_size (int): 批处理大小,默认20(阿里云限制最多25个)

        Returns:
            List[Dict]: 包含嵌入向量的响应列表
        """
        if model is None:
            model = Config.EMBEDDING_MODEL

        try:
            logger.info(f"开始创建嵌入向量,使用模型: {model}")
            logger.info(f"总文本数量: {len(texts)}, 批处理大小: {batch_size}")

            all_embeddings = []
            total_batches = (len(texts) + batch_size - 1) // batch_size

            for i in range(0, len(texts), batch_size):
                batch_texts = texts[i:i + batch_size]
                batch_num = i // batch_size + 1
                
                logger.info(f"处理批次 {batch_num}/{total_batches},包含 {len(batch_texts)} 个文本")

                # 使用阿里云dashscope创建嵌入
                response = TextEmbedding.call(
                    model=model,
                    input=batch_texts,
                    api_key=self.api_key
                )

                if response.status_code == 200:
                    batch_embeddings = response.output.get('embeddings', [])
                    all_embeddings.extend(batch_embeddings)
                    logger.info(f"批次 {batch_num} 完成,获取到 {len(batch_embeddings)} 个嵌入向量")
                else:
                    raise Exception(f"批次 {batch_num} 嵌入API调用失败: {response.message}")

            logger.info(f"嵌入向量创建完成,共 {len(all_embeddings)} 个向量")
            return all_embeddings

        except Exception as e:
            logger.error(f"嵌入向量创建失败: {str(e)}")
            raise

    def cosine_similarity(self, vec1: np.ndarray, vec2: np.ndarray) -> float:
        """
        计算两个向量之间的余弦相似度

        Args:
            vec1 (np.ndarray): 第一个向量
            vec2 (np.ndarray): 第二个向量

        Returns:
            float: 余弦相似度值 (0-1之间)
        """
        # 计算向量的点积除以向量范数的乘积
        dot_product = np.dot(vec1, vec2)
        norm_product = np.linalg.norm(vec1) * np.linalg.norm(vec2)

        # 避免除零错误
        if norm_product == 0:
            return 0.0

        return dot_product / norm_product

    def context_enriched_search(self, query: str, k: int = None, context_size: int = None) -> List[str]:
        """
        执行上下文增强的语义搜索

        Args:
            query (str): 查询文本
            k (int): 返回的相关块数量,默认使用实例变量
            context_size (int): 包含的上下文块数量,默认使用实例变量

        Returns:
            List[str]: 相关文本块及其上下文
        """
        if k is None:
            k = self.top_k
        if context_size is None:
            context_size = self.context_size

        logger.info(f"开始上下文增强搜索,查询: {query[:50]}...")

        try:
            # 为查询创建嵌入向量
            query_embedding_response = self.create_embeddings([query])
            query_embedding = np.array(query_embedding_response[0].get('embedding', []))

            similarity_scores = []

            # 计算查询与每个文档块的相似度
            for i, chunk_embedding in enumerate(self.embeddings):
                chunk_vector = np.array(chunk_embedding.get('embedding', []))
                similarity = self.cosine_similarity(query_embedding, chunk_vector)
                similarity_scores.append((i, similarity))

            # 按相似度降序排序
            similarity_scores.sort(key=lambda x: x[1], reverse=True)

            # 获取最相关的块索引
            top_index = similarity_scores[0][0]

            # 计算上下文范围
            start_idx = max(0, top_index - context_size)
            end_idx = min(len(self.text_chunks), top_index + context_size + 1)

            # 返回相关块及其上下文
            result_chunks = [self.text_chunks[i] for i in range(start_idx, end_idx)]

            logger.info(f"搜索完成,返回 {len(result_chunks)} 个上下文块")
            return result_chunks

        except Exception as e:
            logger.error(f"上下文增强搜索失败: {str(e)}")
            raise

    def generate_response(self, query: str, context_chunks: List[str],
                          model: str = None) -> str:
        """
        基于检索的上下文生成回答

        Args:
            query (str): 用户查询
            context_chunks (List[str]): 检索到的上下文块
            model (str): 使用的语言模型,默认使用配置文件中的设置

        Returns:
            str: 生成的回答
        """
        if model is None:
            model = Config.LLM_MODEL

        try:
            logger.info("开始生成回答")

            # 构建系统提示
            system_prompt = """你是一个专业的AI助手,请严格基于提供的上下文信息来回答问题。
如果无法从给定上下文中得出答案,请回复:"我无法从提供的信息中找到答案。" """

            # 构建用户消息
            context_text = "\n\n".join([f"上下文 {i + 1}:\n{chunk}" for i, chunk in enumerate(context_chunks)])
            user_message = f"{context_text}\n\n问题: {query}"

            logger.info(f"使用模型: {model}")
            logger.info(f"用户消息长度: {len(user_message)} 字符")

            # 调用语言模型生成回答
            response = Generation.call(
                model=model,
                messages=[
                    {'role': 'system', 'content': system_prompt},
                    {'role': 'user', 'content': user_message}
                ],
                api_key=self.api_key
            )

            logger.info(f"API响应状态码: {response.status_code}")
            logger.info(f"API响应类型: {type(response)}")

            if response is None:
                raise Exception("API响应为空")

            if response.status_code == 200:
                if hasattr(response, 'output') and response.output is not None:
                    # 阿里云GenerationResponse的格式
                    if hasattr(response.output, 'text'):
                        answer = response.output.text
                        logger.info("回答生成完成")
                        return answer
                    elif hasattr(response.output, 'choices') and response.output.choices:
                        # 兼容OpenAI格式
                        answer = response.output.choices[0].message.content
                        logger.info("回答生成完成")
                        return answer
                    else:
                        logger.error(f"响应格式不支持: {response.output}")
                        raise Exception("API响应格式不支持")
                else:
                    logger.error(f"响应中没有output: {response}")
                    raise Exception("API响应格式错误:没有output")
            else:
                error_msg = response.message if hasattr(response, 'message') else '未知错误'
                raise Exception(f"生成API调用失败: {error_msg}")

        except Exception as e:
            logger.error(f"回答生成失败: {str(e)}")
            raise

    def evaluate_response(self, query: str, ai_response: str, true_answer: str) -> Dict:
        """
        评估AI回答的质量

        Args:
            query (str): 原始查询
            ai_response (str): AI生成的回答
            true_answer (str): 标准答案

        Returns:
            Dict: 包含评分和评估结果的字典
        """
        try:
            logger.info("开始评估回答质量")

            # 构建评估提示
            evaluation_prompt = f"""你是一个智能评估系统,请评估AI助手的回答质量。

评估标准:
- 1.0分:回答完全正确且详细
- 0.8分:回答基本正确,略有不足
- 0.5分:回答部分正确
- 0.0分:回答错误或无关

用户问题: {query}
AI回答: {ai_response}
标准答案: {true_answer}

请给出评分(0.0-1.0)和简要理由:"""

            logger.info(f"评估提示长度: {len(evaluation_prompt)} 字符")

            # 调用模型进行评估
            response = Generation.call(
                model=Config.LLM_MODEL,
                messages=[
                    {'role': 'system', 'content': '你是一个专业的评估专家。'},
                    {'role': 'user', 'content': evaluation_prompt}
                ],
                api_key=self.api_key
            )

            logger.info(f"评估API响应状态码: {response.status_code}")

            if response is None:
                raise Exception("评估API响应为空")

            if response.status_code == 200:
                if hasattr(response, 'output') and response.output is not None:
                    # 阿里云GenerationResponse的格式
                    if hasattr(response.output, 'text'):
                        evaluation_result = response.output.text
                        logger.info("评估完成")
                        return {
                            "evaluation": evaluation_result,
                            "query": query,
                            "ai_response": ai_response,
                            "true_answer": true_answer
                        }
                    elif hasattr(response.output, 'choices') and response.output.choices:
                        # 兼容OpenAI格式
                        evaluation_result = response.output.choices[0].message.content
                        logger.info("评估完成")
                        return {
                            "evaluation": evaluation_result,
                            "query": query,
                            "ai_response": ai_response,
                            "true_answer": true_answer
                        }
                    else:
                        logger.error(f"评估响应格式不支持: {response.output}")
                        raise Exception("评估API响应格式不支持")
                else:
                    logger.error(f"评估响应中没有output: {response}")
                    raise Exception("评估API响应格式错误:没有output")
            else:
                error_msg = response.message if hasattr(response, 'message') else '未知错误'
                raise Exception(f"评估API调用失败: {error_msg}")

        except Exception as e:
            logger.error(f"回答评估失败: {str(e)}")
            raise

    def load_document(self, pdf_path: str = None) -> None:
        """
        加载并处理PDF文档

        Args:
            pdf_path (str): PDF文件路径,如果为None则使用配置文件中的路径
        """
        if pdf_path is None:
            pdf_path = Config.PDF_PATH

        logger.info(f"开始加载文档: {pdf_path}")

        # 提取文本
        text = self.extract_text_from_pdf(pdf_path)

        # 分块处理
        self.text_chunks = self.chunk_text(text)

        # 创建嵌入向量
        self.embeddings = self.create_embeddings(self.text_chunks)

        logger.info("文档加载和处理完成")

    def load_validation_data(self, val_file_path: str = None) -> List[Dict]:
        """
        加载验证数据集

        Args:
            val_file_path (str): 验证数据文件路径,如果为None则使用配置文件中的路径

        Returns:
            List[Dict]: 验证数据列表
        """
        if val_file_path is None:
            val_file_path = Config.VAL_FILE_PATH

        try:
            logger.info(f"加载验证数据: {val_file_path}")

            with open(val_file_path, 'r', encoding='utf-8') as f:
                data = json.load(f)

            logger.info(f"验证数据加载完成,共 {len(data)} 条记录")
            return data

        except Exception as e:
            logger.error(f"验证数据加载失败: {str(e)}")
            raise

    def run_query(self, query: str) -> Dict:
        """
        运行完整的查询流程

        Args:
            query (str): 用户查询

        Returns:
            Dict: 包含检索结果和生成回答的字典
        """
        logger.info(f"开始处理查询: {query}")

        try:
            # 执行上下文增强搜索
            context_chunks = self.context_enriched_search(query)

            # 生成回答
            answer = self.generate_response(query, context_chunks)

            result = {
                "query": query,
                "context_chunks": context_chunks,
                "answer": answer,
                "num_context_chunks": len(context_chunks)
            }

            logger.info("查询处理完成")
            return result

        except Exception as e:
            logger.error(f"查询处理失败: {str(e)}")
            raise

    def run_evaluation(self, val_data: List[Dict], num_samples: int = None) -> List[Dict]:
        """
        运行评估测试

        Args:
            val_data (List[Dict]): 验证数据
            num_samples (int): 测试样本数量,如果为None则使用配置文件中的设置

        Returns:
            List[Dict]: 评估结果列表
        """
        if num_samples is None:
            num_samples = Config.EVAL_SAMPLES

        logger.info(f"开始运行评估测试,样本数量: {num_samples}")

        results = []

        for i, sample in enumerate(val_data[:num_samples]):
            try:
                logger.info(f"处理样本 {i + 1}/{num_samples}")

                # 运行查询
                query_result = self.run_query(sample['question'])

                # 评估回答
                evaluation = self.evaluate_response(
                    sample['question'],
                    query_result['answer'],
                    sample['ideal_answer']
                )

                # 合并结果
                result = {
                    "sample_id": i + 1,
                    "query": sample['question'],
                    "ai_answer": query_result['answer'],
                    "true_answer": sample['ideal_answer'],
                    "evaluation": evaluation['evaluation'],
                    "context_chunks": query_result['context_chunks']
                }

                results.append(result)

            except Exception as e:
                logger.error(f"样本 {i + 1} 处理失败: {str(e)}")
                continue

        logger.info(f"评估测试完成,成功处理 {len(results)} 个样本")
        return results


def main():
    """
    主函数 - 演示RAG系统的完整功能
    """
    print("=" * 60)
    print("上下文增强RAG系统演示")
    print("=" * 60)

    # 验证配置
    if not Config.validate_config():
        print("\n配置验证失败,请检查config.py文件中的设置。")
        print("请确保:")
        print("1. 设置了有效的API密钥")
        print("2. PDF文件和验证数据文件存在")
        print("3. 文件路径正确")
        return

    try:
        # 初始化RAG系统
        print("\n1. 初始化RAG系统...")
        rag_system = ContextEnrichedRAG()

        # 加载文档
        print("\n2. 加载PDF文档...")
        rag_system.load_document()

        # 加载验证数据
        print("\n3. 加载验证数据...")
        val_data = rag_system.load_validation_data()

        # 运行单个查询示例
        print("\n4. 运行查询示例...")
        sample_query = val_data[0]['question']
        print(f"查询: {sample_query}")

        result = rag_system.run_query(sample_query)
        print(f"回答: {result['answer']}")
        print(f"使用的上下文块数量: {result['num_context_chunks']}")

        # 运行评估测试
        print("\n5. 运行评估测试...")
        evaluation_results = rag_system.run_evaluation(val_data)

        # 显示评估结果
        print("\n" + "=" * 60)
        print("评估结果汇总")
        print("=" * 60)

        for i, result in enumerate(evaluation_results):
            print(f"\n样本 {i + 1}:")
            print(f"问题: {result['query']}")
            print(f"AI回答: {result['ai_answer'][:100]}...")
            print(f"标准答案: {result['true_answer'][:100]}...")
            print(f"评估结果: {result['evaluation']}")
            print("-" * 40)

        print("\nRAG系统演示完成!")

    except Exception as e:
        print(f"\n错误: {str(e)}")
        print("请检查API密钥配置和文件路径是否正确。")


if __name__ == "__main__":
    main()

配置文件

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
RAG系统配置文件
==============

本文件包含RAG系统的所有配置参数,包括API密钥、模型设置、文件路径等。
"""

import os
from typing import Dict, Any


class Config:
    """RAG系统配置类"""

    # API配置
    API_KEY = os.getenv("ALI_API_KEY", "sk-0c198b40580347fdxxx")  # 阿里云API密钥
    BASE_URL = "https://dashscope.aliyuncs.com/api/v1/"  # 阿里云API基础URL

    # 模型配置
    EMBEDDING_MODEL = "text-embedding-v2"  # 阿里云嵌入模型
    LLM_MODEL = "qwen-max"  # 阿里云语言模型

    # 文档处理配置
    CHUNK_SIZE = 1000  # 文档块大小
    CHUNK_OVERLAP = 200  # 块间重叠大小
    CONTEXT_SIZE = 1  # 上下文块数量
    TOP_K = 1  # 检索的top-k结果

    # 文件路径配置
    PDF_PATH = "2888年Java程序员找工作最新场景题.pdf"  # PDF文档路径
    VAL_FILE_PATH = "../data/java_val.json"  # 验证数据文件路径

    # 生成参数
    TEMPERATURE = 0.1  # 生成温度
    MAX_TOKENS = 500  # 最大生成token数

    # 评估配置
    EVAL_SAMPLES = 5  # 评估样本数量

    @classmethod
    def get_all_config(cls) -> Dict[str, Any]:
        """获取所有配置参数"""
        return {
            "api_key": cls.API_KEY,
            "base_url": cls.BASE_URL,
            "embedding_model": cls.EMBEDDING_MODEL,
            "llm_model": cls.LLM_MODEL,
            "chunk_size": cls.CHUNK_SIZE,
            "chunk_overlap": cls.CHUNK_OVERLAP,
            "context_size": cls.CONTEXT_SIZE,
            "top_k": cls.TOP_K,
            "pdf_path": cls.PDF_PATH,
            "val_file_path": cls.VAL_FILE_PATH,
            "temperature": cls.TEMPERATURE,
            "max_tokens": cls.MAX_TOKENS,
            "eval_samples": cls.EVAL_SAMPLES
        }

    @classmethod
    def validate_config(cls) -> bool:
        """验证配置参数的有效性"""
        if cls.API_KEY == "your_api_key_here":
            print("警告: 请设置有效的API密钥")
            return False

        if not os.path.exists(cls.PDF_PATH):
            print(f"错误: PDF文件不存在: {cls.PDF_PATH}")
            return False

        if not os.path.exists(cls.VAL_FILE_PATH):
            print(f"错误: 验证数据文件不存在: {cls.VAL_FILE_PATH}")
            return False

        return True


# 环境变量配置示例
ENV_CONFIG_EXAMPLE = """
# 在运行前,请设置以下环境变量或在config.py中直接修改

# 阿里云API密钥
export ALI_API_KEY="your_actual_api_key_here"

# 或者创建.env文件
echo "ALI_API_KEY=your_actual_api_key_here" > .env
"""

RAG评估文件示例

question (问题) ↓ ideal_answer (标准答案) ← reference (答案来源) ↓ has_answer (是否有答案) ← reasoning (判断依据)

在RAG系统评估中的作用

  1. 输入测试:使用 question 作为用户查询

  2. 质量评估:将系统回答与 ideal_answer 对比

  3. 准确性验证:通过 reference 确认信息来源

  4. 期望管理:通过 has_answer 设置合理期望.

    1. 期望行为变化

    设置为 true 时:

    • 系统应该能够找到相关信息并给出完整回答

    • 期望回答包含详细的技术内容和具体信息

    设置为 false 时:

    • 系统应该回答 "I cannot find the answer" 或类似的无答案响应

    • 期望系统诚实承认无法提供相关信息

  5. 评估解释:通过 reasoning 理解评估标准

json 复制代码
[
    {
        "question": "Java程序员面试中常见的技术问题有哪些?",
        "ideal_answer": "Java程序员面试中常见的技术问题包括:Java基础(面向对象、集合框架、异常处理)、多线程和并发编程、JVM原理和调优、Spring框架(IOC、AOP、事务管理)、数据库(SQL优化、索引、事务)、设计模式、算法和数据结构、微服务架构、分布式系统等。面试官通常会考察候选人的理论基础和实践经验。",
        "reference": "Java程序员面试技术问题章节",
        "has_answer": true,
        "reasoning": "文档详细介绍了Java程序员面试中常见的技术问题类型和考察重点。"
    },
    {
        "question": "Spring框架在Java开发中的重要性是什么?",
        "ideal_answer": "Spring框架在Java开发中具有重要地位,主要体现在:1)IOC容器管理对象依赖,降低代码耦合度;2)AOP面向切面编程,实现横切关注点的模块化;3)提供统一的事务管理机制;4)集成各种数据访问技术;5)支持微服务架构开发。Spring已成为Java企业级开发的标准框架。",
        "reference": "Spring框架应用章节",
        "has_answer": true,
        "reasoning": "文档详细说明了Spring框架的核心特性和在企业开发中的重要作用。"
    },
    {
        "question": "JVM调优有哪些常用的方法?",
        "ideal_answer": "JVM调优的常用方法包括:1)调整堆内存大小(-Xms、-Xmx);2)设置新生代和老年代比例(-XX:NewRatio);3)选择垃圾收集器(G1、CMS、ParallelGC);4)调整GC参数(-XX:MaxGCPauseMillis);5)监控GC日志分析性能瓶颈;6)使用JVM监控工具(JProfiler、MAT)分析内存使用情况。",
        "reference": "JVM调优和性能优化章节",
        "has_answer": true,
        "reasoning": "文档提供了详细的JVM调优方法和参数配置指导。"
    },
    {
        "question": "Java多线程编程中需要注意什么?",
        "ideal_answer": "Java多线程编程需要注意:1)线程安全问题,使用synchronized、volatile、Lock等机制;2)避免死锁,合理设计锁的获取顺序;3)使用线程池管理线程资源;4)注意内存可见性问题;5)合理使用并发工具类(CountDownLatch、CyclicBarrier、Semaphore);6)避免过度使用线程,考虑性能开销。",
        "reference": "多线程和并发编程章节",
        "has_answer": true,
        "reasoning": "文档详细说明了多线程编程的关键注意事项和最佳实践。"
    },
    {
        "question": "数据库优化在Java项目中的作用是什么?",
        "ideal_answer": "数据库优化在Java项目中起到关键作用:1)提高查询性能,减少响应时间;2)优化索引设计,提升查询效率;3)合理设计表结构,避免冗余;4)使用连接池管理数据库连接;5)优化SQL语句,避免全表扫描;6)分库分表处理大数据量;7)使用缓存减少数据库压力。",
        "reference": "数据库优化和性能调优章节",
        "has_answer": true,
        "reasoning": "文档详细介绍了数据库优化的重要性和具体方法。"
    },
    {
        "question": "微服务架构的优势和挑战是什么?",
        "ideal_answer": "微服务架构的优势:1)服务独立部署,技术栈灵活;2)团队独立开发,提高开发效率;3)故障隔离,提高系统稳定性;4)易于扩展和维护。挑战:1)分布式系统复杂性增加;2)服务间通信开销;3)数据一致性问题;4)运维复杂度提高;5)需要完善的监控和日志系统。",
        "reference": "微服务架构设计章节",
        "has_answer": true,
        "reasoning": "文档全面分析了微服务架构的优缺点和适用场景。"
    },
    {
        "question": "设计模式在Java开发中的应用场景有哪些?",
        "ideal_answer": "设计模式在Java开发中的常见应用场景:1)单例模式用于配置管理、数据库连接池;2)工厂模式用于对象创建;3)观察者模式用于事件处理;4)策略模式用于算法选择;5)代理模式用于权限控制、缓存;6)适配器模式用于接口兼容;7)装饰器模式用于功能扩展。",
        "reference": "设计模式应用章节",
        "has_answer": true,
        "reasoning": "文档详细介绍了各种设计模式在Java项目中的实际应用。"
    },
    {
        "question": "Java程序员找工作需要多少年经验?",
        "ideal_answer": "Java程序员找工作的经验要求:1)初级开发:0-2年经验,掌握Java基础、Spring框架、数据库;2)中级开发:3-5年经验,具备系统设计能力、性能优化经验;3)高级开发:5年以上经验,具备架构设计、团队管理能力;4)专家级:8年以上经验,具备技术决策、创新研究能力。",
        "reference": "Java程序员职业发展章节",
        "has_answer": true,
        "reasoning": "文档详细说明了不同级别Java程序员的经验要求和技能标准。"
    },
    {
        "question": "初级Java程序员的薪资水平如何?",
        "ideal_answer": "初级Java程序员的薪资水平:1)一线城市:8-15K/月;2)二线城市:6-12K/月;3)三线城市:4-8K/月。薪资影响因素:1)技术能力水平;2)学历背景;3)公司规模和行业;4)地区经济发展水平;5)项目经验丰富程度。建议关注技能提升而非短期薪资。",
        "reference": "Java程序员薪资分析章节",
        "has_answer": true,
        "reasoning": "文档提供了详细的薪资水平分析和影响因素说明。"
    },
    {
        "question": "Java程序员如何提升自己的技能?",
        "ideal_answer": "Java程序员技能提升方法:1)深入学习Java核心技术(JVM、并发编程);2)掌握主流框架(Spring、MyBatis、Dubbo);3)学习数据库优化和分布式系统;4)参与开源项目,提升代码质量;5)阅读技术书籍和源码;6)参加技术会议和培训;7)实践项目,积累实战经验。",
        "reference": "Java程序员技能提升章节",
        "has_answer": true,
        "reasoning": "文档提供了系统性的技能提升路径和方法指导。"
    },
    {
        "question": "简历中应该突出哪些Java相关技能?",
        "ideal_answer": "简历中应突出的Java技能:1)核心技术:Java基础、JVM调优、多线程编程;2)框架技术:Spring全家桶、MyBatis、Redis;3)数据库:MySQL、Oracle、MongoDB;4)中间件:消息队列、缓存、搜索引擎;5)工具:Git、Maven、Docker;6)项目经验:具体项目描述、技术难点、解决方案。",
        "reference": "Java程序员简历编写章节",
        "has_answer": true,
        "reasoning": "文档详细指导了简历中Java技能的展示方法和重点内容。"
    },
    {
        "question": "Java程序员面试中如何展示项目经验?",
        "ideal_answer": "面试中展示项目经验的方法:1)准备项目背景、技术架构、个人职责;2)重点说明技术难点和解决方案;3)量化项目成果(性能提升、用户增长);4)展示代码质量和设计思路;5)准备项目中的技术细节和优化点;6)说明团队协作和沟通能力。",
        "reference": "Java程序员面试技巧章节",
        "has_answer": true,
        "reasoning": "文档提供了详细的面试中项目经验展示技巧和要点。"
    },
    {
        "question": "大厂Java程序员面试的流程是怎样的?",
        "ideal_answer": "大厂Java程序员面试流程:1)简历筛选和HR初筛;2)技术笔试或在线编程测试;3)技术一面:基础知识和算法;4)技术二面:系统设计和项目经验;5)技术三面:架构设计和深度技术;6)HR面:薪资谈判和公司文化;7)背景调查和offer发放。",
        "reference": "大厂面试流程章节",
        "has_answer": true,
        "reasoning": "文档详细描述了大厂面试的完整流程和各环节重点。"
    },
    {
        "question": "Java程序员职业发展路径是什么?",
        "ideal_answer": "Java程序员职业发展路径:1)初级开发:掌握基础技术,完成功能开发;2)中级开发:负责模块设计,解决技术难题;3)高级开发:负责系统架构,指导团队开发;4)技术专家:技术决策,创新研究;5)技术管理:团队管理,项目管理;6)架构师:系统架构设计,技术战略规划。",
        "reference": "Java程序员职业规划章节",
        "has_answer": true,
        "reasoning": "文档提供了清晰的职业发展路径和每个阶段的要求。"
    },
    {
        "question": "学习Java应该从哪些方面开始?",
        "ideal_answer": "学习Java的路径:1)Java基础:语法、面向对象、集合框架;2)进阶知识:多线程、JVM原理、反射机制;3)框架学习:Spring、MyBatis、SpringBoot;4)数据库:MySQL、Redis、MongoDB;5)工具使用:Git、Maven、IDEA;6)项目实践:从简单项目开始,逐步增加复杂度。",
        "reference": "Java学习路径章节",
        "has_answer": true,
        "reasoning": "文档提供了系统性的Java学习路径和重点内容。"
    },
    {
        "question": "Java程序员需要掌握哪些框架?",
        "ideal_answer": "Java程序员需要掌握的框架:1)Spring框架:IOC、AOP、事务管理;2)SpringBoot:快速开发、自动配置;3)SpringCloud:微服务架构;4)MyBatis:数据持久层;5)Redis:缓存和分布式锁;6)消息队列:RabbitMQ、Kafka;7)搜索引擎:Elasticsearch;8)监控工具:Prometheus、Grafana。",
        "reference": "Java框架技术栈章节",
        "has_answer": true,
        "reasoning": "文档详细介绍了Java开发中常用的框架和技术栈。"
    },
    {
        "question": "算法和数据结构对Java程序员的重要性?",
        "ideal_answer": "算法和数据结构对Java程序员的重要性:1)提高代码效率,解决性能问题;2)面试必备技能,大厂必考内容;3)培养编程思维,提升代码质量;4)解决复杂业务问题的基础;5)理解框架源码和系统设计;6)提升技术深度和广度。建议重点掌握:排序、查找、树、图、动态规划等。",
        "reference": "算法和数据结构重要性章节",
        "has_answer": true,
        "reasoning": "文档强调了算法和数据结构在Java开发中的重要作用。"
    },
    {
        "question": "Java程序员如何准备面试?",
        "ideal_answer": "Java程序员面试准备:1)复习基础知识:Java核心、框架原理、数据库;2)刷算法题:LeetCode、剑指Offer;3)准备项目经验:技术难点、解决方案、优化点;4)学习系统设计:分布式、高并发、高可用;5)模拟面试:找朋友或同事进行模拟;6)了解公司:技术栈、业务方向、文化氛围。",
        "reference": "Java程序员面试准备章节",
        "has_answer": true,
        "reasoning": "文档提供了全面的面试准备策略和方法。"
    },
    {
        "question": "学习Java的最佳实践是什么?",
        "ideal_answer": "学习Java的最佳实践:1)理论与实践结合,多写代码;2)阅读优秀开源项目源码;3)参与技术社区讨论;4)建立知识体系,系统学习;5)定期总结和复习;6)关注技术发展趋势;7)培养问题解决能力;8)保持持续学习的习惯。",
        "reference": "Java学习最佳实践章节",
        "has_answer": true,
        "reasoning": "文档总结了学习Java的有效方法和最佳实践。"
    },
    {
        "question": "Java程序员需要了解哪些新技术?",
        "ideal_answer": "Java程序员需要了解的新技术:1)云原生:Kubernetes、Docker、微服务;2)大数据:Hadoop、Spark、Flink;3)AI/ML:机器学习框架、深度学习;4)区块链:智能合约、分布式账本;5)边缘计算:IoT、5G应用;6)低代码平台:快速开发工具;7)DevOps:CI/CD、自动化部署。",
        "reference": "Java新技术趋势章节",
        "has_answer": true,
        "reasoning": "文档介绍了Java程序员需要关注的新技术发展趋势。"
    }
]
相关推荐
VI8664956I2635 分钟前
AEO:从搜索引擎到答案引擎,AI时代搜索优化的新战场
人工智能·搜索引擎
国际云,接待1 小时前
从CentOS迁移到TencentOS:9%成功率的一键替换实操
服务器·网络·人工智能·腾讯云
CSTechEi2 小时前
【IEEE/EI/Scopus检索】2025年第五届机器学习与大数据管理国际会议 (MLBDM 2025)
大数据·人工智能·机器学习·大数据管理·ei学术会议
要努力啊啊啊2 小时前
YOLOv5 模型结构详解
人工智能·深度学习·yolo·计算机视觉·目标跟踪
来自于狂人2 小时前
[特殊字符] 一键搭建AI语音助理:基于DashScope+GRadio的智能聊天机器人技术全解
人工智能·机器人
heyheyhey_2 小时前
大模型之深度学习PyTorch篇——导学、创建、运算
人工智能·pytorch·深度学习
大囚长2 小时前
未来的随身AI IDC--AI手机
人工智能·智能手机
UQI-LIUWJ2 小时前
论文略读:Large Language Models Assume People are More Rational than We Really are
人工智能·语言模型·自然语言处理
nancy_princess3 小时前
4. 时间序列预测的自回归和自动方法
人工智能·数据挖掘·回归
机器之心3 小时前
谢赛宁团队新基准让LLM集体自闭,DeepSeek R1、Gemini 2.5 Pro都是零分
人工智能·llm