Aller au contenu principal

Recueillir les commentaires des utilisateurs

La collecte et l'enregistrement des commentaires des utilisateurs sont essentiels pour comprendre la qualité réelle de votre application GenAI. MLflow offre un moyen structuré de recueillir les commentaires sous forme d'évaluations sur les traces, ce qui vous permet de suivre la qualité au fil du temps, d'identifier les points à améliorer et de créer des datasets d'évaluation à partir des données de production.

Évaluations des traces

Prérequis

Choisissez la méthode d'installation appropriée en fonction de votre environnement :

Pour les déploiements de production, installez le package mlflow-tracing :

Bash
pip install --upgrade mlflow-tracing

Le package mlflow-tracing est optimisé pour une utilisation en production avec des dépendances minimales et de meilleures caractéristiques de performance.

L’API log_feedback est disponible dans les deux packages. Vous pouvez donc recueillir les commentaires des utilisateurs, quelle que soit la méthode d’installation que vous choisissez.

remarque

MLflow 3 est requis pour la collecte des commentaires des utilisateurs. MLflow 2.x n'est pas pris en charge en raison de limitations de performances et de fonctionnalités manquantes essentielles pour une utilisation en production.

Pourquoi collecter les retours des utilisateurs ?

Les commentaires des utilisateurs fournissent la vérité terrain sur les performances de votre application :

  1. **Signaux de qualité réels** — Comprenez comment les utilisateurs réels perçoivent les sorties de votre application
  2. Amélioration continue – Identifiez les modèles de commentaires négatifs pour orienter le développement.
  3. Création de données d'entraînement : utilisez les commentaires pour créer des datasets d'évaluation de haute qualité
  4. Monitoring de la qualité : Suivez les indicateurs de satisfaction dans le temps et à travers les différents segments d’utilisateurs.
  5. Affinement de modèle — Exploitez les données de feedback pour améliorer vos modèles sous-jacents.

Types de commentaires

MLflow prend en charge divers types de commentaires par l'intermédiaire de son système d'évaluation :

Type de commentaires

Description

Cas d'utilisation courants

Retour binaire

Simple pouce levé/baissé ou correct/incorrect

Signaux rapides de satisfaction des utilisateurs

Scores numériques

Évaluations sur une échelle (par exemple, 1-5 étoiles)

Évaluation détaillée de la qualité

Commentaires catégoriels

Options à choix multiples

Classification des problèmes ou des types de réponse

Commentaires textuels

Commentaires libres

Explications détaillées pour l'utilisateur

Type de commentaires

Description

Cas d'utilisation courants

Retour binaire

Simple pouce levé/baissé ou correct/incorrect

Signaux rapides de satisfaction des utilisateurs

Scores numériques

Évaluations sur une échelle (par exemple, 1-5 étoiles)

Évaluation détaillée de la qualité

Commentaires catégoriels

Options à choix multiples

Classification des problèmes ou des types de réponse

Commentaires textuels

Commentaires libres

Explications détaillées pour l'utilisateur

Comprendre le modèle de données de Feedback

Dans MLflow, les retours d'utilisateurs sont capturés à l'aide de l'entité Feedback , qui est un type d'Évaluation qui peut être attachée à des traces ou des spans spécifiques. L'entité Feedback offre un moyen structuré de stocker :

  • Valeur : Le retour d'information réel (booléen, numérique, texte ou données structurées)
  • Source : Informations sur qui ou quoi a fourni le feedback (utilisateur humain, juge LLM ou code)
  • Justification : explication facultative des commentaires.
  • Métadonnées : Contexte supplémentaire comme les Timestamp ou les attributs personnalisés

La compréhension de ce modèle de données vous aide à concevoir des systèmes efficaces de collecte de commentaires qui s'intègrent parfaitement aux capacités d'évaluation et de monitoring de MLflow. Pour des informations détaillées sur le schéma d'entité de retour d'information et tous les champs disponibles, consultez la section Feedback dans les concepts Span.

Collecte de commentaires des utilisateurs finaux

Lors de l'implémentation de la collecte de feedback en production, vous devez Link les retours des utilisateurs à des traces spécifiques. Il existe deux approches que vous pouvez utiliser :

  1. Utilisation des ID de requête client – Générez vos propres ID uniques lors du traitement des requêtes et référencez-les ultérieurement pour les retours.
  2. Utilisation des ID de trace MLflow — Utilisez l'ID de trace généré automatiquement par MLflow

