-----------------------------------

Acquista i software ArcGIS tramite Studio A&T srl, rivenditore autorizzato dei prodotti Esri.

I migliori software GIS, il miglior supporto tecnico!

I migliori software GIS, il miglior supporto tecnico!
Azienda operante nel settore GIS dal 2001, specializzata nell’utilizzo della tecnologia ArcGIS e aderente ai programmi Esri Italia Business Network ed Esri Partner Network

-----------------------------------



mercoledì 28 ottobre 2026

Distributed AI Agents in .NET

NicoGIS Tech Weekly · Engineering Deep Dive

Distributed AI Agents in .NET: Fan-Out/Fan-In, Durable Orchestration, State Management e Verifiable Results

28 ottobre 2026 · C# / .NET 10 · AI Agents · Distributed Systems

Dal parallelismo delle chiamate LLM a un sistema distribuito capace di recuperare i task, governare gli effetti collaterali e produrre risultati verificabili.

Abstract

Un sistema multi-agente non diventa distribuito soltanto perché avvia molte chiamate LLM in parallelo. Per essere utilizzabile in produzione deve garantire durabilità dell’orchestrazione, controllo della concorrenza, idempotenza, provenienza dei risultati e validazione indipendente dal modello. Questo laboratorio costruisce una pipeline fan-out/fan-in in .NET 10, distinguendo un prototipo eseguibile localmente da un deployment durevole su Azure.

Contratto del laboratorio: un coordinatore divide un dataset in partizioni; worker indipendenti producono osservazioni strutturate; un verificatore deterministico accetta solo risultati che rispettano schema, origine e vincoli; il reducer aggrega in modo ripetibile. Non si presume che un consenso fra LLM equivalga a una prova.

1. Il modello di esecuzione: fan-out/fan-in

Dato un insieme di unità di lavoro W = {w₁, …, wₙ}, il fan-out crea attività indipendenti Tᵢ = Analyze(wᵢ). Il fan-in attende le attività richieste e calcola R = Reduce(Validate(T₁), …, Validate(Tₙ)). Il parallelismo riduce il tempo totale soltanto se le attività sono sufficientemente indipendenti e le risorse esterne non sono già saturate.

INPUT DATASET → PARTITIONER → [WORKER 1 | WORKER 2 | … | WORKER N] → VALIDATION → DEDUPLICATION → REDUCER → AUDITABLE REPORT

La capacità effettiva dipende dal limite più stretto fra quote del provider LLM, connessioni, CPU, memoria, I/O e throughput del sistema di persistenza. Il numero di agenti attivi non deve essere usato come unica metrica di scalabilità.

2. Architettura di riferimento e confini di responsabilità

Separiamo orchestrator (decide quali attività attendere), activities (eseguono I/O e chiamate LLM), validator (controlli deterministici), result store (persistenza idempotente) e reducer (aggregazione).

HTTP / JOB SUBMISSION
↓
DURABLE ORCHESTRATOR (workflow ID, retry policy, fan-out/fan-in)
↓
ACTIVITIES → LLM / TOOLS → IMMUTABLE RESULT STORE
↓
SCHEMA + SOURCE + POLICY VALIDATORS
↓
DETERMINISTIC REDUCER → REPORT + PROVENANCE

Un agent framework gestisce conversazioni, strumenti e sessioni; un durable workflow engine gestisce il ciclo di vita delle esecuzioni. Le due responsabilità sono complementari, non intercambiabili.

3. Stato durevole, checkpoint e deterministic replay

Nei motori di orchestrazione durevole, lo stato di avanzamento viene ricostruito da eventi persistiti o checkpoint. Per questo l’orchestratore deve evitare operazioni non deterministiche: chiamate HTTP dirette, generazione casuale, letture dell’orologio di sistema e mutazioni esterne vanno spostate nelle attività. In caso di replay, il coordinatore deve poter ricostruire la stessa sequenza logica di decisioni.

La durabilità non implica exactly-once execution degli effetti esterni. Un’attività può completare una scrittura e interrompersi prima che il workflow registri il successo: il retry può rieseguire la stessa operazione. Il contratto corretto è at-least-once delivery + idempotent side effects.

