Quand l’entrée n’est pas du texte
Une entrée multimodale est une requête qui contient des images ou des documents en plus des mots. Le modèle les lit, mais votre service transporte désormais de gros fichiers, vérifie leur type et paie à l’image ou à la page. C’est important, car une requête de quelques centaines d’octets peut peser plusieurs mégaoctets.
Un utilisateur photographie un ticket de caisse et envoie un contrat de dix pages. Une seule requête a maintenant trois parties et trois façons d’être refusée.
À la fin de cette leçon
Remarque
Je peux dire ce qui change quand l’entrée est une image ou un document.
Mots-clés
| Mot | Ce que ça veut dire | Exemple |
|---|---|---|
| Partie | un morceau de la requête : texte, image ou document | |
| Type de média | l’étiquette du format d’un fichier | image/png |
| Taille de la charge | les octets qu’une requête transporte | |
| Limite de pages | le plus grand nombre de pages d’un document | |
| Prix unitaire | le coût d’une image ou d’une page |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Le labo ne prend que du texte
Attention
Le point d’accès au modèle du labo accepte du texte. Un Run ne peut pas y envoyer une image ni un document. Cette leçon part d’une forme de requête enregistrée, inventée pour la leçon, et la vérifie avec du code.
Une requête devient une liste de parties
Une entrée texte est une seule chaîne. Une entrée multimodale est une liste de parties, chacune avec un type et ses données. Les données d’un fichier voyagent en octets, souvent encodés en texte, ce qui les alourdit d’un tiers environ.
Voici une forme enregistrée, inventée pour cette leçon. Chaque vrai fournisseur a ses propres noms de champs : lisez toujours sa documentation.
Une forme de requête enregistrée
{ "messages": [{ "role": "user", "content": [
{ "type": "text", "text": "Que montre ce ticket de caisse ?" },
{ "type": "image", "media": "image/png", "data": "<base64, 412000 octets>" },
{ "type": "document", "media": "application/pdf", "data": "<base64, 880000 octets>" }
] }] }Vérifiez chaque partie avant d’envoyer
Vérifiez chaque partie selon une règle que vous fixez : le type de média, la taille en octets et le nombre de pages. Vérifiez aussi les totaux : combien d’images, combien d’octets en tout.
Refusez tôt, avec une raison claire. Une requête qui échoue chez le fournisseur vous a déjà coûté l’envoi, et peut-être de l’argent.
Exemple travaillé : limites et prix
| Partie | Ce que c’est | Vérification | Coût en unités |
|---|---|---|---|
| 1 | texte, 1800 caractères | aucune limite ici | 2 |
| 2 | image, png, 412000 octets | permise, sous 5 Mo | 4 |
| 3 | image, gif, 90000 octets | gif n’est pas dans la liste | 4 |
| 4 | document, 24 pages | au-dessus de la limite de 20 pages | 48 |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Un coût par image et par page
Le texte se paie au jeton. Une image ou une page se paie souvent un montant fixe, ou un nombre de jetons selon sa taille. Additionnez les parties avant l’appel, pour qu’une règle de budget puisse refuser une requête coûteuse.
Quand c’est possible, envoyez moins : réduisez la taille de l’image ou n’envoyez que les pages utiles.
Une erreur fréquente
Attention
Beaucoup font confiance au nom ou à l’extension du fichier. Un nom peut dire png sur un fichier qui n’en est pas un. Vérifiez le type d’après les octets, côté serveur, et gardez aussi les limites de taille. L’extension est un indice, pas un fait.
Écrire l’aide du modèle, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function demander(messages, echantillon, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"statut \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire l’aide du modèle, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (erreur) {' ' console.log(\"Échantillon enregistré : aucun modèle n’a répondu (\" + erreur.message + \").\");' ' const liste = [].concat(echantillon);' ' const tour = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return liste[Math.min(tour, liste.length - 1)];' ' }' '}' 'module.exports = { demander };' >> model.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le vérificateur, partie 1
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { demander } = require(\"./model\");' '// Une requête enregistrée, inventée : chaque fournisseur a sa propre forme.' 'const requete = { parties: [' ' { type: \"texte\", nbCar: 1800 },' ' { type: \"image\", genre: \"png\", octets: 412000 },' ' { type: \"image\", genre: \"gif\", octets: 90000 },' ' { type: \"document\", pages: 24, octets: 880000 },' '] };' 'const LIMITES = { images: 3, octetsImage: 5000000, pages: 20, genres: [\"png\", \"jpeg\"] };' 'const PRIX = { image: 4, page: 2, milliersCar: 1 }; // unités inventées' > shapes.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le vérificateur, partie 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function verifier(req) {' ' const problemes = [];' ' let images = 0;' ' let cout = 0;' ' req.parties.forEach(function (p, i) {' ' const n = i + 1;' ' if (p.type === \"texte\") cout += Math.ceil(p.nbCar / 1000) * PRIX.milliersCar;' ' if (p.type === \"image\") {' ' images++;' ' cout += PRIX.image;' ' if (LIMITES.genres.indexOf(p.genre) < 0) problemes.push(\"partie \" + n + \" : genre \" + p.genre + \" refusé\");' ' if (p.octets > LIMITES.octetsImage) problemes.push(\"partie \" + n + \" : image trop lourde\");' ' }' >> shapes.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le vérificateur, partie 3
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' if (p.type === \"document\") {' ' cout += p.pages * PRIX.page;' ' if (p.pages > LIMITES.pages) problemes.push(\"partie \" + n + \" : trop de pages\");' ' }' ' });' ' if (images > LIMITES.images) problemes.push(\"trop d’images\");' ' return { problemes: problemes, cout: cout };' '}' >> shapes.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Écrire le vérificateur, partie 4
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const resultat = verifier(requete);' 'resultat.problemes.forEach(function (p) { console.log(\"Problème :\", p); });' 'console.log(\"Coût :\", resultat.cout, \"unités (un prix inventé)\");' 'async function main() {' ' const texte = \"Texte du ticket : pain 2,10, lait 1,05, total 3,15. Quel est le total ?\";' ' const echantillon = \"Le total est de 3,15.\";' ' console.log(\"Texte seul, appel réel :\", await demander([{ role: \"user\", content: texte }], echantillon, 30));' '}' 'main();' >> shapes.js"Affichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Vérifier la requête enregistrée
docker exec -w /home/node/project agent node shapes.jsAffichée ici, non exécutée. Créez un compte gratuit pour appuyer sur Exécuter et la saisir dans le terminal de votre propre labo.
Une sortie possible
| Ligne | Ce qu’elle montre |
|---|---|
| Problème : partie 3 : genre gif refusé | Le type de média n’est pas dans la liste |
| Problème : partie 4 : trop de pages | 24 pages dépassent la limite de 20 |
| Coût : 58 unités | Texte 2, deux images 8 et un document 48 |
| Texte seul, appel réel | Une courte réponse à une question en texte, ou un échantillon enregistré étiqueté |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.