Accueil / Articles

Du prototype à la production : ce que j’ai appris en développant des systèmes d’IA agente réels

Passer de la version prototype à la production pour des IA agentes avec LangChain et LangGraph : pourquoi les prototypes stagne, l’architecture du pipeline de documents, et ce qui entrave le processus.

Éditeur : REACTAPP.TOP14 min de lecture3267 mots

Sources: From Prototype to Production: What I Learned Building Real Agentic AI Systems with LangChain and LangGraph

Ce guide reconstitue le parcours allant des matières premières à un système fonctionnel pour l’ouvrage : « De prototype à production : Ce que j’ai appris en développant des systèmes d’IA agents réels avec LangChain et LangGraph ». L’accent est mis sur des étapes opérationnelles, des vérifications explicites, ainsi que du code que vous pouvez intégrer directement dans un dépôt sans devoir deviner l’intention derrière chaque élément.

Cessez de créer des prototypes d’IA. Commencez à déployer des systèmes d’IA capables de résister au monde réel.

Pour l’étape « Arrêter la création de prototypes d’IA », définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Préférez des unités petites et testables aux scripts complexes. Lorsqu’une étape échoue, l’échec doit indiquer une seule responsabilité plutôt qu’un processus embrouillé. Faites approuver par des humains les étapes qui engagent de l’argent ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

Pourquoi LangChain et LangGraph ?

Pour l’étape Why LangChain et LangGraph, définissez les entrées, le responsable de l’étape ainsi que les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définissez des vérifications de succès et refusez toute complétion partielle silencieuse. Faites approuver par un humain les actions qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

L’architecture : des documents aux décisions

Pour l’étape « Architecture à partir de documents », définissez les entrées, le responsable de l’étape et les critères d’achèvement avant de modifier du code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des jetons ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Faites approuver par un humain les étapes qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

Document Upload
      ↓
Chunking Pipeline (LangChain TextSplitter)
      ↓
Embedding Generation (Azure OpenAI ada-002)
      ↓
Vector Store (Azure AI Search — Hybrid Search)
      ↓
LangGraph Agent Orchestration
    ├── Classifier Agent (GPT-4)
    ├── Extractor Agent (GPT-4)
    ├── Validator Agent (GPT-3.5)
    └── HITL Node (Human-in-the-Loop)
      ↓
Structured Output → Backend → Database

Niveau 1 : Le pipeline RAG

Pour l’étape RAG du niveau 1, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du système. Citez les passages qui ont réellement servi de base à la réponse. Sans citations, les opérateurs ne peuvent pas distinguer une hallucination d’un manque d’indexation.

Stratégie de segmentation

Pour l’étape de la Stratégie de segmentation, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez ensemble le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations ultérieures. Imposez une approbation humaine pour les actions qui entraînent des dépenses ou modifient des données de production. La configuration en temps de compilation ne suffit pas à garantir la complétude du processus métier. Pour l’étape de la Stratégie de segmentation, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définissez des vérifications de succès et refusez toute complétion partielle silencieuse.

from langchain.text_splitter import RecursiveCharacterTextSplitter
splitter = RecursiveCharacterTextSplitter(
    chunk_size=512,
    chunk_overlap=102,  # 20% overlap
    separators=["\n\n", "\n", ". ", " ", ""],
    length_function=len,
)chunks = splitter.split_documents(documents)

Incorporation et stockage vectoriel

Lors de la phase d’incorporation et de stockage vectoriel, notez d’abord les exigences : entrées requises, signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque l’environnement passe de la démonstration à des environnements partagés. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Changer fréquemment les prompts ne résout que rarement un système de récupération insuffisant.

from langchain_openai import AzureOpenAIEmbeddings
from langchain_community.vectorstores import AzureSearch
embeddings = AzureOpenAIEmbeddings(
    azure_deployment="text-embedding-ada-002",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
    api_version="2024-02-01",
)vector_store = AzureSearch(
    azure_search_endpoint=os.getenv("AZURE_SEARCH_ENDPOINT"),
    azure_search_key=os.getenv("AZURE_SEARCH_KEY"),
    index_name="tax-documents",
    embedding_function=embeddings.embed_query,
)

Recherche hybride — L’optimisation la plus importante que vous ayez réalisée

Lorsque vous travaillez sur l’étape « The Most » de la recherche hybride, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du système. Créez des points de contrôle après les étapes coûteuses. Le mécanisme de reprise ne doit pas facturer à nouveau la même appel au LLM lorsque un opérateur réessaie un nœud ultérieur.

