Pular para o conteúdo principal

Colete feedback do usuário

Coletar e registrar o feedback do usuário é essencial para entender a qualidade real de seu aplicativo GenAI. MLflow fornece uma maneira estruturada de capturar feedback como avaliações em traços, permitindo que o senhor acompanhe a qualidade ao longo do tempo, identifique áreas de melhoria e crie um conjunto de dados de avaliação a partir dos dados de produção.

Avaliações de rastreamento

Pré-requisitos

Escolha o método de instalação apropriado com base no seu ambiente:

Para implementações de produção, instale o pacote mlflow-tracing:

Bash
pip install --upgrade mlflow-tracing

O pacote mlflow-tracing é otimizado para uso em produção com dependências mínimas e melhores características de desempenho.

O log_feedback API está disponível em ambos os pacotes, de modo que o senhor pode coletar feedback do usuário independentemente do método de instalação escolhido.

nota

O MLflow 3 é necessário para coletar feedback do usuário. MLflow O 2.x não é compatível devido a limitações de desempenho e à falta de recursos essenciais para uso em produção.

Por que coletar feedback do usuário?

O feedback do usuário fornece a verdade básica sobre o desempenho do seu aplicativo:

  1. Sinais de qualidade do mundo real - entenda como os usuários reais percebem as saídas do seu aplicativo
  2. Melhoria contínua - Identificar padrões no feedback negativo para orientar o desenvolvimento
  3. criação de dados de treinamento - Use o feedback para criar um conjunto de dados de avaliação de alta qualidade
  4. Monitoramento da qualidade - Acompanhe as métricas de satisfação ao longo do tempo e em diferentes segmentos de usuários
  5. Ajuste fino do modelo - Aproveite os dados de feedback para melhorar seus modelos subjacentes

Tipos de feedback

O MLflow oferece suporte a vários tipos de feedback por meio de seu sistema de avaliação:

Tipo de feedback

Descrição

Casos de uso comuns

Feedback binário

Simples polegares para cima/para baixo ou corretos/incorretos

Sinais rápidos de satisfação do usuário

Pontuações numéricas

Classificações em uma escala (por exemplo, de 1 a 5 estrelas)

Avaliação detalhada da qualidade

Feedback categórico

Opções de múltipla escolha

Classificação de problemas ou tipos de resposta

Feedback de texto

Comentários de formato livre

Explicações detalhadas para o usuário

Entendendo o modelo de dados de feedback

No MLflow, o feedback do usuário é capturado usando a entidade Feedback , que é um tipo de avaliação que pode ser anexada a traços ou períodos específicos. A entidade Feedback fornece uma forma estruturada de armazenar:

  • Valor : O feedback real (booleano, numérico, texto ou dados estruturados)
  • Fonte : informações sobre quem ou o que forneceu o feedback (usuário humano, juiz do LLM ou código)
  • Justificativa : explicação opcional para o feedback
  • Metadados : contexto adicional, como carimbos de data/hora ou atributos personalizados

Compreender esse modelo de dados ajuda você a projetar sistemas eficazes de coleta de feedback que se integram perfeitamente aos recursos de avaliação e monitoramento do MLflow. Para obter informações detalhadas sobre o esquema da entidade Feedback e todos os campos disponíveis, consulte a seção Feedback nos conceitos de Span.

Coleta de feedback do usuário final

Ao implementar a coleta de feedback na produção, você precisa vincular o feedback do usuário a rastreamentos específicos. Há duas abordagens que você pode usar:

  1. Usando IDs de solicitação de cliente - Gere seus próprios IDs exclusivos ao processar solicitações e consulte-os posteriormente para obter feedback
  2. Uso de IDs de rastreamento do MLflow - Use a ID de rastreamento gerada automaticamente pelo MLflow

Entendendo o fluxo de coleta de feedback

Ambas as abordagens seguem um padrão semelhante:

  1. Durante a solicitação inicial : Seu aplicativo gera um ID exclusivo de solicitação do cliente ou recupera o ID de rastreamento gerado pelo MLFlow

  2. Depois de receber a resposta : o usuário pode fornecer feedback referenciando qualquer ID Ambas as abordagens seguem um padrão semelhante:

  3. Durante a solicitação inicial : Seu aplicativo gera um ID exclusivo de solicitação do cliente ou recupera o ID de rastreamento gerado pelo MLFlow

  4. Depois de receber a resposta : o usuário pode fornecer feedback referenciando qualquer ID

  5. Os comentários são registros : MLflow's log_feedback API cria uma avaliação anexada ao rastreamento original

  6. análise e monitoramento : O senhor pode consultar e analisar o feedback em todos os rastros

Implementar a coleta de feedback

A abordagem mais simples é usar o ID de rastreamento que o MLflow gera automaticamente para cada rastreamento. Você pode recuperar essa ID durante o processamento da solicitação e devolvê-la ao cliente:

Implementação de backend

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,
}

