惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Heimdal Security Blog
A
Arctic Wolf
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Simon Willison's Weblog
Simon Willison's Weblog
L
LINUX DO - 热门话题
MongoDB | Blog
MongoDB | Blog
T
Threat Research - Cisco Blogs
D
Docker
爱范儿
爱范儿
T
Tenable Blog
C
Check Point Blog
B
Blog
C
Cisco Blogs
Vercel News
Vercel News
The Cloudflare Blog
T
Threatpost
NISL@THU
NISL@THU
T
Tor Project blog
V2EX - 技术
V2EX - 技术
P
Palo Alto Networks Blog
Application and Cybersecurity Blog
Application and Cybersecurity Blog
T
Tailwind CSS Blog
G
GRAHAM CLULEY
P
Privacy & Cybersecurity Law Blog
SecWiki News
SecWiki News
博客园 - 司徒正美
S
Security @ Cisco Blogs
GbyAI
GbyAI
S
Secure Thoughts
Microsoft Security Blog
Microsoft Security Blog
The Register - Security
The Register - Security
Recorded Future
Recorded Future
Cloudbric
Cloudbric
Webroot Blog
Webroot Blog
N
News and Events Feed by Topic
Y
Y Combinator Blog
博客园_首页
T
Troy Hunt's Blog
The Hacker News
The Hacker News
雷峰网
雷峰网
Google DeepMind News
Google DeepMind News
U
Unit 42
AWS News Blog
AWS News Blog
PCI Perspectives
PCI Perspectives
V
Visual Studio Blog
博客园 - 聂微东
有赞技术团队
有赞技术团队
酷 壳 – CoolShell
酷 壳 – CoolShell

博客园 - Yaopengfei

第五节:Claude Code的用法实操汇总 第十五节:LlamaIndex框架使用2() 第十四节:LlamaIndex框架介绍和入门1(基本用法、提示词、节点等) 第三节:Trae的基本使用 和 Skill的详细用法 第二节:Python基础2 第十三节:FastApi入门以及AI智能体案例落地实操 第二节:GitHub Copilot 用法(基于VSCode/VS、添加DeepSeek/千问等任意模型) 第一节:AI工具总览(Copilot、Cursor、Claude Code、OpenCode、Codex、Trae等) 第一节:Python基础1(数据类型、命名规范、输入输出、运算符、类型转换等) 第十二节:MinerU处理复制PDF 和 综合性金融案例实战 第十一节:多检索查询、混合检索(多检索+RRF重排)、检索后优化(文档压缩) 第九节:RAG进阶和Advanced RAG简介 博文阅读密码验证 - 博客园 第七节:LangChain框架Chain链和Agent代理详解 第六节:LangChain框架Model和数据检索详解 第五节:LangChain框架简介和快速入门(模型、模板、解释器、向量、RAG、代理) 第四节:补充pip相关指令大全 第三节:RAG基础(概念、工作流程、文档分块、向量和Embedding、RAG案例等等) 博文阅读密码验证 - 博客园 第一节:Python相关环境安装和配置(Python、PyCharm、Anaconda) 第三十六节:EFCore10.0新增功能和中断性变更 第七节:框架版本大升级(CoreMvc10.x + EFCore10.x) 第五十二节:Core10.0中OpenApi自定义文档(Swagger) 博文阅读密码验证 - 博客园 第三节:C#13、C#14新语法(数字字符串比较、Null分配、扩展成员新写法等等) 第二节:如何理解Embedding以及基于内存库简单实操
第十节:预检索-索引优化(摘要、父子、假设问题、元数据、混合索引)
Yaopengfei · 2026-05-09 · via 博客园 - Yaopengfei

一. 摘要索引

1. 功能

  实现了基于摘要检索的高级 RAG 智能问答,通过生成文档摘要用于向量检索、原始全文用于回答,大幅提升检索准确率与召回率,支持 Chroma 本地持久化,可根据文档原文精准回答问题。解决了处理大量文档时,如何快速准确地找到所需信息这一痛点。

2. 特点

  检索精度高、语义匹配强、回答更完整、不编造内容、单库持久化无依赖报错、可落地生产、支持批量高速构建、重启自动加载向量库。

3. 核心流程