Identificatore raccomandato: SHA256(workflowId | partitionId | analysisVersion | inputHash). Un vincolo univoco sul result store impedisce di inserire due volte lo stesso risultato logico.

4. Modello dati: risultati verificabili

public sealed record WorkItem(
    string WorkflowId,
    string PartitionId,
    string InputSha256,
    string Payload);

public sealed record Finding(
    string SourceId,
    string Claim,
    string Evidence,
    double Confidence);

public sealed record AgentResult(
    string WorkflowId,
    string PartitionId,
    string InputSha256,
    string ModelId,
    string PromptVersion,
    IReadOnlyList<Finding> Findings);

public sealed record VerifiedFinding(
    string PartitionId,
    string SourceId,
    string Claim,
    string Evidence);

Confidence è un'autovalutazione del modello, non una probabilità calibrata né una prova. La provenienza deve includere almeno versione del prompt, modello, hash dell'input, identificativo del task e riferimenti alla fonte.

5. Laboratorio riproducibile: progetto .NET 10 senza dipendenze esterne

Il primo laboratorio è intenzionalmente eseguibile senza credenziali cloud né endpoint LLM. Un worker simulato restituisce osservazioni su record di input; la stessa interfaccia potrà poi essere sostituita con un client Microsoft Agent Framework. Il coordinatore locale implementa concorrenza limitata, cancellation e fan-in, ma non è un durable orchestrator.

dotnet new console -n NicoGIS.DistributedAgents -f net10.0
cd NicoGIS.DistributedAgents
# Sostituire Program.cs con il codice seguente
dotnet run
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;

var inputs = Enumerable.Range(1, 24)
    .Select(i => new WorkItem("demo-2026-10-28", $"part-{i:D3}",
        Hash($"record-{i}"), $"record-{i}"))
    .ToArray();

var results = new ConcurrentDictionary<string, AgentResult>();
using var gate = new SemaphoreSlim(initialCount: 4);
using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var watch = Stopwatch.StartNew();

await Task.WhenAll(inputs.Select(async item =>
{
    await gate.WaitAsync(timeout.Token);
    try
    {
        var result = await AnalyzeAsync(item, timeout.Token);
        // Demo: idempotenza in memoria, NON durevole.
        results.TryAdd($"{item.WorkflowId}:{item.PartitionId}:{item.InputSha256}", result);
    }
    finally { gate.Release(); }
}));

var accepted = results.Values
    .SelectMany(Validate)
    .GroupBy(x => (x.PartitionId, x.SourceId, x.Claim))
    .Select(g => g.First())
    .OrderBy(x => x.PartitionId, StringComparer.Ordinal)
    .ThenBy(x => x.SourceId, StringComparer.Ordinal)
    .ToArray();

Console.WriteLine($"Partitions: {inputs.Length}; accepted: {accepted.Length}; elapsed: {watch.Elapsed}");
foreach (var f in accepted.Take(5))
    Console.WriteLine($"{f.PartitionId} | {f.SourceId} | {f.Claim}");

static async Task<AgentResult> AnalyzeAsync(WorkItem item, CancellationToken ct)
{
    await Task.Delay(80, ct); // Sostituire con una chiamata agent/LLM.
    return new AgentResult(item.WorkflowId, item.PartitionId, item.InputSha256,
        "deterministic-demo", "prompt-v1",
        [new Finding(item.PartitionId, "Record elaborato", item.Payload, 0.9)]);
}

static IEnumerable<VerifiedFinding> Validate(AgentResult result)
{
    foreach (var f in result.Findings)
    {
        if (f.SourceId != result.PartitionId ||
            string.IsNullOrWhiteSpace(f.Claim) ||
            string.IsNullOrWhiteSpace(f.Evidence)) continue;
        yield return new(result.PartitionId, f.SourceId, f.Claim, f.Evidence);
    }
}

static string Hash(string value) => Convert.ToHexString(
    SHA256.HashData(Encoding.UTF8.GetBytes(value)));

public sealed record WorkItem(string WorkflowId, string PartitionId,
    string InputSha256, string Payload);
public sealed record Finding(string SourceId, string Claim,
    string Evidence, double Confidence);
