Claude Agent SDK: Costruire Agenti AI in Produzione

Claude Agent SDK: Costruire Agenti AI in Produzione

Perché il Claude Agent SDK nel 2026

Nel 2026 il paradigma degli agenti AI ha smesso di essere un esperimento da laboratorio ed è diventato infrastruttura reale. Aziende e team di sviluppo costruiscono pipeline dove Claude non risponde soltanto a una domanda, ma pianifica, delega, usa strumenti e porta a termine task complessi in autonomia. Il problema? Farlo bene è tutt’altro che banale.

L’Anthropic Agent SDK (spesso chiamato semplicemente Claude Agent SDK o Agents SDK) è la risposta ufficiale a questa complessità. Fornisce primitive pronte all’uso per costruire agenti che orchestrano altri agenti, gestiscono la memoria, chiamano tool reali e recuperano da errori — tutto con una DX pensata per chi mette le cose in produzione, non solo in un notebook Colab.

In questa guida vedremo come strutturare un agente reale con l’SDK, come collegare tool use e memoria, come orchestrare sistemi multi-agente e come evitare gli errori più comuni. Se hai già letto la guida al Tool Use con Claude API o il tutorial su come creare un agente AI con JavaScript e Claude, sei nel posto giusto per il passo successivo.

Cos’è Esattamente il Claude Agent SDK

L’Anthropic Agent SDK non è una libreria separata: è un layer di astrazione costruito sopra l’API Anthropic che fornisce:

  • Agent loop automatico: gestisce autonomamente il ciclo tool_use → tool_result senza che tu debba scrivere ogni volta il while loop a mano.
  • Tool registry: un sistema per registrare strumenti con schema JSON e handler corrispondente.
  • Memory primitives: supporto out-of-the-box per memoria a breve termine (conversazione) e a lungo termine (vector store / KV).
  • Orchestrazione multi-agente: pattern per creare agenti orchestratori che delegano a sotto-agenti specializzati.
  • Guardrails: meccanismi per limitare l’autonomia, definire budget di token, impostare timeout.

La filosofia è “bring your own infrastructure”: l’SDK non ti impone un database o un vettore store specifico, ma ti dà le astrazioni per collegare ciò che vuoi.

Setup e Struttura Base di un Agente

Partiamo dall’installazione. Il package ufficiale si chiama @anthropic-ai/sdk e include le primitive agente a partire dalla versione 0.27+.

npm install @anthropic-ai/sdk
# oppure con pnpm
pnpm add @anthropic-ai/sdk

Un agente minimale con tool use e agent loop gestito dall’SDK:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Definizione strumenti
const tools: Anthropic.Tool[] = [
  {
    name: "search_web",
    description: "Cerca informazioni aggiornate sul web",
    input_schema: {
      type: "object",
      properties: {
        query: { type: "string", description: "La query di ricerca" },
        max_results: {
          type: "number",
          description: "Numero massimo di risultati (default 5)",
          default: 5,
        },
      },
      required: ["query"],
    },
  },
  {
    name: "save_note",
    description: "Salva un appunto nella memoria persistente",
    input_schema: {
      type: "object",
      properties: {
        key: { type: "string", description: "Chiave identificativa" },
        content: { type: "string", description: "Contenuto da salvare" },
      },
      required: ["key", "content"],
    },
  },
];

// Handler strumenti (la tua logica reale)
async function executeTool(
  name: string,
  input: Record<string, unknown>
): Promise<string> {
  switch (name) {
    case "search_web":
      // Qui chiami la tua API di ricerca (Brave, Serper, ecc.)
      return JSON.stringify({ results: [`Risultato per: ${input.query}`] });
    case "save_note":
      // Qui scrivi su DB o KV store
      console.log(`[NOTE] ${input.key}: ${input.content}`);
      return "Note salvata con successo.";
    default:
      throw new Error(`Tool sconosciuto: ${name}`);
  }
}

// Agent loop
async function runAgent(userMessage: string): Promise<string> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  while (true) {
    const response = await client.messages.create({
      model: "claude-opus-4-5",
      max_tokens: 4096,
      tools,
      messages,
    });

    // Aggiungi risposta assistant alla conversazione
    messages.push({ role: "assistant", content: response.content });

    // Se l'agente ha finito, restituisci il testo
    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((b) => b.type === "text");
      return textBlock ? (textBlock as Anthropic.TextBlock).text : "";
    }

    // Gestisci tool use
    if (response.stop_reason === "tool_use") {
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      for (const block of response.content) {
        if (block.type === "tool_use") {
          const result = await executeTool(
            block.name,
            block.input as Record<string, unknown>
          );
          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }

      messages.push({ role: "user", content: toolResults });
    }
  }
}

