Skip to content
··9 menit baca

Membangun Chatbot Keuangan dengan Self-Evaluation: Perjalanan Melalui Data, Kode, dan Perjuangan

Saya membangun chatbot keuangan yang menjawab pertanyaan tentang S&P 500 menggunakan data SEC — dengan agent self-critique yang memperbaiki jawaban sebelum kamu melihatnya.

Update (2026): Proyek chatbot keuangan di bawah ini akhirnya menjadi agent S&P 500 (MVP diluncurkan Sep 2024). Setelah meluncurkan itu, saya mengalihkan fokus ke Sydney untuk konten blog dan dua produk baru: STRATUM (marketing intelligence) dan DIALOGUE (podcast generation). Pipeline data SEC dan pola self-evaluation yang dijelaskan di sini menginformasikan semuanya.

Tanya Sydney →


Postingan asli dari Apr 2024 dipertahankan di bawah untuk konteks.

Dalam postingan blog ini, saya ingin berbagi pengalaman saya mengerjakan proyek hobi yang menarik — membangun chatbot/agent keuangan dengan kemampuan self-evaluation. Proyek ini bertujuan membuat agent yang bisa menjawab pertanyaan tentang kondisi keuangan dan tren di perusahaan-perusahaan S&P 500 di AS, menggunakan data langsung dari filing resmi SEC untuk memastikan akurasi dan menghindari halusinasi. Manfaat utama dari agent ini adalah (atau akan jadi :P):

  • Memungkinkan kamu bertanya tentang kondisi keuangan, dan tren di perusahaan-perusahaan S&P 500 di AS.
  • Ia menggunakan data langsung dari filing keuangan resmi perusahaan-perusahaan ini dengan SEC, untuk menghindari halusinasi
  • Ia memberikan referensi detail di bawah setiap jawaban sehingga kamu bisa melakukan fact-check jika mau
  • Database-nya memiliki data filing keuangan 5-10 tahun terakhir sehingga kamu bisa meminta agent untuk memberikan reasoning tentang tren dari waktu ke waktu
  • Sebelum setiap jawaban dikembalikan ke kamu, ada agent lain, yang tugasnya mengkritik jawaban draft dari LLM dan mengusulkan cara memperbaikinya
  • Saran-saran ini dan seluruh konteks kemudian dibagikan dengan agent asli. Agent kemudian bisa memutuskan untuk memformulasikan query berbeda untuk mengambil informasi lebih baik dari database atau cukup memasukkan saran ke dalam jawaban akhir
  • Jawaban akhir ini kemudian diberikan ke pengguna.

Tentu saja, saya tidak punya akses ke Bloomberg terminal jadi saya tidak tahu apakah chatbot Bloomberg sudah bisa melakukan semua hal di atas. (Tebakan saya bahwa bisa — sampai tingkat tertentu. Seberapa bagus bagian self-critique dan revisinya, saya kurang yakin tapi ingin tahu :P)

Bagaimanapun, saya ingin mencoba ini karena terasa cukup kompleks untuk pembelajaran saya saat ini dan bisa berpotensi berguna.

Saya ingin menjawab pertanyaan seperti:

  • Bagaimana tren pengeluaran marketing Apple selama 5-10 tahun terakhir?
  • Apa saja akuisisi besar yang dilakukan oleh perusahaan XYZ selama 5 tahun terakhir
  • Bandingkan pengeluaran R&D Nvidia dan Microsoft selama 5 tahun terakhir
  • dll.

Ok jadi di mana saya dengan proyek ini? Apa yang saya pelajari? Apa perjuangannya?

Bagaimana cara saya mendapatkan data dari SEC?

Salah satu tantangan awal yang saya hadapi adalah mendapatkan data yang diperlukan dari SEC. Meskipun SEC menyediakan panduan dan dokumentasi tentang mengakses data EDGAR (dan di sini), butuh waktu untuk memahami proses mengunduh filing keuangan untuk setiap perusahaan secara massal.