(一)、存储阶段(入库)

  ① 原始文档(Word + 网页)→ 切分为标准大小的文档块

  ② 大模型为每个文档块生成精简摘要

  ③ vectorstore(Chroma):

    A. 摘要文本做向量化(用于检索匹配) Document(page_content=summary)  自动向量化了

    B. 原始全文明文存在 metadata 中(用于最终回答)

  ④ 所有的内容持久化存入本地 Chroma

  ⑤ 通过 doc_id 唯一标识每个文档

(二)、问答阶段(检索)

  ① 用户提问 → 转换为向量

  ② 遍历 vectorstore,匹配相似度最高的摘要

  ③ 直接从摘要对应的 metadata 中取出原始全文

  ④ 原始全文输入大模型

  ⑤ 大模型基于原文生成最终回答

3. 重点补充

(1) 如何存储?

new_doc = Document(
    page_content=summary,         # 👈 这是 **摘要**
    metadata={
        "original_content": 原文,  # 👈 明文存在 metadata
        "doc_id": uuid
    }
)

当执行入库存储的时候,即 db = Chroma.from_documents(final_docs, embeddings, persist_directory=CHROMA_PATH)干了三件事

 ① 把 page_content 摘要 当作明文入库保存

 ② 把 page_content 摘要 丢给 Embedding 模型 → 生成向量 入库保存

 ③ 把 metadata 里的原文原样明文存,不做任何向量化

记住结论:构建的Documnet格式存储的时候:适用于任何场景

  page_content:明文 + 向量 双存

  metadata:只存明文,不存向量

(2) 为什么要存摘存了向量还要存明文?

  ① 检索命中后,你可以看到匹配的摘要是什么

  ② Chroma 底层必须保留原始文本,向量只是用来做相似度匹配

  ③ 向量只能用来算相似度,不能直接读给人看、也不能给 LLM 用

4. 代码分享

import os
import uuid
from pathlib import Path
from langchain_chroma import Chroma
from langchain_community.document_loaders import UnstructuredWordDocumentLoader, WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
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

# 你的模型配置
from model_config import llm, get_local_embeddings

# ===================== 1. 配置 =====================
CHROMA_PATH = "./full_rag_chroma"


# ===================== 2. 获取检索器 =====================
def get_retriever():
    embeddings = get_local_embeddings()

    # ===================== ✅ 判断本地库是否存在 =====================
    if Path(CHROMA_PATH).exists() and len(os.listdir(CHROMA_PATH)) > 0:
        print("✅ 加载本地向量库")
        db = Chroma(persist_directory=CHROMA_PATH, embedding_function=embeddings)
        return db.as_retriever(search_kwargs={"k": 2})

    # ===================== 第一次运行:创建库 =====================
    print("🔍 首次运行,构建向量库...")

    # 1. 加载文档 和 网页
    url = "https://review.jschina.com.cn/suwangyuanchuang/202604/t20260424_s69eae060e4b05172eb1e94cd.shtml"
    loaders = [UnstructuredWordDocumentLoader("人事管理流程.docx"), WebBaseLoader(url)]
    raw_docs = []
    for loader in loaders:
        raw_docs.extend(loader.load())

    # 2. 切分
    splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
    split_docs = splitter.split_documents(raw_docs)

    # # 3. 生成摘要 【逐一生成】
    # summary_chain = ChatPromptTemplate.from_template("总结:{page_content}") | llm | StrOutputParser()
    #
    # # 4. 摘要做检索文本,原文存入 metadata
    # final_docs = []
    # for doc in split_docs:
    #     summary = summary_chain.invoke({"page_content": doc.page_content})
    #     new_doc = Document(page_content=summary, metadata={"original_content": doc.page_content, "doc_id": str(uuid.uuid4())})
    #     final_docs.append(new_doc)
    #
    # print(final_docs)
    # quit()

    # 3. 批量生成摘要(🚀 速度极快)
    print("📝 开始批量生成文档摘要...")
    summary_prompt = ChatPromptTemplate.from_template("总结下面的内容:\n{page_content}")
    summary_chain = summary_prompt | llm | StrOutputParser()

    # 🚀🚀🚀 批量并发生成,速度提升 8 倍
    summaries = summary_chain.batch([{"page_content": doc.page_content} for doc in split_docs], max_concurrency=8)

    # 4. 组装最终文档(摘要 + 原文)
    final_docs = []
    for summary, doc in zip(summaries, split_docs):
        new_doc = Document(page_content=summary, metadata={"original_content": doc.page_content, "doc_id": str(uuid.uuid4())})
        final_docs.append(new_doc)

    # 5. 存入 Chroma(干3件事情)
    # ① 把 page_content 摘要 当作明文入库保存
    # ② 把 page_content 摘要 丢给 Embedding 模型 → 生成向量 入库保存
    # ③ 把 metadata 里的原文原样明文存,不做任何向量化
    db = Chroma.from_documents(final_docs, embeddings, persist_directory=CHROMA_PATH)

    print("✅ 向量库构建完成!")
    return db.as_retriever(search_kwargs={"k": 2})


