Aller au contenu principal

Module 10 — Projet : assistant outillé de bout en bout

Dix modules pour arriver ici : un assistant de notes de frais qui lit les justificatifs, connaît la politique, se souvient de la conversation, appelle des outils et sait demander une validation humaine avant les gestes lourds. Ce module rassemble le tout dans un projet livrable — code, interface minimale, gestion des erreurs, tests, déploiement — et fixe les décisions qui font la différence entre une démo et un service utilisable.

L'architecture, vue de haut

┌────────────────────────────────────────────────────────────────┐
│ FastAPI (cours 40) │
├────────────────────────────────────────────────────────────────┤
│ RunnableWithMessageHistory │
│ (SQLChatMessageHistory par employe_id) │
├────────────────────────────────────────────────────────────────┤
│ Agent LangGraph │
│ ┌──────────────┐ ┌─────────────────────────────────────┐ │
│ │ modele │◀─▶│ outils │ │
│ │ gpt-4o-mini │ │ - chercher_politique (RAG Chroma) │ │
│ │ T = 0 │ │ - convertir_en_eur │ │
│ └──────────────┘ │ - verifier_plafond (interrupt) │ │
│ │ - ajouter_ligne_tableur (interrupt) │ │
│ └─────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ Traçage LangSmith + callback CoutCumule │
└────────────────────────────────────────────────────────────────┘

Six briques, chacune du cours précédent : le modèle (module 2), les outils (module 7), le RAG (modules 4-5), la mémoire (module 6), l'agent (module 8), le traçage (module 9). Rien de neuf ; du bon assemblage.

Le squelette de l'agent

from langgraph.prebuilt import create_react_agent
from langgraph.checkpoint.postgres import PostgresSaver
from langchain_openai import ChatOpenAI

MODELE = ChatOpenAI(model="gpt-4o-mini", temperature=0, timeout=30)

OUTILS = [
chercher_politique, # RAG sur politique-2026.pdf
convertir_en_eur,
verifier_plafond, # renvoie {"autorise": bool, "motif": str}
ajouter_ligne_tableur,
]

PROMPT_SYSTEME = """Vous êtes l'assistant de gestion des notes de frais.
- Utilisez chercher_politique pour toute question sur la règle.
- Convertissez en euros avant toute décision de plafond.
- Vérifiez le plafond AVANT d'ajouter une ligne.
- Répondez en français, brièvement, en citant l'article de politique."""

with PostgresSaver.from_conn_string(DSN) as saver:
agent = create_react_agent(
model=MODELE,
tools=OUTILS,
prompt=PROMPT_SYSTEME,
checkpointer=saver,
interrupt_before=["tools"], # attention : filtrer côté application
)

Interface minimale

FastAPI (cours 40) expose deux points d'entrée : un pour envoyer un message, un pour approuver une action en attente. Le flux (stream) est essentiel : l'utilisateur voit la réponse s'écrire.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

@app.post("/chat/{employe_id}")
async def chat(employe_id: str, message: str):
config = {"configurable": {"thread_id": employe_id}}

async def flux():
async for event in agent.astream_events(
{"messages": [("user", message)]}, config=config, version="v2",
):
if event["event"] == "on_chat_model_stream":
yield event["data"]["chunk"].content

return StreamingResponse(flux(), media_type="text/plain")

@app.post("/approuver/{employe_id}")
async def approuver(employe_id: str, approuve: bool):
config = {"configurable": {"thread_id": employe_id}}
if approuve:
return agent.invoke(None, config=config)
return {"statut": "annule"}

Le thread_id du checkpointer est l'identifiant employé : chaque utilisateur retrouve son fil, l'humain-dans-la-boucle fonctionne d'une requête à l'autre sans état côté serveur.

Gestion des erreurs d'outil

Un outil peut échouer pour trois raisons distinctes, chacune traitée différemment :

  • Erreur d'argument (ValidationError de pydantic). Le message est renvoyé au modèle sous forme de ToolMessage : le modèle corrige et retente. Aucune remontée à l'utilisateur.
  • Erreur métier (plafond dépassé, catégorie interdite). L'outil retourne un résultat structuré {"autorise": false, "motif": "..."} et le modèle formule une réponse à l'utilisateur. Pas d'exception.
  • Erreur d'infrastructure (le tableur ne répond pas, la base est saturée). L'outil lève une exception, un try autour du agent.invoke la capture, on renvoie une erreur 503 à l'utilisateur. Le modèle ne doit pas essayer de résoudre un incident système.
