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