第4章 Retrieval

一、RAG介绍

1.1 大模型的局限

1)知识滞后

LLM 因其具有海量参数,需要花费相当的物力与时间成本进行预训练和微调,同时商用 LLM 还需要进行各种安全测试与风险评估等。因此 LLM 会存在知识滞后的问题。

2)知识缺失

在专有领域,LLM 无法学习到所有的专业知识细节,因此在面向专业领域知识的提问时,无法给出可靠准确的回答。

3)幻觉

LLM 在生成回答时,可能会“胡言乱语”,这种现象称之为 LLM 的“幻觉”。“幻觉”可以体现为错误陈述、编造事实、错误的复杂推理或者复杂语境下理解能力不足等。

“幻觉”产生的原因:训练知识存在偏差,这些错误信息被 LLM 学习后在输出中复现

  • LLM 训练时过度泛化,将普通的模式应用在特定场合导致不准确输出;
  • LLM 本身没有真正学习到训练数据中深层次的含义,导致在一些需要深入理解或复杂推理的任务中出错;
  • LLM 缺乏某些领域的相关知识,在面临这些领域的相关问题时编造不存在的信息;
  • 大模型生成内容的不可控,尤其是在金融和医疗领域等领域,一次金额评估的错误,一次医疗诊断的失误,哪怕只出现一次都是致命的。但这些错误对于非专业人士来说难以辨识。目前还没有能够百分之百解决这种情况的方案;

1.2 什么是RAG

为了改善大模型在时效性、可靠性与准确性方面的不足,各种针对 LLM 优化的方法应运而生。RAG(Retrieval-Augmented Generation,检索增强生成)就是其中一种被广泛研究和应用的优化架构。

RAG 的基本思想为:将传统的生成式大模型和实时信息检索技术相结合,为大模型补充来自外部的相关数据和上下文,来帮助大模型生成更加准确可靠的内容。这使得大模型在生成内容时可以依赖实时与个性化的数据和知识,而非仅仅依赖训练知识。就相当于在大模型回答时给它一本参考书。

 

可以说,当应用需求集中在利用大模型去回答特定私有领域的知识,且知识库足够大时,那么除了微调大模型外,RAG 就是非常有效的一种解决方案。LangChain 对这一流程提供了解决方案。

1.3 RAG优缺点

1)RAG的优点

  • 相比提示词工程,RAG 有更丰富的上下文和数据样本,可以不需要用户提供过多的背景描述,就能生成比较符合用户预期的答案。
  • 相比于模型微调,RAG 可以提升问答内容的时效性和可靠性。
  • 在一定程度上保护了业务数据的隐私性。

2)RAG的缺点

  • 由于每次问答都涉及外部系统数据检索,因此 RAG 的响应时延相对较高。
  • 引用的外部知识数据会消耗大量的模型 Token 资源。

1.4 RAG流程

典型的RAG有两个主要流程:

  • 索引:从数据源提取数据,构建索引。
  • 检索生成:接受用户查询并从索引中检索相关数据,然后将其传递给模型。。

索引阶段:

  • 从各种数据源加载数据➡️
  • 将文档切分为小块➡️
  • 对文本块进行嵌入➡️
  • 存储嵌入向量。

检索生成阶段:

  • 根据用户输入,使用检索器从存储中检索相关文本块➡️
  • 大模型使用包含问题和检索结果的提示生成回答。

二、文档加载

数据源可能包含多种格式的文件,如文本文档、Markdown,PDF 等。因此我们首先需要对各种格式的文件进行处理。LangChain 实现和集成了众多文档加载器,方便从不同格式的文件中加载数据。可在 https://docs.langchain.com/oss/python/integrations/document_loaders 查看所有集成的文档加载器。

LangChain 所有文档加载器都实现了 BaseLoader 接口,接口提供了通用的 load(一次加载所有文档) 与 lazy_load(以延迟方式加载文档) 方法,用于从数据源加载数据并处理为 Document 对象。

LangChain 实现了 Document 抽象,用于表示文本单元及其元数据,它包含三个属性:

  • page_content:文本内容字符串。
  • metadata:包含元数据的字典,如文档的来源等。
  • id:可选,文档标识符。

2.1 加载 TXT

提前准备txt文本文档,代码操作:

# pip install langchain_community
from langchain_community.document_loaders import TextLoader

loader = TextLoader(
    "../assets/agent_prompts.txt",  # 指定读取文件路径
    encoding="utf-8"  # 指定编码格式
).load() # 返回List[Document]

print(loader) # [Document(metadata={'source': '../assets/agent_prompts.txt'}, page_content='

2.2 加载 CSV

将其复制保存为 .csv 文件(例如 test_chinese.csv),建议使用 UTF-8 编码以确保中文正常显示:

姓名,年龄,城市,职业,备注
张伟,28,北京,软件工程师,"喜欢编程和开源项目"
李娜,34,上海,产品经理,"擅长用户调研与需求分析"
王强,25,广州,数据分析师,"熟练使用Python和SQL"
赵敏,42,深圳,高级架构师,"10年以上后端开发经验"
刘洋,31,成都,UI设计师,"注重用户体验与视觉美感"
陈静,29,杭州,前端开发,"熟悉Vue和React框架"
黄磊,37,武汉,运维工程师,"精通Linux和自动化部署"
周婷,26,西安,测试工程师,"擅长自动化测试与质量保障"
  • 加载csv文件所有列
from langchain_community.document_loaders import CSVLoader

# 加载所有列
docs = CSVLoader(
    file_path="../assets/test_chinese.csv",  # csv文件路径
    encoding="utf-8"  # 编码
).load()  # 返回List[Document]

print(docs)

image

  • 加载csv文件部分列
from langchain_community.document_loaders import CSVLoader

# 加载所有列
docs = CSVLoader(
    file_path="../assets/test_chinese.csv",  # csv文件路径
    encoding="utf-8",  # 编码
    metadata_columns=["姓名", "职业"],  # 指定元数据列
    content_columns=["城市","备注"]  # 将指定列作为:内容列
).load()  # 返回List[Document]

print(docs)

image

✅ metadata_columns 和 content_columns 的区别与联系

📌 共同点

  • 两者都是可选参数(Sequence[str] 类型,如列表或元组)。
  • 都用于控制 CSV 中哪些列参与生成 Document 的 内容(page_content) 或 元数据(metadata)。
  • 它们共同决定了每一行如何被转换为一个 langchain_core.documents.Document 对象。

🔍 核心区别

