Aller au contenu
English

Contrôler la boucle d'exécution

  • Leçon 6
  • 50 min
  • Module 2 : Représenter l'état et les outils

Ce que vous saurez faire

Assurez-vous que chaque exécution de l’agent se termine pour une raison précise, quoi que fasse le modèle. Prouvez-le avec des tests. Incluez un modèle qui appelle des outils inexistants et un modèle qui échoue sans cesse.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

  • La limite d’étapes, la limite de temps et la vérification des appels répétés de l’assistant de billets de soutien, dans Limiter la boucle. Cette leçon intègre les mêmes idées à la boucle de l’agent et en ajoute deux.
  • L’état de la tâche et son stop_reason (raison d’arrêt), dans Un état de tâche explicite. Les résultats d’erreur des outils, dans Contrats d’outils.

Ce qu’il vous faut sur votre ordinateur

Aucun compte ni aucune clé : chaque décision du modèle est un enregistrement. Pour cette leçon, gemma3:4b dans Ollama a traité cinq billets sans règles d’arrêt, sur un ordinateur avec 16 Go de mémoire. Tests effectués avec Python 3.12.13 sur macOS. Les commandes Windows n’ont pas été testées.

Un client paie par carte dans un magasin. Le lecteur affiche « declined » (refusée). La caissière réessaie, car les lecteurs échouent parfois une première fois. Cette fois, ça fonctionne. Bien.

La carte d’un autre client est refusée trois fois de suite. La caissière s’arrête et demande un autre moyen de paiement. Elle ne passe pas la carte tout l’après-midi. Parfois, le paiement passe et le client lui redonne la carte pour le même achat. Elle ne facture pas deux fois : quelque chose ne va pas, et elle s’arrête. Si la file derrière le client devient trop longue, la responsable intervient.

Trois règles : réessayer après un échec, s’arrêter après trois échecs de suite, ne jamais répéter ce qui a déjà fonctionné. Plus une limite de temps. La caissière n’a pas besoin de comprendre pourquoi la carte échoue. Elle a besoin de règles.

Dans la boucle de l’agent, le modèle choisit la prochaine étape : une lecture ou finish (terminer). Rien ne l’oblige à terminer. À ce stade, la boucle a une seule limite : 8 appels au modèle. Elle s’arrête ensuite avec max_steps (limite d’étapes). Est-ce suffisant?

Pour le savoir, cinq exécutions de gemma3:4b ont été enregistrées pour cette leçon, sans aucune autre règle, jusqu’à 12 appels au modèle (reference/c10/m02/record_unbounded.py). Ce sont de vraies exécutions de l’agent du projet, pas des scénarios inventés :

  • T-80001 (la commande du voisin) : après une recherche refusée, le modèle a appelé get_customer_orders 11 fois de suite. Chaque fois, il a reçu la même liste vide.
  • T-80009 (un identifiant de commande à 5 chiffres) : il a alterné six fois entre deux appels. D’abord get_order avec LK-55190 (refusé par le contrat), puis search_policy avec « invalid order id » (identifiant de commande invalide).
  • T-90608 (la liste des commandes est indisponible, par simulation) : il a appelé get_customer_orders 11 fois. Le service a dépassé le délai 11 fois.
  • T-64872 (facturé deux fois) : il a proposé la même résolution invalide six fois. Il a ensuite donné quatre réponses coupées à la limite de jetons. Cette exécution a attendu le modèle pendant 111,9 secondes.
  • T-80005 (un hamac en retard, en français) : ses trois premières résolutions ont été refusées. La quatrième a été acceptée, à l’appel 5.

Quatre des cinq exécutions n’ont jamais terminé. Une limite de 8 étapes les arrêterait, mais seulement après 8 appels. Chaque appel renvoie toute la conversation et peut prendre plusieurs secondes. La limite ne dit rien sur la raison du problème. Personne n’en tire donc de leçon.

L’idée : une boucle qui termine toujours, pour une raison précise

Section intitulée « L’idée : une boucle qui termine toujours, pour une raison précise »

La terminaison signifie que la boucle se termine sur tous les chemins, quelle que soit la réponse du modèle. La boucle du projet se termine pour exactement une des raisons suivantes, inscrite dans state.stop_reason : finished (terminé), max_steps, time (temps), cost (coût), repeated_call (appel répété), too_many_errors (trop d’erreurs), no_recording (aucun enregistrement) ou provider_error (erreur du fournisseur).

