Il problema: un compito che si divide per mestieri
Un articolo tecnico nasce da tre lavori diversi: raccogliere i dati, scrivere, rileggere. Se li chiedi a un solo prompt, il modello li mescola; se scrivi tu il grafo dei passi come nell'articolo su
LangChain e LangGraph, scrivi codice di struttura per un flusso che è già una catena di ruoli. CrewAI propone
un'altra strada: dichiari agenti con ruolo, obiettivo e storia, dichiari task con descrizione e risultato atteso, li metti in una crew e chiami kickoff.
La documentazione ufficiale lo presenta come un framework open source che unisce le Crews (squadre di agenti autonomi) ai Flows (flussi guidati da eventi che gestiscono stato ed esecuzione). Questo
articolo si concentra sulle Crews, spiega che cosa fanno davvero i parametri e dove CrewAI ti toglie visibilità.
Una premessa pratica prima del codice: i pacchetti crewai e crewai-tools dichiarano su PyPI Python <3.14,>=3.10 (letto il 2026-11-12, versione 1.15.41 di entrambi). Gli esempi qui
sotto sono stati eseguiti con Python 3.10.20 in un ambiente virtuale temporaneo, perché con il 3.14 l'installazione non è prevista. Nessun modello è stato chiamato: usiamo una sottoclasse di BaseLLM che risponde
con testo fisso e registra i prompt ricevuti. Così si vede come CrewAI costruisce i prompt, non come ragiona un modello.
Il modello finto
CrewAI accetta come llm anche un oggetto che estende crewai.BaseLLM. Il nostro implementa call, che riceve i messaggi, li salva in _prompt_ricevuti e risponde nel formato
«Thought / Final Answer» che il ciclo dell'agente sa interpretare; supports_function_calling restituisce False perché questo modello non propone chiamate di strumento. Riconosci qui lo schema di tutta la serie:
un ciclo che interroga il modello e interpreta la risposta, con il modello sostituito per poter provare il resto. Serve a una cosa sola: mostrare i prompt. Non dimostra nulla sulla qualità di un modello vero.
from crewai import BaseLLM
class LLMFinto(BaseLLM):
"""Modello finto per CrewAI: non chiama nessuna rete.
Risponde con 'Final Answer' e registra ogni prompt che riceve."""
def __init__(self, nome="finto", **kw):
super().__init__(model=nome, **kw)
self._prompt_ricevuti = []
def call(self, messages, tools=None, callbacks=None, available_functions=None,
from_task=None, from_agent=None, response_model=None):
testo = messages if isinstance(messages, str) else "\n".join(m["content"] for m in messages)
self._prompt_ricevuti.append(testo)
ruolo = from_agent.role if from_agent else "?"
return f"Thought: Ho tutto.\nFinal Answer: [{ruolo}] lavoro completato"
def supports_function_calling(self) -> bool:
return False
Il file si chiama finto.py e lo importano i blocchi seguenti. Se togli la riga che registra il prompt, tutto funziona lo stesso, ma perdi l'unico modo che hai per vedere cosa riceve ciascun agente: nel debug di una crew vera,
è la prima cosa che vorrai.
Agent, Task, Crew: come si leggono insieme
Un Agent ha tre campi obbligatori: role, goal, backstory. Un Task ne ha due: description ed expected_output; l'agente a cui assegnarlo è
opzionale. Una Crew riceve agents e tasks e, per default, li esegue in sequenza. Il parametro che collega i task è context: elencando un task precedente, il suo output viene
inserito nel prompt del task che segue. Segnaposto come {tema} nelle descrizioni si riempiono con il dizionario passato a kickoff(inputs=...).
Il blocco costruisce due agenti e due task, esegue la crew e poi stampa il prompt effettivamente ricevuto dal secondo agente. È la prova più diretta di cosa fanno role, goal e
backstory: non sono metadati, sono testo del prompt.
import os
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true" # niente rete verso il servizio di telemetria
from crewai import Agent, Task, Crew, Process
from finto import LLMFinto
llm = LLMFinto("finto-1")
analista = Agent(role="Analista", goal="Raccogliere dati sugli ordini",
backstory="Lavori sul gestionale da anni e non inventi numeri.",
llm=llm, allow_delegation=False)
redattore = Agent(role="Redattore", goal="Scrivere una nota chiara",
backstory="Trasformi dati in frasi semplici.",
llm=llm, allow_delegation=False)
raccolta = Task(description="Raccogli i dati su {tema}", expected_output="Elenco di dati",
agent=analista)
nota = Task(description="Scrivi una nota", expected_output="Una nota di tre righe",
agent=redattore, context=[raccolta]) # riceve l'output di `raccolta`
crew = Crew(agents=[analista, redattore], tasks=[raccolta, nota], process=Process.sequential)
esito = crew.kickoff(inputs={"tema": "ordini in ritardo"})
print("output finale :", esito.raw)
print("chiamate al modello:", len(llm._prompt_ricevuti))
print("--- prompt ricevuto dal Redattore ---")
print(llm._prompt_ricevuti[-1])
# Output:
# output finale : [Redattore] lavoro completato
# chiamate al modello: 2
# --- prompt ricevuto dal Redattore ---
# You are Redattore. Trasformi dati in frasi semplici.
# Your personal goal is: Scrivere una nota chiara
#
# Current Task: Scrivi una nota
#
# This is the expected criteria for your final answer: Una nota di tre righe
# you MUST return the actual complete content as the final answer, not a summary.
#
# This is the context you're working with:
# [Analista] lavoro completato
#
# Provide your complete response:
Eseguito due volte (Python 3.10.20, crewai 1.15.41) con uscita identica. Il prompt del Redattore comincia con «You are Redattore.», seguito dalla backstory, poi l'obiettivo (goal), il task corrente, il criterio di
expected_output e, sotto «This is the context you're working with», l'output dell'Analista: context=[raccolta] funziona così, copiando l'output intero nel prompt. Due osservazioni: la cornice del prompt è in inglese
anche se la crew è in italiano, quindi una crew italiana produce prompt misti; e poiché il contesto è inserito per intero, un task a monte con un output lungo allunga ogni prompt a valle. Quanto lo allunga dipende dal tuo output: qui non è stato misurato in token.
Che cosa vale se non scrivi il parametro
I tutorial mostrano parametri come max_iter o memory senza dire il valore predefinito, che è ciò che decide il comportamento della tua crew se non li scrivi. Il blocco legge i valori dai campi dei modelli
Pydantic della versione installata, così non dipendono dalla memoria di nessuno.
from crewai import Agent, Crew, Task
# Che cosa vale se non scrivi il parametro (letto dai campi dei modelli Pydantic di crewai 1.15.41)
voci = {Agent: ["verbose", "allow_delegation", "max_iter", "max_rpm", "memory", "cache", "reasoning"],
Crew: ["process", "memory", "cache", "max_rpm", "planning"],
Task: ["context", "async_execution", "human_input", "guardrail"]}
for classe, nomi in voci.items():
print(classe.__name__)
for nome in nomi:
print(f" {nome:18} = {classe.model_fields[nome].default!r}")
# Output:
# Agent
# verbose = False
# allow_delegation = False
# max_iter = 25
# max_rpm = None
# memory = None
# cache = True
# reasoning = False
# Crew
# process = <Process.sequential: 'sequential'>
# memory = False
# cache = False
# max_rpm = None
# planning = False
# Task
# context = NOT_SPECIFIED
# async_execution = False
# human_input = False
# guardrail = None
Tre valori meritano attenzione. max_iter vale 25 per agente: è il tetto ai giri di ragionamento e strumenti di un task, e il valore predefinito è alto, perciò un agente che gira a vuoto consuma molte chiamate prima di fermarsi.
cache è True sull'agente (si riutilizzano i risultati degli strumenti) ma False sulla crew: non sono lo stesso interruttore. allow_delegation è False: la delega tra agenti va
accesa a mano, e conviene accenderla solo dove serve. Per i parametri sul ritmo di chiamata, max_rpm (richieste al minuto) è None, cioè nessun limite.
Sequenziale o gerarchica
I processi disponibili sono due: Process.sequential (i task nell'ordine in cui li dichiari) e Process.hierarchical (un agente manager distribuisce il lavoro). Un elenco dei valori dell'enumerazione
nella versione installata contiene solo questi due; un terzo processo «consensuale», citato in tutorial più vecchi, non c'è. La documentazione aggiunge che per il processo gerarchico devi specificare manager_llm oppure un
manager_agent personalizzato. Il blocco verifica entrambe le cose: costruire la crew gerarchica senza manager fallisce con un errore di validazione, e con manager_llm CrewAI crea da sé un agente «Crew Manager» con due strumenti di delega.
import os
os.environ["CREWAI_DISABLE_TELEMETRY"]="true"; os.environ["OTEL_SDK_DISABLED"]="true"
from crewai import Agent, Task, Crew, Process
from finto import LLMFinto
llm = LLMFinto("finto-2")
r = Agent(role="Analista", goal="Raccogliere dati", backstory="Esperto.", llm=llm)
t = Task(description="Raccogli dati", expected_output="Elenco", agent=r)
try:
Crew(agents=[r], tasks=[t], process=Process.hierarchical)
except Exception as e:
print(type(e).__name__, "-", str(e).strip().splitlines()[-1].split(" [type=")[0].strip())
c = Crew(agents=[r], tasks=[t], process=Process.hierarchical, manager_llm=llm)
out = c.kickoff()
print(out.raw)
print("manager_agent:", c.manager_agent.role if c.manager_agent else None)
print("strumenti del manager:", [x.name for x in c.manager_agent.tools] if c.manager_agent else None)
# Output:
# ValidationError - Attribute `manager_llm` or `manager_agent` is required when using hierarchical process.
# [Crew Manager] lavoro completato
# manager_agent: Crew Manager
# strumenti del manager: ['Delegate work to coworker', 'Ask question to coworker']
Qui il manager è il modello finto, che risponde subito «lavoro completato» senza delegare: l'uscita mostra la struttura (il manager creato e i suoi strumenti Delegate work to coworker e Ask question to coworker), non
il comportamento di un manager vero. Ne ricavi una regola di scelta: nel sequenziale sai in anticipo chi fa cosa e in che ordine; nel gerarchico aggiungi un agente che decide, quindi almeno una chiamata in più a ogni passo di coordinamento e
un percorso meno prevedibile da riprodurre. Se il tuo processo si può scrivere come elenco ordinato, il sequenziale è la scelta più semplice da collaudare.
Strumenti: decoratore e classe
CrewAI offre due modi per scrivere strumenti propri, entrambi in crewai.tools. Il decoratore @tool("nome") trasforma una funzione: il docstring diventa la descrizione che il modello legge e lo schema degli
argomenti nasce dai type hint. La classe BaseTool serve quando lo strumento ha stato o configurazione: dichiari name, description, uno args_schema Pydantic e il metodo _run. Come per
l'articolo su LangChain, il blocco usa un dizionario al posto di un servizio, per restare eseguibile offline, e chiama run sullo strumento direttamente, senza un agente.
import os
os.environ["CREWAI_DISABLE_TELEMETRY"]="true"; os.environ["OTEL_SDK_DISABLED"]="true"
from crewai.tools import tool, BaseTool
from pydantic import BaseModel, Field
ORDINI = {"A-1042": "in consegna"}
@tool("Stato ordine")
def stato_ordine(id: str) -> str:
"""Restituisce lo stato di un ordine dato il codice (formato A-NNNN)."""
return ORDINI.get(id, "ordine inesistente")
class Input(BaseModel):
id: str = Field(description="Codice ordine, formato A-NNNN")
class StatoOrdineTool(BaseTool):
name: str = "Stato ordine (classe)"
description: str = "Restituisce lo stato di un ordine dato il codice."
args_schema: type[BaseModel] = Input
def _run(self, id: str) -> str:
return ORDINI.get(id, "ordine inesistente")
print(type(stato_ordine).__name__, "|", stato_ordine.name)
print(stato_ordine.args_schema.model_json_schema()["properties"])
print(stato_ordine.run(id="A-1042"))
print(StatoOrdineTool().run(id="A-9999"))
# Output:
# Tool | Stato ordine
# {'id': {'title': 'Id', 'type': 'string'}}
# in consegna
# ordine inesistente
Il decoratore produce un oggetto di tipo Tool con lo schema {'id': {'title': 'Id', 'type': 'string'}}; la classe fa la stessa cosa con più codice, ma ti dà un posto per lo stato (una connessione, una chiave). Per
i dati va ripetuto ciò che vale per ogni agente: la protezione sta nei permessi dello strumento, non nella frase che gli chiede di fare il bravo. L'insieme di strumenti già pronti sta nel pacchetto crewai-tools (elencato
nella documentazione), da installare a parte; non sono stati eseguiti qui perché molti richiedono una chiave o la rete.
Memoria: l'assetto è cambiato
Molti tutorial descrivono tre memorie separate (breve termine, lungo termine, entità). La documentazione consultata il 2026-11-12 descrive un'altra cosa: un sistema di memoria unificato, una sola classe Memory che sostituisce
i tipi separati, in cui un modello analizza il contenuto al salvataggio per dedurne ambito, categorie e importanza, e il recupero combina somiglianza semantica, recenza e importanza. Si può usare da sola, con una crew (memory=True
oppure un'istanza configurata) o con un agente. La conseguenza pratica è doppia: la memoria richiede un modello (e di norma un modello di embedding) quindi ha un costo per ogni salvataggio e per ogni richiamo, e dati personali finiti
lì dentro persistono. Non l'abbiamo eseguita: serve un modello vero. Il discorso generale sulla memoria è in
Memory Systems: Context, History e Knowledge Graphs.
Scegliere CrewAI, e quando no
La scelta dipende dalla forma del problema, non dalla popolarità. I criteri che emergono da quanto visto:
- Sì, quando i passi sono ruoli in sequenza (ricerca, stesura, revisione) e vuoi dichiararli più che programmarli.
- Sì, per un prototipo che deve girare presto: sono poche righe, come nel blocco sequenziale.
- Con cautela, quando ti serve vedere ogni passo: parte del coordinamento è dentro il framework e i prompt, come hai visto, li compone lui.
- No, quando il flusso ha rami e riprese su cui vuoi controllo esplicito: lì conviene un grafo, come in LangGraph.
La stessa documentazione propone di combinare i due livelli: i Flows come struttura che governa lo stato, le Crews come unità di lavoro. È un'indicazione di progetto del framework, non una verità universale: vale la pena
provarla solo se hai già un flusso di eventi da governare. Un dettaglio che smentisce tutorial più vecchi: CrewAI ha metodi asincroni (akickoff nativo, kickoff_async basato su thread), quindi non è vero che le crew siano solo sincrone.
Sul confronto con l'altro grande approccio conversazionale, AutoGen, tieni presente che la sua documentazione dichiara oggi il progetto archiviato (vedi
AutoGen e AG2).
Costi, errori, collaudo
Tre abitudini evitano le sorprese. Imposta max_iter in modo esplicito, perché il predefinito è 25 e un agente che si incaglia consuma chiamate a ogni giro. Scrivi expected_output con struttura e lunghezza precise: è
il testo che finisce nel prompt come criterio, e un output sintetico riduce il contesto che i task successivi dovranno portarsi dietro. Collauda la crew con un modello finto come qui: puoi verificare la catena dei context,
il processo e il prompt senza spendere nulla, e riservare i modelli veri a una prova sola, misurata. Il costo reale di una crew dipende dal modello e dai testi, e non è stato misurato qui. Il tema dei test è
ripreso in Testing e Evaluation.
Domande frequenti
CrewAI funziona con Python 3.14?
Secondo i metadati su PyPI (letti il 2026-11-12) crewai 1.15.41 richiede Python maggiore o uguale a 3.10 e minore di 3.14. Per questo gli esempi sono stati eseguiti con Python 3.10.
Che cosa fa davvero il backstory di un agente?
Entra nel prompt: nel prompt registrato dal modello finto compare subito dopo «You are» e il ruolo, prima dell'obiettivo. Non è un metadato; scriverlo bene cambia l'input del modello, ma l'effetto sulla qualità dipende dal modello e non è stato misurato.
Come passo l'output di un task al successivo?
Con il parametro context del task successivo, che elenca i task da cui prendere l'output. CrewAI inserisce quegli output nel prompt, per intero, sotto la dicitura del contesto.
Che cosa sai fare adesso
Sai descrivere una squadra con Agent, Task e Crew, collegare i task con context, e leggere i prompt che CrewAI costruisce. Sai quali valori predefiniti governano la crew se non li scrivi,
quando il processo gerarchico richiede un manager e come scrivere uno strumento con decoratore o classe. Sai anche provare tutto con un modello finto prima di spendere una chiamata. Nel prossimo articolo,
AutoGen e AG2: Conversazione Multi-Agente Emergente, il coordinamento non è una sequenza di ruoli ma una conversazione. Per confrontare le due strade con le altre architetture, vedi Orchestrazione Multi-Agente: Architetture Production.
Fonti
- CrewAI - Introduction - Crews e Flows; consultata il 2026-11-12
- CrewAI - Processes - processi sequenziale e gerarchico, obbligo di
manager_llmomanager_agent; consultata il 2026-11-12 - CrewAI - Crews - metodi
akickoffekickoff_async,token_usage; consultata il 2026-11-12 - CrewAI - Memory - sistema di memoria unificato; consultata il 2026-11-12
- crewai su PyPI - versione 1.15.41 e requisito Python; letto con l'API JSON del registro il 2026-11-12. I valori predefiniti e il comportamento dei blocchi sono quelli osservati con i listati, non letti dalla documentazione

Commenti
Caricamento commenti...
Accedi per commentare