特性metadata_columnscontent_columns
作用 指定哪些列仅作为元数据(放入 Document.metadata 指定哪些列用于构建文档内容(Document.page_content
是否出现在 page_content 中 ❌ 不出现(即使没设 content_columns,它们也会被排除) ✅ 出现(拼接后作为主要内容)
默认行为 默认为空 → 所有列都可能进入内容 默认为空 → 若未设置,则使用“非 metadata 列”作为内容
优先级 高:被列为 metadata 的列永远不会进入内容 中:若设置了,则只用这些列生成内容(但仍会排除 metadata 列)

🧠 工作逻辑(按源码和文档说明)

当加载一行 CSV 数据时,CSVLoader 按以下规则处理:

    1. 先确定元数据列:
      所有在 metadata_columns 中的列 → 放入 metadata 字典。

    2. 再确定内容列:

      • 如果 显式指定了 content_columns
        则从 这些列中排除掉 metadata_columns 的部分,剩下的用于生成 page_content
      • 如果 未指定 content_columns
        则使用 所有不在 metadata_columns 中的列 作为内容列。
    3. 生成 page_content
      将选中的内容列按 "key: value\n" 格式拼接成字符串(例如:"备注: 喜欢编程和开源项目\n城市: 北京")。

2.3 加载 JSON

LangChain 实现了 JSONLoader,用来将 JSON 和 JSONL 数据转换为 LangChain 文档对象。它使用指定的 jq 模式来解析 JSON 文件,从而将特定字段提取到 LangChain 文档的内容和元数据中。

jq 是一个轻量级、命令行下的 JSON 处理器,支持强大的查询、过滤、映射等操作。LangChain 的 JSONLoader 内部集成了 jq 引擎(通过 Python 的 jq 库),让你可以用简洁的 jq 表达式来“挖出”想要的字段。

✅ 不需要安装命令行 jq,LangChain 会用内置的解析器处理这些表达式。

2.3.1. 基本 jq 语法速览

表达式含义
. 当前输入(整个 JSON)
.field 获取对象中 field 字段的值
.[] 遍历数组或对象的所有元素(生成多个输出)
.field[] 先取 field,再遍历其内容(如果它是数组)
.key[].text 取 key 字段(应为数组),对每个元素取 .text

⚠️ 关键点:jq 表达式可以产生多个结果,JSONLoader 会把每个结果变成一个 Document

2.3.2 📚 逐个解析你的例子

✅ 示例 1:
json
// JSON 数据
[{"text": "A"}, {"text": "B"}, {"text": "C"}]
python
jq_schema = ".[].text"

解析过程:

  1. . → 整个数组 [{"text":"A"}, ...]
  2. .[] → 展开数组,得到三个对象:{"text":"A"}{"text":"B"}{"text":"C"}
  3. .text → 对每个对象取 .text 字段
  4. 最终输出:"A""B""C"(三个字符串)

JSONLoader 会创建 3 个 Document,每个的 page_content 分别是 "A""B""C"


✅ 示例 2:
json
// JSON 数据
{"key": [{"text": "X"}, {"text": "Y"}]}
python
jq_schema = ".key[].text"

解析过程:

  1. . → 整个对象 {"key": [...]}
  2. .key → 取出数组 [{"text":"X"}, {"text":"Y"}]
  3. .[] → 展开数组,得到两个对象
  4. .text → 分别取 .text
  5. 输出:"X""Y"

✅ 生成 2 个 Document。


✅ 示例 3:
json
// JSON 数据
["Hello", "World", "LangChain"]
python
jq_schema = ".[]"

解析过程:

  1. . → 整个数组
  2. .[] → 展开,得到三个字符串
  3. 输出:"Hello""World""LangChain"

✅ 生成 3 个 Document,内容就是这些字符串。

如果要从 JSON Lines 文件加载文档,需传递 json_lines=True。

常见 jq schema 参考:

JSON        -> [{"text": ...}, {"text": ...}, {"text": ...}]
jq_schema   -> ".[].text"  

JSON        -> {"key": [{"text": ...}, {"text": ...}, {"text": ...}]}
jq_schema   -> ".key[].text"

JSON        -> ["...", "...", "..."]
jq_schema   -> ".[]"

详细用法可参考 https://jqlang.org/manual/#basic-filters

2.3.3 案例

测试用 JSON(保存为 test_data.json
{
  "category": "科技新闻",
  "source": "内部知识库",
  "articles": [
    {
      "id": 101,
      "title": "大模型推理速度提升新方法",
      "content": "研究人员提出了一种新的量化技术,可将推理速度提升3倍。",
      "author": "王工",
      "tags": ["AI", "大模型", "优化"],
      "publish_date": "2025-04-10"
    },
    {
      "id": 102,
      "title": "LangChain 支持多模态输入",
      "content": "最新版本的 LangChain 已支持图像与文本联合处理。",
      "author": "李博士",
      "tags": ["LangChain", "多模态", "RAG"],
      "publish_date": "2025-05-22"
    },
    {
      "id": 103,
      "title": "中文 RAG 系统评测基准发布",
      "content": "首个面向中文的 RAG 评测数据集 C-RAGBench 正式开源。",
      "author": "张研究员",
      "tags": ["RAG", "中文", "评测"],
      "publish_date": "2025-06-15"
    }
  ]
}
  • 举例:提取所有字段
# pip install langchain_community jq
import json

from langchain_community.document_loaders import JSONLoader

# 提取所有字段
docs = JSONLoader(
    file_path="../assets/test_data.json",  # json文件路径
    jq_schema=".",  # 提取所有字段
    text_content=False  # 提取内容是否为字符串格式
).load()

print(docs)

# 解析 page_content 为 Python 对象
raw_json_str = docs[0].page_content
data = json.loads(raw_json_str)

# 现在可以正常访问中文字段
print(data["category"])  # 输出:科技新闻
print(data["articles"][0]["title"])  # 输出:大模型推理速度提升新方法

image

  • 举例:提取指定字段中的内容
# pip install langchain_community jq
from langchain_community.document_loaders import JSONLoader

# 提取所有字段
docs = JSONLoader(
    file_path="../assets/test_data.json",  # json文件路径
    jq_schema=".articles[].content",  # 提取.articles[].content的内容
    text_content=False  # 提取内容是否为字符串格式
).load()

print(docs)

image

2.4 加载 HTML 网页

WebBaseLoader(以及它底层的 requests + BeautifulSoup)能不能解析网页内容?

✅ 能 —— 但 仅限于“静态网页”。
❌ 不能 —— 对于 JavaScript 动态渲染的内容(如 Vue、React、Angular 等 SPA 单页应用)。

类型特点示例能否被 WebBaseLoader 解析?
静态网页 HTML 内容直接写在服务器返回的源码中 新闻站、博客、传统网站 ✅ 可以
动态网页(SPA) 初始 HTML 是空壳,内容由浏览器执行 JS 后生成 https://pfsc.agri.cn/#/priceMarket、百度百科(部分)、Vue/React 应用 ❌ 无法解析

案例代码:

# pip install langchain_community beautifulsoup4
import os

os.environ[
    "USER_AGENT"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36"

import bs4
from langchain_community.document_loaders import WebBaseLoader

docs = WebBaseLoader(
    # 网址序列
    web_paths=("https://www.baidu.com",),
    # 传给 BeautifulSoup 的解析参数,parse_only 表示只提取指定标签的元素
    # bs_kwargs={"parse_only": bs4.SoupStrainer("p", class_="title")}
).load()

print(docs)

2.5 加载Markdown

可以使用 Unstructured 文档加载器来加载多种类型的文件,关于如何在 LangChain 中使用 unstructured 生态系统,可参考这里

可使用 UnstructuredMarkdownLoader 加载 Markdown 文件,需要 unstructured 包。

from langchain_community.document_loaders import UnstructuredMarkdownLoader

docs = UnstructuredMarkdownLoader(
    file_path="../assets/agent_prompts.md",  # markdown文件路径
    mode="elements"  # 加载模式: 1."single"表示返回单个Document对象,2."elements"表示按元素切分文档
).load()

print(docs)

2.6 加载 Doc/Docx

2.6.1. Docx2txtLoader

🔧 底层依赖
  • 使用 docx2txt 库
  • 仅支持 .docx(不支持旧版 .doc
📌 特点
项目说明
功能 纯文本提取(忽略格式、表格结构、标题层级等)
输出 单个 Document 对象,page_content 是整个文档的纯文本(换行符保留)
速度 ⚡ 极快(轻量级库)
安装 pip install docx2txt
适用场景 只关心文字内容,不在乎结构;快速批量处理 .docx
⚠️ 缺点
  • 无法区分 标题、段落、列表、表格;
  • 表格会被转成“乱序文本”,难以还原;
  • 不支持 .doc(二进制格式)。

案例代码:

# pip install docx2txt
from langchain_community.document_loaders import Docx2txtLoader

docs = Docx2txtLoader(
    file_path="../assets/agent.docx"  # docx文件路径
).load()

print(docs)

image

2.6.2 UnstructuredWordDocumentLoader

🔧 底层依赖
📌 特点
项目说明
功能 智能解析文档结构(可识别 Title、NarrativeText、Table、ListItem 等)
模式
  • mode="single":返回一个 Document(类似 Docx2txtLoader)
  • mode="elements":返回多个 Document,每个代表一个语义元素(如标题、段落、表格)
策略 支持 strategy="fast"(快速)或 strategy="hi_res"(高精度,需额外依赖)
表格处理 能较好保留表格结构(尤其在 hi_res 模式下)
安装 pip install "unstructured[local-inference]"(若要用 hi_res)
✅ 优势
  • 结构感知:知道哪里是标题、哪里是正文;
  • 元数据丰富:每个 element 可带 category(如 "Title")、page_number 等;
  • 支持旧版 .doc
  • 可与后续的 分块(text splitting) 或 RAG 增强更好结合。

案例:

# pip install langchain_community unstructured[docx]
from langchain_community.document_loaders import UnstructuredWordDocumentLoader

docs = UnstructuredWordDocumentLoader(
    file_path="../assets/agent.docx",  # docx文件路径
    # 加载模式:
    #   single 返回单个Document对象
    #   elements 按标题等元素切分文档
    mode="single"
).load()

print(docs)

2.7 加载 PDF

PDF 存在多种来源格式,包括扫描版(图片 PDF)、电子文本版、混合版。并且布局格式也多种多样,包括单列布局、双列布局甚至竖排文本布局。并且包含段落、标题、页眉页脚、表格、数学公式、化学式、特殊符号、图片等各种元素。

因此,PDF 解析存在很多挑战。对于复杂 PDF,需要进行文本提取、布局检测、表格解析、公式识别等处理。

2.7.1 PyMuPDFLoader(也叫 FitzLoader

注意:在 LangChain 中,它通常叫 PyMuPDFLoader,但底层是 PyMuPDF(又名 fitz)。

🔧 底层依赖
  • 基于 PyMuPDF(C++ 引擎,性能强)
  • 需要编译二进制依赖(但 PyPI 提供预编译 wheel)
📌 特点
项目说明
安装 pip install pymupdf
输出 默认每页一个 Document(可通过 split_pages=False 合并)
元数据 sourcepage, 还可包含 total_pages
速度 ⚡ 非常快(C++ 引擎)
文本提取能力 强,能较好处理多栏、表格、复杂布局
图像提取 ✅ 支持(可额外提取图片)
OCR 能力 ❌ 本身不支持 OCR(但可配合 Tesseract 处理扫描件)
加密 PDF ✅ 支持
✅ 优点
  • 文本提取准确率高;
  • 速度快,内存效率高;
  • 能保留更多原始布局信息;
  • 支持 PDF 注解、书签、链接等高级特性。
⚠️ 缺点
  • 安装包稍大(~10MB);
  • 在某些 ARM 架构(如树莓派)上需手动编译(但 x86_64 通常没问题)。
 案例:
from langchain_community.document_loaders import PyMuPDFLoader

docs = PyMuPDFLoader(
    file_path="../assets/使用LangChain1.0与FAISS构建企业本地RAG应用.pdf",  # pdf文件路径
    mode="page"  # 加载模式: 1."single"表示返回单个Document对象,2."page"表示按页切分文档
).load()

print(docs)

2.7.2 UnstructuredPDFLoader

UnstructuredPDFLoader是对 unstructured 库的封装。支持布局识别与 OCR 提取文字。

使用UnstructuredPDFLoader,需要先下载 PopplerTesseract OCR

Poppler 是一个开源的 PDF 文档处理库,用于渲染、解析和操作 PDF 文件。下载解压后将 .../poppler-24.08.0/Library/bin 添加到环境变量 Path 中即可。

Tesseract OCR 用于提取图片中的文字,在安装时需要选择 Additional language data(download) 来添加中文语言包。

安装后,将安装时设置的安装目录添加到环境变量 Path 中。

 准备一个带图片的pdf,测试能否获取到里面内容,如下:

image

代码:

# pip install unstructured[local-inference]
from langchain_community.document_loaders import UnstructuredPDFLoader

docs = UnstructuredPDFLoader(
    file_path="../assets/un.pdf",  # pdf文件路径
    # 加载模式:
    #   single: 返回单个Document对象
    #   elements: 按标题等元素切分文档
    mode="elements",
    # 加载策略:
    #   fast: pdfminer 提取并处理文本
    #   ocr_only: 转换为图片并进行 OCR
    #   hi_res: 识别文档布局,将OCR 输出与 pdfminer 输出融合
    strategy="hi_res",
    # 推断表格结构:仅 hi_res 下起效,如果为 True 则会在表格元素的元数据中添加 text_as_html
    infer_table_structure=True,
    # OCR 使用的语言: eng 英文,chi_sim 中文简体。语言列表参考 https://github.com/tesseract-ocr/langdata
    languages=["eng", "chi_sim"],
    # 更多参数详见 https://github.com/Unstructured-IO/unstructured/blob/main/unstructured/partition/pdf.py
).load()

print(docs)

输出:

image

2.7.3 MinerU

 MinerU 提供了 PDF、Word、PPT、图片等文件的解析,支持图像提取、OCR、公式、表格解析等功能。

调用在线服务:https://mineru.net/apiManage/docs。可以从本地批量上传文件进行解析,并接收解析结果。

import os
import requests

def upload_files(file_paths: list[str]) -> str:
    """批量上传文件"""
    url = "https://mineru.net/api/v4/file-urls/batch"
    api_key = os.getenv("MINERU_API_KEY")
    header = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
    }

    files_info = [
        {
            "name": os.path.basename(file_path),  # 文件名
            "is_ocr": True,  # 是否启用 ocr
            "data_id": f"file_{i}",  # 文件对应唯一标识 id
        }
        for i, file_path in enumerate(file_paths)
    ]  # 动态生成文件信息

    data = {
        "enable_formula": True,  # 是否开启公式识别
        "enable_table": True,  # 是否开启表格识别
        "language": "ch",  # 文档语言
        "files": files_info,
    }

    try:
        response = requests.post(url, headers=header, json=data)
        if response.status_code == 200:
            result = response.json()
            print("response success. result:{}".format(result))
            if result["code"] == 0:
                batch_id = result["data"]["batch_id"]
                urls = result["data"]["file_urls"]
                print("batch_id:{}\nurls:{}".format(batch_id, urls))
                for i in range(0, len(urls)):
                    with open(file_paths[i], "rb") as f:
                        res_upload = requests.put(urls[i], data=f)
                        if res_upload.status_code == 200:
                            print(f"{urls[i]} upload success")
                        else:
                            print(f"{urls[i]} upload failed")
                return batch_id
            else:
                print("apply upload url failed,reason:{}".format(result.msg))
        else:
            print(
                "response not success. status:{} ,result:{}".format(
                    response.status_code, response
                )
            )
    except Exception as err:
        print(err)

def download_files(batch_id):
    """批量获取任务结果"""
    os.makedirs("parsed_files", exist_ok=True)
    url = f"https://mineru.net/api/v4/extract-results/batch/{batch_id}"
    api_key = os.getenv("MINERU_API_KEY")
    header = {
        "Content-Type": "application/json",
        "Authorization": f"Bearer {api_key}",
    }
    res = requests.get(url, headers=header)
    extract_results = res.json()["data"]["extract_result"]
    failed_files = set()  # 失败文件集合
    done_files = set()  # 完成文件集合
    while True:
        for result in extract_results:
            if result["state"] == "failed":
                failed_files.add(str(result))
            elif result["state"] == "done":
                done_files.add(str(result))
                full_zip_url = result["full_zip_url"]
                res_download = requests.get(full_zip_url, stream=True)
                with open(
                    f"parsed_files/{result['file_name']}_{result['data_id']}.zip", "wb"
                ) as f:
                    for chunk in res_download.iter_content(chunk_size=1024):
                        f.write(chunk)
        if len(failed_files) + len(done_files) == len(extract_results):
            break
    for i in failed_files:
        print(i)
    for i in done_files:
        print(i)

file_paths = ["assets/sample.pdf"]
batch_id = upload_files(file_paths)
download_files(batch_id)

三、文档切分

3.1 为什么切分文档?

获取 Document 对象后,需要将其切分成 Chunk。之所以要进行切分是出于以下考虑:

  • 后续需要根据提问检索出相关的内容放入 Prompt,如果答案出现在某一个 Document 对象中,那么将检索到的整个 Document 对象直接放入 Prompt 中并不是最优的选择,因为 Document 可能包含非常多无关的信息,这些无效信息会干扰大模型的生成。

有研究发现,尽管大模型能够处理长文本输入,但它们在利用长上下文方面存在显著不足。尤其是在多文档问答和键值检索等任务中,当相关信息位于输入文本的中间时,模型的性能显著下降。这种现象表明,当前的语言模型在长输入上下文中未能充分利用信息,尤其是位于中间部分的信息。

  • 大模型存在最大输入的 Token 限制,如果一个 Document 非常大,在输入大模型时会被截断,导致信息缺失。

基于此,一个方法是将完整的 Document 进行分块处理(Chunking),将 Document 切分为一个个小块(Chunk)。无论是在存储还是检索过程中,都将以这些块为基本单位,这样能有效地避免内容噪声干扰和超出最大 Token 的问题。

3.2 切分策略

  • 按照固定字符数或 Token 数来切分,但可能会在不适当的位置切断句子。
  • 递归使用多个分隔符切分,同时尽量保证字符数或 Token 数不超出限制。能保证不切断完整的句子。
  • 语义切分:根据文本的语义内容切分,旨在保持相关信息的集中和完整,适用于需要高度语义保持的场景。但处理速度较慢,且可能出现不同块之间长度极不均衡的情况。具体切分过程为:将相邻的几个句子拼成一个句组。对所有句组进行嵌入,并比较嵌入向量的距离,找到语义变化大的位置,根据阈值确定切分点(比如计算相邻句子嵌入向量的余弦距离,取距离分布的第 N 百分位值作为阈值,高于此值则切分)。按照切分点切分出若干个语义段,并合并某些长度很短的语义段。

3.3 RecursiveCharacterTextSplitter

RecursiveCharacterTextSplitte(递归字符文本切分器)是最常用的切分器,它由一个字符列表作为参数,默认列表为 ["\n\n", "\n", " ", ""],并且会尝试按顺序使用这些字符进行切分,直到块足够小。由此尽可能地将所有段落(然后是句子,最后是词)保持在一起,因为这些段落通常看起来是语义上最相关的文本片段。

同时为了保证段之间语义完整,可以设置每个块之间有一部分重叠。

举例:

# pip install langchain-text-splitters
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1.加载PDF文档
docs = PyMuPDFLoader(
    file_path="../assets/使用LangChain1.0与FAISS构建企业本地RAG应用.pdf",  # pdf文件路径
    mode="page"  # 加载模式: 1."single"表示返回单个Document对象,2."page"表示按页切分文档
).load()

# 2.创建文档切分器
text_spliter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "", "", "", "……", "", ""],  # 分隔符列表,默认为["\n\n", "\n", " ", ""]
    chunk_size=400,  # 切分块大小
    chunk_overlap=50,  # 切分块重叠大小
    length_function=len,  # 可选:计算文本长度的函数,默认为字符串长度,可自定义函数实现按token数切分
    add_start_index=True  # 可选:块的元数据中添加此块的起始索引
)

# 3.切分文档
docs = text_spliter.split_documents(docs)
print(docs)

四、文档嵌入

使用嵌入模型生成文档的嵌入向量,后续检索时用于与查询的嵌入向量进行相似度计算。

2018年谷歌推出的 BERT 能够将文本嵌入为简单的向量表示,但是 BERT 并未针对有效生成句子嵌入进行优化,由此促使了 Sentence-BERT 的诞生。Sentence-BERT 调整了 BERT 的架构以及预训练任务以生成包含语义的句子嵌入向量,这些嵌入向量可以通过余弦相似度等相似性指标轻松进行比较,大大降低了查找相似句子等任务的计算开销。

常用嵌入模型:

模型

机构

描述

bge-large-zh

北京智源研究院(BAAI)

开源,向量维度1024,序列长度512

bge-base-zh

BAAI

开源,向量维度768,序列长度512

bge-small-zh

BAAI

开源,向量维度512,序列长度512

bge-m3

BAAI

开源,多语言,向量维度1024,序列长度8192

text-embedding-3-small

OpenAI

多语言,向量维度1536,序列长度8192

text-embedding-3-large

OpenAI

多语言,向量维度3072,序列长度8192

案例:

# pip install sentence-transformers langchain_huggingface
import os
from langchain_community.document_loaders import PyMuPDFLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 1.加载PDF文档
docs = PyMuPDFLoader(
    file_path="../assets/使用LangChain1.0与FAISS构建企业本地RAG应用.pdf",  # pdf文件路径
    mode="page"  # 加载模式: 1."single"表示返回单个Document对象,2."page"表示按页切分文档
).load()

# 2.创建文档切分器
text_spliter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "", "", "", "……", "", ""],  # 分隔符列表,默认为["\n\n", "\n", " ", ""]
    chunk_size=400,  # 切分块大小
    chunk_overlap=50,  # 切分块重叠大小
    length_function=len,  # 可选:计算文本长度的函数,默认为字符串长度,可自定义函数实现按token数切分
    add_start_index=True  # 可选:块的元数据中添加此块的起始索引
)

# 3.切分文档
docs = text_spliter.split_documents(docs)
print(docs)

# 4.提取文本文档的内容
docs_text = [doc.page_content for doc in docs]

# 获取项目根路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 拼接模型路径
model_path = os.path.join(project_root, "models", "bge-base-zh-v1.5")

# 5.创建向量模型,HuggingFaceEmbeddings表示模型
embedding = HuggingFaceEmbeddings(
    model_name=model_path,  # 'BAAI/bge-base-zh-v1.5'
    model_kwargs={'device': 'cpu'},  # 或 'cuda' 如果有 GPU
    encode_kwargs={
        'batch_size': 8,
        'normalize_embeddings': True  # BGE 要求归一化
    }
)

# 5.向量化,embed_documents多文本嵌入,不能直接传入Document
embeddings =embedding.embed_documents(docs_text)
print(embeddings)

五、向量存储

5.1 向量数据库的理解

假设你是一名摄影师,拍了大量的照片。为了方便管理和查找,你决定将这些照片存储到一个数据库中。传统的关系型数据库(如 MySQL、PostgreSQL 等)可以帮助你存储照片的元数据,比如拍摄时间、地点、相机型号等。

但是,当你想要根据照片的内容(如颜色、纹理、物体等)进行搜索时,传统数据库可能无法满足你的需求,因为它们通常以数据表的形式存储数据,并使用查询语句进行精确搜索。那么此时,向量数据库就可以派上用场。

我们可以构建一个多维的空间使得每张照片特征都存在于这个空间内,并用已有的维度进行表示,比如时间、地点、相机型号、颜色….此照片的信息将作为一个点,存储于其中。以此类推,即可在该空间中构建出无数的点,而后我们将这些点与空间坐标轴的原点相连接,就成为了一条条向量,当这些点变为向量之后,即可利用向量的计算进一步获取更多的信息。当要进行照片的检索时,也会变得更容易更快捷。

注意,在向量数据库中进行检索时,并不是检索唯一的匹配结果,而是查询和目标向量最为相似的一些向量,具有模糊性。

延伸思考一下,只要对图片、视频、商品等素材进行向量化,就可以实现以图搜图、视频相关推荐、相似商品推荐等功能。

5.2 常用的向量数据库

LangChain提供了众多向量存储的集成,包括开源的本地向量存储与云托管的私有向量存储。并公开了一个标准接口,可以轻松地在向量存储之间进行交换。

常用向量数据库:

向量数据库

描述

FAISS

一个用于高效相似性搜索和密集向量聚类的库

Chroma

开源的轻量级向量数据库,有极简的 API

Milvus

开源的专为向量搜索设计的云原生数据库。性能强悍,功能丰富。覆盖轻量级的原型开发到十亿级向量的大规模生产系统

Pgvector

开源关系型数据库 PostgreSQL 的扩展,为PostgreSQL增加了向量数据类型和相似性搜索功能

Redis

开源内存数据结构存储,现已原生支持向量相似性搜索功能

Elasticsearch

开源分布式搜索和分析引擎,提供了一个基于文档的数据库,结构化、非结构化和向量数据通过高效的列式存储统一管理

这里我们使用 Milvus 作为向量存储。

5.3 Milvus 介绍

Milvus 通过 数据库Collections实体 的结构管理数据。Collections 和实体就类似关系型数据库中的表和记录。具体来说,Collection 是一个二维表,具有固定的列和变化的行。每列代表一个字段,每行代表一个实体。

Collection 通过 Collection Schema 来定义有哪些字段以及字段的类型、索引等。一个 Collection Schema 有一个主键、最多四个向量字段和若干标量字段。

主键用于唯一标识一个实体,只接受 Int64 或 VarChar 值。插入实体时,默认情况下应包含主键值。但是,如果在创建 Collections 时启用了 AutoId,Milvus 将在插入数据时生成主键值,此时插入的实体中不应包含主键值。

向量字段用于存储文本、图像和音频等非结构化数据类型的嵌入,可以是密集向量、稀疏向量或二进制向量。通常,密集向量用于语义搜索,而稀疏向量则更适合全文或词性匹配。

标量字段通常用来存储一些元数据,并可以在搜索时通过元数据进行过滤,以提高搜索结果的正确性。

字段类型

字段

描述

向量字段

密集向量

FLOAT_VECTOR

32位浮点数列表

FLOAT16_VECTOR

16位半精度浮点数列表

BFLOAT16_VECTOR

16位浮点数列表,精度稍低,但指数范围与 Float32 相同

INT8_VECTOR

8位有符号整数向量

稀疏向量

SPARSE_FLOAT_VECTOR

非零数字及其序列号列表

二进制向量

BINARY_VECTOR

一个0和1的列表

标量字段

VARCHAR

字符串

BOOL

存储true或false

INT

INT8、INT16、INT32、INT64

FLOAT

32位浮点数

DOUBLE

64位双精度浮点数

ARRAY

相同数据类型元素的有序集合

JSON

结构化的键值数据

索引是建立在数据之上的附加结构,可以加快搜索速度。不同字段数据类型适用不同的索引类型。比如 FLOAT_VECTOR 可使用 HNSW(分层导航小世界)索引,VARCHAR 可使用 INVERTED(反转)索引。详见https://milvus.io/docs/index-explained.md

Milvus 提供了多个版本以在不同场景下选择合适的使用方式:

  • Milvus Lite:本地轻量化运行,通过pip 即可安装。但 Milvus Lite 有一些限制,比如 Milvus Lite 仅支持 FLAT 索引类型。无论在 Collections 中指定了哪种索引类型,它都使用 FLAT 类型。
  • Milvus Standalone:单点部署,支持通过 Docker 部署。
  • Milvus Distributed:分布式部署,支持在 Kubernetes 集群上部署。

5.4.Milvus Lite

5.4.1 Milvus Lite安装

Milvus Lite。它能让你在个人电脑上,以最快速度、最低成本体验到 Milvus 几乎所有的核心功能。当你完成了学习和开发,准备将应用部署到正式的生产环境时,再将代码无缝迁移到标准版的Milvus服务上即可。

Milvus Lite 直接通过 pip 安装,无需单独下载:
pip install milvus
pip install pymilvus

安装完成后会自带轻量版服务(无需额外启动 Docker)。

5.4.2 持久化启动

在conda命令行先运行:

正确用法(命令行启动 milvus-server)

如果不通过 --proxy-port  默认使用19530端口

milvus-server --data <data_path> --proxy-port <port_number>
示例(Windows):
milvus-server --data F:\milvus_data --proxy-port 19531

✅ 启动后,Milvus 会将元数据和向量索引存储在 F:\milvus_data,并监听 19531 端口。

示例(Linux/macOS):
milvus-server --data ~/milvus_data --proxy-port 19531

5.5 创建 Collection

Collection、Fields、Schema 的关系:

  • Collection = 表(包含多个字段)

  • Fields = 列定义(每个字段的类型和属性)

  • Schema = 表结构(所有字段的集合)

案例代码:

from pymilvus import DataType, MilvusClient

# todo 1.实例化向量数据库客户端
client = MilvusClient(
    uri="http://localhost:19530"  # 数据库文件路径./milvus_demo.db
)
print("✅ 初始化成功!")


# todo 2.创建schema
def build_schema():
    """
    创建schema
    :return:
    """
    return MilvusClient.create_schema(
        auto_id=True,  # 自动分配主键
        enable_dynamic_field=True,  # 启用动态字段,未能在Schema中申明的字段会以键值对的形式存储在这个动态字段中
    ).add_field(  # 添加id字段,类型为整数,设置为主键
        field_name="id", datatype=DataType.INT64, is_primary=True
    ).add_field(  # 添加向量字段,类型为浮点数向量,维度为768
        field_name="vector", datatype=DataType.FLOAT_VECTOR, dim=768
    ).add_field(  # 添加text字段,类型为字符串,最大长度为1024
        field_name="text", datatype=DataType.VARCHAR, max_length=1024
    ).add_field(  # 添加 metadata字段,类型为JSON
        field_name="metadata", datatype=DataType.JSON
    )


# todo 3.创建索引
def build_index():
    """
    创建index
    :return:
    """
    # 创建索引
    index_params = MilvusClient.prepare_index_params()
    index_params.add_index(
        field_name="vector",  # 指定建立索引的字段
        index_type="AUTOINDEX",  # 指定索引类型
        metric_type="L2",  # 向量相似度度量方式
    )

    return index_params


# todo 3.如果存在则删除collection
if client.has_collection(collection_name="test_collection"):
    # 删除 collection
    # 在 Milvus 中删除数据后,存储空间不会立即释放。虽然删除数据会将实体标记为 "逻辑删除",但实际空间可能不会立即释放。
    # Milvus 会在后台自动压缩数据。这个过程会将较小的数据段合并为较大的数据段,并删除"逻辑删除"的数据或已超过有效时间的数据。
    # 一个名为 Garbage Collection (GC) 的独立进程会定期删除这些 "已删除 "的数据段,从而释放它们占用的存储空间。

    # 删除 collection
    client.drop_collection(collection_name="test_collection")

# todo 4.创建 collection
client.create_collection(
    collection_name="test_collection",  # collection名称
    schema=build_schema(),  # collection 的 schema 信息
    index_params=build_index(),  # collection 的索引信息
)

# 查询 collection
print("列出所有 collection:", client.list_collections())

5.6 操作实体

5.6.1 插入实体

将之前的word文档向量化,然后存储起来:

import os

from langchain_community.document_loaders import UnstructuredWordDocumentLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pymilvus import MilvusClient

# 1.加载docx文件
docs = UnstructuredWordDocumentLoader(
    file_path="../assets/agent.docx",  # docx文件路径
    # 加载模式:
    #   single 返回单个Document对象
    #   elements 按标题等元素切分文档
    mode="single"
).load()


# 2.创建文档切分器
text_spliter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "", "", "", "……", "", ""],  # 分隔符列表,默认为["\n\n", "\n", " ", ""]
    chunk_size=400,  # 切分块大小
    chunk_overlap=50,  # 切分块重叠大小
    length_function=len,  # 可选:计算文本长度的函数,默认为字符串长度,可自定义函数实现按token数切分
    add_start_index=True  # 可选:块的元数据中添加此块的起始索引
)

