利用LlamaIndex和Gemini 2.0构建财务报告检索系统

利用LlamaIndex和Gemini 2.0构建财务报告检索系统

财务报告对于评估公司的健康状况至关重要。它们长达数百页,很难有效地提取具体的见解。分析师和投资者要花费数小时翻阅资产负债表、损益表和脚注,只为回答一些简单的问题,如:2024 年公司的收入是多少?随着 LLM 模型和矢量搜索技术的最新进展,我们可以使用 LlamaIndex 和相关框架自动进行财务报告分析。这篇博文将探讨我们如何使用 LlamaIndex、ChromaDB、Gemini2.0 和 Ollama 构建一个强大的财务 RAG 系统,精确地回答来自冗长报告的查询。

学习目标

  • 了解高效分析对财务报告检索系统的需求。
  • 了解如何使用 LlamaIndex 对财务报告进行预处理和矢量化。
  • 探索 ChromaDB,为文档检索构建强大的向量数据库。
  • 使用 Gemini 2.0 和 Llama 3.2 为金融数据分析实施查询引擎。
  • 使用 LlamaIndex 探索高级查询路由技术,以增强洞察力。

为什么需要财务报告检索系统?

财务报告包含有关公司业绩的重要信息,包括收入、支出、负债和盈利能力。然而,这些报告篇幅巨大、冗长,而且充满专业术语,分析师、投资者和高管手动提取相关信息非常耗时。

财务报告检索系统可通过自然语言查询实现这一过程的自动化。用户可以简单地提出 “2023 年的收入是多少?”或 “总结一下 2023 年的流动性问题”等问题,而无需搜索 PDF 文件系统会快速检索并总结相关部分,从而节省人工操作的时间。

项目实施

要实施项目,我们首先需要设置环境并安装所需的库:

步骤 1:设置环境

首先,我们将创建一个用于开发工作的 conda 环境。

$conda create --name finrag python=3.12
$conda activate finrag

步骤 2:安装必要的Python库

安装 libraires 是任何项目实施的关键步骤:

$pip install llama-index llama-index-vector-stores-chroma chromadb
$pip install llama-index-llms-gemini llama-index-llms-ollama
$pip install llama-index-embeddings-gemini llama-index-embeddings-ollama
$pip install python-dotenv nest-asyncio pypdf

步骤 3:创建项目目录

现在创建一个项目目录,并创建一个名为 .env 的文件,在该文件中放入所有 API 密钥,以便安全管理 API 密钥。

# on .env file
GOOGLE_API_KEY="<your-api-key>"

我们从 .env 文件加载环境变量,以安全地存储敏感的 API 密钥。这将确保我们的双子座应用程序接口(Gemini API)或谷歌应用程序接口(Google API)始终受到保护。

我们将使用 Jupyter Notebook 完成项目。创建一个 Jupyter Notebook 文件,然后开始逐步实施。

步骤 4:加载API密钥

现在,我们将加载下面的 API 密钥:

import os
from dotenv import load_dotenv
load_dotenv()
GEMINI_API_KEY = os.getenv("GOOGLE_API_KEY")
# Only to check .env is accessing properly or not.
# print(f"GEMINI_API_KEY: {GEMINI_API_KEY}")

现在,我们的环境已经准备就绪,可以进入下一个最重要的阶段了。

使用Llamaindex处理文件

从 AnnualReports 网站收集赛车游戏公司的财务报告。

点击此处下载。

第一页看起来像

赛车游戏公司的财务报告

Source: Report

这些报告总共有 123 页,但我只需将报告中的财务报表提取出来,然后为我们的项目创建一个新的 PDF。

我是怎么做的呢?使用 PyPDF 库非常简单。

from pypdf import PdfReader
from pypdf import PdfWriter
reader = PdfReader("NASDAQ_MSGM_2023.pdf")
writer = PdfWriter()
# page 66 to 104 have financial statements.
page_to_extract = range(66, 104)
for page_num in page_to_extract:
writer.add_page(reader.pages[page_num])
output_pdf = "Motorsport_Games_Financial_report.pdf"
with open(output_pdf, "wb") as outfile:
writer.write(output_pdf)
print(f"New PDF created: {output_pdf}")

新报告文件只有 38 页,这有助于我们快速嵌入文件。

加载和分割财务报告

在项目数据目录中,放入新创建的 Motorsport_Games_Financial_report.pdf 文件,该文件将为项目编制索引。

财务报告通常为 PDF 格式,包含大量表格数据、脚注和法律声明。我们使用 LlamaIndex 的 SimpleDirectoryReader 来加载这些文件并将其转换为文档。

from llama_index.core import SimpleDirectoryReader
documents = SimpleDirectoryReader("./data").load_data()

