Construindo um chatbot financeiro com auto-avaliação: uma jornada por dados, código e dificuldades
Construí um chatbot financeiro que responde perguntas sobre o S&P 500 usando dados da SEC — com um agente de autocrítica que melhora as respostas antes de você as ver.
Atualização (2026): O projeto de chatbot financeiro abaixo acabou se tornando o agente S&P 500 (MVP lançado em setembro de 2024). Depois de lançar isso, mudei o foco para Sydney para conteúdo de blog e dois novos produtos: STRATUM (inteligência de marketing) e DIALOGUE (geração de podcast). O pipeline de dados da SEC e os padrões de auto-avaliação descritos aqui informaram todos eles.
Post original de abril de 2024 preservado abaixo para contexto.
Neste post, quero compartilhar minha experiência trabalhando em um projeto de hobby fascinante — construir um chatbot/agente financeiro com capacidades de auto-avaliação. Este projeto tem como objetivo criar um agente que possa responder perguntas sobre condições financeiras e tendências entre as empresas do S&P 500 nos EUA, usando dados diretamente de documentos oficiais da SEC para garantir precisão e evitar alucinações. Os principais benefícios deste agente são (ou serão :P):
- Permitir que você faça perguntas sobre condições financeiras e tendências entre empresas do S&P 500 nos EUA.
- Usa dados diretamente dos documentos financeiros oficiais dessas empresas para a SEC, para evitar alucinações
- Fornece referências detalhadas abaixo de cada resposta para que você possa verificar os fatos se quiser
- O banco de dados tem os últimos 5-10 anos de dados de documentos financeiros para que você possa pedir ao agente que raciocine sobre tendências ao longo do tempo
- Antes de cada resposta ser retornada a você, existe outro agente, cujo trabalho é criticar a rascunho de resposta do LLM e propor como melhorá-la
- Essas sugestões e todo o contexto são então compartilhados com o agente original. O agente pode então decidir formular consultas diferentes para recuperar melhores informações do banco de dados ou simplesmente incorporar as sugestões na resposta final
- Essa resposta final é então fornecida ao usuário.
Obviamente, não tenho acesso ao terminal Bloomberg, então não faço ideia se o chatbot Bloomberg já pode fazer tudo isso. (Meu palpite é que pode — até certo ponto. Quão boa é a parte de autocrítica e revisão, não tenho certeza, mas adoraria saber :P)
De qualquer forma, queria tentar isso, pois parece complexo o suficiente para o meu aprendizado neste estágio e pode ser potencialmente útil.
Quero responder perguntas como:
- Qual é a tendência de gastos com marketing da Apple nos últimos 5-10 anos?
- Quais são todas as principais aquisições feitas pela empresa XYZ nos últimos 5 anos
- Compare os gastos com P&D da Nvidia e Microsoft nos últimos 5 anos
- etc.
Ok, então onde estou com este projeto? O que aprendi? Quais são as dificuldades?
Como posso obter os dados da SEC?
Um dos desafios iniciais que enfrentei foi obter os dados necessários da SEC. Embora a SEC forneça guias e documentação sobre como acessar os dados do EDGAR (e aqui), levou algum tempo para compreender o processo de download de documentos financeiros para cada empresa em escala.
Existe outra forma de prosseguir para o próximo passo sem ter que baixar e processar os documentos da SEC por conta própria?
Procurei na documentação do Langchain e encontrei um retriever financeiro chamado Kay.ai. Testei o retriever para ver como o restante do fluxo de trabalho poderia funcionar. O retriever funciona como esperado para consultas básicas. No entanto, ele não suporta chamadas assíncronas ou filtragem avançada de metadados. Então decidi continuar explorando fazer essa parte por conta própria.
Compartilhando scripts Python reais
Script Python para baixar documentos financeiros do SEC EDGAR
Depois de muitas tentativas e erros, com a ajuda do chatGPT, este é o código que baixa automaticamente documentos XBRL e TXT do banco de dados da Securities and Exchange Commission (SEC) dos EUA. Ele é projetado para buscar documentos recentes de empresas especificadas usando seu Central Index Key (CIK).
Por que quero baixar tanto o arquivo .zip quanto o arquivo .txt?
O arquivo .txt para cada empresa é super abrangente. Ele tem muitos metadados valiosos sobre cada documento, como o tipo de formulário (10K ou 10Q), o período de relatório, a data de arquivamento, o CIK/nome da empresa, etc. Esses são os tipos de metadados que vou precisar mais tarde para construir o agente. Todos eles são capturados no topo do arquivo assim:
<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>
O arquivo .txt tem muito mais dados além do relatório financeiro principal. Por causa disso, cada arquivo é muito grande (como 10+ MB ou até 40+ MB). E para uma empresa, quero baixar todos os 10K e 10Q dos últimos 5-10 anos, então estamos falando facilmente de 20+ arquivos por empresa. Tentar processar/limpar caracteres desnecessários/chunkar esses arquivos grandes é altamente improdutivo porque levará muito tempo para fazer com um laptop normal e o custo do embedding será astronômico. Então precisei encontrar outra forma.
É aqui que entra o arquivo .zip. Em cada um dos arquivos .zip, você encontrará o relatório financeiro principal no formato .htm e outros conteúdos. O problema é que esses relatórios financeiros principais não são nomeados de forma consistente ao longo dos anos, entre as empresas. E o relatório .htm não tem todos os metadados valiosos em um formato organizado como o arquivo .txt.
Combinando os metadados com o relatório financeiro principal
Este é o script que automatiza o processo de extração de demonstrações financeiras e seus metadados associados dos documentos de múltiplas empresas. Ele é projetado para lidar com os arquivos baixados do banco de dados SEC EDGAR, que inclui arquivos .zip e .txt.
Você pode ver que fiz algumas suposições:
- Você baixa tanto as versões .txt quanto .zip para cada relatório financeiro
- O relatório financeiro principal é o maior arquivo .htm dentro de cada arquivo .zip. Isso "deve" ser verdade, pois o outro conteúdo é extraído do arquivo ".htm" principal
Limpar conteúdo/caracteres desnecessários antes do chunking
A boa notícia é que usando a abordagem acima, cada relatório (seja 10K ou 10Q) agora tem um tamanho menor que 3 MB apenas. Mas ainda é muito longo e contém muitas informações que não precisamos, então precisamos limpá-lo ainda mais antes do chunking. Caso contrário, o processo de embedding levará muito tempo e custará muito dinheiro. Imagine, se você gastar apenas $0,10 por relatório para embedding, isso já é cerca de $2,50/empresa para relatórios 10K e 10Q dos últimos 5 anos. Se você quiser cobrir a maior parte do S&P 500 ou estender o período para os últimos 10 anos, o valor aumenta muito rapidamente.
Então aqui está o script para fazer a limpeza. Após o processo, cada output tem menos de 0,2 MB, 10x menor. Cada arquivo ainda tem todos os metadados valiosos sobre os quais falamos anteriormente.
Agora, estamos prontos para a etapa de chunking/embedding.
Qual vector store devo usar?
Há muitas opções aqui (com mais de 50 opções de vector store). Mas como preciso fazer filtragem avançada na próxima etapa usando metadados, um self-querying retriever parece encaixar bem. Digo "parece" porque eles têm desvantagens. Experimentei várias opções, incluindo Chroma, ElasticSearch e FAISS até agora.
Embora Chroma e ElasticSearch fornecessem funcionalidade robusta, seus tamanhos de índice eram relativamente grandes (550+ MB para Chroma e 800+ MB para ElasticSearch). Esses índices incluem apenas os embeddings para 5 empresas de teste APENAS. Isso não é bom porque à medida que escalo para o restante do S&P 500, o tamanho final do índice pode ser 100x maior. Novamente, não adequado para meu laptop local :D
O índice FAISS, por outro lado, é de apenas 200+ MB para as mesmas 5 empresas de teste. O FAISS carece de muita filtragem avançada/integração nativa com Langchain, então preciso investigar mais. Se você tiver alguma sugestão, me avise. (Uma exploração mais aprofundada de vector stores alternativos como Weaviate ou Pinecone pode ser benéfica?)
Estruturação de consultas
Embora seja bom que eu inclua muito de metadados em cada documento/chunk e os tenha como parte dos embeddings, armazenados no vector store, como dizemos à máquina quais filtros usar? Lance do Langchain explicou "Estruturação de consultas para filtros de metadados" neste vídeo.
Segui a abordagem e criei o objeto pydantic abaixo. Este objeto inclui campos que correspondem a tags de metadados reais encontradas em documentos financeiros, como tipos de formulário (10-K, 10-Q), períodos de relatório, datas de arquivamento e identificadores de empresa.
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
Por exemplo, esta é a lista de perguntas e as respostas do LLM. Você pode ver que o LLM perde o "conformed period of reporting" em alguns casos:
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, então o que espero alcançar com este post longo (MUITO longo)?
1. Os códigos Python compartilhados te ajudam de alguma forma.
2. Você pode compartilhar de volta seus pensamentos/comentários ou me aconselhar sobre:
- Como melhorar a estruturação de consultas que estou fazendo? FYI, também estou usando o LLM para gerar sub-questões relacionadas a uma pergunta de entrada.
- Como posso melhorar o embedding/uso do vector store, especialmente a parte de filtragem?
- ou qualquer outra sugestão que eu não tenha pensado :)
Atualização julho de 2024
Este chatbot NÃO está funcionando ainda, então se você tentar usar o chatbot atual no meu site e fazer perguntas financeiras, ele não sabe :D
Tenha em mente a curiosidade :)
Você já trabalhou com documentos da SEC ou tentou construir um agente com auto-avaliação? Adoraria ouvir como você abordou os desafios de vector store e filtragem de metadados.
Abraços,
Chandler
P.S: há um curso recém-lançado que planejo fazer "Generative AI for Software Development Skill Certificate" no Coursera.