Apakah ada cara lain bagi saya untuk melanjutkan ke langkah berikutnya tanpa harus mengunduh dan memproses filing dari SEC sendiri?

Saya menelusuri dokumentasi Langchain dan menemukan bahwa ada financial retriever bernama Kay.ai. Saya menguji retriever untuk melihat bagaimana workflow selanjutnya mungkin bekerja. Retriever bekerja sesuai harapan untuk query dasar. Namun, ia tidak mendukung panggilan asynchronous atau filtering metadata lanjutan. Jadi saya memutuskan untuk terus mengeksplorasi melakukan bagian ini sendiri.

Berbagi script Python yang sebenarnya

Script Python untuk mengunduh filing keuangan dari SEC EDGAR

Setelah banyak trial and error, dengan bantuan chatGPT, ini adalah kode yang secara otomatis mengunduh filing XBRL dan TXT dari database U.S. Securities and Exchange Commission (SEC). Ia dirancang untuk mengambil filing terbaru untuk perusahaan tertentu menggunakan Central Index Key (CIK) mereka.

Mengapa saya ingin mengunduh file .zip dan file .txt?

File .txt untuk setiap perusahaan sangat komprehensif. Ia memiliki banyak metadata berharga tentang setiap filing seperti jenis form (10K atau 10Q), periode pelaporan, tanggal filing, CIK/nama perusahaan, dll... Ini adalah jenis metadata yang akan saya butuhkan nanti untuk membangun agent. Semua ini tertangkap dengan rapi di bagian atas file seperti:

<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>

File .txt punya banyak data lain juga, selain laporan keuangan inti. Karena itu, setiap file sangat besar (seperti 10+ MB atau bahkan 40+ MB). Dan untuk satu perusahaan, saya ingin mengunduh semua 10K dan 10Q selama 5-10 tahun terakhir jadi kita bicara tentang minimal 20+ file per perusahaan. Mencoba memproses/membersihkan karakter yang tidak perlu/memecah file-file besar ini sangat tidak produktif karena akan memakan waktu lama dengan laptop biasa dan juga biaya embedding akan sangat besar. Jadi saya perlu menemukan cara lain.

Di sinilah file .zip berperan. Di setiap file .zip, kamu akan menemukan laporan keuangan inti dalam format .htm dan konten lainnya. Masalahnya adalah laporan keuangan inti ini tidak diberi nama secara konsisten selama bertahun-tahun, di berbagai perusahaan. Dan laporan .htm tidak memiliki semua metadata berharga dalam format yang rapi seperti file .txt.

Menggabungkan metadata dengan laporan keuangan utama

Ini adalah script yang mengotomasi proses mengekstrak laporan keuangan dan metadata terkait dari filing beberapa perusahaan. Ia dirancang untuk menangani file-file yang diunduh dari database SEC EDGAR, yang mencakup file .zip dan .txt.

Kamu bisa melihat bahwa saya membuat beberapa asumsi:

  • Kamu mengunduh versi .txt dan .zip untuk setiap laporan keuangan
  • Laporan keuangan inti adalah file .htm terbesar di dalam setiap file .zip. Ini "seharusnya" benar karena konten lainnya diekstrak dari file ".htm" utama

Membersihkan konten/karakter yang tidak perlu sebelum chunking

Kabar baiknya adalah dengan pendekatan di atas, setiap laporan (baik 10K atau 10Q) sekarang berukuran kurang dari 3 MB saja. Tapi itu masih terlalu panjang dan mengandung begitu banyak informasi yang tidak kita butuhkan jadi kita perlu membersihkannya lebih banyak lagi sebelum chunking. Kalau tidak, proses embedding akan berjalan sangat lama dan biayanya mahal. Bayangkan, kalau kamu hanya menghabiskan $0,1 per laporan untuk embedding, itu sudah sekitar $2,5/perusahaan untuk laporan 10K dan 10Q selama 5 tahun terakhir. Kalau kamu ingin mencakup sebagian besar S&P 500 atau memperpanjang periode ke 10 tahun terakhir, biayanya bertambah sangat cepat.