由于报告的篇幅非常大,无法作为单个文档进行处理,因此我们将其分割成较小的块或节点。每个小块对应一个页面或部分,有助于更有效地检索。

from copy import deepcopy
from llama_index.core.schema import TextNode
def get_page_nodes(docs, separator="\n---\n"):
"""Split each document into page node, by separator."""
nodes = []
for doc in docs:
doc_chunks = doc.text.split(separator)
for doc_chunk in doc_chunks:
node = TextNode(
text=doc_chunk,
metadata=deepcopy(doc.metadata),
)
nodes.append(node)
return nodes

要了解文件提取过程,请参阅下图。

文件提取过程

现在,我们的财务数据已经准备好进行矢量化和存储以备检索。

使用ChromaDB建立矢量数据库

我们将使用 ChromaDB 快速、准确地建立本地矢量数据库。我们的金融文本嵌入式表示法将存储到 ChromaDB 中。

我们将初始化矢量数据库,并使用 Ollama 配置 Nomic-embed-text 模型,以生成本地嵌入。

import chromadb
from llama_index.llms.gemini import Gemini
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.vector_stores.chroma import ChromaVectorStore
from llama_index.core import Settings
embed_model = OllamaEmbedding(model_name="nomic-embed-text")
chroma_client = chromadb.PersistentClient(path="./chroma_db")
chroma_collection = chroma_client.get_or_create_collection("financial_collection")
vector_store = ChromaVectorStore(chroma_collection=chroma_collection)

最后,我们使用 LLamaIndex 的 VectorStoreIndex 创建一个矢量索引。该索引将我们的矢量数据库与 LlamaIndex 的查询引擎连接起来。

from llama_index.core import VectorStoreIndex, StorageContext
storage_context = StorageContext.from_defaults(vector_store=vector_store)
vector_index = VectorStoreIndex.from_documents(documents=documents, storage_context=storage_context, embed_model=embed_model)

上述代码将使用 nomic-embed-text 从金融文本文件中创建矢量索引。这需要时间,具体取决于本地系统的规范。

索引创建完成后,您就可以在必要时重复使用嵌入的代码,而无需重新创建索引。

vector_index = VectorStoreIndex.from_vector_store(
vector_store=vector_store, embed_model=embed_model
)

这将允许你使用存储中的 chromadb 嵌入文件。

现在,我们的重载工作已经完成,是时候查询报告并放松一下了。

使用Gemini 2.0查询财务数据

一旦我们的财务数据建立了索引,我们就可以提出自然语言问题并得到准确的答案。我们将使用 Gemini-2.0 Flash 模型进行查询,该模型可与我们的矢量数据库交互,获取相关部分并生成有见地的回复。

设置Gemini 2.0

from llama_index.llms.gemini import Gemini
llm = Gemini(api_key=GEMINI_API_KEY, model_name="models/gemini-2.0-flash")

使用带有矢量索引的Gemini 2.0启动查询引擎

query_engine = vector_index.as_query_engine(llm=llm, similarity_top_k=5)

示例查询和响应

下面是多个查询和不同的响应:

查询-1

response = query_engine.query("what is the revenue of on 2022 Year Ended December 31?")
print(str(response))

响应基于报告的查询响应1

来自报告的相应图片:

基于报告的查询响应1的原数据出处

查询-2

response = query_engine.query(
"what is the Net Loss Attributable to Motossport Games Inc. on 2022 Year Ended December 31?"
)
print(str(response))

响应

基于报告的查询响应2

来自报告的相应图片:

基于报告的查询响应2的原数据出处

查询-3

response = query_engine.query(
"What are the Liquidity and Going concern for the Company on December 31, 2023"
)
print(str(response))

响应

基于报告的查询响应3

查询-4

response = query_engine.query(
"Summarise the Principal versus agent considerations of the company?"
)
print(str(response))

响应

基于报告的查询响应4

来自报告的相应图片:

基于报告的查询响应4原数据出处

查询-5

response = query_engine.query(
"Summarise the Net Loss Per Common Share of the company with financial data?"
)
print(str(response))

响应

基于报告的查询响应5

来自报告的相应图片:

基于报告的查询响应5原数据出处

查询-6

response = query_engine.query(
"Summarise Property and equipment consist of the following balances as of December 31, 2023 and 2022 of the company with financial data?"
)
print(str(response))

响应

基于报告的查询响应6

来自报告的相应图片:

基于报告的查询响应6原数据出处

查询-7

response = query_engine.query(
"Summarise The Intangible Assets on December 21, 2023 of the company with financial data?"
)
print(str(response))

响应

基于报告的查询响应7

查询-8

response = query_engine.query(
"What are leases of the company with yearwise financial data?"
)
print(str(response))

响应

