Vai al contenuto

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.

Tempo di lettura: 25 minuti

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.

Attenzione: non è consulenza finanziaria

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: SerperDevTool e ScrapeWebsiteTool. 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_date per 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#

Le due versioni dell'analisi finanziaria Versione A: il nome dell'azienda va all'analista di notizie, che usa SerperDevTool e ScrapeWebsiteTool e passa fatti con data e link all'autore; un guardrail controlla avviso e link prima di salvare nota_notizie.md. Versione B, senza chiavi esterne: un CSV di bilancio fittizio viene letto dallo strumento calcola_indici, scritto in Python, che calcola gli indici; l'analista li commenta, l'autore scrive la nota con guardrail sull'avviso, e alla fine Python stampa gli stessi indici per il confronto. Versione A · notizie dal web (serve la chiave Serper) {azienda} per esempio Enel Analista di notizie SerperDevTool ScrapeWebsiteTool fatti con data e link Autore della nota rischi e opportunità guardrail: avviso e link presenti nota_notizie.md da verificare sulle fonti Versione B · senza chiavi esterne: i conti li fa Python Bilancio CSV fittizio, tre anni @tool calcola_indici Python calcola ROE, margini, debiti Analista chiama lo strumento e commenta Autore rischi e opportunità guardrail Nota + indici stampati per il confronto gli stessi numeri, senza passare dal modello il modello commenta i numeri, non li calcola
In alto la versione A, che parte dalle notizie del web. In basso la versione B: lo strumento calcola_indici in verde è codice Python, e alla fine Python stampa gli stessi numeri accanto alla nota per il confronto.

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:

.env
MODEL=openai/gpt-4.1-mini
OPENAI_API_KEY=sk-la-tua-chiave
SERPER_API_KEY=la-tua-chiave-serper
CREWAI_DISABLE_TELEMETRY=true

Per 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.

bilancio_fittizio_aurora.csv
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,1520

Se non hai mai visto un bilancio, ecco cosa calcola lo strumento, in parole semplici (sono definizioni semplificate, adatte a questo esercizio):

IndiceCome si calcolaCosa racconta
Crescita dei ricaviricavi di quest'anno rispetto all'anno primase l'azienda vende di più
Margine EBITDAEBITDA diviso ricaviquanto resta delle vendite dopo i costi della gestione
Margine nettoutile netto diviso ricaviquanto guadagna davvero su ogni euro venduto
ROEutile netto diviso patrimonio nettoquanto rende il capitale dei soci
Liquidità correnteattivo corrente diviso passivo correntese 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 / EBITDAPFN divisa EBITDAquanti 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:

indici_bilancio.py (1 di 3)
@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).name tiene 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 cercherebbe passwd nella cartella corrente e risponderebbe con un errore. È lo stesso principio del sandbox di FileReadTool, applicato a mano.
  • Il modulo csv fa parte di Python: non serve installare niente. csv.DictReader trasforma 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#

indici_bilancio.py (2 di 3)
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.

indici_bilancio.py (3 di 3)
    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#

nota_notizie.py (pezzo)
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#

indici_bilancio.py
# /// 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#

nota_notizie.py
# /// 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 file

Al 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:

Terminale
uv run --env-file .env indici_bilancio.py
uv run --env-file .env nota_notizie.py

Con 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#

  1. 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_indici sia gli strumenti di ricerca. Numeri dal codice, contesto dalle notizie.
  2. 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).
  3. Calcola prima, non durante. Se gli indici servono sempre, puoi chiamare calcola_indici.run(...) prima del kickoff e passare il risultato negli inputs. L'agente non può più «dimenticarsi» di usare lo strumento.
  4. Nota strutturata. Con output_pydantic la nota diventa un oggetto con rischi: list[str] e opportunita: list[str], pronto per un foglio di calcolo o una dashboard.
  5. 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.
Attenzione

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.

In breve
  • 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"), ScrapeWebsiteTool e inject_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.