# 3.切分文档
docs = text_spliter.split_documents(docs)
# print(docs)
# 4.提取文本文档的内容
docs_text = [doc.page_content for doc in docs]

# 5.实例化向量数据库客户端
client = MilvusClient(
    uri="http://localhost:19530"  # 数据库文件路径./milvus_demo.db
)
print("✅ 初始化成功!")

# 获取项目根路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 拼接模型路径
model_path = os.path.join(project_root, "models", "bge-base-zh-v1.5")

# 6.创建向量模型,HuggingFaceEmbeddings表示模型
embedding = HuggingFaceEmbeddings(
    model_name=model_path,  # 'BAAI/bge-base-zh-v1.5'
    model_kwargs={'device': 'cpu'},  # 或 'cuda' 如果有 GPU
    encode_kwargs={
        'batch_size': 8,
        'normalize_embeddings': True  # BGE 要求归一化
    }
)

# 7.向量化,embed_documents多文本嵌入
embeddings = embedding.embed_documents(docs_text)

# 8.转换数据格式,将加载的文档和它们的向量表示转换为适合插入 Milvus 向量数据库的数据结构
data = [
    {
        "vector": embedding,
        "text": doc.page_content,
        "metadata": doc.metadata
    }
    for doc, embedding, in zip(docs, embeddings)
]