基于报告的查询响应8

来自报告的相应图片:

基于报告的查询响应8原数据出处

使用Llama 3.2进行本地查询

在本地利用 Llama 3.2 查询财务报告,而无需依赖基于云的模型。

设置Llama 3.2:1b

local_llm = Ollama(model="llama3.2:1b", request_timeout=1000.0)
local_query_engine = vector_index.as_query_engine(llm=local_llm, similarity_top_k=3)

查询-9

response = local_query_engine.query(
"Summary of chart of Accrued expenses and other liabilities using the financial data of the company"
)
print(str(response))

响应

基于报告的查询响应9

来自报告的相应图片:

基于报告的查询响应9原数据出处

使用LlamaIndex进行高级查询路由选择

有时,我们既需要详细的检索,也需要总结性的见解。我们可以通过结合矢量索引和摘要索引来实现这一点。

  • 矢量索引用于精确的文档检索
  • 摘要索引用于简明的财务摘要

我们已经建立了矢量索引,现在我们将创建一个摘要索引,使用分层方法来总结财务报表。

from llama_index.core import SummaryIndex
summary_index = SummaryIndex(nodes=page_nodes)

然后集成 RouterQueryEngine,它可根据查询类型有条件地决定是从摘要索引还是矢量索引。

from llama_index.core.tools import QueryEngineTool
from llama_index.core.query_engine.router_query_engine import RouterQueryEngine
from llama_index.core.selectors import LLMSingleSelector

现在创建摘要查询引擎

summary_query_engine = summary_index.as_query_engine(
llm=llm, response_mode="tree_summarize", use_async=True
)

该摘要查询引擎将被集成到摘要工具中,而矢量查询引擎将被集成到矢量工具中。

# Creating summary tool
summary_tool = QueryEngineTool.from_defaults(
query_engine=summary_query_engine,
description=(
"Useful for summarization questions related to Motorsport Games Company."
),
)
# Creating vector tool
vector_tool = QueryEngineTool.from_defaults(
query_engine=query_engine,
description=(
"Useful for retriving specific context from the Motorsport Games Company."
),
)

这两种工具都已完成,现在我们通过路由器将这些工具连接起来,这样当查询通过路由器时,路由器就会通过分析用户查询来决定使用哪种工具。

# Router Query Engine
adv_query_engine = RouterQueryEngine(
llm=llm,
selector=LLMSingleSelector.from_defaults(llm=llm),
query_engine_tools=[summary_tool, vector_tool],
verbose=True,
)

我们的高级查询系统已全部安装完毕,现在可查询我们新推出的高级查询引擎。

查询-10

response = adv_query_engine.query(
"Summarize the charts describing the revenure of the company."
)
print(str(response))

响应

基于报告的查询响应10原数据出处

您可以看到,我们的智能路由器会决定使用摘要工具,因为用户在查询中要求摘要。

查询-11

response = adv_query_engine.query("What is the Total Assets of the company Yearwise?")
print(str(response))

响应

基于报告的查询响应12

在这里,路由器选择了矢量工具,因为用户询问的是具体信息,而不是摘要。

本文使用的所有代码都在这里

小结

我们可以利用 LlamaIndex、ChromaDB 和高级 LLM 高效分析财务报告。该系统可实现自动财务洞察、实时查询和强大的汇总功能。这类系统使财务分析更方便、更高效,从而在投资、交易和经营过程中做出更好的决策。

  • 由 LLM 驱动的文档检索系统可大幅减少分析复杂财务报告所花费的时间。
  • 使用云和本地 LLM 的混合方法可确保系统设计的成本效益、隐私保护和灵活性。
  • LlamaIndex 的模块化框架可以轻松实现财务报告整理工作流程的自动化。
  • 这类系统可适用于法律文件、医疗报告和监管备案等不同领域,因此是一种通用的 RAG 解决方案。

常见问题

Q1. 系统如何处理不同的财务报告?

A. 系统设计用于处理任何结构化的财务文件,将其分解为文本块,嵌入并存储在 ChromaDB 中。新报告可以动态添加,无需重新建立索引。

Q2. 能否将其扩展到生成财务图表和可视化效果?

A. 可以,通过集成 Matplotlib、Pandas 和 Streamlit,您可以将收入增长、净亏损分析或资产分布等趋势可视化。

Q3. 查询路由系统如何提高准确性?

A. RouterQueryEngine 会自动检测查询是否需要汇总响应或特定的财务数据检索。这样可以减少不相关的输出,确保回复的准确性。

Q4. 该系统是否适用于实时财务分析?

A. 可以,但这取决于向量存储更新的频率。您可以使用 OpenAI 嵌入式应用程序接口(API)持续摄取管道,动态查询实时财务报告。

评论留言