Il pattern è semplice ma efficace: il loop continua finché stop_reason non è end_turn. Ogni iterazione può produrre chiamate a tool multipli in parallelo — e Claude lo farà se può, risparmiando latenza.

Orchestrazione Multi-Agente: Orchestrator + Sub-Agent

Il pattern più potente del Claude Agent SDK è la gerarchia agente-orchestratore. Un agente principale riceve il task, lo scompone e delega parti a sotto-agenti specializzati. Ogni sotto-agente ha il suo set di tool, il suo system prompt e opera in modo indipendente.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Sub-agente specializzato in ricerca
async function researchAgent(topic: string): Promise<string> {
  const response = await client.messages.create({
    model: "claude-haiku-4-5", // modello più leggero per task semplici
    max_tokens: 1024,
    system:
      "Sei un agente di ricerca. Cerca e sintetizza informazioni sul topic dato. Sii conciso e fattuale.",
    messages: [{ role: "user", content: `Ricerca: ${topic}` }],
  });
  const textBlock = response.content.find((b) => b.type === "text");
  return textBlock ? (textBlock as Anthropic.TextBlock).text : "";
}

// Sub-agente specializzato in scrittura
async function writerAgent(
  research: string,
  format: string
): Promise<string> {
  const response = await client.messages.create({
    model: "claude-sonnet-4-5",
    max_tokens: 2048,
    system:
      "Sei un copywriter tecnico. Trasforma la ricerca in contenuto strutturato nel formato richiesto.",
    messages: [
      {
        role: "user",
        content: `Research:
${research}

Formato richiesto: ${format}`,
      },
    ],
  });
  const textBlock = response.content.find((b) => b.type === "text");
  return textBlock ? (textBlock as Anthropic.TextBlock).text : "";
}

// Orchestratore principale
async function orchestrator(task: string): Promise<string> {
  const orchestratorTools: Anthropic.Tool[] = [
    {
      name: "delegate_research",
      description: "Delega una ricerca al sotto-agente specializzato",
      input_schema: {
        type: "object",
        properties: {
          topic: { type: "string", description: "Argomento da ricercare" },
        },
        required: ["topic"],
      },
    },
    {
      name: "delegate_writing",
      description: "Delega la scrittura al sotto-agente scrittore",
      input_schema: {
        type: "object",
        properties: {
          research: {
            type: "string",
            description: "Materiale di ricerca da usare",
          },
          format: {
            type: "string",
            description: "Formato output (blog, report, email, ecc.)",
          },
        },
        required: ["research", "format"],
      },
    },
  ];

  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: task },
  ];

  while (true) {
    const response = await client.messages.create({
      model: "claude-opus-4-5",
      max_tokens: 4096,
      system:
        "Sei un orchestratore. Scomponi il task, delega ai sotto-agenti e sintetizza i risultati.",
      tools: orchestratorTools,
      messages,
    });

    messages.push({ role: "assistant", content: response.content });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((b) => b.type === "text");
      return textBlock ? (textBlock as Anthropic.TextBlock).text : "";
    }

    if (response.stop_reason === "tool_use") {
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      for (const block of response.content) {
        if (block.type === "tool_use") {
          let result = "";

          if (block.name === "delegate_research") {
            const input = block.input as { topic: string };
            result = await researchAgent(input.topic);
          } else if (block.name === "delegate_writing") {
            const input = block.input as { research: string; format: string };
            result = await writerAgent(input.research, input.format);
          }

          toolResults.push({
            type: "tool_result",
            tool_use_id: block.id,
            content: result,
          });
        }
      }

      messages.push({ role: "user", content: toolResults });
    }
  }
}

// Esempio di utilizzo
const result = await orchestrator(
  "Crea un report tecnico su Bun.js vs Node.js per il 2026"
);
console.log(result);

Notare come l’orchestratore usa claude-opus-4-5 (capacità di ragionamento alta) mentre i sotto-agenti usano modelli più leggeri — un’ottimizzazione fondamentale per i costi in produzione. Questo si collega a quanto già discusso nella guida sul RAG con Claude API, dove il routing intelligente tra modelli è già un pattern consolidato.

Memoria e Context Management in Produzione

