Construire un chatbot financier auto-évaluant : un voyage à travers les données, le code et les difficultés
J'ai construit un chatbot financier qui répond aux questions sur le S&P 500 en utilisant les données SEC — avec un agent de self-critique qui améliore les réponses avant que tu ne les voies.
Mise à jour (2026) : Le projet de chatbot financier décrit ci-dessous a finalement abouti à l'agent S&P 500 (MVP lancé en sept. 2024). Après ce lancement, j'ai centré mon attention sur Sydney pour le contenu du blog et deux nouveaux produits : STRATUM (intelligence marketing) et DIALOGUE (génération de podcasts). Le pipeline de données SEC et les patterns d'auto-évaluation décrits ici ont influencé tous ces projets.
Article original d'avril 2024 conservé ci-dessous pour le contexte.
Dans ce billet, je veux partager mon expérience de travail sur un projet passionnant en loisir — construire un chatbot/agent financier avec des capacités d'auto-évaluation. Ce projet vise à créer un agent capable de répondre à des questions sur les conditions financières et les tendances des entreprises du S&P 500 aux États-Unis, en utilisant des données directement issues des déclarations officielles auprès de la SEC pour garantir l'exactitude et éviter les hallucinations. Les principaux avantages de cet agent sont (ou seront :P) :
- Te permettre de poser des questions sur les conditions financières et les tendances des entreprises du S&P 500 aux États-Unis.
- Il utilise des données directement issues des déclarations financières officielles de ces entreprises auprès de la SEC, pour éviter les hallucinations
- Il fournit des références détaillées sous chaque réponse afin que tu puisses vérifier la réponse si tu le souhaites
- La base de données contient les 5 à 10 dernières années de données de déclarations financières, donc tu peux demander à l'agent de raisonner sur les tendances dans le temps
- Avant que chaque réponse ne te soit retournée, il y a un autre agent, dont le travail est de critiquer la réponse préliminaire du LLM et de proposer des améliorations
- Ces suggestions et l'ensemble du contexte sont ensuite partagés avec l'agent original. L'agent peut alors décider de formuler différentes requêtes pour récupérer de meilleures informations de la base de données ou simplement incorporer les suggestions dans la réponse finale
- Cette réponse finale est ensuite fournie à l'utilisateur.
Évidemment, je n'ai pas accès au terminal Bloomberg, donc je n'ai aucune idée si le chatbot Bloomberg peut déjà faire tout ce qui précède. (Mon hypothèse éclairée est que oui — dans une certaine mesure. À quel point la partie self-critique et la révision sont bonnes, je n'en suis pas sûr, mais j'adorerais le savoir :P)
Quoi qu'il en soit, je voulais tenter ce défi car il semble suffisamment complexe pour mon niveau d'apprentissage actuel et pourrait être potentiellement utile.
Je veux pouvoir répondre à des questions comme :
- Quelle est la tendance des dépenses marketing d'Apple au cours des 5 à 10 dernières années ?
- Quelles sont toutes les acquisitions majeures réalisées par l'entreprise XYZ au cours des 5 dernières années ?
- Comparer les dépenses R&D de Nvidia et Microsoft au cours des 5 dernières années
- etc.
Alors, où en suis-je avec ce projet ? Qu'ai-je appris ? Quelles sont les difficultés ?
Comment puis-je obtenir les données de la SEC ?
L'un des défis initiaux auxquels j'ai été confronté était d'obtenir les données nécessaires de la SEC. Bien que la SEC fournisse des guides et de la documentation sur l'accès aux données EDGAR (et ici), il m'a fallu du temps pour comprendre le processus de téléchargement des déclarations financières pour chaque entreprise à grande échelle.
Y a-t-il une autre façon pour moi de passer à l'étape suivante sans avoir à télécharger et traiter moi-même les déclarations de la SEC ?
J'ai parcouru la documentation Langchain et j'ai trouvé qu'il existait un récupérateur financier appelé Kay.ai. J'ai testé le récupérateur pour voir comment le reste du workflow pourrait fonctionner. Le récupérateur fonctionne comme prévu pour les requêtes basiques. Cependant, il ne prend pas en charge les appels asynchrones ni le filtrage avancé des métadonnées. J'ai donc décidé de continuer à explorer cette partie par moi-même.
Partage de scripts Python réels
Script Python pour télécharger les déclarations financières de SEC EDGAR
Après beaucoup d'essais et d'erreurs, avec l'aide de chatGPT, voici le code qui télécharge automatiquement les déclarations XBRL et TXT de la base de données de la U.S. Securities and Exchange Commission (SEC). Il est conçu pour récupérer les déclarations récentes pour des entreprises spécifiques en utilisant leur Central Index Key (CIK).
Pourquoi veux-je télécharger à la fois le fichier .zip et le fichier .txt ?
Le fichier .txt pour chaque entreprise est super complet. Il contient beaucoup de métadonnées précieuses sur chaque déclaration comme le type de formulaire (10K ou 10Q), la période de rapport, la date de dépôt, le CIK/nom de l'entreprise, etc... Ce sont les types de métadonnées dont j'aurai besoin plus tard pour construire l'agent. Tout cela est bien capturé en haut du fichier comme :
<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>
Le fichier .txt contient également une tonne d'autres données, en plus du rapport financier principal. C'est pourquoi chaque fichier est très volumineux (comme 10+ MB ou même 40+ MB). Et pour une entreprise, je veux télécharger tous les 10K et 10Q des 5 à 10 dernières années, donc on parle facilement de 20+ fichiers par entreprise. Essayer de traiter/nettoyer les caractères inutiles/découper ces gros fichiers est hautement improductif car cela prendra beaucoup de temps à faire avec un ordinateur portable normal et aussi le coût de l'embedding sera astronomique. J'avais donc besoin de trouver une autre façon.
C'est là qu'intervient le fichier .zip. Dans chacun des fichiers .zip, tu trouveras le rapport financier principal en format .htm et d'autres contenus. Le problème est que ces rapports financiers principaux ne sont pas nommés de façon cohérente au fil des années, ni entre les entreprises. Et le rapport .htm n'a pas toutes les précieuses métadonnées dans un format ordonné comme le fichier .txt.
Combinaison des métadonnées avec le rapport financier principal
Voici le script qui automatise le processus d'extraction des états financiers et de leurs métadonnées associées à partir des déclarations de plusieurs entreprises. Il est conçu pour gérer les fichiers téléchargés depuis la base de données SEC EDGAR, qui inclut à la fois des fichiers .zip et .txt.
Tu peux voir que j'ai fait quelques hypothèses :
- Tu télécharges à la fois les versions .txt et .zip pour chaque rapport financier
- Le rapport financier principal est le plus grand fichier .htm à l'intérieur de chaque fichier .zip. Cela "devrait" être vrai puisque les autres contenus sont extraits du fichier principal ".htm"
Nettoyer le contenu/les caractères inutiles avant le découpage
La bonne nouvelle est qu'avec l'approche ci-dessus, chaque rapport (10K ou 10Q) a maintenant une taille de moins de 3 MB seulement. Mais c'est encore trop long et contient tellement d'informations dont nous n'avons pas besoin, nous devons donc le nettoyer davantage avant le découpage. Sinon, le processus d'embedding sera très long et coûtera beaucoup d'argent. Imagine, si tu dépenses seulement 0,1 $ par rapport pour l'embedding, c'est déjà environ 2,5 $/entreprise pour les rapports 10K et 10Q des 5 dernières années. Si tu veux couvrir la plupart du S&P 500 ou étendre la période aux 10 dernières années, ça s'accumule très rapidement.
Alors voici le script pour faire le nettoyage. Après le processus, chaque sortie fait moins de 0,2 Mo, soit 10 fois plus petit. Chaque fichier contient encore toutes les précieuses métadonnées dont nous avons parlé précédemment.
Maintenant, nous sommes prêts pour l'étape de découpage/embedding.
Quel magasin vectoriel devrais-je utiliser ?
Il y a beaucoup d'options ici (avec plus de 50 options de magasin vectoriel). Mais parce que j'ai besoin de faire un filtrage avancé à l'étape suivante en utilisant les métadonnées, un récupérateur auto-interrogeant semble approprié. Je dis "semble" car ils ont des inconvénients. J'ai expérimenté avec plusieurs options, notamment Chroma, ElasticSearch et FAISS jusqu'à présent.
Bien que Chroma et ElasticSearch aient fourni des fonctionnalités robustes, leurs tailles d'index étaient relativement grandes (550+ MB pour Chroma et 800+ Mo pour ElasticSearch). Ces index ne comprennent que les embeddings pour 5 entreprises test SEULEMENT. Ce n'est pas bon car en le mettant à l'échelle pour le reste du S&P 500, la taille finale de l'index pourrait être 100 fois plus grande. Encore une fois, pas adapté à mon ordinateur portable local :D
L'index FAISS, en revanche, ne fait qu'environ 200+ Mo pour les mêmes 5 entreprises test. FAISS manque de beaucoup de filtrage avancé/intégration native avec Langchain, donc j'ai besoin d'approfondir la recherche. Si tu as des suggestions, fais-le moi savoir. (Une exploration supplémentaire de magasins vectoriels alternatifs comme Weaviate ou Pinecone pourrait être bénéfique ?)
Structuration des requêtes
Bien que ce soit bien d'inclure beaucoup de métadonnées dans chaque déclaration/chunk et de les avoir dans le cadre des embeddings stockés dans le magasin vectoriel, comment dire à la machine quels filtres utiliser ? Lance de Langchain a expliqué la "structuration des requêtes pour les filtres de métadonnées" dans cette vidéo.
J'ai suivi l'approche et créé l'objet pydantic ci-dessous. Cet objet inclut des champs qui correspondent à des balises de métadonnées réelles trouvées dans les déclarations financières, tels que les types de formulaires (10-K, 10-Q), les périodes de rapport, les dates de dépôt et les identifiants d'entreprise.
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
Par exemple, voici la liste de questions et les réponses du LLM. Tu peux voir que le LLM manque la "période de rapport conforme" dans certains cas :
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'\}
Alors, qu'est-ce que j'espère accomplir avec ce long (TRÈS LONG) post ?
1. Que les codes Python partagés t'aident d'une quelconque façon.
2. Que tu puisses me partager tes réflexions/commentaires ou me conseiller sur :
- Comment améliorer la structuration des requêtes que je fais ? FYI, j'utilise aussi le LLM pour générer des sous-questions liées à une question d'entrée.
- Comment puis-je améliorer l'embedding/l'utilisation du magasin vectoriel, surtout la partie filtrage ?
- Ou toute autre suggestion à laquelle je n'ai pas pensé :)
Mise à jour juillet 2024
Ce chatbot NE FONCTIONNE PAS encore, donc si tu essaies d'utiliser le chatbot actuel sur mon site et de poser des questions financières, il ne sait pas :D
Surveille le gap de curiosité :)
As-tu travaillé avec des déclarations SEC ou essayé de construire un agent auto-évaluant ? J'adorerais entendre comment tu as abordé les défis du magasin vectoriel et du filtrage des métadonnées.
Cordialement,
Chandler
P.S : il y a un nouveau cours que je prévois de suivre : "Generative AI for Software Development Skill Certificate" sur Coursera.





