Aller au contenu principal

Prompt versioning et gestion de catalogue

Prompt versioning et gestion de catalogue

En production, vos prompts évoluent autant que votre code. Sans versioning, vous perdez la trace des modifications, vous ne pouvez pas revenir en arrière, et vous ne savez pas quelle version tourne en production. Cette leçon vous montre comment gérer vos prompts comme des artefacts logiciels.

Pourquoi versionner les prompts

Un changement de prompt peut modifier radicalement le comportement de votre application. Contrairement au code, ces changements sont souvent subtils et difficiles à détecter sans tests automatisés. Le versioning vous permet de :

  • Tracer chaque modification et son auteur
  • Revenir en arrière en cas de régression
  • Comparer les performances entre versions
  • Auditer les changements pour la conformité (RGPD, Qualiopi)

Structure d’un catalogue de prompts

import json
from datetime import datetime
from pathlib import Path

class PromptCatalog:
    """Gestionnaire de catalogue de prompts avec versioning."""

    def __init__(self, catalog_dir: str = "prompts/"):
        self.catalog_dir = Path(catalog_dir)
        self.catalog_dir.mkdir(parents=True, exist_ok=True)

    def save_prompt(self, name: str, system_prompt: str,
                    model: str, temperature: float,
                    metadata: dict = None) -> str:
        """Sauvegarde une nouvelle version d'un prompt."""
        prompt_dir = self.catalog_dir / name
        prompt_dir.mkdir(exist_ok=True)

        # Déterminer le numéro de version
        versions = sorted(prompt_dir.glob("v*.json"))
        version = len(versions) + 1
        version_str = f"v{version:03d}"

        # Sauvegarder
        prompt_data = {
            "name": name,
            "version": version_str,
            "created_at": datetime.now().isoformat(),
            "model": model,
            "temperature": temperature,
            "system_prompt": system_prompt,
            "metadata": metadata or {},
            "status": "draft"  # draft | active | deprecated
        }

        filepath = prompt_dir / f"{version_str}.json"
        filepath.write_text(json.dumps(prompt_data, indent=2,
                                        ensure_ascii=False))

        return version_str

    def get_prompt(self, name: str,
                   version: str = "latest") -> dict:
        """Récupère un prompt par nom et version."""
        prompt_dir = self.catalog_dir / name

        if version == "latest":
            versions = sorted(prompt_dir.glob("v*.json"))
            if not versions:
                raise FileNotFoundError(f"Prompt '{name}' non trouvé")
            filepath = versions[-1]
        else:
            filepath = prompt_dir / f"{version}.json"

        return json.loads(filepath.read_text())

    def get_active(self, name: str) -> dict:
        """Récupère la version active d'un prompt."""
        prompt_dir = self.catalog_dir / name
        for filepath in sorted(prompt_dir.glob("v*.json"),
                               reverse=True):
            data = json.loads(filepath.read_text())
            if data["status"] == "active":
                return data
        raise FileNotFoundError(
            f"Aucune version active pour '{name}'"
        )

    def activate(self, name: str, version: str):
        """Active une version et désactive les autres."""
        prompt_dir = self.catalog_dir / name

        for filepath in prompt_dir.glob("v*.json"):
            data = json.loads(filepath.read_text())
            if data["version"] == version:
                data["status"] = "active"
            elif data["status"] == "active":
                data["status"] = "deprecated"
            filepath.write_text(json.dumps(data, indent=2,
                                            ensure_ascii=False))

    def list_versions(self, name: str) -> list[dict]:
        """Liste toutes les versions d'un prompt."""
        prompt_dir = self.catalog_dir / name
        versions = []
        for filepath in sorted(prompt_dir.glob("v*.json")):
            data = json.loads(filepath.read_text())
            versions.append({
                "version": data["version"],
                "status": data["status"],
                "created_at": data["created_at"],
                "model": data["model"]
            })
        return versions

Utilisation dans votre application

from openai import OpenAI

client = OpenAI()
catalog = PromptCatalog("prompts/")

# Sauvegarder un prompt
catalog.save_prompt(
    name="classificateur-tickets",
    system_prompt="""Tu es un classificateur de tickets de support.
Catégorise chaque ticket parmi : bug, feature, question, billing.
Réponds en JSON avec category, priority (1-5), summary.""",
    model="gpt-5.3",
    temperature=0.1,
    metadata={
        "author": "marie.dupont",
        "description": "Classification automatique des tickets Zendesk",
        "changelog": "Version initiale"
    }
)

# En production : utiliser la version active
def classifier_ticket(ticket_text: str) -> dict:
    """Classifie un ticket en utilisant le prompt actif."""
    prompt_config = catalog.get_active("classificateur-tickets")

    response = client.responses.create(
        model=prompt_config["model"],
        instructions=prompt_config["system_prompt"],
        input=ticket_text,
        temperature=prompt_config["temperature"]
    )

    return {
        "result": json.loads(response.output_text),
        "prompt_version": prompt_config["version"]
    }

Versioning avec Git

Pour les équipes, stockez vos prompts dans un dépôt Git dédié :

prompts/
  classificateur-tickets/
    prompt.yaml
    tests/
      test_cases.json
    CHANGELOG.md
  extracteur-factures/
    prompt.yaml
    tests/
      test_cases.json
    CHANGELOG.md

Format YAML pour la lisibilité :

# prompts/classificateur-tickets/prompt.yaml
name: classificateur-tickets
model: gpt-5.3
temperature: 0.1
max_output_tokens: 500
system_prompt: |
  Tu es un classificateur de tickets de support.
  Catégorise chaque ticket parmi : bug, feature, question, billing.
  Réponds en JSON avec category, priority (1-5), summary.
schema:
  type: object
  properties:
    category:
      type: string
      enum: [bug, feature, question, billing]
    priority:
      type: integer
    summary:
      type: string
  required: [category, priority, summary]
  additionalProperties: false

Bonnes pratiques

  1. Un prompt = un fichier : ne mélangez pas plusieurs prompts dans un seul fichier
  2. Changelog obligatoire : documentez chaque modification et sa raison
  3. Tests associés : chaque prompt a un jeu de test qui vérifie son comportement
  4. Revue de prompt : les modifications de prompts passent par la même revue de code que le code
  5. Rollback rapide : pouvoir revenir à la version précédente en moins d’une minute
  6. Tags de déploiement : marquez les versions déployées en production

Mise en pratique

  1. Créez un PromptCatalog pour 3 prompts de votre application
  2. Sauvegardez 2-3 versions de chaque prompt avec des modifications progressives
  3. Implémentez le mécanisme d’activation/désactivation
  4. Ajoutez un jeu de tests pour chaque prompt
  5. Simulez un rollback après une régression

Points clés à retenir

  • Les prompts sont des artefacts logiciels qui nécessitent du versioning
  • Chaque version est un fichier JSON ou YAML avec métadonnées
  • Le statut (draft, active, deprecated) permet le déploiement progressif
  • Stockez les prompts dans Git avec des tests et un changelog
  • Le rollback doit être instantané en cas de régression