Comprendre le flux de collecte des commentaires

Les deux approches suivent un modèle similaire :

  1. Pendant la requête initiale : votre application génère un ID de requête client unique ou récupère l'ID de trace généré par MLflow
  2. Après avoir reçu la réponse : l’utilisateur peut fournir des commentaires en référençant l’un ou l’autre des ID
  3. Les commentaires sont enregistrés : l'API log_feedback de MLflow crée une évaluation associée à la trace originale
  4. Analyse et monitoring : Vous pouvez query et analyser les retours d’information sur toutes les traces.

Mise en œuvre de la collecte de commentaires

L'approche la plus simple consiste à utiliser l'ID de trace que MLflow génère automatiquement pour chaque trace. Vous pouvez récupérer cet ID pendant le traitement de la requête et le renvoyer au client :

Mise en œuvre du back-end

Python
import mlflow
from fastapi import FastAPI, Query
from mlflow.client import MlflowClient
from mlflow.entities import AssessmentSource
from pydantic import BaseModel
from typing import Optional

app = FastAPI()

class ChatRequest(BaseModel):
message: str

class ChatResponse(BaseModel):
response: str
trace_id: str # Include the trace ID in the response

@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
"""
Process a chat request and return the trace ID for feedback collection.
"""
# Your GenAI application logic here
response = process_message(request.message) # Replace with your actual processing logic

# Get the current trace ID
trace_id = mlflow.get_current_active_span().trace_id

return ChatResponse(
response=response,
trace_id=trace_id
)

class FeedbackRequest(BaseModel):
is_correct: bool # True for thumbs up, False for thumbs down
comment: Optional[str] = None

@app.post("/feedback")
def submit_feedback(
trace_id: str = Query(..., description="The trace ID from the chat response"),
feedback: FeedbackRequest = ...,
user_id: Optional[str] = Query(None, description="User identifier")
):
"""
Collect user feedback using the MLflow trace ID.
"""
# Log the feedback directly using the trace ID
mlflow.log_feedback(
trace_id=trace_id,
name="user_feedback",
value=feedback.is_correct,
source=AssessmentSource(
source_type="HUMAN",
source_id=user_id
),
rationale=feedback.comment
)

return {
"status": "success",
"trace_id": trace_id,
}

Exemple de mise en œuvre frontale

Vous trouverez ci-dessous un exemple de l'implémentation front-end pour une application basée sur React :

JavaScript
// React example for chat with feedback
import React, { useState } from 'react';

function ChatWithFeedback() {
const [message, setMessage] = useState('');
const [response, setResponse] = useState('');
const [traceId, setTraceId] = useState(null);
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);

