Aller au contenu

RAG versionné avec Strapi et Revisionary

Apprenez à créer un pipeline RAG qui interroge le contenu de Strapi tout en respectant l’historique des versions, grâce à Revisionary pour des réponses à jour et traçables.

Par l’équipe Revisionary · · 5 min de lecture

photo of computer cables
Photo de Kvistholt Photography sur Unsplash

Strapi peut stocker votre contenu, et Revisionary enregistre chaque modification. En exportant la dernière version de chaque entrée vers un magasin vectoriel, vous pouvez construire un système de Retrieval‑Augmented Generation (RAG) qui répond toujours à partir du contenu le plus récent approuvé tout en conservant une traçabilité complète. Les étapes sont simples :

  1. Installez et configurez Revisionary afin que chaque modification soit versionnée.
  2. Exportez la version actuelle du contenu que vous souhaitez augmenter.
  3. Indexez ces textes dans une base vectorielle, en stockant l’ID de version Revisionary associé.
  4. Lorsqu’un utilisateur pose une question, récupérez les vecteurs les plus pertinents, obtenez la version exacte depuis Strapi, et alimentez le texte à votre LLM.
  5. Si le contenu change, ré‑indexez uniquement les entrées concernées ; l’ID de version vous permet de retracer quelle réponse a utilisé quel contenu.

Voici un guide complet, prêt pour la production.

1. Prérequis

  • Un projet Strapi 5 avec le plugin Revisionary installé (téléchargé sous forme de zip et activé dans config/plugins.ts).
  • Node 18+ et une base de données PostgreSQL/MySQL/SQLite (selon les besoins de Strapi).
  • Accès à une base vectorielle (par ex. : Pinecone, Weaviate ou une solution open‑source comme Qdrant).
  • Un point d’accès LLM qui accepte un prompt et renvoie une complétion (OpenAI, Anthropic, etc.).
  • Connaissances de base en JavaScript/TypeScript et en async/await.

2. Installer Revisionary et vérifier la versionnage

bash
# Depuis la racine de votre projet Strapi
npm install ./revisionary-plugin.zip   # le zip que vous avez acheté

Activez le plugin dans config/plugins.ts :

js
module.exports = {
  // ...autres plugins
  revisionary: {
    enabled: true,
    // optionnel : configurer la rétention, la confidentialité des audits, etc.
  },
};

Démarrez Strapi et confirmez que le panneau « Historique des versions » apparaît lorsque vous modifiez un type de contenu. Voir le guide [/docs/everyday-use/history] pour plus de détails.

3. Choisir le contenu à augmenter

Tous les collections ne doivent pas être recherchables. Les candidats typiques sont :

  • Articles de blog
  • Documentation produit
  • Articles de base de connaissances
  • Pages légales ou de politique

Marquez ces types de contenu comme versionnés dans les paramètres de Revisionary (Revisionary → Settings → Content types). Cela garantit que chaque modification crée une nouvelle version pouvant être référencée ultérieurement.

4. Exporter la dernière version de chaque entrée

Revisionary stocke chaque version dans la table revisionary_versions. La version la plus récente d’un enregistrement peut être récupérée via l’API Content avec le paramètre de requête populate=revision, ou en utilisant un endpoint personnalisé.

Voici un petit script Node qui récupère la version la plus récente de chaque entrée d’une collection donnée :

js
import fetch from 'node-fetch';

const STRAPI_URL = 'https://cms.example.com';
const API_TOKEN = 'YOUR_STRAPI_API_TOKEN';
const COLLECTION = 'articles'; // à changer selon votre type de contenu

async function fetchLatestVersions() {
  const res = await fetch(
    `${STRAPI_URL}/api/${COLLECTION}?populate=revision&pagination[pageSize]=1000`,
    { headers: { Authorization: `Bearer ${API_TOKEN}` } }
  );
  const { data } = await res.json();
  return data.map(entry => ({
    id: entry.id,
    versionId: entry.attributes.revision?.id, // ID de version Revisionary
    title: entry.attributes.title,
    body: entry.attributes.body,
    locale: entry.attributes.locale,
  }));
}

fetchLatestVersions().then(console.log).catch(console.error);

5. Indexer le contenu dans un magasin vectoriel

Chaque document que vous indexez doit inclure le texte, les métadonnées et le versionId de Revisionary. Cela vous permet de récupérer exactement la bonne version plus tard et de conserver une trace de provenance.

Exemple avec Pinecone (l’API est similaire pour les autres fournisseurs) :

js
import { PineconeClient } from '@pinecone-database/pinecone';
import { embed } from './my-embedding-service.js'; // votre propre fonction d’encodage

const pinecone = new PineconeClient();
await pinecone.init({ apiKey: process.env.PINECONE_API_KEY, environment: 'us-west1-gcp' });
const index = pinecone.Index('strapi-rag');

