\n\n\n\n Il mio viaggio nella costruzione di AI conversazionale con SDK - AgntBox Il mio viaggio nella costruzione di AI conversazionale con SDK - AgntBox \n

Il mio viaggio nella costruzione di AI conversazionale con SDK

📖 11 min read2,089 wordsUpdated Apr 4, 2026

Ciao a tutti, Nina qui, tornata dal mio laboratorio digitale (che, ammettiamolo, è principalmente il mio tavolo da cucina con un sacco di caffè tiepido). Oggi voglio parlare di qualcosa che ha fatto il giro nei miei canali Slack e ha ossessionato le mie sessioni di codifica notturne: il mondo a volte frustrante, spesso brillante degli AI SDK, in particolare quando cerchi di costruire un agente conversazionale che sembri davvero una conversazione, non solo un bot per FAQ glorificato.

La mia particolare ossessione ultimamente è stata integrare l’Assistants API di OpenAI. Ora, so cosa state pensando, “Nina, l’Assistants API? È vecchia!” E sì, non avete torto. È uscita da un po’. Ma ascoltatemi: non sono qui per darvi un tutorial base su come chiamare client.beta.assistants.create(). Li abbiamo visti tutti. Il mio focus oggi è su un problema specifico e intricato che ho incontrato e su come ho lottato con l’SDK per farlo funzionare come volevo: gestire conversazioni multi-turn con stato e strumenti personalizzati senza perdere la testa (o il contesto del mio utente). Pensate oltre le semplici domande e risposte. Pensate a flussi di lavoro complessi, domande di follow-up e chiamate ad strumenti dinamiche basate sull’intento dell’utente in evoluzione.

La Stabilità della Mia Sanità: Perché l’Assistants API (ancora) Conta

Prima dell’Assistants API, costruire qualsiasi cosa con stato usando i modelli di OpenAI spesso sembrava essere un giocoliere con troppe palline in aria. Eri responsabile della gestione della cronologia dei messaggi, ingegnerizzazione dei prompt per il contesto e orchestrazione delle chiamate agli strumenti da solo. Era… estenuante. L’Assistants API prometteva di toglierci gran parte di quel peso dalle spalle gestendo i thread, i messaggi e persino l’orchestrazione degli strumenti. E per casi semplici, funziona magnificamente.

Il mio progetto attuale coinvolge la creazione di un assistente finanziario “intelligente”. Non solo uno che ti dice il tuo saldo, ma uno che può aiutarti a pianificare budget, suggerire strategie di investimento in base alla tua tolleranza al rischio e persino simulare scenari finanziari futuri. Questo richiede molte interazioni, ricordando affermazioni precedenti (“Voglio risparmiare per una casa,” “Il mio reddito è X,” “E se investo in Y?”), e soprattutto, chiamando funzioni specifiche (come recuperare dati azionari in tempo reale o eseguire un algoritmo di previsione di budget) al momento giusto.

Il mio primo tentativo è stato un glorioso disastro. Passavo l’intera cronologia dei messaggi avanti e indietro, cercando di infilare istruzioni complesse nel prompt del sistema e controllando manualmente se fosse necessaria una chiamata a uno strumento. Funzionava, ma era fragile, lento e un incubo da debug. È stato allora che ho deciso di dare il massimo nell’Assistants API e nel suo SDK.

L’Abbraccio dell’SDK: Thread, Messaggi e il Fuggente `run`

Il cuore dell’Assistants API è il concetto di `Threads` e `Messages`. Crei un `Thread` per una conversazione, aggiungi `Messages` ad esso e poi “esegui” l’`Assistant` su quel `Thread`. L’`Assistant` poi elabora i messaggi, decide se ha bisogno di chiamare uno strumento e genera una risposta. Abbastanza semplice, giusto?