# ===================== 3. RAG 问答链 =====================
def build_rag_chain():
    retriever = get_retriever()
    prompt = ChatPromptTemplate.from_template(
        """
        请根据原文回答问题:
        {context}        
        问题:{question}
        """
    )

    def format_docs(docs):
        return "\n".join([d.metadata["original_content"] for d in docs])

    rag_chain = (
        RunnableMap(
            {
                "context": lambda x: format_docs(retriever.invoke(x["question"])),
                "question": lambda x: x["question"],
            }
        )
        | prompt
        | llm
        | StrOutputParser()
    )
    return rag_chain


# ===================== 运行 =====================
if __name__ == "__main__":
    chain = build_rag_chain()
    ans = chain.invoke({"question": "病假的请假流程?"})
    print("回答:\n", ans)

View Code

二. 父子索引

1. 功能

   实现了基于父子分块检索的高级 RAG 智能问答,通过子文档碎片做向量检索、父文档完整原文做回答,兼顾检索精度与上下文完整性,支持内存 / MySQL 两种持久化存储,可根据网页 / 文档原文精准回答问题。解决了长文本分块后信息碎片化、检索丢失上下文、回答不完整的痛点

2. 特点

  检索更精准、上下文更完整、不丢失段落信息、不编造内容、支持持久化存储、可调试、可落地生产、支持大文件长文本处理。

  ①子文档:只用于向量检索匹配,不直接给模型看

  ②父文档:不向量化,只用于给大模型提供完整上下文

  ③检索用子块,回答用父块 → 这是父子分块 RAG 的核心思想

3. 核心流程

 (一)、存储阶段(入库)

    ① 原始网页文本 → 分割为父文档(1000 字,完整长文本)

    ② 父文档 → 二次分割为子文档(400 字,短片段)

    ③ vectorstore:

        A.存储「子文档文本 + 子文档向量 Embedding」(用于检索匹配)

        B.子文档绑定父文档 ID

    ④ docstore(内存 / MySQL):

        A. 存储「父文档完整原文」(纯文本,无向量)

        B. 通过 doc_id 与子文档关联

    ⑤ 自动完成父子文档关联与存储

 (二)、问答阶段(检索)

    ① 用户提问 → 转换为向量

    ② 遍历 vectorstore,匹配相似度最高的子文档碎片

    ③ 提取子文档中绑定的父文档 ID

    ④ 依据 doc_id 去 docstore 调取完整父文档原文

    ⑤ 完整原文输入大模型

    ⑥ 大模型基于完整上下文生成回答

4. 重点分析

(1). 为什么代码中没有看到父子文档通过id绑定的代码?

  这句代码retriever.add_documents(raw_docs)内部的ParentDocumentRetriever自动干了五件事

    ① 把原文切成父块

    ② 给每个父块生成一个 uuid

    ③ 把父块切成子块

    ④ 给每个子块自动加上 metadata: {"doc_id": 父块 ID}

    ⑤ 子块存入向量库vectorstore,父块存入 docstorestore

 格式如下:

   #父文档(存在 docstorestore)

   父文档1: id=uuid1, content="完整长文本"

  #子文档(存在 vectorstore)

   子文档1: page_content="碎片1", metadata={"doc_id": uuid1}

5. 代码分享