Exemplo de implementação de front-end

abaixo é um exemplo de implementação de front-end para um aplicativo baseado em 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) => setMessage(e.target.value)} placeholder="Ask a question..." />
<button onClick={sendMessage}>Send</button>

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

principais detalhes da implementação

AssessmentSource : O objeto AssessmentSource identifica quem ou o que forneceu o feedback:

  • source_type: Pode ser " HUMAN " para feedback do usuário ou " LLM_JUDGE " para avaliação automatizada
  • source_id: identifica o usuário ou sistema específico que está fornecendo feedback

Armazenamento de feedback : o feedback é armazenado como avaliações no rastreamento, o que significa:

  • Está permanentemente associado à interação específica
  • Ele pode ser consultado junto com os dados de rastreamento
  • Ele fica visível na interface do usuário do MLflow ao visualizar o rastreamento

Lidar com diferentes tipos de feedback

Você pode estender qualquer uma das abordagens para oferecer suporte a um feedback mais complexo. Aqui está um exemplo usando IDs de rastreamento:

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
}

Tratamento de feedback com respostas de transmissão

Ao usar respostas de transmissão (eventos enviados pelo servidor ou WebSockets), a ID do rastreamento não estará disponível até que a transmissão seja concluída. Isso representa um desafio único para a coleta de feedback que exige uma abordagem diferente.

Por que a transmissão é diferente

Nos padrões tradicionais de solicitação-resposta, você recebe a resposta completa e o ID de rastreamento juntos. Com transmissão:

  1. Os tokens chegam de forma incremental : A resposta é construída ao longo do tempo, conforme a transmissão tokens do LLM
  2. A conclusão do rastreamento é adiada : A ID do rastreamento só é gerada após o término de toda a transmissão
  3. A interface de usuário de feedback deve esperar : os usuários não podem fornecer feedback até que tenham a resposta completa e o ID de rastreamento

Implementação de back-end com SSE

Veja como implementar a transmissão com entrega de ID de rastreamento no final da transmissão:

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={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no", # Disable proxy buffering
}
)

Implementação de front-end para transmissão

Trate os eventos de transmissão e ative o feedback somente após receber a ID do rastreamento:

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={() => submitFeedback(true)} disabled={feedbackSubmitted} className="feedback-btn positive">
👍 Yes
</button>
<button onClick={() => 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) => setMessage(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isStreaming && handleSend()}
placeholder="Type your message..."
disabled={isStreaming}
/>
<button onClick={handleSend} disabled={isStreaming || !message.trim()}>
{isStreaming ? 'Streaming...' : 'Send'}
</button>
</div>
</div>
);
}

considerações fundamentais para a transmissão

Ao implementar a coleta de feedback com respostas de transmissão, tenha em mente os seguintes pontos:

  1. Tempo de ID do rastreamento : A ID do rastreamento só fica disponível após a conclusão da transmissão. Crie sua interface de usuário para lidar com isso normalmente, desativando os controles de feedback até que o ID de rastreamento seja recebido.

  2. Estrutura do evento : Use um formato de evento consistente com um campo type para distinguir entre tokens de conteúdo, eventos de conclusão e erros. Isso torna a análise e o tratamento de eventos mais confiáveis.

  3. Gerenciamento de estado : Rastreie o conteúdo da transmissão e o ID de rastreamento separadamente. Reset todos os estados no início de cada nova interação para evitar problemas de dados desatualizados.

  4. Tratamento de erros : Inclua eventos de erro na transmissão para tratar graciosamente as falhas. Certifique-se de que os erros sejam registrados no rastreamento quando possível para depuração.

  5. Gerenciamento de buffer :

    • Use o cabeçalho X-Accel-Buffering: no para desativar o buffer de proxy
    • Implemente o buffer de linha adequado no frontend para lidar com mensagens SSE parciais
    • Considere a implementação da lógica de reconexão para interrupções de rede
  6. Otimização do desempenho :

    • Adicione pequenos atrasos entre os tokens (asyncio.sleep(0.01)) para evitar sobrecarregar os clientes
    • lote vários tokens se eles chegarem muito rapidamente
    • Considere a implementação de mecanismos de contrapressão para clientes lentos

Analisando dados de feedback

Depois de coletar o feedback, o senhor pode analisá-lo para obter percepções sobre a qualidade do seu aplicativo e a satisfação do usuário.

Visualizando feedback na interface do usuário do Trace

UI de avaliações de rastreamento

Obtendo rastreamentos com feedback usando o SDK

Visualizando feedback na interface do usuário do Trace

rastrear feedback

Obtendo rastreamentos com feedback usando o SDK

Primeiro, recupere os traços de uma janela de tempo específica:

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

Analisando padrões de feedback usando o SDK

Extraia e analise o feedback dos rastreamentos:

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")

Analisando o feedback multidimensional

Para obter um feedback mais detalhado com as classificações:

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")

Próximas etapas