자체 평가 금융 챗봇 구축: 데이터, 코드, 그리고 고군분투의 여정
SEC 데이터를 사용하여 S&P 500 질문에 답하는 금융 챗봇을 만들었습니다 — 답변을 보여주기 전에 스스로 개선하는 자체 비평 에이전트와 함께.
업데이트 (2026): 아래의 금융 챗봇 프로젝트는 결국 S&P 500 에이전트가 되었습니다 (2024년 9월 MVP 출시). 이를 출시한 후, 블로그 콘텐츠를 위한 Sydney와 두 가지 새로운 제품: STRAŦUM (마케팅 인텔리전스)과 DIALØGUE (팟캐스트 생성)에 집중하게 되었습니다. 여기서 설명하는 SEC 데이터 파이프라인과 자체 평가 패턴이 이 모든 것에 영향을 주었습니다.
2024년 4월 원본 글은 맥락을 위해 아래에 보존됩니다.
이 블로그 글에서는 매력적인 취미 프로젝트 — 자체 평가 기능을 갖춘 금융 챗봇/에이전트를 구축한 경험을 공유하고 싶습니다. 이 프로젝트는 공식 SEC 제출 자료의 데이터를 직접 사용하여 미국 S&P 500 기업의 재무 상태와 추세에 대한 질문에 답할 수 있는 에이전트를 만드는 것을 목표로 합니다. 이 에이전트의 주요 이점은 (또는 앞으로 그렇게 될 것입니다 :P):
- 미국 S&P 500 기업의 재무 상태와 추세에 대한 질문을 할 수 있습니다.
- 이 기업들의 SEC 공식 재무 보고서의 데이터를 직접 사용하여 환각을 방지합니다
- 각 답변 아래에 상세한 참조를 제공하여 원하시면 답변을 팩트체크할 수 있습니다
- 데이터베이스에 지난 5-10년의 재무 제출 데이터가 있어 시간에 따른 추세에 대해 에이전트에게 추론을 요청할 수 있습니다
- 각 답변이 사용자에게 반환되기 전에, LLM의 초안 답변을 비평하고 개선 방법을 제안하는 역할을 하는 또 다른 에이전트가 있습니다
- 이 제안과 전체 컨텍스트는 원래 에이전트와 공유됩니다. 그러면 에이전트는 데이터베이스에서 더 나은 정보를 검색하기 위해 다른 쿼리를 구성하거나 단순히 제안을 최종 답변에 통합할 수 있습니다
- 이 최종 답변이 그 다음 사용자에게 제공됩니다.
물론, Bloomberg 터미널에 접근할 수 없어서 Bloomberg 챗봇이 이미 위의 모든 것을 할 수 있는지 모릅니다. (제 추측으로는 어느 정도는 가능할 것입니다. 자체 비평 부분과 수정이 얼마나 좋은지는 확실하지 않지만 알고 싶습니다 :P)
어쨌든, 이것은 현재 제 학습 단계에 충분히 복잡하고 잠재적으로 유용할 수 있다고 느꼈기 때문에 시도해보고 싶었습니다.
다음과 같은 질문에 답하고 싶습니다:
- Apple의 지난 5-10년간 마케팅 지출 추세는 어떻게 되나요?
- XYZ 기업이 지난 5년간 한 모든 주요 인수는 무엇인가요?
- 지난 5년간 Nvidia와 Microsoft의 R&D 지출을 비교해보세요
- 등등.
그래서 이 프로젝트에서 어디까지 왔나요? 무엇을 배웠나요? 어떤 고난이 있었나요?
SEC에서 데이터를 어떻게 가져올 수 있나요?
처음 직면한 도전 중 하나는 SEC에서 필요한 데이터를 확보하는 것이었습니다. SEC는 EDGAR 데이터 접근에 대한 가이드와 문서를 제공하지만 (여기도), 대규모로 각 회사의 재무 보고서를 다운로드하는 과정을 이해하는 데 시간이 걸렸습니다.
SEC에서 직접 다운로드하고 처리하지 않고도 다음 단계로 진행할 수 있는 다른 방법이 있을까요?
Langchain 문서를 살펴보니 Kay.ai라는 금융 검색기가 있었습니다. 나머지 워크플로우가 어떻게 작동할지 보기 위해 검색기를 테스트했습니다. 기본 쿼리에 대해서는 예상대로 작동했습니다. 그러나 비동기 호출이나 고급 메타데이터 필터링을 지원하지 않았습니다. 그래서 이 부분을 직접 해보기로 계속 탐색하기로 했습니다.
실제 Python 스크립트 공유
SEC EDGAR에서 재무 보고서를 다운로드하는 Python 스크립트
많은 시행착오 끝에 chatGPT의 도움을 받아, 이것은 미국 증권거래위원회(SEC) 데이터베이스에서 XBRL과 TXT 보고서를 자동으로 다운로드하는 코드입니다. 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 파일에는 핵심 재무 보고서 외에도 엄청난 양의 다른 데이터가 있습니다. 그 때문에 각 파일이 매우 크며 (10+ MB 또는 심지어 40+ MB). 그리고 한 회사에 대해 지난 5-10년의 모든 10K와 10Q를 다운로드하려면 회사당 쉽게 20개 이상의 파일이 됩니다. 이런 큰 파일을 처리/정리/청킹하는 것은 매우 비생산적입니다. 일반 노트북으로는 오래 걸리고 임베딩 비용도 천문학적일 것이기 때문입니다. 그래서 다른 방법을 찾아야 했습니다.
여기서 .zip 파일이 등장합니다. 각 .zip 파일 안에는 핵심 재무 보고서가 .htm 형식으로 있고 기타 콘텐츠가 있습니다. 문제는 이 핵심 재무 보고서가 수년에 걸쳐, 회사 간에 일관된 방식으로 이름이 붙여지지 않는다는 것입니다. 그리고 .htm 보고서에는 .txt 파일처럼 깔끔한 형식의 모든 귀중한 메타데이터가 없습니다.
메타데이터와 주요 재무 보고서 결합
이것은 여러 기업의 보고서에서 재무제표와 관련 메타데이터를 자동으로 추출하는 스크립트입니다. SEC EDGAR 데이터베이스에서 다운로드한 .zip과 .txt 파일을 모두 포함하는 파일을 처리하도록 설계되었습니다.
몇 가지 가정을 한 것을 볼 수 있습니다:
- 각 재무 보고서의 .txt와 .zip 버전을 모두 다운로드합니다
- 핵심 재무 보고서는 각 .zip 파일 내에서 가장 큰 .htm 파일입니다. 다른 콘텐츠는 메인 ".htm" 파일에서 추출되므로 이것이 "맞아야" 합니다
청킹 전 불필요한 콘텐츠/문자 정리
좋은 소식은 위의 접근 방식을 사용하면 각 보고서(10K 또는 10Q)의 크기가 3MB 미만으로 줄어든다는 것입니다. 하지만 여전히 너무 길고 필요하지 않은 정보가 많아서 청킹 전에 더 정리해야 합니다. 그렇지 않으면 임베딩 과정이 매우 오래 걸리고 많은 비용이 듭니다. 보고서당 임베딩에 $0.1만 쓴다고 해도, 지난 5년간의 10K와 10Q 보고서만으로 이미 회사당 약 $2.5입니다. S&P 500 대부분을 커버하거나 기간을 지난 10년으로 연장하면 매우 빠르게 늘어납니다.
그래서 정리를 수행하는 스크립트가 여기 있습니다. 처리 후 각 출력은 0.2 MB 미만, 10배 작아집니다. 각 파일에는 여전히 앞서 이야기한 모든 귀중한 메타데이터가 있습니다.
이제 청킹/임베딩 단계를 진행할 준비가 되었습니다.
어떤 벡터 저장소를 사용해야 할까요?
여기에 많은 옵션이 있습니다 (50개 이상의 벡터 저장소 옵션). 하지만 다음 단계에서 메타데이터를 사용한 고급 필터링이 필요하기 때문에, self-querying retriever가 적합해 보입니다. "보입니다"라고 한 이유는 단점이 있기 때문입니다. Chroma, ElasticSearch, FAISS를 포함한 여러 옵션을 실험해봤습니다.
Chroma와 ElasticSearch는 견고한 기능을 제공했지만, 인덱스 크기가 상대적으로 컸습니다 (Chroma 550+ MB, ElasticSearch 800+ MB). 이 인덱스는 5개 테스트 회사만 포함한 것입니다. 나머지 S&P 500으로 확장하면 최종 인덱스 크기가 100배 더 커질 수 있으므로 좋지 않습니다. 역시 제 노트북에는 적합하지 않습니다 :D
FAISS 인덱스는 반면에 같은 5개 테스트 회사에 대해 200+ MB 정도입니다. 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
추신: 수강할 계획인 새로 출시된 강좌가 있습니다 — Coursera의 "Generative AI for Software Development Skill Certificate".





