26.RAG进阶(Advanced RAG)-假设性问题索引

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

假设性问题索引做的事情

对文档内容(切分之后的文档),提炼出三个问题,把提炼出来的问题转成向量存到数据库,然后比对的数据就是问题,转换成问题后语意会比较短,如果我们问的问题与提炼出来的问题相似那么它就会比较精准,生成的问题是通过大模型搞的,所以问题的合适性无法控制,这种的索引使用的不是很多

代码

效果图:

python 复制代码
# 导入类型提示工具:List用来标注「字符串列表」这种数据类型,让代码更规范,也能配合Pydantic做格式校验
from typing import List

# 导入内存字节存储:在内存里以键值对形式存原始文档,key是文档ID,value是完整文档内容
from langchain_core.stores import InMemoryByteStore
# 导入Chroma向量数据库:轻量级本地向量库,存文本向量,做语义相似度搜索
from langchain_chroma import Chroma
# 导入纯文本加载器:专门读取本地.txt文本文件,把文件内容转成LangChain标准文档格式
from langchain_community.document_loaders import TextLoader
# 导入递归字符文本分割器:把长文档按语义切成小块,兼顾检索精度和上下文完整性
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 导入OpenAI格式大模型客户端:通义千问兼容OpenAI接口,可直接复用这个类
from langchain_openai.chat_models import ChatOpenAI
# 导入通义千问嵌入模型(本代码最终用了本地BGE模型,这个导入暂时没用到)
from langchain_community.embeddings.dashscope import DashScopeEmbeddings
# 导入多向量检索器:核心组件,支持「向量库存检索用的内容 + 字节库存原始内容」的两级检索
from langchain_classic.retrievers import MultiVectorRetriever
# 导入LangChain标准文档类:所有文本的统一格式,包含page_content(文本)和metadata(元数据)
from langchain_core.documents import Document
# 导入字符串输出解析器:把大模型返回的消息对象转成纯文本字符串
from langchain_core.output_parsers import StrOutputParser
# 导入聊天提示词模板:标准化给大模型的提问格式,用占位符填充动态内容
from langchain_core.prompts import ChatPromptTemplate
# 导入可运行映射:并行执行多个处理步骤,组装成字典传给后续流程
from langchain_core.runnables import RunnableMap
# 导入HuggingFace嵌入模型:加载本地嵌入模型,把文本转成数字向量
from langchain_huggingface import HuggingFaceEmbeddings

# 导入Pydantic的基础模型和字段工具:用来定义数据格式,约束大模型的输出结构
# 作用:让大模型必须返回固定格式的JSON,程序可以直接解析,不用手动处理乱码、格式错误
from pydantic import BaseModel, Field

# 导入uuid模块:生成全局唯一的随机ID,用来绑定「假设性问题」和「原始文档」
import uuid
# 导入操作系统模块:读取环境变量,避免密钥硬写在代码里
import os
# 导入环境变量加载工具:读取项目根目录的.env文件,把配置加载到系统环境变量
from dotenv import load_dotenv

# 加载.env文件里的配置(比如API密钥、接口地址)
# 为什么调用:敏感信息不写死在代码里,更安全,换环境也不用改代码
load_dotenv()


# -------------------------- 1. 初始化嵌入模型 --------------------------
# 本地嵌入模型的文件路径(BGE中文大模型,专门做文本向量化,检索效果好)
# 入参来源:手动填写本地模型存放的绝对路径,需要提前下载好模型文件
embedding_model_path = r'D:\huanjing\ai模型\BAAI\bge-large-zh-v1___5'

# 初始化嵌入模型实例
# 为什么调用:向量数据库只能存向量、算相似度,需要嵌入模型做「文本 → 数字向量」的转换
# 入参:model_name指定模型的本地路径
embeddings_model = HuggingFaceEmbeddings(
model_name=embedding_model_path
)


