Intégrez le feedback utilisateur et préparez le déploiement

Nous avons construit et évalué notre système RAG pour l'assistant virtuel de la mairie de Trifouillis-sur-Loire. Pour qu'il s'améliore continuellement et réponde au mieux aux besoins des agents comme Josiane, il est essentiel de mettre en place une boucle de feedback.

Qu’est-ce que c’est une boucle de feedback ?

Cela signifie collecter les retours des utilisateurs sur la qualité des réponses, stocker ces informations précieuses, et les rendre accessibles pour analyse.

Intégrez un mécanisme de feedback dans Streamlit

Le moyen le plus direct d'obtenir un retour est de le demander à l'utilisateur juste après avoir fourni une réponse. Nous allons ajouter des boutons simples "👍" (Pouce levé) / "👎" (Pouce baissé) à notre interface.

  • Approche : Utiliser les boutonsst.buttonde Streamlit. Pour s'assurer que chaque bouton est unique même s'ils apparaissent plusieurs fois dans l'historique, nous utiliserons une clé unique basée sur l'index du message ou un identifiant unique de l'interaction.

  • Implémentation dans MistralChat.py: Juste après l'affichage de la réponse de l'assistant dans la boucle principale, ajoutons les boutons :

interaction_key_base = f"feedback_{len(st.session_state.messages) - 1}"
cols = st.columns([1, 1, 10])

with cols[0]:
    if st.button("👍", key=f"{interaction_key_base}_up"):
        try:
            log_feedback(st.session_state.get("last_interaction_id"), 1)
            st.toast("Merci pour votre retour positif ! 👍")
        except Exception as e:
            logging.error(f"Erreur feedback positif: {e}")
            st.toast("Erreur lors de l'enregistrement du feedback.")

with cols[1]:
    if st.button("👎", key=f"{interaction_key_base}_down"):
        try:
            log_feedback(st.session_state.get("last_interaction_id"), -1)
            st.toast("Merci, votre retour nous aide à nous améliorer. 👎")
        except Exception as e:
            logging.error(f"Erreur feedback négatif: {e}")
            st.toast("Erreur lors de l'enregistrement du feedback.")
  • Gestion de l'état : Le point crucial ici est de pouvoir associer le clic sur le bouton à l'interaction correcte. Comme Streamlit ré-exécute le script à chaque interaction, l'ID de l'interaction doit être stocké (par exemple dans st.session_state) au moment où l'interaction est enregistrée, pour être récupéré lorsque le bouton de feedback est cliqué. Les clés uniques (key=...) sont essentielles pour que Streamlit distingue les différents boutons.

Stockage des interactions et du feedback en base de données

Pour que le feedback soit utile, il doit être stocké de manière persistante avec les détails de la conversation. Nous utilisons pour cela une base de données relationnelle (comme PostgreSQL ou SQLite) et l'ORM SQLAlchemy, gérés via notre module utils/database.py.

  • Le cœur : utils/database.pyCe module contient la logique pour interagir avec la base de données. Il définit :

    • La connexion à la base de données.

    • Le modèle de données (la classeInteractionmappée à une table SQL) qui structure comment les informations sont sauvegardées (timestamp, question, réponse, contexte RAG utilisé, score de feedback, commentaire, etc.).

    • Des fonctions utilitaires pour initialiser la base (init_db), enregistrer une interaction complète (log_interaction), et mettre à jour une interaction avec le feedback reçu (log_feedback).

from sqlalchemy import create_engine, Column, Integer, String, Text, DateTime, JSON
from sqlalchemy.orm import sessionmaker, declarative_base
import datetime
import logging

DATABASE_URL = "sqlite:///./interactions.db"

engine = create_engine(
    DATABASE_URL,
    connect_args={"check_same_thread": False} if "sqlite" in DATABASE_URL else {}
)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


class Interaction(Base):
    __tablename__ = "interactions"

    id = Column(Integer, primary_key=True, index=True)
    timestamp = Column(DateTime, default=datetime.datetime.utcnow)
    user_query = Column(Text, nullable=False)
    contexts = Column(JSON, nullable=True)
    llm_response = Column(Text, nullable=True)
    feedback_score = Column(Integer, nullable=True)
    feedback_comment = Column(Text, nullable=True)


def init_db():
    try:
        Base.metadata.create_all(bind=engine)
        logging.info("Base de données initialisée.")
    except Exception as e:
        logging.error(f"Erreur lors de l'initialisation de la DB: {e}")


