Skip to content
··4分で読めます

自己評価する金融チャットボットの構築:データ、コード、そして苦悩の旅

SECデータを使ってS&P 500の質問に答える金融チャットボットを構築しました。回答を表示する前に自己批評エージェントが改善を行います。

更新(2026年): 以下の金融チャットボットプロジェクトは、最終的にS&P 500エージェント(2024年9月にMVPをローンチ)になりました。それをリリースした後、ブログコンテンツ向けのSydneyと2つの新製品:STRATUM(マーケティングインテリジェンス)と**DIALOGUE**(ポッドキャスト生成)にフォーカスを移しました。ここで説明しているSECデータパイプラインと自己評価パターンは、これらすべてに影響を与えました。

Sydneyに聞く →


2024年4月のオリジナル投稿を以下にそのまま残しています。

このブログ記事では、魅力的な趣味プロジェクトでの経験を共有したいと思います。それは自己評価機能を持つ金融チャットボット/エージェントの構築です。このプロジェクトは、米国のS&P 500企業の財務状況やトレンドに関する質問に答えられるエージェントの作成を目指しています。正確性を確保しハルシネーションを避けるために、SECの公式提出書類のデータを直接使用しています。このエージェントの主なメリットは(またはこれからそうなる予定です :P):

  • 米国のS&P 500企業の財務状況やトレンドについて質問できます
  • これらの企業がSECに提出した公式財務書類のデータを直接使用し、ハルシネーション回避します
  • 各回答の下に詳細な参照情報を提供するので、必要に応じて回答をファクトチェックできます
  • データベースには過去5〜10年分の財務提出データが含まれているため、時系列のトレンドについてエージェントに推論させることができます
  • 各回答がユーザーに返される前に、LLMのドラフト回答批評し、改善方法を提案する別のエージェントが存在します
  • これらの提案と全体のコンテキストが元のエージェントに共有されます。エージェントはデータベースからより良い情報を取得するために異なるクエリを策定するか、単に提案を最終回答に組み込むことができます
  • この最終回答がユーザーに提供されます

もちろん、Bloombergターミナルにアクセスできないので、Bloombergチャットボットが上記のすべてをすでに実行できるかどうかはわかりません。(おそらくある程度はできるというのが私の推測です。自己批評部分と修正がどれほど優れているかは確信がありませんが、知りたいですね :P)

いずれにせよ、私の現在の学習段階で十分に複雑であり、潜在的に役立つ可能性があるため、これを試してみたかったのです。

以下のような質問に答えたいと思っています:

  • 過去5〜10年間のAppleのマーケティング費用のトレンドは?
  • XYZ企業が過去5年間に行った主要な買収は?
  • 過去5年間のNvidiaとMicrosoftのR&D費用を比較してください
  • など

このプロジェクトの進捗状況はどうでしょうか?何を学びましたか?苦労している点は?

SECからデータを取得するには?

最初に直面した課題の一つは、SECから必要なデータを取得することでした。SECはEDGARデータへのアクセスに関するガイドとドキュメントを提供していますが(こちらも参照)、各企業の財務提出書類を大規模にダウンロードするプロセスを理解するのに時間がかかりました。

SEC提出書類を自分でダウンロード・処理せずに次のステップに進む方法はないか?

Langchainのドキュメントを調べたところ、Kay.aiという金融リトリーバーがあることを発見しました。残りのワークフローがどのように機能するかを確認するためにリトリーバーをテストしました。リトリーバーは基本的なクエリに対しては期待通りに動作しました。しかし、非同期呼び出しや高度なメタデータフィルタリングはサポートしていませんでした。そこで、この部分は自分で探求することにしました。

実際のPythonスクリプトの共有

SEC EDGARから財務提出書類をダウンロードするPythonスクリプト

多くの試行錯誤の末、chatGPTの助けを借りて、こちらがXBRLおよびTXT提出書類をU.S. Securities and Exchange Commission(SEC)データベースから自動的にダウンロードするコードです。Central Index Key(CIK)を使用して、指定した企業の最近の提出書類を取得するように設計されています。