Il existe deux types de limites. Les limites de budget sont vérifiées avant chaque appel au modèle. Elles arrêtent toute exécution trop longue :

  • La limite d’étapes : au plus 8 appels au modèle.
  • Le budget de temps : au plus 120 secondes à attendre le modèle. Lors d’une relecture, la boucle additionne les latences enregistrées. La relecture s’arrête donc là où l’exécution réelle s’arrêterait.
  • Le plafond de dépenses : au plus 0,05 $ US par tâche, selon le nombre de jetons et les prix datés.

Les règles de comportement sont vérifiées après chaque appel au modèle. Elles arrêtent rapidement une exécution bloquée :

  • Détection des appels répétés : la même lecture, avec les mêmes arguments, après sa réussite. La réponse ne peut pas changer; le modèle est donc bloqué.
  • Erreurs consécutives : trois échecs de suite arrêtent la boucle. Un échec est une lecture refusée ou échouée, une résolution refusée ou une réponse sans étape utilisable. Une réussite remet le compteur à 0.

Le schéma place les vérifications dans l’ordre :

Un flux. Avant chaque appel au modèle : 8 étapes ou plus, temps du modèle supérieur à 120 s ou coût supérieur à 0,05 $ US? Oui : arrêter avec max_steps, time ou cost. Non : appeler le modèle. Erreur du fournisseur ou aucune réponse enregistrée : arrêter avec provider_error ou no_recording. Sinon : y a-t-il une étape utilisable? Non : ajouter 1 aux erreurs consécutives. Terminer : la résolution est-elle valide, avec toutes les commandes consultées? Oui : arrêter, finished. Non : ajouter 1 aux erreurs consécutives. Lecture : le même appel a-t-il déjà réussi? Oui : arrêter, repeated_call. Non : run_read avec le contrat, la permission et le service. Réussite : remettre les erreurs consécutives à 0, revenir au début. Erreur : ajouter 1 aux erreurs consécutives. Après cet ajout : 3 erreurs consécutives? Oui : arrêter, too_many_errors. Non : revenir au début.
Les vérifications de run_loop dans resolver/loop.py, dans l'ordre. Chaque chemin mène à un arrêt nommé ou revient à la première vérification, qui compte les étapes.
Description textuelle du diagramme

Avant chaque appel au modèle, la boucle vérifie le budget : 8 étapes, 120 secondes de temps du modèle, 0,05 $ US. Si un budget est épuisé, elle s’arrête avec max_steps, time ou cost.

Elle appelle ensuite le modèle. Une erreur du fournisseur l’arrête avec provider_error; un enregistrement manquant l’arrête avec no_recording.

Si la réponse n’a aucune étape utilisable, le compteur d’erreurs augmente de 1. Si la réponse est finish, la boucle vérifie la résolution : valide, et toutes les commandes consultées. Si oui, elle s’arrête avec finished; sinon, le compteur d’erreurs augmente de 1. Si la réponse est une lecture ayant déjà réussi avec les mêmes arguments, elle s’arrête avec repeated_call. Sinon, run_read l’exécute : une réussite remet le compteur à 0, une erreur ajoute 1.

Après une erreur, s’il y en a 3 de suite, la boucle s’arrête avec too_many_errors. Sinon, elle revient à la vérification du budget.

Les valeurs viennent des enregistrements. Le nombre maximal d’appels au modèle pour une exécution réelle sur Azure était de 4 pour chat-small et de 5 pour chat-strong. La limite de 8 laisse donc une marge. La tâche réelle la plus lente a pris 72.857 secondes de temps du modèle (gemma3:4b). La limite de 120 secondes n’arrête donc qu’une exécution très loin de la normale. La tâche chat-strong la plus chère a coûté environ 0,033 $ US. Le plafond de 0,05 $ US par tâche n’arrête donc qu’une exécution qui dépasse largement la normale. La règle des trois erreurs correspond à la requête (prompt), qui dit au modèle : « Si un outil échoue, vous pouvez réessayer une fois. »

Pourquoi seule une répétition réussie arrête la boucle

