Aller au contenu principal

Implémenter les Citations en Python

Mis à jour le 29 juillet 2026

Le pipeline complet de citations

Vous allez maintenant assembler un pipeline RAG citant ses sources, du premier appel API jusqu’à la structure de données que consommera votre interface. Le principe à garder en tête est que le modèle ne va jamais chercher les documents lui-même : il signale qu’il aurait besoin d’une recherche, vous exécutez cette recherche dans votre propre base, puis vous lui renvoyez les extraits accompagnés de leurs identifiants. C’est cet aller-retour qui produit une réponse dont chaque affirmation reste rattachable à un document réel.

Concrètement, un cycle complet demande deux appels à chat.complete. Le premier déclenche le tool call, le second génère la réponse citée une fois les sources injectées dans l’historique. Nous allons dérouler ce cycle sur un cas simple : un assistant documentaire interne interrogé sur les bonnes pratiques de déploiement d’un LLM.

Cadrer le modèle dès le message système

Le message système fixe le contrat : répondre à partir des sources, citer systématiquement, et surtout admettre l’absence d’information plutôt que de la combler. Cette dernière consigne est celle qui fait la différence en production, car c’est sur les questions mal couvertes par la base documentaire que les réponses dérivent.

from mistralai import Mistral
import json

client = Mistral(api_key="votre-clé-api")

# Message système qui encourage les citations
system_message = {
    "role": "system",
    "content": (
        "Vous êtes un assistant documentaire. "
        "Répondez aux questions en vous appuyant sur les sources fournies. "
        "Citez systématiquement vos sources. "
        "Si l'information ne figure dans aucune source, dites-le explicitement."
    )
}

user_message = {
    "role": "user",
    "content": "Quelles sont les bonnes pratiques pour déployer un LLM en production ?"
}

messages = [system_message, user_message]

Déclarer l’outil de recherche

L’outil décrit à quoi ressemble une recherche documentaire dans votre système : ici une simple fonction search_documentation prenant une requête textuelle. Le premier appel n’attend pas de réponse rédigée ; il attend que le modèle formule lui-même la requête qu’il juge pertinente, ce qui vous évite d’écrire une couche de reformulation.

reference_tool = {
    "type": "function",
    "function": {
        "name": "search_documentation",
        "description": "Recherche dans la base documentaire",
        "parameters": {
            "type": "object",
            "properties": {
                "query": {
                    "type": "string",
                    "description": "Requête de recherche"
                }
            },
            "required": ["query"]
        }
    }
}

# Premier appel : le modèle demande à utiliser l'outil
response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages,
    tools=[reference_tool]
)

Renvoyer les résultats sous forme de Référence Objects

C’est l’étape structurante. Les résultats de votre moteur de recherche remontent dans un ToolMessage dont le contenu ne se limite pas à du texte : il embarque une liste de Référence Objects, chacun portant un reference_id, une URL, un titre et les extraits retenus. Ce sont ces identifiants que le modèle réutilisera pour marquer ses citations, et que votre frontend résoudra ensuite en liens.

from mistralai.models import ToolMessage

# Simuler les résultats de recherche documentaire
tool_results = {
    "content": "Voici les sources trouvées.",
    "references": [
        {
            "type": "reference",
            "reference_id": "ref-001",
            "url": "https://docs.example.com/deployment-guide",
            "title": "Guide de déploiement LLM",
            "snippets": [
                "Toujours configurer des rate limits pour protéger le service.",
                "Monitorer la latence P99 et le taux d'erreur en continu."
            ]
        },
        {
            "type": "reference",
            "reference_id": "ref-002",
            "url": "https://docs.example.com/security",
            "title": "Sécurité des applications IA",
            "snippets": [
                "Filtrer les entrées utilisateur pour prévenir les injections.",
                "Ne jamais exposer les clés API côté client."
            ]
        },
        {
            "type": "reference",
            "reference_id": "ref-003",
            "url": "https://docs.example.com/scaling",
            "title": "Scalabilité et performance",
            "snippets": [
                "Utiliser le caching sémantique pour réduire les appels API.",
                "Dimensionner les instances selon le trafic de pointe."
            ]
        }
    ]
}

