Aller au contenu principal

Extraction d'entités structurées

Extraction d’entités structurées

L’extraction d’entités structurées est l’un des cas d’usage les plus demandés en production : transformer du texte libre (emails, documents, pages web) en données exploitables par votre application. En combinant le Structured Output avec des prompts spécialisés, vous pouvez construire des pipelines d’extraction fiables sans entraîner de modèle NER dédié.

Pipeline d’extraction de base

import json
from openai import OpenAI

client = OpenAI()

def extraire_entites(texte: str, schema: dict,
                     instructions: str = "") -> dict:
    """Extrait des entités structurées d'un texte libre."""
    system = (
        "Tu es un extracteur d'entités spécialisé. "
        "Extrais UNIQUEMENT les informations présentes dans le texte. "
        "Si une information est absente, utilise null. "
        "Ne déduis JAMAIS d'informations non explicitement mentionnées."
    )
    if instructions:
        system += f"\n\nInstructions supplémentaires : {instructions}"

    response = client.responses.create(
        model="gpt-5.3",
        instructions=system,
        input=f"Texte à analyser :\n\n{texte}",
        text={
            "format": {
                "type": "json_schema",
                "name": "extraction",
                "schema": schema,
                "strict": True
            }
        },
        temperature=0.0
    )
    return json.loads(response.output_text)

Exemple 1 : extraction de CV

cv_schema = {
    "type": "object",
    "properties": {
        "identite": {
            "type": "object",
            "properties": {
                "nom_complet": {"type": "string"},
                "email": {"type": ["string", "null"]},
                "telephone": {"type": ["string", "null"]},
                "localisation": {"type": ["string", "null"]},
                "linkedin": {"type": ["string", "null"]}
            },
            "required": ["nom_complet", "email", "telephone",
                          "localisation", "linkedin"],
            "additionalProperties": False
        },
        "titre_actuel": {"type": ["string", "null"]},
        "annees_experience": {"type": ["integer", "null"]},
        "competences": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "nom": {"type": "string"},
                    "niveau": {
                        "type": "string",
                        "enum": ["debutant", "intermediaire",
                                 "avance", "expert"]
                    },
                    "categorie": {
                        "type": "string",
                        "enum": ["langage", "framework", "outil",
                                 "methodologie", "soft_skill"]
                    }
                },
                "required": ["nom", "niveau", "categorie"],
                "additionalProperties": False
            }
        },
        "experiences": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "entreprise": {"type": "string"},
                    "poste": {"type": "string"},
                    "date_debut": {"type": "string"},
                    "date_fin": {"type": ["string", "null"]},
                    "description": {"type": "string"}
                },
                "required": ["entreprise", "poste", "date_debut",
                              "date_fin", "description"],
                "additionalProperties": False
            }
        },
        "formations": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "etablissement": {"type": "string"},
                    "diplome": {"type": "string"},
                    "annee": {"type": ["string", "null"]}
                },
                "required": ["etablissement", "diplome", "annee"],
                "additionalProperties": False
            }
        },
        "langues": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "langue": {"type": "string"},
                    "niveau": {
                        "type": "string",
                        "enum": ["a1", "a2", "b1", "b2",
                                 "c1", "c2", "natif"]
                    }
                },
                "required": ["langue", "niveau"],
                "additionalProperties": False
            }
        }
    },
    "required": ["identite", "titre_actuel", "annees_experience",
                  "competences", "experiences", "formations", "langues"],
    "additionalProperties": False
}

cv_texte = """
Marie Dupont - Développeuse Full Stack Senior
Email: [email protected] | Tel: +33 6 12 34 56 78
Paris, France | linkedin.com/in/mariedupont

8 ans d'expérience en développement web.

Compétences : Python (expert), TypeScript (avancé), React (avancé),
Django (expert), PostgreSQL (avancé), Docker (intermédiaire),
Scrum (avancé)

Expérience :
- TechCorp (2022-présent) : Lead Developer Full Stack
  Architecte principal d'une plateforme SaaS B2B, équipe de 6 dev.
- StartupAI (2019-2022) : Développeuse Senior
  Développement d'APIs ML en Python/FastAPI.

Formation :
- EPITECH Paris, Master Informatique (2017)

Langues : Français (natif), Anglais (C1), Espagnol (B1)
"""

