{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "a435d3d6",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\29448\\AppData\\Local\\Temp\\ipykernel_31076\\1036803851.py:1: DeprecationWarning: `langchain-community` is being sunset and is no longer actively maintained. See https://github.com/langchain-ai/langchain-community/issues/674 for details and migration guidance toward standalone integration packages.\n",
      "  from langchain_community.document_loaders import PyMuPDFLoader\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "文档类型：<class 'list'>\n",
      "PDF 共354页\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.document_loaders import PyMuPDFLoader\n",
    "\n",
    "#创建加载器实例，传入 PDF 文件路径\n",
    "\n",
    "pdf_loader = PyMuPDFLoader(\"./car_info.pdf\")\n",
    "\n",
    "#调用 load（） 方法。 返回一个Document 列表 （每页一个 Document）\n",
    "pdf_pages = pdf_loader.load()\n",
    "\n",
    "#看看加载结果\n",
    "\n",
    "print(f\"文档类型：{type(pdf_pages)}\")\n",
    "print(f\"PDF 共{len(pdf_pages)}页\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "0e957443",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "元数据：{'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'creator': 'PTC Arbortext Publishing Engine', 'creationdate': '2023-06-16T15:35:59+08:00', 'source': './car_info.pdf', 'file_path': './car_info.pdf', 'total_pages': 354, 'format': 'PDF 1.7', 'title': '', 'author': '', 'subject': '', 'keywords': '', 'moddate': '2023-06-16T17:41:45+08:00', 'trapped': '', 'modDate': \"D:20230616174145+08'00'\", 'creationDate': \"D:20230616153559+08'00'\", 'page': 0}\n",
      "内容预览：欢迎\n",
      "感谢您选择了具有优良安全性、舒适性、动力性和经济性的Lynk & Co领克汽车。\n",
      "首次使用前请仔细、完整地阅读本手册内容，将有助于您更好地了解和使用车辆。\n",
      "本手册中的所有资料均为出版时的最新资料，但本公司将对产品进行不断的改进和优化，您所购的车辆可能与本手册中的描述有所不同，请以实际\n",
      "接收的车辆为准。\n",
      "如您有任何问题，或需要预约服务，请拨打电话4006-010101 联系我们。您也可以开车前\n"
     ]
    }
   ],
   "source": [
    "# 查看第一页的内容和元数据\n",
    "first_page = pdf_pages[0]\n",
    "print(f\"元数据：{first_page.metadata}\")\n",
    "print(f\"内容预览：{first_page.page_content[:200]}\") #截取200个字符"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "5d6fe44c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import re \n",
    "\n",
    "# ---- 问题1：PDF 解析产生的多余换行符 ----\n",
    "# 现象：一句话被拆成多行，中间插了 \\n\n",
    "# 例如：\"领克汽车\\n车顶行李架\\n最大载荷\"\n",
    "# 解决：用正则匹配并删除非中文字符之间的换行符\n",
    "raw_text = \"领克汽车\\n车顶行李架\\n最大载荷。\"\n",
    "pattern = re.compile(r'[^一](\\n)[^一]', re.DOTALL)\n",
    "clean_text = re.sub(pattern, lambda m: m.group(0).replace('\\n', ''), raw_text)\n",
    "\n",
    "# ---- 问题2：特殊符号干扰 ----\n",
    "# 现象：PDF 中的项目符号 • 、多余空格等\n",
    "clean_text = clean_text.replace('•', '')\n",
    "clean_text = clean_text.replace('  ', ' ')  # 合并多余空格\n",
    "# ---- 问题3：页眉页脚噪声 ----\n",
    "# 现象：每页都有 \"第X页\"、\"公司名称\" 等重复内容\n",
    "# 解决：根据元数据中的页码信息，过滤掉固定位置的噪声文本\n",
    "\n",
    "def clean_pdf_text(text: str) -> str:\n",
    "    \"\"\"清洗 PDF 解析出的文本，去除常见噪声\"\"\"\n",
    "    import re\n",
    "\n",
    "    # 删除非中文字符之间的换行符\n",
    "    text = re.sub(r'[^一](\\n)[^一]', lambda m: m.group(0).replace('\\n', ''), text)\n",
    "\n",
    "    # 删除项目符号和多余空格\n",
    "    text = text.replace('•', '').replace('  ', ' ')\n",
    "\n",
    "    # 删除连续的换行符（保留一个）\n",
    "    text = re.sub(r'\\n{2,}', '\\n', text)\n",
    "    return text.strip()\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "28f9e688",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "---块1---\n",
      "Langchain 是一个让你的 LLM 变得更强大的开源框架\n",
      "\n",
      "---块2---\n",
      "你想开发一个基于 LLM 的应用，需要什么组件他都有，直接使用就行\n",
      "\n",
      "---块3---\n",
      "甚至针对常规的应用流程，它利用 Chain 这个概念已经内置标准化方案了\n",
      "\n",
      "---块4---\n",
      "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# 手动实现固定字符分块\n",
    "from langchain_text_splitters import CharacterTextSplitter\n",
    "\n",
    "# 1. 定义分块器\n",
    "text_splitter = CharacterTextSplitter(\n",
    "    separator=\"。\",   #指定分隔符，默认为 “\\n\\n”\n",
    "    chunk_size=50,     # 每个块的最大字符数\n",
    "    chunk_overlap=10,    # 每个块之间的重叠字符数\n",
    "    length_function=len,  # 计算字符数的函数\n",
    "    is_separator_regex=False # 是否将分隔符视为正则表达式\n",
    ")\n",
    "\n",
    "# 2. 执行分块，返回字符串列表\n",
    "text = \"\"\"\n",
    "Langchain 是一个让你的 LLM 变得更强大的开源框架。\n",
    "你想开发一个基于 LLM 的应用，需要什么组件他都有，直接使用就行。\n",
    "甚至针对常规的应用流程，它利用 Chain 这个概念已经内置标准化方案了。\n",
    "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
    "\"\"\"\n",
    "chunks = text_splitter.split_text(text)\n",
    "for i, chunk in enumerate(chunks):\n",
    "    print(f\"---块{i+1}---\\n{chunk}\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "863080a2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--- 块1 ---\n",
      "LangChain 是一个让你的 LLM 变得更强大的开源框架。\n",
      "\n",
      "--- 块2 ---\n",
      "你想开发一个基于 LLM 的应用，需要什么组件它都有，直接使用就行。\n",
      "\n",
      "--- 块3 ---\n",
      "甚至针对常规的应用流程，它利用 Chain 这个概念已经内置标准化方案了。\n",
      "\n",
      "--- 块4 ---\n",
      "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
      "\n"
     ]
    }
   ],
   "source": [
    "''' \n",
    "* RecursiveCharacterTextSplitter 递归字符文本分割\n",
    "RecursiveCharacterTextSplitter 将按不同的字符递归地分割(按照这个优先级[\"\\n\\n\", \"\\n\", \" \", \"\"])，\n",
    "    这样就能尽量把所有和语义相关的内容尽可能长时间地保留在同一位置\n",
    "RecursiveCharacterTextSplitter需要关注的是4个参数：\n",
    "\n",
    "* separators - 分隔符字符串数组\n",
    "* chunk_size - 每个文档的字符数量限制\n",
    "* chunk_overlap - 两份文档重叠区域的长度\n",
    "* length_function - 长度计算函数\n",
    "'''\n",
    "from langchain_text_splitters import RecursiveCharacterTextSplitter\n",
    "\n",
    "# 创建递归字符分割器\n",
    "text_splitter = RecursiveCharacterTextSplitter(\n",
    "    # 分隔符优先级：段落 → 换行 → 句号 → 空格 → 硬切\n",
    "    separators=[\"\\n\\n\", \"\\n\", \"。\", \"！\", \"？\", \" \", \"\"],\n",
    "\n",
    "    # 每个块最大 50 字符\n",
    "    chunk_size = 50,\n",
    "    # 相邻块重叠 10 字符 （chunk_size 的 20%）\n",
    "    chunk_overlap = 10,\n",
    "    # 长度计算函数\n",
    "    length_function=len\n",
    ")\n",
    "\n",
    "# 对单段文本进行分割\n",
    "text = \"\"\"\n",
    "LangChain 是一个让你的 LLM 变得更强大的开源框架。\n",
    "你想开发一个基于 LLM 的应用，需要什么组件它都有，直接使用就行。\n",
    "甚至针对常规的应用流程，它利用 Chain 这个概念已经内置标准化方案了。\n",
    "下面我们从新兴的大语言模型技术栈的角度来看看为何它的理念这么受欢迎。\n",
    "\"\"\"\n",
    "chunks = text_splitter.split_text(text)\n",
    "for i, chunk in enumerate(chunks):\n",
    "    print(f\"--- 块{i+1} ---\\n{chunk}\\n\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "174c088a",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "切分后的文件数量：4243\n",
      "切分后的字符数（可以用来大致评估 token 数）：148617\n"
     ]
    }
   ],
   "source": [
    "# 对整个文件进行切割\n",
    "split_docs = text_splitter.split_documents(pdf_pages)\n",
    "print(f\"切分后的文件数量：{len(split_docs)}\")\n",
    "print(f\"切分后的字符数（可以用来大致评估 token 数）：{sum([len(doc.page_content) for doc in split_docs])}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "119ef139",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "有效块数量：4243\n",
      "总字符数（可大致评估 Token 数）：148617\n"
     ]
    }
   ],
   "source": [
    "# 过滤掉 page_content 为空或仅含空白的文档\n",
    "valid_docs = [\n",
    "    doc for doc in split_docs \n",
    "    if doc.page_content and doc.page_content.strip()\n",
    "]\n",
    "\n",
    "print(f\"有效块数量：{len(valid_docs)}\")\n",
    "print(f\"总字符数（可大致评估 Token 数）：{sum(len(d.page_content) for d in valid_docs)}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "579fe5e6",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "向量维度：1024\n",
      "前5个值：[-0.013686168007552624, 0.015642698854207993, -0.052721332758665085, 0.033709585666656494, -0.08253694325685501]\n"
     ]
    }
   ],
   "source": [
    "# 安装 dashscope 依赖：uv pip install dashscope -i https://mirrors.aliyun.com/pypi/simple\n",
    "\n",
    "from langchain_community.embeddings import DashScopeEmbeddings\n",
    "from dotenv import load_dotenv\n",
    "import os\n",
    "\n",
    "load_dotenv()\n",
    "api_key = os.getenv('QWEN_API_KEY')\n",
    "\n",
    "# 初始化 Embedding 模型\n",
    "# 需要先在 https://dashscope.console.aliyun.com/ 获取 API Key\n",
    "embedding_model = DashScopeEmbeddings(\n",
    "    model=\"text-embedding-v3\",        # 模型名称\n",
    "    dashscope_api_key=api_key   # 替换为你的真实 Key\n",
    ")\n",
    "\n",
    "# 单条文本向量化\n",
    "text = \"RAG系统搭建实战\"\n",
    "embedding = embedding_model.embed_query(text)\n",
    "print(f\"向量维度：{len(embedding)}\")\n",
    "print(f\"前5个值：{embedding[:5]}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "3951a993",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "生成了 4 个向量\n",
      "每个向量维度：1024\n"
     ]
    }
   ],
   "source": [
    "# 批量文本向量化\n",
    "texts = [\n",
    "    \"hello world\",\n",
    "    \"什么是大语言模型\",\n",
    "    \"RAG的概念和原理\",\n",
    "    \"牛顿第一定律是什么\",\n",
    "]\n",
    "\n",
    "embeddings = embedding_model.embed_documents(texts)\n",
    "print(f\"生成了 {len(embeddings)} 个向量\")\n",
    "print(f\"每个向量维度：{len(embeddings[0])}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "5bc26184",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "匹配内容：王老师教学认真负责，是公认的好老师\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.vectorstores import Chroma\n",
    "\n",
    "# 准备一些测试数据\n",
    "datas = [\n",
    "    \"小明特别喜欢吃脆甜多汁的苹果\",\n",
    "    \"小红对榴莲那独特的味道情有独钟\",\n",
    "    \"小明和小丽是一对甜蜜的情侣\",\n",
    "    \"王老师教学认真负责，是公认的好老师\",\n",
    "    \"小李每天都要吃一根香蕉\",\n",
    "    \"小王的男朋友长得阳光帅气，是大家公认的大帅哥\"\n",
    "]\n",
    "\n",
    "# 创建向量数据库并持久化(会同时把文本和对应的向量存入数据库)\n",
    "db = Chroma.from_texts(\n",
    "    texts=datas,\n",
    "    embedding=embedding_model,\n",
    "    collection_metadata={\"hnsw:space\": \"cosine\"},  # 关键配置：指定距离算法为余弦相似度\n",
    "    persist_directory=\"./chroma_db1\"  # 数据库存储路径\n",
    ")\n",
    "\n",
    "# 查询相似文本\n",
    "# 相似度检索\n",
    "query = \"谁是老师\"\n",
    "results = db.similarity_search(query, k=1)  # 返回最相似的 1 条\n",
    "\n",
    "for doc in results:\n",
    "    print(f\"匹配内容：{doc.page_content}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "7e905aae",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "内容：王老师教学认真负责，是公认的好老师 | 相似度分数：0.3909\n",
      "内容：王老师教学认真负责，是公认的好老师 | 相似度分数：0.3909\n",
      "内容：王老师教学认真负责，是公认的好老师 | 相似度分数：0.3909\n"
     ]
    }
   ],
   "source": [
    "# 带分数的相似度检索（分数越低越相似，0 表示完全匹配）\n",
    "query = \"谁是老师\"\n",
    "results = db.similarity_search_with_score(query, k=3)\n",
    "\n",
    "for doc, score in results:\n",
    "    print(f\"内容：{doc.page_content} | 相似度分数：{score:.4f}\")\n",
    "    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "7e7240cc",
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "C:\\Users\\29448\\AppData\\Local\\Temp\\ipykernel_31076\\632587025.py:12: LangChainDeprecationWarning: The class `Chroma` was deprecated in LangChain 0.2.9 and will be removed in 1.0. An updated version of the class exists in the `langchain-chroma package and should be used instead. To use it run `pip install -U `langchain-chroma` and import as `from `langchain_chroma import Chroma``.\n",
      "  vectordb = Chroma(\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.vectorstores import Chroma\n",
    "\n",
    "# 从文档创建向量库(写数据)\n",
    "# vectorstore = Chroma.from_documents(\n",
    "#     documents=valid_docs,\n",
    "#     embedding=embedding_model,\n",
    "#     collection_metadata={\"hnsw:space\": \"cosine\"},\n",
    "#     persist_directory=\"./my_knowledge_db\"\n",
    "# )\n",
    "\n",
    "# 直接加载已持久化的数据库（无需再次添加文档或持久化）\n",
    "vectordb = Chroma(\n",
    "    persist_directory=\"./my_knowledge_db\",\n",
    "    embedding_function=embedding_model\n",
    ")\n",
    "\n",
    "# 创建检索器，设置返回 Top-3 最相关文档\n",
    "retriever = vectordb.as_retriever(search_kwargs={\"k\": 3})"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "b3f89995",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "--- 结果1 ---\n",
      "内容：说明！\n",
      "□当车速在0 – 40km/h的范围内，且车辆在陡坡上低速下坡行驶\n",
      "时，HDC才会激活。...\n",
      "来源：{'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'keywords': '', 'page': 160, 'format': 'PDF 1.7', 'total_pages': 354, 'creationdate': '2023-06-16T15:35:59+08:00', 'author': '', 'subject': '', 'trapped': '', 'source': './car_info.pdf', 'title': '', 'modDate': \"D:20230616174145+08'00'\", 'file_path': './car_info.pdf', 'creator': 'PTC Arbortext Publishing Engine', 'moddate': '2023-06-16T17:41:45+08:00', 'creationDate': \"D:20230616153559+08'00'\"}\n",
      "\n",
      "--- 结果2 ---\n",
      "内容：说明！\n",
      "□当车速在0 – 40km/h的范围内，且车辆在陡坡上低速下坡行驶\n",
      "时，HDC才会激活。...\n",
      "来源：{'keywords': '', 'trapped': '', 'file_path': './car_info.pdf', 'page': 160, 'total_pages': 354, 'creationDate': \"D:20230616153559+08'00'\", 'modDate': \"D:20230616174145+08'00'\", 'moddate': '2023-06-16T17:41:45+08:00', 'subject': '', 'source': './car_info.pdf', 'creationdate': '2023-06-16T15:35:59+08:00', 'format': 'PDF 1.7', 'creator': 'PTC Arbortext Publishing Engine', 'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'title': '', 'author': ''}\n",
      "\n",
      "--- 结果3 ---\n",
      "内容：□当车速在40-60 km/h范围内无法激活HDC功能，车速超过60 km/...\n",
      "来源：{'total_pages': 354, 'producer': 'PDFlib+PDI 9.0.6 (C++/Win64)', 'source': './car_info.pdf', 'page': 160, 'creationdate': '2023-06-16T15:35:59+08:00', 'creator': 'PTC Arbortext Publishing Engine', 'author': '', 'subject': '', 'file_path': './car_info.pdf', 'keywords': '', 'trapped': '', 'moddate': '2023-06-16T17:41:45+08:00', 'creationDate': \"D:20230616153559+08'00'\", 'title': '', 'modDate': \"D:20230616174145+08'00'\", 'format': 'PDF 1.7'}\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# 测试检索\n",
    "query = \"在多少速度范围内，HDC才会激活？\"\n",
    "relevant_docs = retriever.invoke(query)\n",
    "\n",
    "for i, doc in enumerate(relevant_docs):\n",
    "    print(f\"--- 结果{i+1} ---\")\n",
    "    print(f\"内容：{doc.page_content[:100]}...\")\n",
    "    print(f\"来源：{doc.metadata}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "ce141dc1",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_core.prompts import ChatPromptTemplate\n",
    "\n",
    "# 构建 Prompt 模板\n",
    "prompt = ChatPromptTemplate.from_template(\"\"\"\n",
    "你是一个专业的知识库助手。请根据以下检索到的上下文回答用户问题。\n",
    "\n",
    "**规则：**\n",
    "- 只基于提供的上下文回答，不要编造\n",
    "- 如果上下文中没有相关信息，直接说「根据现有资料，我找不到这个问题的答案」\n",
    "- 回答要简洁直接，引用原文时用引号\n",
    "\n",
    "**检索到的上下文：**\n",
    "{context}\n",
    "\n",
    "**用户问题：**\n",
    "{question}\n",
    "\"\"\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "745939b1",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "在0 – 40km/h的范围内，HDC才会激活。\n"
     ]
    }
   ],
   "source": [
    "from langchain_community.chat_models import ChatTongyi\n",
    "from langchain_core.output_parsers import StrOutputParser\n",
    "\n",
    "# 初始化大模型（这里用通义千问，也可以换成 DeepSeek）\n",
    "llm = ChatTongyi(\n",
    "    model=\"qwen-plus\",                 # 模型名称\n",
    "    dashscope_api_key=api_key   # 替换为你的真实 Key\n",
    ")\n",
    "\n",
    "# 拼装上下文\n",
    "context_text = \"\\n\\n---\\n\\n\".join([doc.page_content for doc in relevant_docs])\n",
    "\n",
    "# 构建 Chain 并调用\n",
    "chain = prompt | llm | StrOutputParser()\n",
    "\n",
    "response = chain.invoke({\n",
    "    \"context\": context_text,\n",
    "    \"question\": query\n",
    "})\n",
    "\n",
    "print(response)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "96862cbb",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "开始生成评估数据...\n",
      "处理第1/4个问题：车顶行李架最大载荷是多少？\n",
      "处理第2/4个问题：自适应巡航系统的工作速度范围是多少？\n",
      "处理第3/4个问题：车辆推荐的轮胎胎压是多少（空载前轮）？\n",
      "处理第4/4个问题：保养周期是多少公里或多长时间？\n",
      "\n",
      "开始评估...\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Evaluating: 100%|██████████| 16/16 [03:49<00:00, 14.37s/it]"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "评估结果： {'faithfulness': 0.8333, 'answer_relevancy': 0.5317, 'context_precision': 0.7500, 'context_recall': 0.7500}\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "\n"
     ]
    }
   ],
   "source": [
    "# 使用 ragas 0.4.x 的 legacy metrics（兼容 LangChain LLM）\n",
    "# 注意：ragas 0.4.x 的 collections metrics 需要 InstructorBaseRagasLLM，\n",
    "# 而 legacy metrics 仍支持 LangChain LLM，通过 evaluate() 自动注入\n",
    "from ragas.metrics._faithfulness import faithfulness\n",
    "from ragas.metrics._answer_relevance import answer_relevancy\n",
    "from ragas.metrics._context_precision import context_precision\n",
    "from ragas.metrics._context_recall import context_recall\n",
    "\n",
    "from ragas import evaluate, RunConfig\n",
    "from datasets import Dataset\n",
    "from langchain_community.embeddings import DashScopeEmbeddings\n",
    "from langchain_community.chat_models import ChatTongyi\n",
    "\n",
    "# 初始化模型\n",
    "embedding_model = DashScopeEmbeddings(\n",
    "    model=\"text-embedding-v3\",\n",
    "    dashscope_api_key=api_key\n",
    ")\n",
    "llm = ChatTongyi(model=\"qwen-plus\", api_key=api_key)\n",
    "\n",
    "# ---- 准备测试数据 ----\n",
    "test_questions = [\n",
    "    {\n",
    "        \"question\": \"车顶行李架最大载荷是多少？\",\n",
    "        \"ground_truth\": \"车顶行李架最大载荷为70kg\"\n",
    "    },\n",
    "    {\n",
    "        \"question\": \"自适应巡航系统的工作速度范围是多少？\",\n",
    "        \"ground_truth\": \"自适应巡航系统（ACC）可以在0-150km/h范围内工作\"\n",
    "    },\n",
    "    {\n",
    "        \"question\": \"车辆推荐的轮胎胎压是多少（空载前轮）？\",\n",
    "        \"ground_truth\": \"空载时前轮推荐胎压为230 kPa\"\n",
    "    },\n",
    "    {\n",
    "        \"question\": \"保养周期是多少公里或多长时间？\",\n",
    "        \"ground_truth\": \"保养周期为15,000公里或12个月（以先到者为准）\"\n",
    "    }\n",
    "]\n",
    "\n",
    "# ---- 构建评估数据集 ----\n",
    "evaluation_data = {\n",
    "    \"question\": [],\n",
    "    \"answer\": [],\n",
    "    \"contexts\": [],\n",
    "    \"ground_truth\": []\n",
    "}\n",
    "print(\"开始生成评估数据...\")\n",
    "for i, item in enumerate(test_questions, 1):\n",
    "    question = item[\"question\"]\n",
    "    ground_truth = item[\"ground_truth\"]\n",
    "\n",
    "    print(f\"处理第{i}/{len(test_questions)}个问题：{question}\")\n",
    "\n",
    "    # 1.检索相关文档\n",
    "    retriever = vectordb.as_retriever(search_kwargs={\"k\": 3})\n",
    "    relevant_docs = retriever.invoke(question)\n",
    "    contexts = [doc.page_content for doc in relevant_docs]\n",
    "\n",
    "    # 2.用大模型生成答案\n",
    "    context_text = \"\\n\\n\".join(contexts)\n",
    "    prompt = f\"\"\"基于以下上下文回答问题。如果没有相关信息，请说\"无法从提供的信息中回答\"。\n",
    "\n",
    "    上下文：{context_text}\n",
    "\n",
    "    问题：{question}\n",
    "    答案：\"\"\"\n",
    "    answer = llm.invoke(prompt).content\n",
    "\n",
    "    # 3. 保存评估数据\n",
    "    evaluation_data[\"question\"].append(question)\n",
    "    evaluation_data[\"answer\"].append(answer)\n",
    "    evaluation_data[\"contexts\"].append(contexts)\n",
    "    evaluation_data[\"ground_truth\"].append(ground_truth)\n",
    "\n",
    "# ---- 执行评估 ----\n",
    "dataset = Dataset.from_dict(evaluation_data)\n",
    "\n",
    "# 配置评估运行参数\n",
    "run_config = RunConfig(\n",
    "    timeout=600,       # 单次操作超时时间（秒）\n",
    "    max_workers=1,     # 并发数，本地模型建议设为1\n",
    "    max_retries=10,    # 最大重试次数\n",
    "    max_wait=120,      # 重试间隔上限（秒）\n",
    ")\n",
    "\n",
    "# 定义要使用的评估指标（legacy metrics，已实例化的单例对象）\n",
    "metrics = [\n",
    "    faithfulness,        # 忠实度：答案是否基于上下文\n",
    "    answer_relevancy,    # 答案相关性：答案是否与问题相关\n",
    "    context_precision,   # 上下文精确度：检索到的上下文是否相关\n",
    "    context_recall       # 上下文召回率：是否检索到所有必要信息\n",
    "]\n",
    "\n",
    "print(\"\\n开始评估...\")\n",
    "result = evaluate(\n",
    "    dataset,\n",
    "    metrics=metrics,\n",
    "    embeddings=embedding_model,\n",
    "    llm=llm,\n",
    "    run_config=run_config\n",
    ")\n",
    "\n",
    "# ---- 输出评估结果 ----\n",
    "print(\"评估结果：\", result)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3.11 (shuheAI)",
   "language": "python",
   "name": "shuheai"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
