内容参考于:图灵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)