# ===================== 导入依赖包 =====================
from model_config import llm, get_local_embeddings
from langchain_community.document_loaders import WebBaseLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_chroma import Chroma
from langchain_classic.retrievers import ParentDocumentRetriever
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableMap
from langchain_core.output_parsers import StrOutputParser
from mysql_store import MySQLStore
from langchain_core.stores import InMemoryByteStore

# ===================== 1. 全局配置 =====================
# 要抓取的目标网页(必须保证可正常访问)
TARGET_URL = "https://news.pku.edu.cn/mtbdnew/15ac0b3e79244efa88b03a570cbcbcaa.htm"

# 向量库集合名称
CHROMA_COLLECTION = "split_parents"

# 父子分块大小配置
# 父文档:大段落,用于最终回答
PARENT_CHUNK_SIZE = 1000
# 子文档:小碎片,用于向量检索
CHILD_CHUNK_SIZE = 400

# ===================== 2. 加载网页数据 =====================
loader = WebBaseLoader(TARGET_URL)
raw_docs = loader.load()

# ===================== 3. 定义文档分块器 =====================
# 父文档分割器
parent_splitter = RecursiveCharacterTextSplitter(chunk_size=PARENT_CHUNK_SIZE)

# 子文档分割器
child_splitter = RecursiveCharacterTextSplitter(chunk_size=CHILD_CHUNK_SIZE)

# ===================== 4. 初始化向量库与嵌入模型 =====================
embedding_model = get_local_embeddings()
# 存放:文本的 embedding 向量 + 子文本(小碎片)
vectorstore = Chroma(collection_name=CHROMA_COLLECTION, embedding_function=embedding_model)


# ===================== 6. 初始化文档存储 =====================
# 方式1:内存存储(重启丢失)
# 专门用来存 “完整原文”, 检索完成后,把完整原文拿给大模型读
docstore = InMemoryByteStore()

# 方式2:MySQL持久化存储(如需开启,注释掉上面一行,取消注释下面内容)
# 需要事先建立好langchain_db库
# docstore = MySQLStore(host="xxx", user="root", password="xxx", database="langchain_db")

# ===================== 7. 初始化父子分块检索器 =====================
retriever = ParentDocumentRetriever(
    vectorstore=vectorstore,
    docstore=docstore,
    parent_splitter=parent_splitter,
    child_splitter=child_splitter,
    search_kwargs={"k": 1},
)

# 下面这句话,干了5件事情
# ① 把原文切成父块
# ② 给每个父块生成一个 uuid
# ③ 把父块切成子块
# ④ 给每个子块自动加上 metadata: {"doc_id": 父块 ID}
# ⑤ 子块存入向量库vectorstore,父块存入 docstorestore
retriever.add_documents(raw_docs)
print("父文档存储数量:", len(list(docstore.yield_keys())))

# ===================== 8. 构建 RAG 问答链 =====================
prompt_template = """
    请根据下面给出的上下文来回答问题:
    {context}
    问题: {question}
"""

# 构建提示词模板
prompt = ChatPromptTemplate.from_template(prompt_template)

# 构建完整问答链
rag_chain = (
    RunnableMap({"context": lambda x: retriever.invoke(x["question"]), "question": lambda x: x["question"]})
    | prompt
    | llm
    | StrOutputParser()
)

# ===================== 9. 执行问答 =====================
print("\n------------ 模型回答 ------------------------")
question = "天才AI少女是谁?"
response = rag_chain.invoke({"question": question})
print(response)

# ===================== 10. 调试代码(查看检索效果) =====================
# # 作用:直接从向量库检索【子文档碎片】,查看检索精度
# sub_docs = vectorstore.similarity_search("天才AI少女是谁?", k=1)
# print("\n------------ 检索到的子文档(用于匹配) ------------------------")
# print(sub_docs)
#
# # 作用:通过检索器获取【完整父文档】,查看最终给模型的上下文
# ret_docs = retriever.invoke("天才AI少女是谁?")
# print("\n------------ 检索到的父文档(用于回答) ------------------------")
# print(ret_docs)

View Code

三. 假设性问题索引

1. 功能

    实现了基于假设性问题索引的高级多向量 RAG 智能问答。通过大模型为原文生成模拟人类提问的假设问题,以假设问题做向量检索、 原始文档做回答,大幅解决原文语义偏僻、专业术语多、用户提问表达方式和原文不一致导致检索不到的问题,支持内存存储,可根据本地文档原文精准回答问题。

