24.RAG进阶(Advanced RAG)-摘要索引

内容参考于:图灵AI大模型全栈

摘要索引

原理是先把文档进行分片,然后把分片的内容使用大模型进行生成摘要,然后放到向量数据库中,然后比如分片之后有100个,就创建100个唯一的id(跟身份证一样,由数字、字母组成),然后分别对100个摘要数据和100个分片之后的原文数据进行绑定,也就是说一个id绑定一个摘要一个原文,摘要放到向量数据库中用来做问题的搜索,就是说输入问题后先通过向量搜索摘要,找到摘要后,通过id去获取对于的原文,然后把原文带着问题去问大模型获取答案

摘要索引使用的不多,如果数据没有问题(非常准确),经过摘要后(经过大模型摘要后),会出现问题,大模型会出现幻觉会把无问题的数据摘要的有问题,摘要索引适合于PDF里面的表格、图片

代码,运行时先连接VPN,它要下载 spaCy 模型,如果不连接VPN会很慢

base_llm.py文件的内容

python 复制代码
from langchain_openai import ChatOpenAI
from dotenv import load_dotenv
import os

from langchain_huggingface import HuggingFaceEmbeddings
model_path = r"E:\AiModel\Local_model\BAAI\bge-large-zh-v1___5"  # 替换为实际路径
embeddings_model = HuggingFaceEmbeddings(model_name=model_path)

load_dotenv()
api_key = os.getenv('huoshan')
llm = ChatOpenAI(
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
    model="qwen-plus",
)
python 复制代码
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
# 上面两行是Python文件标准头
# 第1行:告诉Linux/Mac系统用python解释器执行这个文件(Windows系统会忽略)
# 第2行:声明文件编码为UTF-8,避免代码里的中文出现乱码


# -------------------------- 模块导入部分 --------------------------
# 从自定义模块base_llm中导入两个提前初始化好的模型实例
from base_llm import llm, embeddings_model
# - llm:大语言模型实例(比如DeepSeek、GPT这类对话模型),后续用来生成摘要、回答问题
# - embeddings_model:嵌入模型实例,作用是把文本转换成一串数字(向量),实现「语义相似度搜索」

from langchain_core.stores import InMemoryByteStore
# 导入「内存字节存储」工具类
# 作用:在电脑内存里以「键值对」的形式存数据(key是文档ID,value是原始文档内容)
# 为什么需要它:多向量检索需要两个仓库------向量库存「摘要向量」做搜索,字节库存「原始全文」做回答支撑

from langchain_chroma import Chroma
# 导入Chroma向量数据库封装
# Chroma是轻量级本地向量数据库,开箱即用,不需要单独安装服务
# 作用:存储文本对应的向量,用户提问时把问题也转成向量,快速找到语义最相似的文本

from langchain_community.document_loaders import UnstructuredWordDocumentLoader, WebBaseLoader
# 导入两个文档加载器(LangChain社区提供的通用工具)
# - UnstructuredWordDocumentLoader:专门读取本地Word文档(.docx),把文件里的文字转成标准格式
# - WebBaseLoader:专门爬取网页正文内容,把网页里的文字提取出来转成标准格式
# 为什么需要加载器:不同格式的数据源(文件、网页)读取方式不同,用加载器可以统一成后续能处理的格式

from langchain_text_splitters import RecursiveCharacterTextSplitter
# 导入「递归字符文本分割器」
# 作用:把长篇大文档切成一小块一小块(叫chunk),避免文档太长超过模型的输入长度限制,也能提升检索准确度
# 为什么叫「递归字符」:它会优先按段落→句子→单词的顺序拆分,尽量不把一句话切断,比硬切更合理

# MultiVectorRetriever 多用检索器
from langchain_classic.retrievers import MultiVectorRetriever
# 导入「多向量检索器」------这是本代码的核心组件
# 核心设计思路:用「短摘要」做向量检索(语义更集中、召回更准),检索到后再返回「完整原文」
# 解决的问题:直接把长文档切块检索,经常出现单块语义不全、答非所问的情况
# 工作流程:用户提问 → 向量库搜最相似的摘要 → 通过摘要绑定的ID → 去字节库取出完整原始文档

import uuid
# 导入Python内置的uuid模块
# 作用:生成全局唯一的随机字符串ID,给每个文档分配专属编号,用来绑定「摘要」和「原文」

