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

推荐订阅源

宝玉的分享
宝玉的分享
T
Threat Research - Cisco Blogs
H
Hacker News: Front Page
N
News and Events Feed by Topic
Know Your Adversary
Know Your Adversary
Cisco Talos Blog
Cisco Talos Blog
SecWiki News
SecWiki News
C
Cisco Blogs
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
K
Kaspersky official blog
Forbes - Security
Forbes - Security
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
P
Privacy & Cybersecurity Law Blog
H
Heimdal Security Blog
Y
Y Combinator Blog
The GitHub Blog
The GitHub Blog
S
SegmentFault 最新的问题
V
Vulnerabilities – Threatpost
T
Tenable Blog
T
Tailwind CSS Blog
P
Privacy International News Feed
WordPress大学
WordPress大学
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
博客园 - Franky
Hacker News: Ask HN
Hacker News: Ask HN
Jina AI
Jina AI
C
Cybersecurity and Infrastructure Security Agency CISA
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
雷峰网
雷峰网
Vercel News
Vercel News
A
About on SuperTechFans
爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
The Last Watchdog
The Last Watchdog
Engineering at Meta
Engineering at Meta
Spread Privacy
Spread Privacy
Security Archives - TechRepublic
Security Archives - TechRepublic
博客园 - 司徒正美
量子位
博客园 - 三生石上(FineUI控件)
J
Java Code Geeks
Hacker News - Newest:
Hacker News - Newest: "LLM"
Recorded Future
Recorded Future
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Martin Fowler
Martin Fowler
Project Zero
Project Zero

博客园 - 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 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。