Vérifiez le sens de la réponse à partir du billet de soutien, des commandes 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 plusL'IA dans les produits de tous les joursBases d'une carrière en IA : rôles, compétences et feuilles de route et 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.. Ainsi, une réponse en JSON valide mais incorrecte ne peut pas entrer dans la base de données de l’application. Un test le prouve.
Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes
Les règles H1 à H5 de Grace, « Quand une personne doit traiter le billet », dans assistant/prompts/policy.md, vues dans Préciser la tâche. L’injection dans le billet T-80002, vue dans Fournir le contexte en toute sécurité.
Les expressions régulières comme motifs, vues dans Définir un schéma.
Ce qu’il vous faut sur votre ordinateur
~/projects/ticket-assistant après Traiter les résultats incomplets, avec .venv actif : assistant/validate.py contient parse(), et python -m pytest affiche 35 passed, 1 deselected (35 tests réussis, 1 désélectionné).
Un employé de banque reçoit un chèque. Toutes les cases sont remplies : la date, le montant en chiffres et en lettres, le numéro de compte avec le bon nombre de chiffres, une signature. Le formulaire est parfait.
L’employé vérifie quand même trois choses que le formulaire ne peut pas montrer. Ce compte existe-t-il? Appartient-il à la personne qui a signé? Contient-il assez d’argent? Un formulaire parfait avec un numéro de compte inventé reste un mauvais chèque.
Le problème : un JSON valide, une réponse incorrecte
Le billet T-80009 dit : « Ma commande LK-55190 n’est jamais arrivée. C’était un sac de terreau et deux transplantoirs. Pouvez-vous vérifier? » L’identifiant compte cinq chiffres. La vraie commande du client est LK-551906.
Avec une TermeSortie structuréeUne requête qui envoie au fournisseur le schéma de la réponse, pour que le modèle ne puisse écrire qu’une réponse de cette forme. Elle contrôle la forme, pas la vérité : l’application vérifie toujours chaque réponse.En savoir plusDéfinir un schémaCréer des applications fiables avec les API de LLM stricte, le schéma permettait seulement six chiffres. Les TermeModèle hébergéUn modèle qui fonctionne sur les ordinateurs d’un fournisseur. Votre code lui envoie des requêtes par Internet, et vous payez chaque appel.En savoir plusModèle hébergé ou autohébergéLes LLM : comment ils fonctionnent et comment en choisir un n’ont donc pas copié l’identifiant. Ils l’ont complété, chacun avec un chiffre inventé différent (tous enregistrés sur Azure) :
Modèle et requête
order_id dans la réponse
confidence (confiance)
gpt-6-luna (chat-small), requête v1
LK-551904
high (élevée)
gpt-6-luna (chat-small), requête v2
LK-551902
high
gpt-6.1-sol (chat-strong), requête v1
LK-551909
high
gpt-6.1-sol (chat-strong), requête v2
null
high
Aucun de ces identifiants ne figure dans le billet ni dans les commandes de Larkfield. La réponse de 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 plusLes éléments d'une requêteCréer des applications fiables avec les API de LLM avec la requête v1 se contredit même. Son order_id est LK-551909, mais sa réponse dit « Je suis désolé que votre commande LK-55190 ne soit pas arrivée ». Seul chat-strong avec la requête v2 a fait le choix honnête de laisser la commande vide.
Sans schéma, trois des quatre mêmes essais ont copié LK-55190 tel quel. La barrière 1 l’a rejeté à cause de son motif. Avec le schéma, les mêmes modèles ont écrit une réponse qui passe la barrière 1. La contrainte a transformé une copie honnête en une invention pleine d’assurance. Chaque champ a le bon type, chaque valeur d’énumération est permise et le motif correspond. La réponse reste incorrecte.
Le schéma est le formulaire. La barrière 2 est l’employé de banque. Elle compare la réponse à des éléments que le schéma ne peut pas connaître : le texte du billet, les données de Larkfield et la politique de Grace. Chaque vérification est une règle d’affaires. Cette règle vient du fonctionnement de l’entreprise, pas de la forme du JSON.
Deux sortes de règles d’affaires comptent surtout ici :
Identifiants cités. Une réponse qui nomme une commande, un client ou un produit fait référence à quelque chose d’extérieur. La règle pose ces questions : est-ce que cet élément existe, et est-il permis ici? Un identifiant de commande doit figurer dans le billet et exister dans la table des commandes. Il doit aussi appartenir au client qui a écrit le billet.
Règles de politique. Quand le billet mentionne une brûlure, needs_human doit être vrai (règle TermeRègles H1 à H5Les règles de la politique de support de Grace qui disent quand une personne doit prendre un billet en charge : H1 sécurité ou blessure, H2 menace juridique, H3 demande hors de la politique, H4 commande d’un autre client ou instructions à l’assistant, H5 aucune demande lisible.En savoir plusPréciser la tâcheCréer des applications fiables avec les API de LLM), peu importe la décision du modèle. Une réponse ne doit jamais dire qu’un remboursement a été fait, car l’assistant ne peut pas en faire.
La barrière 2 s’exécute seulement sur les réponses qui ont passé la barrière 1. Chaque règle peut donc se fier aux types. analysis.order.order_id est toujours du texte avec six chiffres.
Voici les règles de commande, appliquées à la main à la réponse de 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 plusCe qu'est une API de LLMCréer des applications fiables avec les API de LLM au billet T-80009 (requête v2). Chaque valeur vient de l’enregistrement, du billet et de data/orders.sqlite :
Règle
La réponse dit
Les données disent
Résultat
L’identifiant de commande figure dans le billet
LK-551902
le texte du billet contient seulement LK-55190
échoue : order_not_in_ticket
La commande existe et appartient au client
LK-551902
cette commande n’existe pas; le client C-88930 a LK-551906
non vérifiée : la première règle a déjà échoué
Un état vient seulement d’une recherche
status: null
aucune recherche n’a été faite
réussit
Le billet mentionne la sécurité, une action en justice ou des instructions
needs_human: false
aucun de ces mots dans le billet
réussit
La langue correspond à celle du billet
en
anglais
réussit
La réponse affirme qu’un remboursement a été fait ou demande un mot de passe
« …veuillez nous indiquer quand vous avez passé la commande… »
aucune expression de ce genre
réussit
Une règle échoue, donc la réponse est rejetée avec un problème. La vérification demande quelques comparaisons de chaînes et une requête à la base de données. Elle coûte peu, donne le même résultat chaque fois et explique son résultat.
La barrière 2 du projet est la fonction check_rules(). Elle renvoie une liste de problèmes, vide quand toutes les règles réussissent :
Code de problème
La règle
Son origine
order_not_in_ticket
L’identifiant de commande doit figurer dans le texte du billet.
La description du schéma : « exactement comme le billet l’écrit »
order_not_customers
Une commande inexistante, ou appartenant à un autre client, exige needs_human: true.
Règle de politique H4 (accès)
status_not_from_lookup
L’état d’une commande doit venir d’une recherche de commande, jamais du billet.
Règle de soutien : l’assistant énonce seulement les faits qu’il possède
safety_needs_human
Des mots liés à la sécurité dans le billet exigent needs_human: true.
H1
legal_needs_human
Des mots liés au droit exigent needs_human: true.
H2
injection_needs_human
Des instructions à l’assistant dans le billet exigent needs_human: true.
H4
empty_needs_human
Un billet vide exige needs_human: true.
H5
wrong_language
language doit correspondre au billet : français avec au moins trois mots français courants, sinon anglais.
La réponse est envoyée dans la langue du client
claims_action
La réponse ne doit pas dire qu’un remboursement, une annulation ou un changement a été fait.
Règle de soutien 8 : l’assistant rédige seulement des brouillons
asks_secret
La réponse ne doit pas demander un mot de passe ou un numéro de carte.
Règles de soutien
Certaines règles cherchent des mots avec des expressions régulières. TermeAmiraLa développeuse de Larkfield. Elle écrit le code des outils du service d’assistance dans les cours. a tiré deux leçons concrètes en les écrivant. Sa première liste de mots liés à la sécurité interprétait « burner » (brûleur) comme une brûlure. Un billet sur un brûleur de barbecue défectueux devenait un problème de sécurité : une fausse alerte. Son premier motif de remboursement détectait « vérifier si le remboursement a été envoyé », une question, pas une affirmation. Les deux motifs sont maintenant corrigés, mais la leçon reste : les règles basées sur les mots sont un filet de sécurité, pas un juge. Elles repèrent les cas clairs à peu de frais. Elles ne comprennent pas le billet.
Les règles de commande ont besoin des données de Larkfield, accessibles par une petite classe, OrderBook. Elle ouvre data/orders.sqlite en lecture seule. SQLite est une base de données dans un seul fichier. Le module sqlite3 de Python peut l’ouvrir dans un mode qui refuse tout changement. La validation lit les commandes. Elle ne doit jamais pouvoir en modifier une.
Le schéma contient un champ confidence : « low » (faible), « medium » (moyenne) ou « high » (élevée). TermeTomásL’ingénieur en IA de Larkfield. Il propose souvent un raccourci rapide, et la leçon montre pourquoi il échoue. demande : « Pourquoi ne pas enregistrer seulement les réponses avec une confiance élevée? » Les enregistrements lui répondent. Ce tableau compte les réponses aux 69 billets avec la requête v2 et une sortie structurée. « Correcte » signifie que la réponse a passé les deux barrières. Les valeurs de team et de needs_human correspondent aussi à la réponse attendue :
Modèle
Confiance
Correcte
Incorrecte
gpt-6-luna (chat-small)
high
62
3
gpt-6-luna (chat-small)
medium
2
1
gpt-6-luna (chat-small)
low
1
0
Gemma 3 4B
high
24
38
Gemma 3 4B
medium
0
5
Gemma a indiqué « high » pour 62 réponses, dont 38 étaient incorrectes. chat-small a indiqué « high » pour l’identifiant inventé LK-551902. Le champ de confiance contient les propres mots du modèle, pas une probabilité mesurée. C’est un autre champ que le modèle écrit, et il peut être aussi incorrect que les autres.
Utilisez-le donc seulement comme indice pour les personnes. Un service de soutien peut signaler les réponses « low » et « medium » pour que l’agent les lise plus attentivement. Ne l’utilisez jamais pour sauter une vérification. Ne laissez jamais « high » décider de ce qui entre dans la base de données.
Exemple détaillé : les deux barrières sur de vraies réponses
Voici maintenant tout le pipeline. L’image est celle de Définir un schéma, et vous en avez maintenant toutes les parties.
La génération, deux barrières et la base de données. Seule une réponse qui passe les deux barrières est enregistrée; toute autre réponse sort avec un code de problème.Description textuelle du diagramme
De gauche à droite : un billet et la génération, où le modèle écrit une réponse. Ensuite, la barrière 1, la forme, et la barrière 2, le sens, avec les règles d’affaires et la table des commandes. Enfin, la base de données, qui contient seulement des résultats valides. Une erreur du fournisseur sort avec failed. La barrière 1 rejette avec refusal, truncated, malformed_json ou schema. La barrière 2 rejette avec des codes comme order_not_in_ticket et safety_needs_human. Après chaque sortie, un agent traite le billet sans brouillon et rien n’est enregistré.
Après les étapes ci-dessous, la commande analyse exécute les deux barrières. Pour T-80009 avec chat-small, la réponse passe la barrière 1 et s’arrête à la barrière 2 :
Fenêtre de terminal
python-massistantanalyseT-80009
La sortie :
{
"ticket_id": "T-80009",
"status": "rejected",
"analysis": {
"language": "en",
"team": "delivery",
"needs_human": false,
"reason": "The order has not arrived, so this is a delivery issue.",
"confidence": "high",
"order": {
"order_id": "LK-551902",
"status": null
},
"reply": "I’m sorry your order hasn’t arrived. Standard delivery in Canada takes 3 to 5 business days; please let us know when you placed the order so our delivery team can look into it."
},
"problems": [
{
"code": "order_not_in_ticket",
"message": "LK-551902 does not appear in the ticket."
}
],
"error": ""
}
rejected | chat-small | prompt v2 | 1 call(s) | 1880 tokens in, 434 out
status (état) prend l’une de trois valeurs. valid (valide) : les deux barrières ont été franchies. rejected (rejeté) : une barrière a arrêté la réponse, et problems (problèmes) indique pourquoi. failed (échec) : le fournisseur n’a donné aucune réponse. La dernière ligne répète l’état pour qu’un script puisse le lire.
"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."
}
],
"error": ""
}
rejected | gemma3:4b | prompt v2 | 1 call(s) | 1776 tokens in, 124 out
Chaque champ de cette réponse est valide. Deux règles d’affaires l’arrêtent. C’est ce que promettait Fournir le contexte en toute sécurité : les délimiteurs réduisent le risque, et la validation assure la défense.
Faites d’abord une prédiction. Vous avez vu certaines de ces réponses; la barrière qui les arrête pourrait vous surprendre.
Explorer
Barrière 1, barrière 2 ou enregistrement?
Prédisez d’abord : Pour chaque réponse : la barrière 1 l'arrête-t-elle (la forme), la barrière 2 l'arrête-t-elle (le sens), ou passe-t-elle les deux pour entrer dans la base de données? Demandez-vous ensuite : la réponse enregistrée est-elle correcte?
Qu’est-ce qui bloque cette réponse avant la base de données?
Prédites
0 sur 10
Justes
0 sur 0
Réponse 1 sur 10 : Qu’est-ce qui bloque cette réponse avant la base de données?
Réponse 1 sur 10 : T-80009 avec une sortie structurée stricte
gpt-6-luna (chat-small) sur Azure
Billet T-80009
My order LK-55190 never arrived. It was a bag of potting soil and two trowels. Can you check?
Requête
Requête v2, sortie structurée (stricte)
Réponse
finish_reason stop, 434 jetons de sortie, dont 329 de raisonnement caché
Texte de la réponse
{"language":"en","team":"delivery","needs_human":false,"reason":"The order has not arrived, so this is a delivery issue.","confidence":"high","order":{"order_id":"LK-551902","status":null},"reply":"I’m sorry your order hasn’t arrived. Standard delivery in Canada takes 3 to 5 business days; please let us know when you placed the …
La barrière 2 (le sens) la bloque :order_not_in_ticket
LK-551902 does not appear in the ticket.
La sortie structurée stricte a imposé 6 chiffres, donc le modèle a inventé le dernier : LK-551902. C’est valide selon le schéma. Seule la règle d’affaires voit que le billet ne dit jamais LK-551902.
Décision : Demandez à une personne. N’enregistrez rien : l’agent traite le billet sans ce brouillon.
De vraies réponses de gpt-6-luna (chat-small) et de gpt-6.1-sol (chat-strong) sur Azure, et de Gemma 3 4B avec Ollama, ainsi qu'un refus construit. Chaque résultat vient de validate() du projet.Description textuelle de l’explorateur
L’explorateur montre une réponse à la fois, avec le billet, la requête, finish_reason (raison de fin) et le texte de la réponse. Vous choisissez la barrière 1, la barrière 2 ou « passe les deux : enregistrée ». Le vrai résultat du projet s’affiche ensuite : les codes et messages de problème, la raison et la décision.
Les dix réponses et leurs résultats. Un : chat-small, T-80009 avec une sortie structurée stricte, order_id LK-551902 : barrière 2, order_not_in_ticket. Deux : chat-small, T-80008 : passe les deux barrières et est enregistrée. Trois : Gemma sans schéma, T-80008 : barrière 1, malformed_json. Quatre : Gemma, T-80002, l’injection : barrière 2, injection_needs_human et claims_action. Cinq : chat-small, T-64849, l’échelle en solde : passe les deux barrières et est enregistrée, mais l’équipe est incorrecte : warranty (garantie) au lieu de returns (retours). Six : Gemma, T-65190, commande LK-104233 copiée de l’exemple de la requête : barrière 2, order_not_in_ticket. Sept : chat-small, T-64765 avec une limite de 60 jetons : barrière 1, truncated. Huit : chat-strong, T-80009 avec la requête v1, LK-551909 : barrière 2, order_not_in_ticket. Neuf : Gemma, T-64722, order_id « null » : barrière 1, schema. Dix : le refus construit : barrière 1, refusal.
Revenez à l’échelle en solde, T-64849. Le client a utilisé l’échelle deux fois, elle était instable, et il demande s’il peut encore la retourner. L’équipe attendue est returns; chat-small a choisi warranty, avec confidence: "medium". La réponse passe les deux barrières. Aucune règle ne peut connaître la bonne équipe : c’est un jugement, et les barrières vérifient des faits. L’agent qui lit le brouillon et l’évaluation de Comparer des requêtes repèrent ce genre d’erreur. Les barrières bloquent les réponses impossibles et interdites. Elles ne rendent pas les autres correctes.
Construction : la barrière 2, les commandes et la base de données
Travaillez dans ~/projects/ticket-assistant, avec .venv actif.
Créez le fichier assistant/orders.py, qui donne accès aux commandes en lecture seule :
assistant/orders.py
"""Read-only access to Larkfield's orders (data/orders.sqlite, synthetic)."""
import sqlite3
from pathlib import Path
from .data importORDERS_DB
classOrderBook:
"""Read-only access to orders.sqlite."""
def__init__(self, path: Path =ORDERS_DB):
self.path = path
deffind(self, order_id: str) -> dict|None:
con = sqlite3.connect(f"file:{self.path}?mode=ro",uri=True) # read-only connection
con.row_factory = sqlite3.Row
try:
order = con.execute("SELECT * FROM orders WHERE order_id = ?", (order_id,)).fetchone()
if order isNone:
returnNone
items = con.execute("SELECT product, quantity, unit_price FROM order_items WHERE order_id = ? "
"ORDER BY product", (order_id,)).fetchall()
finally:
con.close()
return {**dict(order), "items": [dict(i) for i in items]}
mode=ro ouvre le fichier en lecture seule. Les paramètres ? transmettent l’identifiant de commande comme donnée, jamais comme partie du texte SQL.
Ouvrez assistant/validate.py. Remplacez tout ce qui précède @dataclass (la chaîne de documentation et les importations) par ce bloc. Il ajoute trois importations et les motifs de mots :
assistant/validate.py (top of the file)
"""Two gates between the model and the database: the schema, then the business rules."""
# Asking for a password or a card number. "Don't send us your password" is fine: check what comes before.
ASKS_SECRET= re.compile(r"(?i)(?<!n't )(?<!n’t )(?<!not )(?<!never )\b(send|tell|give) us your (password|full card "
r"number|card number)\b")
(?<!whether ) est une assertion arrière (look-behind) : le motif correspond seulement si « whether » suivi d’une espace ne le précède pas immédiatement. C’est la correction pour « check whether the refund has been sent » (vérifier si le remboursement a été envoyé).
Gardez Problem, Verdict et parse() tels quels, deux lignes vides sous ce bloc.
À la fin de assistant/validate.py, ajoutez la barrière 2 et une fonction qui exécute les deux barrières :
ticket.customer_id vient du service de soutien, pas du modèle : le modèle ne le voit jamais. looked_up contient les résultats des recherches de commandes; il reste vide jusqu’à l’ajout de l’outil de recherche au module 4.
Créez le fichier assistant/store.py, la base de données de l’application. C’est le seul code qui écrit un résultat :
assistant/store.py
"""The application database: only checked results go in."""
return con.execute("SELECT COUNT(*) FROM results").fetchone()[0]
La vérification se trouve dans save(), pas seulement dans le code qui l’appelle. Un futur appelant qui oublie les barrières ne peut toujours pas enregistrer un résultat invalide : il reçoit explicitement InvalidResult. Le fichier results.sqlite figure déjà dans .gitignore.
Créez le fichier assistant/analyse.py. Il fait passer un billet dans tout le pipeline :
assistant/analyse.py
"""One ticket in, one checked result out."""
from dataclasses import asdict, dataclass, field
from .context import build_request
from .data import Ticket
from .orders import OrderBook
from .providers import Completion, ProviderError
from .schema import TicketAnalysis, response_format
from .validate import Problem, validate
@dataclass
classOutcome:
ticket_id: str
status: str# "valid", "rejected" (a gate stopped it) or "failed" (no answer)
completions est une liste parce que le module 4 fait plusieurs appels pour certains billets.
Remplacez assistant/__main__.py par cette version. analyse utilise maintenant analyse_ticket, affiche le résultat et enregistre un résultat valide avec --save. Les deux commandes reçoivent --no-schema :
assistant/__main__.py
"""Command line.
python -m assistant analyse T-80008 one ticket: schema and checks, prompt v2
python -m assistant analyse T-80008 --save ... and save it to results.sqlite if it is valid
answer = outcome.analysis.model_dump() if outcome.analysis elseNone
note =", ".join(p.code for p in outcome.problems) or ("failed"if outcome.status =="failed"else"")
rows.append(Row(ticket, answer, note))
print(json.dumps(score(rows),indent=2))
print("\n".join(misses(rows)))
return0
if __name__ =="__main__":
sys.exit(main())
analyse renvoie maintenant 1 quand le résultat n’est pas valide, pour qu’un script puisse voir la différence. Avec --save, un appel échoué devient aussi un problème, donc save() le refuse.
Le test du premier essai du module 1 cherche finish_reason: stop dans la sortie, et analyse ne l’affiche plus. Dans tests/test_first_run.py, remplacez la dernière ligne du premier test par :
tests/test_first_run.py
assert'"team": "delivery"'in out and out.strip().splitlines()[-1].startswith("valid | chat-small")
Remplacez tests/test_validate.py par la version complète. La première partie est identique à celle de la dernière leçon; les nouveaux tests utilisent de vrais enregistrements pour les deux barrières :
tests/test_validate.py
"""Every answer here is a real recorded response, except the refusal (constructed: see recordings/constructed.jsonl)."""
import json
import pytest
from assistant.context import build_request
from assistant.data import Ticket
from assistant.orders import OrderBook
from assistant.providers import Completion
from assistant.schema import TicketAnalysis, response_format
from assistant.validate import check_rules, parse, validate
assert[p.code for p in verdict.problems]==["injection_needs_human", "claims_action"]
test_business_rules utilise des réponses écrites à la main (answer(...)), une règle par ligne, sur de vrais billets. test_schema_valid_is_not_the_same_as_correct consigne volontairement une limite : il réussit parce que les barrières laissent passer une équipe incorrecte.
Créez le fichier tests/test_store.py. Ce test prouve la promesse du module :
tests/test_store.py
import pytest
from assistant.schema import TicketAnalysis
from assistant.store import InvalidResult, ResultStore
for verdict in (Verdict(None,[Problem("malformed_json","x")]), Verdict(GOOD,[Problem("promise","x")])):
with pytest.raises(InvalidResult):
store.save("T-1", verdict,"v2","chat-small")
assert store.count() ==0
deftest_a_valid_result_is_saved(tmp_path):
store =ResultStore(tmp_path /"results.sqlite")
store.save("T-1",Verdict(GOOD),"v2","chat-small")
assert store.count() ==1
tmp_path est une fixture pytest (ressource de test) : un nouveau dossier vide pour chaque test. Ainsi, les tests ne touchent jamais à votre vrai results.sqlite.
Exécutez les tests :
Fenêtre de terminal
python-mpytest
La sortie se termine par :
42 passed, 1 deselected in 0.38s
Exécutez les deux exemples détaillés ci-dessus : analyse T-80009 et analyse T-80002 --model gemma3:4b. Les deux se terminent par rejected (rejeté).
À vous de jouer : l’évaluation, maintenant avec les deux barrières
Relancez l’évaluation du module 2. Elle utilise maintenant une sortie structurée et les deux barrières. Ses notes (notes) comptent les codes de problème :
Fenêtre de terminal
python-massistanteval--promptv2
Faites d’abord une prédiction : quel code apparaît dans les notes pour chat-small? Ensuite, lancez-la avec --model gemma3:4b et comparez.
Afficher la réponse
Pour chat-small, la sortie commence par :
{
"tickets": 69,
"no_answer": 0,
"team_correct": 67,
"team_scored": 68,
"needs_human_agree": 67,
"needs_human_true_caught": "9/9",
"notes": {
"order_not_in_ticket": 1
}
}
La seule note concerne T-80009, le chiffre inventé. Pour Gemma 3 4B, les notes sont :
"notes": {
"schema": 2,
"order_not_in_ticket": 2,
"claims_action": 1,
"injection_needs_human, claims_action": 1
}
schema (schéma) 2 correspond au motif qu’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 plusModèle hébergé ou autohébergéLes LLM : comment ils fonctionnent et comment en choisir un n’a pas imposé (T-64722 et T-80009). order_not_in_ticket (commande absente du billet) 2 correspond à LK-104233, copié de l’exemple de la requête dans T-64765 et T-65190. claims_action (affirme avoir effectué une action) 1 correspond à T-65288, une question sur un montant débité sur une carte. Gemma a répondu « Nous avons remboursé le montant incorrect de 7,01 $ » : l’assistant ne peut pas faire cela. La ligne injection_needs_human, claims_action correspond à T-80002 (injection exigeant une personne et affirmation d’action). Gemma n’a aussi donné la bonne valeur de needs_human (intervention humaine nécessaire) que pour 30 des 69 billets de soutien. Aucune barrière ne peut corriger cela : il disait « une personne doit s’en occuper » presque partout.
Erreur courante
La réponse respecte le schéma, donc elle est correcte
Symptôme : Tomás examine les journaux (logs) de T-80009. « La réponse est valide : sortie structurée en mode strict, chaque champ vérifié par Pydantic, confiance élevée. Pourquoi l’avoir rejetée? Enregistrez les réponses qui respectent le schéma et retirez la barrière 2 : elle crée seulement du travail. » Sans la barrière 2, results.sqlite reçoit une ligne avec la commande LK-551902 pour T-80009. L’agent voit un identifiant de commande qui semble réel. Une recherche au module 4 ne trouverait pas cette commande. Pire encore, elle pourrait trouver la commande d’un autre client portant ce numéro.
Cause : le schéma vérifie la forme, pas les faits. Le mode strict imposait six chiffres, alors le modèle en a inventé un : LK-551902 (chat-small), LK-551909 (chat-strong). Pydantic l’accepte, car il correspond à ^LK-\d{6}$. La valeur confidence: "high" (confiance élevée) du modèle n’est qu’un autre champ qu’il a écrit.
Correction : gardez la barrière 2. Vérifiez chaque identifiant écrit par le modèle à l’aide d’une source fiable : ici, le texte du billet et la table des commandes. order_not_in_ticket bloque les trois identifiants inventés. Gardez le test test_a_schema_valid_order_id_that_the_ticket_does_not_contain_is_rejected, pour que personne ne retire la règle par accident. Expliquez ensuite la limite à Tomás : T-64849 franchit les deux barrières avec la mauvaise équipe. Valide signifie « permis », pas « correct ».
Grace veut une preuve que les barrières bloquent des réponses absentes des enregistrements. Écrivez des sorties simulées : des réponses écrites à la main, dans le format de réponse du fournisseur. Montrez ensuite que les barrières rejettent les catégories de billets impossibles et les références manquantes.
Créez tests/test_practice.py. Écrivez une fonction utilitaire qui transforme un dictionnaire en Completion, comme la simulation, avec Completion.from_response.
Partez d’une réponse valide pour le billet T-80008 (commande LK-581106), et vérifiez qu’elle franchit les deux barrières.
Ajoutez des tests pour les catégories impossibles : les équipes "shipping", "Delivery" (D majuscule), "delivery, returns" et "". Prédisez d’abord le code de problème.
Ajoutez des tests pour les références manquantes. Utilisez un identifiant de commande absent du billet (LK-551906). Utilisez ensuite une commande inexistante (LK-999999), dans un billet écrit à la main qui la mentionne.
Montrez qu’aucune de ces réponses n’atteint un ResultStore.
Dans le fichier, indiquez que les sorties simulées sont écrites à la main, comme le cours le fait pour son refus construit.
Afficher un exemple de réponse
tests/test_practice.py
"""Module practice: impossible categories and missing references, in hand-written mock outputs."""
import json
import pytest
from assistant.data import Ticket, load_ticket
from assistant.orders import OrderBook
from assistant.providers import Completion
from assistant.store import InvalidResult, ResultStore
from assistant.validate import validate
BOOK=OrderBook()
TICKET=load_ticket("T-80008") # "where is my order LK-581106?"
defmock_output(**change) -> Completion:
"""A hand-written answer in the provider's response shape (not a model output)."""
answer = {"language": "en", "team": "delivery", "needs_human": False, "reason": "A late order.",
python -m pytest tests/test_practice.py donne 8 passed (8 tests réussis) : le test d’équipe compte quatre fois. Une équipe impossible n’atteint jamais la barrière 2. La barrière 1 la bloque avec schema, car l’énumération permet seulement cinq noms exacts. La commande LK-999999 n’existe nulle part, donc order_not_customers (commande n’appartenant pas au client) demande une personne. La valeur false de needs_human n’est pas permise pour une commande que le service de soutien ne peut associer au client.
L’instantané end-of-m03 ne contient pas ce fichier. Avec ce fichier, python -m pytest donne 50 passed, 1 deselected (50 tests réussis, 1 désélectionné).
============================== 2 passed in 0.08s ===============================
Essayez d’enregistrer un résultat valide et un résultat invalide :
Fenêtre de terminal
python-massistantanalyseT-80008--save
python-massistantanalyseT-80009--save
Le premier se termine par Saved to results.sqlite. (enregistré dans results.sqlite). Le second se termine par :
T-80009 was not saved: order_not_in_ticket
Le rejet n’est pas silencieux : la commande affiche la raison et retourne 1.
Regardez dans la base de données :
Fenêtre de terminal
python-c"import sqlite3; print(sqlite3.connect('results.sqlite').execute('SELECT ticket_id, team, needs_human, order_id, model FROM results').fetchall())"
Une ligne : le résultat valide. T-80009 n’y est pas.
Expliquez ensuite, en deux ou trois phrases, pourquoi la vérification se trouve dans ResultStore.save() et pas seulement dans analyse. Comparez votre réponse avec l’explication de l’étape 4 de la construction.
Réinitialisation. Si votre projet ne donne pas 42 passed, 1 deselected (42 tests réussis, 1 désélectionné), copiez l’instantané end-of-m03. Suivez les étapes de Réinitialiser le projet : renommez votre dossier, copiez l’instantané dans ~/projects/ticket-assistant, créez le .venv, puis lancez python -m pytest. Le module 4 commence à partir de cet instantané. Supprimez results.sqlite quand vous voulez une base de données vide : le prochain --save la recrée.
Vérifiez votre compréhension
chat-small a écrit l'identifiant de commande LK-551902 pour le billet T-80009, avec une sortie structurée stricte et une confiance « high » (élevée). Quelle barrière le bloque, et pourquoi l'autre ne le peut-elle pas?
Afficher la réponse
La barrière 2, avec order_not_in_ticket : le billet contient seulement LK-55190. La barrière 1 ne peut pas le bloquer. La réponse est du JSON complet, chaque champ contient une valeur permise, et LK-551902 correspond au motif ^LK-\d6$. Seule une vérification du texte du billet (et de la table des commandes) peut détecter l’identifiant inventé. La confiance « high » ne change rien : le modèle a aussi écrit ce champ.
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
Tomás veut enregistrer seulement les réponses avec une confiance « high » et leur faire éviter la barrière 2. Que montrent les enregistrements?
Afficher la réponse
Avec la requête v2 et la sortie structurée, Gemma 3 4B a indiqué « high » pour 62 réponses. Parmi elles, 38 étaient incorrectes. chat-small a indiqué « high » pour l’identifiant de commande inventé de T-80009. La confiance est une affirmation du modèle, pas une probabilité mesurée. Utilisez-la tout au plus comme indice pour l’agent; jamais pour éviter une vérification.
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 ResultStore.save() vérifie-t-il lui-même le verdict, alors qu'analyse_ticket() a déjà exécuté les deux barrières?
Afficher la réponse
Parce que le stockage est le seul endroit qui écrit les résultats. Un futur appelant (un traitement par lots, une nouvelle commande, un module ultérieur) pourrait oublier d’exécuter les barrières. Il pourrait aussi construire un Verdict à la main. Avec la vérification dans save(), tous les chemins vers la base de données suivent la même règle. Une erreur déclenche InvalidResult au lieu d’enregistrer silencieusement. test_store.py le prouve pour un échec à la barrière 1 et un échec à la barrière 2.
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
Du JSON valide peut quand même être incorrect. Avec une sortie structurée stricte, les modèles ont inventé le dernier chiffre de LK-55190. Les réponses respectaient le schéma.
Les règles d’affaires vérifient le sens à partir d’éléments inconnus du schéma : texte du billet, table des commandes et politique de Grace. check_rules() est la barrière 2.
Vérifiez chaque identifiant mentionné : il doit apparaître dans le billet, exister et appartenir au client.
La confiance du modèle est un autre champ qu’il a écrit : Gemma a indiqué « high » pour 38 réponses incorrectes.
ResultStore.save() refuse tout résultat qui n’a pas franchi les deux barrières, et tests/test_store.py le prouve. Valide signifie « permis », pas « correct » : T-64849 franchit les deux barrières avec la mauvaise équipe.
Le projet correspond maintenant à l’instantané end-of-m03, avec 42 tests. Ensuite, le modèle reçoit un outil : la recherche de commandes en lecture seule. Le modèle écrit aussi ses arguments, qui exigent donc les mêmes précautions.