Certains appels d’outils doivent attendre un oui
Une recherche de prix ne change rien. Un outil qui enregistre, envoie ou supprime change quelque chose. Si le modèle en demande un par erreur, les dégâts sont réels.
C’est donc votre programme, jamais le modèle, qui décide si un appel risqué s’exécute. Pour un appel risqué, il demande d’abord à une personne.
À la fin de cette leçon
Remarque
Je peux faire attendre l’accord d’une personne à un outil risqué.
Mots clés
| Mot | Ce que ça veut dire |
|---|---|
| Outil de lecture | un outil qui ne fait que regarder des données |
| Outil d’écriture | un outil qui change quelque chose |
| Accord | le oui d’une personne avant l’exécution d’un appel |
| Refus par défaut | refuser sauf s’il y a un oui net |
| Journal | la trace de ce qui a été demandé et exécuté |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Classez chaque outil selon ce qu’il peut changer
Donnez un risque à chaque outil. Un outil de lecture ne fait que regarder : il peut s’exécuter tout de suite. Un outil d’écriture change quelque chose : il lui faut un oui.
Gardez le risque dans votre propre table. Le modèle ne dit jamais si un appel est risqué, et un modèle qui se trompe ne peut pas contourner la table.
Exemple travaillé : trois outils et leur risque
| Outil | Ce qu’il fait | Risque | S’exécute |
|---|---|---|---|
| prix_de | Cherche un prix | lecture | Tout de suite |
| ajouter_note | Ajoute à une liste de notes | écriture | Après un oui |
| supprimer_note | Retire une note | écriture | Après un oui |
| tout autre nom | Absent de la table | inconnu | Jamais |
Les cellules que les étudiants remplissent restent vides dans cet aperçu.
Enregistrer un assistant d’appel du modèle
docker exec -w /home/node/project agent node -e "const poser=async(m,t,c)=>{const r=await fetch((process.env.AI_URL||'http://ai:4000')+'/v1/chat/completions',{method:'POST',headers:{'content-type':'application/json',authorization:'Bearer lab'},body:JSON.stringify({model:'lab',messages:m,tools:t,tool_choice:c,max_tokens:120})});const j=await r.json();if(r.ok)return j.choices[0].message;throw Error(r.status+' '+j.error.message)};require('fs').writeFileSync('ia.cjs','exports.poser='+poser+';');console.log('ia.cjs enregistré : poser(messages, outils) renvoie le message du modèle.')"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.
Demandez, et refusez par défaut
Affichez ce que l’outil va faire et lisez une réponse. Seul o est un oui. Tout le reste, une ligne vide ou l’absence de réponse, est un non.
C’est le refus par défaut : un appel ne s’exécute qu’avec un oui net. Dans l’exécution suivante, les réponses sont fournies d’avance : tous les étudiants voient les mêmes lignes. Tapée à la main, elle vous attendrait.
Filtrer trois appels avec des réponses fixes
printf 'o\nn\n' | docker exec -i -w /home/node/project agent node -e "const rl=require('readline').createInterface({input:process.stdin});const it=rl[Symbol.asyncIterator]();const R={prix_de:'lecture',ajouter_note:'ecriture',supprimer_note:'ecriture'};const N=['lait'];const F={prix_de:a=>40,ajouter_note:a=>N.push(a.texte),supprimer_note:a=>N.splice(a.index,1)};(async()=>{const j=[];for(const c of [{n:'prix_de',a:{article:'poire'}},{n:'ajouter_note',a:{texte:'oeufs'}},{n:'supprimer_note',a:{index:0}}]){let ok=R[c.n]=='lecture';if(ok===false){process.stdout.write('Autoriser '+c.n+' '+JSON.stringify(c.a)+' ? (o/n) ');const l=await it.next();ok=l.value?.trim()=='o';console.log(ok?'o':'n')}if(ok)F[c.n](c.a);j.push(c.n+' : '+(ok?'exécuté':'refusé'))}console.log(j.join(', '));console.log('Notes : '+N.join(', '));rl.close()})()"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.
Laisser un modèle demander, et refuser
printf 'n\n' | docker exec -i -w /home/node/project agent node -e "const{poser}=require('./ia.cjs');const rl=require('readline').createInterface({input:process.stdin});const it=rl[Symbol.asyncIterator]();const T=[{type:'function',function:{name:'ajouter_note',description:'Enregistre une note.',parameters:{type:'object',properties:{texte:{type:'string'}}}}}];(async()=>{let c;try{c=(await poser([{role:'user',content:'Note : acheter du lait'}],T,'required')).tool_calls[0].function}catch(e){console.log('Refus ('+e.message+'). Exemple enregistré :');c={name:'ajouter_note',arguments:JSON.stringify({texte:'acheter du lait'})}}process.stdout.write('Le modèle veut '+c.name+' '+c.arguments+'. Autoriser ? (o/n) ');const l=await it.next();const ok=l.value?.trim()=='o';console.log(ok?'o':'n');console.log(ok?'ajouter_note exécuté.':'Refusé. Rien ne s’est exécuté.');rl.close()})()"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.
Gardez un journal de chaque appel
Notez chaque appel : son nom, si on a demandé à une personne, ce qu’elle a répondu et s’il s’est exécuté. Un journal montre ce qu’un assistant a fait, et ce qu’on l’a empêché de faire.
Notez aussi les refus. Un appel refusé que personne ne voit devient une énigme plus tard.
Une erreur courante
Attention
Beaucoup pensent que le modèle décide si un appel risqué est sûr. Ce n’est pas le cas : il peut se tromper ou être dupé par un texte qu’il lit. Votre programme décide, d’après votre propre table, et demande à une personne avant toute écriture.
Pour aller plus loin : changer les réponses
Dans la première exécution, changez les réponses fournies o et n en n et n. Relancez et lisez la ligne des notes.
Ensuite, changez prix_de en ecriture dans R. Écrivez ce que le programme demande maintenant.