from langchain_core.documents import Document
# 导入LangChain的标准文档类
# 这是LangChain里所有文本的统一格式,包含两个核心属性:
# - page_content:文本的具体内容(字符串)
# - metadata:文本的附加信息(字典),比如来源、ID、作者等
# 为什么统一用它:加载器、分割器、向量库都认这个格式,各个组件可以无缝衔接

from langchain_core.output_parsers import StrOutputParser
# 导入「字符串输出解析器」
# 作用:把大模型返回的「消息对象」转换成纯文本字符串
# 为什么需要:大模型原生返回的是带角色、带格式的对象,我们最终需要的只是文字内容

from langchain_core.prompts import ChatPromptTemplate
# 导入「聊天提示词模板」
# 作用:定义给大模型的提问模板,用占位符(比如{doc})填充动态内容,批量生成标准化的提问

from langchain_core.runnables import RunnableMap
# 导入「可运行映射」
# 作用:把多个处理步骤组合成字典,并行处理输入,给后续步骤同时提供多个变量
# 什么时候用:比如问答时需要同时把「检索到的文档」和「用户问题」传给提示词,就用它来组装


# -------------------------- 1. 加载文档 --------------------------
# 定义要爬取的网页地址
url = "https://news.pku.edu.cn/mtbdnew/15ac0b3e79244efa88b03a570cbcbcaa.htm"
# 入参来源:手动指定的合法网页URL,后续传给网页加载器

# 初始化文档加载器列表(可以同时加载多个不同来源的文档)
loaders = [
    UnstructuredWordDocumentLoader("人事管理流程.docx"),
    WebBaseLoader(url)
]
# 列表里放了两个加载器实例,分别对应本地Word文件和在线网页
# 入参说明:
#   UnstructuredWordDocumentLoader的入参:本地Word文件的路径(字符串)
#       → 入参来源:本地存在的docx文件,建议和代码放同一目录,也可以写绝对路径
#   WebBaseLoader的入参:网页URL字符串
#       → 入参来源:上面定义的url变量
# 为什么用列表:统一管理多个数据源,后面循环批量加载,代码更简洁


docs = []
# 初始化空列表,用来存放所有加载完成的Document对象
# 后续每个加载器读取到的文档都会追加到这个列表里

for loader in loaders:
    docs.extend(loader.load())
# 循环遍历每个加载器,执行「读取文档」操作,把结果全部加到docs列表
# 调用的函数:loader.load()
#   为什么要调用:加载器只是定义了「要加载谁」,调用load()才会真正去读文件/爬网页
#   入参:无参数,加载目标在创建加载器的时候就已经传进去了
#   返回值:Document对象的列表(一个文件/网页可能返回1个或多个Document)
# 逻辑说明:逐个加载所有数据源,把所有文档汇总到一个列表,后续统一处理


# -------------------------- 2. 文档切块 --------------------------
# 分割器
text_split = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
# 创建文本分割器实例
# 为什么要切块:原始文档太长(比如整篇新闻、整份制度文件),直接向量化会丢失细节,也可能超过模型的输入长度
# 入参说明:
#   chunk_size=1024:每个文本块的最大字符数,这里设为1024个字符
#       → 入参来源:手动配置,一般500-2000都常用,根据模型上下文、检索精度调整
#   chunk_overlap=100:相邻两个文本块的重叠字符数,这里是100个字符
#       → 入参来源:手动配置,作用是避免切块把一句话切断,保证上下文语义连贯
# 其他常用可传入参:
#   separators:指定分割的优先级,默认是["\n\n", "\n", " ", ""](先按段落切,再按行切...)
#   length_function:计算长度的函数,默认按字符数统计

docs = text_split.split_documents(docs)
# 调用分割器的批量切块方法,把所有长文档切成小块
# 为什么调用:把长文档列表转换成小块文档列表,后续才能生成摘要、存入向量库
# 入参:docs ------ 前面加载好的长文档列表
#   入参来源:上一步循环加载得到的文档集合
# 返回值:切割后的小块Document列表,每个小块都保留了原文档的元数据
# 逻辑:遍历每个长文档,按照chunk_size和chunk_overlap的规则切成多个小文档,覆盖原docs变量