Section intitulée « Pourquoi seule une répétition réussie arrête la boucle »

La règle des appels répétés pourrait arrêter toute répétition. Elle ne le fait pas, car répéter après un échec est souvent approprié. Pour T-90601, le service des commandes dépasse le délai une fois (par simulation). chat-small a demandé get_order LK-645001 et reçu service_timeout (délai du service dépassé). Il a redemandé avec les mêmes arguments, reçu la commande et terminé avec la bonne réponse. Une règle qui arrêterait chaque répétition confierait ce billet à une personne sans raison.

La règle des erreurs s’occupe des appels qui continuent d’échouer. Pour T-90608, la liste des commandes dépasse le délai chaque fois. Le troisième dépassement consécutif arrête la boucle, à l’appel 3.

Un petit exemple détaillé : T-80009, appel par appel

Section intitulée « Un petit exemple détaillé : T-80009, appel par appel »

Voici les quatre premiers appels de l’exécution sans limites de T-80009, avec les règles du projet appliquées :

Appel Demande du modèle Réponse de la boucle Erreurs consécutives Répétition d’une réussite?
1 get_order LK-55190 invalid_arguments (arguments invalides) 1 non
2 search_policy « invalid order id » réussite 0 non
3 get_order LK-55190 invalid_arguments 1 non : le premier a échoué
4 search_policy « invalid order id » non exécuté 1 oui : arrêt, repeated_call

La règle des erreurs ne se déclenche jamais ici, car chaque recherche réussie remet le compteur à 0. La limite d’étapes ne se déclencherait qu’à l’appel 8. La règle des appels répétés détecte le problème à l’appel 4, après 7,99 secondes de temps du modèle plutôt qu’environ 15.

Chaque exécution dans l’explorateur est l’une des cinq exécutions sans limites de gemma3:4b. L’explorateur applique les règles aux appels enregistrés, dans le même ordre que run_loop. Un script de vérification le compare à la boucle réelle du projet pour les 1 080 combinaisons de paramètres.

Explorer

Où la boucle s'arrête-t-elle?

Raison de l’arrêt
repeated_call
Appels au modèle
3
Temps du modèle
16,2 s

L’appel 3 demandait get_customer_orders avec les mêmes arguments qu’un appel réussi plus tôt. La boucle s’arrête avant son exécution. Aucune proposition : le billet va à une personne. Sans aucune règle, l’exécution enregistrée a continué jusqu’à 12 appels, le maximum permis, sans jamais terminer.

  • Exécuté : une lecture réussie ou une résolution valide
  • Exécuté : une erreur (refus, échec ou réponse inutilisable)
  • Jamais demandé : la boucle s’était arrêtée
T-80001 : la commande du voisin : arrêtée par repeated_call après 3 appels au modèle
AppelDemande du modèleRéponse de la boucleTemps (s)
1get_order("LK-615204")other_customer: No order LK-615204 is available for this customer. (1 de suite)3,3
2get_customer_orders()ok10,0
3get_customer_orders()non exécuté : la boucle s’arrête (appel répété)16,2
4get_customer_orders()jamais demandé-
5get_customer_orders()jamais demandé-
6get_customer_orders()jamais demandé-
7get_customer_orders()jamais demandé-
8get_customer_orders()jamais demandé-
9get_customer_orders()jamais demandé-
10get_customer_orders()jamais demandé-
11get_customer_orders()jamais demandé-
12get_customer_orders()jamais demandé-
Cinq exécutions réelles de gemma3:4b (Ollama, en local) sur des billets Larkfield, enregistrées sans règles d'arrêt, jusqu'à 12 appels au modèle. L'explorateur applique les règles de resolver/loop.py aux appels enregistrés.
Description textuelle de l’explorateur

L’explorateur affiche une exécution enregistrée à la fois. Il y en a cinq. T-80001 concerne la commande du voisin. T-80009 a un identifiant de commande à 5 chiffres. Dans T-90608, la liste des commandes est indisponible (par simulation). T-64872 concerne une double facturation, et T-80005 un hamac en retard, en français.

La commande principale est la limite d’étapes, de 1 à 12 appels au modèle; le projet utilise 8. Les autres commandes sont la règle des appels répétés (désactivée, même appel réussi, tout appel identique) et celle des erreurs consécutives (désactivée, 3). La dernière est le budget de temps (désactivé, 120 s, 30 s).