Qui le cose si fanno interessanti. Il mio assistente finanziario ha bisogno di fare cose come:

  • Monitorare obiettivi: “Voglio risparmiare $50.000 per un acconto.”
  • Raccogliere informazioni: “Qual è il tuo attuale reddito mensile?”
  • Eseguire calcoli: “In base a ciò, quanto tempo ci vorrà se risparmio $1.000 al mese?”
  • Fornire consigli: “Considera di diversificare il tuo portafoglio con fondi indicizzati a basso costo.”

Ognuno di questi richiede spesso una chiamata a uno strumento personalizzato. Per esempio, `calculate_savings_timeline(goal_amount, monthly_income, monthly_savings)`. La sfida non è solo definire lo strumento; è far sì che l’Assistant:

  1. Riconosca di aver bisogno dello strumento.
  2. Identifichi tutti i parametri necessari (anche se sono sparsi tra più messaggi dell’utente).
  3. Chieda all’utente i parametri mancanti con naturalezza.
  4. Esegua lo strumento e integri i risultati.

Il Problema dei Parametri: Quando l’Assistant Chiede di Più

Immaginate che un utente dica, “Quanto tempo ci vorrà per risparmiare per una casa?” Il mio strumento `calculate_savings_timeline` ha bisogno di `goal_amount`, `monthly_income`, e `monthly_savings`. L’Assistant, essendo intelligente, spesso si rende conto che mancano informazioni. L’SDK, attraverso l’oggetto `run`, ti dirà che è `requires_action`.

Il mio approccio iniziale era aspettare lo stato `requires_action` e poi presentare all’utente una generica “Quali informazioni ti servono?”. Era goffo. L’utente spesso non sapeva quali parametri fossero necessari allo strumento. Un approccio migliore, su cui ho eventualmente scommesso, ha coinvolto l’ispezione delle `tool_calls` all’interno dello stato `requires_action`.


def submit_tool_outputs(client, thread_id, run_id, tool_outputs):
 return client.beta.threads.runs.submit_tool_outputs(
 thread_id=thread_id,
 run_id=run_id,
 tool_outputs=tool_outputs
 )

# ... all'interno del tuo ciclo di conversazione ...

if run.status == 'requires_action':
 tool_outputs = []
 missing_params = {}
 for tool_call in run.required_action.submit_tool_outputs.tool_calls:
 function_name = tool_call.function.name
 arguments = json.loads(tool_call.function.arguments)

 # Ecco il trucco: controlla se un parametro necessario è mancante
 # Questo assume che i tuoi strumenti siano definiti con uno schema
 # Per semplicità, diciamo che sappiamo che 'monthly_savings' è sempre necessario
 if function_name == "calculate_savings_timeline" and "monthly_savings" not in arguments:
 missing_params['monthly_savings'] = 'Quanto pensi di risparmiare ogni mese?'
 
 # ... ulteriori controlli sui parametri qui ...

 if missing_params:
 # Informare l'utente su cosa manca
 user_prompt = "Ho bisogno di un po' più di informazioni per aiutarti con questo. "
 for param, question in missing_params.items():
 user_prompt += f"{question} "
 return {"status": "waiting_for_user_input", "prompt": user_prompt}
 else:
 # Tutti i parametri sono presenti, eseguire lo strumento
 output = execute_tool_function(function_name, arguments) # La tua funzione per eseguire la logica reale dello strumento
 tool_outputs.append({
 "tool_call_id": tool_call.id,
 "output": json.dumps(output)
 })
 
 if tool_outputs:
 run = submit_tool_outputs(client, thread.id, run.id, tool_outputs)
 # Continua a monitorare l'esecuzione fino a quando non sia completata o richieda una nuova azione
 else:
 # Questo caso gestisce se abbiamo rilevato parametri mancanti e abbiamo chiesto all'utente
 pass

Questo snippet è una versione semplificata, ma l’idea principale è di intercettare `requires_action`, vedere cosa vuole fare l’Assistant e, se mancano argomenti, chiedere all’utente specificamente per essi. Questo rende la conversazione molto più naturale, come un umano che fa domande di chiarimento, invece di un generico “errore.”

Il Fuggente Stato: Tieni Traccia Attraverso i Turni