# -------------------------- 2. 加载并切分文档 --------------------------
# 初始化纯文本加载器,指定要读取的本地txt文件和编码格式
# 入参说明:
#   第1个参数:本地文本文件的路径,和代码同目录直接写文件名即可
#   encoding="utf-8":指定文件编码,避免中文乱码
loader = TextLoader("deepseek介绍.txt", encoding="utf-8")
# 执行加载:真正读取文件内容,返回Document对象的列表
# 为什么调用:加载器只是定义了目标文件,调用load()才会真正读取文件内容
docs = loader.load()


# 初始化递归文本分割器(设置块大小和重叠字符数)
# 为什么要切块:原始文档太长,直接向量化会丢失细节,也可能超过模型输入长度限制
# 入参说明:
#   chunk_size=1024:每个文本块的最大字符数,这里是1024个字符
#   chunk_overlap=100:相邻块的重叠字符数,避免把一句话切断,保证语义连贯
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
# 批量切分所有文档,把长文档切成小块
# 入参:docs ------ 前面加载好的原始文档列表
# 返回值:切割后的小块Document列表,每个块都保留原文件的元数据
docs = text_splitter.split_documents(docs)


# -------------------------- 3. 初始化大语言模型 --------------------------
# 初始化通义千问大模型(通过OpenAI兼容接口调用)
# 入参说明:
#   model:模型名称,qwen-plus是通义的主力通用模型
#   api_key:API密钥,从环境变量读取,来自.env文件
#   base_url:API接口地址,从环境变量读取
# 入参来源:os.getenv()读取前面load_dotenv加载的环境变量
llm = ChatOpenAI(
model="qwen-plus",
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv("DASHSCOPE_BASE_URL")
)


# -------------------------- 4. 定义结构化输出数据模型 --------------------------
# 定义假设性问题的数据模型,继承Pydantic的BaseModel
# 为什么要定义这个类:约束大模型的输出格式,确保它返回标准JSON,程序能直接解析
# 这是「结构化输出」功能的核心:大模型会严格按照这个类的字段和描述来返回内容
class HypotheticalQuestions(BaseModel):
"""生成假设性问题"""
# 定义questions字段:类型是字符串列表
# Field的作用:给字段加约束和描述
#   ... :表示这个字段是必填项,不能缺失
#   description:字段的文字描述,会传给大模型,告诉大模型这个字段应该放什么内容
questions: List[str] = Field(..., description="List of questions")


# -------------------------- 5. 构建假设性问题生成链 --------------------------
# 创建提示词模板:告诉大模型任务要求、输出格式、示例
# 核心任务:给一段文档,生成3个用户可能会问的问题
# 为什么要生成假设性问题:这是HyDE(假设文档嵌入)的优化思路
#   普通检索是「用户问题 → 匹配原文片段」,但用户提问句式和原文书面句式差异大,容易搜不准
#   提前给每个文档块生成用户口吻的问题,检索时「用户问题 → 匹配假设问题」,句式更接近,召回更准
prompt = ChatPromptTemplate.from_template(
"""请基于以下文档生成3个假设性问题(必须使用JSON格式):
{doc}

要求:
   1. 输出必须为合法JSON格式,包含questions字段
   2. questions字段的值是包含3个问题的数组
   3. 使用中文提问
   示例格式:
   {{
       "questions": ["问题1", "问题2", "问题3"]
   }}"""
)
# 注意:示例里的{{ }}是转义写法,因为模板本身用{}做占位符,两个{{才会输出一个{


# 创建「假设性问题生成链」(LangChain LCEL链式写法)
# 执行流程:输入Document对象 → 提取文本 → 组装提示词 → 调用大模型 → 解析成结构化对象 → 提取问题列表
chain = (
       # 第一步:接收Document对象x,提取它的文本内容,赋值给模板里的{doc}占位符
       {"doc": lambda x: x.page_content}
       # 第二步:把文本填充进提示词模板,生成完整的提问
       | prompt
       # 第三步:调用大模型,并开启结构化输出
       # with_structured_output的作用:让大模型严格按照HypotheticalQuestions的格式返回
       # 并且自动把返回的JSON字符串解析成HypotheticalQuestions类的实例对象
       | llm.with_structured_output(HypotheticalQuestions)
       # 第四步:从结构化对象里提取出questions字段(也就是问题列表)
       # x是上一步返回的HypotheticalQuestions对象,x.questions就是字符串列表
       | (lambda x: x.questions)
)

