La frontière de l'application
- Leçon 4
- 50 min
- Module 1 : Commencer par une seule requête au modèle
Leçon 4 sur 19 · Créer des applications fiables avec les API de LLMLa frontière de l'application
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
Regroupez tout le code qui communique avec un fournisseur de modèles au même endroit. Gardez la clé API hors du code et du navigateur. Transformez chaque échec du fournisseur en message clair. Vous pourrez ensuite changer de fournisseur sans toucher au reste de l’assistant.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les parties d’une requête et le rôle de la clé, vus dans Les éléments d’une requête.
- Les classes de données et les exceptions en Python, vues dans Les enregistrements structurés et Exceptions et journalisation. Les variables d’environnement et les secrets, vus dans Configuration et secrets. Ces trois leçons font partie de Python pour l’ingénierie de l’IA en pratique.
Ce qu’il vous faut sur votre ordinateur
- Le projet
ticket-assistantavecassistant/raw.pyde la dernière leçon,.venvactif etpython -m pytestqui donne2 passed(2 tests réussis). - Le dossier de laboratoires
~/projects/nextia-learning-labsde Un premier essai sans compte. Vous y copierez trois fichiers de tests. - Facultatif, pour l’exercice du module : Ollama et
gemma3:4b, comme dans la dernière leçon. Sans eux, vous verrez quand même le traitement d’un échec. C’est le but.
Vous n’avez besoin ni de compte ni de clé.
Un adaptateur de voyage
Section intitulée « Un adaptateur de voyage »Quand vous voyagez dans un autre pays, le chargeur de votre téléphone ne convient pas à la prise murale. Vous ne reconstruisez pas le chargeur. Vous placez un petit adaptateur de voyage entre la fiche et la prise. Le téléphone et le chargeur restent les mêmes dans chaque pays. Seul l’adaptateur connaît la prise locale.
La même idée protège la clé de votre maison. Vous la gardez dans votre poche, pas collée sur la porte d’entrée. Toute personne qui accède à la porte peut utiliser une clé collée dessus.
Le problème : du code fournisseur partout et une clé au mauvais endroit
Section intitulée « Le problème : du code fournisseur partout et une clé au mauvais endroit »Imaginez l’assistant sans frontière. Le code du service de soutien importe le SDK du fournisseur à cinq endroits. Chaque endroit lit la clé à sa façon. Puis TermeLarkfieldUne boutique en ligne d’articles pour la maison. Ce n’est pas une vraie entreprise. Tous les cours Nextia utilisent son service d’assistance et ses données comme exemple.En savoir plus L'IA dans les produits de tous les joursBases d'une carrière en IA : rôles, compétences et feuilles de route passe à un autre fournisseur, ou à un modèle local pour les tests. TermeAmiraLa développeuse de Larkfield. Elle écrit le code des outils du service d’assistance dans les cours. doit trouver et modifier chaque endroit, puis les tester à nouveau.
TermeTomásL’ingénieur en IA de Larkfield. Il propose souvent un raccourci rapide, et la leçon montre pourquoi il échoue. propose une solution plus rapide. « Pourquoi nous faut-il un serveur? L’application de soutien tourne dans le navigateur de l’agent. Mettons la clé API dans le JavaScript de la page et appelons le fournisseur à partir de là. » Cela fonctionne dans une démonstration. La section « Le raccourci de Tomás » ci-dessous montre ce que n’importe qui peut alors faire avec la clé.
Il y a un troisième problème : le fournisseur échoue parfois. Il est lent, refuse une clé ou reste inaccessible. Sans plan, chaque échec affiche une longue trace d’erreur Python au milieu de l’écran de l’agent.
L’idée : une frontière, trois parties
Section intitulée « L’idée : une frontière, trois parties »La frontière de l’application sépare votre application de quelque chose que vous ne contrôlez pas, ici le fournisseur. Derrière cette frontière, le code se divise en trois parties. Chacune a un seul rôle :
- La logique d’affaires lit le billet de soutien, construit la requête et utilise la réponse. Elle ne connaît rien aux fournisseurs.
- L’adaptateur est le seul code qui communique avec un fournisseur. Comme l’adaptateur de voyage, il transforme un appel standard,
complete(request), en appel propre au fournisseur. Il transforme aussi les erreurs du fournisseur en erreurs propres à l’assistant. - La configuration précise le fournisseur, le modèle, l’adresse, la clé et le temps d’attente. Elle vient des TermeVariable d’environnementUne valeur nommée que le système donne à un programme au démarrage, par exemple une clé d’API. La valeur reste hors du code et hors de Git.En savoir plus Configuration et secretsPython pour l'ingénierie de l'IA en pratique, pas du code.
Description textuelle du diagramme
Une zone en pointillés délimite le serveur de Larkfield, qui est digne de confiance. Elle contient quatre parties. La logique d’affaires, dans main.py et context.py, lit le billet, construit la requête et utilise la réponse. Elle transmet la requête, un dictionnaire Python, à l’adaptateur dans providers.py. L’adaptateur contient MockProvider et OpenAICompatibleProvider. C’est le seul code qui appelle un fournisseur, et il transforme les erreurs du fournisseur en ProviderError. La configuration comprend config.py et le fichier .env, qui n’est jamais inclus dans un commit (enregistrement de modifications). Elle contient le fournisseur, le modèle, l’URL de base, la clé API et le délai d’expiration. Elle donne la clé uniquement à l’adaptateur. Pour la simulation, l’adaptateur lit les enregistrements. Hors de la zone, à gauche, l’application de soutien tourne dans le navigateur de l’agent et n’a aucune clé. Elle envoie le billet au serveur. À droite, l’adaptateur envoie des requêtes HTTPS avec la clé au fournisseur : Azure, OpenAI ou Ollama sur votre ordinateur.
Pourquoi cette séparation aide : chaque changement a un seul endroit où aller. Un nouveau fournisseur nécessite une nouvelle classe d’adaptateur. Un nouveau modèle nécessite une ligne dans .env. Le code qui utilise la réponse ne change dans aucun des deux cas.
Un petit exemple détaillé : des paramètres qui cachent la clé
Section intitulée « Un petit exemple détaillé : des paramètres qui cachent la clé »Voici les paramètres et le choix de l’adaptateur en action, dans une session Python du projet terminé. La clé est une valeur de test inventée.
from assistant.config import Settings, make_provider, redactsettings = Settings(provider="openai_compatible", model="gemma3:4b", base_url="http://localhost:11434/v1", api_key="test-key-1234567890abcdef")settingsmake_provider(settings)make_provider(Settings())print(redact("Request failed. Authorization: Bearer abcdefgh12345678"))print(redact("helen.brooks@example.com paid with 4111 1111 1111 1111 for LK-714258"))La sortie :
Settings(provider='openai_compatible', model='gemma3:4b', base_url='http://localhost:11434/v1', timeout_s=30.0, reasoning_effort='')<assistant.providers.OpenAICompatibleProvider object at 0x100e7d6d0><assistant.providers.MockProvider object at 0x107d22e40>Request failed. Authorization: [REDACTED][EMAIL REDACTED] paid with [CARD NUMBER REDACTED] for LK-714258Quatre points à observer. Afficher les paramètres montre tout sauf la clé. make_provider choisit l’adaptateur selon les paramètres, et les paramètres par défaut donnent la simulation. redact masque une clé, une adresse courriel et un numéro de carte avant d’envoyer le texte à un journal. L’identifiant de commande reste, car le journal en a besoin. Les adresses de vos objets (0x…) seront différentes.
Étape 1 : le fournisseur réel dans l’adaptateur
Section intitulée « Étape 1 : le fournisseur réel dans l’adaptateur »L’adaptateur reçoit un deuxième fournisseur, OpenAICompatibleProvider. Il appelle tout serveur qui prend en charge l’API TermeChat CompletionsLe format de requête et de réponse de l’API de clavardage d’OpenAI : un nom de modèle, une liste de messages avec des rôles, et des paramètres. Azure, Ollama et beaucoup d’autres fournisseurs acceptent le même format (« compatible avec OpenAI »), donc un seul SDK fonctionne avec tous.En savoir plus Les éléments d'une requêteCréer des applications fiables avec les API de LLM compatible avec OpenAI, au moyen du SDK openai. Ouvrez assistant/providers.py et faites quatre changements.
-
Dans la chaîne de documentation en haut, remplacez la phrase sur le fournisseur unique :
assistant/providers.py (docstring) """The adapter: the only code that talks to a model provider.The rest of the assistant builds a request (a dict in the Chat Completions format) and gets back a`Completion`. Two providers do the work:- MockProvider replays responses that were recorded for the course. No account, no network.- OpenAICompatibleProvider calls any server that speaks the OpenAI-compatible Chat Completions API:a local Ollama server, OpenAI, or another provider. Its settings come from environment variables.""" -
Ajoutez deux importations pour obtenir les lignes suivantes :
assistant/providers.py (imports) import contextlibimport hashlibimport jsonimport timefrom dataclasses import dataclass, fieldfrom pathlib import Path -
Après la classe
ProviderError, ajoutez deux autres erreurs :assistant/providers.py (after ProviderError) class ProviderTimeout(ProviderError):"""No complete answer within the timeout."""class RateLimited(ProviderError):"""HTTP 429: too many requests. `retry_after` is the wait in seconds that the provider asked for."""def __init__(self, message: str, retry_after: float | None = None):super().__init__(message, status=429)self.retry_after = retry_after -
À la fin du fichier, ajoutez la traduction des erreurs et le nouveau fournisseur :
assistant/providers.py (end of the file) @contextlib.contextmanagerdef translate_errors():"""Turn the SDK's exceptions into the assistant's own, so that no other file imports `openai`."""import openaitry:yieldexcept openai.APITimeoutError as e:raise ProviderTimeout("The provider did not answer within the timeout.") from eexcept openai.RateLimitError as e:wait = e.response.headers.get("retry-after")raise RateLimited("HTTP 429: rate limited.", float(wait) if wait else None) from eexcept openai.APIStatusError as e:raise ProviderError(f"HTTP {e.status_code}: {e.message}", status=e.status_code) from eexcept openai.APIConnectionError as e:raise ProviderError("Cannot reach the provider (connection error).") from eclass OpenAICompatibleProvider:"""Calls a server that speaks the OpenAI-compatible Chat Completions API."""def __init__(self, base_url: str, api_key: str, timeout_s: float = 30.0):from openai import OpenAI # imported here, so that the mock works without a network library set up# max_retries=0: the assistant decides about retries itself (retry.py).self.client = OpenAI(base_url=base_url, api_key=api_key or "not-needed", timeout=timeout_s, max_retries=0)def complete(self, request: dict) -> Completion:start = time.monotonic()with translate_errors():response = self.client.chat.completions.create(**request)return Completion.from_response(response.model_dump(), round(time.monotonic() - start, 3))
Ces lignes contiennent trois décisions. D’abord, un délai d’expiration : le temps maximal pendant lequel le code attend une réponse. Ici, c’est 30 secondes, sauf si les paramètres indiquent autre chose. Sans ce délai, une connexion bloquée peut immobiliser l’écran de l’agent pendant plusieurs minutes. Ensuite, max_retries=0. Le SDK réessaie certaines erreurs de lui-même. Mais une nouvelle tentative répète une requête, et cette répétition implique une décision sur le temps et l’argent. Le module 6 prend cette décision dans retry.py; le SDK ne doit donc pas la prendre. Enfin, translate_errors transforme chaque erreur du SDK en ProviderError. Le reste de l’assistant gère un seul type d’erreur et n’importe jamais openai. 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 n’a pas besoin de clé. La valeur "not-needed" remplit donc l’argument obligatoire du SDK.
Étape 2 : la configuration au même endroit
Section intitulée « Étape 2 : la configuration au même endroit »Créez le fichier assistant/config.py. Il lit les paramètres dans les variables d’environnement et dans le fichier .env du dossier du projet, s’il existe.
"""Configuration from environment variables, the choice of provider, and redaction of secrets.
The key is read here and given only to the provider. It is never printed, logged or saved."""
import osimport refrom dataclasses import dataclass, fieldfrom pathlib import Path
from .providers import MockProvider, OpenAICompatibleProvider
ENV_FILE = Path(__file__).resolve().parent.parent / ".env"
def load_env_file(path: Path | None = None) -> None: """Read KEY=value lines from .env into the environment (variables already set win).""" path = path or ENV_FILE # looked up at call time, so that a test can point ENV_FILE elsewhere if not path.exists(): return for line in path.read_text(encoding="utf-8").splitlines(): line = line.strip() if line and not line.startswith("#") and "=" in line: name, value = line.split("=", 1) os.environ.setdefault(name.strip(), value.strip().strip('"').strip("'"))
@dataclass(frozen=True)class Settings: provider: str = "mock" # "mock" or "openai_compatible" model: str = "chat-small" # the recordings were made with chat-small and chat-strong base_url: str = "" # for example http://localhost:11434/v1 for Ollama api_key: str = field(default="", repr=False) # repr=False: printing the settings never shows the key timeout_s: float = 30.0 reasoning_effort: str = "" # sent only when set (some models need it, others reject it)
@classmethod def from_env(cls) -> "Settings": load_env_file() env = os.environ return cls( provider=env.get("ASSISTANT_PROVIDER", "mock"), model=env.get("ASSISTANT_MODEL", "chat-small"), base_url=env.get("ASSISTANT_BASE_URL", ""), api_key=env.get("ASSISTANT_API_KEY", ""), timeout_s=float(env.get("ASSISTANT_TIMEOUT_S", "30")), reasoning_effort=env.get("ASSISTANT_REASONING_EFFORT", ""), )
def make_provider(settings: Settings): if settings.provider == "mock": return MockProvider() if settings.provider == "openai_compatible": if not settings.base_url: raise ValueError("Set ASSISTANT_BASE_URL, for example http://localhost:11434/v1 for Ollama.") return OpenAICompatibleProvider(settings.base_url, settings.api_key, settings.timeout_s) raise ValueError(f"Unknown ASSISTANT_PROVIDER {settings.provider!r}: use mock or openai_compatible.")
SECRET_PATTERNS = [ (re.compile(r"sk-[A-Za-z0-9_\-]{8,}"), "sk-[REDACTED]"), (re.compile(r"(?i)(api[-_]?key|authorization)(\"?\s*[:=]\s*\"?)(bearer\s+)?[^\s\",}]+"), r"\1\2[REDACTED]"), (re.compile(r"(?i)bearer\s+[A-Za-z0-9._\-]{8,}"), "Bearer [REDACTED]"), (re.compile(r"\b\d(?:[ -]?\d){12,18}\b"), "[CARD NUMBER REDACTED]"), # 13-19 digits, with spaces or dashes (re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"), "[EMAIL REDACTED]"),]
def redact(text: str) -> str: """Hide keys, card numbers and email addresses before text goes to a log.""" for pattern, replacement in SECRET_PATTERNS: text = pattern.sub(replacement, text) return text| Partie | Son rôle |
|---|---|
load_env_file() |
Lit .env. Une variable déjà définie par l’interpréteur de commandes a priorité. Une commande ponctuelle peut donc changer un paramètre. |
Settings |
Regroupe tous les paramètres dans un objet. frozen=True : rien ne les modifie par accident. repr=False sur api_key : afficher ou journaliser les paramètres ne révèle jamais la clé. |
make_provider() |
Le seul endroit qui choisit l’adaptateur. La simulation est le choix par défaut. Le projet fonctionne donc sans aucun fichier .env. |
redact() |
Masque les clés, les numéros de carte et les adresses courriel avant d’envoyer le texte à un journal. Une liste de motifs sert de protection : elle détecte les formes courantes, pas tous les secrets. |
Étape 3 : le fichier d’exemples de paramètres
Section intitulée « Étape 3 : le fichier d’exemples de paramètres »Créez le fichier .env.example dans le dossier du projet. Il sert de modèle pour votre propre .env et ne contient aucun secret. Vous pouvez donc l’inclure sans risque dans un commit.
# Copy this file to .env and fill it in. Never commit .env: it can hold a key.# The default needs nothing: the mock provider replays the course's recorded responses.ASSISTANT_PROVIDER=mockASSISTANT_MODEL=chat-small
# Option 1, free and local: Ollama (https://ollama.com), then `ollama pull gemma3:4b`# ASSISTANT_PROVIDER=openai_compatible# ASSISTANT_BASE_URL=http://localhost:11434/v1# ASSISTANT_MODEL=gemma3:4b
# Option 2, your own key with a hosted provider that offers the OpenAI-compatible API (paid per token)# ASSISTANT_PROVIDER=openai_compatible# ASSISTANT_BASE_URL=https://api.openai.com/v1# ASSISTANT_MODEL=<a model name from your provider># ASSISTANT_API_KEY=<your key>
# Optional# ASSISTANT_TIMEOUT_S=30# ASSISTANT_REASONING_EFFORT= (some reasoning models need "none" to use tools; most models need nothing)Le fichier .gitignore du projet contient déjà .env et conserve .env.example grâce à la ligne !.env.example. Git ne prend donc jamais votre vrai .env, même avec git add ..
Étape 4 : la ligne de commande et raw.py utilisent les paramètres
Section intitulée « Étape 4 : la ligne de commande et raw.py utilisent les paramètres »-
Remplacez
assistant/__main__.pypar cette version. Elle demande le fournisseur à la configuration et transforme un échec du fournisseur en une seule ligne claire :assistant/__main__.py """Command line.python -m assistant analyse T-80008 analyse one ticket (the provider comes from .env; default: mock)"""import argparseimport jsonimport sysfrom .config import Settings, make_providerfrom .context import build_requestfrom .data import load_ticketfrom .providers import ProviderErrordef main(argv: list[str] | None = None) -> int:parser = argparse.ArgumentParser(prog="python -m assistant")commands = parser.add_subparsers(dest="command", required=True)commands.add_parser("analyse", help="analyse one ticket").add_argument("ticket_id")args = parser.parse_args(argv)try:ticket = load_ticket(args.ticket_id)except KeyError as error:print(error.args[0], file=sys.stderr)return 2settings = Settings.from_env()request = build_request(ticket, settings.model, "v1")try:completion = make_provider(settings).complete(request)except ProviderError as error: # the provider failed: say so clearly, without a tracebackprint(f"No answer: {error}", file=sys.stderr)return 1try:print(json.dumps(json.loads(completion.text), indent=2, ensure_ascii=False))except (TypeError, json.JSONDecodeError):print(completion.text)print("(This answer is not valid JSON.)")print(f"{completion.model} | finish_reason: {completion.finish_reason} | {completion.input_tokens} tokens in, "f"{completion.output_tokens} out | {completion.latency_s} s")return 0if __name__ == "__main__":sys.exit(main())La ligne de résumé ne dit plus « au moment de l’enregistrement » : avec un fournisseur réel, le temps est réel.
-
Dans
assistant/raw.py, utilisez aussi les paramètres. Supprimezimport oset ajoutezfrom .config import Settingsjuste avantfrom .context import build_request. Modifiez ensuitemainpour que ses premières lignes et sa partie--sendsoient les suivantes :assistant/raw.py (main, changed lines) def main(argv: list[str]) -> int:settings = Settings.from_env()ticket = load_ticket(argv[0] if argv else "T-80008")body = build_request(ticket, settings.model, "v1")print(json.dumps(body, indent=2, ensure_ascii=False))if "--send" not in argv:return 0if not settings.base_url:print("Set ASSISTANT_BASE_URL first (see .env.example).", file=sys.stderr)return 2headers = {"Content-Type": "application/json"}if settings.api_key:headers["Authorization"] = f"Bearer {settings.api_key}"response = httpx.post(settings.base_url.rstrip("/") + "/chat/completions", json=body, headers=headers,timeout=settings.timeout_s)Le reste du fichier ne change pas. Maintenant,
raw.pylit aussi.envet utilise le même délai d’expiration que l’assistant. -
Copiez les trois nouveaux fichiers de tests depuis l’instantané :
Fenêtre de terminal cp ../nextia-learning-labs/C08/snapshots/end-of-m01/tests/test_*.py tests/Fenêtre de terminal Copy-Item ..\nextia-learning-labs\C08\snapshots\end-of-m01\tests\test_*.py tests\Fenêtre de terminal cp ../nextia-learning-labs/C08/snapshots/end-of-m01/tests/test_*.py tests/test_providers.pyvérifie la clé de la requête et la simulation.test_config.pyvérifie que la clé n’apparaît jamais et queredactfonctionne.test_live.pyenvoie une requête réelle, mais seulement quand vous le demandez. -
Lancez les tests :
Fenêtre de terminal python -m pytestLa dernière ligne :
======================= 15 passed, 1 deselected in 0.02s =======================Le test exclu est celui avec le fournisseur réel.
pytest.inil’exclut, sauf si vous lancezpython -m pytest -m live. -
Relancez la première commande :
python -m assistant analyse T-80008. Le JSON reste le même, et la dernière ligne estgpt-6-luna-2026-09-22 | finish_reason: stop | 1032 tokens in, 241 out | 2.678 s.
Votre projet correspond maintenant à l’instantané end-of-m01. Sur macOS et Linux, vérifiez-le avec diff -r -x .venv -x __pycache__ -x .pytest_cache ../nextia-learning-labs/C08/snapshots/end-of-m01 . : aucune sortie signifie aucune différence.
Le raccourci de Tomás : la clé dans le navigateur
Section intitulée « Le raccourci de Tomás : la clé dans le navigateur »L’idée de Tomás supprime le serveur. Le navigateur de l’agent appelle directement le fournisseur, avec la clé dans le JavaScript de la page. Le schéma compare cette approche à celle d’Amira.
Description textuelle du diagramme
Deux conceptions. Dans celle du haut, le raccourci de Tomás, le JavaScript de la page dans le navigateur de l’agent contient la clé. Le navigateur envoie la requête avec « Authorization : Bearer » et la clé directement au fournisseur. Une note indique que l’onglet Network (réseau) des outils de développement montre cet en-tête à toute personne qui ouvre la page. Elle peut copier la clé et dépenser l’argent de Larkfield. Dans celle du bas, la conception d’Amira, le navigateur n’a aucune clé. Il envoie le billet et la session de l’agent au serveur de Larkfield. Le serveur conserve la clé dans une variable d’environnement et envoie la requête, avec la clé, au fournisseur.
Erreur courante
La clé dans le navigateur
Symptôme : la démonstration fonctionne et rien ne semble anormal. Le danger reste invisible jusqu’à ce que quelqu’un regarde. Un visiteur ouvre la page de soutien, appuie sur F12 pour ouvrir les outils de développement et sélectionne l’onglet Network. Dans la requête au fournisseur, les en-têtes affichent Authorization: Bearer (autorisation par jeton porteur), suivi de la clé complète. La même clé se trouve dans le fichier JavaScript de la page, visible dans l’onglet Sources (sources). Ceci décrit ce que le navigateur montre. Le cours n’a pas publié de page contenant une vraie clé.
Cause : le navigateur peut montrer tout ce qu’il exécute. Le code d’une page n’est pas secret, et le code minifié ou caché reste lisible. Une clé dans une page est une clé collée sur la porte d’entrée. Quiconque la copie peut envoyer n’importe quelle requête sur le compte de Larkfield, aux frais de Larkfield. Cela dure jusqu’à ce que quelqu’un le remarque et révoque la clé.
Correction : gardez la clé sur le serveur, dans une variable d’environnement, et donnez-la uniquement à l’adaptateur. Le navigateur communique avec le serveur de Larkfield, qui vérifie l’identité de l’agent, puis appelle le fournisseur. Si une clé s’est déjà trouvée dans une page, un journal ou un commit, considérez-la comme divulguée. Créez une nouvelle clé chez le fournisseur et supprimez l’ancienne. Supprimer le fichier ne suffit pas, car des copies peuvent exister.
Ce que cela signifie pour un vrai service d’IA : le serveur intermédiaire n’est pas une charge inutile. C’est l’endroit où conserver la clé, puis appliquer les contrôles de ce cours : le schéma, les permissions et le budget. Un navigateur ne peut faire respecter aucun de ces contrôles.
Quand le fournisseur échoue
Section intitulée « Quand le fournisseur échoue »L’adaptateur gère maintenant trois types d’échec. Un délai d’expiration : aucune réponse à temps. Une 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 : HTTP 429, trop de requêtes (le module 6 la gère). Et toutes les autres erreurs : une mauvaise clé, une requête refusée, une erreur du serveur ou aucune connexion. Chacune devient une ProviderError. La ligne de commande affiche une ligne commençant par No answer: (aucune réponse) et se termine avec le code 1.
Description textuelle du diagramme
L’adaptateur appelle le fournisseur et attend au plus ASSISTANT_TIMEOUT_S secondes. Si une réponse revient, elle devient une Completion que la commande affiche, avec le code de sortie 0. Si rien ne revient à temps, l’adaptateur lève ProviderTimeout. HTTP 429 devient RateLimited, avec le temps d’attente de l’en-tête Retry-After. HTTP 400, 401 ou une erreur serveur 5xx devient une ProviderError avec le statut. Une connexion échouée devient une ProviderError qui indique que le fournisseur est inaccessible. Les quatre cas se terminent de la même façon. La commande affiche « No answer : » avec la raison et se termine avec le code 1, sans trace d’erreur ni clé.
Voici des échecs réels du projet terminé. Le premier ne nécessite rien : l’adresse pointe vers Ollama, mais Ollama ne tourne pas.
ASSISTANT_PROVIDER=openai_compatible ASSISTANT_BASE_URL=http://localhost:11434/v1 ASSISTANT_MODEL=gemma3:4b python -m assistant analyse T-80008No answer: Cannot reach the provider (connection error).Une mauvaise clé, envoyée à Termechat-smallLe nom que le code de Larkfield donne à un petit modèle hébergé peu coûteux : gpt-6-luna sur Azure. Le code utilise ce nom, donc le modèle peut changer sans modifier le code.En savoir plus Ce qu'est une API de LLMCréer des applications fiables avec les API de LLM sur Azure (ASSISTANT_API_KEY=not-a-real-key-0000) :
No answer: HTTP 401: Error code: 401 - {'error': {'code': '401', 'message': 'Access denied due to invalid subscription key or wrong API endpoint. Make sure to provide a valid key for an active subscription and use a correct regional API endpoint for your resource.'}}Et un délai d’expiration de 1 seconde, ASSISTANT_TIMEOUT_S=1. chat-small prend environ 3 secondes, et un modèle local encore en chargement prend plus de temps. Les deux ont donné le même résultat réel :
No answer: The provider did not answer within the timeout.Aucun de ces messages ne contient la clé. Le message 401 reprend le texte du fournisseur. Ce texte explique le problème sans montrer la clé envoyée. Choisissez le délai d’expiration selon les latences réelles. Pour les billets du cours, chat-small a pris 2,72 secondes à la TermeMédianeLa valeur du milieu quand on trie les valeurs. La moitié est plus basse, l’autre plus haute. Un seul appel très lent la change peu : c’est un bon temps « habituel ».En savoir plus Mesurer la performance en pratiqueLes LLM : comment ils fonctionnent et comment en choisir un et 4,72 secondes pour les 5 % les plus lents. Un délai de 30 secondes laisse une marge pour les ralentissements, tout en libérant l’écran de l’agent.
Exercice du module : du simulateur à un modèle local, puis retour
Section intitulée « Exercice du module : du simulateur à un modèle local, puis retour »Cet exercice utilise .env, comme un vrai déploiement. Il montre la frontière en action : vous changez de fournisseur sans modifier une seule ligne de code.
-
Copiez le modèle dans votre propre fichier
.env:Fenêtre de terminal cp .env.example .envFenêtre de terminal Copy-Item .env.example .envFenêtre de terminal cp .env.example .env -
Dans
.env, ajoutez#devant les deux lignes du simulateur. Retirez#des trois lignes de l’option 1. Enregistrez le fichier. -
Si Ollama ne fonctionne pas, exécutez maintenant
python -m assistant analyse T-80008. Vous voyezNo answer: Cannot reach the provider (connection error).(aucune réponse : impossible de joindre le fournisseur, erreur de connexion). Si vous n’avez pas Ollama, passez à l’étape 6. -
Démarrez Ollama et exécutez la commande de nouveau. La réponse vient de 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. Lors de l’essai du cours, elle était entourée de délimiteurs de bloc TermeMarkdownUne façon de noter la mise en forme en texte simple, par exemple **gras** ou # pour un titre. Les modèles l’ajoutent souvent : un programme qui attend du JSON simple doit l’enlever ou refuser la réponse.. La commande a donc affiché le texte et
(This answer is not valid JSON.)(cette réponse n’est pas du JSON valide). La dernière ligne étaitgemma3:4b | finish_reason: stop | 1092 tokens in, 193 out | 9.011 s(modèle, raison de l’arrêt, jetons en entrée et en sortie, durée). -
Exécutez le test avec le fournisseur réel :
python -m pytest -m live. L’essai du cours a donné1 passed, 15 deselected(1 test réussi, 15 non sélectionnés). Les autres tests utilisent toujours le simulateur :conftest.pyconfigure le simulateur pour chaque test qui n’est pas marquélive(réel). -
Revenez au simulateur : supprimez
.env, ou remettez les lignes du simulateur. Exécutezpython -m assistant analyse T-80008. La ligne attendue revient :… 1032 tokens in, 241 out | 2.678 s(jetons en entrée et en sortie, durée). -
Exécutez
python -m pytest. Vous voyez15 passed, 1 deselected(15 tests réussis, 1 non sélectionné).
Afficher la réponse complète de Gemma 3 4B lors de l’essai du cours
Résultat enregistré avec Ollama 0.40.1, à partir du projet terminé :
```json{ "language": "en", "team": "delivery", "needs_human": true, "reason": "The customer is inquiring about the status of their order and the delivery date, which falls under the delivery team's purview, and requires investigation.", "confidence": "high", "order": { "order_id": "LK-581106", "status": null }, "reply": "Hello, thank you for contacting us regarding order LK-581106. I'm so sorry for the delay in receiving your delivery date – let me investigate this immediately for you. I’ll check the tracking information to see the current status of your garden shed and provide you with an estimated delivery timeframe. We appreciate your patience and will keep you updated on the progress."}```(This answer is not valid JSON.)gemma3:4b | finish_reason: stop | 1092 tokens in, 193 out | 9.011 sL’équipe est la bonne, mais needs_human vaut vrai pour une simple question de livraison. De plus, la réponse n’est pas du JSON valide. Les prochains modules traitent ces deux problèmes : le module 2 avec la requête, le module 3 avec la vérification.
À vous de jouer : la clé qui ne s’affiche jamais
Section intitulée « À vous de jouer : la clé qui ne s’affiche jamais »Définissez une fausse clé pour une seule commande, puis essayez de faire afficher cette clé par le projet. Sur macOS ou Linux :
ASSISTANT_API_KEY=test-key-1234567890abcdef python -c "from assistant.config import Settings; print(Settings.from_env())"Avant de l’exécuter, prédisez la sortie. Ensuite, cherchez test-key dans toute la sortie.
Afficher la réponse
La sortie affiche les paramètres sans la clé : Settings(provider='mock', model='chat-small', base_url='', timeout_s=30.0, reasoning_effort=''). repr=False exclut api_key du texte affiché, donc test-key n’apparaît nulle part. La clé reste dans settings.api_key, car l’adaptateur en a besoin. Masquer la clé à l’affichage protège les journaux (logs), où surviennent la plupart des fuites. Cela ne rend pas la valeur secrète à l’intérieur du programme.
Vérification du module
Section intitulée « Vérification du module »Répondez dans vos propres mots, puis comparez avec l’exemple de réponse.
- Quel code dans
ticket-assistantgère la clé d’API? Nommez tous les endroits où la clé passe et ceux où elle ne doit jamais aller. - Que se passe-t-il quand le fournisseur échoue? Prenez trois cas : le fournisseur ne fonctionne pas, la clé est incorrecte et le fournisseur est trop lent.
Afficher un exemple de réponse
- La clé commence dans une variable d’environnement,
ASSISTANT_API_KEY, définie dans l’interpréteur de commandes ou dans.env. Git ignore.env. Seul.env.example, sans clé, fait l’objet d’un commit (enregistrement dans le dépôt).config.pyla lit dansSettings.api_key, qui reste masqué lors de l’affichage des paramètres.make_providerla transmet àOpenAICompatibleProvider, et le SDK l’envoie dans l’en-têteAuthorizationpar HTTPS. Aucun autre code ne la voit. Elle ne doit jamais se trouver dans le code, un commit, un journal, un message d’erreur ou un navigateur. - L’adaptateur transforme chaque échec en
ProviderError. La ligne de commande affiche une seule ligne et se termine avec le code 1. Fournisseur arrêté :No answer: Cannot reach the provider (connection error).Clé incorrecte :No answer: HTTP 401: …(aucune réponse : HTTP 401), avec le message du fournisseur, sans clé. Trop lent : le délai maximal met fin à l’attente. Le message estNo answer: The provider did not answer within the timeout.(aucune réponse : le fournisseur n’a pas répondu dans le délai maximal). L’adaptateur ne réessaie pas, car les nouvelles tentatives du SDK sont désactivées. Le module 6 ajoute des nouvelles tentatives avec des limites.
Revenir à la fin du module 1
Section intitulée « Revenir à la fin du module 1 »Si votre projet n’obtient pas 15 passed, 1 deselected, copiez l’instantané end-of-m01 en suivant les étapes de Réinitialiser le projet. Utilisez end-of-m01 au lieu de start dans la commande de copie. Le module 2 commence à partir de cet instantané.
Vérification des connaissances
Section intitulée « Vérification des connaissances »Vérifiez votre compréhension
Larkfield passe de chat-small sur Azure à un modèle d'un autre fournisseur avec une API compatible avec OpenAI. Quels fichiers changent et lesquels restent identiques?
Afficher la réponse
Aucun fichier Python ne change. Le nouveau fournisseur utilise la même API, donc OpenAICompatibleProvider fonctionne avec lui. Seule la configuration change : ASSISTANT_BASE_URL, ASSISTANT_MODEL et ASSISTANT_API_KEY dans .env ou dans l’environnement du serveur. Si le nouveau fournisseur avait une API différente, il faudrait une nouvelle classe d’adaptateur dans providers.py. make_provider la choisirait. La logique d’affaires ne change jamais.
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 dit : « La page est réservée à nos agents, avec une connexion obligatoire. Donc, la clé dans le JavaScript est en sécurité. » Que répondez-vous?
Afficher la réponse
Chaque agent qui se connecte peut ouvrir les outils de développement et copier la clé. Toute personne qui accède à l’ordinateur portable ou à la session d’un agent peut aussi le faire. La connexion contrôle qui voit la page, pas ce que la page révèle. Avec la clé, une personne peut appeler le fournisseur directement, sans passer par l’application de soutien. Aucun contrôle ne s’applique, et Larkfield paie les frais. La clé doit rester sur le serveur, derrière la connexion obligatoire, jamais dans la page.
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 le résultat : vous définissez ASSISTANT_TIMEOUT_S=60 et le fournisseur est en panne. Que voit l'agent, et après combien de temps?
Afficher la réponse
Cela dépend du type de panne du fournisseur. Si la connexion est refusée immédiatement, l’agent voit « No answer : Cannot reach the provider (connection error). » (aucune réponse : impossible de joindre le fournisseur, erreur de connexion) après environ une seconde. Si le fournisseur accepte la connexion, mais ne répond jamais, l’agent attend les 60 secondes complètes. Ensuite, l’agent voit « No answer : The provider did not answer within the timeout. » (aucune réponse : le fournisseur n’a pas répondu dans le délai maximal). Le délai maximal représente donc la plus longue attente que vous acceptez. Choisissez-le à partir des latences réelles, plutôt que de le rendre aussi grand que possible.
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
- La frontière de l’application sépare le code en logique d’affaires, adaptateur et configuration. L’adaptateur (
providers.py) est le seul code qui appelle un fournisseur. La configuration se trouve dansconfig.pyet.env. - Les paramètres viennent des variables d’environnement.
.envne fait jamais l’objet d’un commit;.env.examplesert de modèle sans secrets. - La clé d’API reste uniquement sur le serveur et atteint seulement l’adaptateur.
Settingsne l’affiche jamais.redact()masque les clés, les numéros de carte et les adresses courriel avant l’envoi du texte à un journal. Une clé dans un navigateur est une clé divulguée. - Chaque échec du fournisseur devient un
ProviderError, et la commande affiche une seule ligneNo answer:(aucune réponse). Un délai maximal limite l’attente. Les nouvelles tentatives du SDK sont désactivées, donc le module 6 décide des nouvelles tentatives. - Passer du simulateur à un modèle local, puis revenir, ne demande aucun changement de code : seulement de
.env.
Les modèles et les API changent vite; les chiffres de ce module représentent un instantané. La frontière, elle, reste. Ensuite, le module 2 traite la requête comme une spécification que vous pouvez tester.
Leçon suivante : Préciser la tâche