Xây dựng Chatbot tài chính tự đánh giá: Hành trình qua dữ liệu, code và vật lộn
Tôi xây một chatbot tài chính trả lời câu hỏi về S&P 500 sử dụng dữ liệu SEC — với agent tự phê bình cải thiện câu trả lời trước khi bạn thấy chúng.
Cập nhật (2026): Dự án chatbot tài chính bên dưới cuối cùng trở thành S&P 500 agent (MVP ra mắt tháng 9 2024). Sau khi ship, tôi chuyển trọng tâm sang Sydney cho nội dung blog và hai sản phẩm mới: STRAŦUM (marketing intelligence) và DIALØGUE (podcast generation). Pipeline dữ liệu SEC và pattern tự đánh giá được mô tả ở đây đã ảnh hưởng đến tất cả.
Bài viết gốc từ tháng 4 năm 2024 được giữ nguyên bên dưới cho bối cảnh.
Trong bài viết này, tôi muốn chia sẻ kinh nghiệm làm việc trên một dự án thú vị — xây dựng chatbot/agent tài chính với khả năng tự đánh giá. Dự án này nhắm tạo một agent có thể trả lời câu hỏi về tình hình tài chính và xu hướng của các công ty trong S&P 500 ở Mỹ, sử dụng dữ liệu trực tiếp từ hồ sơ SEC chính thức để đảm bảo độ chính xác và tránh hallucination. Lợi ích chính của agent này là (hoặc sẽ là :P):
- Cho phép bạn hỏi câu hỏi về tình hình tài chính, và xu hướng của các công ty trong S&P 500 ở Mỹ.
- Nó sử dụng dữ liệu trực tiếp từ hồ sơ tài chính chính thức của các công ty này với SEC, để tránh hallucination
- Nó cung cấp tham chiếu chi tiết bên dưới mỗi câu trả lời để bạn có thể kiểm chứng nếu muốn
- Database có dữ liệu hồ sơ tài chính 5-10 năm gần nhất nên bạn có thể yêu cầu agent phân tích xu hướng theo thời gian
- Trước khi mỗi câu trả lời được trả về cho bạn, có một agent khác, có nhiệm vụ phê bình câu trả lời nháp của LLM và đề xuất cách cải thiện
- Các đề xuất này và toàn bộ bối cảnh sau đó được chia sẻ với agent gốc. Agent có thể quyết định xây dựng query khác để lấy thông tin tốt hơn từ database hoặc đơn giản tích hợp các đề xuất vào câu trả lời cuối cùng
- Câu trả lời cuối cùng này sau đó được cung cấp cho người dùng.
Rõ ràng, tôi không có quyền truy cập Bloomberg terminal nên không biết chatbot của Bloomberg đã có thể làm tất cả những điều trên chưa. (Dự đoán có cơ sở của tôi là có thể — ở một mức độ nhất định. Phần tự phê bình và sửa đổi tốt đến đâu, tôi không chắc nhưng rất muốn biết :P)
Dù sao, tôi muốn thử vì nó đủ phức tạp cho việc học của tôi ở giai đoạn này và có thể hữu ích.
Tôi muốn trả lời những câu hỏi như:
- Chi phí marketing của Apple có xu hướng gì trong 5-10 năm qua?
- Tất cả các thương vụ mua lại lớn của công ty XYZ trong 5 năm qua là gì?
- So sánh chi tiêu R&D của Nvidia và Microsoft trong 5 năm qua
- v.v.
Ok vậy tôi đang ở đâu với dự án này? Tôi đã học được gì? Những khó khăn là gì?
Làm sao lấy dữ liệu từ SEC?
Một trong những thách thức ban đầu là lấy dữ liệu cần thiết từ SEC. Mặc dù SEC cung cấp hướng dẫn và tài liệu về truy cập dữ liệu EDGAR (và ở đây), mất một thời gian để hiểu quy trình tải hồ sơ tài chính cho từng công ty ở quy mô lớn.
Có cách nào khác để tiến đến bước tiếp theo mà không cần tự tải và xử lý hồ sơ từ SEC?
Tôi xem qua tài liệu Langchain và thấy có financial retriever gọi là Kay.ai. Tôi test retriever để xem phần còn lại của workflow có thể hoạt động thế nào. Retriever hoạt động như mong đợi cho các query cơ bản. Tuy nhiên, nó không hỗ trợ gọi bất đồng bộ hay lọc metadata nâng cao. Nên tôi quyết định tiếp tục tự khám phá phần này.
Chia sẻ Python script thực tế
Python script để tải hồ sơ tài chính từ SEC EDGAR
Sau nhiều thử nghiệm, với sự giúp đỡ của chatGPT, đây là code tự động tải hồ sơ XBRL và TXT từ database của SEC. Nó được thiết kế để lấy hồ sơ gần đây cho các công ty cụ thể sử dụng Central Index Key (CIK) của họ.
Tại sao tôi muốn tải cả file .zip và file .txt?
File .txt cho mỗi công ty cực kỳ toàn diện. Nó có nhiều metadata giá trị về mỗi hồ sơ như loại form (10K hoặc 10Q), kỳ báo cáo, ngày nộp, CIK/tên công ty, v.v... Đây là các loại metadata tôi sẽ cần sau này để xây agent. Tất cả được ghi neatly ở đầu file như:
<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 còn có rất nhiều dữ liệu khác, ngoài báo cáo tài chính cốt lõi. Vì thế, mỗi file rất lớn (như 10+ MB hoặc thậm chí 40+ MB). Và cho một công ty, tôi muốn tải tất cả 10K và 10Q trong 5-10 năm qua nên chúng ta đang nói về dễ dàng 20+ file mỗi công ty. Cố gắng xử lý/dọn ký tự thừa/chunk những file lớn này là cực kỳ không hiệu quả vì nó mất rất lâu với laptop bình thường và chi phí embedding sẽ khổng lồ. Nên tôi cần tìm cách khác.
Đây là lúc file .zip hữu ích. Trong mỗi file .zip, bạn sẽ tìm thấy báo cáo tài chính cốt lõi ở định dạng .htm và các nội dung khác. Vấn đề là các báo cáo tài chính cốt lõi này không được đặt tên nhất quán qua các năm, giữa các công ty. Và báo cáo .htm không có tất cả metadata giá trị ở định dạng gọn gàng như file .txt.
Kết hợp metadata với báo cáo tài chính chính
Đây là script tự động hóa quá trình trích xuất báo cáo tài chính và metadata liên quan từ hồ sơ của nhiều công ty. Nó được thiết kế để xử lý các file tải từ SEC EDGAR database, bao gồm cả file .zip và .txt.
Bạn có thể thấy tôi đã đưa ra một số giả định:
- Bạn tải cả phiên bản .txt và .zip cho mỗi báo cáo tài chính
- Báo cáo tài chính cốt lõi là file .htm lớn nhất trong mỗi file .zip. Điều này "nên" đúng vì nội dung khác được trích xuất từ file ".htm" chính
Dọn dẹp nội dung/ký tự thừa trước khi chunking
Tin tốt là với cách tiếp cận trên, mỗi báo cáo (10K hoặc 10Q) giờ có kích thước dưới 3 MB. Nhưng vẫn quá dài và chứa quá nhiều thông tin không cần thiết nên cần dọn thêm trước khi chunking. Nếu không, quá trình embedding sẽ chạy rất lâu và tốn nhiều tiền. Tưởng tượng, nếu bạn chỉ tốn $0.1 mỗi báo cáo cho embedding, đó đã là khoảng $2.5/công ty cho báo cáo 10K và 10Q trong 5 năm qua. Nếu bạn muốn bao phủ hầu hết S&P 500 hoặc mở rộng kỳ hạn sang 10 năm, nó cộng dồn rất nhanh.
Nên đây là script để dọn dẹp. Sau quá trình, mỗi output dưới 0.2 Mb, nhỏ hơn 10 lần. Mỗi file vẫn có tất cả metadata giá trị mà chúng ta đã nói ở trên.
Giờ chúng ta sẵn sàng cho bước chunking/embedding.
Nên dùng vector store nào?
Có nhiều lựa chọn ở đây (với hơn 50 tùy chọn vector store). Nhưng vì tôi cần lọc nâng cao ở bước tiếp theo sử dụng metadata, self-querying retriever có vẻ phù hợp. Tôi nói "có vẻ" vì chúng có nhược điểm. Tôi đã thử nghiệm nhiều tùy chọn, bao gồm Chroma, ElasticSearch và FAISS cho đến giờ.
Trong khi Chroma và ElasticSearch cung cấp chức năng mạnh mẽ, kích thước index của chúng tương đối lớn (550+ MB cho Chroma và 800+ MB cho ElasticSearch). Các index này chỉ bao gồm embeddings cho 5 công ty test THÔI. Điều này không tốt vì khi mở rộng ra phần còn lại S&P 500, kích thước index cuối cùng có thể lớn gấp 100 lần. Lại không phù hợp cho laptop của tôi :D
FAISS index, mặt khác, chỉ khoảng 200+ MB cho cùng 5 công ty test. FAISS thiếu nhiều tính năng lọc nâng cao/tích hợp native với Langchain, nên tôi cần tìm hiểu thêm. Nếu bạn có đề xuất nào, cho tôi biết. (Khám phá thêm các vector store thay thế như Weaviate hoặc Pinecone có thể hữu ích?)
Query structuring
Mặc dù tốt khi tôi bao gồm nhiều metadata vào mỗi hồ sơ/chunk và có chúng là phần của embeddings, lưu trong vector store, làm sao chúng ta nói cho máy biết dùng filter nào? Lance từ Langchain giải thích "Query structuring for metadata filters" trong video này.
Tôi theo cách tiếp cận đó và tạo pydantic object bên dưới. Object này bao gồm các trường tương ứng với các tag metadata thực tế trong hồ sơ tài chính, như loại form (10-K, 10-Q), kỳ báo cáo, ngày nộp, và mã công ty.
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.",
)
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
Ví dụ, đây là danh sách câu hỏi và phản hồi từ LLM. Bạn có thể thấy LLM bỏ sót "conformed period of reporting" trong một số trường hợp:
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 vậy tôi hy vọng đạt được gì với bài viết dài (DÀI) này?
1. Các Python code được chia sẻ giúp ích cho bạn bằng cách nào đó.
2. Bạn có thể chia sẻ lại suy nghĩ/nhận xét hoặc tư vấn cho tôi về:
- Làm sao cải thiện query structuring tôi đang làm? FYI, tôi cũng đang dùng LLM để tạo sub-questions liên quan đến câu hỏi đầu vào.
- Làm sao cải thiện embedding/sử dụng vector store, đặc biệt phần filtering?
- hoặc bất kỳ đề xuất nào khác mà tôi chưa nghĩ đến :)
Cập nhật tháng 7 2024
Chatbot này CHƯA hoạt động nên nếu bạn thử dùng chatbot hiện tại trên trang web của tôi và hỏi câu hỏi tài chính, nó không biết :D
Mind the curiosity gap :)
Bạn đã làm việc với hồ sơ SEC hoặc thử xây agent tự đánh giá chưa? Tôi rất muốn nghe cách bạn tiếp cận thách thức vector store và lọc metadata.
Thân mến,
Chandler
P.S: có một khóa học mới ra mà tôi đang lên kế hoạch học "Generative AI for Software Development Skill Certificate" trên Coursera.





