LangChain MultiVectorRetriever:一个文档存多个向量 个人主页 for_ever_love__ 欢迎各位大佬莅临其他栏目: 大模型开发从0到1 其他栏目: iOS项目总结大全 其他栏目: 我想学python了 其他栏目: iOS UI 文章目录LangChain MultiVectorRetriever一个文档存多个向量一、核心思路二、用法一小块检索返回大块三、用法二用摘要做向量四、用法三用假设问题做向量强烈推荐五、用法四混合多种表示六、持久化别用 InMemoryStore七、什么时候用它什么时候用 ParentDocument八、常见坑九、小结LangChain MultiVectorRetriever一个文档存多个向量切分有个绕不开的矛盾小块检索准但缺上下文大块上下文全但检索不准。父子文档检索前面讲过是一种解法MultiVectorRetriever 是另一种更灵活的——同一个文档存多种向量表示。这篇讲它的四种用法以及什么时候它比父子文档更合适。一、核心思路传统做法一个 chunk → 一个向量。MultiVector 的做法一个文档 → 多个向量都指向它。原始文档 D完整内容存在 docstore ↑ ↑ ↑ 向量1 向量2 向量3 摘要 子块A 假设问题检索时用任意一种向量去匹配命中哪个都算数但返回的始终是原始文档 D的完整内容。这就同时拿到了精准匹配和完整上下文。二、用法一小块检索返回大块这是最常用、也最直观的一种。fromlangchain.retrievers.multi_vectorimportMultiVectorRetrieverfromlangchain_community.vectorstoresimportChromafromlangchain_openaiimportOpenAIEmbeddingsfromlangchain_text_splittersimportRecursiveCharacterTextSplitterfromlangchain.storageimportInMemoryStorefromlangchain_core.documentsimportDocumentimportuuid# 原始文档docs[Document(page_content第三章 差旅报销……2000 字的完整章节,metadata{source:handbook.md,chapter:3}),]# 1. 给每个文档分配 iddoc_ids[str(uuid.uuid4())for_indocs]# 2. 把每个文档切成小块这些小块用来做向量child_splitterRecursiveCharacterTextSplitter(chunk_size200)sub_docs[]fori,docinenumerate(docs):forchunkinchild_splitter.split_text(doc.page_content):sub_docs.append(Document(page_contentchunk,metadata{doc_id:doc_ids[i]},# 关键指回父文档))# 3. 向量库存小块vectorstoreChroma(collection_namemulti,embedding_functionOpenAIEmbeddings())vectorstore.add_documents(sub_docs)# 4. docstore 存完整文档storeInMemoryStore()store.mset([(doc_ids[i],docs[i])foriinrange(len(docs))])# 5. 组装retrieverMultiVectorRetriever(vectorstorevectorstore,docstorestore,id_keydoc_id,)resultsretriever.invoke(住宿费标准上浮条件是什么)print(len(results[0].page_content))# 完整的 2000 字章节和 ParentDocumentRetriever 的区别ParentDocumentRetrieverMultiVectorRetriever父子关系自动维护手动用 id 关联每个父文档的向量一组同粒度子块可以是多组、不同粒度/不同表示去重自动默认不去重可能返回重复所以要手动去重defdedup(docs):seen,outset(),[]fordindocs:keyd.metadata.get(doc_id)ord.page_content[:80]ifkeynotinseen:seen.add(key)out.append(d)returnout resultsdedup(retriever.invoke(问题))三、用法二用摘要做向量有些文档很长很啰嗦直接用原文向量化效果不好。先让 LLM 生成一段摘要用摘要做向量。fromlangchain_core.promptsimportChatPromptTemplatefromlangchain_core.output_parsersimportStrOutputParser summary_chain(ChatPromptTemplate.from_template(用 100 字概括下面文档的核心内容\n{doc})|ChatOpenAI(modelgpt-4o-mini,temperature0)|StrOutputParser())summariessummary_chain.batch([{doc:d.page_content}fordindocs],config{max_concurrency:5})summary_docs[Document(page_contents,metadata{doc_id:doc_ids[i]})fori,sinenumerate(summaries)]vectorstore.add_documents(summary_docs)为什么有用摘要去掉了细节噪声向量更能代表这篇文档在讲什么。对于这篇文档是讲什么的这类宽泛提问效果特别好。代价建索引时要对每篇文档调一次 LLM。1000 篇文档就是 1000 次调用——用便宜模型批量跑。四、用法三用假设问题做向量强烈推荐这是我最喜欢的一种。思路是让 LLM 反过来生成这篇文档能回答哪些问题用这些问题做向量。QUESTION_PROMPTChatPromptTemplate.from_template( 下面是文档的一段内容。请生成 3 个「这段内容能够回答的问题」。 要求问题要具体、用词接近真实用户的提问方式。每行一个不要编号。 文档内容 {doc} )question_chain(QUESTION_PROMPT|ChatOpenAI(modelgpt-4o-mini,temperature0)|StrOutputParser())defbuild_question_vectors(docs,doc_ids):out[]fori,docinenumerate(docs):rawquestion_chain.invoke({doc:doc.page_content[:2000]})forqinraw.strip().split(\n):qq.strip(- ).strip()ifq:out.append(Document(page_contentq,metadata{doc_id:doc_ids[i]}))returnout question_docsbuild_question_vectors(docs,doc_ids)vectorstore.add_documents(question_docs)为什么效果好因为检索本质是用问题匹配文档但问题和文档的表述方式天然不同——用户问咋报销文档写费用报销流程。用假设问题做向量后检索变成了用户问题 vs 假设问题的匹配两边都是问题表述空间一致了匹配准确率明显提升。这在 FAQ、制度文档、产品手册上效果非常显著。五、用法四混合多种表示最强的做法是几种向量都存让它们互相补# 1. 小块向量针对细节问题vectorstore.add_documents(sub_docs)# 2. 摘要向量针对宽泛问题vectorstore.add_documents(summary_docs)# 3. 假设问题向量针对口语化提问vectorstore.add_documents(question_docs)三者指向同一个doc_id任何一路命中都会返回完整原文。代价是向量库体积变成 3 倍。我的建议排序预算选哪种只能选一种假设问题提升最明显能选两种假设问题 小块不差钱三种全上六、持久化别用 InMemoryStore和父子文档那篇一样InMemoryStore重启就丢会导致命中了向量却取不到原文。fromlangchain_community.storageimportRedisStore storeRedisStore(redis_urlredis://localhost:6379)# 或自己实现基于数据库/文件的 BaseStore七、什么时候用它什么时候用 ParentDocument场景推荐只需小块检索 大块返回ParentDocumentRetriever代码少自动去重想用摘要/假设问题做向量MultiVectorRetriever想同时存多种表示MultiVectorRetriever文档很短、不需要切分MultiVector直接存原文 假设问题一句话要多种表示就用 MultiVector只是检索小、返回大用 ParentDocument 更省事。八、常见坑现象原因解法返回了重复的文档MultiVector 不去重按doc_id手动去重报找不到 docdocstore里没有对应 id检查mset是否都写进去了重启后取不到原文用了InMemoryStore换 Redis / 数据库向量库巨大存了太多种表示优先保留假设问题建索引很慢逐篇调 LLM 生成摘要/问题batchmax_concurrency 便宜模型九、小结MultiVector 的核心一个文档多个向量命中任一都返回完整原文四种表示子块细节、摘要宽泛、假设问题口语化效果最好、混合假设问题解决了问题和文档表述空间不一致的根本问题性价比最高和 ParentDocument 的区别它不去重、需手动关联 id但支持任意多种表示必须用持久化的 docstoreRedis/数据库InMemoryStore重启即丢建索引的 LLM 调用要批量 并发 便宜模型。下一篇讲 SelfQueryRetriever——让模型自己把自然语言转成结构化查询。