retriever = vector_store.as_retriever(
    search_type="hybrid",
    search_kwargs={
        "k": 5,
        "score_threshold": 0.75,
    }
)

Cache d’incrustation Redis — Réduction des coûts de 40 %

Lors du travail sur l’étape Redis Embedding Cache 40, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non traités font partie intégrante du produit, et non d’améliorations ultérieures. Mesurez le taux de rappel sur un ensemble fixe de questions avant d’ajuster les prompts. Le changement fréquent des prompts résout rarement un système de récupération insuffisant. Lors du travail sur l’étape Redis Embedding Cache 40, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès et refusez les terminations partielles silencieuses.

from langchain.cache import RedisSemanticCache
from langchain.globals import set_llm_cache
import redis
redis_client = redis.Redis(
    host=os.getenv("REDIS_HOST"),
    port=6380,
    password=os.getenv("REDIS_PASSWORD"),
    ssl=True,
)set_llm_cache(
    RedisSemanticCache(
        redis_=redis_client,
        embedding=embeddings,
        score_threshold=0.99,  # Very high — only cache exact or near-exact matches
    )
)

Niveau 2 : Orchestration des agents LangGraph

L’étape des agents LangGraph au niveau 2 fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un enregistrement exemplaire, un cas d’échec ainsi que des notes de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Maintenez l’état du graphe plat et typé. Les blocs imbriqués masquent l’identité du nœud qui a écrit tel champ et perturbent la reprise après interruption.

Pourquoi LangGraph plutôt que des chaînes simples ?

Le concept LangGraph Over Simple fonctionne le mieux lorsqu’il est considéré comme une surface mesurable. Capturez un transcript parfait, un cas d’échec et la note de réversion avant d’élargir le périmètre. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du graphe. Garantissez que l’état du graphe soit plat et typé. Les blocs imbriqués masquent l’identité du nœud qui a écrit tel champ, ce qui perturbe la reprise après interruption.

Définition de l’état

La phase de Définition de l’état fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un enregistrement idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Documentez ensemble le parcours réussi et le parcours de récupération. Les tentatives répétées, les contrôles humains et le traitement des messages non livrés font partie intégrante du produit, et non d’une mise en forme ultérieure. Maintenez l’état du graphe plat et typé. Les blocs imbriqués masquent le fait que tel nœud a écrit telle champ et perturbent la reprise après interruption. La phase de Définition de l’état fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un enregistrement idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Considérez cette phase comme un contrat entre les entrées et les sorties validées. Nommez les artefacts, définites des vérifications de succès et refusez toute complétion partielle silencieuse.

from typing import TypedDict, Annotated, List, Optional
from langgraph.graph import StateGraph, END
import operator
class DocumentProcessingState(TypedDict):
    # Input
    document_id: str
    document_text: str
    document_chunks: List[str]

    # Processing
    document_type: Optional[str]
    extracted_fields: Optional[dict]
    validation_errors: List[str]
    retry_count: int

    # Output
    confidence_score: float
    requires_human_review: bool
    processing_notes: str
    final_result: Optional[dict]

Construction du graphe

Pour l’étape de construction du graphe, définissez les entrées, le responsable de l’étape et les critères d’arrêt avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Enregistrez les temps d’exécution ainsi que le coût des jetons ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le parcours passe de l’environnement de démonstration à des environnements partagés. Faites approuver par un humain les arêtes qui entraînent des dépenses ou modifient des données de production. La connexion en temps de compilation ne garantit pas la complétude du processus métier.

from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
workflow = StateGraph(DocumentProcessingState)# Add nodes
workflow.add_node("classifier", classify_document)
workflow.add_node("extractor", extract_fields)
workflow.add_node("validator", validate_extraction)
workflow.add_node("human_review", flag_for_human_review)
workflow.add_node("finalizer", finalize_result)# Define edges
workflow.set_entry_point("classifier")workflow.add_conditional_edges(
    "classifier",
    route_after_classification,
    {
        "extract": "extractor",
        "human_review": "human_review",
        "unknown": END,
    }
)workflow.add_conditional_edges(
    "extractor",
    route_after_extraction,
    {
        "validate": "validator",
        "retry": "extractor",
        "human_review": "human_review",
    }
)workflow.add_conditional_edges(
    "validator",
    route_after_validation,
    {
        "finalize": "finalizer",
        "retry": "extractor",
        "human_review": "human_review",
    }
)workflow.add_edge("finalizer", END)
workflow.add_edge("human_review", END)# Compile with checkpointing for resumability
memory = MemorySaver()
app = workflow.compile(checkpointer=memory)

Le nœud de classification

