Un état de tâche explicite
- Leçon 4
- 50 min
- Module 2 : Représenter l'état et les outils
Leçon 4 sur 18 · Agents d'IA et orchestration des flux de travailUn état de tâche explicite
1. Choisir le bon modèle d'orchestration
2. Représenter l'état et les outils
3. Raisonnement, planification et contexte
4. Approbation humaine et reprise
5. Plusieurs exécutants et intégrations
6. Évaluer et exploiter un agent borné
Ce que vous saurez faire
Gardez tout ce qu’une tâche sait, décide et fait de travers dans des champs typés que votre code peut vérifier. Ainsi, le texte d’un modèle ne peut jamais passer pour un fait.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- L’agent unique et sa boucle, dans Limites entre flux de travail et agent : le modèle propose une étape, et l’application l’exécute.
- Pourquoi une application doit garder la conversation et la renvoyer avec chaque requête, dans Gérer l’état de la conversation du cours Créer des applications fiables avec les API de LLM.
- Les modèles Pydantic (
BaseModel, champs typés, validation), dans Définir un schéma.
Ce qu’il vous faut sur votre ordinateur
- Le projet
resolution-workflowà la fin du module 1, après Décider du niveau de complexité : ajouter l’évaluation au projet. Dans le dossier du projet, avec l’environnement virtuel actif,python -m pytestaffiche23 passed(23 tests réussis). - Si votre projet ne fonctionne plus, copiez l’instantané
end-of-m01, comme l’indique le README des instantanés.
Cette leçon n’ajoute aucun fichier au projet. Vous lisez l’état d’exécutions réelles dans une session Python. Chaque décision de modèle sur cette page est un enregistrement : gemma3:4b dans Ollama, ou gpt-6-luna sur Azure sous chat-small. Vous n’avez besoin ni de compte ni de clé. Les modèles et les cadres d’agents changent vite. Les chiffres de ce module sont donc un instantané; la méthode reste valable. Testé avec Python 3.12.13 sur macOS. Les commandes Windows ne sont pas testées.
Un atelier de vélos et ses fiches de travail
Section intitulée « Un atelier de vélos et ses fiches de travail »Une cliente apporte un vélo à un atelier de réparation. Pendant les deux jours suivants, elle et le mécanicien échangent vingt textos. « Les freins grincent. » « Peut-être la roue arrière aussi? » « Pouvez-vous aussi regarder la chaîne? » « Les nouvelles plaquettes de frein coûtent 30 dollars, ça vous va? » « Oui, allez-y. »
Le troisième jour, un autre mécanicien prend le vélo en charge. Il pourrait lire les vingt messages et essayer de comprendre où en est le travail. Ou il pourrait lire la fiche de travail sur le guidon. Elle contient quelques lignes fixes : le travail, les vérifications faites, les pièces approuvées et l’argent dépensé. La dernière ligne indique l’état : en attente de pièces.
Les messages gardent une trace de la discussion. La fiche de travail garde une trace du travail. « Peut-être la roue arrière aussi? » figure dans les messages, mais personne n’a vérifié la roue arrière. Seule la fiche de travail indique ce qui est vrai.
Le problème : la conversation comme état
Section intitulée « Le problème : la conversation comme état »L’agent de ce cours communique avec son modèle par messages. Chaque appel au modèle renvoie le message système, le billet de soutien et toutes les étapes précédentes. Après quelques étapes, cet historique de conversation contient tout ce qui s’est passé, sous forme de texte. Tomás fait donc une suggestion : « L’historique de conversation, c’est l’état. On n’a besoin de rien d’autre. »
Regardez d’abord une exécution réelle. Le billet T-80001 vient de Tom. Sa voisine lui a demandé de vérifier sa commande à elle, LK-615204. gemma3:4b, un petit modèle local, a exécuté la boucle de l’agent pour ce billet. Voici ses trois premières étapes :
- Il a demandé
get_orderavec LK-615204. L’application a refusé :No order LK-615204 is available for this customer.(Aucune commande LK-615204 n’est disponible pour ce client.) La commande appartient à une autre cliente. - Il a demandé
get_customer_orders, les commandes de Tom. Tom n’en a aucune, donc le résultat était une liste vide. Dans la même réponse, il a rédigé un brouillon qui mentionne LK-615204. - Il a demandé
get_customer_ordersde nouveau.
Supposons maintenant que votre code doit répondre à une question : quelles commandes cette tâche a-t-elle vraiment vues? La politique de Grace exige cette réponse avant toute action. Une action sur une commande qu’aucun outil n’a renvoyée n’est jamais permise. Si l’historique de conversation sert d’état, le code doit trouver la réponse dans le texte. Le texte mentionne LK-615204 cinq fois : dans le billet, l’appel, le refus et deux brouillons de réponse. Une recherche d’identifiants de commande dans l’historique trouve exactement une commande : celle de la voisine.
Une vérification fondée sur l’historique considérerait donc la commande de la voisine comme connue. C’est le contraire de la réalité. L’application a refusé cette commande, et aucun outil n’a renvoyé de commande.
L’idée : un état de tâche explicite
Section intitulée « L’idée : un état de tâche explicite »L’état d’une tâche représente ce que l’application sait de cette tâche maintenant. Il comprend son objectif, ses découvertes, sa progression, ses décisions et ses erreurs. Un état de tâche explicite inscrit chacun de ces éléments dans un champ typé distinct. « Explicite » signifie que le code n’a rien à déduire du texte. Il suffit de lire un champ pour obtenir chaque réponse.
Le projet garde cet état dans TaskState, un modèle Pydantic dans resolver/state.py. Le schéma montre deux fois la même exécution de T-80001 : comme historique de conversation, puis comme état de tâche.
Description textuelle du diagramme
Le côté gauche présente les 8 messages de l’historique de conversation. La commande LK-615204 de la voisine apparaît dans le billet, l’appel du modèle, le refus et deux brouillons de réponse.
Le côté droit présente les champs de l’état de la tâche. L’état est stopped, après 3 étapes, à cause de la limite d’étapes. Les preuves contiennent trois résultats d’outils, mais aucun n’est une commande. Le champ seen_orders est donc vide. La proposition est None, il n’y a aucune erreur, et l’utilisation compte 3 appels au modèle, 6 104 jetons en entrée et 9,981 secondes.
Lisez le côté droit comme une fiche de travail. Les preuves sont une liste des résultats d’outils que l’application a vraiment reçus. Le modèle ne peut rien y ajouter. Seul le code de la boucle ajoute une ligne Evidence, après avoir exécuté un outil. seen_orders() lit les identifiants de commande uniquement dans les résultats réussis. Pour T-80001, cette méthode renvoie un ensemble vide, ce qui correspond à la réalité.
Voici les champs de TaskState à cette étape du projet :
| Champ | Ce qu’il contient | Qui y écrit |
|---|---|---|
goal, attachments |
le texte du billet et ses pièces jointes | le programme d’exécution, une fois, à partir de la tâche |
status, step, stop_reason |
la progression de la tâche, le nombre d’appels au modèle, la raison de fin de la boucle | la boucle et le programme d’exécution |
plan |
le plan actuel du modèle (mode structuré seulement) | la boucle, à partir de la réponse du modèle |
evidence |
chaque résultat d’outil : outil, arguments, ok, code, données, identifiants de commande |
la boucle, après avoir exécuté un outil |
proposal |
la résolution, après sa validation par le code | la boucle, seulement si les vérifications réussissent |
errors |
les erreurs du fournisseur, pour la trace et pour une personne | la boucle |
usage |
les appels au modèle, les jetons, le coût, le temps du modèle | la boucle, après chaque appel |
events |
tout ce qui s’est passé, dans l’ordre (la trace) | la boucle |
messages |
l’historique de conversation : le contexte de travail du prochain appel au modèle | la boucle |
L’historique de conversation est toujours là, dans messages. Le modèle en a besoin, car il ne voit rien d’autre. Mais le code ne prend jamais de décision en le lisant. Au module 4, l’état gagne deux champs, approval et operations : qui a approuvé quoi, et quelles écritures ont eu lieu.
Un petit exemple détaillé : trois étapes de T-80001
Section intitulée « Un petit exemple détaillé : trois étapes de T-80001 »Suivez l’état de l’exécution de gemma3:4b, étape par étape. Chaque ligne montre les champs après cette étape :
| Après l’étape | Le modèle a demandé | L’application a répondu | evidence (codes) |
seen_orders() |
status |
|---|---|---|---|---|---|
| 0 | aucune | aucune | running |
||
| 1 | get_order LK-615204 |
refus | other_customer |
aucune | running |
| 2 | get_customer_orders |
succès, une liste vide | other_customer, ok |
aucune | running |
| 3 | get_customer_orders |
succès, une liste vide | other_customer, ok, ok |
aucune | stopped |
Trois choses changent, et chaque changement est visible. step compte les appels au modèle. evidence gagne une ligne par appel d’outil, avec un code qui indique ce qui s’est passé. status change une fois, à la fin, parce qu’une limite de 3 étapes a arrêté cette exécution. Rien dans le tableau ne dépend de ce que le modèle a écrit. Les brouillons de réponse qui mentionnent LK-615204 sont dans l’historique, pas dans l’état.
Lire l’état d’une exécution
Section intitulée « Lire l’état d’une exécution »Faites ces étapes dans le dossier du projet, avec l’environnement virtuel actif.
-
Démarrez Python :
Fenêtre de terminal python -
Exécutez T-80001 avec les décisions enregistrées de gemma3:4b. La limite de 3 étapes garde l’exécution courte :
from resolver.config import Settings, make_providerfrom resolver.data import load_taskfrom resolver.loop import Limits, accept_resolutionfrom resolver.runner import run_tasktask = load_task("T-80001")provider = make_provider(Settings.from_env())state = run_task(task, "agent_structured", "gemma3:4b", provider.complete, limits=Limits(max_steps=3))state.status, state.stop_reason, state.stepPython affiche :
('stopped', 'max_steps', 3) -
Regardez l’historique de conversation. Comptez les mentions de la commande de la voisine :
[m["role"] for m in state.messages]history = " ".join(m["content"] for m in state.messages)history.count("LK-615204")['system', 'user', 'assistant', 'user', 'assistant', 'user', 'assistant', 'user']5Dans ce mode structuré, chaque résultat d’outil retourne au modèle sous forme de message
user(utilisateur). Avec les appels d’outils, c’est un messagetool(outil). Le code qui lit l’historique doit connaître les deux formats. Le code qui lit l’état n’en a pas besoin. -
Interrogez maintenant l’état :
[(e.step, e.tool, e.ok, e.code, e.order_ids) for e in state.evidence]state.seen_orders()[(1, 'get_order', False, 'other_customer', []), (2, 'get_customer_orders', True, 'ok', []), (3, 'get_customer_orders', True, 'ok', [])]set() -
Comparez avec la méthode de Tomás : une recherche d’identifiants de commande dans l’historique.
import reset(re.findall(r"LK-\d{6}", history)){'LK-615204'} -
Donnez à la vérification de la boucle une proposition qui agit sur la commande de la voisine. Cette proposition est construite : aucun modèle ne l’a écrite.
refund = {"outcome": "resolve", "rule": "W3", "reply": "We will refund the shed.", "reason": "lost","actions": [{"tool": "request_refund", "order_id": "LK-615204", "amount": 899.0,"reason": "RFD-LATE", "payment_id": None}]}accept_resolution(state, refund)'request_refund on LK-615204: that order was not returned by a tool in this task. Look it up first; never act on an order that you have not seen.' -
Affichez les champs qu’une personne lirait en premier :
print(state.model_dump_json(include={"goal", "status", "step", "plan", "stop_reason", "errors", "usage"}, indent=2))La sortie commence par l’objectif (le texte du billet) et se termine par l’utilisation :
"model_calls": 3,"input_tokens": 6104,"output_tokens": 302,"cost_usd": 0.0,"model_seconds": 9.981. Un modèle local ne coûte rien, donc le coût est de 0,0. Le temps est le temps enregistré du modèle. -
Tapez
exit()pour quitter Python.
accept_resolution utilise seen_orders(). Cette fonction refuse donc le remboursement, avec une raison que le modèle peut lire. Une vérification qui lit l’historique le laisserait passer.
L’état forme une petite machine à états
Section intitulée « L’état forme une petite machine à états »Le champ status ne peut prendre que quelques valeurs. Chaque valeur a un sens fixe, et seuls certains changements sont permis. C’est une machine à états : un ensemble fixe d’états et les transitions permises entre eux. Elle répond à « où en est ce billet? » pour chaque billet, en un mot.
Description textuelle du diagramme
Une tâche commence dans running. Elle quitte running de quatre façons possibles :
- Une limite arrête la boucle avant toute proposition : stopped. Une personne prend le billet.
- La proposition est reply_only ou ask_customer, donc rien ne change : done.
- La proposition est hand_to_person : handed_to_person.
- La proposition est resolve, avec des actions : waiting_approval.
À partir du module 4 : une personne approuve (executing) ou refuse (handed_to_person). Depuis executing, la tâche passe à done quand chaque écriture a été faite une fois, et à handed_to_person si une écriture échoue.
Le programme d’exécution fixe le statut à partir de l’état, jamais du texte du modèle. Voici le code, dans resolver/runner.py :
if state.proposal is None: state.status = "stopped" elif state.proposal.actions: state.status = "waiting_approval" elif state.proposal.outcome == "hand_to_person": state.status = "handed_to_person" else: state.status = "done"Remarquez la première ligne. Une exécution qui s’arrête sans proposition ne disparaît pas sous forme d’erreur. Elle a son propre état, et une personne reçoit le billet. L’évaluation du module 1 compte un arrêt de la même façon : comme un transfert à une personne.
Ce que l’état apporte à Larkfield
Section intitulée « Ce que l’état apporte à Larkfield »Chaque personne chez Larkfield lit un champ différent. Grace approuve les changements et veut voir exactement ce qui va se passer. Elle lit proposal, que le code a validé, et evidence, qui montre la source de chaque fait. Omar demande combien coûte un billet, et usage répond pour chaque tâche. Mei est responsable des services de commandes et de paiements. Elle lit les codes dans evidence (service_timeout, service_unavailable) pour voir quand ses services ont fait échouer une tâche.
L’état rend aussi les modules suivants possibles. Au module 4, le flux de travail s’arrête à waiting_approval (en attente d’approbation) et peut attendre plusieurs jours. Un point de contrôle enregistre TaskState dans un fichier SQLite. L’exécution reprend plus tard à partir des champs enregistrés. Un historique de conversation en mémoire disparaît quand le processus se termine.
Quand utiliser un état explicite, et ce qu’il coûte
Section intitulée « Quand utiliser un état explicite, et ce qu’il coûte »Utilisez un état typé quand une tâche comporte plusieurs étapes, ou quand le code doit vérifier ses décisions. Utilisez-le aussi quand la tâche peut changer quelque chose, ou doit s’arrêter et reprendre plus tard. Le flux de travail de résolution des billets réunit ces quatre conditions.
Vous n’en avez pas besoin pour un seul appel au modèle, comme la rédaction d’une réponse. Dans ce cas, la requête et la réponse forment toute la tâche. L’assistant de billets du cours Créer des applications fiables avec les API de LLM gardait seulement une conversation. Cela suffisait pour la rédaction.
Le coût, c’est du code et de la discipline. Chaque champ a besoin d’un responsable d’écriture, et ce responsable doit toujours être votre code, pas le modèle. Quand vous ajoutez un champ, vous devez décider qui le remplit et quand. Vous gardez aussi les messages, car le modèle en a besoin. L’état est donc un ajout de données, pas un remplacement. En retour, chaque vérification lit un champ, et chaque personne lit le champ dont elle a besoin.
Erreur courante
« L'historique de conversation, c'est l'état, on n'a pas besoin d'une base de données »
Symptôme : Tomás écrit la vérification « ne jamais agir sur une commande jamais vue » comme une recherche dans l’historique. Chaque numéro LK- dans les messages compte comme une commande vue. Pour T-80001, la recherche renvoie {'LK-615204'}, la commande de la voisine. Une proposition de remboursement de 899,00 dollars sur cette commande passe maintenant la vérification.
Cause : l’historique mélange trois types de texte : ce que le client a écrit, ce que le modèle a écrit et ce que l’application a renvoyé. Une recherche ne peut pas les distinguer. Un refus mentionne la commande qu’il refuse, et un brouillon du modèle peut mentionner n’importe quelle commande.
Correction : enregistrez les faits dans des champs typés au moment où ils se produisent. La boucle ajoute une ligne Evidence après chaque appel d’outil, avec ok et les identifiants des commandes réellement renvoyées. Les vérifications lisent state.seen_orders(), jamais les messages.
Ce que cela signifie pour un vrai service d’IA : une conversation est une entrée pour le modèle, pas un registre pour l’application. Tout ce dont dépend une vérification, une personne ou un processus ultérieur appartient à un champ que seul votre code remplit.
À vous de jouer
Section intitulée « À vous de jouer »Faites une prédiction avant l’exécution. Le billet T-90103 concerne la jardinière en cèdre fissurée, et le client a joint une photo. Exécutez-le avec l’agent par défaut (chat-small, appels d’outils) dans une session Python, comme dans les étapes ci-dessus :
s = run_task(load_task("T-90103"), "agent", "chat-small", provider.complete)s.status, s.step, s.seen_orders(), s.proposal.outcome, len(s.proposal.actions)Quelle sera la valeur de status, et pourquoi?
Afficher la réponse
('waiting_approval', 2, {'LK-640436'}, 'resolve', 2)Le modèle a fait une lecture (get_order LK-640436), puis proposé une résolution avec deux actions : reship_item et create_return_label. La résolution contient des actions, donc le programme d’exécution fixe waiting_approval. Rien n’est écrit : les actions restent des propositions jusqu’à leur approbation par une personne, au module 4. seen_orders() contient LK-640436 parce que get_order l’a renvoyée.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Le modèle écrit dans son brouillon de réponse : « J'ai vérifié la commande LK-640436 et elle a été livrée. » Aucun outil n'a retourné LK-640436 pendant cette tâche. LK-640436 apparaît-elle dans state.seen_orders()? Expliquez.
Afficher la réponse
Non. seen_orders() lit seulement les preuves, et seulement les lignes où ok vaut vrai. Après avoir exécuté un outil, la boucle ajoute une ligne de preuve contenant les identifiants des commandes retournées. Une phrase du modèle se trouve dans les messages, pas dans les preuves. La boucle refuse donc toute action ultérieure sur LK-640436 jusqu’à ce qu’un outil retourne cette commande.
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
Une exécution se termine parce que la limite d'étapes arrête la boucle. Que valent status et proposal, et qu'arrive-t-il au billet?
Afficher la réponse
proposal vaut None (aucune valeur), car le modèle n’a jamais fourni de résolution valide. Le programme d’exécution définit status (état) à stopped (arrêté). Le billet de soutien est transmis à une personne. L’arrêt n’est pas caché : stop_reason (raison de l’arrêt) indique pourquoi (max_steps, limite d’étapes). Les preuves montrent ce que l’exécution a trouvé avant l’arrêt. L’évaluation compte une exécution arrêtée comme une transmission à une personne.
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 le projet conserve-t-il les messages dans TaskState si aucune vérification ne les lit?
Afficher la réponse
Le modèle en a besoin. Un modèle ne conserve rien entre deux requêtes. Chaque appel au modèle doit donc inclure la conversation jusqu’ici : le billet, les appels précédents et leurs résultats. Les messages constituent le contexte de travail du modèle. Le module 4 les utilise aussi pour reprendre une boucle après un point de contrôle. Mais ils servent d’entrée au modèle, pas de faits pour le code.
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
- Un historique de conversation garde une trace des échanges. Il mélange le texte du client, celui du modèle et les résultats de l’application. Une vérification ne peut donc pas s’y fier.
- Un état de tâche explicite conserve chaque fait dans un champ typé. Les champs sont l’objectif, les preuves, l’étape, le plan, la proposition, les erreurs, l’utilisation, l’état et la raison de l’arrêt. Dans le module 4, l’approbation et les écritures s’y ajoutent.
- Seule l’application écrit les preuves.
seen_orders()les lit, etaccept_resolutionrefuse une action sur toute autre commande. Pour T-80001, l’historique mentionne cinq fois la commande du voisin; l’état ne contient aucune commande. statusest une petite machine à états :running(en cours), puisstopped,done(terminé),handed_to_person(transmis à une personne) ouwaiting_approval(en attente d’approbation). Un arrêt a son propre état, et une personne reçoit le billet.
Ensuite, vous rendez les outils eux-mêmes stricts : chaque appel doit respecter un contrat avant son exécution.
Leçon suivante : Contrats d'outils