public sealed record AgentResult(string WorkflowId, string PartitionId,
    string InputSha256, string ModelId, string PromptVersion,
    IReadOnlyList<Finding> Findings);
public sealed record VerifiedFinding(string PartitionId, string SourceId,
    string Claim, string Evidence);

Output atteso: 24 partizioni elaborate, 24 osservazioni accettate, cinque righe di anteprima. Il tempo varia in base alla macchina. Il demo non valida ancora la corrispondenza crittografica fra InputSha256 e contenuto originale: nel servizio reale la verifica deve avvenire recuperando l'input autorevole dal data store.

6. Da worker simulato ad agente LLM

Il punto di integrazione è AnalyzeAsync. In produzione il worker dovrebbe invocare un agente configurato con istruzioni versionate, un output JSON vincolato da schema e tool con permessi minimi. L'implementazione concreta dipende dalla versione dei pacchetti Microsoft Agent Framework: evitare di copiare API prerelease senza verificare la documentazione corrispondente.

Un contratto consigliato è IAgentAnalyzer.AnalyzeAsync(WorkItem, CancellationToken). Il client LLM deve gestire timeout, retry per errori transitori, rate limit 429, circuit breaker e telemetria per ogni tentativo. Il retry della chiamata LLM non va confuso con il retry dell'attività durevole.

public interface IAgentAnalyzer
{
    Task<AgentResult> AnalyzeAsync(WorkItem item, CancellationToken cancellationToken);
}

// Contratto stabile dell'applicazione: l'adapter concreto può utilizzare
// Microsoft Agent Framework, Microsoft.Extensions.AI o un endpoint REST.
// L'output deve essere deserializzato e validato PRIMA di essere persistito.

7. Durable orchestration: pattern e pseudocodice

Con Azure Durable Functions / Durable Task, il coordinatore programma attività persistenti e ne attende il completamento. Il seguente frammento è pseudocodice architetturale, non un esempio compilabile: nomi e firme delle API cambiano fra SDK e release.

// PSEUDOCODICE — non copiare come API reale
workflow = await LoadWorkflowInput();
partitions = DeterministicPartition(workflow.InputManifest);
tasks = partitions.Select(p => ScheduleDurableActivity("Analyze", p));
rawResults = await WhenAll(tasks);
verified = await ScheduleDurableActivity("ValidateAndPersist", rawResults);
report = await ScheduleDurableActivity("ReduceAndPublish", verified);
return report;

Per migliaia di partizioni, evitare di caricare grandi payload nella history dell'orchestratore. Persistire i dati in Blob Storage o database e passare soltanto URI, hash e identificativi. Quando il fan-out è enorme, introdurre batch gerarchici e sub-orchestrations per limitare dimensione della history e pressione sullo scheduler.

Il Durable Task Scheduler è il backend di scheduling/gestione durevole: il modello di programmazione scelto (Durable Functions o SDK Durable Task) deve essere verificato con la release installata.

8. Idempotenza, race condition e failure injection

La combinazione critica è: side effect completato + acknowledgment perso. Un retry può ripetere la pubblicazione. Per questo una scrittura deve essere atomica rispetto alla chiave logica del task. SQL Server può usare un vincolo UNIQUE e una transazione; Blob Storage può utilizzare precondizioni di creazione; l'outbox pattern separa la persistenza del risultato dall'invio di eventi esterni.

CREATE TABLE dbo.AgentResults (
    Id bigint IDENTITY(1,1) PRIMARY KEY,
    WorkflowId nvarchar(100) NOT NULL,
    PartitionId nvarchar(100) NOT NULL,
    InputSha256 char(64) NOT NULL,
    ResultJson nvarchar(max) NOT NULL,
    CreatedAtUtc datetime2 NOT NULL DEFAULT SYSUTCDATETIME(),
    CONSTRAINT UQ_AgentResults UNIQUE (WorkflowId, PartitionId, InputSha256)
);

Test di resilienza: terminare un worker dopo il commit ma prima dell'ack; introdurre HTTP 429; rendere un task più lento degli altri; duplicare un messaggio; interrompere il coordinatore; cambiare la versione del prompt. Ogni test deve produrre un esito osservabile e verificabile.