なぜ.zipファイルと.txtファイルの両方をダウンロードしたいのか?

各企業の.txtファイルは非常に包括的です。フォームタイプ(10Kまたは10Q)、報告期間、提出日、企業CIK/名前など、各提出書類に関する多くの貴重なメタデータが含まれています。これらは後でエージェントを構築する際に必要となるメタデータの種類です。これらはすべてファイルの先頭に整然とキャプチャされています:

<SEC-DOCUMENT>0000320193-19-000119.txt : 20191031
<SEC-HEADER>0000320193-19-000119.hdr.sgml : 20191031
<ACCEPTANCE-DATETIME>20191030181236
ACCESSION NUMBER:		0000320193-19-000119
CONFORMED SUBMISSION TYPE:	10-K
PUBLIC DOCUMENT COUNT:		96
CONFORMED PERIOD OF REPORT:	20190928
FILED AS OF DATE:		20191031
DATE AS OF CHANGE:		20191030

FILER:

	COMPANY DATA:
		COMPANY CONFORMED NAME:			Apple Inc.
		CENTRAL INDEX KEY:			0000320193
		STANDARD INDUSTRIAL CLASSIFICATION:	ELECTRONIC COMPUTERS [3571]
		IRS NUMBER:				942404110
		STATE OF INCORPORATION:			CA
		FISCAL YEAR END:			0928

	FILING VALUES:
		FORM TYPE:		10-K
		SEC ACT:		1934 Act
		SEC FILE NUMBER:	001-36743
		FILM NUMBER:		191181423

	BUSINESS ADDRESS:
		STREET 1:		ONE APPLE PARK WAY
		CITY:			CUPERTINO
		STATE:			CA
		ZIP:			95014
		BUSINESS PHONE:		(408) 996-1010

	MAIL ADDRESS:
		STREET 1:		ONE APPLE PARK WAY
		CITY:			CUPERTINO
		STATE:			CA
		ZIP:			95014

	FORMER COMPANY:
		FORMER CONFORMED NAME:	APPLE INC
		DATE OF NAME CHANGE:	20070109

	FORMER COMPANY:
		FORMER CONFORMED NAME:	APPLE COMPUTER INC
		DATE OF NAME CHANGE:	19970808
</SEC-HEADER>

.txtファイルにはコアの財務レポート以外にも膨大なデータが含まれています。そのため、各ファイルは非常に大きく(10MB以上、場合によっては40MB以上)なります。1つの企業について過去5〜10年分のすべての10Kと10Qをダウンロードしたいので、企業あたり簡単に20以上のファイルになります。これらの大きなファイルの不要な文字を処理/クリーンアップ/チャンク化しようとするのは非常に非生産的です。通常のラップトップでは長時間かかり、エンベディングのコストも天文学的になるからです。そこで別の方法を見つける必要がありました。

ここで.zipファイルの出番です。各.zipファイルの中には、コアの財務レポートが.htm形式で含まれており、その他のコンテンツもあります。問題は、これらのコアの財務レポートが年度や企業間で一貫した命名がされていないことです。また、.htmレポートには.txtファイルのような整然とした形式で貴重なメタデータがすべて含まれているわけではありません。

メタデータとメインの財務レポートの結合

こちらが、複数の企業の提出書類から財務諸表と関連メタデータを抽出するプロセスを自動化するスクリプトです。SEC EDGARデータベースからダウンロードした.zipファイルと.txtファイルの両方を処理するように設計されています。

いくつかの前提条件を設けていることがわかるでしょう:

  • 各財務レポートの.txtと.zipの両バージョンをダウンロードすること
  • コアの財務レポートは各.zipファイル内で最も大きい.htmファイルであること。メインの「.htm」ファイルからその他のコンテンツが抽出されるため、これは「正しいはず」です

チャンキング前の不要なコンテンツ/文字のクリーンアップ