Uno dei maggiori mal di testa era assicurarsi che se un utente fornisce un’informazione (“Il mio reddito è di $5.000 al mese”), l’Assistant la ricordasse per le chiamate agli strumenti successive all’interno della stessa conversazione, anche se non era utilizzata immediatamente. L’Assistants API gestisce bene questo all’interno di un `Thread` mantenendo la cronologia dei messaggi. Tuttavia, a volte vuoi guidarlo esplicitamente o fornirgli uno sfondo che non fa parte di un messaggio diretto dell’utente.

Mi sono ritrovata a usare `metadata` sugli oggetti `Thread` e `Message` più di quanto pensassi inizialmente. Per esempio, se l’utente dichiara esplicitamente la propria tolleranza al rischio, potrei memorizzarlo nei metadati del thread. Sebbene l’Assistant non legga direttamente questi metadati nel suo processo di ragionamento, sono inestimabili per la logica della mia applicazione quando devo precompilare argomenti per le chiamate agli strumenti o prendere decisioni su quali strumenti siano anche pertinenti.

Un altro schema che ho adottato è stato iniettare messaggi “di sistema” nel thread se avevo bisogno di ricordare esplicitamente all’Assistant alcuni fatti derivanti da una chiamata a uno strumento o da un processo interno. Per esempio, dopo che `calculate_savings_timeline` restituisce che ci vorranno 10 anni, potrei aggiungere un messaggio come: `{“role”: “user”, “content”: “Il piano di risparmio dell’utente indica un periodo di 10 anni per raggiungere il suo obiettivo.”}`. Questo non proviene direttamente dall’utente, ma aiuta a rafforzare il contesto per le risposte successive dell’Assistant.

Gestire l’Uscita dello Strumento e le Azioni Successive

Quando uno strumento viene eseguito, ottieni un output. L’Assistant poi prende questo output e genera una risposta. Ma cosa succede se l’output dello strumento stesso provoca un nuovo insieme di domande o una nuova chiamata a uno strumento? Per esempio, lo strumento `calculate_savings_timeline` potrebbe restituire che l’obiettivo è irraggiungibile con i risparmi attuali. L’Assistant dovrebbe quindi idealmente suggerire, “Forse dovremmo considerare modi per aumentare i tuoi risparmi mensili o ridurre l’importo del tuo obiettivo.” Non si tratta solo di emettere testo; si tratta di concatenare passaggi logici.

L’SDK dell’Assistants API gestisce questo splendidamente mantenendo attivo l’oggetto `run`. Dopo aver `submit_tool_outputs`, il `run` spesso torna a `in_progress` e poi potrebbe generare un nuovo `requires_action` (per un’altra chiamata a uno strumento) o `completed`. Il mio ciclo principale per gestire il flusso della conversazione appare più o meno così:


def get_assistant_response(client, thread_id, assistant_id, user_message):
 client.beta.threads.messages.create(
 thread_id=thread_id,
 role="user",
 content=user_message
 )

 run = client.beta.threads.runs.create(
 thread_id=thread_id,
 assistant_id=assistant_id
 )

 while run.status in ['queued', 'in_progress', 'cancelling']:
 time.sleep(1) # Sii un buon cittadino, non sovraccaricare l'API
 run = client.beta.threads.runs.retrieve(thread_id=thread_id, run_id=run.id)

 if run.status == 'completed':
 messages = client.beta.threads.messages.list(thread_id=thread_id, order="desc")
 for msg in messages.data:
 if msg.role == "assistant":
 # Trova l'ultimo messaggio dell'assistente, spesso il primo
 return msg.content[0].text.value # Si presume contenuto di testo
 return "Nessuna risposta dall'assistente."

 elif run.status == 'requires_action':
 tool_outputs = []
 for tool_call in run.required_action.submit_tool_outputs.tool_calls:
 function_name = tool_call.function.name
 arguments = json.loads(tool_call.function.arguments)
 
 # Qui è dove si applica la logica personalizzata per gestire parametri mancanti o eseguire strumenti
 # Per questo esempio semplificato, presumiamo che tutti i parametri siano presenti e eseguiamo
 print(f"L'assistente vuole chiamare {function_name} con args: {arguments}")
 
 output = execute_tool_function(function_name, arguments) # La tua logica di esecuzione dello strumento
 tool_outputs.append({
 "tool_call_id": tool_call.id,
 "output": json.dumps(output)
 })
 
 # Invia i risultati degli strumenti e continua l'esecuzione
 run = client.beta.threads.runs.submit_tool_outputs(
 thread_id=thread_id,
 run_id=run.id,
 tool_outputs=tool_outputs
 )
 # Richiama ricorsivamente questa funzione per elaborare l'esecuzione aggiornata
 # Attento alla profondità della ricorsione in produzione, potrebbe servire un ciclo
 return get_assistant_response(client, thread_id, assistant_id, "") # Passa un messaggio vuoto
 
 elif run.status == 'failed':
 return f"L'esecuzione dell'assistente è fallita: {run.last_error.message}"
 
 else:
 return f"Stato di esecuzione imprevisto: {run.status}"