La memoria è il tallone d’Achille di molti agenti in produzione. L’SDK non gestisce la persistenza per te — sei tu a scegliere il backend — ma fornisce pattern chiari per le tre tipologie principali:

  • In-context memory: la conversazione nel messaggio. Limitata dal context window (200k token in Claude Opus).
  • External memory: database, KV store, vector database. Recuperata tramite tool use.
  • In-weights memory: ciò che il modello ha imparato durante il training. Non modificabile a runtime.

Il pattern più efficace per sessioni lunghe è il sliding window con summarization:

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

const MAX_MESSAGES = 20; // soglia prima di comprimere

async function summarizeHistory(
  messages: Anthropic.MessageParam[]
): Promise<string> {
  const response = await client.messages.create({
    model: "claude-haiku-4-5",
    max_tokens: 512,
    system: "Sintetizza questa conversazione in modo conciso, preservando i fatti chiave.",
    messages: [
      {
        role: "user",
        content: `Conversazione da riassumere:
${JSON.stringify(messages, null, 2)}`,
      },
    ],
  });
  const textBlock = response.content.find((b) => b.type === "text");
  return textBlock ? (textBlock as Anthropic.TextBlock).text : "";
}

class AgentWithMemory {
  private messages: Anthropic.MessageParam[] = [];
  private summary: string = "";

  async chat(userInput: string): Promise<string> {
    // Se la storia è troppo lunga, comprimi
    if (this.messages.length >= MAX_MESSAGES) {
      this.summary = await summarizeHistory(this.messages);
      this.messages = []; // reset storia dettagliata
      console.log("[Memory] Storia compressa.");
    }

    // Costruisci il contesto con summary + storia recente
    const systemPrompt = this.summary
      ? `Contesto precedente:
${this.summary}

Riprendi da qui.`
      : "Sei un assistente AI utile e preciso.";

    this.messages.push({ role: "user", content: userInput });

    const response = await client.messages.create({
      model: "claude-sonnet-4-5",
      max_tokens: 2048,
      system: systemPrompt,
      messages: this.messages,
    });

    const textBlock = response.content.find((b) => b.type === "text");
    const assistantText = textBlock
      ? (textBlock as Anthropic.TextBlock).text
      : "";

    this.messages.push({ role: "assistant", content: assistantText });

    return assistantText;
  }
}

// Utilizzo
const agent = new AgentWithMemory();
console.log(await agent.chat("Ciao! Sto costruendo un e-commerce con Next.js."));
console.log(
  await agent.chat("Quale database mi consigli per gestire i prodotti?")
);

Per progetti più complessi, integra questa logica con Supabase o un vector store come Pinecone — pattern già esplorato in dettaglio nella guida su Supabase nel 2026.

Guardrails, Error Handling e Rate Limiting

Un agente in produzione deve essere resiliente. Le cose che vanno storte: timeout sui tool, rate limit API, risposta malformata, loop infiniti. Ecco come gestirli sistematicamente.

import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Configurazione guardrails
const AGENT_CONFIG = {
  maxIterations: 10, // previene loop infiniti
  maxTokensBudget: 50000, // budget totale per sessione
  toolTimeoutMs: 15000, // timeout per singolo tool
};

// Wrapper con retry e timeout per i tool
async function safeToolExecute(
  toolName: string,
  input: Record<string, unknown>,
  timeoutMs: number = AGENT_CONFIG.toolTimeoutMs
): Promise<string> {
  const timeoutPromise = new Promise<never>((_, reject) =>
    setTimeout(() => reject(new Error(`Tool ${toolName} timeout`)), timeoutMs)
  );

  try {
    const result = await Promise.race([
      executeToolLogic(toolName, input), // la tua logica
      timeoutPromise,
    ]);
    return result as string;
  } catch (error) {
    if (error instanceof Error && error.message.includes("timeout")) {
      return `ERRORE: Tool ${toolName} ha superato il timeout di ${timeoutMs}ms.`;
    }
    return `ERRORE: ${error instanceof Error ? error.message : "Errore sconosciuto"}`;
  }
}

async function executeToolLogic(
  name: string,
  input: Record<string, unknown>
): Promise<string> {
  // Simulazione tool con possibile lentezza
  await new Promise((r) => setTimeout(r, 100));
  return `Risultato da ${name}: ${JSON.stringify(input)}`;
}

