Quale architettura di prompt e gestione delle chiamate usare con l'API di ChatGPT per implementare un assistente conversazionale multicanale?
Se devi creare un assistente che risponda in modo coerente suSlack,WhatsAppe unsito web, la sfida principale non è solo la generazione del testo, macome mantenere il contestofra canali con formati, limiti di token e latenze differenti. In questo articolo scoprirai, passo dopo passo, l’architettura diprompt dinamici, le tecniche digestione delle sessionie i pattern dirate'limiting e cachingper ottimizzare costi e performance dell’API di ChatGPT.
1. Analisi delle differenze tra i canali
Prima di progettare l’architettura, è fondamentale capire le peculiarità di ciascun canale.
1.1 Formato dei messaggi
- Slack: supporta markdown, blocchi interattivi (button, select) e messaggi multi'parte.
- WhatsApp: testo semplice, emoji, media (immagini, audio) ma senza markup avanzato.
- Sito web (chat widget): HTML/CSS, possibilità di rendering di componenti React o Vue.
Queste differenze influenzano ilprompt template: bisogna generare output adatti al canale (es. markdown per Slack, plain text per WhatsApp).
1.2 Limiti di token e costi
L’API di OpenAI conta i token sia in input che in output. WhatsApp ha un limite pratico di 4096 caratteri per messaggio, Slack è più flessibile ma il payload JSON non dovrebbe superare i 16 KB, e il widget web può gestire messaggi più lunghi ma con attenzione alla UI.
Strategia:
- Usareprompt brevie delegare al modello il lavoro di sintesi.
- Troncare o segmentare le risposte quando il conteggio dei token supera il limite del canale.
1.3 Latenza
WhatsApp Business API aggiunge una latenza di rete (circa 300'500 ms). Slack è più veloce (150'200 ms). Il widget web dipende dalla connessione del cliente. Per garantire una UX fluida, è utile implementarecaching localeepre'fetchingdi risposte prevedibili.
2. Strategie di mantenimento del contesto
Il contesto è la chiave per una conversazione coerente. Ecco le tre tecniche più usate.
2.1 Session IDs (stateful)
Ogni utente o conversazione riceve unsession_idunivoco. Il backend conserva unacoda di messaggi(es. ultimi 10 scambi) in un database veloce (Redis). Quando arriva un nuovo messaggio, il prompt è costruito così:
const prompt = `
You are a helpful assistant.
Conversation history:
${history.join('\n')}
User: ${userMessage}
Assistant:`;Vantaggi:
- Facile da implementare.
- Coerenza immediata finché la sessione è attiva.
Svantaggi:
- Scalabilità limitata se la cronologia cresce (costi token).
- Perdita di contesto se l’utente cambia canale e non condividiamo lo stesso
session_id.
2.2 Vector Store + Retrieval'Augmented Generation (RAG)
Salviamo ogni scambio come embedding (es. OpenAItext-embedding-ada-002) in unvector store(Pinecone, Weaviate, o Redis Vector). Quando arriva un nuovo messaggio, recuperiamo iksnippet più rilevanti e li includiamo nel prompt.
// Pseudo'code
const embedding = await openai.embeddings.create({input: userMessage, model: 'text-embedding-ada-002'});
const relevant = await vectorStore.query({vector: embedding, topK: 5});
const prompt = `You are an assistant. Use the following context:
${relevant.map(r => r.text).join('\n')}
User: ${userMessage}
Assistant:`;Benefici:
- Scalabile: la cronologia può essere illimitata.
- Funziona anche se l’utente cambia canale, basta condividere lo stesso
user_id.
Costi aggiuntivi per gli embedding, ma si riducono i token del prompt.
2.3 Hybrid: Session + RAG
Combiniamo i due approcci: manteniamo una piccola cronologia recente (es. 3 messaggi) e integriamo i risultati RAG per il resto. Questo garantisce sia la continuità immediata sia la ricchezza di conoscenza a lungo termine.
3. Progettazione di prompt dinamici e modulari
Un prompt ben strutturato deve adattarsi al canale e al tipo di risposta.
3.1 Template base
{% raw %}
{% if channel == 'slack' %}
You are a helpful assistant. Respond using Slack markdown (bold, code blocks, lists).
{% elif channel == 'whatsapp' %}
You are a helpful assistant. Respond with plain text, limit to 4096 characters.
{% else %}
You are a helpful assistant. Respond in HTML suitable for a web chat widget.
{% endif %}
Context:
{{retrieved_context}}
Conversation:
{{history}}
User: {{user_message}}
Assistant:
{% endraw %}Usando un motore di template (Jinja2, Mustache o Handlebars) è possibile iniettare variabili dinamiche.
3.2 Moduli riutilizzabili
- Modulo “saluto”: frase di benvenuto personalizzata per Slack vs WhatsApp.
- Modulo “richiesta azione”: genera pulsanti Slack o quick'reply WhatsApp.
- Modulo “fallback”: messaggio di errore generico con link al supporto.
Esempio in Node.js:
const templates = {
greeting: {
slack: '*Ciao* :wave:, come posso aiutarti?',
whatsapp: 'Ciao! Come posso aiutarti?',
web: '<strong>Ciao</strong>! Come posso aiutarti?'
},
// ... altri moduli
};
function buildPrompt({channel, history, userMessage, context}) {
const base = templates.greeting[channel];
return `${base}\n\nContext:\n${context}\n\nConversation:\n${history.join('\n')}\nUser: ${userMessage}\nAssistant:`;
}4. Pattern di rate'limiting, retries e caching
L’API di OpenAI ha limiti di richieste al minuto (RPM) e token al minuto (TPM). Per un assistente multicanale è cruciale non superare questi limiti.
4.1 Rate'limiting centralizzato
Implementa untoken bucketcondiviso fra tutti i canali. Ogni chiamata consuma 1 token; se il bucket è vuoto, la richiesta viene accodata o restituita con messaggio di "tempo di attesa".
// pseudocode con redis
const bucketKey = 'api:token_bucket';
const capacity = 1000; // RPM consentito
const refillRate = 1000 / 60; // token al secondo
async function acquireToken() {
const tokens = await redis.get(bucketKey);
if (tokens > 0) {
await redis.decr(bucketKey);
return true;
}
return false;
}4.2 Retry con backoff esponenziale
In caso di 429 (Too Many Requests) o errori di rete, riprova con backoff:
let attempt = 0;
while (attempt setTimeout(r, wait));
attempt++;
} else { throw e; }
}
}4.3 Caching delle risposte frequenti
Alcune domande (es. orari di apertura, FAQ) hanno risposte statiche. Usa una cache in'memory (NodeCache) o Redis con TTL 1 h.
const cacheKey = `faq:${userMessage.toLowerCase()}`;
let answer = await redis.get(cacheKey);
if (!answer) {
answer = await callChatGPT(payload);
await redis.set(cacheKey, answer, 'EX', 3600);
}5. Flusso completo dell’assistente multicanale
- Ricezione messaggioda Slack, WhatsApp o Webhook del sito.
- Normalizza il payload (testo, media, user_id, channel).
- Recupera o crea
session_idassociato auser_id. - Recupera contesto recente (Redis) e/o embedding rilevanti (vector store).
- Costruisci il prompt usando il template dinamico.
- Esegui
rate'limit check'callChatGPTcon retry. - Salva la risposta in cache se è idempotente.
- Invia la risposta nel formato richiesto dal canale.
- Aggiorna la cronologia della sessione.
Diagramma semplificato (ASCII):
+-----------+ 1 +-----------+ 2 +----------------+ 3 +-----------------+
| Client |--->| Router |--->| Normalizer |--->| Session Manager |
+-----------+ +-----------+ +----------------+ +-----------------+
^ | | |
| v v v
+-----------+ 4 +-----------+ 5 +----------------+ 6 +-----------+
| Cache / |6. Esempio pratico: Node.js + Express + Redis + Pinecone
Di seguito trovi un mini'progetto funzionante.
// server.js
const express = require('express');
const {Configuration, OpenAIApi} = require('openai');
const redis = require('redis').createClient();
const {PineconeClient} = require('@pinecone-database/pinecone');
const app = express();
app.use(express.json());
const openai = new OpenAIApi(new Configuration({apiKey: process.env.OPENAI_API_KEY}));
const pinecone = new PineconeClient();
await pinecone.init({apiKey: process.env.PINECONE_API_KEY, environment: 'us-east1-gcp'});
const index = pinecone.Index('chat-assistant');
// Middleware per rate'limit (token bucket)
app.use(async (req, res, next) => {
const allowed = await acquireToken(); // funzione del punto 4.1
if (!allowed) return res.status(429).json({error: 'Rate limit exceeded'});
next();
});
app.post('/webhook', async (req, res) => {
const {channel, user_id, text} = req.body; // formato normalizzato
const sessionKey = `sess:${user_id}`;
const history = JSON.parse(await redis.get(sessionKey) || '[]');
// 2️⃣ Embedding e RAG
const embed = await openai.createEmbedding({model: 'text-embedding-ada-002', input: text});
const {matches} = await index.query({vector: embed.data[0].embedding, topK: 5, includeMetadata: true});
const retrieved = matches.map(m => m.metadata.text).join('\n');
// 3️⃣ Prompt dinamico
const prompt = buildPrompt({channel, history, userMessage: text, context: retrieved});
// 4️⃣ Chiamata API con retry
let answer;
try {
answer = await callChatGPTWithRetry(prompt);
} catch (e) {
return res.status(500).json({error: 'OpenAI error'});
}
// 5️⃣ Aggiorna sessione
history.push({role: 'user', content: text});
history.push({role: 'assistant', content: answer});
await redis.set(sessionKey, JSON.stringify(history), 'EX', 86400); // 24h TTL
// 6️⃣ Formattazione per canale
const formatted = formatForChannel(answer, channel);
// invia risposta al canale (Slack, WhatsApp, Webhook)
await sendMessage(channel, user_id, formatted);
res.json({status: 'ok'});
});
app.listen(3000, () => console.log('Server listening on :3000'));
// Funzioni di supporto (es. buildPrompt, callChatGPTWithRetry, formatForChannel, sendMessage) sono implementate nei file separati per chiarezza.Questo esempio dimostra come collegare tutti i componenti descritti:sessione Redis,vector store Pinecone,prompt modulareerate'limiting.
7. Takeaway azionabili
- Standardizza l’identificatore utente(es. email o UUID) su tutti i canali per condividere il contesto.
- Usasessione breve + RAGper bilanciare costi token e coerenza.
- Implementatemplate di prompt per canalecon Jinja/Handlebars.
- Applica untoken bucket globaleper rispettare i limiti di RPM/TPM.
- Caching delle risposte statiche riduce drasticamente i costi API.
8. Conclusione
Costruire un assistente conversazionale multicanale con l’API di ChatGPT non è solo questione di chiamare il modello, ma diarchitettare prompt intelligenti,gestire il contestoin modo scalabile eottimizzare costicon rate'limiting e caching. Seguendo le linee guida presentate " differenze tra canali, strategie di sessione/RAG, template modulari e pattern di resilienza " potrai lanciare un bot coerente su Slack, WhatsApp e il tuo sito web, garantendo un’esperienza utente fluida e professionale.
Non dimenticare di monitorare metriche chiave (latency, token usage, error rate) e di iterare sui prompt basandoti sui feedback reali. Con la giusta architettura, il tuo assistente potrà crescere insieme al tuo business, mantenendo sempre il contesto e la qualità della conversazione.
Domande Frequenti
Come mantenere il contesto quando l'utente passa da Slack a WhatsApp?
Usa un identificatore utente comune (es. email) e salva la cronologia in un datastore condiviso (Redis). Recupera la cronologia o gli embedding rilevanti al cambio di canale e includili nel nuovo prompt.
Qual è il modo più economico per ridurre i token usati nei prompt?
Adotta una strategia ibrida: conserva solo gli ultimi 3'4 scambi nel prompt e utilizza Retrieval'Augmented Generation (RAG) per includere informazioni storiche sotto forma di embedding, riducendo così il testo diretto.