# 9.插入向量数据库
resp = client.insert(collection_name="test_collection", data=data)
print("✅ 插入成功!", resp)

输出:

image

✅ 字段解释

字段含义
'insert_count' 成功插入的实体(行)数量。这里是 4,表示你这次插入了 4 条向量记录。
'ids' 每条记录被分配的主键 ID 列表。顺序与你插入的数据一一对应。

5.6.2 查询实体

可以根据id查询,也可以根据通过过滤条件进行过滤,参考:https://milvus.io/docs/zh/boolean.md

from pymilvus import MilvusClient


# 实例化向量数据库客户端
client = MilvusClient(
    uri="http://localhost:19530"  # 数据库文件路径./milvus_demo.db
)
print("✅ 初始化成功!")

# todo: 1.通过主键查询实体
resp = client.get(
    collection_name="test_collection",
    ids=[462794262917160909, 462794262917160910],
    output_fields=["text", "metadata"]
)
print(resp)

# todo: 2.通过过滤条件
resp = client.query(
    collection_name="test_collection",
    filter="metadata['source'] == '../assets/agent.docx'",  # 过滤条件metadata['source'] == '../assets/agent.docx'进行过滤
    output_fields=["text", "metadata"],
    limit=1  # 限制返回数量,默认为10,
)
print(resp)

