Aller au contenu principal
Leçon 14 sur 16 · Document AI — OCR et Intelligence Documentaire

QnA multi-documents

Leçon 14 sur 16 Mis à jour le 3 novembre 2026 Développeur
Sur cette page
  1. Quand la réponse est répartie sur plusieurs fichiers
  2. Tout envoyer dans un seul appel
  3. Séparer l’extraction de l’interrogation
  4. Passer au RAG documentaire
  5. Un workflow de comparaison de bout en bout
  6. Points clés à retenir

Mise à jour du 2 novembre 2026 — OCR. OCR 4.0 (mistral-ocr-4-0) est retiré depuis le 30 septembre 2026. Utilisez OCR 4.1 (mistral-ocr-4-1), en disponibilité générale depuis le 31 août au même prix, ou l’alias mistral-ocr-latest, qui pointe vers lui depuis le 16 juillet. OCR 4.1 ajoute des scores de confiance au niveau des blocs. Complément du 3 novembre 2026 : un PDF local se transmet en base64 sous la forme {"type": "document_url", "document_url": "data:application/pdf;base64,…"} ; le type document_base64 des anciens exemples n’existe ni dans la documentation ni dans le SDK mistralai, et ces exemples auraient échoué. Sources : OCR Processor · Document QnA. Corrigé le 3 novembre 2026 : le code lisait page.document_annotation page par page ; dans le SDK mistralai 3.7.0, l’annotation de document est un champ unique de la réponse (response.document_annotation), et l’annotation d’une image se lit dans image.image_annotation.

Quand la réponse est répartie sur plusieurs fichiers

Dans de nombreux cas professionnels, la réponse à une question ne se trouve pas dans un seul document. Vous devez comparer des contrats, croiser des factures avec des bons de commande, ou analyser une série de rapports. Trois stratégies existent, et le bon réflexe consiste à choisir en fonction du volume plutôt que de partir d’emblée sur la plus sophistiquée.

Tout envoyer dans un seul appel

La méthode la plus directe consiste à empiler plusieurs blocs document_url dans le même message. Le modèle voit les deux contrats côte à côte et peut réellement les comparer, ce qu’aucun traitement document par document ne permettrait. On passe ici sur mistral-large-latest parce que la comparaison relève du raisonnement, pas de la simple lecture.

import os
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

response = client.chat.complete(
    model="mistral-large-latest",
    messages=[
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": (
                        "Compare les conditions de paiement "
                        "de ces deux contrats."
                    )
                },
                {
                    "type": "document_url",
                    "document_url": "https://exemple.com/contrat-A.pdf"
                },
                {
                    "type": "document_url",
                    "document_url": "https://exemple.com/contrat-B.pdf"
                }
            ]
        }
    ]
)

print(response.choices[0].message.content)

La contrainte apparaît vite : la somme des documents ne doit pas dépasser la fenêtre de contexte du modèle. Deux plaquettes de dix pages passent sans difficulté ; deux contrats-cadres de cent pages, non.

Séparer l’extraction de l’interrogation

Pour les gros volumes, la stratégie recommandée dissocie les deux temps. Vous passez chaque document à l’OCR, vous conservez le Markdown obtenu, puis vous construisez vous-même le contexte envoyé au LLM. L’intérêt est le contrôle : la troncature à 5 000 caractères visible dans l’exemple, le nom de chaque document conservé en titre de section, le message système qui campe le rôle d’assistant juridique. Vous décidez précisément de ce qui entre dans la fenêtre de contexte, au lieu de le subir.

import os
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

def extraire_texte(url: str) -> str:
    """Extrait le texte d'un document via OCR."""
    response = client.ocr.process(
        model="mistral-ocr-latest",
        document={"type": "document_url", "document_url": url}
    )
    return "\n\n".join(p.markdown for p in response.pages)


# Extraction OCR de chaque document
documents = {
    "Contrat A": extraire_texte("https://exemple.com/contrat-A.pdf"),
    "Contrat B": extraire_texte("https://exemple.com/contrat-B.pdf"),
    "Contrat C": extraire_texte("https://exemple.com/contrat-C.pdf"),
}

# Construction du prompt avec le contexte
contexte = "\n\n---\n\n".join(
    f"## {nom}\n\n{texte[:5000]}"  # Limiter si nécessaire
    for nom, texte in documents.items()
)

response = client.chat.complete(
    model="mistral-large-latest",
    messages=[
        {
            "role": "system",
            "content": (
                "Vous êtes un assistant juridique. Analysez les "
                "documents fournis et répondez aux questions."
            )
        },
        {
            "role": "user",
            "content": (
                f"Voici les documents :\n\n{contexte}\n\n"
                f"Question : Quel contrat offre les meilleures "
                f"conditions de résiliation anticipée ?"
            )
        }
    ]
)

Passer au RAG documentaire