Jadi ini script untuk melakukan pembersihan. Setelah prosesnya, setiap output kurang dari 0,2 Mb, 10x lebih kecil. Setiap file masih memiliki semua metadata berharga yang kita bicarakan tadi.

Sekarang, kita siap untuk langkah chunking/embedding.

Vector store mana yang harus saya gunakan?

Ada banyak pilihan di sini (dengan lebih dari 50 pilihan vector store). Tapi karena saya perlu melakukan filtering lanjutan di langkah berikutnya menggunakan metadata, self-querying retriever tampaknya cocok. Saya katakan "tampaknya" karena mereka datang dengan kekurangan. Saya bereksperimen dengan beberapa pilihan, termasuk Chroma, ElasticSearch dan FAISS sejauh ini.

Meskipun Chroma dan ElasticSearch menyediakan fungsionalitas yang kokoh, ukuran indeks mereka relatif besar (550+ MB untuk Chroma dan 800+ MB untuk ElasticSearch). Indeks-indeks ini hanya mencakup embedding untuk 5 perusahaan uji SAJA. Ini tidak bagus karena saat saya scale out ke seluruh S&P 500, ukuran indeks akhir bisa 100x lebih besar. Lagi-lagi, tidak cocok untuk laptop lokal saya :D

Indeks FAISS, di sisi lain, hanya sekitar 200+ MB untuk 5 perusahaan uji yang sama. FAISS kurang memiliki banyak filtering lanjutan/integrasi native dengan Langchain, jadi saya perlu menyelidiki lebih lanjut. Kalau kamu punya saran, beri tahu saya. (Mungkin eksplorasi lebih lanjut tentang vector store alternatif seperti Weaviate atau Pinecone bisa bermanfaat?)

Query structuring

Meskipun bagus bahwa saya menyertakan banyak metadata ke setiap filing/chunk dan menjadikannya bagian dari embedding, disimpan di vector store, bagaimana kita memberitahu mesin filter mana yang harus digunakan? Lance dari Langchain menjelaskan "Query structuring for metadata filters" dalam video ini.

Saya mengikuti pendekatan tersebut dan membuat objek pydantic di bawah ini. Objek ini mencakup field yang sesuai dengan tag metadata aktual yang ditemukan dalam filing keuangan, seperti jenis form (10-K, 10-Q), periode pelaporan, tanggal filing, dan identitas perusahaan.

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.",
    )
    form_type: str = Field(
        None,
        description="Form type to filter by, such as 10-K or 10-Q.",
    )
# 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

Misalnya, ini daftar pertanyaan dan respons dari LLM. Kamu bisa melihat bahwa LLM melewatkan "conformed period of reporting" dalam beberapa kasus:

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'\}

Ok jadi apa yang saya harapkan dengan postingan panjang (PANJANG) ini?

1. Kode Python yang dibagikan membantu kamu dengan cara apa pun.

2. Kamu bisa berbagi kembali pemikiran/komentar atau memberi saran kepada saya tentang:

  • Bagaimana memperbaiki query structuring yang saya lakukan? FYI, saya juga menggunakan LLM untuk menghasilkan sub-pertanyaan terkait pertanyaan input.
  • Bagaimana saya bisa memperbaiki embedding/penggunaan vector store, terutama bagian filtering?
  • atau saran lain yang belum saya pikirkan :)

Update Jul 2024

Chatbot ini BELUM berfungsi jadi kalau kamu mencoba menggunakan chatbot saat ini di situs saya dan bertanya pertanyaan keuangan, ia tidak tahu :D

Perhatikan curiosity gap-nya :)

Pernahkah kamu bekerja dengan filing SEC atau mencoba membangun agent self-evaluating? Saya ingin sekali mendengar bagaimana kamu mengatasi tantangan vector store dan metadata filtering.

Salam,

Chandler

P.S: ada kursus baru yang saya rencanakan untuk ambil "Generative AI for Software Development Skill Certificate" di Coursera.

Lanjutkan Membaca

Perjalanan Saya
Terhubung
Bahasa
Preferensi