5.6.3 删除实体

from pymilvus import MilvusClient


# 实例化向量数据库客户端
client = MilvusClient(
    uri="http://localhost:19530"  # 数据库文件路径./milvus_demo.db
)
print("✅ 初始化成功!")

# todo: 1.通过主键删除实体
resp = client.delete(
    collection_name="test_collection",
    ids=[462794262917160909, 462794262917160910]
)
print(resp)

# todo: 2.通过过滤条件删除
resp = client.delete(
    collection_name="test_collection",
    filter='text LIKE "你是%"',  # 使用text前缀进行过滤
)
print(resp)

六、检索与生成

6.1 检索

检索阶段:用户输入查询➡️计算嵌入向量➡️在向量存储中检索相似向量➡️返回相似向量对应的内容。

举例:检索过程

# pip install langchain_community unstructured[docx]
import os
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pymilvus import MilvusClient

# 1.加载docx文件
docs = UnstructuredWordDocumentLoader(
    file_path="../assets/agent.docx",  # docx文件路径
    # 加载模式:
    #   single 返回单个Document对象
    #   elements 按标题等元素切分文档
    mode="single"
).load()

# 2.创建文档切分器
text_spliter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "", "", "", "……", "", ""],  # 分隔符列表,默认为["\n\n", "\n", " ", ""]
    chunk_size=400,  # 切分块大小
    chunk_overlap=50,  # 切分块重叠大小
    length_function=len,  # 可选:计算文本长度的函数,默认为字符串长度,可自定义函数实现按token数切分
    add_start_index=True  # 可选:块的元数据中添加此块的起始索引
)