result = extraire_entites(cv_texte, cv_schema)

Exemple 2 : extraction d’emails commerciaux

email_schema = {
    "type": "object",
    "properties": {
        "intention": {
            "type": "string",
            "enum": ["demande_devis", "reclamation", "information",
                     "commande", "partenariat", "autre"]
        },
        "urgence": {
            "type": "string",
            "enum": ["critique", "haute", "normale", "basse"]
        },
        "expediteur": {
            "type": "object",
            "properties": {
                "nom": {"type": ["string", "null"]},
                "entreprise": {"type": ["string", "null"]},
                "email": {"type": ["string", "null"]}
            },
            "required": ["nom", "entreprise", "email"],
            "additionalProperties": False
        },
        "produits_mentionnes": {
            "type": "array",
            "items": {"type": "string"}
        },
        "montants_mentionnes": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "valeur": {"type": "number"},
                    "devise": {"type": "string"},
                    "contexte": {"type": "string"}
                },
                "required": ["valeur", "devise", "contexte"],
                "additionalProperties": False
            }
        },
        "dates_cles": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "date": {"type": "string"},
                    "signification": {"type": "string"}
                },
                "required": ["date", "signification"],
                "additionalProperties": False
            }
        },
        "actions_requises": {
            "type": "array",
            "items": {"type": "string"}
        },
        "resume": {"type": "string"}
    },
    "required": ["intention", "urgence", "expediteur",
                  "produits_mentionnes", "montants_mentionnes",
                  "dates_cles", "actions_requises", "resume"],
    "additionalProperties": False
}

Extraction en batch

Pour traiter de gros volumes, parallélisez les extractions :

import asyncio
from openai import AsyncOpenAI

async_client = AsyncOpenAI()

async def extraire_batch(textes: list[str], schema: dict,
                          max_concurrent: int = 10) -> list[dict]:
    """Extraction parallèle avec contrôle de concurrence."""
    semaphore = asyncio.Semaphore(max_concurrent)

    async def extract_one(texte: str) -> dict:
        async with semaphore:
            response = await async_client.responses.create(
                model="gpt-5.3",
                instructions="Extrais les entités du texte fourni. "
                             "Utilise null pour les champs absents.",
                input=texte,
                text={
                    "format": {
                        "type": "json_schema",
                        "name": "extraction",
                        "schema": schema,
                        "strict": True
                    }
                },
                temperature=0.0
            )
            return json.loads(response.output_text)

    tasks = [extract_one(t) for t in textes]
    return await asyncio.gather(*tasks, return_exceptions=True)

Évaluer la qualité de l’extraction

def evaluer_extraction(attendu: dict, obtenu: dict,
                       champs: list[str]) -> dict:
    """Compare l'extraction obtenue avec la référence."""
    correct = 0
    total = len(champs)
    erreurs = []

    for champ in champs:
        val_attendue = attendu.get(champ)
        val_obtenue = obtenu.get(champ)

        if val_attendue == val_obtenue:
            correct += 1
        else:
            erreurs.append({
                "champ": champ,
                "attendu": val_attendue,
                "obtenu": val_obtenue
            })

    return {
        "precision": correct / total if total > 0 else 0,
        "correct": correct,
        "total": total,
        "erreurs": erreurs
    }

Mise en pratique

  1. Choisissez un type de document de votre domaine (contrats, factures, rapports)
  2. Définissez un schéma d’extraction avec au moins 10 champs
  3. Créez 5 documents d’exemple et leurs extractions attendues
  4. Implémentez le pipeline et mesurez la précision
  5. Identifiez les champs où le modèle fait le plus d’erreurs et ajustez le prompt

Points clés à retenir

  • L’extraction structurée remplace les modèles NER dédiés pour la plupart des cas
  • Utilisez temperature=0.0 pour maximiser la cohérence
  • Rendez les champs facultatifs nullable plutôt qu’optionnels
  • Parallélisez avec asyncio pour les gros volumes
  • Évaluez systématiquement la qualité avec un jeu de test annoté
  • Les instructions du system prompt guident le contenu, le schéma contraint la structure