const sendMessage = async () => {
try {
const res = await fetch('/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});

const data = await res.json();
setResponse(data.response);
setTraceId(data.trace_id);
setFeedbackSubmitted(false);
} catch (error) {
console.error('Chat error:', error);
}
};

const submitFeedback = async (isCorrect, comment = null) => {
if (!traceId || feedbackSubmitted) return;

try {
const params = new URLSearchParams({
trace_id: traceId,
...(userId && { user_id: userId }),
});

const res = await fetch(`/feedback?${params}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
is_correct: isCorrect,
comment: comment,
}),
});

if (res.ok) {
setFeedbackSubmitted(true);
// Optionally show success message
}
} catch (error) {
console.error('Feedback submission error:', error);
}
};

return (
<div>
<input value={message} onChange={(e) =&gt; setMessage(e.target.value)} placeholder="Ask a question..." />
<button onClick={sendMessage}>Send</button>

{response && (
<div>
<p>{response}</p>
<div className="feedback-buttons">
<button onClick={() =&gt; submitFeedback(true)} disabled={feedbackSubmitted}>
👍
</button>
<button onClick={() =&gt; submitFeedback(false)} disabled={feedbackSubmitted}>
👎
</button>
</div>
{feedbackSubmitted && Thanks for your feedback!</span>}
</div>
)}
</div>
);
}

Détails clés de l'implémentation

**AssessmentSource** : AssessmentSource L'objet identifie qui ou quoi a fourni le feedback :

  • source_type: Peut être "HUMAN" pour les commentaires des utilisateurs ou "LLM_JUDGE" pour l'évaluation automatisée
  • source_id: identifie l'utilisateur ou le système spécifique fournissant des retours d'expérience

**Stockage des commentaires** : les commentaires sont stockés sous forme d’évaluations sur la trace, ce qui signifie :

  • Il est associé de manière permanente à l'interaction spécifique
  • Il peut être interrogé avec les données de trace
  • C'est visible dans l'interface utilisateur MLflow lors de la consultation de la trace.

Gestion des différents types de feedback

Vous pouvez étendre l'une ou l'autre approche pour prendre en charge des retours d'information plus complexes. Voici un exemple d’utilisation d’ID de trace :

Python
from mlflow.entities import AssessmentSource

@app.post("/detailed-feedback")
def submit_detailed_feedback(
trace_id: str,
accuracy: int = Query(..., ge=1, le=5, description="Accuracy rating from 1-5"),
helpfulness: int = Query(..., ge=1, le=5, description="Helpfulness rating from 1-5"),
relevance: int = Query(..., ge=1, le=5, description="Relevance rating from 1-5"),
user_id: str = Query(..., description="User identifier"),
comment: Optional[str] = None
):
"""
Collect multi-dimensional feedback with separate ratings for different aspects.
Each aspect is logged as a separate assessment for granular analysis.
"""
# Log each dimension as a separate assessment
dimensions = {
"accuracy": accuracy,
"helpfulness": helpfulness,
"relevance": relevance
}

for dimension, score in dimensions.items():
mlflow.log_feedback(
trace_id=trace_id,
name=f"user_{dimension}",
value=score / 5.0, # Normalize to 0-1 scale
source=AssessmentSource(
source_type="HUMAN",
source_id=user_id
),
rationale=comment if dimension == "accuracy" else None
)

return {
"status": "success",
"trace_id": trace_id,
"feedback_recorded": dimensions
}

Gestion des commentaires avec des réponses en streaming

Lorsque vous utilisez des réponses de streaming (Server-Sent Events ou WebSockets), l'ID de trace n'est pas disponible tant que le stream n'est pas terminé. Cela représente un défi unique pour la collecte de feedback qui nécessite une approche différente.

Pourquoi le streaming est différent

Dans les modèles traditionnels de requête-réponse, vous recevez la réponse complète et l'ID de trace ensemble. Avec le streaming :

  1. Les jetons arrivent de manière incrémentielle : La réponse est construite au fil du temps à mesure que les jetons sont Stream depuis le LLM
  2. Achèvement de la trace différé : L’ID de trace n’est généré qu’une fois le Stream entier terminé.
  3. L'interface utilisateur de feedback doit attendre : les utilisateurs ne peuvent pas fournir de feedback tant qu'ils n'ont pas à la fois la réponse complète et l'ID de trace

Implémentation backend avec SSE

Voici comment implémenter le streaming avec la livraison de l’ID de trace à la fin du stream :

Python
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import mlflow
import json
import asyncio
from typing import AsyncGenerator

@app.post("/chat/stream")
async def chat_stream(request: ChatRequest):
"""
Stream chat responses with trace ID sent at completion.
"""
async def generate() -> AsyncGenerator[str, None]:
try:
# Start MLflow trace
with mlflow.start_span(name="streaming_chat") as span:
# Update trace with request metadata
mlflow.update_current_trace(
request_message=request.message,
stream_start_time=datetime.now().isoformat()
)

# Stream tokens from your LLM
full_response = ""
async for token in your_llm_stream_function(request.message):
full_response += token
yield f"data: {json.dumps({'type': 'token', 'content': token})}\n\n"
await asyncio.sleep(0.01) # Prevent overwhelming the client

# Log the complete response to the trace
span.set_attribute("response", full_response)
span.set_attribute("token_count", len(full_response.split()))

# Get trace ID after completion
trace_id = span.trace_id

# Send trace ID as final event
yield f"data: {json.dumps({'type': 'done', 'trace_id': trace_id})}\n\n"

except Exception as e:
# Log error to trace if available
if mlflow.get_current_active_span():
mlflow.update_current_trace(error=str(e))

yield f"data: {json.dumps({'type': 'error', 'error': str(e)})}\n\n"

return StreamingResponse(
generate(),
media_type="text/event-stream",
headers={
&quot;Cache-Control&quot;: &quot;no-cache&quot;,
&quot;Connection&quot;: &quot;keep-alive&quot;,
&quot;X-Accel-Buffering&quot;: &quot;no&quot;, # Disable proxy buffering
}
)

Implémentation front-end pour le streaming

Gérez les événements en streaming et activez le feedback uniquement après avoir reçu l'identifiant de trace :

JavaScript
// React hook for streaming chat with feedback
import React, { useState, useCallback } from 'react';

function useStreamingChat() {
const [isStreaming, setIsStreaming] = useState(false);
const [streamingContent, setStreamingContent] = useState('');
const [traceId, setTraceId] = useState(null);
const [error, setError] = useState(null);

const sendStreamingMessage = useCallback(async (message) => {
// Reset state
setIsStreaming(true);
setStreamingContent('');
setTraceId(null);
setError(null);

try {
const response = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message }),
});

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');

// Keep the last incomplete line in the buffer
buffer = lines.pop() || '';

for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.slice(6));

switch (data.type) {
case 'token':
setStreamingContent((prev) => prev + data.content);
break;
case 'done':
setTraceId(data.trace_id);
setIsStreaming(false);
break;
case 'error':
setError(data.error);
setIsStreaming(false);
break;
}
} catch (e) {
console.error('Failed to parse SSE data:', e);
}
}
}
}
} catch (error) {
setError(error.message);
setIsStreaming(false);
}
}, []);

return {
sendStreamingMessage,
streamingContent,
isStreaming,
traceId,
error,
};
}

// Component using the streaming hook
function StreamingChatWithFeedback() {
const [message, setMessage] = useState('');
const [feedbackSubmitted, setFeedbackSubmitted] = useState(false);
const { sendStreamingMessage, streamingContent, isStreaming, traceId, error } = useStreamingChat();

const handleSend = () => {
if (message.trim()) {
setFeedbackSubmitted(false);
sendStreamingMessage(message);
setMessage('');
}
};

const submitFeedback = async (isPositive) => {
if (!traceId || feedbackSubmitted) return;

try {
const response = await fetch(`/feedback?trace_id=${traceId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
is_correct: isPositive,
comment: null,
}),
});

if (response.ok) {
setFeedbackSubmitted(true);
}
} catch (error) {
console.error('Feedback submission failed:', error);
}
};