# 在单个文档上调用链,测试输出效果(已注释)
# 为什么注释:用来调试单个文档的生成效果,批量生成前先验证格式是否正确
# print(chain.invoke(docs[0]))


# -------------------------- 6. 批量生成所有文档的假设性问题 --------------------------
# 批量执行问题生成链,给所有文档块一次性生成假设性问题
# 函数:chain.batch() ------ 批量执行链
# 为什么调用:文档块数量多,逐个调用太慢,用批量并行处理提升效率
# 入参说明:
#   第1个参数:docs ------ 切分后的所有文档块列表
#       入参来源:前面split_documents得到的文档块集合
#   第2个参数:配置字典 {"max_concurrency": 5}
#       max_concurrency:最大并发数,同时最多发5个请求给大模型
#       入参来源:手动配置,根据API限流规则调整,避免并发太高被拒绝
# 返回值:hypothetical_questions是二维列表
#   外层长度=文档块数量,内层每个元素是对应文档生成的3个问题组成的列表
hypothetical_questions = chain.batch(docs, {"max_concurrency": 5})
# 打印生成结果,查看是否符合预期
print(hypothetical_questions)


# -------------------------- 7. 初始化存储与多向量检索器 --------------------------
# 初始化Chroma向量数据库
# 作用:存储「假设性问题」的向量,用户提问时做相似度搜索
# 入参说明:
#   collection_name="hypo-questions":集合名称,专门存假设性问题的向量
#   embedding_function=embeddings_model:嵌入模型实例,自动做文本向量化
vectorstore = Chroma(
   collection_name="hypo-questions", embedding_function=embeddings_model
)

# 初始化内存字节存储
# 作用:存储原始的文档块内容,用ID做key
# 为什么需要:向量库存问题做检索,字节库存原文做回答,两级结构
store = InMemoryByteStore()

# 定义元数据里的ID键名,值为"doc_id"
# 作用:统一元数据里的字段名,用来存放文档唯一ID,实现问题和原文的绑定
id_key = "doc_id"

# 配置多向量检索器
# 核心逻辑:向量库存「假设性问题」做搜索,字节库存「原始文档」做返回
# 入参说明:
#   vectorstore:向量数据库实例,存问题向量
#   byte_store:字节存储实例,存原始文档
#   id_key:元数据中存储文档ID的键名,用来关联问题和原文
retriever = MultiVectorRetriever(
   vectorstore=vectorstore,
   byte_store=store,
   id_key=id_key,
)


# -------------------------- 8. 数据入库(问题+原文绑定) --------------------------
# 给每个原始文档块生成一个全局唯一的随机ID
# 为什么生成ID:给每个文档块分配专属编号,让多个假设问题都指向同一个原始文档
# 逻辑:docs有多少个元素,就生成多少个ID,顺序和docs一一对应
doc_ids = [str(uuid.uuid4()) for _ in docs]


# 将生成的问题转换为带元数据的文档对象
# 核心逻辑:每个文档块生成3个问题,这3个问题的元数据都绑定同一个文档ID
# 这样用户搜中任意一个问题,都能通过ID找到对应的原始文档
question_docs = []
# 遍历所有生成的问题,i是文档块的索引,question_list是对应文档的3个问题
for (i, question_list) in enumerate(hypothetical_questions):
   # 把当前文档的3个问题,逐个转成Document对象,扩展到总列表里
   question_docs.extend(
       # 列表推导式:每个问题s都创建一个Document
       # page_content是问题文本,metadata里存入对应原始文档的ID
       [Document(page_content=s, metadata={id_key: doc_ids[i]}) for s in question_list]
   )


# 把所有假设性问题文档存入向量数据库
# 为什么调用:只有把问题存进向量库,后续才能做相似度检索
# 入参:question_docs ------ 带元数据的问题Document列表
# 内部逻辑:自动提取每个问题的文本,转成向量,和元数据一起存入Chroma
retriever.vectorstore.add_documents(question_docs)