# -------------------------- 3. 批量生成文档摘要 --------------------------
# 创建摘要生成链
chain = (
        {"doc": lambda x: x.page_content}
        | ChatPromptTemplate.from_template("总结下面的文档:\n\n{doc}")
        | llm
        | StrOutputParser()
)
# 用LangChain的链式语法(LCEL)搭建一条「摘要生成流水线」
# 功能:输入一个Document对象 → 提取文本 → 生成提示词 → 调用大模型 → 输出摘要字符串
# 为什么用链式写法:代码简洁、支持批量调用、各个步骤可复用
# 逐段拆解:
# 1. {"doc": lambda x: x.page_content}
#    → 输入处理:接收Document对象x,提取它的文本内容,赋值给变量doc
#    → 入参x:Document对象,后续批量调用时来自docs列表的每个元素
# 2. | ChatPromptTemplate.from_template("总结下面的文档:\n\n{doc}")
#    → 管道符| 表示把上一步的输出传给下一步
#    → from_template:根据字符串创建提示词模板,{doc}是占位符,会被上一步的文本替换
#    → 入参:手写的提示词模板字符串
# 3. | llm
#    → 把组装好的提示词传给大语言模型,生成摘要回答
# 4. | StrOutputParser()
#    → 把大模型返回的消息对象转成纯字符串,得到最终的摘要文本

summ = chain.batch(docs, {'max_concurrency': 5})
# 批量执行摘要链,给所有文档块一次性生成摘要
# 函数:chain.batch() ------ 批量执行链
# 为什么调用:文档块数量多,逐个调用太慢,用批量并行处理提升效率
# 入参说明:
#   第1个参数:docs ------ 切割后的小块文档列表
#       → 入参来源:上一步分割得到的所有文档块
#   第2个参数:配置字典 {'max_concurrency': 5}
#       → max_concurrency:最大并发数,同时最多发5个请求给大模型
#       → 入参来源:手动配置,根据模型API的限流规则调整,避免并发太高被拒绝
# 返回值:summ是字符串列表,每个元素是对应文档块的摘要,顺序和docs一一对应


# -------------------------- 4. 初始化存储与检索器 --------------------------
# 存摘要之后转换成emb的内容
vector = Chroma(collection_name='summ', embedding_function=embeddings_model)
# 创建Chroma向量数据库实例
# 作用:存储摘要文本对应的向量,后续用户提问时做相似度搜索
# 入参说明:
#   collection_name='summ':集合名称(相当于数据库里的表名),这里命名为summ(摘要缩写)
#       → 入参来源:手动命名,用来区分不同的向量集合
#   embedding_function=embeddings_model:嵌入模型实例,负责把文本转成向量
#       → 入参来源:最开头从base_llm导入的嵌入模型
# 其他常用可传入参:
#   persist_directory:本地持久化文件夹路径,不传则只存在内存里,程序关闭数据就消失
# 逻辑:初始化一个专门存摘要向量的库,自动用指定模型做向量化

# 存原文档
store = InMemoryByteStore()
# 创建内存字节存储实例
# 作用:存储原始的完整文档块,用ID做key
# 为什么用它:多向量检索器需要两个存储,向量库存摘要做搜索,字节库存原文做回答
# 入参:无必传参数,默认创建一个空的内存键值对存储
# 特点:程序退出数据就丢失,适合演示;生产环境一般换成Redis、本地文件等持久化存储

id_key = 'doc_id'
# 定义元数据里的键名字符串,值为'doc_id'
# 作用:作为元数据的字段名,用来存放文档的唯一ID,实现摘要和原文的绑定
# 来源:手动定义的字符串,后续会在Document的metadata里用这个key存ID值

# 创建多用检索器
retriever = MultiVectorRetriever(
    vectorstore=vector,
    byte_store=store,
    id_key=id_key
)
# 创建多向量检索器实例------这是整个代码的核心
# 为什么调用:实现「摘要检索 + 原文返回」的两级检索,提升召回准确度
# 入参说明:
#   vectorstore=vector:向量数据库实例,存摘要向量
#       → 入参来源:上面创建的vector变量
#   byte_store=store:字节存储实例,存原始文档
#       → 入参来源:上面创建的store变量
#   id_key=id_key:元数据中存储文档ID的键名
#       → 入参来源:上面定义的id_key变量
# 其他常用可传入参:
#   search_kwargs={"k": 3}:检索时返回最相似的3个结果
#   search_type="mmr":用最大边际相关性检索,避免结果重复
# 工作原理:
#   存数据:摘要带doc_id存入向量库,原文用doc_id当key存入字节库
#   取数据:用户提问→搜向量库得到相似摘要→从摘要元数据拿doc_id→去字节库取对应原文


