10 esempi pratici
24Esempio 6 · Analisi finanziaria
Due squadre per un'analisi finanziaria: una legge le notizie dal web, l'altra commenta un bilancio i cui indici sono calcolati da Python. Impari perché i conti non si chiedono al modello.
Il titolare di una ferramenta deve decidere se chiedere un nuovo finanziamento e vuole capire come stanno davvero i conti dell'azienda. Un risparmiatore vuole farsi un'idea di cosa si dice di una società quotata prima di leggere i documenti ufficiali. In tutti e due i casi il lavoro è simile: raccogliere dati, calcolare qualche indicatore, mettere in fila rischi e opportunità in una nota che si legge in cinque minuti.
In questo esempio costruisci due versioni della stessa squadra. La versione A cerca notizie recenti sul web e scrive una nota con le fonti. La versione B non ha bisogno di chiavi esterne: legge un bilancio inventato e fa calcolare gli indici a Python, mentre i modelli si limitano a commentarli. Alla fine capirai perché, quando ci sono di mezzo i numeri, la versione B è quella giusta.
Questo esempio insegna a costruire agenti, non a investire. Le note prodotte non sono consigli di investimento e non vanno usate per comprare, vendere o chiedere prestiti. I modelli linguistici sbagliano i numeri: li ricopiano male, li arrotondano, li inventano quando mancano. Ogni cifra va controllata sulle fonti ufficiali: il bilancio depositato, i comunicati dell'azienda, i documenti pubblicati per gli investitori.
- Problema
- Trasformare notizie o numeri di bilancio in una nota breve ed equilibrata su rischi e opportunità, con i numeri giusti e le fonti.
- La squadra (agenti)
- Versione A: Analista di notizie e Autore della nota. Versione B: Analista di bilancio (con il calcolatore) e Autore della nota.
- Strumenti
- A:
SerperDevTooleScrapeWebsiteTool. B:calcola_indici, uno strumento scritto da te con@tool. - Chiavi e costi
- A: chiave del modello e
SERPER_API_KEY(Serper ha un piano gratuito). B: solo il modello, anche gratis con Ollama. - Cosa impari di nuovo
- Perché i conti vanno fatti dal codice e non dal modello; la ricerca di sole notizie con
search_type="news";inject_dateper dare all'agente la data di oggi; un guardrail che impone avviso e fonti.
Perché è tra i più usati#
Nella classifica l'analisi finanziaria e dei titoli è al 6° posto, con 37,8 punti su 100. È un caso d'uso amatissimo da chi impara:
- Su GitHub, al 15 settembre 2026, 501 repository hanno «crewai financial» nel nome o nella descrizione e altri 417 «crewai stock». Tra i 562 progetti applicativi con l'argomento
crewai, il 14,1% riguarda finanza e titoli: è la terza categoria su quel segnale. - CrewAI stessa lo usa come esempio: nel repository ufficiale c'è la crew stock_analysis e il corso Multi AI Agent Systems with crewAI di DeepLearning.AI ha una lezione di analisi finanziaria.
C'è però un dato onesto da dire: nessuno dei sondaggi aziendali che abbiamo consultato mette l'analisi finanziaria tra gli usi principali degli agenti. La sua posizione viene soprattutto da tutorial e progetti personali, non da aziende che lo usano in produzione.
Chi fa i conti?#
Un LLM: Large Language Model, grande modello linguistico: un programma addestrato su enormi quantità di testo che, data una frase, prevede le parole che seguono. È il motore di ChatGPT, Claude, Gemini. glossario non calcola: prevede il Token: Il pezzetto di testo con cui ragiona un modello: una parola corta, un pezzo di parola o un segno di punteggiatura. Secondo le stime di OpenAI, in inglese un token vale in media circa ¾ di parola. I servizi a pagamento contano (e fanno pagare) i token. glossario successivo più probabile. Quando scrive «12,4%» non ha fatto una divisione, ha prodotto cifre che sembrano giuste in quel punto del testo. Spesso lo sono, a volte no, e dalla frase non lo capisci. Per un riassunto di notizie è un rischio accettabile, purché ci siano le fonti. Per un indice di bilancio no.
La soluzione è dividere il lavoro in base a chi lo sa fare meglio:
- Python fa i conti. Una divisione in Python dà sempre lo stesso risultato, la puoi ricontrollare e non si inventa niente.
- Il modello li commenta. Leggere una serie di numeri e dire «i ricavi crescono ma i margini si stringono» è proprio il genere di cosa in cui i modelli sono bravi.
In CrewAI il ponte tra i due è uno Strumento (tool): Una funzione che l'agente può chiamare per agire fuori dal modello: cercare sul web, leggere un file, interrogare un database, inviare una email. glossario scritto da te con il Decoratore: Una riga che inizia con @ sopra una funzione e le aggiunge un comportamento. In CrewAI: @agent, @task, @start, @listen. glossario @tool (capitolo 12). L'agente lo chiama, riceve gli Indice di bilancio: Un rapporto tra due voci del bilancio, per esempio utile diviso patrimonio, che riassume in un numero la redditività, la liquidità o i debiti di un'azienda. glossario già calcolati e deve solo riportarli e interpretarli.
Lo schema#
Entrambe sono crew sequenziali con due agenti. La differenza sta tutta nel primo agente e nei suoi strumenti. Il secondo, l'autore, ha lo stesso mestiere in tutte e due: trasformare materiale grezzo in una nota leggibile. In tutte e due un Guardrail: Un controllo automatico sul risultato di un task. Se il controllo fallisce, l'errore torna all'agente che riprova. glossario controlla che la nota contenga l'avviso «non è una consulenza finanziaria».
Preparazione#
Crea una cartella analisi-finanziaria e aprila nel terminale. Il file .env è quello del capitolo 7, più la chiave di Serper se vuoi provare la versione A:
MODEL=openai/gpt-4.1-mini
OPENAI_API_KEY=sk-la-tua-chiave
SERPER_API_KEY=la-tua-chiave-serper
CREWAI_DISABLE_TELEMETRY=truePer la sola versione B puoi togliere SERPER_API_KEY e, se vuoi, usare MODEL=ollama/qwen2.5:7b senza nessuna chiave.
Per la versione B scarica bilancio_fittizio_aurora.csv nella stessa cartella. È un bilancio inventato di un'azienda che non esiste, «Ferramenta Aurora S.r.l.»: valori in migliaia di euro, una riga per anno.
anno,ricavi,ebitda,utile_netto,attivo_corrente,passivo_corrente,liquidita,debiti_finanziari,patrimonio_netto
2023,4200,504,168,1650,1100,310,1200,1400
2024,4750,523,143,1720,1350,190,1650,1480
2025,5100,459,92,1800,1640,120,2100,1520Se non hai mai visto un bilancio, ecco cosa calcola lo strumento, in parole semplici (sono definizioni semplificate, adatte a questo esercizio):
| Indice | Come si calcola | Cosa racconta |
|---|---|---|
| Crescita dei ricavi | ricavi di quest'anno rispetto all'anno prima | se l'azienda vende di più |
| Margine EBITDA | EBITDA diviso ricavi | quanto resta delle vendite dopo i costi della gestione |
| Margine netto | utile netto diviso ricavi | quanto guadagna davvero su ogni euro venduto |
| ROE | utile netto diviso patrimonio netto | quanto rende il capitale dei soci |
| Liquidità corrente | attivo corrente diviso passivo corrente | se riesce a pagare i debiti a breve (sotto 1 è un campanello) |
| Posizione finanziaria netta (PFN) | debiti finanziari meno liquidità | il debito «vero», tolta la cassa |
| PFN / EBITDA | PFN divisa EBITDA | quanti anni di margine servirebbero per ripagare il debito |
Il codice, pezzo per pezzo#
Versione B: lo strumento che fa i conti#
Due piccole Funzione: Un blocco di codice con un nome che fa un lavoro e restituisce un risultato. Si definisce con def e si chiama con le parentesi: somma(2, 3). glossario di servizio evitano la divisione per zero e formattano i risultati. Poi viene lo strumento vero e proprio:
@tool("Calcolatore di indici di bilancio")
def calcola_indici(nome_file: str) -> str:
"""Legge un bilancio in formato CSV (valori in migliaia di euro, una riga per anno) e restituisce
gli indici principali già calcolati. Usalo sempre invece di fare i conti a mente."""
nome = Path(nome_file).name # accetta solo un nome di file della cartella corrente
if not nome.endswith(".csv") or not Path(nome).exists():
return f"Errore: il file {nome} non esiste in questa cartella o non è un CSV."
with open(nome, encoding="utf-8", newline="") as f:
anni = list(csv.DictReader(f))
righe = []
ricavi_prima = None
for a in anni:
v = {chiave: float(valore) for chiave, valore in a.items() if chiave != "anno"}
crescita = percentuale(v["ricavi"] - ricavi_prima, ricavi_prima) if ricavi_prima else "n.d."
pfn = v["debiti_finanziari"] - v["liquidita"] # posizione finanziaria netta
righe.append(
f"Anno {a['anno']}: ricavi {v['ricavi']:.0f} mila euro (crescita {crescita}); "
f"margine EBITDA {percentuale(v['ebitda'], v['ricavi'])}; "
# ... gli altri indici, uguali nella forma
)
ricavi_prima = v["ricavi"]
return "\n".join(righe)Tre dettagli contano.
- La docstring, il testo tra le triple virgolette, è la descrizione che l'agente legge per decidere quando usare lo strumento. «Usalo sempre invece di fare i conti a mente» è un'istruzione rivolta al modello.
Path(nome_file).nametiene solo il nome del file e butta via eventuali cartelle. Il nome del file lo sceglie il modello, quindi non ti fidi: se provasse con../../etc/passwd, lo strumento cercherebbepasswdnella cartella corrente e risponderebbe con un errore. È lo stesso principio del sandbox diFileReadTool, applicato a mano.- Il modulo
csvfa parte di Python: non serve installare niente.csv.DictReadertrasforma ogni riga in un Dizionario: Una raccolta di coppie nome → valore tra parentesi graffe:{"citta": "Arezzo"}. glossario con i nomi delle colonne.
Il risultato è una riga di testo per anno con tutti gli indici. Lo strumento restituisce testo perché è ciò che l'agente legge meglio.
Versione B: l'analista, il guardrail, il confronto finale#
analisi = Task(
description=(
"Usa lo strumento Calcolatore di indici di bilancio sul file {file}.\n"
"Riporta gli indici esattamente come li restituisce lo strumento: non ricalcolarli, "
"non arrotondarli e non aggiungerne altri. Poi descrivi come sono cambiati negli anni."
),
expected_output=(...),
agent=analista,
)
def controlla_avviso(risultato: TaskOutput) -> tuple[bool, Any]:
"""Guardrail: la nota deve contenere l'avviso, altrimenti torna all'agente."""
if AVVISO.lower() not in risultato.raw.lower():
return (False, f"Manca l'avviso. Inizia la nota con la frase: {AVVISO}.")
return (True, risultato.raw)La descrizione del task insiste: riportare, non ricalcolare. L'agente analista ha anche max_iter=5: il suo lavoro è una chiamata allo strumento e un commento, e se si perde è meglio che si fermi presto. Il guardrail è una funzione normale che controlla una cosa sola, la presenza dell'avviso. Se manca, CrewAI rimanda la nota all'autore con il messaggio d'errore (capitolo 13). I (...) sostituiscono testi che trovi per intero nello script completo.
print("\n=== NOTA (salvata anche in nota_bilancio.md) ===")
print(risultato.raw)
# Gli stessi indici calcolati direttamente da Python: confrontali con i numeri della nota.
print("\n=== INDICI CALCOLATI DA PYTHON, PER IL CONFRONTO ===")
print(calcola_indici.run(nome_file=FILE_BILANCIO))L'ultima riga chiama lo strumento direttamente da Python, senza passare dal modello, con .run(...). Così sotto la nota hai i numeri veri e puoi controllare in un colpo d'occhio se l'autore li ha copiati bene.
Versione A: le notizie dal web#
analista = Agent(
role="Analista di notizie finanziarie",
goal="Raccogliere i fatti recenti e verificabili su {azienda}, ognuno con data e link della fonte",
backstory=(...),
tools=[SerperDevTool(search_type="news", n_results=8), ScrapeWebsiteTool()],
inject_date=True, # l'agente conosce la data di oggi e può capire cosa è "recente"
max_iter=10,
llm=MODELLO,
verbose=True,
)SerperDevTool di solito fa una ricerca normale su Google. Con search_type="news" cerca solo tra le notizie: abbiamo verificato nel codice di CrewAI 1.15.21 che i valori ammessi sono "search" e "news". n_results=8 limita i risultati, e quindi i token da leggere. ScrapeWebsiteTool apre gli articoli più interessanti (capitolo 12). inject_date=True aggiunge al prompt la data di oggi: senza, il modello non sa in che giorno siamo e non può distinguere una notizia di ieri da una di due anni fa.
Il guardrail di questa versione controlla due cose: l'avviso e la presenza di almeno un link ("http" nel testo). È un controllo grezzo, non verifica che i link siano giusti, ma impedisce la nota senza fonti.
Il codice completo#
Versione B, senza chiavi esterne#
# /// script
# requires-python = ">=3.10,<3.14"
# dependencies = ["crewai[tools]==1.15.21"]
# ///
"""Python calcola gli indici di un bilancio fittizio, un analista li commenta e un autore scrive la nota."""
import csv
import os
import sys
from pathlib import Path
from typing import Any
from crewai import Agent, Crew, Process, Task, TaskOutput
from crewai.tools import tool
# Il modello si sceglie nel file .env (capitolo 7). Se MODEL manca, si usa quello scritto qui.
MODELLO = os.getenv("MODEL", "openai/gpt-4.1-mini")
FILE_BILANCIO = "bilancio_fittizio_aurora.csv"
AVVISO = "Questa nota non è una consulenza finanziaria"
def percentuale(parte: float, totale: float) -> str:
return f"{parte / totale * 100:.1f}%" if totale else "n.d."
def rapporto(a: float, b: float) -> str:
return f"{a / b:.2f}" if b else "n.d."
@tool("Calcolatore di indici di bilancio")
def calcola_indici(nome_file: str) -> str:
"""Legge un bilancio in formato CSV (valori in migliaia di euro, una riga per anno) e restituisce
gli indici principali già calcolati. Usalo sempre invece di fare i conti a mente."""
nome = Path(nome_file).name # accetta solo un nome di file della cartella corrente
if not nome.endswith(".csv") or not Path(nome).exists():
return f"Errore: il file {nome} non esiste in questa cartella o non è un CSV."
with open(nome, encoding="utf-8", newline="") as f:
anni = list(csv.DictReader(f))
righe = []
ricavi_prima = None
for a in anni:
v = {chiave: float(valore) for chiave, valore in a.items() if chiave != "anno"}
crescita = percentuale(v["ricavi"] - ricavi_prima, ricavi_prima) if ricavi_prima else "n.d."
pfn = v["debiti_finanziari"] - v["liquidita"] # posizione finanziaria netta
righe.append(
f"Anno {a['anno']}: ricavi {v['ricavi']:.0f} mila euro (crescita {crescita}); "
f"margine EBITDA {percentuale(v['ebitda'], v['ricavi'])}; "
f"margine netto {percentuale(v['utile_netto'], v['ricavi'])}; "
f"ROE {percentuale(v['utile_netto'], v['patrimonio_netto'])}; "
f"liquidità corrente {rapporto(v['attivo_corrente'], v['passivo_corrente'])}; "
f"debiti finanziari/patrimonio {rapporto(v['debiti_finanziari'], v['patrimonio_netto'])}; "
f"posizione finanziaria netta {pfn:.0f} mila euro; "
f"PFN/EBITDA {rapporto(pfn, v['ebitda'])}"
)
ricavi_prima = v["ricavi"]
return "\n".join(righe)
def controlla_avviso(risultato: TaskOutput) -> tuple[bool, Any]:
"""Guardrail: la nota deve contenere l'avviso, altrimenti torna all'agente."""
if AVVISO.lower() not in risultato.raw.lower():
return (False, f"Manca l'avviso. Inizia la nota con la frase: {AVVISO}.")
return (True, risultato.raw)
analista = Agent(
role="Analista di bilancio per piccole imprese",
goal="Descrivere con precisione come sono cambiati i conti di {azienda} negli anni del bilancio",
backstory=(
"Lavori da vent'anni con i bilanci delle PMI italiane. Non fai mai i conti a mente: "
"usi il calcolatore e riporti i numeri esattamente come li ricevi."
),
tools=[calcola_indici],
max_iter=5,
llm=MODELLO,
verbose=True,
)
autore = Agent(
role="Autore di note per la direzione",
goal="Scrivere una nota breve ed equilibrata su rischi e opportunità di {azienda}",
backstory=(
"Scrivi per titolari che non sono esperti di finanza. Frasi corte, niente gergo, "
"nessun consiglio di investimento: metti in fila i fatti e ciò che potrebbe andare bene o male."
),
llm=MODELLO,
verbose=True,
)
analisi = Task(
description=(
"Usa lo strumento Calcolatore di indici di bilancio sul file {file}.\n"
"Riporta gli indici esattamente come li restituisce lo strumento: non ricalcolarli, "
"non arrotondarli e non aggiungerne altri. Poi descrivi come sono cambiati negli anni."
),
expected_output=(
"Gli indici di ogni anno copiati dallo strumento, seguiti da 4-5 osservazioni "
"sull'andamento, in italiano."
),
agent=analista,
)
nota = Task(
description=(
f"Scrivi la nota su {{azienda}} usando solo i numeri dell'analisi. Inizia con la frase: {AVVISO}.\n"
"Non inventare numeri e non dare consigli di acquisto, vendita o finanziamento."
),
expected_output=(
"Una nota in Markdown, in italiano, massimo 300 parole: l'avviso iniziale, un riassunto di 3 righe, "
"3 rischi e 2 opportunità, ciascuno con il numero che lo giustifica."
),
agent=autore,
context=[analisi],
guardrail=controlla_avviso,
markdown=True,
output_file="nota_bilancio.md",
)
crew = Crew(agents=[analista, autore], tasks=[analisi, nota], process=Process.sequential, verbose=True)
if __name__ == "__main__":
if not Path(FILE_BILANCIO).exists():
sys.exit(f"Non trovo {FILE_BILANCIO}: scaricalo e mettilo nella stessa cartella dello script.")
risultato = crew.kickoff(inputs={"azienda": "Ferramenta Aurora S.r.l.", "file": FILE_BILANCIO})
print("\n=== NOTA (salvata anche in nota_bilancio.md) ===")
print(risultato.raw)
# Gli stessi indici calcolati direttamente da Python: confrontali con i numeri della nota.
print("\n=== INDICI CALCOLATI DA PYTHON, PER IL CONFRONTO ===")
print(calcola_indici.run(nome_file=FILE_BILANCIO))Una nota sulla riga f"Scrivi la nota su {{azienda}} ...": è una f-string, perché dentro c'è la variabile AVVISO. Nelle f-string le graffe doppie {{azienda}} diventano graffe singole, quindi a CrewAI arriva il normale Segnaposto {…}: Una parola tra graffe, come {citta}, dentro i testi di agenti e task. Al kickoff(inputs={...}) viene sostituita con il valore vero. glossario {azienda}.
Versione A, con le notizie dal web#
# /// script
# requires-python = ">=3.10,<3.14"
# dependencies = ["crewai[tools]==1.15.21"]
# ///
"""Un analista raccoglie notizie recenti su un'azienda quotata e un autore scrive una nota su rischi e opportunità."""
import os
from typing import Any
from crewai import Agent, Crew, Process, Task, TaskOutput
from crewai_tools import ScrapeWebsiteTool, SerperDevTool
# Il modello si sceglie nel file .env (capitolo 7). Se MODEL manca, si usa quello scritto qui.
MODELLO = os.getenv("MODEL", "openai/gpt-4.1-mini")
AVVISO = "Questa nota non è una consulenza finanziaria"
def controlla_nota(risultato: TaskOutput) -> tuple[bool, Any]:
"""Guardrail: senza avviso o senza link alle fonti, la nota torna all'agente."""
testo = risultato.raw
if AVVISO.lower() not in testo.lower():
return (False, f"Manca l'avviso. Inizia la nota con la frase: {AVVISO}.")
if "http" not in testo:
return (False, "Mancano le fonti: accanto a ogni fatto metti il link dell'articolo da cui viene.")
return (True, testo)
analista = Agent(
role="Analista di notizie finanziarie",
goal="Raccogliere i fatti recenti e verificabili su {azienda}, ognuno con data e link della fonte",
backstory=(
"Lavori nella redazione di un giornale economico. Distingui i fatti dalle opinioni "
"e non riporti mai un numero senza il link da cui viene."
),
tools=[SerperDevTool(search_type="news", n_results=8), ScrapeWebsiteTool()],
inject_date=True, # l'agente conosce la data di oggi e può capire cosa è "recente"
max_iter=10,
llm=MODELLO,
verbose=True,
)
autore = Agent(
role="Autore di note per risparmiatori prudenti",
goal="Scrivere una nota breve ed equilibrata su rischi e opportunità di {azienda}",
backstory=(
"Scrivi per persone che non sono esperte di finanza. Non dai mai consigli di acquisto o vendita: "
"metti in fila i fatti e spieghi cosa potrebbe andare bene o male."
),
llm=MODELLO,
verbose=True,
)
raccolta = Task(
description=(
"Cerca le notizie delle ultime settimane su {azienda}. Apri e leggi le 3 più rilevanti.\n"
"Per ogni fatto annota la data, cosa è successo e il link. Se un numero compare in una sola fonte, "
"segnalalo. Separa i fatti dalle opinioni dei giornalisti."
),
expected_output="Un elenco di 5-8 fatti in italiano, ciascuno con data, descrizione breve e link della fonte.",
agent=analista,
)
nota = Task(
description=(
f"Scrivi la nota su {{azienda}} usando solo i fatti raccolti. Inizia con la frase: {AVVISO}.\n"
"Accanto a ogni fatto o numero metti il link della fonte. Non dare consigli di acquisto o vendita."
),
expected_output=(
"Una nota in Markdown, in italiano, massimo 350 parole: avviso iniziale, riassunto di 3 righe, "
"3 rischi e 3 opportunità con link, e una sezione 'Da verificare' con i numeri da controllare "
"sulle fonti ufficiali dell'azienda."
),
agent=autore,
context=[raccolta],
guardrail=controlla_nota,
markdown=True,
output_file="nota_notizie.md",
)
crew = Crew(agents=[analista, autore], tasks=[raccolta, nota], process=Process.sequential, verbose=True)
if __name__ == "__main__":
risultato = crew.kickoff(inputs={"azienda": "Enel"})
print("\n=== NOTA (salvata anche in nota_notizie.md) ===")
print(risultato) # stampa il testo finale, lo stesso salvato nel fileAl posto di "Enel" puoi scrivere il nome di qualsiasi società quotata. L'abbiamo scelta solo perché è molto presente nelle notizie italiane.
Eseguirlo#
Dalla cartella analisi-finanziaria:
uv run --env-file .env indici_bilancio.py
uv run --env-file .env nota_notizie.pyCon la versione B vedrai l'analista chiamare «Calcolatore di indici di bilancio», ricevere le tre righe di indici e commentarle. Poi l'autore scrive la nota, che finisce anche in nota_bilancio.md, e in fondo Python stampa gli indici per il confronto. Con la versione A vedrai prima le ricerche su Serper e l'apertura di alcuni articoli. La nota con i link finisce in nota_notizie.md. Ogni ricerca consuma un credito Serper.
Non abbiamo eseguito questo esempio dal vivo con un modello: qui non trovi un output reale. Gli script sono stati verificati con CrewAI 1.15.21 senza chiamare il modello, e lo strumento calcola_indici è stato provato da solo sul CSV del corso. Per il 2025, per esempio, restituisce margine EBITDA 9.0%, ROE 6.1%, liquidità corrente 1.10 e PFN/EBITDA 4.31: sono i numeri con cui confrontare la tua nota.
Come migliorarlo#
- Unisci le due versioni su un'azienda vera. Prendi i numeri dal bilancio ufficiale di una società, copiali tu in un CSV (controllandoli) e dai al primo agente sia
calcola_indicisia gli strumenti di ricerca. Numeri dal codice, contesto dalle notizie. - Un guardrail sui numeri. Scrivi una funzione che estrae dalla nota tutte le percentuali e controlla che ciascuna compaia nell'output di
calcola_indici. Se l'autore ne ha inventata una, la nota torna indietro (capitolo 13). - Calcola prima, non durante. Se gli indici servono sempre, puoi chiamare
calcola_indici.run(...)prima del kickoff e passare il risultato negliinputs. L'agente non può più «dimenticarsi» di usare lo strumento. - Nota strutturata. Con
output_pydanticla nota diventa un oggetto conrischi: list[str]eopportunita: list[str], pronto per un foglio di calcolo o una dashboard. - Bilanci in PDF. Con la knowledge del capitolo 15 l'agente può consultare un bilancio in PDF. Ricorda però che anche così i numeri li legge il modello: per i conti torna sempre a uno strumento in Python.
Anche nella versione B l'autore può copiare male un indice o attribuirlo all'anno sbagliato: confronta la nota con i numeri stampati sotto.
Le notizie non sono dati. Serper restituisce quello che trova: articoli vecchi ripubblicati, pagine a pagamento che lo scraper non riesce a leggere, opinioni presentate come fatti. Il guardrail controlla che ci siano dei link, non che dicano il vero.
Niente consigli di investimento. Una nota generata non conosce la tua situazione né i tuoi obiettivi. Per imparare a leggere un'informazione finanziaria, la CONSOB ha una sezione di educazione finanziaria per i risparmiatori.
Dati riservati. Il bilancio di un'azienda non quotata non è pubblico. Mandarlo a un modello online significa consegnarlo al provider: chiedi il permesso o usa Ollama.
Prova tu: chiedi i conti al modello
Togli tools=[calcola_indici] dall'analista e cambia la descrizione del task in «Leggi questi dati e calcola margine EBITDA, ROE e PFN/EBITDA per ogni anno», incollando nel testo le tre righe del CSV. Rilancia e confronta i numeri con quelli stampati da Python in fondo. Poi rilancia una seconda volta. Cosa noti?
Soluzione. Con un modello grande molti numeri saranno giusti, ma quasi sempre qualcuno si discosta: un arrotondamento diverso, una divisione sbagliata, un indice calcolato con la voce sbagliata (per esempio il ROE diviso per i ricavi). Con un modello piccolo gli errori sono più frequenti. E soprattutto i numeri possono cambiare tra una esecuzione e l'altra, cosa che una divisione in Python non fa mai. È la dimostrazione pratica di perché lo strumento esiste.
- L'analisi finanziaria è 6ª nella classifica: 501 repository «financial» e 417 «stock» su GitHub, ma nessun sondaggio aziendale la mette tra gli usi principali.
- Un LLM non calcola, prevede cifre plausibili. I conti si fanno in Python, dentro uno strumento
@tool; il modello li commenta. - La versione A usa
SerperDevTool(search_type="news"),ScrapeWebsiteTooleinject_date=True; la versione B non ha bisogno di chiavi esterne. - Un guardrail impone l'avviso «non è una consulenza finanziaria» e, nella versione A, la presenza delle fonti.
- Ogni numero di una nota generata va controllato sulle fonti ufficiali.
Nel prossimo esempio passiamo alle vendite: un flow che dà un punteggio ai potenziali clienti e prepara le email solo per quelli più promettenti, con una persona che approva prima di tutto.
Corso indipendente, non affiliato a CrewAI Inc. Contenuti verificati su CrewAI 1.15.21 il 15 settembre 2026.