I team di platform engineering gestiscono un flusso costante di richieste ripetitive: onboarding utenti, gestione API key, verifica dello stato dei servizi, rotazione credenziali. La maggior parte di questi compiti segue procedure ben definite che un umano esegue passo per passo. E se un agente AI potesse gestirle al suo posto?
In questo articolo descriverò l’architettura di un agente operativo AI-powered che automatizza le attività comuni di piattaforma dando a un LLM accesso agli strumenti interni attraverso un’interfaccia di tool-calling strutturata.
Il Problema#
Una giornata tipica in un team di piattaforma assomiglia a questo:
- “Puoi creare un repo GitLab per il progetto X?”
- “Ho bisogno dell’accesso AWS per il nuovo sviluppatore che entra la prossima settimana.”
- “Qual è lo stato attuale dell’LLM Gateway?”
- “Puoi ruotare l’API key del servizio Y?”
- “Aggiungi questi utenti al progetto Jira.”
Ogni richiesta è semplice, ma collettivamente consumano ore di tempo ingegneristico. Le procedure sono documentate, le API esistono, i permessi ci sono. Manca solo un layer di dispatch intelligente che possa capire la richiesta ed eseguire i passi giusti.
Architettura dell’Agente#
L’agente segue un loop semplice:
Richiesta Utente -> LLM (capisce l'intento) -> Selezione Tool -> Esecuzione Tool -> RispostaL’LLM agisce come il “cervello” che:
- Capisce cosa vuole l’utente
- Seleziona il/i tool giusti
- Passa i parametri corretti
- Interpreta i risultati
- Decide se sono necessari altri passi
from strands import Agent
from strands.models import BedrockModel
from .tools import (
aws_tools,
gitlab_tools,
jira_tools,
keycloak_tools,
email_tools,
)
def create_agent() -> Agent:
model = BedrockModel(
model_id="anthropic.claude-sonnet-4-20250514",
region_name="eu-central-1",
)
tools = [
*aws_tools,
*gitlab_tools,
*jira_tools,
*keycloak_tools,
*email_tools,
]
system_prompt = """Sei un assistente operativo di piattaforma per il team di engineering.
Aiuti con:
- Gestione account e utenti AWS
- Operazioni sui repository GitLab
- Amministrazione progetti Jira
- Gestione utenti Keycloak
- Rotazione credenziali dei servizi
Conferma sempre le operazioni distruttive prima di eseguirle.
Non esporre mai segreti nelle tue risposte.
In caso di dubbio, chiedi chiarimenti invece di indovinare."""
return Agent(
model=model,
tools=tools,
system_prompt=system_prompt,
)Definire i Tool#
Ogni tool è una funzione Python con una docstring chiara che dice all’LLM cosa fa e di quali parametri ha bisogno:
from strands.tools import tool
@tool
def dynamodb_get_item(
table_name: str,
key: dict,
profile: str = "default",
) -> dict:
"""Recupera un elemento da una tabella DynamoDB.
Args:
table_name: Il nome della tabella DynamoDB
key: La chiave primaria dell'elemento da recuperare
profile: Profilo AWS da usare
Returns:
L'elemento se trovato, altrimenti un dict vuoto
"""
import boto3
session = boto3.Session(profile_name=profile)
client = session.client("dynamodb", region_name="eu-central-1")
response = client.get_item(TableName=table_name, Key=key)
return response.get("Item", {})Integrazione con Secrets Manager#
La gestione dei segreti è un compito comune di piattaforma. L’agente deve poter elencare, recuperare e ruotare segreti:
@tool
def secrets_manager_put(
secret_name: str,
secret_value: str,
description: str | None = None,
profile: str = "default",
) -> str:
"""Crea o aggiorna un segreto in AWS Secrets Manager."""
import boto3
session = boto3.Session(profile_name=profile)
client = session.client("secretsmanager", region_name="eu-central-1")
try:
response = client.update_secret(
SecretId=secret_name,
SecretString=secret_value,
**({"Description": description} if description else {}),
)
except client.exceptions.ResourceNotFoundException:
response = client.create_secret(
Name=secret_name,
SecretString=secret_value,
**({"Description": description} if description else {}),
)
return response["ARN"]Sicurezza: Dry-Run di Default#
Il prompt di sistema dice all’agente di confermare le operazioni distruttive, ma dovresti applicarlo anche a livello di tool:
@tool
def s3_delete_objects(
bucket: str,
prefix: str,
dry_run: bool = True,
profile: str = "default",
) -> dict:
"""Elimina oggetti da un bucket S3 che corrispondono a un prefisso.
Args:
dry_run: Se True, elenca solo gli oggetti che verrebbero eliminati
"""
import boto3
session = boto3.Session(profile_name=profile)
s3 = session.client("s3", region_name="eu-central-1")
paginator = s3.get_paginator("list_objects_v2")
objects = []
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
for obj in page.get("Contents", []):
objects.append({"Key": obj["Key"]})
if dry_run:
return {
"mode": "dry_run",
"would_delete": len(objects),
"sample_keys": [o["Key"] for o in objects[:10]],
}
# Elimina in batch da 1000 (limite API S3)
deleted = 0
for i in range(0, len(objects), 1000):
batch = objects[i : i + 1000]
s3.delete_objects(Bucket=bucket, Delete={"Objects": batch})
deleted += len(batch)
return {"mode": "live", "deleted": deleted}Il default dry_run = True significa che l’agente deve optare esplicitamente per il comportamento distruttivo. L’LLM farà naturalmente prima un dry-run, mostrerà i risultati all’utente, e procederà con dry_run=False solo dopo la conferma.
Eseguire l’Agente#
L’agente può essere esposto come CLI, bot Slack o API web. Ecco un semplice loop CLI:
def main():
agent = create_agent()
print("Platform Operations Agent")
print("Digita 'quit' per uscire\n")
while True:
user_input = input("Tu: ").strip()
if user_input.lower() in ("quit", "exit"):
break
response = agent(user_input)
print(f"\nAgente: {response}\n")Esempio di interazione:
Tu: Elenca tutte le API key nella tabella DynamoDB dell'LLM Gateway
Agente: Ho trovato 23 API key attive nella tabella. Ecco un riepilogo:
- 8 key per servizi di produzione
- 10 key per servizi non-produzione
- 5 key per consumatori esterni
Vuoi che mostri i dettagli per una categoria specifica?Lezioni Imparate#
Dopo mesi di utilizzo di questo agente in produzione:
Le descrizioni dei tool sono critiche. L’LLM seleziona i tool in base alle docstring. Descrizioni vaghe portano a selezioni sbagliate. Sii specifico su cosa fa ogni tool e quando usarlo.
Default alla sicurezza. Ogni tool distruttivo dovrebbe avere dry-run come default. L’agente mostrerà naturalmente all’utente cosa intende fare prima di eseguire.
Tool focalizzati. Un tool deve fare una cosa sola. Un tool
secrets_manager_getè meglio di un toolsecrets_manager_manageche tenta di fare tutto in base a un parametro “action”.Logga tutto. Ogni invocazione di tool dovrebbe essere loggata con i suoi parametri e risultato. Questo crea un audit trail e aiuta a debuggare quando l’agente sbaglia.
Inizia in piccolo. Non cercare di automatizzare tutto in una volta. Inizia con le tre richieste più comuni, dimostra che il pattern funziona, poi aggiungi altri tool nel tempo.
Cosa Viene Dopo#
L’evoluzione naturale è connettere l’agente al sistema di ticketing. Quando arriva una nuova richiesta nel service desk Jira, l’agente può:
- Leggere la richiesta
- Classificarla
- Eseguire la procedura standard
- Aggiornare il ticket con i risultati
- Inviare una notifica al richiedente
Questo trasforma il team di piattaforma da una coda di ticket in un servizio automatizzato, liberando gli ingegneri per il lavoro che richiede davvero giudizio umano.