# -------------------------- 5. 存入数据(摘要+原文绑定) --------------------------
# docs 长度100     生成100个随机数
doc_ids = [str(uuid.uuid4()) for i in docs]
# 用列表推导式,给每个文档块生成一个全局唯一的ID字符串
# 为什么生成ID:给每个文档块分配专属编号,让摘要和原文能一一对应
# 函数:uuid.uuid4()
#   入参:无,生成随机的UUID
#   返回值:UUID对象,转成字符串后是36位的唯一随机串
# 逻辑:docs有多少个元素,就生成多少个ID,顺序和docs一一对应,第i个ID对应第i个文档


# 创建摘要文档列表(包含元数据) metadata可以用来关联原始文档和摘要文档会自动找到匹配关系
summary_docs = [
    Document(page_content=s, metadata={id_key: doc_ids[i]}) for i, s in enumerate(summ)
]
# 把摘要字符串包装成标准的Document对象,同时带上对应的文档ID元数据
# 为什么这么做:向量库只能存Document对象,而且必须在元数据里带上doc_id才能关联原文
# 列表推导式逻辑:
#   enumerate(summ):遍历摘要列表,同时拿到索引i和摘要内容s
#   page_content=s:文档内容就是摘要文本
#   metadata={id_key: doc_ids[i]}:元数据里存入对应的文档ID
# 结果:summary_docs是Document列表,每个摘要都绑定了对应原文的ID

# 把压缩之后的内容添加到向量数据库
retriever.vectorstore.add_documents(summary_docs)
# 把所有摘要文档存入向量数据库
# 为什么调用:只有把摘要存进向量库,后续才能做相似度检索
# 入参:summary_docs ------ 带元数据的摘要Document列表
#   入参来源:上一步生成的summary_docs
# 内部执行逻辑:
#   1. 提取每个摘要Document的文本内容
#   2. 用嵌入模型把文本转换成向量
#   3. 把向量、文本、元数据一起存入Chroma的summ集合
#   4. 每个摘要向量都绑定了对应的doc_id


# 源文档加载到内容
retriever.docstore.mset(list(zip(doc_ids, docs)))
# 批量把原始文档存入字节存储
# 为什么调用:把原文和ID绑定存入,后续检索到摘要ID后,才能取出对应的原始文档
# 函数:mset() ------ 批量设置键值对(multi set)
# 入参:列表,每个元素是(键, 值)的元组,格式类似 [(id1, doc1), (id2, doc2), ...]
#   入参来源:
#       - doc_ids:文档ID列表
#       - docs:原始文档块列表
#       - zip(doc_ids, docs):把两个列表按顺序配对成元组
#       - list(...):转成列表格式,符合mset的入参要求
# 逻辑:key是文档ID,value是对应的原始Document对象,批量存入内存字节存储
# 到此,摘要和原文就通过doc_id一一对应起来了


# -------------------------- 【注释掉的完整RAG问答链】 --------------------------
# 下面这段是完整的「检索+回答」RAG流程,当前代码只演示检索过程,所以被注释掉了
# prompt = ChatPromptTemplate.from_template("根据下面的文档回答问题:\n\n{doc}\n\n问题: {question}")
# 定义问答提示词模板
# 作用:给大模型的指令模板,有两个占位符:{doc}放检索到的参考文档,{question}放用户问题
# 为什么被注释:属于完整问答功能,当前只演示检索步骤,暂时注释

# # 生成问题回答链
# chain = RunnableMap({
#     "doc": lambda x: retriever.invoke(x["question"]),
#     "question": lambda x: x["question"]
# }) | prompt | llm | StrOutputParser()
# 搭建完整的RAG问答链
# 为什么用RunnableMap:需要同时生成「doc」和「question」两个变量,传给后面的提示词模板
# 内部逻辑:
#   - "doc"分支:接收用户问题,调用检索器获取相关的原始文档
#   - "question"分支:把用户问题原样传递
#   两个分支并行执行完,一起传给提示词模板,再调用大模型生成回答
# 为什么被注释:问答功能整体暂不运行,所以注释