2. 特点

   检索精度极高、适配人类自然口语提问、解决原文难检索问题、不编造内容、本地文件运行、批量生成效率高、结构清晰、可调试、可直接落地使用

    - 假设问题 page_content:明文 + 向量 双存(存入Chroma,用于检索)

    - metadata:只存明文doc_id,不向量化

    - 原始文档:只明文存入内存,不向量化、不进向量库

3. 核心流程

(一)、存储阶段(入库)

    ① 加载本地TXT文档 → 切分为标准大小的原始文档块

    ② 大模型为每个文档块生成 3 个假设性问题

    ③ vectorstore:

      A. 存储「假设性问题明文 + 假设性问题向量 + 绑定原始文档doc_id」

      B. 作用:专门用于向量相似度检索匹配

    ④ store(内存):

      A. 存储「未修改的原始文档明文」

      B. 作用:专门用于最终给大模型生成回答

    ⑤ 人工循环绑定:假设问题通过metadata携带doc_id,一对一关联对应原始文档块

 (二)、问答阶段(检索)

    ① 用户提问 → 问题转换为向量

    ② 遍历向量库,优先匹配相似度最高的假设性问题(不是原文)

    ③ 提取假设问题metadata中绑定的原始文档doc_id

    ④ 根据doc_id去内存store中调取完整原始原文块

    ⑤ 原始原文送入大模型,严格依据原文生成最终答案

4. 重点分析

(1). 为什么要用假设问题检索?

  ① 原文晦涩、专业词多、生硬,人类很少这样提问;

  ② 假设问题贴近人类口语、通俗易懂,用户提问更容易命中;

  ③ 向量匹配假设问题,匹配成功率远高于直接匹配原文;

  ④ 检索用问题,回答用原文,兼顾检索准确率和回答真实性。

5. 代码分享

# ===================== 导入依赖包 =====================
from model_config import llm, get_local_embeddings
from typing import List
from langchain_core.stores import InMemoryByteStore
from langchain_chroma import Chroma
from langchain_community.document_loaders import TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_classic.retrievers import MultiVectorRetriever
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
import uuid
from pydantic import BaseModel, Field


# ===================== 1. 加载并切分本地文档 =====================
# 加载本地txt文本数据
loader = TextLoader("deepseek介绍.txt", encoding="utf-8")
raw_docs = loader.load()

# 文档分块:将长文本切分为1024字符的片段
text_splitter = RecursiveCharacterTextSplitter(chunk_size=1024, chunk_overlap=100)
split_docs = text_splitter.split_documents(raw_docs)


# ===================== 2. 结构化输出:为每个文档生成假设性问题 =====================
# 结构化输出模型:限定输出格式为 问题列表
class HypotheticalQuestions(BaseModel):
    questions: List[str] = Field(..., description="假设性问题列表")


# 提示词模板:要求根据文档内容生成3个规范JSON格式的问题
prompt = ChatPromptTemplate.from_template(
    """请基于以下文档生成3个假设性问题(必须使用JSON格式):
    {doc}
    要求:
    1. 输出必须为合法JSON格式,包含questions字段
    2. questions字段的值是包含3个问题的数组
    3. 使用中文提问
    示例格式:
    {{
        "questions": ["问题1", "问题2", "问题3"]
    }}"""
)

# 构建问题生成链:输入文档 → 生成问题 → 结构化输出
question_chain = (
    {"doc": lambda x: x.page_content} | prompt | llm.with_structured_output(HypotheticalQuestions) | (lambda x: x.questions)
)

# 批量并发生成问题(提升速度)
hypothetical_questions = question_chain.batch(split_docs, {"max_concurrency": 5})

# print(hypothetical_questions)  #[[question1,question2,question3],[]]
# quit()


# ===================== 3. 多向量检索:存储与关联数据 =====================
# 向量库:存储【生成的假设问题】的向量(用于检索匹配)
embeddings_model = get_local_embeddings()
vectorstore = Chroma(collection_name="hypo-questions", embedding_function=embeddings_model)

# 文档存储:内存存储【原始文档明文】(用于最终回答)
# 基于内存
store = InMemoryByteStore()
id_key = "doc_id"

