Limiter la boucle
- Leçon 13
- 45 min
- Module 4 : Appeler des outils en toute sécurité
Leçon 13 sur 19 · Créer des applications fiables avec les API de LLMLimiter la boucle
1. Commencer par une seule requête au modèle
2. La rédaction de requêtes, une tâche d'ingénierie
3. Sorties structurées et validation
4. Appeler des outils en toute sécurité
5. Conversations et diffusion en continu
6. Contrôles de fiabilité et de coût
Ce que vous saurez faire
Assurez-vous que la boucle d’outils se termine toujours, peu importe ce que fait le modèle. Assurez-vous aussi qu’aucun outil ne peut modifier une commande ou déplacer de l’argent sans approbation humaine.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
run_tool, les vérifications et le message de l’outil, dans Exécuter dans le code de l’application.- Le résultat
Outcomedeanalyse_ticket(valid(valide),rejected(rejeté),failed(échec)), dans Valider le sens.
Ce qu’il vous faut sur votre ordinateur
- Le projet après Exécuter dans le code de l’application :
assistant/tools.pyterminé ettests/test_tools.py.python -m pytestaffiche49 passed, 1 deselected(49 tests réussis, 1 désélectionné). - Si vous avez sauté cette leçon, copiez l’instantané
end-of-m03en suivant Réinitialiser le projet. Faites ensuite les étapes des deux leçons précédentes de ce module. Ou copiezend-of-m04et comparez-le aux étapes de cette leçon.
Un nouveau collègue au téléphone
Section intitulée « Un nouveau collègue au téléphone »Un gérant de magasin demande à un nouveau collègue : « Appelez le fournisseur pour savoir quand les hamacs arrivent. » Le collègue appelle. Personne ne répond. Il rappelle, encore et encore, tout l’après-midi. La question était claire; il manquait une règle d’arrêt.
Un bon gérant ajoute trois règles. Appelez au maximum trois fois. Arrêtez après vingt minutes. Si vous obtenez deux fois la même réponse, arrêtez et dites-le-moi. Et une autre règle, pour un autre type d’appel : n’acceptez jamais de payer quoi que ce soit sans ma signature.
Le problème : le modèle décide quand la boucle se termine
Section intitulée « Le problème : le modèle décide quand la boucle se termine »Dans la dernière leçon, la boucle a fait un tour : le modèle a demandé un outil, reçu le résultat et répondu. Mais rien n’oblige le modèle à répondre. Après chaque résultat d’outil, il peut demander un autre outil. L’application continue tant qu’il en demande.
Aucune boucle enregistrée du cours n’a mal tourné. Sur les 69 billets de soutien, 46 ont nécessité un appel au modèle et 23 en ont nécessité deux. Aucune boucle n’a répété un appel. L’échec ci-dessous est donc construit. TermeAmiraLa développeuse de Larkfield. Elle écrit le code des outils du service d’assistance dans les cours. a écrit à la main deux réponses où le modèle demande deux fois la même commande. Le projet les conserve sous le modèle constructed-example.
call 1 -> get_order({"order_id":"LK-581106"}) ok: shippedcall 2 -> get_order({"order_id":"LK-581106"}) the same call againcall 3 -> ...Un modèle qui se répète une fois se répétera souvent encore, car il voit presque la même conversation chaque fois. Une boucle sans limite continue alors jusqu’à ce qu’un événement extérieur l’interrompe. Chaque appel renvoie toute la conversation : le deuxième appel de T-80008 contenait déjà 2 181 jetons (tokens) d’entrée. Cinquante appels enverraient plus de 100 000 jetons. C’est toute la limite de jetons du TermeDéploiementUn modèle qu’un compte configure chez un fournisseur, avec son propre nom et ses propres limites. Larkfield a deux déploiements sur Azure, le nuage de Microsoft : chat-small et chat-strong.En savoir plus Gérer les limites du fournisseurCréer des applications fiables avec les API de LLM de TermeLarkfieldUne boutique en ligne d’articles pour la maison. Ce n’est pas une vraie entreprise. Tous les cours Nextia utilisent son service d’assistance et ses données comme exemple.En savoir plus L'IA dans les produits de tous les joursBases d'une carrière en IA : rôles, compétences et feuilles de route (x-ratelimit-limit-tokens: 100000 dans les en-têtes de réponse enregistrés). Un seul billet bloqué empêcherait de traiter les 44 autres de la journée.
L’idée : une boucle limitée
Section intitulée « L’idée : une boucle limitée »Une boucle limitée a des limites que le code impose, pour qu’elle se termine toujours. Le modèle choisit ce qu’il demande. Le code choisit combien de temps la conversation peut durer. Le projet utilise quatre limites, une pour chaque façon dont une boucle peut devenir incontrôlable :
| Limite | Valeur dans le projet | Ce qu’elle arrête |
|---|---|---|
| Étapes | au maximum 4 appels au modèle | un modèle qui ne donne jamais de réponse finale |
| Temps | au maximum 60 s pour toute la boucle | des appels lents dont les durées s’accumulent |
| Taille du résultat | au maximum 2 000 caractères par résultat d’outil | un résultat énorme qui remplit le contexte et gonfle la facture |
| Appels répétés | le même outil avec les mêmes arguments deux fois | un modèle bloqué |
Les valeurs viennent des enregistrements. Les vraies boucles ont nécessité au maximum 2 appels au modèle; 4 laisse donc une marge. L’appel médian a pris 1,55 s; 60 s est donc généreux. Le plus long résultat d’outil réel comptait 350 caractères. Quand une limite arrête la boucle, le résultat est failed : aucune réponse n’est enregistrée. Une personne traite le billet, comme pour tout autre échec.
Suivez la boucle dans l’image à partir du haut. Chaque chemin se termine dans une case : une réponse ou un arrêt.
Description textuelle du diagramme
La boucle commence par une requête qui contient l’outil get_order.
- Avant chaque appel au modèle : 4 appels au modèle ont-ils déjà été faits? Si oui, arrêter : max_steps.
- Appeler le modèle. La réponse contient-elle des appels d’outils? Sinon, c’est la réponse finale : elle passe aux deux barrières.
- Si oui, pour chaque appel : est-ce le même outil avec les mêmes arguments qu’un appel précédent? Si oui, arrêter : repeated_call.
- Sinon : run_tool fait les vérifications, puis la lecture. Le résultat est coupé à 2 000 caractères.
- Plus de 60 secondes se sont-elles écoulées depuis le début? Si oui, arrêter : time. Sinon, revenir à l’étape 1.
Chaque arrêt mène au même résultat : failed. Aucune réponse n’est enregistrée, et une personne prend le billet.
La boucle dans le code
Section intitulée « La boucle dans le code »Voici la boucle du projet. Lisez-la à côté de l’image : chaque return (retour) correspond à une case.
"""The tool loop: the model proposes calls, the application decides, and the loop always ends."""
import jsonimport timefrom dataclasses import dataclass, fieldfrom typing import Callable
from .orders import OrderBookfrom .providers import Completionfrom .tools import run_tool
@dataclassclass Limits: max_steps: int = 4 # model calls in one loop max_seconds: float = 60.0 # wall-clock time for the whole loop max_result_chars: int = 2000 # a tool result longer than this is cut before the model sees it
@dataclassclass Step: name: str arguments: str outcome: str # "ok", "refused: <code>" or "stopped: repeated call"
@dataclassclass LoopResult: completion: Completion | None # the final answer, or None when the loop was stopped completions: list[Completion] = field(default_factory=list) steps: list[Step] = field(default_factory=list) looked_up: dict[str, dict] = field(default_factory=dict) # orders that the application really returned stopped: str = "" # "", "max_steps", "repeated_call" or "time"
def run_tool_loop(complete: Callable[[dict], Completion], request: dict, customer_id: str, book: OrderBook, limits: Limits = Limits(), clock: Callable[[], float] = time.monotonic) -> LoopResult: result = LoopResult(None) messages = list(request["messages"]) seen = set() start = clock() for _ in range(limits.max_steps): completion = complete({**request, "messages": list(messages)}) # a copy: the list grows below result.completions.append(completion) if not completion.tool_calls: result.completion = completion return result messages.append({"role": "assistant", "content": completion.text, "tool_calls": [ {"id": c.id, "type": "function", "function": {"name": c.name, "arguments": c.arguments}} for c in completion.tool_calls]}) for call in completion.tool_calls: signature = (call.name, call.arguments.replace(" ", "")) if signature in seen: result.steps.append(Step(call.name, call.arguments, "stopped: repeated call")) result.stopped = "repeated_call" return result seen.add(signature) output, outcome = run_tool(call.name, call.arguments, customer_id, book) result.steps.append(Step(call.name, call.arguments, outcome)) if output["ok"]: result.looked_up[output["order"]["order_id"]] = output["order"] text = json.dumps(output, ensure_ascii=False)[:limits.max_result_chars] messages.append({"role": "tool", "tool_call_id": call.id, "content": text}) if clock() - start > limits.max_seconds: result.stopped = "time" return result result.stopped = "max_steps" return resultQuatre détails méritent un deuxième regard :
for _ in range(limits.max_steps)est la limite d’étapes. Une bouclewhile Trueferait confiance au modèle pour s’arrêter.- La signature d’un appel comprend son nom et ses arguments sans espaces. Ainsi,
{"order_id": "LK-581106"}et{"order_id":"LK-581106"}comptent comme le même appel. looked_upconserve seulement les commandes renvoyées parrun_tool. La règle d’affairesstatus_not_from_lookupl’utilise : le modèle ne peut pas affirmer un état que le code n’a pas vu.completeetclocksont des paramètres. Le projet transmet lecompletedu fournisseur et l’horloge réelle. Un test peut transmettre un faux modèle et une fausse horloge. Ainsi, aucun test n’attend 60 secondes.
La vérification des appels répétés détecte un modèle bloqué, pas un modèle qui échoue. Deux identifiants erronés différents ne sont pas une répétition. La limite d’étapes les arrête.
Voir une limite arrêter une boucle
Section intitulée « Voir une limite arrêter une boucle »L’explorateur contient maintenant la boucle construite. Choisissez Repeated call (constructed) (appel répété, construit), puis parcourez-la étape par étape et comparez-la à Own order (sa propre commande).
Explorer
Une boucle qui se termine par une réponse, et une boucle que le code arrête
Prédisez d’abord : Dans la boucle construite, le modèle demande LK-581106 deux fois. À quelle étape la boucle s'arrête-t-elle? Une réponse atteint-elle les deux barrières?
T-80008, client C-81265: “Hello, where is my order LK-581106? The confirmation email said the garden shed shipped on Monday, but I have no delivery date yet.”
Exemple construit. Aucun modèle n’a écrit ces réponses : aucune boucle enregistrée n’a répété un appel, donc celle-ci a été écrite à la main pour tester la limite.
- Appels au modèle
- 2
- Jetons jusqu’ici
- aucun (construit)
- Résultat de l’outil
- ok, stopped: repeated call
- Résultat
- failed
Étape 8 sur 8. Aucune réponse à vérifier : The tool loop was stopped: repeated_call.
1 Application → modèle Appel au modèle 1 : 2 messages (system, user) et la description de get_order
Construit : aucun appel au modèle n’a été fait.
2 Modèle Propose un appel
get_order({"order_id":"LK-581106"})3 Application Vérifie la proposition, puis lit la commande
- réussie Le nom de l’outil est get_order
- réussie Les arguments sont du JSON avec une seule clé, order_id
- réussie order_id est LK- suivi de 6 chiffres
- réussie La commande existe
- réussie La commande appartient au client de ce billet
4 Application → modèle Renvoie le résultat de l’outil
{ "ok": true, "order": { "order_id": "LK-581106", "status": "shipped", "ordered_on": "2026-10-01", "shipped_on": "2026-10-05", "delivered_on": null, "shipping_fee": 0, "total": 899, "currency": "CAD", "items": [ { "product": "garden shed", "quantity": 1, "unit_price": 899 } ] } }5 Application → modèle Appel au modèle 2 : 4 messages (system, user, assistant, tool) et la description de get_order
Construit : aucun appel au modèle n’a été fait.
6 Modèle Propose un appel
get_order({"order_id":"LK-581106"})7 Application Arrête la boucle : appel répété
The same call with the same arguments was already made.
8 Application Aucune réponse pour les deux barrières : failed
The tool loop was stopped: repeated_call.
Description textuelle de l’explorateur
Le même explorateur que dans la leçon Exécuter dans le code de l’application, avec deux boucles : sa propre commande (T-80008, réelle) et appel répété (T-80008, construit).
Appel répété, construit : appel au modèle 1 avec 2 messages; aucun jeton, car aucun modèle n’a été appelé. Le modèle propose get_order avec LK-581106. Les cinq vérifications réussissent et l’application renvoie la commande : shipped (expédiée). Appel au modèle 2 avec 4 messages. Le modèle propose de nouveau get_order avec LK-581106. L’application arrête la boucle : appel répété; le même appel avec les mêmes arguments a déjà été fait. Il n’y a pas de réponse finale. Résultat : failed, « The tool loop was stopped : repeated_call. » (la boucle d’outils a été arrêtée : appel répété).
Sa propre commande, réelle : deux appels au modèle, la commande renvoyée une fois, une réponse finale avec l’état shipped. Les deux barrières sont franchies : valid. 4 224 jetons en entrée, 123 en sortie.
La boucle construite s’arrête à l’étape 7, avant la deuxième consultation. Aucune réponse n’atteint les barrières; rien ne peut donc être enregistré.
Relier la boucle à l’assistant
Section intitulée « Relier la boucle à l’assistant »-
Créez
assistant/loop.pyavec le code ci-dessus. -
Ouvrez
assistant/analyse.py. Ajoutez ces deux importations, en ordre alphabétique avec les autres :from .loop import Limits, LoopResult, Step, run_tool_loopfrom .tools import TOOLS -
Dans
Outcome, ajoutez le champstepsaprèsproblems. Dansto_dict, ajoutez la clétool_stepsaprès la cléproblems:assistant/analyse.py (Outcome) problems: list[Problem] = field(default_factory=list)steps: list[Step] = field(default_factory=list)"tool_steps": [asdict(s) for s in self.steps], -
Remplacez
analyse_ticketpar cette version :assistant/analyse.py (analyse_ticket) def analyse_ticket(ticket: Ticket, provider, model: str, prompt: str = "v2", structured: bool = True,tools: bool = True, book: OrderBook | None = None, limits: Limits = Limits(),reasoning_effort: str = "") -> Outcome:book = book or OrderBook()request = build_request(ticket, model, prompt, response_format() if structured else None,TOOLS if tools else None, reasoning_effort)try:if tools:result = run_tool_loop(provider.complete, request, ticket.customer_id, book, limits)else:completion = provider.complete(request)result = LoopResult(completion, [completion])except ProviderError as error:return Outcome(ticket.ticket_id, "failed", None, error=str(error))if result.stopped:return Outcome(ticket.ticket_id, "failed", None, steps=result.steps, completions=result.completions,error=f"The tool loop was stopped: {result.stopped}.")verdict = validate(result.completion, ticket, book, result.looked_up)return Outcome(ticket.ticket_id, "valid" if verdict.valid else "rejected", verdict.analysis, verdict.problems,result.steps, result.completions) -
Ouvrez
assistant/__main__.py. Ajoutez--no-toolssous la ligne qui ajoute--save, et--toolssous la lignemany = …:one.add_argument("--no-tools", action="store_true", help="do not offer the order lookup")many.add_argument("--tools", action="store_true", help="offer the order lookup (default: off)") -
Dans le même fichier, transmettez le réglage et les options :
assistant/__main__.py (changed lines) options = dict(model=model, prompt=args.prompt, structured=not args.no_schema,reasoning_effort=settings.reasoning_effort)outcome = analyse_ticket(ticket, provider, tools=not args.no_tools, **options)outcome = analyse_ticket(ticket, provider, tools=args.tools, **options)Le premier est dans la branche
analyse, le deuxième dans la boucle deeval. Mettez aussi à jour la chaîne de documentation en haut :analyseutilise maintenant des outils.evaln’en utilise aucun, sauf si vous ajoutez--tools. -
Copiez
tests/test_loop.pyettests/test_analyse.pyde l’instantanéend-of-m04dans votre dossiertests. Procédez comme pour les enregistrements dans Décrire un outil. Ils testent les boucles enregistrées, la répétition construite, la limite d’étapes et la limite de temps. -
Exécutez les tests :
Fenêtre de terminal python -m pytest -
Exécutez le billet de démonstration, puis la boucle construite :
Fenêtre de terminal python -m assistant analyse T-80008python -m assistant analyse T-80008 --model constructed-example
L’étape 8 affiche 59 passed, 1 deselected (59 tests réussis, 1 désélectionné). La dernière ligne de la première commande de l’étape 9 :
valid | chat-small | prompt v2 | 2 call(s) | 4224 tokens in, 123 outLa deuxième commande affiche les étapes de la boucle et s’arrête (réponses construites, donc 0 jeton) :
"tool_steps": [ { "name": "get_order", "arguments": "{\"order_id\":\"LK-581106\"}", "outcome": "ok" }, { "name": "get_order", "arguments": "{\"order_id\":\"LK-581106\"}", "outcome": "stopped: repeated call" } ], "error": "The tool loop was stopped: repeated_call."}failed | constructed-example | prompt v2 | 2 call(s) | 0 tokens in, 0 outEssayez aussi Termechat-strongLe nom que le code de Larkfield donne à un modèle hébergé plus grand : gpt-6.1-sol sur Azure. Il est souvent plus exact que chat-small et coûte environ 14 à 18 fois plus par appel.En savoir plus Les éléments d'une requêteCréer des applications fiables avec les API de LLM : python -m assistant analyse T-80008 --model chat-strong. La véritable erreur HTTP 400 de Décrire un outil se termine maintenant par failed | chat-strong | prompt v2 | 0 call(s) | 0 tokens in, 0 out, plutôt que par un plantage.
Une approbation avant toute écriture
Section intitulée « Une approbation avant toute écriture »Chaque limite ci-dessus protège une lecture. Une lecture qui échoue gaspille du temps et des jetons; vous pouvez la répéter. Une écriture est différente : un remboursement, une commande annulée, un courriel à un client. Arrêter une boucle ne permet pas d’annuler une écriture erronée.
L’assistant de Larkfield n’a aucun outil d’écriture, et c’est voulu. La règle 8 de la politique de TermeGraceLa responsable du service d’assistance de Larkfield. Elle écrit la politique de support et décide ce qu’est une bonne réponse à un client. précise qu’il rédige seulement des brouillons, sans jamais rembourser ni modifier une commande. L’outil d’écriture le plus sûr est celui qui n’existe pas.
Mais les billets demandent des écritures. T-80003 dit : « Veuillez rembourser les 129,00 dollars sur ma carte aujourd’hui. Je préférerais ne pas le retourner, il est lourd. » Supposons que Larkfield veuille un jour que l’assistant prépare des remboursements. La conception doit conserver la signature du gérant dans l’histoire du téléphone : une personne approuve avant tout changement. L’image présente une conception pour cela, pas du code du projet.
Description textuelle du diagramme
Une conception seulement : l’assistant de ce cours n’a aucun outil d’écriture.
- Le modèle propose request_refund avec la commande LK-428830 et le montant 129,00.
- L’assistant fait les mêmes vérifications que pour une lecture : le format, la commande du client, un montant ne dépassant pas le total de la commande.
- Il enregistre une demande en attente avec l’état « waiting » (en attente). Aucun argent ne bouge.
- Un agent humain voit la demande en attente dans l’application de soutien, avec la commande, la règle et la raison du modèle.
- Si l’agent approuve, son action envoie le remboursement au système de paiement, avec l’identifiant de demande comme clé d’idempotence. Le système de paiement l’effectue une seule fois.
- Si l’agent refuse, la demande est fermée, avec une raison.
Trois idées soutiennent cette conception :
- L’outil du modèle crée seulement une proposition.
request_refundécrit une ligne avec l’état « waiting ». Il n’a aucun accès au système de paiement. - Une personne approuve dans l’application, pas dans la conversation. L’agent voit la commande, la règle et la raison, puis appuie sur un bouton. Le modèle ne peut pas y appuyer, et aucun texte de billet ne peut y accéder.
- L’écriture s’exécute une seule fois. L’identifiant de demande est transmis au système de paiement comme clé d’idempotence. Si la même approbation arrive deux fois, le système rembourse une seule fois. Gérer les limites du fournisseur explique pourquoi les requêtes répétées en ont besoin.
Pour T-80003, la politique de Grace donne la réponse : aucun remboursement avant le retour de l’article. Un agent refuserait la demande en quelques secondes. L’étape d’approbation prend du temps à cet agent pour chaque écriture. C’est pourquoi vous la concevez seulement pour les écritures, jamais pour les lectures.
Erreur courante
« Le modèle signalera une demande de remboursement à une personne »
Symptôme : TermeTomásL’ingénieur en IA de Larkfield. Il propose souvent un raccourci rapide, et la leçon montre pourquoi il échoue. soutient qu’une étape d’approbation demande trop de travail : « La requête dit qu’un remboursement hors politique nécessite une personne. Le modèle définit needs_human, et l’agent le voit. » Amira exécute tout le TermeJeu de testUn ensemble fixe d’exemples dont on connaît les bonnes réponses. Chaque version d’un prompt ou d’un modèle passe sur le même ensemble, pour comparer les résultats équitablement.En savoir plus Créer un jeu de test représentatifLes LLM : comment ils fonctionnent et comment en choisir un avec la consultation activée : python -m assistant eval --tools. Le score affiche "needs_human_true_caught": "8/9". Le cas manqué est T-80003, la demande de remboursement. Avec la consultation, Termechat-smallLe nom que le code de Larkfield donne à un petit modèle hébergé peu coûteux : gpt-6-luna sur Azure. Le code utilise ce nom, donc le modèle peut changer sans modifier le code.En savoir plus Ce qu'est une API de LLMCréer des applications fiables avec les API de LLM a défini needs_human à faux. Sans la consultation, le même modèle l’a défini à vrai.
Cause : la réponse était correcte (« nous ne pouvons pas rembourser le parasol sans le recevoir en retour »). Mais l’indicateur repose sur un jugement du modèle, et un petit changement dans la conversation l’a modifié. Ici, le changement était un résultat d’outil supplémentaire. Un jugement qui change avec le contexte ne peut pas être la seule protection avant un transfert d’argent.
Correction : placez l’approbation dans la structure, pas dans le jugement du modèle. Un outil d’écriture crée seulement une demande en attente. Seule l’action d’une personne dans l’application la termine. Un indicateur manqué coûte alors une minute à un agent, pas 129 dollars. Pour les indicateurs à ne jamais manquer, ajoutez une règle d’affaires dans le code, comme au module 3 pour les mots liés à la sécurité.
Ce que cela signifie pour un vrai service d’IA : un modèle peut rédiger, trier et suggérer. Toute action difficile à annuler nécessite un contrôle indépendant de la capacité du modèle à réussir cette fois-ci.
L’évaluation complète avec la consultation activée (enregistrements réels) montre aussi un coût en qualité. L’équipe reste à 67 sur 68, mais l’accord sur needs_human passe de 67 à 66 sur 69. Avec 69 billets, un seul billet ne prouve pas une tendance. Mais T-80003 est le billet le plus important pour Grace; l’équipe conserve donc la conception avec approbation.
Exercice du module
Section intitulée « Exercice du module »Ajoutez un deuxième outil en lecture seule : une consultation des billets précédents du client. Utilisez des données synthétiques : trois ou quatre billets inventés dans un dictionnaire Python. Chacun contient un customer_id, une date, une équipe, un état et un court résumé. Au moins une entrée doit appartenir à un autre client.
- Créez
assistant/ticket_tools.pyavec une descriptionGET_TICKET(les quatre parties), une fonctionget_ticket(ticket_id, customer_id, records)et une fonctionrun_ticket_tool(name, arguments, customer_id, records). - Utilisez les mêmes règles que pour
get_order. Vérifiez le format de l’identifiant : T- et 5 chiffres. Refusez les clés supplémentaires. Refusez le billet d’un autre client avec les mêmes mots que pour un billet absent. - Écrivez
tests/test_ticket_tools.pyavec un test pour un billet du client, un pour celui d’un autre client et un pour des arguments incorrects. - N’ajoutez pas
GET_TICKETàTOOLS. Le simulateur n’a aucun enregistrement pour une requête avec deux outils; testez donc les fonctions directement.
Afficher un exemple de réponse
"""Module practice: a read-only lookup of the customer's earlier tickets.
The records are synthetic: made up for this practice, like every ticket of the course.The rules are the same as for get_order: the model proposes, this code decides."""
import jsonimport re
from .tools import ToolRefused
TICKET_ID = re.compile(r"^T-\d{5}$")
PAST_TICKETS = { # synthetic records "T-50001": {"customer_id": "C-81265", "opened_on": "2026-09-02", "team": "delivery", "status": "closed", "summary": "A garden hose arrived two days late. The customer was told the new date."}, "T-50002": {"customer_id": "C-81265", "opened_on": "2026-09-20", "team": "payment", "status": "closed", "summary": "The customer asked for a copy of the invoice. It was sent."}, "T-50003": {"customer_id": "C-38852", "opened_on": "2026-09-28", "team": "delivery", "status": "open", "summary": "A question about the delivery address of a garden shed."},}
GET_TICKET = { "type": "function", "function": { "name": "get_ticket", "description": ("Look up one of this customer's earlier Larkfield support tickets by its ticket ID. " "Read-only. Returns the date, the team, the status and a short summary. " "Tickets of other customers are not available."), "strict": True, "parameters": { "type": "object", "properties": {"ticket_id": {"type": "string", "description": "The ticket ID, T- and 5 digits."}}, "required": ["ticket_id"], "additionalProperties": False, }, },}
def get_ticket(ticket_id: str, customer_id: str, records: dict = PAST_TICKETS) -> dict: """Return the ticket if it exists and belongs to customer_id; otherwise raise ToolRefused.""" if not isinstance(ticket_id, str) or not TICKET_ID.match(ticket_id): raise ToolRefused("invalid_argument", "The ticket ID must be T- followed by 5 digits.") record = records.get(ticket_id) if record is None: raise ToolRefused("not_found", f"No ticket {ticket_id} is available for this customer.") if record["customer_id"] != customer_id: raise ToolRefused("other_customer", f"No ticket {ticket_id} is available for this customer.") return {"ticket_id": ticket_id, **{k: v for k, v in record.items() if k != "customer_id"}}
def run_ticket_tool(name: str, arguments: str, customer_id: str, records: dict = PAST_TICKETS) -> tuple[dict, str]: """Run one proposed call. Return (the result for the model, the outcome for the log).""" try: if name != "get_ticket": raise ToolRefused("unknown_tool", f"There is no tool named {name}.") try: args = json.loads(arguments) except json.JSONDecodeError: raise ToolRefused("invalid_argument", "The arguments are not valid JSON.") from None if not isinstance(args, dict) or set(args) != {"ticket_id"}: raise ToolRefused("invalid_argument", "get_ticket takes exactly one argument: ticket_id.") return {"ok": True, "ticket": get_ticket(args["ticket_id"], customer_id, records)}, "ok" except ToolRefused as refused: return {"ok": False, "error": str(refused)}, f"refused: {refused.code}"import pytest
from assistant.ticket_tools import run_ticket_tool
def test_a_customer_gets_their_own_earlier_ticket(): result, outcome = run_ticket_tool("get_ticket", '{"ticket_id": "T-50001"}', "C-81265") assert outcome == "ok" and result["ticket"]["status"] == "closed" assert "customer_id" not in result["ticket"]
def test_another_customers_ticket_is_refused_like_a_missing_one(): other, other_outcome = run_ticket_tool("get_ticket", '{"ticket_id": "T-50003"}', "C-81265") missing, missing_outcome = run_ticket_tool("get_ticket", '{"ticket_id": "T-59999"}', "C-81265") assert other_outcome == "refused: other_customer" and missing_outcome == "refused: not_found" assert other["error"].replace("T-50003", "X") == missing["error"].replace("T-59999", "X")
@pytest.mark.parametrize("arguments", ['{"ticket_id": "T-5001"}', '{"ticket_id": "T-50001", "customer_id": "C-38852"}', "not json"])def test_bad_arguments_are_refused_before_the_records(arguments): result, outcome = run_ticket_tool("get_ticket", arguments, "C-81265") assert result["ok"] is False and outcome == "refused: invalid_argument"python -m pytest tests/test_ticket_tools.py affiche 5 passed (5 tests réussis). L’outil réutilise ToolRefused; le journal contient donc les mêmes codes de résultat pour les deux outils. Pour proposer cet outil à un modèle en direct, la boucle devrait aussi appeler run_ticket_tool pour le nom get_ticket.
Vérification du module
Section intitulée « Vérification du module »Montrez deux choses avec un seul test : l’application bloque un identifiant non autorisé, et la boucle arrête un appel répété. Écrivez tests/test_module_check.py. Utilisez un modèle programmé : une fonction qui retourne toujours le même appel d’outil construit. Faites-lui demander la commande LK-615204 du voisin, avec l’identifiant client C-20417 de Tom.
Afficher un exemple de réponse
"""Module check: block an unauthorized identifier and stop a repeated-call loop."""import json
from assistant.loop import run_tool_loopfrom assistant.orders import OrderBookfrom assistant.providers import Completion
def scripted_call(order_id): """A constructed model response that asks for get_order (no model wrote it).""" data = {"choices": [{"finish_reason": "tool_calls", "message": {"content": None, "tool_calls": [ {"id": "call_1", "type": "function", "function": {"name": "get_order", "arguments": json.dumps({"order_id": order_id})}}]}}]} return Completion.from_response(data, 0.0)
def test_the_neighbours_order_is_blocked_and_the_repeat_stops_the_loop(): def model(request): return scripted_call("LK-615204") # asks for the neighbour's order every time
result = run_tool_loop(model, {"messages": []}, "C-20417", OrderBook()) assert [s.outcome for s in result.steps] == ["refused: other_customer", "stopped: repeated call"] assert result.stopped == "repeated_call" and result.completion is None assert result.looked_up == {}python -m pytest tests/test_module_check.py affiche 1 passed (1 test réussi). Le premier appel est refusé, donc looked_up reste vide : la commande du voisin n’est jamais entrée dans la conversation. Le deuxième appel est identique, donc la boucle s’arrête avant de l’exécuter.
Vous avez terminé le module quand :
python -m pytestréussit, avec vos nouveaux tests.- Vous pouvez expliquer, pour T-80001, quelle vérification a refusé l’appel. Vous pouvez aussi expliquer pourquoi le modèle a reçu les mêmes mots que pour une commande introuvable.
- Vous pouvez expliquer quelle limite arrête un modèle qui demande quatre identifiants erronés différents. Vous pouvez aussi expliquer pourquoi la vérification des appels répétés ne l’arrête pas.
Réinitialisation. Si votre projet ne fonctionne plus, renommez-le et copiez l’instantané end-of-m04 en suivant Réinitialiser le projet :
mv ticket-assistant ticket-assistant-oldcp -R nextia-learning-labs/C08/snapshots/end-of-m04 ticket-assistantSous Windows, dans PowerShell : Rename-Item ticket-assistant ticket-assistant-old, puis Copy-Item -Recurse nextia-learning-labs\C08\snapshots\end-of-m04 ticket-assistant. Recréez l’TermeEnvironnement virtuelUn dossier avec son propre Python et ses propres paquets, pour un seul projet. Les paquets installés là ne changent pas les autres projets.En savoir plus Isoler les dépendancesPython pour l'ingénierie de l'IA en pratique, comme l’indique le README des instantanés. python -m pytest affiche alors 59 passed, 1 deselected (59 tests réussis, 1 test désélectionné). L’instantané contient les étapes guidées de ce module, mais pas l’exercice du module. Ces étapes ont été testées sur macOS; les commandes Windows n’ont pas été testées.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Un modèle propose get_order avec LK-000001, puis LK-000002, puis LK-000003, puis LK-000004. Quelle limite arrête la boucle, et après combien d'appels au modèle?
Afficher la réponse
La limite d’étapes. Les quatre appels ont des arguments différents, donc la vérification des appels répétés ne trouve jamais de correspondance. Chaque appel est refusé (not_found), et la boucle continue. Après le quatrième appel au modèle et son refus, range(4) est épuisé. La boucle se termine avec stopped: max_steps (arrêt : nombre maximal d’étapes). Le résultat est failed (échec), et une personne prend en charge le billet de soutien. Le test test_the_step_limit_stops_a_model_that_never_answers vérifie ce cas avec une limite de 3.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Pourquoi un outil en lecture seule peut-il s'exécuter sans l'approbation d'une personne, contrairement à un outil de remboursement?
Afficher la réponse
Une lecture incorrecte coûte du temps et des jetons. Les vérifications garantissent qu’elle lit seulement ce que le client peut voir. Vous pouvez la répéter ou l’ignorer. Une écriture incorrecte, comme un remboursement, modifie des montants ou des données et est difficile ou impossible à annuler. Un outil d’écriture crée donc seulement une demande en attente. Une personne l’approuve dans l’application, à un endroit inaccessible aux sorties du modèle et au texte des billets.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédisez : vous définissez Limits(max_result_chars=50) et exécutez T-80008 avec le simulateur. Qu'arrive-t-il au message de l'outil, et pourquoi le simulateur peut-il ensuite échouer?
Afficher la réponse
Le résultat de l’outil pour LK-581106 dépasse 50 caractères. La boucle le coupe donc après 50 caractères, au milieu du JSON. Le modèle verrait un résultat invalide. Avec le simulateur, la deuxième requête diffère maintenant de celle enregistrée, car le message de l’outil fait partie de la requête. Le simulateur répond donc No recording for this request (aucun enregistrement pour cette requête), et le résultat est failed. Les résultats réels comptaient au plus 350 caractères. C’est pourquoi le projet utilise 2 000.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Le modèle décide s’il demande un autre outil, donc le code doit décider quand la boucle se termine. Une boucle limitée possède des limites que le code fait respecter.
- Les limites du projet : au plus 4 appels au modèle, 60 s, 2 000 caractères par résultat d’outil, et aucun appel identique répété. Un arrêt donne le résultat
failed: rien n’est enregistré, et une personne prend en charge le billet. - Aucune boucle enregistrée n’a répété un appel ni nécessité plus de 2 appels au modèle. La boucle à appels répétés de cette leçon est construite, et les tests l’utilisent.
analyseutilise maintenant la recherche, et--no-toolsla désactive.eval --toolsévalue les 69 billets avec cette recherche : équipe 67 sur 68,needs_human(intervention humaine requise) 66 sur 69. Il a manqué T-80003, la demande de remboursement.- L’assistant n’a aucun outil d’écriture. Une écriture nécessiterait une étape d’approbation. L’outil du modèle crée une demande en attente, une personne l’approuve dans l’application. L’écriture s’exécute ensuite une seule fois avec une clé d’idempotence.
Vous avez terminé le module 4. Ensuite, l’assistant maintient une conversation avec un agent, dans les limites d’un budget de jetons.
Leçon suivante : Gérer l'état de la conversation