📘 **TELUS Agriculture & Consumer Goods** 如何通过 **Haystack Agents** 转变促销交易

教程:创建 Vision+Text RAG 管道


概述

在本笔记本中,您将学习如何使用 Haystack 索引和检索图像。完成后,您将能够构建一个检索增强生成 (RAG) 管道,该管道可以回答基于图像和文本的问题。当处理像科学论文、图表或截图这类含义分布在不同模态的数据集时,这将非常有用。

本教程使用了以下新组件,这些组件支持图像索引

  • SentenceTransformersDocumentImageEmbedder:使用 CLIP 模型嵌入图像文档
  • ImageFileToDocument:将图像文件转换为 Haystack Document
  • DocumentTypeRouter:按 MIME 类型(例如,图像与文本)路由检索到的文档
  • DocumentToImageContent:将图像文档转换为 ImageContent,以供我们的 ChatGenerator 处理
  • LLMDocumentContentExtractor:使用支持视觉的 LLM 从基于图像的文档中提取文本内容。

在本笔记本中,我们将介绍所有这些功能,并展示一个使用图像+文本检索+多模态生成的应用。

设置开发环境

首先,我们安装所需的包

%%bash

pip install -q "haystack-ai>=2.16.0" pillow pypdf pypdfium2 "sentence-transformers>=4.1.0"

输入您的 OpenAI API 密钥

import os
from getpass import getpass


if "OPENAI_API_KEY" not in os.environ:
    os.environ["OPENAI_API_KEY"] = getpass("Enter OpenAI API key:")

图像嵌入简介

让我们比较一个文本和两个图像之间的相似度。

首先,让我们下载两个示例图像,一个是苹果,一个是水豚。

from urllib.request import URLopener

url_opener = URLopener()
url_opener.addheader("User-Agent", "Mozilla/5.0")

url_opener.retrieve("https://upload.wikimedia.org/wikipedia/commons/2/26/Pink_Lady_Apple_%284107712628%29.jpg?download", "apple.jpg")
url_opener.retrieve("https://upload.wikimedia.org/wikipedia/commons/thumb/e/e1/Cattle_tyrant_%28Machetornis_rixosa%29_on_Capybara.jpg/960px-Cattle_tyrant_%28Machetornis_rixosa%29_on_Capybara.jpg?download", "capybara.jpg")
from PIL import Image