# 3.切分文档
docs = text_spliter.split_documents(docs)
# print(docs)
# 4.提取文本文档的内容
docs_text = [doc.page_content for doc in docs]

# 5.实例化向量数据库客户端
client = MilvusClient(
    uri="http://localhost:19530"  # 数据库文件路径./milvus_demo.db
)
print("✅ 初始化成功!")

# 获取项目根路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 拼接模型路径
model_path = os.path.join(project_root, "models", "bge-base-zh-v1.5")

# 6.创建向量模型,HuggingFaceEmbeddings表示模型
embedding = HuggingFaceEmbeddings(
    model_name=model_path,  # 'BAAI/bge-base-zh-v1.5'
    model_kwargs={'device': 'cpu'},  # 或 'cuda' 如果有 GPU
    encode_kwargs={
        'batch_size': 8,
        'normalize_embeddings': True  # BGE 要求归一化
    }
)

# 7.向量化,embed_documents多文本嵌入
embeddings = embedding.embed_documents(docs_text)

# 8.转换数据格式,将加载的文档和它们的向量表示转换为适合插入 Milvus 向量数据库的数据结构
data = [
    {
        "vector": embedding,
        "text": doc.page_content,
        "metadata": doc.metadata
    }
    for doc, embedding, in zip(docs, embeddings)
]