def log_interaction(user_query: str, contexts: list[str] | None, llm_response: str) -> int | None:
    db = SessionLocal()
    interaction_id = None
    try:
        interaction = Interaction(
            user_query=user_query,
            contexts=contexts or [],
            llm_response=llm_response
        )
        db.add(interaction)
        db.commit()
        db.refresh(interaction)
        interaction_id = interaction.id
        logging.info(f"Interaction {interaction_id} enregistrée.")
    except Exception as e:
        logging.error(f"Erreur enregistrement interaction: {e}")
        db.rollback()
    finally:
        db.close()
    return interaction_id


def log_feedback(interaction_id: int | None, score: int, comment: str | None = None):
    if interaction_id is None:
        logging.warning("ID d'interaction manquant pour feedback.")
        return

    db = SessionLocal()
    try:
        interaction = db.query(Interaction).filter(Interaction.id == interaction_id).first()
        if interaction:
            interaction.feedback_score = score
            if comment:
                interaction.feedback_comment = comment
            db.commit()
            logging.info(f"Feedback enregistré (ID: {interaction_id}, Score: {score}).")
        else:
            logging.warning(f"Aucune interaction trouvée (ID: {interaction_id}).")
    except Exception as e:
        logging.error(f"Erreur feedback (ID: {interaction_id}): {e}")
        db.rollback()
    finally:
        db.close()

Appel des Fonctions :

  • init_db()doit être appelé une fois au démarrage de l'application Streamlit (MistralChat.py).

  • log_interaction()est appelée juste après avoir généré la réponsellm_response, en lui passant la questionuser_query, lescontextsutilisés (si RAG), et lallm_response. Elle retourne l'ID unique de cette interaction.

  • Cet ID est stocké temporairement (ex:st.session_state.last_interaction_id = interaction_id).

  • Lorsque l'utilisateur clique sur 👍 ou 👎, la fonctionlog_feedback()est appelée avec l'ID stocké et le score (+1 ou -1).

Visualisez les données avec le tableau de bord des feedbacks

Collecter des données ne sert à rien si on ne peut pas les consulter facilement. C'est le rôle du tableau de bord de visualisation, implémenté comme une page Streamlit séparée (pages/1_Feedback_Viewer.py).

  • Objectif : Fournir une interface à Josiane ou aux administrateurs pour :

    • Consulter l'historique des conversations.

    • Voir le feedback associé à chaque réponse.

    • Filtrer ou rechercher des interactions spécifiques (par exemple, celles avec un feedback négatif).

    • Obtenir des statistiques simples (ex: taux de satisfaction moyen).

  • Implémentation (pages/1_Feedback_Viewer.py) : Cette page utilise Streamlit et SQLAlchemy pour interagir avec la même base de données que l'application principale.

import streamlit as st
import pandas as pd
import logging
from sqlalchemy.orm import sessionmaker
from utils.database import engine, Interaction

SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

st.set_page_config(layout="wide", page_title="Feedback Viewer")
st.title("📊 Tableau de Bord - Feedback Utilisateur")


def load_data(limit=100, feedback_filter=None):
    db = SessionLocal()
    try:
        query = db.query(Interaction).order_by(Interaction.timestamp.desc())
        if feedback_filter == "positif":
            query = query.filter(Interaction.feedback_score == 1)
        elif feedback_filter == "negatif":
            query = query.filter(Interaction.feedback_score == -1)
        elif feedback_filter == "sans":
            query = query.filter(Interaction.feedback_score.is_(None))
        
        interactions = query.limit(limit).all()
        data_list = [{
            "ID": i.id,
            "Date": i.timestamp.strftime('%Y-%m-%d %H:%M'),
            "Question": i.user_query,
            "Réponse": i.llm_response,
            "Score": "👍" if i.feedback_score == 1 else ("👎" if i.feedback_score == -1 else "N/A"),
            "Commentaire": i.feedback_comment,
            "Contextes": i.contexts
        } for i in interactions]
        return pd.DataFrame(data_list)

    except Exception as e:
        logging.error(f"Erreur chargement données : {e}")
        st.error("Erreur lors du chargement des données.")
        return pd.DataFrame()

    finally:
        db.close()


st.sidebar.header("Filtres")
limit_rows = st.sidebar.slider("Nombre d'interactions à afficher", 5, 500, 50)
feedback_option = st.sidebar.selectbox(
    "Filtrer par Feedback",
    ["Tous", "Positif", "Négatif", "Sans Feedback"],
    index=0
)

filter_map = {
    "Tous": None,
    "Positif": "positif",
    "Négatif": "negatif",
    "Sans Feedback": "sans"
}
selected_filter = filter_map[feedback_option]

df_interactions = load_data(limit=limit_rows, feedback_filter=selected_filter)

st.metric("Nombre total affiché", len(df_interactions))
st.dataframe(df_interactions, use_container_width=True)