良いニュースは、上記のアプローチを使用することで、各レポート(10Kまたは10Q)のサイズが3MB未満になったことです。しかし、まだ長すぎて不要な情報が多く含まれているため、チャンキングの前にさらにクリーンアップする必要があります。そうしないと、エンベディングプロセスが非常に長くなり、多くのコストがかかります。想像してみてください。1レポートあたりのエンベディングにわずか$0.1かかるだけでも、過去5年間の10Kと10Qレポートで1企業あたり約$2.5になります。S&P 500のほとんどをカバーしたり、期間を過去10年に延長したりすると、あっという間に膨れ上がります。

そこでこちらがクリーニングを行うスクリプトです。処理後、各出力は0.2MB未満で、10倍小さくなりました。各ファイルには先ほど話した貴重なメタデータがすべて含まれています。

これでチャンキング/エンベディングのステップに進む準備ができました。

どのベクトルストアを使うべきか?

こちらには多くの選択肢があります(50以上のベクトルストアオプション)。しかし、次のステップでメタデータを使った高度なフィルタリングが必要なため、self-querying retrieverが適しているように見えます。「見える」と言うのは、デメリットもあるからです。これまでChroma、ElasticSearch、FAISSなどを試しました。

ChromaとElasticSearchは堅牢な機能を提供しましたが、インデックスサイズが比較的大きかったです(Chromaで550MB以上、ElasticSearchで800MB以上)。これらのインデックスはテスト用の5社のみのエンベディングです。S&P 500の残りに拡張すると最終的なインデックスサイズが100倍大きくなる可能性があるため、これは良くありません。やはり私のローカルラップトップには適していません :D

一方、FAISSインデックスは同じ5社のテストで約200MB以上です。FAISSにはLangchainとの高度なフィルタリング/ネイティブ統合機能の多くが欠けているため、さらに調査する必要があります。何か提案があれば教えてください。(WeaviateやPineconeのような代替ベクトルストアの更なる探求が有益かもしれません。)

クエリ構造化

各提出書類/チャンクに多くのメタデータを含め、エンベディングの一部としてベクトルストアに格納するのは良いのですが、どのフィルターを使うかをマシンにどう伝えればよいのでしょうか?LangchainのLanceがこちらの動画で「メタデータフィルターのクエリ構造化」について説明しています。

このアプローチに従い、以下のpydanticオブジェクトを作成しました。このオブジェクトには、フォームタイプ(10-K、10-Q)、報告期間、提出日、企業識別子など、財務提出書類の実際のメタデータタグに対応するフィールドが含まれています。

import datetime
from typing import Optional
from pydantic import BaseModel, Field

class FinancialFilingsSearch(BaseModel):
    """Search over a database of financial filings for a company, using the actual metadata tags from the filings."""

    content_search: str = Field(
        ...,
        description="Similarity search query applied to the content of the financial filings with the SEC.",
    )
    conformed_submission_type: str = Field(
        None,
        description="Filter for the type of the SEC filing, such as 10-K (annual report) or 10-Q (quarterly report). ",
    )
    conformed_period_of_report: Optional[datetime.date] = Field(
        None,
        description= "Filter for the end date (format: YYYYMMDD) of the reporting period for the filing. For a 10-Q, it's the quarter-end date, and for a 10-K, it's the fiscal year-end date. ",
    )
    filed_as_of_date: Optional[datetime.date] = Field(
        None,
        description="Filter for the date (YYYYMMDD) on which the filing was officially submitted to the SEC. Only use if explicitly specified.",
    )
    # date_as_of_change: Optional[datetime.date] = Field(
    #     None,
    #     description="If any information in the filing was updated or amended after the initial filing date, this date reflects when those changes were made.",
    # )
    company_conformed_name: str = Field(
        None,
        description="Filter for official name of the company as registered with the SEC",
    )
    central_index_key: str = Field(
        None,
        description="Central Index Key (CIK): A unique identifier assigned by the SEC to all entities (companies, individuals, etc.) who file with the SEC.",
    )
    standard_industrial_classification: Optional[str] = Field(
        None,
        description="he Standard Industrial Classification Codes that appear in a company's disseminated EDGAR filings indicate the company's type of business. Only use if explicitly specified.",
    )
    # irs_number: Optional[str] = Field(
    #     None,
    #     description="IRS number to filter by.",
    # )
    # state_of_incorporation: Optional[str] = Field(
    #     None,
    #     description="State of incorporation to filter by.",
    # )
    # fiscal_year_end: Optional[str] = Field(
    #     None,
    #     description="The end date of the company's fiscal year, which is used for financial reporting and taxation purposes, like Dec 31 or Sep30",
    # )
    form_type: str = Field(
        None,
        description="Form type to filter by, such as 10-K or 10-Q.",
    )
    # sec_file_number: Optional[str] = Field(
    #     None,
    #     description="SEC file number to filter by.",
    # )
    # film_number: Optional[str] = Field(
    #     None,
    #     description="Film number to filter by.",
    # )
    # former_company: Optional[str] = Field(
    #     None,
    #     description="Former company name to filter by.",
    # )
    # former_conformed_name: Optional[str] = Field(
    #     None,
    #     description="Former conformed name to filter by.",
    # )
    # date_of_name_change: Optional[datetime.date] = Field(
    #     None,
    #     description="Date of name change to consider.",
    # )