# 把原始文档批量存入字节存储
# 函数:mset() 批量设置键值对(multi set)
# 为什么调用:把原始文档和ID绑定存入,检索到问题ID后,就能取出对应原文
# 入参:列表,每个元素是(文档ID, 原始文档)的元组
#   入参来源:
#       - doc_ids:文档ID列表
#       - docs:原始文档块列表
#       - zip(doc_ids, docs):按顺序配对成元组
#       - list(...):转成列表格式,符合mset的入参要求
# 逻辑:key是文档ID,value是对应的原始Document对象,批量存入内存存储
retriever.docstore.mset(list(zip(doc_ids, docs)))


# -------------------------- 9. 检索功能测试 --------------------------
# 定义测试查询问题
query = "deepseek受到哪些攻击?"

# 直接调用向量库的相似度搜索,查找最相似的假设性问题
# 为什么调用:演示检索的第一阶段,看看用户问题匹配到了哪些假设问题
# 入参:用户查询字符串
# 返回值:sub_docs是Document列表,每个元素是匹配到的问题文档,元数据带doc_id
sub_docs = retriever.vectorstore.similarity_search(query)
# 打印匹配到的问题文档
print(sub_docs)


# -------------------------- 10. 构建完整RAG问答链 --------------------------
# 创建问答提示词模板
# 两个占位符:{doc}放检索到的参考文档,{question}放用户问题
prompt1 = ChatPromptTemplate.from_template("根据下面的文档回答问题:\n\n{doc}\n\n问题: {question}")

# 生成完整的问题回答链(RAG全流程)
# 执行流程:输入用户问题 → 检索相关文档 → 组装提示词 → 调用大模型 → 输出纯文本回答
# RunnableMap的作用:并行执行两个分支,同时生成doc和question两个变量,传给后面的提示词
chain = RunnableMap({
   # doc分支:接收用户问题,调用检索器获取对应的原始文档
   # 检索器内部流程:搜向量库的假设问题 → 拿doc_id → 去字节库取原始文档
   "doc": lambda x: retriever.invoke(x["question"]),
   # question分支:把用户问题原样传递给提示词
   "question": lambda x: x["question"]
}) | prompt1 | llm | StrOutputParser()


# -------------------------- 11. 执行问答并打印结果 --------------------------
# 调用问答链,传入用户问题,得到最终回答
# 入参:字典格式,key为question,value是用户的问题字符串
# 返回值:大模型基于检索到的文档生成的回答文本
answer = chain.invoke({"question": query})
print("-------------回答--------------")
print(answer)

相关推荐
闵孚龙1 小时前
动态图机制:为什么 PyTorch 调试起来更舒服
人工智能·pytorch·python
甲维斯2 小时前
还要啥Codex!DeepSeek接入Zcode远程连接!
人工智能
百胜软件@百胜软件2 小时前
百胜软件亮相“AI消费新生活”主题日活动,AI智能运营平台入选市级案例征集
人工智能·生活·零售数字化·数智中台·珠宝行业
专注搞钱3 小时前
GPT-4o写设备Recipe:从3小时到10分钟
数据库·人工智能·gpt·半导体
闻道参看3 小时前
贝芯宠AI灵兽 ELFVET 大模型聚焦临床应用,强化宠物诊疗综合能力
人工智能·宠物
MartinYeung53 小时前
[论文学习]重新思考大型语言模型忘却目标:梯度视角与超越
人工智能·学习·语言模型
财经资讯数据_灵砚智能3 小时前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年6月14日
大数据·人工智能·python·ai·信息可视化·自然语言处理·灵砚智能
m0_380167144 小时前
加密货币价格 API、市场数据 API 与 分析 API 有什么区别?
人工智能·ai·区块链
zyplayer-doc4 小时前
企业知识库安全与权限管理完全指南:从加密到审计的六层防护
人工智能·安全·pdf·编辑器·创业创新