# 9.插入向量数据库
resp = client.insert(collection_name="test_collection", data=data)
print("✅ 插入成功!", resp)


def retrieval(query: str, embad_model, client):
    """
    向量数据库检索
    """

    # 向量化查询
    query_embedding = embad_model.embed_query(query)

    # 查询嵌入
    resp = client.search(
        collection_name="test_collection",  # collection的名称
        data=[query_embedding],  # 搜索的向量
        anns_field="vector",  # 进行向量搜索的字段
        search_params={"metric_type": "L2"},  # 度量方式:L2欧氏距离/ IP内积/ COSINE余弦相似度
        output_fields=["text", "metadata"],  # 返回的字段
        limit=2,  # 返回结果数量
    )
    return resp

context = retrieval(query="get_weather函数什么作用?", embad_model=embedding, client=client)
print(context)

向量相似性搜索算法:ANN(近似近邻)和 KNN(最近邻)搜索是向量相似性搜索的常用方法。

  1. 在 KNN 搜索中,必须将向量空间中的所有向量与搜索请求中携带的查询向量进行比较,然后找出最相似的向量,费时费力。
  2. 而 ANN 搜索算法要求提供一个索引文件,记录向量 Embeddings 的排序顺序。当收到搜索请求时,使用索引文件作为参考,找到可能包含与查询向量最相似的向量嵌入的子组,根据指定的度量类型来测量查询向量与子组中的向量之间的相似度,根据与查询向量的相似度对组成员进行排序,并返回前 K 个成员。不过 ANN 搜索依赖于预建索引,搜索吞吐量、内存使用量和搜索正确性可能会因选择的索引类型而不同。