Pour l’étape du Nœud de classification, définissez les entrées, le responsable de l’étape et les critères de fin avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du graphe. Mettez en place une approbation humaine pour les connexions qui entraînent des dépenses ou modifient des données de production. La connexion au niveau du temps de compilation ne garantit pas la complétude des processus métier.

from langchain_openai import AzureChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import JsonOutputParser
from pydantic import BaseModel, Field
class ClassificationResult(BaseModel):
    document_type: str = Field(
        description="One of: TAX_RETURN, PAYSLIP, TRAVEL_EXPENSE, IMMIGRATION_DOC, INVOICE, UNKNOWN"
    )
    confidence_score: float = Field(
        description="Confidence between 0.0 and 1.0"
    )
    reasoning: str = Field(
        description="Brief reasoning for the classification"
    )llm = AzureChatOpenAI(
    azure_deployment="gpt-4",
    azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
    api_key=os.getenv("AZURE_OPENAI_KEY"),
    api_version="2024-02-15-preview",
    temperature=0,  # Always 0 for classification — determinism matters
)classification_prompt = ChatPromptTemplate.from_messages([
    ("system", """You are an expert tax document classifier.

Classify the document and return ONLY valid JSON matching the schema.
NEVER hallucinate values not present in the document.
If you cannot classify with confidence > 0.7, return UNKNOWN.{format_instructions}"""),
    ("human", "Document text:\n\n{document_text}"),
])parser = JsonOutputParser(pydantic_object=ClassificationResult)classifier_chain = classification_prompt | llm | parserdef classify_document(state: DocumentProcessingState) -> DocumentProcessingState:
    try:
        result = classifier_chain.invoke({
            "document_text": state["document_text"][:3000],  # Truncate for context window
            "format_instructions": parser.get_format_instructions(),
        })

        state["document_type"] = result["document_type"]
        state["confidence_score"] = result["confidence_score"]
        state["processing_notes"] += f"Classified as {result['document_type']} with confidence {result['confidence_score']:.2f}. "

    except Exception as e:
        state["document_type"] = "UNKNOWN"
        state["confidence_score"] = 0.0
        state["processing_notes"] += f"Classification failed: {str(e)}. "

    return state

Fonctions de routage — Le cœur de LangGraph

Pour l’étape Routing Functions The Heart, définissez les entrées, le responsable de l’étape et les critères de sortie avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Documentez conjointement le parcours normal et les scénarios de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations ultérieures. Imposez une approbation humaine pour les actions qui entraînent des dépenses ou modifient des données de production. Une connexion au niveau du temps de compilation ne garantit pas la complétude fonctionnelle. Pour l’étape Routing Functions The Heart, définissez les entrées, le responsable de l’étape et les critères de sortie avant de modifier le code. Les opérateurs doivent pouvoir relancer l’étape à partir d’un point de contrôle connu sans deviner l’état caché. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définites des vérifications de succès et refusez toute complétion partielle silencieuse.

def route_after_classification(state: DocumentProcessingState) -> str:
    if state["document_type"] == "UNKNOWN":
        return "unknown"
    elif state["confidence_score"] < 0.7:
        state["requires_human_review"] = True
        return "human_review"
    else:
        return "extract"
def route_after_extraction(state: DocumentProcessingState) -> str:
    if state["retry_count"] >= 3:
        state["requires_human_review"] = True
        return "human_review"
    elif not state.get("extracted_fields"):
        state["retry_count"] = state.get("retry_count", 0) + 1
        return "retry"
    else:
        return "validate"def route_after_validation(state: DocumentProcessingState) -> str:
    if state["validation_errors"]:
        if state["retry_count"] >= 3:
            state["requires_human_review"] = True
            return "human_review"
        state["retry_count"] = state.get("retry_count", 0) + 1
        return "retry"
    return "finalize"

Niveau 3 : Modèles de fiabilité de production

Lorsque vous travaillez sur l’étape de fiabilité de production du Niveau 3, notez d’abord les exigences : entrées requises, signal de succès et conséquences en cas d’échec partiel. Cette liste de contrôle permet de rester honnête lors des modifications ultérieures du code. Enregistrez les temps d’exécution ainsi que le coût des tokens ou des requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe d’un environnement de démonstration à des environnements partagés. Créez un point de contrôle après les étapes coûteuses. Le système de reprise ne doit pas facturer à nouveau la même appel d’LLM lorsque l’opérateur réessaie un nœud ultérieur.