img = Image.open("apple.jpg")
# We resize the image here just to avoid it taking up too much space in the notebook
img_resized = img.resize((img.width // 6, img.height // 6))
img_resized

接下来,我们将图像文件转换为 Haystack Documents,以便它们可以在下游用于我们的 SentenceTransformersDocumentImageEmbedder 组件。

from haystack.components.converters.image import ImageFileToDocument

image_file_converter = ImageFileToDocument()
image_docs = image_file_converter.run(sources=["apple.jpg", "capybara.jpg"])["documents"]
print(image_docs)

接下来,我们加载我们的嵌入器,并使用 sentence-transformers/clip-ViT-L-14 模型,该模型将文本和图像映射到共享的向量空间。为了计算相似度,我们必须对文本和图像使用相同的 CLIP 模型。

from haystack.components.embedders import SentenceTransformersTextEmbedder
from haystack.components.embedders.image import SentenceTransformersDocumentImageEmbedder

text_embedder = SentenceTransformersTextEmbedder(model="sentence-transformers/clip-ViT-L-14", progress_bar=False)
image_embedder = SentenceTransformersDocumentImageEmbedder(
    model="sentence-transformers/clip-ViT-L-14", progress_bar=False
)

# Warm up the models to load them
text_embedder.warm_up()
image_embedder.warm_up()

让我们运行嵌入器并为图像创建向量嵌入,以查看我们的查询与这两个图像的语义相似度。

import torch
from sentence_transformers import util

query = "A red apple on a white background"
text_embedding = text_embedder.run(text=query)["embedding"]
image_docs_with_embeddings = image_embedder.run(image_docs)["documents"]

# Compare the similarities between the query and two image documents
for doc in image_docs_with_embeddings:
    similarity = util.cos_sim(torch.tensor(text_embedding), torch.tensor(doc.embedding))
    print(f"Similarity with {doc.meta['file_path'].split('/')[-1]}: {similarity.item():.2f}")

正如我们所见,文本与我们的苹果图像最相似,正如预期的那样!因此,CLIP 模型可以为图像和文本创建正确的表示。

带有图像和文本嵌入的多模态检索

在这种方法中,我们将使用 sentence-transformers/clip-ViT-L-14 模型为图像和文本创建嵌入,并使用这些嵌入进行检索。

首先,让我们下载一个示例 PDF 文件,看看如何检索文本和基于图像的文档。

from urllib.request import URLopener

url_opener = URLopener()
url_opener.addheader("User-Agent", "Mozilla/5.0")

url_opener.retrieve("https://arxiv.org/pdf/1706.03762", "attention_is_all_you_need.pdf")

构建图像+文本索引管道

让我们创建一个索引管道来一次性处理我们的图像和 PDF 文件,并将它们写入我们的 Document Store。

因此,在下面的 Pipeline 中,我们正在

  • 为图像文件计算基于图像的嵌入
  • 将 PDF 文件转换为文本 Documents,然后计算基于文本的嵌入
from haystack import Pipeline
from haystack.components.converters import PyPDFToDocument
from haystack.components.converters.image import ImageFileToDocument
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from haystack.components.embedders.image import SentenceTransformersDocumentImageEmbedder
from haystack.components.joiners import DocumentJoiner
from haystack.components.preprocessors.document_splitter import DocumentSplitter
from haystack.components.routers.file_type_router import FileTypeRouter
from haystack.components.writers.document_writer import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore

# Create our document store
doc_store = InMemoryDocumentStore(embedding_similarity_function="cosine")

# Define our components
file_type_router = FileTypeRouter(mime_types=["application/pdf", "image/jpeg"])
final_doc_joiner = DocumentJoiner(sort_by_score=False)
image_converter = ImageFileToDocument()
pdf_converter = PyPDFToDocument()
pdf_splitter = DocumentSplitter(split_by="page", split_length=1)
text_doc_embedder = SentenceTransformersDocumentEmbedder(
    model="sentence-transformers/clip-ViT-L-14", progress_bar=False
)
image_embedder = SentenceTransformersDocumentImageEmbedder(
    model="sentence-transformers/clip-ViT-L-14", progress_bar=False
)
document_writer = DocumentWriter(doc_store)
# Create the Indexing Pipeline
indexing_pipe = Pipeline()
indexing_pipe.add_component("file_type_router", file_type_router)
indexing_pipe.add_component("pdf_converter", pdf_converter)
indexing_pipe.add_component("pdf_splitter", pdf_splitter)
indexing_pipe.add_component("image_converter", image_converter)
indexing_pipe.add_component("text_doc_embedder", text_doc_embedder)
indexing_pipe.add_component("image_doc_embedder", image_embedder)
indexing_pipe.add_component("final_doc_joiner", final_doc_joiner)
indexing_pipe.add_component("document_writer", document_writer)

indexing_pipe.connect("file_type_router.application/pdf", "pdf_converter.sources")
indexing_pipe.connect("pdf_converter.documents", "pdf_splitter.documents")
indexing_pipe.connect("pdf_splitter.documents", "text_doc_embedder.documents")
indexing_pipe.connect("file_type_router.image/jpeg", "image_converter.sources")
indexing_pipe.connect("image_converter.documents", "image_doc_embedder.documents")
indexing_pipe.connect("text_doc_embedder.documents", "final_doc_joiner.documents")
indexing_pipe.connect("image_doc_embedder.documents", "final_doc_joiner.documents")
indexing_pipe.connect("final_doc_joiner.documents", "document_writer.documents")

可视化索引管道

# indexing_pipe.show()

使用 PDF 和图像文件运行索引管道。

indexing_result = indexing_pipe.run(
    data={"file_type_router": {"sources": ["attention_is_all_you_need.pdf", "apple.jpg"]}}
)

检查文档

indexed_documents = doc_store.filter_documents()
print(f"Indexed {len(indexed_documents)} documents")

搜索图像+文本

现在,让我们通过传递查询来设置我们的搜索并从 Document Store 中检索相关数据。

from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever

retriever = InMemoryEmbeddingRetriever(document_store=doc_store)
text_embedding = text_embedder.run(text="An image of an apple")["embedding"]
results = retriever.run(text_embedding)["documents"]

for idx, doc in enumerate(results[:5]):
    print(f"Document {idx+1}:")
    print(f"Score: {doc.score}")
    print(f"File Path: {doc.meta['file_path']}")
    print("")

啊,这真奇怪!看起来前几条结果都不相关。事实上,无论多么不相关,看起来检索到的顶层文档都是基于文本的。

这实际上是在尝试同时使用图像和文本进行多模态检索时一个常见的情况。通常,底层的嵌入模型(此处为 CLIP)并没有经过训练来同时处理文本和图像文档,并且可能会偏向其中一种。在这种情况下,我们选择的模型似乎偏向文本之间的相似性,我们可以通过附加到每个文档的分数来观察这一点。

题外话:像 jina-embeddings-v4Cohere Embed 4 这样的较新模型可能更适合这种情况。

为了解决这个问题,让我们在下面使用一种略有不同的方法。

仅使用文本嵌入的多模态检索

在这种方法中,我们将使用 LLMDocumentContentExtractor 在将图像写入 DocumentStore 之前,先提取图像的文本表示。

  • 这将允许我们在搜索 DocumentStore 时使用仅文本的检索方法。
  • 我们仍然会将实际图像发送给 Vision LLM。这很有用,因为图像可能包含比提取版本更多的信息和细微差别。

使用 LLMDocumentContentExtractor 构建图像+文本索引管道

这一次,在下面的 Pipeline 中,我们正在

  • 使用 LLMDocumentContentExtractor 提取图像的文本表示
  • 将 PDF 文件转换为文本 Documents
  • 使用 mixedbread-ai/mxbai-embed-large-v1 为 PDF 和图像文件的文本内容创建文本嵌入
from haystack import Pipeline
from haystack.components.converters import PyPDFToDocument
from haystack.components.converters.image import ImageFileToDocument
from haystack.components.embedders import SentenceTransformersDocumentEmbedder
from haystack.components.embedders.image import SentenceTransformersDocumentImageEmbedder
from haystack.components.joiners import DocumentJoiner
from haystack.components.preprocessors.document_splitter import DocumentSplitter
from haystack.components.routers.file_type_router import FileTypeRouter
from haystack.components.writers.document_writer import DocumentWriter
from haystack.document_stores.in_memory import InMemoryDocumentStore
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.extractors.image import LLMDocumentContentExtractor

# Create our document store
doc_store = InMemoryDocumentStore(embedding_similarity_function="cosine")

# Define our components
file_type_router = FileTypeRouter(mime_types=["application/pdf", "image/jpeg"])
image_converter = ImageFileToDocument()
pdf_converter = PyPDFToDocument()
pdf_splitter = DocumentSplitter(split_by="page", split_length=1)
final_doc_joiner = DocumentJoiner(sort_by_score=False)
document_writer = DocumentWriter(doc_store)

# Now we use high-performing text-only embedders
doc_embedder = SentenceTransformersDocumentEmbedder(model="mixedbread-ai/mxbai-embed-large-v1", progress_bar=False)

# New LLMDocumentContentExtractor
llm_content_extractor = LLMDocumentContentExtractor(
    chat_generator=OpenAIChatGenerator(model="gpt-4o-mini"), # you can replace this with other chat generators that support vision
    max_workers=1,  # This can be used to parallelize the content extraction
)
# Create the Indexing Pipeline
indexing_pipe = Pipeline()
indexing_pipe.add_component("file_type_router", file_type_router)
indexing_pipe.add_component("pdf_converter", pdf_converter)
indexing_pipe.add_component("pdf_splitter", pdf_splitter)
indexing_pipe.add_component("image_converter", image_converter)
indexing_pipe.add_component("llm_content_extractor", llm_content_extractor)
indexing_pipe.add_component("doc_embedder", doc_embedder)
indexing_pipe.add_component("final_doc_joiner", final_doc_joiner)
indexing_pipe.add_component("document_writer", document_writer)

indexing_pipe.connect("file_type_router.application/pdf", "pdf_converter.sources")
indexing_pipe.connect("pdf_converter.documents", "pdf_splitter.documents")
indexing_pipe.connect("pdf_splitter.documents", "final_doc_joiner.documents")
indexing_pipe.connect("file_type_router.image/jpeg", "image_converter.sources")
indexing_pipe.connect("image_converter.documents", "llm_content_extractor.documents")
indexing_pipe.connect("llm_content_extractor.documents", "final_doc_joiner.documents")
indexing_pipe.connect("final_doc_joiner.documents", "doc_embedder.documents")
indexing_pipe.connect("doc_embedder.documents", "document_writer.documents")
# indexing_pipe.show()
indexing_result = indexing_pipe.run(
    data={"file_type_router": {"sources": ["attention_is_all_you_need.pdf", "apple.jpg"]}}
)
indexed_documents = doc_store.filter_documents()
print(f"Indexed {len(indexed_documents)} documents")

让我们检查我们的图像文档,看看提取了什么内容。

image_doc = [d for d in indexed_documents if d.meta.get("file_path") == "apple.jpg"]
image_doc

太好了,我们有了苹果图像的标题!

现在,让我们运行相同的查询进行检索,看看会得到什么。

from haystack.components.retrievers.in_memory import InMemoryEmbeddingRetriever
from haystack.components.embedders import SentenceTransformersTextEmbedder

retriever = InMemoryEmbeddingRetriever(document_store=doc_store)
text_embedder = SentenceTransformersTextEmbedder(model="mixedbread-ai/mxbai-embed-large-v1", progress_bar=False)
text_embedder.warm_up()
text_embedding = text_embedder.run(text="An image of an apple")["embedding"]
results = retriever.run(text_embedding)["documents"]

for idx, doc in enumerate(results[:5]):
    print(f"Document {idx+1}:")
    print(f"Score: {doc.score}")
    print(f"File Path: {doc.meta['file_path']}")
    print("")

现在我们可以看到,代表 apple.jpg 文件的文档被首先检索到了!我们现在可以使用这种方法在查询时检索图像文档,并且仍然使用图像来回答用户的问题。

图像+文本多模态 RAG

在本节中,我们将演示一个多模态 RAG 管道,该管道基于文本图像标题进行检索,但在生成过程中使用原始图像。这使我们能够结合两种模态的优势:基于文本的索引进行快速有效的检索,以及使用视觉输入进行丰富、有根据的生成。

具体来说:

  • 在索引期间,我们使用 LLMDocumentContentExtractor 从每张图像中提取一个标题,该标题作为可搜索的文本表示。
  • 在查询时,这些标题被嵌入并用于检索相关文档。
  • 检索到的图像文档随后通过 DocumentToImageContent 进行处理,该过程将其转换为 base64 字符串并将其打包为 ImageContent
  • 这些图像对象直接在提示中渲染,并发送给支持视觉的语言模型,如 gpt-4o-mini

这里需要注意的一点是,这一次,我们没有向 ChatPromptBuilder 传递 ChatMessage 对象列表,而是使用 {%- message role="system" -%}{%- message role="user" -%} 直接在提示中定义角色。然后,我们使用 templatize_part 工具渲染 base64 字符串。

这种方法使得我们可以通过文本检索图像和文本,同时在最终答案中利用图像的全部细节。

from haystack import Pipeline
from haystack.components.embedders.sentence_transformers_text_embedder import SentenceTransformersTextEmbedder
from haystack.components.generators.chat import OpenAIChatGenerator
from haystack.components.retrievers.in_memory.embedding_retriever import InMemoryEmbeddingRetriever
from haystack.components.builders import ChatPromptBuilder
from haystack.components.converters.image import DocumentToImageContent
from haystack.components.routers import DocumentTypeRouter

text_embedder = SentenceTransformersTextEmbedder(model="mixedbread-ai/mxbai-embed-large-v1", progress_bar=False)
retriever = InMemoryEmbeddingRetriever(document_store=doc_store, top_k=3)
doc_type_router = DocumentTypeRouter(file_path_meta_field="file_path", mime_types=["image/jpeg", "application/pdf"])
doc_to_image = DocumentToImageContent(detail="auto")
chat_prompt_builder = ChatPromptBuilder(
    required_variables=["question"],
    template="""{% message role="system" %}
You are a friendly assistant that answers questions based on provided documents and images.
{% endmessage %}

{%- message role="user" -%}
Only provide an answer to the question using the images and text passages provided.

These are the text-only documents:
{%- if documents|length > 0 %}
{%- for doc in documents %}
Text Document [{{ loop.index }}] :
{{ doc.content }}
{% endfor -%}
{%- else %}
No relevant text documents were found.
{% endif %}
End of text documents.

Question: {{ question }}
Answer:

Images:
{%- if image_contents|length > 0 %}
{%- for img in image_contents -%}
  {{ img | templatize_part }}
{%- endfor -%}
{% endif %}
{%- endmessage -%}
""")
llm = OpenAIChatGenerator(model="gpt-4o-mini")
# Create the Query Pipeline
pipe = Pipeline()
pipe.add_component("text_embedder", text_embedder)
pipe.add_component("retriever", retriever)
pipe.add_component("doc_type_router", doc_type_router)
pipe.add_component("doc_to_image", doc_to_image)
pipe.add_component("chat_prompt_builder", chat_prompt_builder)
pipe.add_component("llm", llm)

pipe.connect("text_embedder.embedding", "retriever.query_embedding")
pipe.connect("retriever.documents", "doc_type_router.documents")
pipe.connect("doc_type_router.image/jpeg", "doc_to_image.documents")
pipe.connect("doc_to_image.image_contents", "chat_prompt_builder.image_contents")
pipe.connect("doc_type_router.application/pdf", "chat_prompt_builder.documents")
pipe.connect("chat_prompt_builder.prompt", "llm.messages")
# pipe.show()

当我们将查询发送到我们的管道时,我们将收到一个基于苹果图像的响应。retriever 获取相关数据,而像 gpt-4o-mini 这样的支持视觉的语言模型则使用 base64 编码的图像生成响应。

# Run the pipeline with a query about the apple
query = "What is the color of the background of the image with an apple in it?"
result = pipe.run(
    data={"text_embedder": {"text": query}, "chat_prompt_builder": {"question": query}}
)
print(result["llm"]["replies"][0].text)
# Run the pipeline with a query about the pdf document
query = "What is attention in the transformers architecture?"
result = pipe.run(data={"text_embedder": {"text": query}, "chat_prompt_builder": {"question": query}})
print(result["llm"]["replies"][0].text)

下一步

🎉 恭喜!您刚刚使用 Haystack 构建了一个多模态 RAG 系统,并尝试了不同方法来检索图像和文本数据。

您可以在这个 GitHub issue 中跟踪多模态功能的进展。

想继续探索吗?以下是一些很棒的下一步:

要随时了解最新的 Haystack 发展,您可以 订阅我们的时事通讯加入 Haystack discord 社区

(笔记本作者:Sebastian Husch Lee)