Un tableau présente chaque appel au modèle. Il affiche la demande du modèle, la réponse de la boucle, les erreurs consécutives et le temps du modèle cumulé. Une ligne épaisse marque l’arrêt de la boucle.

Avec les limites du projet : T-80001 s’arrête à l’appel 3 (repeated_call, 16,2 s). T-80009 s’arrête à l’appel 4 (repeated_call, 8,0 s). T-90608 s’arrête à l’appel 3 (too_many_errors, 8,4 s). T-64872 s’arrête à l’appel 5 (too_many_errors, 39,1 s). T-80005 s’arrête à l’appel 4 (too_many_errors, 27,9 s), un appel avant que le modèle aurait terminé.

Sans la règle des appels répétés, T-80001 atteint la limite d’étapes : 8 appels et 35,9 s. Sans la règle des erreurs, T-90608 atteint aussi 8 appels. Avec un budget de temps de 30 s, T-64872 s’arrête avant l’appel 5 (time, 30,5 s).

Regardez T-80005 avec les limites du projet. La règle des erreurs l’a arrêté à l’appel 4. Le modèle aurait terminé à l’appel 5 avec reply_only (réponse seulement). C’est le résultat attendu, que l’auteur du cours a rédigé à partir de la politique de Grace. La règle a donc un coût : elle abandonne certaines exécutions qui auraient pu se rétablir. Ici, le compromis est clair : une personne lit un billet, et aucun modèle ne tourne pendant des minutes sur une tâche bloquée.

  1. Copiez resolver/loop.py de l’instantané end-of-m02 par-dessus votre fichier. Les changements sont petits : lisez-les dans la prochaine section.

  2. Ouvrez resolver/config.py. Dans Settings, remplacez la ligne max_steps: int = 8 … par trois lignes :

    resolver/config.py (Settings)
    max_steps: int = 8 # the usage cap, per task
    max_seconds: float = 120.0
    max_cost_usd: float = 0.05
  3. Dans from_env, remplacez la ligne max_steps=int(env.get("RESOLVER_MAX_STEPS", "8"))) par :

    resolver/config.py (from_env)
    max_steps=int(env.get("RESOLVER_MAX_STEPS", "8")),
    max_seconds=float(env.get("RESOLVER_MAX_SECONDS", "120")),
    max_cost_usd=float(env.get("RESOLVER_MAX_COST_USD", "0.05")))
  4. Remplacez limits à la fin de la classe :

    resolver/config.py (limits)
    def limits(self) -> Limits:
    return Limits(max_steps=self.max_steps, max_seconds=self.max_seconds, max_cost_usd=self.max_cost_usd)
  5. Copiez tests/test_loop.py de l’instantané dans votre dossier tests.

  6. Exécutez les tests :

    Fenêtre de terminal
    python -m pytest

    La dernière ligne est 35 passed (35 tests réussis).

Limits gagne trois champs : max_seconds, max_cost_usd et max_errors (3). check_budget s’exécute avant chaque appel au modèle et vérifie maintenant les trois budgets :

resolver/loop.py
def check_budget(state: TaskState, limits: Limits) -> None:
if state.step >= limits.max_steps:
raise Stop("max_steps")
if state.usage.model_seconds > limits.max_seconds:
raise Stop("time")
if state.usage.cost_usd is not None and state.usage.cost_usd > limits.max_cost_usd:
raise Stop("cost")

run_loop conserve deux nouvelles variables locales : calls_seen et errors_in_a_row. La première est un dictionnaire qui associe la signature d’un appel à sa réussite ou à son échec. La signature contient le nom de l’outil et ses arguments compacts. Avant d’exécuter une lecture, la boucle vérifie la signature :

resolver/loop.py (in run_loop, before a read runs)
signature = name + compact(json.loads(args) if isinstance(args, str) and args.strip().startswith("{")
else args)
if calls_seen.get(signature) is True:
state.log("stop", "repeated_call", tool=name, arguments=args)
raise Stop("repeated_call")