Au-delà de quelques dizaines de documents, aucune fenêtre de contexte ne suivra. Il faut alors ne plus envoyer que les passages pertinents, ce qui suppose de découper les textes, de les vectoriser avec mistral-embed, puis de rechercher par similarité au moment de la question. Le découpage avec chevauchement de 200 caractères évite qu’une phrase coupée en deux perde son sens entre deux chunks, et le top_k détermine combien de passages accompagneront finalement la question.

import os
from mistralai.client import Mistral

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

def creer_embeddings(texte: str) -> list[float]:
    """Crée les embeddings d'un texte."""
    response = client.embeddings.create(
        model="mistral-embed",
        inputs=[texte]
    )
    return response.data[0].embedding


def chunker(texte: str, taille: int = 1000, chevauchement: int = 200) -> list[str]:
    """Découpe un texte en chunks avec chevauchement."""
    chunks = []
    debut = 0
    while debut < len(texte):
        fin = debut + taille
        chunk = texte[debut:fin]
        chunks.append(chunk)
        debut = fin - chevauchement
    return chunks


# Pipeline RAG simplifié
def indexer_document(nom: str, texte: str) -> list[dict]:
    """Indexe un document pour la recherche."""
    chunks = chunker(texte)
    index = []

    for i, chunk in enumerate(chunks):
        embedding = creer_embeddings(chunk)
        index.append({
            "document": nom,
            "chunk_id": i,
            "texte": chunk,
            "embedding": embedding
        })

    return index


def rechercher(question: str, index: list[dict], top_k: int = 5) -> list[dict]:
    """Recherche les chunks les plus pertinents."""
    q_embedding = creer_embeddings(question)

    # Calcul de similarité cosinus
    import numpy as np
    scores = []
    for item in index:
        score = np.dot(q_embedding, item["embedding"])
        scores.append((score, item))

    scores.sort(key=lambda x: x[0], reverse=True)
    return [item for _, item in scores[:top_k]]

Résumons le raisonnement de sélection. L’envoi de plusieurs documents dans un seul appel reste imbattable de simplicité et convient à deux ou trois documents courts. L’OCR séparé suivi du QnA apporte la flexibilité et vous rend maître de ce qui est envoyé au LLM, au prix d’un peu de code. Le RAG documentaire, plus lourd à mettre en place, devient indispensable dès que le corpus grossit, parce qu’il est le seul à passer à l’échelle.

Un workflow de comparaison de bout en bout

Les deux techniques se marient : rien n’interdit d’extraire d’abord des données structurées par annotation, puis de confier ces données au LLM pour l’analyse. Sur un lot de factures, chaque document est réduit à trois champs — fournisseur, montant, date — avant qu’une seule requête ne demande au modèle d’y repérer tendances et anomalies. Le coût en tokens devient négligeable, et l’analyse porte sur des chiffres extraits proprement plutôt que sur du texte interprété au vol.

import os
from pydantic import BaseModel, Field
from mistralai.client import Mistral
from mistralai.extra import response_format_from_pydantic_model

client = Mistral(api_key=os.environ["MISTRAL_API_KEY"])

class ResumeFacture(BaseModel):
    fournisseur: str = Field(..., description="Nom du fournisseur")
    montant_ttc: float = Field(..., description="Montant TTC")
    date: str = Field(..., description="Date (YYYY-MM-DD)")

def comparer_factures(chemins: list[str]) -> str:
    """Compare plusieurs factures et retourne une analyse."""
    import base64

    factures = []
    for chemin in chemins:
        with open(chemin, "rb") as f:
            doc_b64 = base64.b64encode(f.read()).decode("utf-8")

        response = client.ocr.process(
            model="mistral-ocr-latest",
            document={
                "type": "document_url",
                "document_url": f"data:application/pdf;base64,{doc_b64}"
            },
            document_annotation_format=response_format_from_pydantic_model(
                ResumeFacture
            )
        )

        if response.document_annotation:
            facture = ResumeFacture.model_validate_json(
                response.document_annotation
            )
            factures.append(facture)

    # Analyse comparative via LLM
    resume = "\n".join(
        f"- {f.fournisseur}: {f.montant_ttc}€ ({f.date})"
        for f in factures
    )

    response = client.chat.complete(
        model="mistral-small-latest",
        messages=[
            {
                "role": "user",
                "content": (
                    f"Voici {len(factures)} factures :\n{resume}\n\n"
                    f"Analyse les tendances et anomalies."
                )
            }
        ]
    )

    return response.choices[0].message.content

Points clés à retenir

  • Trois approches pour le QnA multi-documents : inline, OCR séparé, RAG
  • L’approche inline convient pour 2-3 documents courts
  • L’OCR séparé + QnA offre plus de contrôle sur les tokens consommés
  • Le RAG est indispensable pour les corpus importants (dizaines de documents)
  • Combinez les annotations structurées avec le QnA pour des analyses précises