Circuit Breaker avec Polly (backend C#)

Lorsque vous travaillez avec l’étape Circuit Breaker de Polly, notez d’abord le contrat : les entrées requises, le signal de succès, ainsi que ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Conservez la configuration en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les flags fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans avoir à lire l’ensemble du système. Créez des points de contrôle après les étapes coûteuses. Le mécanisme de reprise ne doit pas facturer à nouveau la même appel d’LLM lorsque un opérateur réessaie un nœud ultérieur.

// In .NET Core — Polly circuit breaker
services.AddHttpClient<ILLMGateway, LLMGateway>()
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .OrResult(r => (int)r.StatusCode >= 500)
            .CircuitBreakerAsync(
                handledEventsAllowedBeforeBreaking: 5,
                durationOfBreak: TimeSpan.FromSeconds(60),
                onBreak: (ex, duration) =>
                    logger.LogWarning("Circuit breaker opened for {Duration}s", duration),
                onReset: () =>
                    logger.LogInformation("Circuit breaker reset")
            )
    )
    .AddPolicyHandler(
        Policy<HttpResponseMessage>
            .Handle<HttpRequestException>()
            .WaitAndRetryAsync(
                retryCount: 3,
                sleepDurationProvider: attempt =>
                    TimeSpan.FromSeconds(Math.Pow(2, attempt)) + // Exponential
                    TimeSpan.FromMilliseconds(new Random().Next(0, 1000)) // Jitter
            )
    );

Régulation de la concurrence (Python)

Lors du travail sur l’étape Python de limitation de la concurrence, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives répétées, les contrôles humains et la gestion des messages non livrés font partie intégrante du produit, et non d’améliorations ultérieures. Enregistrez l’ID de la demande, l’ID du modèle et le temps de réponse pour chaque appel. Sans ces traces, les erreurs intermittentes du fournisseur ressemblent à des bugs de l’application. Lors du travail sur l’étape Python de limitation de la concurrence, notez d’abord le contrat : les entrées requises, le signal de succès et ce qui se passe en cas d’échec partiel. Cette liste de contrôle permet de garantir l’intégrité des modifications ultérieures du code. Considérez cette étape comme un contrat entre les entrées et les sorties validées. Donnez des noms aux artefacts, définez des vérifications de succès et refusez les terminations partielles silencieuses.

import asyncio
from asyncio import Semaphore
class ThrottledDocumentProcessor:
    def __init__(self, max_concurrent: int = 50):
        self._semaphore = Semaphore(max_concurrent)

    async def process_document(
        self,
        document_id: str,
        document_text: str
    ) -> dict:
        async with self._semaphore:
            config = {"configurable": {"thread_id": document_id}}
            result = await app.ainvoke(
                {
                    "document_id": document_id,
                    "document_text": document_text,
                    "document_chunks": [],
                    "validation_errors": [],
                    "retry_count": 0,
                    "confidence_score": 0.0,
                    "requires_human_review": False,
                    "processing_notes": "",
                    "final_result": None,
                },
                config=config,
            )
            return result

Pas de secours de GPT-4 vers GPT-3.5

La phase de transition de GPT-4 à GPT-3.5 fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un transcript idéal, un cas d’échec et la note de réversion avant d’élargir le périmètre. Enregistrez les temps d’exécution ainsi que le coût en tokens ou en requêtes à côté des résultats fonctionnels. Une visibilité précoce des coûts évite les factures inattendues lorsque le processus passe de la démonstration aux environnements partagés. Maintenez l’état du graphe plat et structuré. Les blocs imbriqués masquent l’identité du nœud qui a saisi chaque champ et perturbent la reprise après interruption.

async def classify_with_fallback(document_text: str) -> dict:
    models = [
        ("gpt-4", 0.95),       # Primary — higher confidence weight
        ("gpt-35-turbo", 0.80), # Fallback — lower confidence weight
    ]

    for deployment, confidence_multiplier in models:
        try:
            llm = AzureChatOpenAI(
                azure_deployment=deployment,
                temperature=0,
                request_timeout=30,
            )
            result = await classifier_chain.ainvoke({
                "document_text": document_text,
                "format_instructions": parser.get_format_instructions(),
            })
            result["confidence_score"] *= confidence_multiplier
            result["model_used"] = deployment
            return result
        except Exception as e:
            logger.warning(f"Model {deployment} failed: {e}. Trying next...")
            continue

    raise Exception("All models failed")

Tests de régression CI/CD pour l’ensemble de données idéal