9. Verifiable results: che cosa possiamo davvero dimostrare?

La validazione ha più livelli. Strutturale: JSON/schema e campi obbligatori. Referenziale: SourceId appartiene alla partizione e l'evidenza esiste nel dataset. Semantica deterministica: unità, intervalli, vincoli di dominio e calcoli ripetibili. Revisione assistita: per affermazioni non verificabili automaticamente, il risultato viene marcato come ipotesi e inviato a revisione umana.

Il consenso di cinque agenti sullo stesso claim non è una prova indipendente se condividono modello, prompt, dataset o errore sistematico. Un secondo LLM può aiutare nel triage, ma non sostituisce una fonte autorevole.

Per ogni finding conservare: workflowId, partitionId, hash input, versione prompt, modello, versione tool, riferimento esatto all'evidenza, risultato dei controlli, timestamp e identificativo di esecuzione.

10. Performance engineering: misure e modello di capacità

Per N task indipendenti con durata media t e concorrenza effettiva C, un limite inferiore ideale è T ≳ ceil(N/C) × t, a cui sommare overhead di scheduling, code, retry e riduzione. Se il provider limita a Q richieste al secondo, il throughput non può superare Q senza cambiare quota o workload.

MetricaPerché serve
End-to-end latency P50/P95/P99Misura l'esperienza completa, non solo la risposta del modello
Task throughput e queue ageIndividuano saturazione e accumulo
Retry rate / dead-letter countQuantificano instabilità e fallimenti permanenti
Token per task e costo per risultato verificatoCollegano spesa a qualità effettiva
Validation reject rateRileva degrado di modello, prompt o fonti
Replay / recovery durationMisura la ripresa dopo crash

Il benchmark deve confrontare almeno concorrenza 1, 4, 8 e 16 con dataset fisso, warm-up, stesso modello, stessi limiti di quota e ripetizioni sufficienti a stimare la variabilità. Il prototipo locale usa un delay artificiale: serve a verificare il pattern, non a misurare un LLM reale.

11. Sicurezza, tenant isolation e governance

Ogni worker deve ricevere un contesto di autorizzazione minimo e non poter ampliare i propri privilegi tramite il prompt. Gli strumenti applicano le policy sul server. Separare storage, cache e log per tenant; non inserire segreti nei prompt; utilizzare Managed Identity quando disponibile; limitare e controllare gli endpoint di rete. Le azioni distruttive richiedono un approval gate indipendente dal modello.

La telemetria deve permettere la correlazione workflow → activity → model request → tool call → stored result, evitando di registrare indiscriminatamente dati sensibili o interi prompt in log accessibili a più utenti.

12. Checklist per il passaggio in produzione

  1. Contratto JSON versionato, validazione di schema e provenienza.
  2. Durable orchestration con activity idempotenti e chiavi univoche.
  3. Limiti di concorrenza per modello, tenant e risorsa esterna.
  4. Timeout, retry con backoff/jitter, dead-letter e intervento umano.
  5. Result store transazionale, outbox per pubblicazioni esterne.
  6. Test di crash e replay, duplicazione messaggi e fault injection.
  7. Metriche di qualità oltre a token, latenza e throughput.
  8. Controlli di autorizzazione server-side e audit end-to-end.

13. Riferimenti tecnici

Per verificare le API e la compatibilità delle release, consultare la documentazione ufficiale prima di implementare gli adapter:

Nota editoriale: la parte di codice locale è autosufficiente; la sezione di durable orchestration descrive il pattern senza attribuire firme non verificate a SDK in evoluzione. Versioni, pacchetti e supporto del backend devono essere verificati al momento del deployment.

Conclusioni

La parallelizzazione è soltanto il primo passo. Il valore di una piattaforma multi-agente distribuita emerge quando ogni attività può essere recuperata, ogni effetto esterno può essere deduplicato e ogni risultato può essere ricondotto a una fonte verificabile. Un sistema che genera molte risposte rapidamente non è necessariamente affidabile: l'unità di misura più utile è il costo per risultato verificato e riproducibile.

NicoGIS Tech Weekly — approfondimento tecnico, 28 ottobre 2026.