Questa funzione `get_assistant_response`, sebbene semplificata, mostra il ciclo centrale. La chiave è che `requires_action` non è una strada senza uscita. È un’opportunità per la tua applicazione di intervenire, eseguire lo strumento richiesto e poi restituire i risultati all’Assistente per continuare il suo ragionamento. Questo feedback a ciclo chiuso è ciò che rende possibili conversazioni veramente dinamiche e con stato.

Insegnamenti pratici per il tuo prossimo progetto di Assistente AI

  1. Abbraccia il ciclo di vita di `run.status`: Non limitarti a controllare per `completed`. `requires_action` è tuo amico, non un errore. Costruisci gestori solidi per ciascuno stato rilevante.
  2. Ispeziona `tool_calls` per parametri mancanti: Invece di chiedere istruzioni generiche, approfondisci `run.required_action.submit_tool_outputs.tool_calls` per capire *esattamente* di cosa ha bisogno l’Assistente. Questo eleva la tua esperienza utente.
  3. Uso strategico di messaggi simili a Sistema: Se la logica interna della tua applicazione trae nuovi fatti o ha bisogno di enfatizzare un certo contesto, considera di iniettare messaggi “user” che rappresentano questi fatti. L’Assistente li elaborerà come parte della cronologia del thread.
  4. Metadata per lo stato dell’applicazione: Anche se l’Assistente potrebbe non “leggere” direttamente i metadata di thread o messaggi per il suo ragionamento, è un luogo potente per la tua applicazione per memorizzare e recuperare lo stato rilevante per la conversazione. Pensa alle preferenze dell’utente, alle variabili di sessione attuali, ecc.
  5. Testa i casi limite per l’orchestrazione degli strumenti: Cosa succede se uno strumento fallisce? Cosa se l’utente cambia idea nel mezzo dell’invocazione di uno strumento? Progetta la tua `execute_tool_function` e la gestione degli errori all’interno del ciclo `requires_action` con attenzione.

Costruire agenti AI conversazionali veramente intelligenti non riguarda solo la scelta dell’ultimo modello. Si tratta di maneggiare abilmente gli SDK che forniscono per gestire le complessità dell’interazione umana. L’API degli Assistenti OpenAI, con un po’ di ingegneria pensata attorno alla gestione dello stato e all’orchestrazione degli strumenti, può veramente elevare la tua esperienza conversazionale oltre il semplice scambio di battute. È ancora un viaggio, e sto ancora imparando nuovi trucchi, ma spero che queste intuizioni ti facciano risparmiare alcune notti in bianco e caffè freddi!

Articoli correlati

🕒 Published:

🧰
Written by Jake Chen

Software reviewer and AI tool expert. Independently tests and benchmarks AI products. No sponsored reviews — ever.

Learn more →
Browse Topics: AI & Automation | Comparisons | Dev Tools | Infrastructure | Security & Monitoring
Scroll to Top