LangChain MultiVectorRetriever:一个文档存多个向量

个人主页: > for_ever_love__ <(欢迎各位大佬莅临😊)
其他栏目: > 大模型开发从0到1 <
其他栏目: > iOS项目总结大全 <
其他栏目: > 我想学python了 <
其他栏目: > iOS UI <

文章目录

LangChain MultiVectorRetriever:一个文档存多个向量

切分有个绕不开的矛盾:小块检索准但缺上下文,大块上下文全但检索不准 。

父子文档检索(前面讲过)是一种解法,

MultiVectorRetriever 是另一种更灵活的------同一个文档,存多种向量表示。

这篇讲它的四种用法,以及什么时候它比父子文档更合适。

一、核心思路

传统做法:一个 chunk → 一个向量。

MultiVector 的做法:一个文档 → 多个向量,都指向它。

复制代码
        原始文档 D(完整内容,存在 docstore)
          ↑        ↑        ↑
       向量1     向量2     向量3
     (摘要)  (子块A)  (假设问题)

检索时:

  1. 用任意一种向量去匹配,命中哪个都算数;
  2. 但返回的始终是原始文档 D 的完整内容。

这就同时拿到了"精准匹配"和"完整上下文"。

二、用法一:小块检索,返回大块

这是最常用、也最直观的一种。

python 复制代码
from langchain.retrievers.multi_vector import MultiVectorRetriever
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.storage import InMemoryStore
from langchain_core.documents import Document
import uuid

# 原始文档
docs = [
    Document(page_content="第三章 差旅报销......(2000 字的完整章节)",
             metadata={"source": "handbook.md", "chapter": "3"}),
]

# 1. 给每个文档分配 id
doc_ids = [str(uuid.uuid4()) for _ in docs]

# 2. 把每个文档切成小块(这些小块用来做向量)
child_splitter = RecursiveCharacterTextSplitter(chunk_size=200)
sub_docs = []
for i, doc in enumerate(docs):
    for chunk in child_splitter.split_text(doc.page_content):
        sub_docs.append(Document(
            page_content=chunk,
            metadata={"doc_id": doc_ids[i]},     # 关键:指回父文档
        ))

# 3. 向量库存小块
vectorstore = Chroma(collection_name="multi",
                     embedding_function=OpenAIEmbeddings())
vectorstore.add_documents(sub_docs)

# 4. docstore 存完整文档
store = InMemoryStore()
store.mset([(doc_ids[i], docs[i]) for i in range(len(docs))])

# 5. 组装
retriever = MultiVectorRetriever(
    vectorstore=vectorstore,
    docstore=store,
    id_key="doc_id",
)

results = retriever.invoke("住宿费标准上浮条件是什么?")
print(len(results[0].page_content))    # 完整的 2000 字章节

和 ParentDocumentRetriever 的区别:

ParentDocumentRetriever MultiVectorRetriever
父子关系 自动维护 手动用 id 关联
每个父文档的向量 一组(同粒度子块) 可以是多组、不同粒度/不同表示
去重 自动 默认不去重,可能返回重复

所以要手动去重:

python 复制代码
def dedup(docs):
    seen, out = set(), []
    for d in docs:
        key = d.metadata.get("doc_id") or d.page_content[:80]
        if key not in seen:
            seen.add(key)
            out.append(d)
    return out

results = dedup(retriever.invoke("问题"))

三、用法二:用"摘要"做向量

有些文档很长很啰嗦,直接用原文向量化效果不好。

先让 LLM 生成一段摘要,用摘要做向量。

python 复制代码
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

summary_chain = (
    ChatPromptTemplate.from_template("用 100 字概括下面文档的核心内容:\n{doc}")
    | ChatOpenAI(model="gpt-4o-mini", temperature=0)
    | StrOutputParser()
)

summaries = summary_chain.batch([{"doc": d.page_content} for d in docs],
                                config={"max_concurrency": 5})

summary_docs = [
    Document(page_content=s, metadata={"doc_id": doc_ids[i]})
    for i, s in enumerate(summaries)
]
vectorstore.add_documents(summary_docs)

为什么有用? 摘要去掉了细节噪声,

向量更能代表"这篇文档在讲什么"。

对于"这篇文档是讲什么的"这类宽泛提问效果特别好。

代价 :建索引时要对每篇文档调一次 LLM。

1000 篇文档就是 1000 次调用------用便宜模型批量跑。

四、用法三:用"假设问题"做向量(强烈推荐)

这是我最喜欢的一种。思路是:

让 LLM 反过来生成"这篇文档能回答哪些问题",
用这些问题做向量。

python 复制代码
QUESTION_PROMPT = ChatPromptTemplate.from_template("""
下面是文档的一段内容。请生成 3 个「这段内容能够回答的问题」。
要求:问题要具体、用词接近真实用户的提问方式。每行一个,不要编号。

文档内容:
{doc}
""")

question_chain = (
    QUESTION_PROMPT
    | ChatOpenAI(model="gpt-4o-mini", temperature=0)
    | StrOutputParser()
)