# 初始化多向量检索器
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    byte_store=store,
    id_key=id_key,
)

# 为每个原始文档生成唯一ID
doc_ids = [str(uuid.uuid4()) for _ in split_docs]

# 把生成的问题封装成Document,绑定对应原始文档ID
question_docs = []
for i, question_list in enumerate(hypothetical_questions):
    for question in question_list:
        question_docs.append(Document(page_content=question, metadata={id_key: doc_ids[i]}))

# 向量库:存储「假设性问题明文 + 假设性问题向量 + 关联文档doc_id」
# page_content = "deepseek受到哪些攻击?" 和 metadata = { "doc_id": "uuid-123456" } 都是明文存储,
# 同时对 page_content(假设问题)做 embedding,再存一份
retriever.vectorstore.add_documents(question_docs)
# 文档库:存入【原始文档】(通过doc_id关联)
retriever.docstore.mset(list(zip(doc_ids, split_docs)))


# ===================== 4. 构建RAG问答链并执行 =====================
# 问答提示词模板
answer_prompt = ChatPromptTemplate.from_template("根据下面的文档回答问题:\n\n{doc}\n\n问题: {question}")

# 构建完整问答链
rag_chain = (
    RunnableMap({"doc": lambda x: retriever.invoke(x["question"]), "question": lambda x: x["question"]})
    | answer_prompt
    | llm
    | StrOutputParser()
)

# 执行提问
query = "deepseek受到哪些攻击?"
result = rag_chain.invoke({"question": query})
# 输出结果
print("回答:")
print(result)

# 调试
# sub_docs = retriever.vectorstore.similarity_search(query, k=2)  # 寻找相似性问题
# print(sub_docs)

View Code

四. 元数据索引

1. 功能

    让大模型自动理解用户问题中的「过滤条件」,先按元数据精准过滤,再做语义检索,实现更精准的智能检索, 解决传统向量检索无法精准筛选结构化字段的痛点

2. 核心流程

    用户提问 → 大模型解析 → 元数据过滤 → 语义检索 → 返回结果

  ① 用户提问: 作者A发布的一篇论文

  ② 大模型解析【唯一的一次调用大模型】

    大模型根据你定义的元数据字段,自动拆解为json格式

    {  "query": "论文", "filter": { "author": "A" }, "limit": 1    }

  ③ 元数据过滤(本地执行,不调用模型)

    在所有文档中过滤出:author = A 的文档

  ④ 语义检索(向量匹配)

    在过滤后的结果里,检索与「论文」最相似的内容

  ⑤ 返回最终结果

    返回最匹配的 1 条文档

3. 重点分析

 元数据自查询检索 LLM 调用次数

 总共调用:1 次

 调用位置:解析用户问题,自动生成 元数据过滤条件(filter) + 语义检索词(query)

 后续步骤:向量检索、过滤、排序 → 全部本地执行,不调用模型

4. 代码分享

from langchain_chroma import Chroma
from meta_data import docs, metadata_field_info
from model_config import llm, get_local_embeddings
from langchain_classic.retrievers.self_query.base import SelfQueryRetriever

# ===================== 1. 配置 =====================
# 文档内容描述(告诉大模型:这些文档是什么内容)
document_content_description = "科技文章简介"

# 向量库 + 嵌入模型
embeddings_model = get_local_embeddings()
# 下面这句话干了3件事情
# ① 读取 docs 中的 page_content(正文),保存明文
# ② 调用 embeddings_model 嵌入模型 对page_content文本生成向量
# ③ 原文明文 + 向量 + metadata 元数据 全部存入 Chroma 向量库
vectorstore = Chroma.from_documents(docs, embeddings_model)

# ===================== 2. 创建自查询检索器 =====================
retriever = SelfQueryRetriever.from_llm(
    llm=llm,  # 大模型
    vectorstore=vectorstore,  # 向量库
    document_contents=document_content_description,  # 文档描述
    metadata_field_info=metadata_field_info,  # 元数据字段
    enable_limit=True,  # 允许限制返回数量
    verbose=True,
)