# Ajouter le tool call et la réponse à l'historique
tool_call = response.choices[0].message.tool_calls[0]
messages.append(response.choices[0].message)

messages.append(
    ToolMessage(
        tool_call_id=tool_call.id,
        name="search_documentation",
        content=json.dumps(tool_results, ensure_ascii=False)
    )
)

Remarquez que le message d’origine du modèle est réinjecté dans l’historique avant le ToolMessage : sans ce couple tool call / tool result, l’API refuse la continuation. Le second appel n’a alors plus besoin de la définition des outils, puisque la recherche a déjà eu lieu.

# Deuxième appel : le modèle génère une réponse avec citations
final_response = client.chat.complete(
    model="mistral-large-latest",
    messages=messages
)

Séparer le texte des références

La réponse citée n’est plus une chaîne de caractères mais une séquence de chunks alternant prose et marques de citation. Le parsing consiste à parcourir cette séquence et à trier : les TextChunk reconstituent le texte lisible, les ReferenceChunk livrent les reference_ids effectivement mobilisés. Cette distinction vous donne au passage une information précieuse, à savoir quelles sources le modèle a réellement utilisées parmi celles que vous lui avez fournies.

from mistralai.models import TextChunk, ReferenceChunk

def parse_cited_response(response):
    """Extrait le texte et les références d'une réponse citée."""
    content_parts = response.choices[0].message.content
    text_parts = []
    cited_refs = set()

    for chunk in content_parts:
        if isinstance(chunk, TextChunk):
            text_parts.append(chunk.text)
        elif isinstance(chunk, ReferenceChunk):
            for ref_id in chunk.reference_ids:
                cited_refs.add(ref_id)

    return {
        "text": "".join(text_parts),
        "references_used": list(cited_refs)
    }

result = parse_cited_response(final_response)
print("=== Réponse ===")
print(result["text"])
print("\n=== Sources citées ===")
for ref_id in result["references_used"]:
    print(f"  - {ref_id}")

Préparer l’affichage côté frontend

Aplatir la réponse en texte brut vous ferait perdre la position des citations, donc leur intérêt. Mieux vaut conserver la séquence et l’enrichir : chaque ReferenceChunk est résolu contre une table des références pour porter le titre et l’URL de chaque source. Votre interface reçoit ainsi une liste ordonnée de blocs qu’elle peut rendre en liens cliquables, en infobulles ou en numéros de citation interactifs, sans jamais avoir à réinterpréter le texte.

def format_for_frontend(response, reference_map):
    """Formate la réponse avec citations pour le frontend."""
    content_parts = response.choices[0].message.content
    formatted = []

    for chunk in content_parts:
        if isinstance(chunk, TextChunk):
            formatted.append({
                "type": "text",
                "content": chunk.text
            })
        elif isinstance(chunk, ReferenceChunk):
            refs = []
            for ref_id in chunk.reference_ids:
                if ref_id in reference_map:
                    refs.append({
                        "id": ref_id,
                        "title": reference_map[ref_id]["title"],
                        "url": reference_map[ref_id]["url"]
                    })
            formatted.append({
                "type": "citation",
                "references": refs
            })

    return formatted

Avant d’ouvrir ce pipeline à votre base documentaire complète, faites-le tourner avec trois à cinq sources seulement, comme dans l’exemple ci-dessus. Vous verrez immédiatement si le modèle cite au bon endroit, s’il mobilise plusieurs documents ou s’il s’accroche systématiquement au premier, et vous corrigerez le message système avant que le comportement ne se noie dans le volume.

Points clés à retenir

  • Le pipeline suit un flux en deux appels : tool call puis réponse citée
  • Les références sont passées via ToolMessage avec les Référence Objects
  • La réponse contient un mix de TextChunk et ReferenceChunk à parser
  • Structurez la sortie pour que votre frontend puisse afficher des citations interactives
  • Testez avec 3-5 sources pour valider le comportement avant de passer en production