Après la lecture, calls_seen[signature] = result.ok, et errors_in_a_row revient à 0 après une réussite ou augmente de 1 après une erreur. Une résolution refusée et une réponse sans étape utilisable ajoutent aussi 1. À la fin de chaque étape, trois erreurs consécutives déclenchent Stop("too_many_errors"). Chaque Stop aboutit au même endroit : la raison entre dans state.stop_reason, et l’exécuteur définit l’état à stopped (arrêté).

  1. Exécutez T-80006 avec gemma3:4b :

    Fenêtre de terminal
    python -m resolver run T-80006 --variant agent_structured --model gemma3:4b
  2. Exécutez T-64872 avec gemma3:4b, avec sa trace :

    Fenêtre de terminal
    python -m resolver run T-64872 --variant agent_structured --model gemma3:4b --trace
  3. Exécutez T-80009 avec une limite de 2 étapes. Sur macOS et Linux :

    Fenêtre de terminal
    RESOLVER_MAX_STEPS=2 python -m resolver run T-80009

    Sur Windows, dans PowerShell :

    Fenêtre de terminal
    $env:RESOLVER_MAX_STEPS = "2"; python -m resolver run T-80009; Remove-Item Env:RESOLVER_MAX_STEPS
  4. Exécutez T-80009 avec un budget de temps de 3 secondes, puis un plafond de dépenses de 0,0005 $ US, de la même façon :

    Fenêtre de terminal
    RESOLVER_MAX_SECONDS=3 python -m resolver run T-80009
    RESOLVER_MAX_COST_USD=0.0005 python -m resolver run T-80009

L’étape 1 se termine avec Stopped: too_many_errors. No proposal: the ticket goes to a person. (Arrêt : trop d’erreurs. Aucune proposition : le billet est confié à une personne.) gemma3:4b a envoyé get_order avec un identifiant null, puis proposé deux résolutions invalides. Avant cette leçon, la même commande se terminait avec no_recording à l’étape 4. La boucle a sollicité le modèle une quatrième fois. Mais l’exécution enregistrée s’était arrêtée après trois appels, donc il n’y a pas de quatrième réponse.

L’étape 2 affiche l’enregistrement du projet pour T-64872, une exécution différente de celle de l’explorateur :