HNSW (分层导航小世界)是当下常用的一种基于图的索引算法,可以提高搜索高维浮点数向量时的性能。它具有出色的搜索精度和低延迟,但需要较高的内存开销来维护其分层图结构。该算法构建了一个多层图(类似不同缩放级别的地图),底层包含所有数据点,而上层则由从底层采样的数据点子集组成。在这种层次结构中,每一层都包含代表数据点的节点,节点之间由表示其接近程度的边连接。上层提供远距离跳转,以快速接近目标,而下层则进行细粒度搜索,以获得最准确的结果。其工作原理如下:

  • 入口点:搜索从顶层的一个固定入口点开始,该入口点是图中的一个预定节点。
  • 贪婪搜索:算法贪婪地移动到当前层的近邻,直到无法再接近查询向量为止。上层起到导航作用,作为粗过滤器,为下层的精细搜索找到潜在的入口点。
  • 层层下降:一旦当前层达到局部最小值,算法就会利用预先建立的连接跳转到下层,并重复贪婪搜索。
  • 最后细化:这一过程一直持续到最底层,在最底层进行最后的细化步骤,找出最近的邻居。

6.2 生成

这里指的是通过检索出来的向量数据,通过大模型生成对应问题的答案:

# pip install langchain_community unstructured[docx]
import os
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import PromptTemplate, ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pymilvus import MilvusClient
from langchain_ollama import ChatOllama

# 1.加载docx文件
docs = UnstructuredWordDocumentLoader(
    file_path="../assets/agent.docx",  # docx文件路径
    # 加载模式:
    #   single 返回单个Document对象
    #   elements 按标题等元素切分文档
    mode="single"
).load()

# 2.创建文档切分器
text_spliter = RecursiveCharacterTextSplitter(
    separators=["\n\n", "\n", "", "", "", "……", "", ""],  # 分隔符列表,默认为["\n\n", "\n", " ", ""]
    chunk_size=400,  # 切分块大小
    chunk_overlap=50,  # 切分块重叠大小
    length_function=len,  # 可选:计算文本长度的函数,默认为字符串长度,可自定义函数实现按token数切分
    add_start_index=True  # 可选:块的元数据中添加此块的起始索引
)

# 3.切分文档
docs = text_spliter.split_documents(docs)
# print(docs)
# 4.提取文本文档的内容
docs_text = [doc.page_content for doc in docs]

# 5.实例化向量数据库客户端
client = MilvusClient(
    uri="http://localhost:19530"  # 数据库文件路径./milvus_demo.db
)
print("✅ 初始化成功!")

# 获取项目根路径
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# 拼接模型路径
model_path = os.path.join(project_root, "models", "bge-base-zh-v1.5")

# 6.创建向量模型,HuggingFaceEmbeddings表示模型
embedding = HuggingFaceEmbeddings(
    model_name=model_path,  # 'BAAI/bge-base-zh-v1.5'
    model_kwargs={'device': 'cpu'},  # 或 'cuda' 如果有 GPU
    encode_kwargs={
        'batch_size': 8,
        'normalize_embeddings': True  # BGE 要求归一化
    }
)

# 7.向量化,embed_documents多文本嵌入
embeddings = embedding.embed_documents(docs_text)

# 8.转换数据格式,将加载的文档和它们的向量表示转换为适合插入 Milvus 向量数据库的数据结构
data = [
    {
        "vector": embedding,
        "text": doc.page_content,
        "metadata": doc.metadata
    }
    for doc, embedding, in zip(docs, embeddings)
]

# 9.插入向量数据库
resp = client.insert(collection_name="test_collection", data=data)
# print("✅ 插入成功!", resp)


def retrieval(query: str, embad_model, client):
    """
    向量数据库检索
    """

    # 向量化查询
    query_embedding = embad_model.embed_query(query)

    # 查询嵌入
    resp = client.search(
        collection_name="test_collection",  # collection的名称
        data=[query_embedding],  # 搜索的向量
        anns_field="vector",  # 进行向量搜索的字段
        search_params={"metric_type": "L2"},  # 度量方式:L2欧氏距离/ IP内积/ COSINE余弦相似度
        output_fields=["text", "metadata"],  # 返回的字段
        limit=2,  # 返回结果数量
    )
    return resp


# 10.检索向量数据库
# context = retrieval(query="get_weather函数什么作用?", embad_model=embedding, client=client)
# print(context)

# 11.初始化大模型
llm = ChatOllama(
    base_url="http://127.0.0.1:11434",
    model="qwen3:8b"
)

# 12.准备提示词
# template = """
# 你是一个只能文档助手,请参考上下文内容,给出具体的答案:
# 已知内容:{content}
# 问题:{query}
# """
# prompt = PromptTemplate.from_template(template).format_prompt(content=context, query="get_weather函数什么作用?")

# 13.生成答案
# resp = llm.invoke(prompt)
# print(resp.content)

template = ChatPromptTemplate.from_messages([
    ("system", "你是一个文档助手,请参考上下文内容{content},给出具体的答案:"),
    ("human", "问题:{query}"),
])

# 14.组装链路
chain = ({ # 这是给template准备的数据
             "query": RunnablePassthrough(),  # 获取检索结果
             "content": lambda x: retrieval(query=x, embad_model=embedding, client=client)
         }
         | RunnableLambda(lambda x: print(x) or x)  # 打印中间结果
         | template
         | llm
         | StrOutputParser()
         )

# 15.执行链路
resp_chunks = chain.stream(input="get_weather函数什么作用?")
for chunk in resp_chunks:
    print(chunk, end="", flush=True)  # 流式输出, end=""表示不换行,flush=True表示刷新缓冲区
posted @ 2025-12-10 08:25  酒剑仙*  阅读(23)  评论(0)    收藏  举报