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.

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

A compreensão desse modelo de dados ajuda o senhor 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 no Tracing Data Model.

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

Obtendo traços com feedback por meio do SDK

Visualizando feedback na interface do usuário do Trace

rastrear feedback

Obtendo traços com feedback por meio do 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

Análise de padrões de feedback por meio do 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")

Considerações de produção

Para implantações de produção, consulte nosso guia sobre observabilidade de produção com rastreamento, que abrange:

  • Implementação do ponto de extremidade de coleta de feedback
  • Vinculando feedback a rastreamentos usando IDs de solicitação do cliente
  • Configuração do monitoramento de qualidade em tempo real
  • Práticas recomendadas para processamento de feedback em alto volume

Próximas etapas

Continue sua jornada com estas ações recomendadas e o tutorial.

Guia de referência

Explore a documentação detalhada dos conceitos e recursos mencionados neste guia.