Run T-64872-agent_structured-gemma3:4b: agent_structured, gemma3:4b, ticket T-64872, status stopped
4 model call(s), 9209 tokens in, 334 out, US$0.00000, 13.508 s waiting for the model
1 plan ["Tool: search_policy"]
1 model gemma3:4b: search_policy({"query": "double charge terracotta pot…) (1884 in, 50 out, 2.795 s)
1 tool search_policy({"query": "double charge terracotta pots"}) -> ok
2 plan ["Tool: get_payments"]
2 model gemma3:4b: get_payments({"order_id": "LK-739156"}) (2241 in, 44 out, 2.106 s)
2 tool get_payments({"order_id": "LK-739156"}) -> ok
3 plan ["Tool: finish"]
3 model gemma3:4b: finish({"outcome": "reply_only", "rule": "W7",…) (2409 in, 193 out, 6.589 s)
3 proposal REFUSED: The resolution is not valid: actions.0.create_return_label.sku: Input should be a valid string; act…
4 plan ["Tool: search_policy"]
4 model gemma3:4b: search_policy({"query": "double charge terracotta pot…) (2675 in, 47 out, 2.018 s)
4 stop repeated_call
4 stop repeated_call

La recherche à l’appel 4 est identique à la recherche réussie à l’appel 1. La boucle s’arrête donc avant de l’exécuter. La ligne stop (arrêt) apparaît deux fois dans cette version de la trace. La boucle consigne l’appel répété, puis l’arrêt lui-même.

Les étapes 3 et 4 arrêtent l’exécution enregistrée de chat-small pour T-80009, qui termine normalement après 4 appels :

Stopped: max_steps. No proposal: the ticket goes to a person.
Stopped: time. No proposal: the ticket goes to a person.
Stopped: cost. No proposal: the ticket goes to a person.

L’arrêt lié au temps utilise la latence enregistrée. Après l’appel 3, le temps du modèle est de 1,06 + 0,877 + 1,373 = 3,31 secondes, soit plus de 3. L’arrêt lié au coût survient aussi après l’appel 3, à environ 0,0007 dollar. La boucle vérifie chaque budget avant un appel. Une exécution peut donc dépasser un budget d’un appel, jamais plus.

Comment les 70 exécutions de gemma3:4b se sont terminées

Section intitulée « Comment les 70 exécutions de gemma3:4b se sont terminées »

Les règles de la boucle comptent surtout pour un modèle faible. Voici comment l’exécution enregistrée du projet avec gemma3:4b s’est terminée sur les 70 tâches, avec les règles de cette leçon :

gemma3:4b dans Ollama (en local), une étape JSON par appel, 70 tâches, une exécution. Enregistrements réels du projet, évalués avec la boucle du projet.
Description textuelle du graphique

Un diagramme à barres montrant comment les 70 exécutions se sont terminées : finished 23, too_many_errors 37, repeated_call 10, max_steps 0.

Il y a eu 37 arrêts too_many_errors. Parmi eux, 34 suivaient trois résolutions refusées de suite, 2 des réponses coupées à la limite de jetons et 1 un dépassement du délai du service. Aucune exécution n’a atteint la limite d’étapes. Les règles de comportement ont arrêté chaque exécution bloquée avant, après au plus 8 appels. chat-small et chat-strong ont terminé 69 tâches sur 70. La 70e était T-80002, arrêtée par le filtre de contenu du fournisseur (provider_error). Le modèle local gratuit exécute donc correctement la boucle, et celle-ci vous protège de ses mauvaises décisions. Elle ne les rend pas bonnes : gemma3:4b a proposé la bonne résolution pour 28 des 70 tâches.

Chaque boucle d’agent a besoin d’une limite d’étapes et d’un budget de temps. Ils s’appliquent à tous les modèles et à toutes les défaillances. Ajoutez un plafond de dépenses lorsqu’un fournisseur réel vous facture. Ajoutez les règles de comportement quand vos traces montrent des exécutions bloquées; avec un petit modèle, vous en verrez.

Chaque règle a un coût. Une limite d’étapes basse arrête des exécutions lentes, mais correctes. La règle d’erreurs a abandonné T-80005 un appel trop tôt. La règle d’appels répétés exige une bonne signature : {"order_id":"LK-640436"} et {"order_id": "LK-640436"} doivent compter comme le même appel. La boucle compare donc du JSON compact. Et chaque arrêt confie un billet de soutien à une personne, ce qui lui prend du temps. Choisissez les valeurs à partir de vos enregistrements, comme ici, et revérifiez-les quand vous changez de modèle.

Erreur courante

« Arrêter tout appel répété, par sécurité »

Symptôme : Amira essaie la règle plus stricte « tout appel identique » dans l’explorateur. Sur T-80009, elle arrête la boucle au troisième appel, un appel plus tôt. Amira remplace donc la vérification dans loop.py par if signature in calls_seen:. Maintenant, python -m resolver run T-90601 affiche Stopped: repeated_call. No proposal: the ticket goes to a person. (arrêt : appel répété; aucune proposition; le billet passe à une personne). Le test test_a_failed_call_may_be_tried_again échoue. Le service de commandes avait dépassé le délai une fois (simulation). Le deuxième appel, qui aurait réussi, n’a jamais eu lieu.

Cause : répéter un appel après un échec est souvent la bonne étape. Les dépassements de délai et les courtes pannes passent, et la requête permet une nouvelle tentative.

Correction : arrêtez un appel répété seulement après une réussite, car la réponse ne peut alors plus changer. Laissez la règle d’erreurs gérer les appels qui échouent sans cesse : trois de suite, et la boucle s’arrête.

Ce que cela signifie pour un vrai service d’IA : une règle d’arrêt vous protège seulement si vous comprenez pourquoi le modèle se répète. Testez chaque règle sur des traces réelles, autant celles qui restent bloquées que celles qui reprennent.

Exercice du module : une boucle d’outils simulée aux résultats prévisibles

Section intitulée « Exercice du module : une boucle d’outils simulée aux résultats prévisibles »

Créez votre propre fichier de tests avec un modèle à scénario prédéfini. Ce faux modèle renvoie les étapes que vous écrivez, donc chaque résultat est prévisible. tests/scripted.py en contient un (Scripted, call, finish). Les outils sont les vrais outils de lecture sur une copie neuve de la base de données d’exercice. Les défaillances sont simulées là où vous les demandez.

  1. Créez tests/test_my_loop.py. Utilisez le billet T-90103 (la jardinière fissurée, commande LK-640436).
  2. Écrivez un cas de réussite : un get_order, puis un finish avec une réexpédition. La boucle s’arrête avec finished (terminé) après 2 appels.
  3. Écrivez deux cas d’échec : un modèle qui appelle toujours un outil inconnu, et un service get_order qui dépasse toujours le délai (Faults). Les deux s’arrêtent avec too_many_errors (trop d’erreurs) après 3 appels.
  4. Écrivez un cas qui montre la remise à zéro : deux lectures échouées, une réussite, un échec, puis finish. Il doit se terminer après 5 appels.
  5. Exécutez python -m pytest tests/test_my_loop.py.
Afficher un exemple de réponse
tests/test_my_loop.py
"""Module practice: a mock tool loop with predictable success and failure cases.
The model is SCRIPTED (tests/scripted.py): no model wrote these steps. The tools are the real
read tools on a fresh copy of the practice database, with simulated failures where a test asks for them.
"""
from resolver.agent import run_agent
from resolver.data import load_task
from resolver.loop import Limits
from resolver.runner import new_state
from resolver.systems import Faults
from .scripted import Scripted, call, finish
PLANTER = [{"tool": "reship_item", "order_id": "LK-640436", "sku": "PLANTER", "quantity": 1}]
def run(world, script, faults=None, limits=Limits()):
task = load_task("T-90103") # the cracked cedar planter
state = new_state(task, "agent", "chat-small", "practice")
return run_agent(state, Scripted(script).complete, world("T-90103", faults), "tools", limits)
def test_success_read_then_finish(world):
s = run(world, [[call("get_order", order_id="LK-640436")], [finish("resolve", "W5", PLANTER)]])
assert (s.stop_reason, s.step) == ("finished", 2)
assert [e.code for e in s.evidence] == ["ok"]
def test_an_unknown_tool_again_and_again_ends_the_loop(world):
s = run(world, [[call("refund_now", order_id="LK-640436", amount=89)]])
assert (s.stop_reason, s.step) == ("too_many_errors", 3)
assert [e.code for e in s.evidence] == ["unknown_tool"] * 3
def test_a_service_that_never_answers_ends_the_loop(world):
down = Faults([{"tool": "get_order", "mode": "timeout", "times": 99}]) # simulated
s = run(world, [[call("get_order", order_id="LK-640436")]], faults=down)
assert (s.stop_reason, s.step) == ("too_many_errors", 3)
assert [e.code for e in s.evidence] == ["service_timeout"] * 3
def test_a_success_resets_the_error_count(world):
script = [[call("get_order", order_id="LK-000001")], [call("get_order", order_id="LK-000002")],
[call("get_order", order_id="LK-640436")], [call("get_order", order_id="LK-000003")],
[finish("resolve", "W5", PLANTER)]]
s = run(world, script)
assert (s.stop_reason, s.step) == ("finished", 5)
assert [e.ok for e in s.evidence] == [False, False, True, False]

python -m pytest tests/test_my_loop.py affiche 4 passed (4 tests réussis), et la suite complète affiche 39 passed (39 tests réussis). Le modèle à scénario prédéfini répète sa dernière étape quand le scénario se termine. Un scénario d’une seule étape représente donc un modèle qui ne change jamais d’avis. L’outil inconnu n’est jamais exécuté, et l’appel échoué peut être répété. Aucun des deux cas ne s’arrête donc avec repeated_call (appel répété) : la règle d’erreurs met fin aux deux.

À vous. Changez une condition : exécutez le cas de l’outil inconnu avec Limits(max_errors=2). Prédisez la raison de l’arrêt et le nombre d’appels avant l’exécution. (Réponse : too_many_errors après 2 appels.)

Prouvez que la boucle se termine après des erreurs répétées et des appels d’outils inconnus. Exécutez les deux tests de l’instantané et les vôtres :

Fenêtre de terminal
python -m pytest tests/test_loop.py -k "unknown or repeated_errors" -v
python -m pytest tests/test_my_loop.py

La première commande affiche 2 passed, 9 deselected (2 tests réussis, 9 non sélectionnés). Vous avez terminé le module quand :

  • python -m pytest réussit, avec vos nouveaux tests.
  • Vous pouvez expliquer quelle règle arrête chacune des cinq exécutions sans limites dans l’explorateur, avec les limites du projet. Vous pouvez aussi expliquer pourquoi la limite d’étapes seule ne suffit pas.
  • Vous pouvez expliquer pourquoi un appel répété après un échec est permis, avec T-90601 comme exemple.

Réinitialisation. Si votre projet ne fonctionne plus, renommez-le et copiez l’instantané end-of-m02, comme l’indique le README des instantanés. Recréez l’environnement virtuel. python -m pytest affiche alors 35 passed (35 tests réussis). L’instantané contient les étapes guidées de ce module, mais pas l’exercice du module.

Vérifiez votre compréhension

Un modèle demande get_order avec LK-000001, puis LK-000002, puis LK-000003. Aucune de ces commandes n'existe. Quelle règle arrête la boucle, après combien d'appels au modèle, et pourquoi pas la règle d'appels répétés?

Afficher la réponse

La règle d’erreurs : too_many_errors après 3 appels au modèle. Chaque appel est refusé (not_found, introuvable). Le nombre d’erreurs passe donc de 1 à 2, puis à 3. La boucle s’arrête à la fin de la troisième étape. La règle d’appels répétés ne s’applique jamais, car les trois appels ont des arguments différents et aucun n’a réussi. Le test test_repeated_errors_end_the_loop vérifie ce cas.

Vérifiez votre compréhension

Pourquoi le budget de temps utilise-t-il la latence enregistrée lors d'une relecture, plutôt que le temps réel sur votre ordinateur?

Afficher la réponse

Une relecture répond en quelques millisecondes, donc le temps réel n’atteindrait jamais 120 secondes. Sans modèle actif, vous ne pourriez jamais tester le budget de temps. Avec la latence enregistrée, une relecture attend, sur papier, exactement aussi longtemps que l’exécution réelle. Le budget arrête donc une relecture au même appel que l’exécution réelle. test_the_time_budget le vérifie avec une latence prédéfinie.

Vérifiez votre compréhension

Prédisez le résultat : dans l'explorateur, choisissez T-90608 et désactivez la règle d'erreurs. Gardez les autres limites du projet. Où la boucle s'arrête-t-elle, et pourquoi la règle d'appels répétés n'aide-t-elle pas?

Afficher la réponse

À la limite d’étapes : max_steps (maximum d’étapes) après 8 appels au modèle. Le modèle appelle get_customer_orders sans cesse, et le service dépasse le délai chaque fois. La règle d’appels répétés du projet arrête seulement la répétition d’un appel réussi. Aucun de ces appels n’a réussi. Seule la règle d’erreurs, ou la règle plus stricte « tout appel identique », l’arrêterait plus tôt, au troisième appel.

Récapitulatif

  • Le modèle décide s’il doit s’arrêter, donc le code doit garantir que la boucle se termine. Chaque exécution se termine pour une raison nommée dans stop_reason (raison de l’arrêt).
  • Limites de budget, avant chaque appel : 8 appels au modèle, 120 secondes de temps du modèle, 0,05 $ US par tâche. Règles de comportement, après chaque appel : la répétition d’une lecture réussie, et trois erreurs de suite.
  • Les valeurs viennent des enregistrements. Aucune exécution Azure n’a nécessité plus de 5 appels, et aucune tâche réelle n’a pris plus de 72,9 secondes. Aucune tâche chat-strong n’a coûté plus d’environ 0,033 $ US.
  • Sans les règles, quatre des cinq exécutions réelles de gemma3:4b ne se sont jamais terminées en 12 appels. Avec les règles, elles s’arrêtent après 3 à 5 appels. Le coût : T-80005 s’est arrêté un appel avant de pouvoir terminer.
  • tests/test_loop.py prouve l’arrêt avec un modèle qui ne termine jamais, se répète, appelle des outils inconnus, échoue sans cesse ou propose des résolutions invalides. Vos propres tests à scénario prédéfini le prouvent à nouveau.

Vous avez terminé le module 2. Ensuite, l’agent élabore un plan et le modifie quand les éléments de preuve changent.

Leçon suivante : Planifier et réviser