if not df_interactions.empty:
    selected_id = st.selectbox("Voir contextes pour ID:", df_interactions["ID"].unique())
    if selected_id:
        selected_contexts = df_interactions[df_interactions["ID"] == selected_id]["Contextes"].iloc[0]
        with st.expander(f"Contextes RAG pour Interaction {selected_id}"):
            if selected_contexts:
                for idx, ctx in enumerate(selected_contexts):
                    st.text_area(f"Contexte {idx+1}", ctx, height=100, key=f"ctx_{selected_id}_{idx}")
            else:
                st.write("Aucun contexte RAG enregistré pour cette interaction.")

Fonctionnalités Clés :

  • Connexion sécurisée à la base de données.

  • Requête pour récupérer les interactions (avec tri par date).

  • Options de filtrage (par score de feedback).

  • Affichage clair des données dans un tableau (st.dataframe).

  • Possibilité d'afficher les contextes RAG utilisés pour une interaction donnée (st.expander).

Ce tableau de bord transforme les données brutes en informations exploitables pour améliorer l'assistant.

Et voyons en vidéo ce que cela donne :

À vous de jouer

Les fonctions qui interagissent avec la base de données (  log_interaction  ,  log_feedback  ) sont critiques. Assurons-nous qu'elles fonctionnent correctement avec des tests unitaires.

Contexte

Maintenant que nous avons mis en place la collecte et le stockage du feedback, il est crucial de s'assurer que ces opérations sont fiables. Une erreur dans l'enregistrement pourrait nous faire perdre des informations précieuses. Nous allons écrire quelques tests unitaires simples pour les fonctions clés de utils/database.py.

Consignes

1. Créer un fichier de test : Créez un fichiertests/test_database.py(si vous n'avez pas de dossiertests, créez-le).

2. Utiliserunittest: Mettez en place une classe de test héritant deunittest.TestCase.

3. Testerlog_interaction: Écrivez un testtest_log_interaction_success. Dans ce test :

  • Utilisez@patchpour mockerSessionLocaldeutils.database. Simulez le comportement de la session SQLAlchemy (par exemple, les méthodesadd,commit,refresh,close).

  • Appelezlog_interactionavec des données d'exemple.

  • Vérifiez quedb.adda été appelé une fois.

  • Vérifiez quedb.commita été appelé une fois.

  • Vérifiez que la fonction retourne bien un ID (simulez la récupération de l'ID aprèsdb.refresh).

4. Testerlog_feedback: Écrivez un testtest_log_feedback_updates_score. Dans ce test :

  • MockezSessionLocalet simulez la méthodequery().filter().first()pour retourner un objetInteractionsimulé (avecMagicMock).

  • Appelezlog_feedbackavec un ID d'interaction et un score.

  • Vérifiez que l'attributfeedback_scorede l'objet Interaction simulé a été mis à jour avec le bon score.

  • Vérifiez quedb.commita été appelé.

En résumé

  • Mettez en place une boucle de feedback utilisateur après la construction et l'évaluation initiales.

  • Intégrez un mécanisme simple de collecte (ex: boutons 👍/👎) via Streamlit.

  • Stockez de manière fiable les interactions et le feedback.

  • Fournissez un tableau de bord pour visualiser ces données.

  • Évaluez les options de déploiement (Streamlit Cloud, serveur local, VM, Docker).

Et ce n'est que le début !

Ce parcours vous a équipé des compétences pour construire et affiner un système RAG fonctionnel. Le domaine de l'IA évolue cependant très rapidement. Les prochaines étapes passionnantes pourraient inclure :

  • Agents IA : Aller au-delà du simple RAG en créant des agents capables de planifier, d'utiliser plusieurs outils (comme la recherche web en plus de votre base de connaissances) et d'interagir de manière plus autonome pour résoudre des problèmes complexes.

  • Modèles à Long Contexte : L'émergence de modèles comme Llama 3 de Meta (avec 128k tokens de contexte) ou les futurs modèles annoncés avec des fenêtres de contexte de plusieurs millions de tokens (comme l'évoque la R&D autour de Llama 4) pourrait révolutionner l'approche RAG, permettant potentiellement d'injecter des documents entiers ou des conversations très longues directement dans le prompt.

  • Protocoles d'Intégration Avancés : Des initiatives explorent de nouvelles manières pour les modèles d'interagir avec des bases de connaissances externes de façon standardisée comme MCP (Model Context Protocol), initié par Anthropic.

Vous avez maintenant les bases solides pour explorer ces nouvelles frontières. L'aventure de l'IA conversationnelle ne fait que commencer, et vous êtes prêt à y participer activement pour améliorer les services aux citoyens de Trifouillis-sur-Loire et au-delà !

Et si vous obteniez un diplôme OpenClassrooms ?
  • Formations jusqu’à 100 % financées
  • Date de début flexible
  • Projets professionnalisants
  • Mentorat individuel
Trouvez la formation et le financement faits pour vous