async function indexEntries(entries) {
  const vectors = [];
  for (const entry of entries) {
    const text = `${entry.title}\n\n${entry.body}`;
    const embedding = await embed(text); // renvoie Float32Array
    vectors.push({
      id: `${entry.id}-${entry.versionId}`,
      values: embedding,
      metadata: {
        strapiId: entry.id,
        versionId: entry.versionId,
        title: entry.title,
        locale: entry.locale,
      },
    });
  }
  await index.upsert({ upsertRequest: { vectors } });
}

fetchLatestVersions().then(indexEntries).catch(console.error);

L’ID du vecteur combine l’ID Strapi de l’entrée et l’ID de version Revisionary (${id}-${versionId}) afin de garantir l’unicité entre les versions.

6. Interrogation – De la question à la réponse

Une requête RAG typique se compose de trois étapes :

  1. Encodage de la question de l’utilisateur.
  2. Récupération des k vecteurs les plus proches.
  3. Récupération du contenu versionné complet depuis Strapi et construction du prompt.
js
import { embed } from './my-embedding-service.js';
import fetch from 'node-fetch';

async function retrieveRelevantDocs(question, k = 5) {
  const queryVector = await embed(question);
  const queryRes = await index.query({
    queryRequest: { vector: queryVector, topK: k, includeMetadata: true },
  });
  return queryRes.matches.map(match => match.metadata);
}

async function buildPrompt(question, docs) {
  const context = docs
    .map(d => `Title: ${d.title}\nContent: ${d.body || ''}`)
    .join('\n\n---\n\n');
  return `Answer the question using only the provided context.\n\nContext:\n${context}\n\nQuestion: ${question}\nAnswer:`;
}

async function answerQuestion(question) {
  const docsMeta = await retrieveRelevantDocs(question);
  // Récupérer les corps versionnés complets depuis Strapi (si pas déjà stockés)
  const docs = await Promise.all(
    docsMeta.map(async meta => {
      const res = await fetch(
        `${STRAPI_URL}/api/articles/${meta.strapiId}?populate=revision`,
        { headers: { Authorization: `Bearer ${API_TOKEN}` } }
      );
      const { data } = await res.json();
      const version = data.attributes.revision?.id;
      // S'assurer d'avoir la bonne version ; Revisionary garantit que l’ID de version correspond
      return {
        title: data.attributes.title,
        body: data.attributes.body,
        versionId: version,
      };
    })
  );
  const prompt = await buildPrompt(question, docs);
  // Envoyer le prompt à votre LLM (exemple avec OpenAI)
  const llmRes = await fetch('https://api.openai.com/v1/chat/completions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_KEY}` },
    body: JSON.stringify({ model: 'gpt-4o', messages: [{ role: 'user', content: prompt }] }),
  });
  const { choices } = await llmRes.json();
  return choices[0].message.content;
}

answerQuestion('How do we handle user data deletion?').then(console.log);

Le prompt indique explicitement au modèle de ne se baser que sur le contexte récupéré, évitant ainsi les hallucinations liées à du contenu obsolète.

black and silver laptop computer
Photo de path digital sur Unsplash

7. Maintenir l’index à jour

Chaque fois qu’un éditeur publie une nouvelle version, Revisionary émet un hook de cycle de vie (afterUpdate ou afterPublish). Utilisez un webhook Strapi pour déclencher le ré‑indexage uniquement de l’entrée modifiée.

  1. Créer un webhook dans Strapi admin → Settings → Webhooks → New webhook.
  2. Définissez l’URL vers un endpoint interne (par ex. : /api/rag/reindex).
  3. Choisissez l’événement Entry Published pour la collection versionnée.

Le payload du webhook contient l’ID de l’entrée et le nouveau revision.id. Votre endpoint pourra alors :

  • Récupérer la dernière version (comme à la section 4).
  • Upserter le nouveau vecteur, en écrasant l’ancien ID (${id}-${oldVersion}) ou en le supprimant d’abord.
js
// Gestionnaire de type Express pour le webhook
app.post('/api/rag/reindex', async (req, res) => {
  const { entry, model } = req.body; // Strapi envoie ces champs
  const latest = await fetchLatestVersionsForId(entry.id);
  await indexEntries([latest]); // ré‑upserter le vecteur unique
  res.sendStatus(200);
});

Comme l’ID du vecteur intègre la version, l’ancien vecteur reste consultable jusqu’à ce que vous le supprimiez explicitement. Si vous préférez un magasin propre, appelez index.delete avec l’ancien ID avant l’upsert.

8. Auditer les réponses – Provenance pour la conformité

Les régulateurs exigent de plus en plus une preuve de la provenance des réponses générées par l’IA. Avec Revisionary, vous pouvez stocker un enregistrement de provenance à côté de chaque réponse LLM :