#
# # 生成问题回答
# query = "病假的请假流程?"
# 定义测试用的用户问题
# 为什么被注释:对应问答链的测试,链被注释了,这里也一起注释

# answer = chain.invoke({"question": query})
# 调用问答链,传入问题得到回答
# 入参:字典,key为question,value为用户问题
# 返回值:大模型生成的回答字符串
# 为什么被注释:问答链整体注释,调用也一起注释

# print("-------------回答--------------")
# print(answer)
# 打印回答结果
# 为什么被注释:同上

# retrieved_docs = retriever.invoke(query)
# 直接调用检索器,检索和问题相关的原始文档
# 入参:用户问题字符串
# 返回值:检索到的原始Document列表
# 为什么被注释:属于另一种检索演示方式,后面用了更底层的分步演示,这里注释

# print("-------------检索到的文档--------------")
# print(retrieved_docs)
# 打印检索到的原始文档
# 为什么被注释:同上


# -------------------------- 6. 演示分步检索 --------------------------
sub_docs = retriever.vectorstore.similarity_search("病假的请假流程?")
# 第一步:直接调用向量库的相似度搜索,找到和问题最相似的摘要文档
# 为什么调用:演示多向量检索的「第一阶段------摘要召回」,看搜出来的摘要内容
# 函数:similarity_search() 相似度搜索
# 入参:查询字符串,这里是测试问题"病假的请假流程?"
#   入参来源:手动输入的测试问题
# 可选入参:
#   k=4:默认返回前4个最相似的结果,可以手动指定数量
#   filter:按元数据过滤结果
# 返回值:sub_docs是Document列表,每个元素是相似的摘要文档,元数据里带doc_id
# 逻辑:把查询文本转成向量,和向量库里所有摘要向量计算余弦相似度,返回最相似的结果

print(sub_docs)
# 打印所有检索到的摘要文档列表,查看完整召回结果

print("-------------检索到的文档--------------")
print(sub_docs[0])
# 打印第一个(相似度最高的)摘要文档,查看最相关的摘要内容和它的元数据

summ_id = sub_docs[0].metadata[id_key]
# 从相似度最高的摘要元数据里,取出对应的文档ID
# 作用:拿到这个摘要绑定的原文ID,下一步去字节存储里取原始文档
# 来源:sub_docs[0].metadata字典中,key为id_key(也就是'doc_id')对应的值

orig_doc = retriever.docstore.mget([summ_id])
# 第二步:根据文档ID,从字节存储里取出原始文档
# 为什么调用:演示多向量检索的「第二阶段------取原文」,拿到完整的原始文档块
# 函数:mget() ------ 批量获取(multi get)
# 入参:ID列表,这里是[summ_id],只查询一个ID
#   入参来源:上一步从摘要元数据里提取的summ_id
# 返回值:orig_doc是列表,每个元素是对应ID的原始Document对象
# 逻辑:根据doc_id去内存字节存储里查找对应的原始文档块,返回结果

print("-------------原始文档--------------")
print(orig_doc)
# 打印获取到的原始文档,查看完整的原文内容

相关推荐
IManiy2 小时前
知识点之LangGraph 中的四个核心概念:State、Node、Edge 和Checkpoint
langchain
雮尘3 小时前
LangGraph 与 LangSmith 入门教程(JS/TS 版)
前端·人工智能·langchain
veminhe4 小时前
解决了使用langchain调用聊天模型报的错
langchain
颜酱5 小时前
LangChain上手 Agent:让大模型自己调用工具解决问题
langchain
Niuguangshuo9 小时前
LangChain 学习之旅(二):用 LCEL 与解析器构建标准流水线
学习·langchain·unix
古怪今人10 小时前
Langchain PromptTemplate纯文本模板、ChatPromptTemplate对话消息模板和MessagesPlaceholder消息占位符
langchain
是上好佳佳佳呀10 小时前
【LangChain|Day02】LangChain Prompt 提示词工程笔记
笔记·langchain·prompt
喵叔哟10 小时前
Week 3 --Day 2:LangGraph 进阶
python·langchain