// Agent loop con guardrails
async function safeAgentRun(
  userMessage: string,
  tools: Anthropic.Tool[]
): Promise<{ result: string; tokensUsed: number; iterations: number }> {
  const messages: Anthropic.MessageParam[] = [
    { role: "user", content: userMessage },
  ];

  let iterations = 0;
  let totalTokens = 0;

  while (iterations < AGENT_CONFIG.maxIterations) {
    iterations++;

    const response = await client.messages.create({
      model: "claude-opus-4-5",
      max_tokens: Math.min(
        4096,
        AGENT_CONFIG.maxTokensBudget - totalTokens
      ),
      tools,
      messages,
    });

    totalTokens += response.usage.input_tokens + response.usage.output_tokens;

    // Controllo budget
    if (totalTokens >= AGENT_CONFIG.maxTokensBudget) {
      console.warn(
        `[Guardrail] Budget token esaurito dopo ${iterations} iterazioni.`
      );
      break;
    }

    messages.push({ role: "assistant", content: response.content });

    if (response.stop_reason === "end_turn") {
      const textBlock = response.content.find((b) => b.type === "text");
      return {
        result: textBlock ? (textBlock as Anthropic.TextBlock).text : "",
        tokensUsed: totalTokens,
        iterations,
      };
    }

    if (response.stop_reason === "tool_use") {
      const toolResults: Anthropic.ToolResultBlockParam[] = [];

      // Esegui tool in parallelo dove possibile
      const toolPromises = response.content
        .filter((b) => b.type === "tool_use")
        .map(async (block) => {
          if (block.type === "tool_use") {
            const result = await safeToolExecute(
              block.name,
              block.input as Record<string, unknown>
            );
            return {
              type: "tool_result" as const,
              tool_use_id: block.id,
              content: result,
            };
          }
          return null;
        });

      const results = await Promise.all(toolPromises);
      results.forEach((r) => {
        if (r) toolResults.push(r);
      });

      messages.push({ role: "user", content: toolResults });
    }
  }

  return {
    result: "Agente terminato per limite iterazioni.",
    tokensUsed: totalTokens,
    iterations,
  };
}

Il punto chiave è l’esecuzione parallela dei tool: quando Claude chiede più tool nella stessa risposta, Promise.all li esegue concorrentemente invece di aspettare che uno finisca prima di iniziare il successivo. Con tool lenti (chiamate API esterne, query DB), questo può dimezzare la latenza.

Questa logica di error handling si integra perfettamente con pipeline di automazione come quelle descritte nella guida su n8n + Claude per la gestione email.

Deploy in Produzione: Pattern Architetturali

Un agente che funziona in locale non è un agente in produzione. Le differenze cruciali:

  • Statefulness: HTTP è stateless, le sessioni vanno persistite su Redis o DB.
  • Concorrenza: più utenti simultanei richiedono isolamento delle sessioni.
  • Observability: logging, tracing, metriche di costo per iterazione.
  • Streaming: gli utenti si aspettano risposte in real-time, non batch.

Un endpoint Express tipico per un agente in produzione:

import express from "express";
import Anthropic from "@anthropic-ai/sdk";

const app = express();
app.use(express.json());

const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });

// Store sessioni in-memory (usa Redis in prod)
const sessions = new Map<string, Anthropic.MessageParam[]>();

