Aller au contenu principal

QnA multi-documents

Interroger plusieurs documents simultanément

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. Cette leçon couvre les stratégies pour le QnA multi-documents.

Approche 1 : documents multiples dans un seul appel

Vous pouvez inclure plusieurs documents dans un seul message :

import os
from mistralai 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)

Limites : la somme des documents ne doit pas dépasser la fenêtre de contexte du modèle. Pour de gros documents, cette approche atteint vite ses limites.

Approche 2 : OCR séparé puis QnA sur le texte

Pour les gros volumes, la stratégie recommandée est de séparer l’OCR et le QnA :

import os
from mistralai 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 ?"
            )
        }
    ]
)

Approche 3 : RAG documentaire

Pour des corpus importants (dizaines ou centaines de documents), utilisez un pipeline RAG :

import os
from mistralai 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]]

Comparaison des approches

Chaque approche a ses avantages :

  • Documents dans un seul appel : simple, idéal pour 2-3 documents courts
  • OCR séparé + QnA : flexible, permet de contrôler ce qui est envoyé au LLM
  • RAG documentaire : scalable, indispensable pour les gros corpus

Workflow de comparaison documentaire

Voici un exemple complet de comparaison de factures :

import os
from pydantic import BaseModel, Field
from mistralai 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_base64",
                "document_base64": doc_b64
            },
            document_annotation_format=response_format_from_pydantic_model(
                ResumeFacture
            )
        )

        for page in response.pages:
            if page.document_annotation:
                facture = ResumeFacture.model_validate_json(
                    page.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