
Tomaz Bratanic • 2023-12-18
使用 LlamaIndex 和 Neo4j 构建多模态 RAG 管道
人工智能和大型语言模型领域正在快速发展。一年前,没有人使用过 LLM 来提高他们的工作效率。今天,我们大多数人无法想象没有它们工作,或者不把至少一些小任务分派给 LLM。由于大量的研究和兴趣,LLM 每天都在变得越来越好、越来越聪明。不仅如此,它们的理解能力也开始跨越多种模态。随着 GPT-4-Vision 以及随后的其他 LLM 的推出,今天的 LLM 似乎能够很好地处理和理解图像。这里有一个 ChatGPT 描述图像内容的例子。

正如您所见,ChatGPT 非常擅长理解和描述图像。我们可以在 RAG 应用程序中利用它理解图像的能力,这样就不仅依赖文本来生成准确和最新的答案,还可以结合文本和图片中的信息来生成比以往更准确的答案。使用 LlamaIndex,实现多模态 RAG 管道变得非常简单。受其多模态 Cookbook 示例启发,我决定测试是否可以使用 Neo4j 作为数据库来实现多模态 RAG 应用程序。
要使用 LlamaIndex 实现多模态 RAG 管道,您只需实例化两个向量存储,一个用于图像,一个用于文本,然后查询这两个存储以检索相关信息来生成最终答案。

文章首先被分割成图像和文本。然后,这些元素被转换为向量表示并单独索引。对于文本,我们将使用 ada-002 文本嵌入模型;对于图像,我们将使用双编码器模型 CLIP,它可以将文本和图像嵌入到同一嵌入空间中。当最终用户提出问题时,会执行两次向量相似度搜索:一次查找相关图像,另一次查找相关文档。结果被馈送给多模态 LLM,LLM 会为用户生成答案,展示了处理和利用混合媒体进行信息检索和响应生成的集成方法。
代码已在 GitHub 上提供。
数据预处理
我们将使用我 2022 年和 2023 年的 Medium 文章作为 RAG 应用程序的基础数据集。这些文章包含关于 Neo4j 图数据科学库以及如何将 Neo4j 与 LLM 框架结合的大量信息。当您从 Medium 下载您自己的文章时,它们是 HTML 格式的。因此,我们需要编写一些代码来分别提取文本和图像。
def process_html_file(file_path):
with open(file_path, "r", encoding="utf-8") as file:
soup = BeautifulSoup(file, "html.parser")
# Find the required section
content_section = soup.find("section", {"data-field": "body", "class": "e-content"})
if not content_section:
return "Section not found."
sections = []
current_section = {"header": "", "content": "", "source": file_path.split("/")[-1]}
images = []
header_found = False
for element in content_section.find_all(recursive=True):
if element.name in ["h1", "h2", "h3", "h4"]:
if header_found and (current_section["content"].strip()):
sections.append(current_section)
current_section = {
"header": element.get_text(),
"content": "",
"source": file_path.split("/")[-1],
}
header_found = True
elif header_found:
if element.name == "pre":
current_section["content"] += f"```{element.get_text().strip()}```\n"
elif element.name == "img":
img_src = element.get("src")
img_caption = element.find_next("figcaption")
caption_text = img_caption.get_text().strip() if img_caption else ""
images.append(ImageDocument(image_url=img_src))
elif element.name in ["p", "span", "a"]:
current_section["content"] += element.get_text().strip() + "\n"
if current_section["content"].strip():
sections.append(current_section)
return images, sections
我不会详细介绍解析代码,但我们会根据标头 h1–h4 分割文本并提取图像链接。然后,我们将所有文章通过这个函数运行,以提取所有相关信息。
all_documents = []
all_images = []
# Directory to search in (current working directory)
directory = os.getcwd()
# Walking through the directory
for root, dirs, files in os.walk(directory):
for file in files:
if file.endswith(".html"):
# Update the file path to be relative to the current directory
images, documents = process_html_file(os.path.join(root, file))
all_documents.extend(documents)
all_images.extend(images)
text_docs = [Document(text=el.pop("content"), metadata=el) for el in all_documents]
print(f"Text document count: {len(text_docs)}") # Text document count: 252
print(f"Image document count: {len(all_images)}") # Image document count: 328
我们总共得到 252 个文本块和 328 张图像。我创作了这么多图片有点令人惊讶,但我知道其中一些只是表格结果的图像。我们可以使用视觉模型来过滤掉不相关的图片,但在这里我跳过了这一步。
索引数据向量
如前所述,我们必须实例化两个向量存储,一个用于图像,另一个用于文本。CLIP 嵌入模型的维度是 512,而 ada-002 的维度是 1536。
text_store = Neo4jVectorStore(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
index_name="text_collection",
node_label="Chunk",
embedding_dimension=1536
)
image_store = Neo4jVectorStore(
url=NEO4J_URI,
username=NEO4J_USERNAME,
password=NEO4J_PASSWORD,
index_name="image_collection",
node_label="Image",
embedding_dimension=512
)
storage_context = StorageContext.from_defaults(vector_store=text_store)
现在向量存储已经初始化,我们使用 MultiModalVectorStoreIndex 来索引我们拥有的两种模态信息。
# Takes 10 min without GPU / 1 min with GPU on Google collab
index = MultiModalVectorStoreIndex.from_documents(
text_docs + all_images, storage_context=storage_context, image_vector_store=image_store
)
在幕后,MultiModalVectorStoreIndex 使用文本和图像嵌入模型来计算嵌入,并将结果存储和索引在 Neo4j 中。图像只存储 URL,而不存储实际的 base64 或其他图像表示。
多模态 RAG 管道
这段代码直接复制自 LlamaIndex 多模态 cookbook。我们首先定义一个多模态 LLM 和提示模板,然后将所有内容组合成一个查询引擎。
openai_mm_llm = OpenAIMultiModal(
model="gpt-4-vision-preview", max_new_tokens=1500
)
qa_tmpl_str = (
"Context information is below.\n"
"---------------------\n"
"{context_str}\n"
"---------------------\n"
"Given the context information and not prior knowledge, "
"answer the query.\n"
"Query: {query_str}\n"
"Answer: "
)
qa_tmpl = PromptTemplate(qa_tmpl_str)
query_engine = index.as_query_engine(
multi_modal_llm=openai_mm_llm, text_qa_template=qa_tmpl
)
现在我们可以继续测试它的性能如何。
query_str = "How do vector RAG application work?"
response = query_engine.query(query_str)
print(response)
响应

我们还可以可视化检索到的图像以及它们如何用于帮助生成最终答案。

LLM 收到了两张相同的图片作为输入,这只是表明我重复使用了一些我的图表。然而,我对 CLIP 嵌入感到惊喜,因为它们能够从集合中检索出最相关的图片。在更贴近生产环境的设置中,你可能希望清理并去除重复图片,但这超出了本文的范围。
结论
LLM 的演变速度比我们历史上习惯的要快得多,并且正在跨越多种模态。我坚信,到明年年底,LLM 将很快能够理解视频,并因此能够在与您交谈时捕捉非语言线索。另一方面,我们可以使用图像作为 RAG 管道的输入,增强传递给 LLM 的信息的丰富性,使响应更好、更准确。使用 LlamaIndex 和 Neo4j 实现多模态 RAG 管道非常简单。
代码已在GitHub上提供。