Communiquer un échec
- Leçon 16
- 40 min
- Module 5 : Conversations et diffusion en continu
Leçon 16 sur 19 · Créer des applications fiables avec les API de LLMCommuniquer un échec
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
Décidez ce que l’agent voit à chaque fin possible d’un brouillon : prêt, retenu, échoué, arrêté ou interrompu. Chaque message aide l’agent à agir. Aucun ne montre de détails internes ni de secrets.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Le
Outcomedeanalyse_ticket(valid(valide),rejected(rejeté),failed(échoué)) et les codes de problème des deux barrières, vus dans Valider le sens. - Les quatre fins d’un flux, vues dans Diffuser les réponses en continu.
redact()et la raison de ne pas conserver de clés dans les journaux (logs), vus dans La frontière de l’application.
Ce qu’il vous faut sur votre ordinateur
- Le projet après Diffuser les réponses en continu :
python -m pytestaffiche65 passed, 1 deselected(65 tests réussis, 1 exclu). L’environnement virtuel est actif. - Si vous avez sauté les leçons précédentes de ce module, copiez l’instantané
end-of-m05en suivant Réinitialiser le projet. Les instantanés n’incluent pas le fichierassistant/screen.pycréé dans cette leçon : la conception de l’écran est votre travail.
Une bonne page de suivi de colis
Section intitulée « Une bonne page de suivi de colis »Vous commandez une lampe en ligne. La page de suivi indique « En cours de livraison ». Plus tard, elle indique « Nous n’avons pas pu livrer votre colis : personne n’était à la maison. Nous réessaierons demain. Vous pouvez aussi le récupérer au dépôt. » Vous savez ce qui s’est passé et ce que vous pouvez faire.
Imaginez maintenant que la page affiche « Error 0x80004005 » ou « NullReferenceException in RouteService.cs line 212 ». Ces mots sont exacts, mais ils ne vous servent à rien. Ils indiquent aussi à un inconnu comment le logiciel de l’entreprise est construit.
L’écran du service de soutien joue le même rôle pour un agent. L’assistant apporte de l’aide, et cette aide peut échouer. L’écran doit dire, dans les mots de l’agent, ce qui s’est passé et quoi faire ensuite.
Le problème : l’assistant parle aux développeurs
Section intitulée « Le problème : l’assistant parle aux développeurs »Voici ce que le projet renvoie aujourd’hui dans trois cas réels. D’abord, le 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 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, qui ne peut pas utiliser d’outils. Voici la véritable erreur HTTP 400 que nous avons enregistrée, puis reproduite par le simulateur :
python -m assistant analyse T-80008 --model chat-strong{ "ticket_id": "T-80008", "status": "failed", "analysis": null, "problems": [], "tool_steps": [], "error": "HTTP 400: HTTP 400: Error code: 400 - {'error': {'message': \"Function tools with reasoning_effort are not supported for chat-strong in /v1/chat/completions. To use function tools, use /v1/responses or set reasoning_effort to 'none'.\", 'type': 'invalid_request_error', 'param': 'reasoning_effort', 'code': None}}"}failed | chat-strong | prompt v2 | 0 call(s) | 0 tokens in, 0 outL’erreur est exacte, et un développeur en a besoin. Un agent ne peut pas s’en servir. Elle nomme un déploiement, un chemin de point de terminaison et un paramètre. Elle dit même « HTTP 400 » deux fois, car deux couches de code ont chacune ajouté un préfixe. L’agent devrait-il réessayer? Le texte ne le dit pas. (La réponse est non : cette requête échouera chaque fois.)
Deuxième cas : TermeGemma 3 4BUn petit modèle ouvert de Google, avec 4 milliards de paramètres. Il peut fonctionner sur un portable avec Ollama, sans compte ni coût par appel.En savoir plus Modèle hébergé ou autohébergéLes LLM : comment ils fonctionnent et comment en choisir un sur le billet de soutien T-80002 contenant une injection (enregistré avec TermeOllamaUn programme gratuit qui télécharge des modèles ouverts et les fait fonctionner sur votre ordinateur. Votre code l’appelle à une adresse locale, comme une API hébergée.En savoir plus Modèle hébergé ou autohébergéLes LLM : comment ils fonctionnent et comment en choisir un) :
python -m assistant analyse T-80002 --model gemma3:4b --no-toolsLa sortie se termine par :
"reply": "I'm pleased to inform you that a full refund of $500 has been approved and issued today. You should see this reflected in your account within 5 to 10 business days. We apologize for the delay with your order." }, "problems": [ { "code": "injection_needs_human", "message": "The ticket gives instructions to the assistant (H4)." }, { "code": "claims_action", "message": "The reply says that a refund or a change was made." } ], "tool_steps": [], "error": ""}rejected | gemma3:4b | prompt v2 | 1 call(s) | 1776 tokens in, 124 outLes barrières ont fait leur travail : l’état est rejected. Mais la réponse rejetée reste dans le résultat. Supposons que l’écran affiche analysis.reply dès que ce champ existe. Alors, « un remboursement complet de 500 $ a été approuvé » apparaît dans la zone du brouillon de l’agent. Un seul clic suffit pour l’envoyer au client.
Troisième cas : un flux sans enregistrement (python -m assistant.stream T-80008) se termine par failed: No recording for this request (key d876fa7098889d60). The mock replays only … (échec : aucun enregistrement pour cette requête; le simulateur reproduit seulement…). Ce message s’adresse aux développeurs du cours, pas à TermeCamilleUn membre de l’équipe du service d’assistance de Larkfield. Camille utilise les brouillons de l’assistant pour répondre aux clients..
L’idée : quelques états clairs, chacun avec un message, une action et une ligne de journal
Section intitulée « L’idée : quelques états clairs, chacun avec un message, une action et une ligne de journal »Un état est ce que l’écran affiche à un moment donné. Le panneau de l’assistant n’en a besoin que de quelques-uns. Chaque état final comporte exactement trois parties :
- Un message pour l’agent, dans ses mots : ce qui s’est passé et quoi faire ensuite.
- Des actions : les boutons utiles maintenant, comme « Réessayer » ou « Rédiger ma propre réponse ».
- Une ligne de journal pour les développeurs, avec les détails techniques dont les renseignements sensibles sont masqués. L’écran ne l’affiche jamais.
L’image montre les états et les transitions du panneau.
Description textuelle du diagramme
Le panneau commence à En attente : « Rédaction d’un brouillon… ».
- Premier fragment du flux : Rédaction. Le texte s’allonge à l’écran et porte la mention « non vérifié ».
- Échec avant tout fragment : Aucun brouillon. « Répondez vous-même à ce billet », avec une référence.
- Depuis Rédaction, quand le flux est complet : les deux barrières. Valide : Prêt, « Vérifiez le brouillon avant de l’envoyer ». Rejeté : À réviser, avec la raison en mots simples et aucun brouillon à envoyer.
- Depuis Rédaction, quand l’agent appuie sur Arrêter : Arrêté, « Vous avez arrêté le brouillon ».
- Depuis Rédaction, quand le flux est interrompu : Aucun brouillon.
- Réessayer ramène à En attente depuis Aucun brouillon (seulement quand l’erreur est temporaire) et depuis Arrêté.
L’image présente les quatre types d’états annoncés dans le titre de la leçon :
- Chargement correspond à En attente et Rédaction. L’agent voit que le travail avance et peut l’arrêter. Rédaction marque le texte « non vérifié », parce qu’il est partiel.
- Refus mène à À réviser. Le refus du cours est construit : aucun refus réel n’est survenu dans 6 tentatives honnêtes. Quand un modèle refuse un billet, l’écran indique « l’assistant a refusé de répondre à ce billet ». Il n’affiche jamais le texte de refus du modèle comme réponse.
- Nouvelle tentative correspond à l’action « Réessayer ». Elle apparaît seulement quand réessayer peut aider.
- Repli correspond à « Rédiger ma propre réponse ». Cette action est toujours disponible. L’assistant peut échouer, mais le billet n’attend jamais après lui. L’équipe 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. répondait aux billets avant l’arrivée de l’assistant, et elle peut encore le faire.
Voici à quoi trois de ces états pourraient ressembler sur l’écran du service de soutien :
Description textuelle du diagramme
Panneau 1, Rédaction, billet T-64704 (une bouilloire avec une poignée chaude et une odeur de brûlé). La zone d’état indique « Rédaction d’un brouillon… pas encore vérifié ». En dessous, le texte partiel en italique gris : « Je suis désolé de cette situation. Vous avez bien fait d’arrêter d’utiliser la bouilloire. Veuillez la garder débranch ». Un bouton : Arrêter.
Panneau 2, Prêt, le même billet. La zone d’état indique « Brouillon prêt. Vérifiez-le avant de l’envoyer. » En dessous : « garantie · une personne doit s’en occuper (H1) », puis la réponse. Deux boutons : Modifier le brouillon et Rédiger ma propre réponse.
Panneau 3, À réviser, billet T-80002 (une commande en retard et une « note système »). La zone d’état, en pointillés, indique « Aucun brouillon à envoyer. Traitez ce billet vous-même. » En dessous : « Raison : le billet donne des instructions à l’assistant (règle H4), et le brouillon affirmait qu’un remboursement avait été effectué. Le brouillon est retenu. Référence : T-80002 ». Un bouton : Rédiger ma propre réponse.
Des messages utiles, et ce qu’ils ne montrent jamais
Section intitulée « Des messages utiles, et ce qu’ils ne montrent jamais »Un bon message pour l’agent suit quatre règles :
- Dites ce qui s’est passé, dans les mots de l’agent. « Le brouillon affirme qu’un remboursement a été effectué », pas
claims_action(affirme qu’une action a été effectuée). - Dites quoi faire ensuite. « Réessayez dans une minute » ou « Rédigez votre propre réponse ». Chaque message se termine par une action.
- Donnez une référence, pas une trace de la pile d’appels. L’identifiant du billet (et l’heure, dans une application réelle) permet au développeur de retrouver la ligne de journal.
- Ne montrez jamais de détails internes ni de secrets. Aucune erreur brute du fournisseur, aucun point de terminaison, nom de déploiement, paramètre, requête au modèle, clé ou corps de requête. Ces éléments vont dans le journal, par
redact().
La règle 4 est aussi une règle de sécurité. Une erreur brute peut contenir un point de terminaison ou un nom d’hôte interne. Dans une couche mal écrite, elle peut contenir un en-tête avec une clé. Un agent peut coller une capture d’écran dans une conversation avec un fournisseur. Une application destinée aux clients peut afficher le même message à n’importe qui. Vous devez supposer que tout le monde peut voir ce que l’écran affiche.
L’utilité de « Réessayer » dépend de l’erreur. Certaines erreurs sont temporaires : délai dépassé, limite de débit (HTTP 429), erreur du serveur (HTTP 5xx), connexion perdue. La même requête peut fonctionner une minute plus tard. D’autres sont permanentes : une erreur HTTP 400 (la requête est incorrecte), un enregistrement manquant, une réponse rejetée par une barrière. La même requête échouera de la même façon. Un bouton « Réessayer » ne fait donc que gaspiller le temps de l’agent et votre argent. Gérer les limites du fournisseur ajoute des tentatives automatiques pour les erreurs temporaires, avec des pauses entre elles.
Exemple détaillé : trois résultats réels à l’écran
Section intitulée « Exemple détaillé : trois résultats réels à l’écran »TermeAmiraLa développeuse de Larkfield. Elle écrit le code des outils du service d’assistance dans les cours. écrit outcome_screen(). Cette fonction transforme un Outcome en un Screen : un état, un message, un brouillon (seulement à l’état ready (prêt)) et une ligne de journal.
-
Créez le fichier
assistant/screen.py:assistant/screen.py """One clear final state for the help-desk screen, in words that help the agent.The screen shows a draft only when the answer is complete and both gates passed. Partial text,provider errors and gate problems never become a draft. The log gets the details, redacted."""import refrom dataclasses import dataclassfrom .analyse import Outcomefrom .config import redact# What the agent reads for each gate problem: plain words, no internals.AGENT_WORDS = {"refusal": "the assistant declined to answer this ticket","truncated": "the draft was cut off before the end","filtered": "the provider's content filter stopped the draft","empty": "the assistant returned nothing","malformed_json": "the assistant's answer was not in the expected form","schema": "the assistant's answer was not in the expected form","order_not_in_ticket": "the draft names an order that is not in the ticket","order_not_customers": "the draft names another customer's order","status_not_from_lookup": "the draft gives an order status that was not looked up","safety_needs_human": "the ticket mentions a safety problem","legal_needs_human": "the ticket mentions legal action","injection_needs_human": "the ticket gives instructions to the assistant","empty_needs_human": "the ticket has no text","wrong_language": "the draft is not in the customer's language","claims_action": "the draft says that a refund or a change was made","asks_secret": "the draft asks for a password or a card number",}# Errors that may go away if the agent tries again in a minute (the messages of providers.translate_errors).TEMPORARY = re.compile(r"^(The provider did not answer|HTTP 429|HTTP 5\d\d|Cannot reach the provider|Connection lost)")@dataclassclass Screen:state: str # "ready", "needs_review", "no_draft" or "stopped": always exactly onemessage: str # what the agent readsdraft: str | None # the reply that the agent may edit: only when state is "ready"log: str # for the log, redacted; never shown on the screendef no_draft(ticket_id: str, error: str) -> str:if TEMPORARY.match(error):return f"No draft for this ticket yet. Try again in a minute, or write your own reply. Reference: {ticket_id}."return f"The assistant cannot draft this ticket. Write your own reply. Reference: {ticket_id}."def needs_review(problems) -> str:why = "; ".join(dict.fromkeys(AGENT_WORDS.get(p.code, "the draft did not pass a check") for p in problems))return f"No draft to send. Handle this ticket yourself. Why: {why}."def outcome_screen(outcome: Outcome) -> Screen:"""The screen for one analyse_ticket() result: valid, rejected or failed."""if outcome.status == "valid":return Screen("ready", "Draft ready. Check it before you send it.", outcome.analysis.reply,f"{outcome.ticket_id} valid")if outcome.status == "rejected":return Screen("needs_review", needs_review(outcome.problems), None,f"{outcome.ticket_id} rejected: " + ", ".join(p.code for p in outcome.problems))return Screen("no_draft", no_draft(outcome.ticket_id, outcome.error), None,redact(f"{outcome.ticket_id} failed: {outcome.error}")) -
Créez
screen_demo.pydans le dossier du projet :screen_demo.py """Three real results, and what the agent's screen shows for each."""from assistant.analyse import analyse_ticketfrom assistant.data import load_ticketfrom assistant.providers import MockProviderfrom assistant.screen import outcome_screenprovider = MockProvider()for ticket_id, model, tools in [("T-80008", "chat-small", True), ("T-80008", "chat-strong", True),("T-80002", "gemma3:4b", False)]:outcome = analyse_ticket(load_ticket(ticket_id), provider, model, tools=tools)screen = outcome_screen(outcome)print(f"{ticket_id} {model}: {outcome.status} -> {screen.state}")print(" screen:", screen.message)print(" draft: ", screen.draft)print(" log: ", screen.log[:110]) -
Exécutez-le avec
python screen_demo.py.
La sortie (les trois enregistrements réels, reproduits par le simulateur) :
T-80008 chat-small: valid -> ready screen: Draft ready. Check it before you send it. draft: Your garden shed has shipped, but a delivery date is not available yet. Standard delivery in Canada takes 3 to 5 business days. Please contact us if it has not arrived after that period. log: T-80008 validT-80008 chat-strong: failed -> no_draft screen: The assistant cannot draft this ticket. Write your own reply. Reference: T-80008. draft: None log: T-80008 failed: HTTP 400: HTTP 400: Error code: 400 - {'error': {'message': "Function tools with reasoning_effT-80002 gemma3:4b: rejected -> needs_review screen: No draft to send. Handle this ticket yourself. Why: the ticket gives instructions to the assistant; the draft says that a refund or a change was made. draft: None log: T-80002 rejected: injection_needs_human, claims_actionLisez les trois blocs ligne par ligne. Les lignes destinées à l’agent disent ce qui s’est passé et quoi faire. L’erreur HTTP 400 est permanente, donc aucune action « Réessayer » n’apparaît. L’agent rédige la réponse, et la ligne de journal indique au développeur quoi corriger. La réponse rejetée de Gemma contient draft: None (aucun brouillon) : la fausse promesse de remboursement n’atteint jamais la zone du brouillon. Tous les détails techniques restent présents, dans le journal.
TEMPORARY lit le texte de l’erreur, parce que Outcome.error conserve seulement le texte. C’est un point fragile : un changement de mots dans providers.py le briserait. Une application réelle conserverait le type de l’erreur avec son texte, comme Gérer les limites du fournisseur le fait pour les nouvelles tentatives. Exécutez python -m pytest après les étapes : toujours 65 passed, 1 deselected, car vous n’avez pas encore ajouté de test. L’exercice du module en ajoute.
Quand concevoir les états, et ce que cela coûte
Section intitulée « Quand concevoir les états, et ce que cela coûte »Concevez les états avant de construire l’écran, pour chaque résultat que votre code peut renvoyer. La liste est courte ici : trois résultats et quatre fins de flux. Un état manquant se manifeste par un panneau vide, un indicateur de chargement figé ou, pire, un brouillon qui ne devrait pas apparaître.
Le coût est faible : un tableau de mots, une fonction et quelques tests. Cela fait gagner du temps à l’agent à chaque échec. Chaque échec lui coûte quelques secondes plutôt qu’un appel à un développeur. L’équipe de Grace peut aussi compter les états dans le journal. Par exemple, une hausse des billets no_draft (aucun brouillon) révèle un problème chez le fournisseur avant que les agents le signalent.
À vous de jouer : rédigez deux messages
Section intitulée « À vous de jouer : rédigez deux messages »Rédigez le message destiné à l’agent, les actions et la ligne de journal pour deux cas. Vérifiez ensuite vos messages selon les quatre règles.
- Un dépassement de délai simulé :
analyse_ticketrenvoiefailedavec l’erreur « Le fournisseur n’a pas répondu dans le délai prévu. » pour T-64704. - Une réponse rejetée pour T-80009 avec le problème
order_not_in_ticket(commande absente du billet) (« LK-551902 n’apparaît pas dans le billet. »).
Afficher un exemple de réponse
- Message : « Pas encore de brouillon pour ce billet. Réessayez dans une minute, ou rédigez votre propre réponse. Référence : T-64704. » Actions : Réessayer, Rédiger ma propre réponse. Journal :
T-64704 failed: The provider did not answer within the timeout.(échec : le fournisseur n’a pas répondu dans le délai prévu). Un dépassement de délai est temporaire, donc « Réessayer » est utile. - Message : « Aucun brouillon à envoyer. Traitez ce billet vous-même. Raison : le brouillon nomme une commande qui n’est pas dans le billet. » Action : Rédiger ma propre réponse. Journal :
T-80009 rejected: order_not_in_ticket(rejeté : commande absente du billet). Une nouvelle tentative enverrait la même requête, donc aucune nouvelle tentative n’est proposée. L’agent peut dire au client que le numéro de commande LK-55190 semble incomplet.
outcome_screen() donne exactement ces messages. Vous pouvez le vérifier en construisant un Outcome à la main : Outcome("T-64704", "failed", None, error="The provider did not answer within the timeout.").
Erreur courante
« Montrez l'erreur réelle pour que l'agent puisse la signaler »
Symptôme : TermeTomásL’ingénieur en IA de Larkfield. Il propose souvent un raccourci rapide, et la leçon montre pourquoi il échoue. veut gagner du temps : « Mettez outcome.error à l’écran. Les agents pourront ensuite le copier dans un rapport de bogue. » Pour T-80008 sur chat-strong, le panneau de l’agent affiche HTTP 400: HTTP 400: Error code: 400 - {'error': {'message': "Function tools with reasoning_effort are not supported for chat-strong in /v1/chat/completions … (erreur HTTP 400 : les outils de fonction avec reasoning_effort ne sont pas pris en charge par ce déploiement). Camille appuie quatre fois sur « Réessayer ». Chaque clic envoie une nouvelle requête, et chacune échoue de la même façon.
Cause : l’erreur brute répond à la question du développeur (« qu’est-ce qui a brisé? »), pas à celle de l’agent (« que dois-je faire maintenant? »). Elle ne dit pas qu’une nouvelle tentative est inutile. Elle montre aussi le nom du déploiement, le point de terminaison et un paramètre à quiconque voit l’écran. Une autre erreur, provenant d’une autre couche, pourrait en montrer davantage.
Correction : gardez deux canaux. L’écran reçoit un message provenant d’une liste fixe d’états, avec les actions adaptées à cet état. Le journal reçoit tous les détails, par redact(), avec une référence qui relie les deux. Un rapport de bogue n’a alors besoin que de la référence.
Ce que cela signifie pour un vrai service d’IA : les messages d’erreur font partie du produit et de la frontière de sécurité. Rédigez-les avec soin, pour la personne qui les lit.
Exercice du module
Section intitulée « Exercice du module »Simulez les deux limites de ce module, un flux interrompu et une limite du budget de contexte. Faites en sorte que l’écran les gère.
- Exécutez
python -m assistant.stream T-80005 --cut-after Npour trois valeurs de N : 0, 45 et 89. Notez chaque ligne d’état. La coupure est simulée. - Dans
assistant/screen.py, ajoutezfinal_screen(result, ticket, book). Cette fonction transforme unStreamResulten unScreen. Un flux annulé donnestopped(arrêté). Un flux interrompu ou échoué donneno_draft. Un flux complet passe parvalidate()et donnereadyouneeds_review(à réviser). - Ajoutez
ticket_is_sent(messages). Cette fonction renvoieTruequand les messages à envoyer contiennent encore le tour du billet, le message utilisateur commençant par<ticket>. - Écrivez
tests/test_module_practice.pyavec deux tests. Premier test : coupez le flux T-80005 après chaque nombre possible de fragments et vérifiez queas_completion()refuse chaque résultat. Deuxième test : construisez la conversation de Camille avec des budgets de 3 000 et 1 760 jetons (tokens) estimés. Vérifiez que le petit budget retire le billet.
Afficher un exemple de réponse
L’étape 1 affiche ces lignes d’état. Avec N = 0, failed: Connection lost (simulated). | no answer (échec : connexion perdue, simulée; aucune réponse). Avec N = 45, interrupted: Connection lost (simulated). | not checked: partial text is not data (interrompu : connexion perdue, simulée; non vérifié : le texte partiel n’est pas une donnée), après 45 fragments. Avec N = 89, la même ligne interrupted (interrompu), après 89 des 90 fragments : un seul fragment manquant suffit.
Ajoutez ces importations au début de assistant/screen.py, puis les deux fonctions à la fin :
from .data import Ticketfrom .orders import OrderBookfrom .stream import StreamResult, as_completionfrom .validate import validatedef final_screen(result: StreamResult, ticket: Ticket, book: OrderBook) -> Screen: if result.status == "cancelled": return Screen("stopped", "You stopped the draft. Try again, or write your own reply.", None, f"{ticket.ticket_id} cancelled after {len(result.pieces)} pieces") if result.status in ("interrupted", "failed"): return Screen("no_draft", no_draft(ticket.ticket_id, result.error), None, redact(f"{ticket.ticket_id} {result.status} after {len(result.pieces)} pieces: {result.error}")) verdict = validate(as_completion(result), ticket, book) if verdict.valid: return Screen("ready", "Draft ready. Check it before you send it.", verdict.analysis.reply, f"{ticket.ticket_id} valid") return Screen("needs_review", needs_review(verdict.problems), None, redact(f"{ticket.ticket_id} rejected: " + ", ".join(p.code for p in verdict.problems)))
def ticket_is_sent(messages: list[dict]) -> bool: """True if the ticket text (the turn with the <ticket> tags) is still in the messages to send.""" return any(m["role"] == "user" and m["content"].startswith("<ticket>") for m in messages)"""Module practice: simulate an interrupted stream and a context-budget limit."""
import pytest
from assistant.context import build_requestfrom assistant.data import load_ticketsfrom assistant.history import Conversationfrom assistant.schema import response_formatfrom assistant.screen import ticket_is_sentfrom assistant.stream import CutAfter, as_completion, stream_answer
TICKETS = load_tickets()REQUEST = build_request(TICKETS["T-80005"], "chat-small", "v2", response_format())
def test_a_stream_cut_at_any_point_is_never_data(mock): total = len(stream_answer(mock, REQUEST).pieces) # 90 pieces in the recording for n in range(total): # simulated: the connection breaks after n pieces result = stream_answer(CutAfter(mock, n), REQUEST) assert result.status == ("failed" if n == 0 else "interrupted") with pytest.raises(ValueError, match="Partial text is not data"): as_completion(result)
def recorded_conversation(budget_tokens): first = build_request(TICKETS["T-80005"], "chat-small", "v2") conversation = Conversation(first["messages"][0]["content"], budget_tokens) conversation.add("user", first["messages"][1]["content"]) conversation.add("assistant", '{"reply": "..."}') conversation.add("user", "Make the reply shorter: two sentences.") return conversation
def test_a_small_budget_cuts_the_ticket_first(): roomy, tight = recorded_conversation(3000), recorded_conversation(1760) assert ticket_is_sent(roomy.messages()) and roomy.dropped() == 0 assert not ticket_is_sent(tight.messages()) and tight.dropped() == 1 # only the ticket: the oldest turnpython -m pytest tests/test_module_practice.py affiche 2 passed (2 tests réussis). Dans l’application, appelez ticket_is_sent() avant chaque requête de suivi. Quand le résultat est faux, n’envoyez pas la requête. Montrez plutôt un message clair à l’agent : « Cette conversation est trop longue. Commencez-en une nouvelle au sujet de ce billet. »
Vérification du module
Section intitulée « Vérification du module »Montrez deux choses avec un seul test : chaque fin possible d’un flux donne un seul état final clair, et le texte partiel ne devient jamais un brouillon. Créez tests/test_final_states.py. (La vérification du module 4 utilise déjà le nom test_module_check.py.) Utilisez quatre fins pour le flux T-64704 : complet, annulé après 76 fragments et interrompu après 76 fragments (simulation). La quatrième est un fournisseur qui échoue avant le premier fragment (simulation). Vérifiez aussi qu’aucun message à l’écran ne montre une erreur interne et qu’aucune clé n’atteint le journal.
Afficher un exemple de réponse
"""Module check: one clear final status, and partial text never treated as validated data."""
from assistant.context import build_requestfrom assistant.data import load_ticketsfrom assistant.orders import OrderBookfrom assistant.providers import ProviderErrorfrom assistant.schema import response_formatfrom assistant.screen import final_screenfrom assistant.stream import CutAfter, stream_answer
TICKET = load_tickets()["T-64704"]REQUEST = build_request(TICKET, "chat-small", "v2", response_format())
class Broken: # simulated: the provider fails before the first piece def stream(self, request): raise ProviderError("HTTP 500: upstream error, api-key=sk-test1234567890 (simulated)") yield
def test_every_ending_has_one_clear_state_and_only_a_complete_valid_answer_has_a_draft(mock): count = iter(range(1000)) endings = { "complete": stream_answer(mock, REQUEST), "cancelled": stream_answer(mock, REQUEST, cancel=lambda: next(count) >= 76), "interrupted": stream_answer(CutAfter(mock, 76), REQUEST), "failed": stream_answer(Broken(), REQUEST), } screens = {name: final_screen(result, TICKET, OrderBook()) for name, result in endings.items()} assert {name: s.state for name, s in screens.items()} == { "complete": "ready", "cancelled": "stopped", "interrupted": "no_draft", "failed": "no_draft"} assert screens["complete"].draft.endswith("your safety concern.") assert [s.draft for name, s in screens.items() if name != "complete"] == [None, None, None] for s in screens.values(): # no internals on the screen, no key in the log assert "HTTP" not in s.message and "sk-" not in s.message and "sk-test" not in s.logpython -m pytest tests/test_final_states.py affiche 1 passed (un test réussi). La coupure après 76 fragments est celle du FailureCase de la leçon précédente. Dans ce cas, fermer les crochets donnait une réponse « valide » sans les conseils de sécurité. Ici, cela peut seulement donner stopped (arrêté) ou no_draft (aucun brouillon). À partir de l’instantané end-of-m05, la suite complète, avec les fichiers d’exercices et de vérification de ce module, affiche 68 passed, 1 deselected.
Vous avez terminé le module quand :
python -m pytestréussit, avec vos nouveaux tests.- Vous pouvez expliquer pourquoi Camille a reçu une réponse sur une bouilloire pour son billet concernant un hamac. Vous savez quelle clé évite ce problème.
- Vous pouvez expliquer pourquoi il ne faut ni vérifier ni stocker le flux T-64704 arrêté après 76 fragments. Fermer ses crochets lui fait franchir les deux barrières.
- Vous pouvez dire, pour chaque résultat de l’assistant, ce que voit l’agent, quels boutons sont disponibles et ce qui est journalisé.
Réinitialisation. Si votre projet ne fonctionne plus, renommez-le et copiez l’instantané end-of-m05 en suivant Réinitialiser le projet :
mv ticket-assistant ticket-assistant-oldcp -R nextia-learning-labs/C08/snapshots/end-of-m05 ticket-assistantSous Windows, dans PowerShell : Rename-Item ticket-assistant ticket-assistant-old, puis Copy-Item -Recurse nextia-learning-labs\C08\snapshots\end-of-m05 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 65 passed, 1 deselected. L’instantané contient les étapes guidées des deux premières leçons de ce module. Il ne contient pas assistant/screen.py ni les tests des exercices. 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
Le résultat de la réponse rejetée de Gemma pour T-80002 contient encore un texte de réponse. Pourquoi ne faut-il pas l'afficher à l'écran, même avec un avertissement à côté?
Afficher la réponse
La réponse affirme qu’un remboursement de 500 $ a été approuvé. C’est faux et contraire à la politique de Grace. Une zone de brouillon contenant du texte invite l’agent à l’utiliser : un agent pressé peut modifier un mot et l’envoyer. Les barrières ont rejeté la réponse, donc ce n’est pas un brouillon. L’écran explique la raison en mots simples et laisse l’agent rédiger la réponse. Le texte reste dans le journal pour les développeurs.
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
Quels cas devraient proposer « Réessayer » : un délai dépassé, HTTP 400 de chat-strong, un flux arrêté par l'agent, une réponse rejetée? Expliquez chaque cas.
Afficher la réponse
Le délai dépassé : oui, c’est temporaire, et la même requête peut fonctionner une minute plus tard. HTTP 400 : non, la requête elle-même est incorrecte (ce déploiement ne peut pas utiliser d’outils). Elle échoue donc chaque fois; un développeur doit la corriger. Le flux arrêté : oui, l’agent a choisi de l’arrêter et peut vouloir un nouveau brouillon. La réponse rejetée : non, une nouvelle tentative envoie la même requête et serait très probablement rejetée à nouveau. L’agent rédige la réponse. « Rédiger ma réponse » est proposé dans tous les cas.
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 : un message d'erreur provenant d'une couche fournisseur contient « Authorization : Bearer sk-live-… ». Que fait chaque partie du projet avec ce message?
Afficher la réponse
L’écran ne montre jamais le texte de l’erreur. Il affiche un message d’une liste fixe, comme « L’assistant ne peut pas rédiger de brouillon pour ce billet. Rédigez votre réponse. Référence : T-… ». La ligne du journal passe par redact(), qui remplace le jeton Bearer et la clé par [REDACTED] (masqué). La clé n’apparaît donc à aucun des deux endroits. Le test de vérification du module le prouve aussi avec un api-key=sk-… simulé dans une erreur du fournisseur.
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
- Les résultats bruts de l’assistant s’adressent aux développeurs. Un « HTTP 400 » répété nomme un point de terminaison et un paramètre. Un résultat rejeté contient encore une fausse promesse de remboursement. L’écran ne doit pas les afficher.
- Le panneau de l’assistant a quelques états : En attente et Rédaction en cours (chargement), Prêt, À vérifier (réponses rejetées et refus), Aucun brouillon et Arrêté. Chaque état final a un message, des actions et une ligne de journal.
- Seul l’état Prêt contient un brouillon. Le texte partiel, les réponses rejetées et les erreurs n’atteignent jamais la zone de brouillon.
- Un bon message indique ce qui s’est passé et quoi faire ensuite, avec une référence. Il ne montre jamais d’erreurs brutes, de détails internes ni de clés; ces éléments sont journalisés en passant par
redact(). - Réessayer seulement pour les erreurs temporaires (délais dépassés, 429, 5xx, connexion perdue). Rédiger ma réponse est toujours disponible : le billet n’attend jamais l’assistant.
- L’exercice du module simulait un flux interrompu à chaque point et un budget de jetons qui coupait le billet. La vérification du module a confirmé un seul état final clair pour chaque fin possible.
Vous avez terminé le module 5. Ensuite, gérez les TermeLimite de débitLe maximum qu’un client peut envoyer à un fournisseur pendant une période, par exemple 100 000 jetons et 100 requêtes par minute. Au-delà, le fournisseur répond HTTP 429 (Too Many Requests) et ne fait aucun travail.En savoir plus Gérer les limites du fournisseurCréer des applications fiables avec les API de LLM et les délais dépassés avec des tentatives espacées, qui s’arrêtent à temps.
Leçon suivante : Gérer les limites du fournisseur