js
async function answerWithProvenance(question) {
  const answer = await answerQuestion(question);
  const docsMeta = await retrieveRelevantDocs(question);
  const provenance = docsMeta.map(m => ({
    strapiId: m.strapiId,
    versionId: m.versionId,
    title: m.title,
  }));
  // Persister dans votre propre table d’audit ou dans le journal d’audit de Revisionary via l’API
  await fetch(`${STRAPI_URL}/api/audit-logs`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_TOKEN}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ question, answer, provenance }),
  });
  return answer;
}

Le journal d’audit pourra être exporté ultérieurement pour une révision juridique, répondant ainsi à la checklist [/blog/ai-regulation-for-publishers-using-strapi-compliance-checklist].

9. Sécurité et contrôle d’accès

  • Authentification : Protégez les endpoints RAG avec la même stratégie JWT que vous utilisez pour l’API Strapi.
  • Limitation de débit : Appliquez le middleware de rate‑limit décrit dans [/blog/protect-strapi-nextjs-admin-api-from-brute-force] afin d’éviter les abus du service d’encodage.
  • Confidentialité des données : Si vous stockez des données personnelles, activez les paramètres Privacy de Revisionary pour anonymiser les IP dans le journal d’audit [/docs/settings/privacy].
  • Principe du moindre privilège : Créez un token API dédié avec uniquement les permissions read sur la collection versionnée et create sur la collection audit‑log.

10. Structure complète du projet exemple

code
my-rag-app/
├─ src/
│  ├─ index.js            # point d’entrée, démarre le serveur Express
│  ├─ rag/
│  │   ├─ fetch.js        # fonctions des sections 4‑6
│  │   ├─ embed.js        # wrapper autour de votre modèle d’encodage
│  │   └─ llm.js          # appel à OpenAI/Anthropic
│  └─ webhook/
│      └─ reindex.js      # gestionnaire webhook (section 7)
├─ .env                    # STRAPI_URL, API_TOKEN, PINECONE_KEY, OPENAI_KEY
└─ package.json

Déployez le serveur à côté de votre instance Strapi (même VPC) afin de réduire la latence et d’éviter d’exposer directement le magasin vectoriel au public.

11. Tests et monitoring

  • Tests unitaires : Simulez l’API Strapi et le magasin vectoriel pour vérifier que l’ID de version correct est utilisé.
  • Test d’intégration : Publiez une nouvelle version d’un article, déclenchez le webhook, puis interrogez la même question et assurez‑vous que la réponse reflète le contenu mis à jour.
  • Monitoring : Loggez le nombre de vecteurs upsertés par jour et la latence de l’étape de récupération. Alertez si la latence dépasse un seuil, signe possible d’un index obsolète.

12. Quand ré‑indexer tout le contenu

Un ré‑index complet est rarement nécessaire, mais vous pouvez le faire :

  • Après une modification majeure du schéma (nouveaux champs impactant le contexte).
  • Lors du passage à un nouveau modèle d’encodage.
  • Périodiquement, afin de purger les vecteurs appartenant à des versions supprimées (utilisez la corbeille de Revisionary pour les identifier).

Script simple pour itérer sur toutes les entrées versionnées et reconstruire l’index par lots :

js
async function fullReindex() {
  const pageSize = 100;
  let page = 1;
  while (true) {
    const res = await fetch(`${STRAPI_URL}/api/articles?pagination[page]=${page}&pagination[pageSize]=${pageSize}&populate=revision`, {
      headers: { Authorization: `Bearer ${API_TOKEN}` },
    });
    const { data } = await res.json();
    if (data.length === 0) break;
    const entries = data.map(e => ({
      id: e.id,
      versionId: e.attributes.revision?.id,
      title: e.attributes.title,
      body: e.attributes.body,
      locale: e.attributes.locale,
    }));
    await indexEntries(entries);
    page++;
  }
}

Exécutez cela pendant une fenêtre de maintenance ; le magasin vectoriel supporte les upserts en masse.

Conclusion

En tirant parti des capacités de versionnage de Revisionary, vous pouvez bâtir un pipeline RAG qui :

  • Répond toujours à partir du contenu le plus récent approuvé.
  • Fournit une traçabilité inviolable liant chaque réponse à une version précise du contenu.
  • Réagit automatiquement aux mises à jour via les webhooks Strapi, maintenant ainsi l’index vectoriel à jour sans ré‑indexation complète.
  • Répond aux exigences de conformité grâce aux enregistrements de provenance intégrés et aux paramètres de confidentialité du journal d’audit.

Le résultat est une expérience IA‑augmentée fiable, tant pour les développeurs que pour les équipes de contenu, combinant la flexibilité d’un CMS headless avec la puissance des LLM modernes.

Mots-clésIARAGStrapiRevisionaryLLMGestion de contenu

L’équipe Revisionary

Les développeurs de Revisionary, un plugin d’historique des versions pour Strapi 5. Nous écrivons sur Strapi, le développement web, l’IA et la sécurité des applications.

Cet article a été rédigé avec l'aide d'une IA.

Tous les articles