def build_question_vectors(docs, doc_ids):
    out = []
    for i, doc in enumerate(docs):
        raw = question_chain.invoke({"doc": doc.page_content[:2000]})
        for q in raw.strip().split("\n"):
            q = q.strip("- ").strip()
            if q:
                out.append(Document(page_content=q,
                                    metadata={"doc_id": doc_ids[i]}))
    return out

question_docs = build_question_vectors(docs, doc_ids)
vectorstore.add_documents(question_docs)

为什么效果好?

因为检索本质是"用问题匹配文档 ",

但问题和文档的表述方式天然不同 ------

用户问"咋报销",文档写"费用报销流程"。

用"假设问题"做向量后,

检索变成了"用户问题 vs 假设问题"的匹配,两边都是问题,
表述空间一致了
,匹配准确率明显提升。

这在 FAQ、制度文档、产品手册上效果非常显著。

五、用法四:混合多种表示

最强的做法是几种向量都存,让它们互相补:

python 复制代码
# 1. 小块向量(针对细节问题)
vectorstore.add_documents(sub_docs)

# 2. 摘要向量(针对宽泛问题)
vectorstore.add_documents(summary_docs)

# 3. 假设问题向量(针对口语化提问)
vectorstore.add_documents(question_docs)

三者指向同一个 doc_id,

任何一路命中都会返回完整原文。

代价是向量库体积变成 3 倍。

我的建议排序:

预算 选哪种
只能选一种 假设问题(提升最明显)
能选两种 假设问题 + 小块
不差钱 三种全上

六、持久化:别用 InMemoryStore

和父子文档那篇一样,InMemoryStore 重启就丢 ,

会导致"命中了向量却取不到原文"。

python 复制代码
from langchain_community.storage import RedisStore

store = RedisStore(redis_url="redis://localhost:6379")
# 或自己实现基于数据库/文件的 BaseStore

七、什么时候用它,什么时候用 ParentDocument

场景 推荐
只需"小块检索 + 大块返回" ParentDocumentRetriever(代码少,自动去重)
想用摘要/假设问题做向量 MultiVectorRetriever
想同时存多种表示 MultiVectorRetriever
文档很短、不需要切分 MultiVector(直接存原文 + 假设问题)

一句话:要"多种表示"就用 MultiVector,
只是"检索小、返回大"用 ParentDocument 更省事。

八、常见坑

现象 原因 解法
返回了重复的文档 MultiVector 不去重 按 doc_id 手动去重
报"找不到 doc" docstore 里没有对应 id 检查 mset 是否都写进去了
重启后取不到原文 用了 InMemoryStore 换 Redis / 数据库
向量库巨大 存了太多种表示 优先保留假设问题
建索引很慢 逐篇调 LLM 生成摘要/问题 batch + max_concurrency + 便宜模型

九、小结

  • MultiVector 的核心:一个文档多个向量,命中任一都返回完整原文;
  • 四种表示:子块(细节)、摘要(宽泛)、假设问题(口语化,效果最好)、混合;
  • 假设问题解决了"问题和文档表述空间不一致"的根本问题,性价比最高;
  • 和 ParentDocument 的区别:它不去重、需手动关联 id,
    但支持任意多种表示
    ;
  • 必须用持久化的 docstore (Redis/数据库),InMemoryStore 重启即丢;
  • 建索引的 LLM 调用要批量 + 并发 + 便宜模型。

下一篇讲 SelfQueryRetriever------让模型自己把自然语言转成结构化查询。

相关推荐
迅猛龙办公室1 小时前
Python实现获取星期字符串
java·前端·python
XMAIPC_Robot1 小时前
国产FPGA替代Xilinx在储能电力采集场景可行性评估与移植经验
人工智能·算法·机器学习·fpga开发·rk3588+fpga
loulanyue_1 小时前
从视觉感知到具身世界模型:物理世界中的自我进化智能体——读普林斯顿刘世隆NICE Workshop演讲
人工智能·深度学习
阿里云大数据AI技术1 小时前
在 EMR Serverless Spark 的 Ray 作业中使用 Checkpoint 断点续跑
人工智能·spark
wewowilling1 小时前
Coze智能体入门 --- AI Agent开发入门--第一章
python·低代码·agent·零代码·coze
Wx-bishekaifayuan1 小时前
springboot公考微信小程序35511-计算机课程设计、毕业设计
spring boot·后端·python·django·课程设计·express·旅游
小猴子爱上树2 小时前
跨马翻译:批量图片翻译+视频字幕+智能抠图,一站式跨境电商翻译工具
python·音视频
溪语流沙2 小时前
【Web全栈进阶】OAuth实战——GitHub第三方登录
前端·python·github
计算机编程指导师2 小时前
【计算机毕设选题】基于Hadoop的全球企业邮件安全合规数据可视化分析系统源码 毕业设计 选题推荐 毕设选题 数据分析 机器学习
大数据·hadoop·python·数据分析·毕业设计·课程设计·企业邮件