try:
resultat = agent.invoke(...)
except HTTPError as e:
return {"erreur": "Service temporairement indisponible", "detail": str(e)}, 503

Tests

Trois niveaux, imbriqués :

  • Unitaires sur les outils. Chaque outil est une fonction Python : test_convertir_en_eur_avec_devise_inconnue, test_verifier_plafond_paris_hotel. Rapides, déterministes, indispensables.
  • Intégration sur les chaînes. Une chaîne RAG appelée sur cinq questions de référence doit citer les bons articles. Utilise pytest + le jeu d'évaluation du module 9.
  • Bout en bout sur l'agent. Cinq scénarios utilisateurs, chacun avec la séquence attendue d'outils. On accepte de la souplesse sur le texte final, mais l'ordre des appels d'outils doit être stable.
def test_agent_convertit_avant_verifier():
resultat = agent.invoke({"messages": [("user", "45 USD au restaurant, ajoute la ligne.")]})
outils_appeles = [m.name for m in resultat["messages"] if isinstance(m, ToolMessage)]
assert outils_appeles.index("convertir_en_eur") < outils_appeles.index("verifier_plafond")

Passage en production

Cinq réglages à ne pas oublier :

  • Version des modèles épinglée. gpt-4o-mini-2024-07-18 plutôt que gpt-4o-mini (alias qui change). Un modèle qui bouge sans que vous le sachiez est une régression garantie.
  • Version des dépendances épinglée. langchain-core==0.3.15, langgraph==0.2.60. Une mise à jour mineure a déjà cassé le rendu des tool_calls.
  • Journalisation applicative en plus du traçage. Le traçage voit les Runnable ; les journaux voient les requêtes HTTP, les erreurs métier, les authentifications.
  • Rate limiting côté API. Un utilisateur mal intentionné (ou un script en boucle) peut vider un budget mensuel en une heure.
  • Circuit breaker sur le fournisseur LLM. En cas de panne, l'application dégrade proprement au lieu de bloquer.

Maîtriser les coûts

Un assistant LLM en production a trois postes de coût, dans l'ordre décroissant : le modèle, les plongements, la base vectorielle.

  • Cache sur le plongement des questions : deux questions identiques ne recalculent pas le vecteur. Économie facile de 20-30 %.
  • Cache sur les appels modèles idempotents : SQLiteCache local, ou RedisCache en production. Aucune requête identique n'est refacturée.
  • Deux modèles à deux niveaux : gpt-4o-mini pour 95 % des cas, gpt-4o seulement quand le premier ne suffit pas (à détecter par un score de confiance ou une reformulation).
  • Analyse mensuelle du callback CoutCumule par catégorie : une hausse silencieuse sur une catégorie signale une régression de prompt ou une nouvelle boucle d'agent.
La démo qui coûte 400 dollars par mois

Un assistant démonstratif oublié en production, sans limite de sessions, avec un modèle premium et sans cache, atteint facilement plusieurs centaines de dollars par mois. Chaque nouveau service LLM commence par un budget journalier plafonné et une alerte sur dépassement — c'est aussi important que les tests unitaires.

En résumé

  • L'architecture cible tient sur une page : FastAPI en tête, RunnableWithMessageHistory pour la mémoire, un agent LangGraph avec outils et checkpoint, LangSmith en traçage.
  • Trois catégories d'erreurs d'outil, trois traitements : erreur d'argument → renvoyée au modèle, erreur métier → résultat structuré, erreur infra → exception applicative.
  • Trois niveaux de tests — outils, chaînes, agent bout en bout — couvrent ce que les tests unitaires seuls ne verraient pas dans un système probabiliste.
  • Les coûts se maîtrisent avec cache, deux modèles à deux niveaux, budget journalier plafonné et surveillance mensuelle par catégorie ; c'est ce qui sépare une démo d'un service.

Récapitulatif suivant : la carte complète du fil rouge, les fils qui traversent le cours, et l'annonce de l'examen.