La phase CI CD du Golden Dataset fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un enregistrement idéal, un cas d’échec et la note de rollback avant d’élargir le périmètre. Conservez les configurations en dehors du code de l’application. Les fichiers d’environnement, les bases de données secrètes et les indicateurs fonctionnels doivent être regroupés en un seul endroit que les opérateurs peuvent auditer sans devoir lire l’ensemble du graphique. Maintenez un état du graphique plat et typé. Les blocs imbriqués masquent l’identité du nœud qui a écrit tel champ, ce qui perturbe la reprise après interruption.

# golden_dataset.py
GOLDEN_DOCUMENTS = [
    {
        "id": "GD001",
        "text": "Employee: John Smith\nDate: 15-Mar-2026\nGross Pay: $4,500\nNet Pay: $3,200...",
        "expected_type": "PAYSLIP",
        "expected_fields": {
            "employee_name": "John Smith",
            "date": "2026-03-15",
            "gross_amount": 4500.0,
        },
        "min_confidence": 0.85,
    },
    # ... 200 more golden examples across all document types
]
async def run_regression_suite():
    passed = 0
    failed = 0
    failures = []

    for golden_doc in GOLDEN_DOCUMENTS:
        result = await processor.process_document(
            golden_doc["id"],
            golden_doc["text"]
        )

        # Check type
        if result["document_type"] != golden_doc["expected_type"]:
            failures.append({
                "id": golden_doc["id"],
                "expected_type": golden_doc["expected_type"],
                "actual_type": result["document_type"],
                "failure": "wrong_classification",
            })
            failed += 1
            continue

        # Check confidence
        if result["confidence_score"] < golden_doc["min_confidence"]:
            failures.append({
                "id": golden_doc["id"],
                "expected_confidence": golden_doc["min_confidence"],
                "actual_confidence": result["confidence_score"],
                "failure": "low_confidence",
            })
            failed += 1
            continue

        passed += 1

    pass_rate = passed / len(GOLDEN_DOCUMENTS)

    if pass_rate < 0.95:  # 95% pass rate required
        raise Exception(
            f"Regression suite failed: {pass_rate:.1%} pass rate "
            f"({failed} failures). Blocking deployment."
        )

    return {"passed": passed, "failed": failed, "pass_rate": pass_rate}

Niveau 4 : Observabilité

La phase d’observabilité du Niveau 4 fonctionne le mieux lorsqu’elle est considérée comme une surface mesurable. Capturez un enregistrement idéal, un cas d’échec et la note de rollback avant d’élargir le périmètre. Documentez ensemble le parcours normal et le parcours de récupération. Les tentatives de réessai, les contrôles manuels et le traitement des messages non livrés font partie intégrante du produit, et non d’une amélioration ultérieure.

Le suivi avec OpenTelemetry à chaque appel d’IA

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from azure.monitor.opentelemetry.exporter import AzureMonitorTraceExporter
tracer_provider = TracerProvider()
tracer_provider.add_span_processor(
    BatchSpanProcessor(
        AzureMonitorTraceExporter(
            connection_string=os.getenv("APPLICATIONINSIGHTS_CONNECTION_STRING")
        )
    )
)
trace.set_tracer_provider(tracer_provider)
tracer = trace.get_tracer(__name__)async def classify_document_traced(
    state: DocumentProcessingState,
) -> DocumentProcessingState:
    with tracer.start_as_current_span("classify_document") as span:
        span.set_attribute("document.id", state["document_id"])
        span.set_attribute("document.length", len(state["document_text"]))

        start_time = time.time()
        result = await classify_document(state)
        latency_ms = (time.time() - start_time) * 1000

        span.set_attribute("llm.document_type", result["document_type"])
        span.set_attribute("llm.confidence_score", result["confidence_score"])
        span.set_attribute("llm.latency_ms", latency_ms)
        span.set_attribute("llm.model", "gpt-4")
        span.set_attribute("llm.requires_human_review", result["requires_human_review"])

        return result

Métriques clés à suivre

METRICS_TO_TRACK = {
    # Reliability
    "classification_accuracy": "% correct vs golden dataset",
    "confidence_score_distribution": "histogram — drift detection",
    "human_review_rate": "% documents escalated — should stay < 5%",

    # Performance
    "end_to_end_latency_p50": "median processing time per document",
    "end_to_end_latency_p99": "tail latency — SLA violations",
    "llm_call_latency": "broken down by model and node",

    # Cost
    "tokens_per_document": "input + output tokens",
    "cache_hit_rate": "Redis embedding cache effectiveness",
    "monthly_api_spend": "trend by document type",

    # Business
    "documents_processed_per_hour": "throughput",
    "analyst_hours_saved_weekly": "business impact",
    "error_rate_by_document_type": "which types fail most",
}

Les résultats

Leçons clés tirées

Conclusion

Liste de contrôle opérationnelle