# ===================== 3. 测试检索 =====================
if __name__ == '__main__':
    # 测试1:作者A的文章
    # res1 = retriever.invoke('作者A发布的一篇论文')   #无数据 (作者、论文导致的)
    # res1 = retriever.invoke("我想了解一篇评分在9分以上的文章")
    # res1 = retriever.invoke("2024年发布的文章")
    # res1 = retriever.invoke("A的文章,且是2025年发布的")
    res1 = retriever.invoke('A发布的一篇论文')
    for doc in res1:
        print(doc.page_content, " | 元数据:", doc.metadata)

    # 测试2:作者B在2024年发布的文章
    # res1 = retriever.invoke("作者B在2024年发布的文章")

    # 测试3:评分9分以上的文章
    # res1 = retriever.invoke("我想了解一篇评分在9分以上的文章")

View Code

五. 混合索引

1. 功能

  实现了BM25关键词检索 + 向量语义检索融合的混合索引RAG。同时保留传统关键词精准匹配能力与向量语义理解能力,通过权重加权融合排序,弥补单一检索缺陷,大幅度提升检索稳定性与准确率,适合口语模糊、关键词混杂、语义模糊的用户提问。

2. 特点

双检索互补、兼顾关键词+语义、抗干扰能力强、检索鲁棒性高、权重可自由调节、逻辑简单易懂、无复杂算法、落地简单、适合通用知识库

3. 检索原理

(1) 向量检索

  理解问题含义、匹配语义相似的内容。

  优点:语义理解强、模糊提问也能匹配。

  缺点:精准关键词匹配弱、容易跑偏。

(2) BM25关键词检索

  基于词频统计、纯文本字面严格匹配关键词。

  优点:精准命中关键词、专业词汇不会丢失。

  缺点:完全不理解语义,只能字面匹配。

(3) 混合检索

  融合向量检索与BM25关键词检索,双通路并行检索、加权合并重排。

  优点:同时具备语义理解+关键词精准匹配能力,抗干扰性强、检索鲁棒性高,弥补单一检索缺陷。

  缺点:权重需要人工调优,不合理权重会降低检索效果。

补充特有规则:

    ① 同时执行 向量检索 + BM25 检索;

    ② 各自返回 top-k 文档;

    ③ 按权重加权融合排序,示例权重 weights=[0.5, 0.5](BM25权重0.5,向量权重0.5);

    ④ 去重排序后返回最优结果给大模型回答;

    ⑤ 权重调优:关键词要求高 → 调大BM25权重;语义模糊 → 调大向量权重。

4. 其它

 - 检索阶段:0次大模型调用(纯本地算法)。

- 生成回答:1次大模型调用。

5. 代码分享

# ===================== 导入依赖包 =====================
from model_config import llm, get_local_embeddings
from langchain_community.document_loaders import TextLoader
from langchain_chroma import Chroma
from langchain_community.retrievers import BM25Retriever
from langchain_classic.retrievers import EnsembleRetriever
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableMap
from langchain_text_splitters import RecursiveCharacterTextSplitter

# 待检索问题
question = "社会影响"

# ===================== 1. 加载并切分文档 =====================
# 加载本地文本数据
loader = TextLoader("deepseek介绍.txt", encoding="utf-8")
raw_docs = loader.load()
# 文本分块
text_splitter = RecursiveCharacterTextSplitter(chunk_size=512, chunk_overlap=50)
split_docs = text_splitter.split_documents(raw_docs)

# ===================== 2. 初始化向量检索器[语义相似度检索] =====================
embeddings_model = get_local_embeddings()
vectorstore = Chroma.from_documents(documents=split_docs, embedding=embeddings_model)
vector_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# print("------------------- 向量检索(语义) -------------------------")
# vector_docs = vector_retriever.invoke(question)
# print(vector_docs)

# ===================== 3. 初始化 BM25 关键词检索器 =====================
# BM25:关键词匹配检索,基于词频统计的传统检索算法
bm25_retriever = BM25Retriever.from_documents(split_docs, k=3)
# print("------------------- BM25 检索(关键词) -------------------------")
# bm25_docs = bm25_retriever.invoke(question)
# print(bm25_docs)