// Endpoint con streaming SSE
app.post("/api/agent/chat", async (req, res) => {
  const { sessionId, message } = req.body as {
    sessionId: string;
    message: string;
  };

  if (!sessionId || !message) {
    return res.status(400).json({ error: "sessionId e message obbligatori" });
  }

  // Recupera storia sessione
  if (!sessions.has(sessionId)) {
    sessions.set(sessionId, []);
  }
  const history = sessions.get(sessionId)!;
  history.push({ role: "user", content: message });

  // Header per SSE
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache");
  res.setHeader("Connection", "keep-alive");

  try {
    // Streaming con SDK
    const stream = await client.messages.stream({
      model: "claude-sonnet-4-5",
      max_tokens: 2048,
      system: "Sei un assistente AI per sviluppatori. Rispondi in italiano.",
      messages: history,
    });

    let fullText = "";

    for await (const chunk of stream) {
      if (
        chunk.type === "content_block_delta" &&
        chunk.delta.type === "text_delta"
      ) {
        const text = chunk.delta.text;
        fullText += text;
        // Invia chunk al client
        res.write(`data: ${JSON.stringify({ text })}

`);
      }
    }

    // Salva risposta in storia
    history.push({ role: "assistant", content: fullText });

    res.write("data: [DONE]

");
    res.end();

    console.log(
      `[Agent] Sessione ${sessionId}: ${history.length} messaggi totali`
    );
  } catch (error) {
    console.error("[Agent Error]", error);
    res.write(
      `data: ${JSON.stringify({ error: "Errore interno dell'agente" })}

`
    );
    res.end();
  }
});

app.listen(3000, () => console.log("Agent server su http://localhost:3000"));

Per approfondire il pattern di streaming, leggi la guida completa su Streaming Response con Claude API — copre tutti i dettagli SSE e gestione del buffer in JavaScript.

FAQ e Domande Frequenti

Qual è la differenza tra Claude Agent SDK e LangChain?

LangChain è un framework generico multi-provider con una forte astrazione su concetti come “chain” e “agent”. Il Claude Agent SDK (ovvero l’@anthropic-ai/sdk con le sue primitive agente) è invece specifico per Claude, più leggero, con meno overhead e una DX più diretta. Se usi esclusivamente Claude e non hai bisogno di portabilità tra LLM, l’SDK ufficiale è quasi sempre la scelta migliore: meno dipendenze, meno bug di compatibilità, aggiornamenti allineati con le nuove funzionalità del modello.

Quante iterazioni può fare un agente prima di diventare troppo costoso?

Dipende dal modello e dal numero di token per iterazione. Con Claude Haiku, 20 iterazioni da 1000 token costano meno di 1 centesimo. Con Claude Opus su task complessi (4000 token per iterazione), 10 iterazioni possono arrivare a 0,50-1 euro. La strategia vincente è routing intelligente: usa Opus per pianificazione e orchestrazione, Haiku per tool call e sotto-task semplici. Il prompt caching (vedi la guida dedicata su Prompt Caching in Claude API) può ridurre i costi del 70-90% su system prompt e contesti ripetuti.

Come gestisco un agente che si blocca in un loop?

Il pattern più robusto è il triplo guardrail: limite sul numero di iterazioni (es. 15 max), limite sul budget token totale per sessione e timeout assoluto sul task (es. 5 minuti). In caso di superamento del limite, l’agente deve restituire un messaggio di errore chiaro invece di continuare. Valuta anche il pattern “observation + reflection”: dopo ogni N iterazioni, chiedi al modello di valutare se sta facendo progressi o è bloccato, e in caso decide autonomamente di interrompere.

Posso usare il Claude Agent SDK con framework come Next.js o Hono?

Assolutamente sì. L’SDK è framework-agnostic: gira su qualsiasi runtime Node.js o Bun. In Next.js 16 puoi usarlo nelle Route Handlers (app/api/agent/route.ts) con risposta streaming tramite ReadableStream. Con Hono, l’integrazione è ancora più pulita grazie al supporto nativo SSE. L’unica attenzione è il timeout delle serverless function: su Vercel il limite è 300 secondi per i piani Pro, sufficiente per la maggior parte degli agenti, ma task molto lunghi vanno eseguiti su runtime persistenti (Fly.io, Railway, EC2).

Conclusione

Il Claude Agent SDK trasforma Claude da “API che risponde” a “sistema che agisce”. I concetti chiave da portare a casa sono tre: il pattern tool use + agent loop come fondamento, l’orchestrazione gerarchica per task complessi, e i guardrails sistematici per tenere tutto sotto controllo in produzione.

Non è magia: è ingegneria. Gli agenti affidabili sono quelli progettati con timeout, budget token, error handling e logging fin dal primo giorno. La differenza tra un prototipo e un agente in produzione sta quasi sempre in questi dettagli operativi, non nella scelta del modello.

Il passo successivo? Guarda il pattern MCP (Model Context Protocol) per connettere strumenti esterni in modo standardizzato — una lettura indispensabile se vuoi costruire agenti che si integrano con l’ecosistema più ampio di tool e servizi.

Suggerimenti e Risorse

🔧 Tool: Usa MCP Server in Node.js per standardizzare i tuoi tool e renderli riutilizzabili tra più agenti senza riscrivere la logica ogni volta.

💡 Pro tip: Inizia sempre con un modello leggero (Haiku) per testare la logica dell’agente. Passa a Sonnet o Opus solo quando il task lo richiede davvero — risparmierai il 90% dei costi in fase di sviluppo.

🎯 Pattern: Separa sempre definizione tool (schema JSON), handler tool (logica) e agent loop (orchestrazione) in moduli distinti. Questa separazione rende il codice testabile, riutilizzabile e facile da debuggare quando l’agente si comporta in modo inatteso.

Condividi

Articoli Recenti

Categorie popolari