return (
<div className="streaming-chat">
<div className="chat-messages">
{streamingContent && (
<div className="message assistant">
{streamingContent}
{isStreaming && ...</span>}
</div>
)}
{error && <div className="error-message">Error: {error}</div>}
</div>

{/* Feedback buttons - only enabled when trace ID is available */}
{streamingContent && !isStreaming && traceId && (
<div className="feedback-section">
Was this response helpful?</span>
<button onClick={() =&gt; submitFeedback(true)} disabled={feedbackSubmitted} className="feedback-btn positive">
👍 Yes
</button>
<button onClick={() =&gt; submitFeedback(false)} disabled={feedbackSubmitted} className="feedback-btn negative">
👎 No
</button>
{feedbackSubmitted && Thank you!</span>}
</div>
)}

<div className="chat-input-section">
<input
type="text"
value={message}
onChange={(e) =&gt; setMessage(e.target.value)}
onKeyPress={(e) =&gt; e.key === 'Enter' &amp;&amp; !isStreaming &amp;&amp; handleSend()}
placeholder="Type your message..."
disabled={isStreaming}
/>
<button onClick={handleSend} disabled={isStreaming || !message.trim()}>
{isStreaming ? 'Streaming...' : 'Send'}
</button>
</div>
</div>
);
}

Considérations clés pour le streaming

Lors de l'implémentation de la collecte de feedback avec des réponses en streaming, gardez ces points à l'esprit :

  1. **Délai d'ID de trace** : l'ID de trace n'est disponible qu'après la fin du streaming. Concevez votre interface utilisateur pour gérer cela avec élégance en désactivant les contrôles de feedback jusqu'à ce que l'ID de trace soit reçu.

  2. Structure d'événement : Utilisez un format d'événement cohérent avec un champ type pour faire la distinction entre les jetons de contenu, les événements de complétion et les erreurs. Cela rend l'analyse et la gestion des événements plus fiables.

  3. Gestion de l'état : suivez séparément le contenu en streaming et l'ID de trace. Reset tout l'état au start de chaque nouvelle interaction pour éviter les problèmes de données obsolètes.

  4. Gestion des erreurs : Incluez les événements d’erreur dans le Stream pour gérer les échecs avec élégance. Assurez-vous que les erreurs sont journalisées dans la trace lorsque cela est possible pour le debugging.

  5. Gestion des tampons :

    • Utilisez l'en-tête X-Accel-Buffering: no pour désactiver la mise en mémoire tampon du proxy
    • Mettre en œuvre une mise en mémoire tampon de ligne appropriée dans le front-end pour gérer les messages SSE partiels.
    • Envisagez de mettre en œuvre une logique de reconnexion pour les interruptions réseau
  6. Optimisation des performances :

    • Ajoutez de petits délais entre les jetons (asyncio.sleep(0.01)) pour éviter de surcharger les clients.
    • Traitez par batch plusieurs jetons s'ils arrivent trop rapidement.
    • Envisagez de mettre en œuvre des mécanismes de régulation de la charge pour les clients lents