# ===================== 4. 混合检索(EnsembleRetriever) =====================
# weights=[0.5, 0.5]:BM25权重0.5,向量权重0.5。
# 关键词要求高 → 调大BM25权重;语义模糊 → 调大向量权重。
ensemble_retriever = EnsembleRetriever(retrievers=[bm25_retriever, vector_retriever], weights=[0.5, 0.5])
# print("------------------- 混合检索(融合结果) -------------------------")
# ensemble_docs = ensemble_retriever.invoke(question)
# print(ensemble_docs)


# ===================== 5. 构建提示词模板=====================
prompt_template = """
    请根据下面给出的上下文来回答问题:
        {context}
    问题: {question}
"""
prompt = ChatPromptTemplate.from_template(prompt_template)

# ===================== 6. 构建问答链 =====================
# 混合检索问答链
hybrid_chain = (
    RunnableMap({"context": lambda x: ensemble_retriever.invoke(x["question"]), "question": lambda x: x["question"]})
    | prompt
    | llm
    | StrOutputParser()
)
# 纯向量检索问答链(对比实验)
vector_chain = (
    RunnableMap({"context": lambda x: vector_retriever.invoke(x["question"]), "question": lambda x: x["question"]})
    | prompt
    | llm
    | StrOutputParser()
)

# ===================== 7. 执行测试 =====================
if __name__ == '__main__':
    print("\n------------ 纯向量检索 ------------------------")
    print(vector_chain.invoke({"question": question}))

    print("------------ 混合检索(向量+BM25权重0.5) ------------------------")
    print(hybrid_chain.invoke({"question": question}))

View Code

六. 总结

统一约定:

page_content:明文+向量 双存|metadata:只存明文、不向量化

1、普通基础RAG

原理:原文直接切分块,文档原文向量化。

存储:向量库存切分文本。

检索:用户问题向量匹配原文碎片。

优点:最简单、代码最少、通俗易懂。

缺点:原文生硬、用户口语难命中、容易断上下文。

2、父子分块RAG(Parent)

原理:大块做回答、小块做检索,内部自动绑定父子ID。

存储:向量库存子块;内存/MySQL存父块。

检索:子块向量匹配 → 找父ID → 返回完整父文档。

优点:检索精细、回答上下文完整、不割裂。

缺点:占用内存大、存储两份文档。

3、摘要索引RAG

原理:LLM生成摘要,摘要用来检索,原文用来回答。

存储:向量库存摘要;metadata存原始全文。

检索:问题匹配摘要向量 → 取出metadata原文回答。

优点:摘要简洁、匹配准确率极高、噪声少。

缺点:需要调用LLM生成摘要、耗费token。

4、假设问题RAG(多向量)

原理:LLM为原文生成多条假设问题,用问题建索引。

存储:向量库存假设问题;内存存原始文档,手动绑定uuid。

检索:用户提问匹配假设问题 → 映射原始文档。

优点:贴合人类口语、解决原文晦涩难检索问题。

缺点:生成问题消耗LLM、生成质量不稳定。

5、元数据自查询RAG(SelfQuery)

原理:大模型自动拆分自然语言:过滤条件+语义查询。

存储:向量库存正文,metadata存标签(作者/年份/评分)。

检索:先元数据过滤、后向量语义检索。

优点:无需手写筛选代码、结构化数据筛选极强。

缺点:如果检索词文档没有,直接返回空数据。

6、混合检索RAG(BM25+向量)

原理:融合向量语义检索与BM25关键词检索,加权融合排序。

存储:向量库存向量;BM25只存明文词频。

检索:两路并行检索 → 加权融合 → 去重重排。

优点:关键词+语义双兼顾、鲁棒性最强、无明显短板。

缺点:权重需要人工调优。

特有规则

① 同时执行向量+BM25;② 各自返回top-k;③ weights=[0.5,0.5];

④ 融合排序返回最优;⑤ 关键词调高BM25权重,语义模糊调高向量权重。

✨ 必考总结(背这句)

1、需要手动绑定ID:假设问题RAG

2、自动绑定ID:父子分块RAG

3、不需要LLM入库:普通、混合、元数据

4、必须LLM入库:摘要、假设问题

5、唯一双检索算法:混合检索

!

  • 作       者 : Yaopengfei(姚鹏飞)
  • 博客地址 : http://www.cnblogs.com/yaopengfei/
  • 声     明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
  • 声     明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。