# Set up language models
llm_35 = ChatOpenAI(model="gpt-3.5-turbo-0125", temperature=0)  # GPT-3.5 model
llm_4 = ChatOpenAI(model="gpt-4-turbo-2024-04-09", temperature=0)  # GPT-4 model for more complex tasks

from langchain_core.prompts import ChatPromptTemplate

system = """You are an expert at converting user questions into database queries. \
You have access to a vector store of financial filings from public companies to the SEC, for building LLM-powered application. \
Given a question, return a detailed database query optimized to retrieve the most relevant results. \
Be as detailed as  possible with your returned query, including all relevant fields and filters. \
Always include conformed_period_of_report. \

If there are acronyms or words you are not familiar with, do not try to rephrase them."""

prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("human", "\{question\}"),
    ]
)

# Assuming `llm` is your already initialized LLM instance
structured_llm = llm_35.with_structured_output(FinancialFilingsSearch)
query_analyzer = prompt | structured_llm

例えば、以下は質問のリストとLLMからの回答です。いくつかのケースでLLMが「conformed period of reporting」を見落としていることがわかります:

Question: What was Google's advertising and marketing spending in the 10-K report for the year 2018?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'central_index_key': '0001652044'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2019?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'central_index_key': '0001652044'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2020?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'conformed_period_of_report': '2020'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2021?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'conformed_submission_type': '10-K', 'form_type': '10-K', 'conformed_period_of_report': '2021'\}
Question: What was Google's advertising and marketing spending in the 10-K report for the year 2022?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'form_type': '10-K'\}
Question: How has Google's advertising and marketing spending trended from 2018 to 2022 according to 10-K filings?
\{'content_search': 'advertising and marketing spending', 'company_conformed_name': 'Alphabet Inc.', 'form_type': '10-K'\}

この長い(本当に長い)投稿で達成したいこと

1. 共有したPythonコードが何らかの形でお役に立てること

2. 以下について皆さんの考え/コメントやアドバイスを共有していただけること:

  • 私が行っているクエリ構造化をどう改善できるか?ちなみに、入力された質問に関連するサブクエスチョンの生成にもLLMを使用しています
  • エンベディング/ベクトルストアの使用、特にフィルタリング部分をどう改善できるか?
  • その他、私が考えもしなかった提案 :)

2024年7月の更新

このチャットボットはまだ動作していませんので、私のサイトの現在のチャットボットで金融の質問をしても、答えられません :D

好奇心の隙間に注意してくださいね :)

SEC提出書類を扱ったり、自己評価エージェントを構築したことはありますか?ベクトルストアとメタデータフィルタリングの課題にどう取り組んだか、ぜひお聞きしたいです。

よろしくお願いします、Chandler

P.S: Courseraで受講予定の新しくリリースされたコース「Generative AI for Software Development Skill Certificate」があります。

続きを読む

私の歩み
つながる
言語
設定