Analyse des données de feedback

Une fois que vous avez collecté les commentaires, vous pouvez les analyser pour obtenir des informations sur la qualité de votre application et la satisfaction des utilisateurs.

Affichage des commentaires dans l'interface utilisateur Trace

Interface utilisateur des évaluations de trace

Obtention des traces avec des retours d'expérience à l'aide du SDK

Affichage des commentaires dans l'interface utilisateur Trace

commentaires de trace

Obtention des traces avec des retours d'expérience à l'aide du SDK

Tout d'abord, récupérez les traces d'une fenêtre de temps spécifique :

Python
from mlflow.client import MlflowClient
from datetime import datetime, timedelta

def get_recent_traces(experiment_name: str, hours: int = 24):
"""Get traces from the last N hours."""
client = MlflowClient()

# Calculate cutoff time
cutoff_time = datetime.now() - timedelta(hours=hours)
cutoff_timestamp_ms = int(cutoff_time.timestamp() * 1000)

# Query traces
traces = client.search_traces(
experiment_names=[experiment_name],
filter_string=f"trace.timestamp_ms > {cutoff_timestamp_ms}"
)

return traces

Analyse des schémas de retour d'information à l'aide du SDK

Extraire et analyser les commentaires des traces :

Python
def analyze_user_feedback(traces):
"""Analyze feedback patterns from traces."""

client = MlflowClient()

# Initialize counters
total_traces = len(traces)
traces_with_feedback = 0
positive_count = 0
negative_count = 0

# Process each trace
for trace in traces:
# Get full trace details including assessments
trace_detail = client.get_trace(trace.info.trace_id)

if trace_detail.data.assessments:
traces_with_feedback += 1

# Count positive/negative feedback
for assessment in trace_detail.data.assessments:
if assessment.name == "user_feedback":
if assessment.value:
positive_count += 1
else:
negative_count += 1

# Calculate metrics
if traces_with_feedback > 0:
feedback_rate = (traces_with_feedback / total_traces) * 100
positive_rate = (positive_count / traces_with_feedback) * 100
else:
feedback_rate = 0
positive_rate = 0

return {
"total_traces": total_traces,
"traces_with_feedback": traces_with_feedback,
"feedback_rate": feedback_rate,
"positive_rate": positive_rate,
"positive_count": positive_count,
"negative_count": negative_count
}

# Example usage
traces = get_recent_traces("/Shared/production-genai-app", hours=24)
results = analyze_user_feedback(traces)

print(f"Feedback rate: {results['feedback_rate']:.1f}%")
print(f"Positive feedback: {results['positive_rate']:.1f}%")
print(f"Total feedback: {results['traces_with_feedback']} out of {results['total_traces']} traces")

Analyse du feedback multidimensionnel

Pour des commentaires plus détaillés avec des évaluations :

Python
def analyze_ratings(traces):
"""Analyze rating-based feedback."""

client = MlflowClient()
ratings_by_dimension = {}

for trace in traces:
trace_detail = client.get_trace(trace.info.trace_id)

if trace_detail.data.assessments:
for assessment in trace_detail.data.assessments:
# Look for rating assessments
if assessment.name.startswith("user_") and assessment.name != "user_feedback":
dimension = assessment.name.replace("user_", "")

if dimension not in ratings_by_dimension:
ratings_by_dimension[dimension] = []

ratings_by_dimension[dimension].append(assessment.value)

# Calculate averages
average_ratings = {}
for dimension, scores in ratings_by_dimension.items():
if scores:
average_ratings[dimension] = sum(scores) / len(scores)

return average_ratings

# Example usage
ratings = analyze_ratings(traces)
for dimension, avg_score in ratings.items():
print(f"{dimension}: {avg_score:.2f}/1.0")

Ressources supplémentaires