Python e PHP nella stessa pipeline AI: FastAPI come orchestrator di LLM per backend Laravel
A inizio novembre 2025 avevo nella mia sandbox personale due prototipi paralleli che avevano lo stesso obiettivo - orchestrare un Retrieval-Augmented Generation su documentazione tecnica interna - costruiti su stack diversi. Uno era tutto in Python con FastAPI, Langchain, sqlite-vss. L'altro era tutto in PHP con un wrapper HTTP su Claude API, parsing manuale di chunk, MySQL con indice full text. Il confronto dopo tre settimane di uso quotidiano è stato istruttivo: il prototipo Python era più veloce da iterare sulle pipeline AI (le librerie ci sono, pronte all'uso) ma si scontrava regolarmente con la logica di business che avevo già codificato in un backend Laravel esistente; il prototipo PHP era più diretto sull'integrazione business ma richiedeva di reinventare ogni primitiva AI. Ho chiuso entrambi e ricostruito una terza versione ibrida: FastAPI come orchestrator degli LLM, Laravel come backend di dominio, comunicazione via HTTP con autenticazione mTLS. È il pattern che tengo oggi come baseline per i progetti di consulenza dove Python è lo strumento giusto per il layer AI ma il cuore applicativo vive già in PHP. Racconto qui come l'ho costruito, dove ho inciampato, e quali trappole evitare.
Perché separare orchestrazione AI e logica di business in stack diversi?
L'ecosistema Python AI nel 2026 è qualitativamente diverso da quello PHP. Librerie come Langchain, LlamaIndex, Haystack, PydanticAI sono maintainate da team dedicati con release cadence settimanale; i binding ufficiali per gli SDK Anthropic e OpenAI aggiungono feature in Python prima che nelle altre lingue (è esplicitamente dichiarato nella documentazione Anthropic sulla donazione MCP alla Agentic AI Foundation del 9 dicembre 2025, con 97M+ download mensili tra Python e TypeScript SDK). Per un workflow AI nuovo, Python arriva con due-tre mesi di vantaggio sulle funzionalità più recenti.
PHP in produzione aziendale ha però il suo vantaggio non negoziabile: è dove vive la logica di business esistente. Un gestionale Laravel con 200.000 righe di codice, controller che orchestrano workflow di fatturazione, un dominio business maturato in anni di iterazione non si riscrive in Python per "stare vicini alle librerie AI". La scelta realistica non è "quale linguaggio scegliere" - è "come far convivere entrambi con il minor debito architetturale possibile". La risposta ingegneristica convenzionale è la separation of concerns: l'orchestrazione AI gira in Python come servizio dedicato; il backend di dominio gira in PHP dove già vive; i due comunicano via HTTP con contract formale.
Quali sono i prerequisiti per questa architettura?
Prima di partire con l'implementazione controllo cinque prerequisiti operativi. Il primo è avere un contract HTTP chiaro tra i due servizi: quali endpoint espone Python, quali parametri accetta, quale è lo schema di risposta. Nella mia pipeline uso OpenAPI 3.1 come schema source of truth - FastAPI lo genera automaticamente dai type hint Pydantic, Laravel lo consuma via HTTP client tipato (uso spatie/laravel-data lato PHP per specchiare lo schema).
Il secondo è la gestione dei segreti. Le API key Anthropic/OpenAI sono long-lived credential che, se esposte, costano soldi reali e aprono rischi di denial-of-wallet. Nella mia architettura le API key vivono solo lato Python in variabili d'ambiente gestite da un secret manager (uso HashiCorp Vault in staging, systemd credential in produzione single-VPS); Laravel non vede mai le API key, vede solo l'endpoint interno Python.
Il terzo è il rate limiting inter-service. Python non deve essere un open endpoint che chiunque può chiamare passando dietro Laravel; servizi distinti devono parlarsi via canali autenticati. Il quarto è il logging correlato: un request ID generato da Laravel deve propagarsi a Python per permettere tracing end-to-end. Il quinto è il deployment unificato: in produzione PMI single-VPS i due servizi convivono sulla stessa macchina con porte diverse, reverse proxy che li distingue, un sistemista che può fermarli e ripartirli con un singolo comando.
Come si struttura il servizio FastAPI
Il servizio Python è deliberatamente minimale: ha un solo compito, orchestrare LLM su richiesta. Non tocca database di business, non gestisce autenticazione utente, non fa autorizzazione applicativa. La struttura che tengo come template ha tre moduli:
fastapi-orchestrator/
├── app/
│ ├── main.py # FastAPI entry point, middleware, health check
│ ├── auth.py # mTLS verification + API key check
│ ├── orchestrator.py # Langchain/LlamaIndex wrapper
│ └── schemas.py # Pydantic models (input/output contract)
├── tests/
├── pyproject.toml # uv/poetry managed
└── DockerfileIl modulo main.py espone tre endpoint: POST /extract per estrazione strutturata da documento, POST /chat per conversazioni RAG-enabled, GET /health per monitoring. Ogni endpoint riceve il suo input via Pydantic model (validation a schema automatica) e restituisce sempre un JSON validato contro lo schema di output. Non esistono endpoint "generic" che ricevono prompt arbitrario - ogni funzionalità ha il suo contract stretto. Questo è fondamentale per evitare che Laravel invii un prompt malevolo al Python (via prompt injection indiretta in dati applicativi) e ottenga comportamenti imprevisti.
L'autenticazione mTLS è il layer di sicurezza primario tra i due servizi. In produzione uso certificati generati internamente tramite openssl con CA privata; FastAPI verifica il certificato client a livello uvicorn e rigetta chiamate non autenticate. L'API key applicativa è un secondo layer (defense in depth): Laravel include nell'header X-Internal-Auth: <secret> un token HMAC che Python valida. Se uno dei due layer salta, il secondo tiene.
Come Laravel consuma il servizio
Lato Laravel costruisco un client HTTP tipato con Saloon (moderno ottimo) o una Service class custom che wrappa Http::timeout()->post(). Il pattern è questo:
namespace App\Services\AiOrchestrator;
use Illuminate\Support\Facades\Http;
use App\Data\InvoiceExtractionRequest;
use App\Data\InvoiceExtractionResponse;
class OrchestratorClient
{
public function __construct(
private readonly string $baseUrl,
private readonly string $internalToken,
) {}
public function extractInvoice(InvoiceExtractionRequest $request): InvoiceExtractionResponse
{
$response = Http::withHeaders(['X-Internal-Auth' => $this->buildHmac($request)])
->timeout(30)
->retry(2, 500)
->post("{$this->baseUrl}/extract", $request->toArray())
->throw();
return InvoiceExtractionResponse::from($response->json());
}
}Il client è registrato come singleton nel container Laravel, iniettato nei service di business che ne hanno bisogno, testabile via Http::fake(). La logica di retry è fondamentale: inferenza LLM può richiedere 5-30 secondi e può fallire per timeout, rate limit, cambio di versione modello. Un retry automatico con exponential backoff gestisce la transitorietà; un circuit breaker (uso league/flysystem-circuit-breaker o implementazione custom) gestisce il failure protratto dove il Python è down.
Se stai pianificando un'architettura che integra Python e PHP per scalare capacità AI senza riscrivere il tuo backend, nel mio hub dedicato all'integrazione AI per aziende trovo raccolti gli articoli tecnici con metodologia applicata che uso nei progetti di consulenza cross-stack.
Quali trappole ho incontrato mettendolo in produzione
La prima trappola è stata il timeout management. LLM inference ha code wait time variabili: una chiamata può completare in 2 secondi o in 28. Un timeout Laravel troppo aggressivo (30s) causa false failure proprio quando l'utente ha atteso di più; un timeout troppo alto (300s) mantiene in hang il queue worker se Python è davvero bloccato. Ho trovato l'equilibrio con adaptive timeout: 45 secondi per extraction semplice, 120 secondi per RAG conversation, 300 secondi per batch embedding. Ogni tipo di richiesta ha il suo SLA operativo.
La seconda trappola è stato il memory leak in FastAPI sotto carico. Langchain carica modelli embedding in memoria e non sempre li libera correttamente su errore - dopo qualche migliaio di richieste RAM piena, il processo Python si blocca. La mitigazione che uso è uvicorn con --max-requests 1000 --max-requests-jitter 100: il worker si ricicla automaticamente prima che la memoria diventi problema, senza interruzione di servizio.
La terza trappola è stata il drift tra versioni Python. Langchain rilascia minor ogni 2-3 settimane e introduce breaking change con regolarità frustrante. La mia difesa è pin esatto delle versioni in pyproject.toml, renovate-bot che apre PR automatiche per upgrade, CI che esegue una regression suite specifica prima di mergiare ogni upgrade. Questa pipeline ha scoperto nella mia sandbox tre regressioni reali tra dicembre 2025 e aprile 2026 che sarebbero arrivate in produzione senza il harness.
La quarta trappola è stata il log correlation. Quando qualcosa va storto in produzione tu vuoi correlare il request ID Laravel con i log Python. Senza un trace ID comune, il debugging è cercare aghi in pagliai. La mia pipeline inietta un X-Request-ID Laravel che Python propaga su tutti i log e su ogni chiamata a Claude/OpenAI (le API supportano metadata.user_id opzionale). Con jq e grep sui log di entrambi i servizi riesco a ricostruire una singola request end-to-end in meno di un minuto.
Come gestisco il deployment su single VPS
Nella mia pipeline personale il target di deployment tipico è un VPS Hetzner AX52 (Ryzen 7 7700, 64 GB RAM DDR5) che ospita insieme Laravel + FastAPI + MySQL + Redis. Docker Compose gestisce i tre servizi applicativi con rete interna dedicata e restart: unless-stopped. Nginx come reverse proxy espone solo Laravel all'esterno e instrada /internal/ai/* verso il container FastAPI nella rete privata. Il certificato mTLS è generato da una CA interna e rigenerato ogni 6 mesi.
Il sizing che ho validato nella mia sandbox: Python+Langchain+embedding model all-MiniLM-L6-v2 consuma ~1.8 GB RAM stabile, con picchi fino a 4 GB sotto carico burst; Laravel+PHP-FPM con 16 worker sta sotto i 3 GB; MySQL+Redis altri 6 GB. Rimane ampio margine per kernel buffer e disk cache. Il costo operativo totale nella mia sandbox è €38/mese per il VPS più €15-80/mese per chiamate API Anthropic a seconda del carico. È un deployment footprint compatibile con budget PMI italiane, e l'architettura ibrida non impone il costo di Kubernetes o di cloud provider premium.
Come gestisco l'osservabilità cross-stack
L'osservabilità in un sistema ibrido Python+PHP è più complicata di un monolite, ma con strumenti standard è gestibile. Nella mia pipeline uso OpenTelemetry come spine dorsale: entrambi i servizi esportano trace e metrics al formato OTLP, un collector locale (uso otelcol-contrib in container separato) li centralizza e li invia a Grafana Tempo per il tracing e Prometheus per le metriche. Il dashboard Grafana correla automaticamente una span Laravel con le span Python che ne derivano, visualizzando end-to-end la request che attraversa i due servizi.
Le metriche che tengo monitorate sulla pipeline ibrida sono cinque: latency P95 per endpoint Python (SLA: sotto 8 secondi per extraction, 20 secondi per RAG); error rate per endpoint (SLA: sotto 1%); tokens consumed per request (per cost monitoring e detection di prompt bloat); retry rate sul client Laravel (SLA: sotto 5% - oltre significa Python instabile); circuit breaker trip count (SLA: zero - ogni trip è incident). Quando una di queste metriche esce dagli SLA scatta un alert strutturato su Telegram (personale) o su PagerDuty (in scenari enterprise più maturi). Il signal-to-noise dell'alerting è calibrato perché ogni falso positivo riduce la prontezza di reazione ai veri positivi - è lo stesso principio che applico ai security alert, generalizzato al performance monitoring.
Quando questa architettura NON ha senso
Non è la soluzione giusta in due scenari. Primo: se il tuo backend non ha una logica di business significativa e stai iniziando da zero, vai diretto in Python - l'orchestrazione AI sarà sempre il cuore dell'applicazione e non vale la pena separare. Secondo: se il volume AI è molto basso (meno di 100 richieste/giorno), la complessità operativa di mantenere due servizi supera il beneficio; una semplice libreria HTTP client in PHP che chiama Claude API direttamente è sufficiente e meno costosa da manutenere.
Il punto di equilibrio, nella mia esperienza, è attorno a 1.000-5.000 richieste AI al giorno con logica di business PHP non banale. Sotto quella soglia, PHP puro. Sopra quella soglia, ibrido. Molto sopra (50.000+), probabilmente hai anche scenari di scalabilità dove ogni servizio vive su macchine separate e l'architettura diventa più seriamente distribuita.
Il report Deloitte "State of AI in the Enterprise 2026" del 21 gennaio 2026 osserva che il 37% delle aziende usa AI solo a livello superficiale senza cambiare i processi - il sintomo tipico è aver adottato ChatGPT o Copilot come strumento standalone senza integrarlo con il dominio applicativo esistente. L'architettura ibrida Python+PHP è esattamente il bridge che permette di uscire da questa fascia superficiale e portare l'AI dentro il workflow business, senza dover riscrivere il core applicativo.
Se stai valutando un'integrazione AI con il tuo backend PHP esistente e vuoi capire se l'architettura ibrida è la scelta giusta per il tuo volume e la tua complessità, il modulo di preventivo gratuito ti risponde in sette domande - circa due minuti - e ti dice se il tuo scenario rientra nel mio ambito o ti indirizzo verso figure più adatte. Per il lavoro preliminare sull'infrastruttura VPS che ospiterà la pipeline ibrida, dove la robustezza di networking e containerizzazione è prerequisito, trovi un inquadramento operativo nel mio articolo su migrazione sicura di VPS unmanaged a zero downtime per aziende e su monitoring proattivo Laravel per prevenire downtime. Un'architettura multi-stack è potente quando l'infrastruttura sottostante è disciplinata; è un disastro amplificato quando non lo è. La differenza sta tutta nel lavoro ingegneristico silenzioso che fai prima di collegare i servizi.