Поделиться через


Сбор отзывов пользователей

Сбор и ведение журнала отзывов пользователей важны для понимания реального качества приложения GenAI. MLflow предоставляет структурированный способ сбора отзывов в виде оценок для трассировок, что позволяет отслеживать качество с течением времени, определять области улучшения и создавать наборы данных оценки из производственных данных.

Предпосылки

Выберите соответствующий метод установки в зависимости от среды:

Производство

Для рабочих развертываний mlflow-tracing установите пакет:

pip install --upgrade mlflow-tracing

Пакет mlflow-tracing оптимизирован для использования в рабочей среде с минимальными зависимостями и улучшенными характеристиками производительности.

Развитие

Для сред разработки установите полный пакет MLflow с дополнительными компонентами Databricks:

pip install --upgrade "mlflow[databricks]>=3.1"

Полный mlflow[databricks] пакет включает все функции, необходимые для локальной разработки и экспериментирования в Databricks.

log_feedback API доступен в обоих пакетах, поэтому вы можете собирать отзывы пользователей независимо от выбранного метода установки.

Замечание

MLflow 3 требуется для сбора отзывов пользователей. MLflow 2.x не поддерживается из-за ограничений производительности и отсутствующих функций, необходимых для использования в рабочей среде.

Зачем собирать отзывы пользователей?

Отзывы пользователей предоставляют правду о производительности приложения:

  1. Сигналы качества реального мира . Узнайте, как фактические пользователи воспринимают выходные данные приложения.
  2. Непрерывное улучшение . Определение шаблонов в отрицательных отзывах для руководства по разработке
  3. Создание обучающих данных . Использование обратной связи для создания высококачественных наборов данных оценки
  4. Мониторинг качества . Отслеживание метрик удовлетворенности с течением времени и между различными сегментами пользователей
  5. Точная настройка модели - Используйте данные обратной связи для улучшения базовых моделей

Типы отзывов

MLflow поддерживает различные типы отзывов через свою систему оценки:

Тип обратной связи Описание Распространенные варианты использования
Двоичная обратная связь Простые пальцы вверх/вниз или правильно/неправильные Быстрые сигналы о удовлетворенности пользователей
Числовые оценки Рейтинги по шкале (например, 1-5 звезд) Подробная оценка качества
Категориальная обратная связь Параметры с несколькими вариантами выбора Классификация проблем или типов ответов
Обратная связь с текстом Комментарии в свободной форме Подробные объяснения пользователей

Общие сведения о модели данных обратной связи

В MLflow отзывы пользователей записываются с помощью сущности Отзывы, которая является типом оценки и может быть присоединена к трассировкам или определённым интервалам. Сущность Обратной связи предоставляет структурированный способ хранения:

  • Значение: фактическая обратная связь (логические, числовые, текстовые или структурированные данные)
  • Источник: информация о том, кто или что предоставил отзыв (человеческий пользователь, судья LLM или код)
  • Обоснование: Необязательное объяснение обратной связи
  • Метаданные: дополнительный контекст, например метки времени или настраиваемые атрибуты

Понимание этой модели данных помогает разрабатывать эффективные системы сбора отзывов, которые легко интегрируются с возможностями оценки и мониторинга MLflow. Подробные сведения о схеме сущности обратной связи и всех доступных полях см. в разделе "Отзывы" в модели данных трассировки.

Сбор отзывов конечных пользователей

При реализации коллекции отзывов в рабочей среде необходимо связать отзывы пользователей с определенными трассировками. Существует два подхода, которые можно использовать:

  1. Использование идентификаторов запросов клиента — создание собственных уникальных идентификаторов при обработке запросов и ссылки на них позже для обратной связи
  2. Использование идентификаторов трассировки MLflow . Используйте идентификатор трассировки, автоматически созданный MLflow

Общие сведения о потоке сбора отзывов

Оба подхода соответствуют аналогичной схеме:

  1. Во время первоначального запроса: приложение создает уникальный идентификатор запроса клиента или извлекает идентификатор трассировки, созданный MLflow.

  2. После получения ответа: пользователь может предоставить отзыв, ссылаясь на один из идентификаторов. Оба подхода следуют аналогичному шаблону:

  3. Во время первоначального запроса: приложение создает уникальный идентификатор запроса клиента или извлекает идентификатор трассировки, созданный MLflow.

  4. После получения ответа: пользователь может предоставить отзыв, ссылаясь на любой идентификатор.

  5. Отзыв регистрируется: API MLflow log_feedback создает оценку, подключенную к исходной трассировке.

  6. Анализ и мониторинг: Вы можете собирать и анализировать отзывы по всем данным трассировок.

Реализация коллекции отзывов

Подход 1. Использование идентификаторов трассировки MLflow

Самый простой подход — использовать идентификатор трассировки, который MLflow автоматически создает для каждой трассировки. Этот идентификатор можно получить во время обработки запроса и вернуть его клиенту:

Реализация бэкенда

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

Пример реализации пользовательского интерфейса

Ниже приведен пример реализации внешнего интерфейса для приложения на основе React:

// 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 && <span>Thanks for your feedback!</span>}
        </div>
      )}
    </div>
  );
}

Подход 2. Использование идентификаторов запросов клиента

Для управления отслеживанием запросов можно использовать собственные уникальные идентификаторы запросов клиента. Этот подход полезен, если необходимо поддерживать собственную систему отслеживания запросов или интегрироваться с существующей инфраструктурой:

Этот подход требует реализации отслеживания запросов, где каждая трассировка имеет client_request_id атрибут. Дополнительные сведения о подключении идентификаторов запросов клиента к трассировкам во время первоначального запроса см. в разделе "Добавление контекста в трассировки".

Реализация бэкенда

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

app = FastAPI()

class ChatRequest(BaseModel):
    message: str

class ChatResponse(BaseModel):
    response: str
    client_request_id: str  # Include the client request ID in the response

@app.post("/chat", response_model=ChatResponse)
def chat(request: ChatRequest):
    """
    Process a chat request and set a client request ID for later feedback collection.
    """
    # Sample: Generate a unique client request ID
    # Normally, this ID would be your app's backend existing ID for this interaction
    client_request_id = f"req-{uuid.uuid4().hex[:8]}"

    # Attach the client request ID to the current trace
    mlflow.update_current_trace(client_request_id=client_request_id)

    # Your GenAI application logic here
    response = process_message(request.message)  # Replace with your actual processing logic

    return ChatResponse(
        response=response,
        client_request_id=client_request_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(
    request: Request,
    client_request_id: str = Query(..., description="The request ID from the original interaction"),
    feedback: FeedbackRequest = ...
):
    """
    Collect user feedback for a specific interaction.

    This endpoint:
    1. Finds the trace using the client request ID
    2. Logs the feedback as an MLflow assessment
    3. Adds tags for easier querying and filtering
    """
    client = MlflowClient()

    # Find the trace using the client request ID
    experiment = client.get_experiment_by_name("/Shared/production-app")
    traces = client.search_traces(
        experiment_ids=[experiment.experiment_id],
        filter_string=f"attributes.client_request_id = '{client_request_id}'",
        max_results=1
    )

    if not traces:
        return {"status": "error", "message": "Unexpected error: request not found"}, 500

    # Log the feedback as an assessment
    # Assessments are the structured way to attach feedback to traces
    mlflow.log_feedback(
        trace_id=traces[0].info.trace_id,
        name="user_feedback",
        value=feedback.is_correct,
        source=AssessmentSource(
            source_type="HUMAN",  # Indicates this is human feedback
            source_id=request.headers.get("X-User-ID")  # Link feedback to the user who provided it
        ),
        rationale=feedback.comment  # Optional explanation from the user
    )

    return {
        "status": "success",
        "trace_id": traces[0].info.trace_id,
    }

Пример реализации пользовательского интерфейса

Ниже приведен пример реализации внешнего интерфейса для приложения на основе React. При использовании идентификаторов запросов клиента интерфейс должен хранить и управлять этими идентификаторами:

// React example with session-based request tracking
import React, { useState, useEffect } from 'react';

function ChatWithRequestTracking() {
  const [message, setMessage] = useState('');
  const [conversations, setConversations] = useState([]);
  const [sessionId] = useState(() => `session-${Date.now()}`);

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

      const data = await res.json();

      // Store conversation with request ID
      setConversations((prev) => [
        ...prev,
        {
          id: data.client_request_id,
          message: message,
          response: data.response,
          timestamp: new Date(),
          feedbackSubmitted: false,
        },
      ]);

      setMessage('');
    } catch (error) {
      console.error('Chat error:', error);
    }
  };

  const submitFeedback = async (requestId, isCorrect, comment = null) => {
    try {
      const params = new URLSearchParams({
        client_request_id: requestId,
      });

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

      if (res.ok) {
        // Mark feedback as submitted
        setConversations((prev) =>
          prev.map((conv) => (conv.id === requestId ? { ...conv, feedbackSubmitted: true } : conv)),
        );
      }
    } catch (error) {
      console.error('Feedback submission error:', error);
    }
  };

  return (
    <div>
      <div className="chat-history">
        {conversations.map((conv) => (
          <div key={conv.id} className="conversation">
            <div className="user-message">{conv.message}</div>
            <div className="bot-response">{conv.response}</div>
            <div className="feedback-section">
              <button onClick={() => submitFeedback(conv.id, true)} disabled={conv.feedbackSubmitted}>
                👍
              </button>
              <button onClick={() => submitFeedback(conv.id, false)} disabled={conv.feedbackSubmitted}>
                👎
              </button>
              {conv.feedbackSubmitted && <span>✓ Feedback received</span>}
            </div>
          </div>
        ))}
      </div>

      <div className="chat-input">
        <input
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && sendMessage()}
          placeholder="Type your message..."
        />
        <button onClick={sendMessage}>Send</button>
      </div>
    </div>
  );
}

Основные детали реализации

AssessmentSource: AssessmentSource объект определяет, кто или что предоставил отзыв:

  • source_type: может быть "HUMAN" для отзывов пользователей или "LLM_JUDGE" для автоматической оценки
  • source_id: определяет конкретного пользователя или систему, предоставляющую отзывы

Хранилище отзывов: Отзывы хранятся как оценки на записи, что означает:

  • Это постоянно связано с конкретным взаимодействием
  • Его можно запрашивать вместе с данными трассировки
  • Он отображается в пользовательском интерфейсе MLflow при просмотре трассировки данных

Обработка различных типов отзывов

Вы можете расширить любой подход для поддержки более сложных отзывов. Ниже приведен пример использования идентификаторов трассировки:

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
    }

Обработка отзывов с потоковыми ответами

При использовании потоковых ответов (Server-Sent События или WebSockets) Идентификатор трассировки недоступен до завершения потока. Это представляет собой уникальную задачу для сбора отзывов, требующих другого подхода.

Почему потоковая передача отличается

В традиционных шаблонах ответа на запросы вы получаете полный ответ и идентификатор трассировки вместе. С потоковой передачей:

  1. Токены поступают постепенно: ответ формируется по мере поступления токенов из LLM
  2. Завершение трассировки отложено: идентификатор трассировки создается только после завершения всего потока.
  3. Пользовательский интерфейс обратной связи должен ждать: пользователи не могут предоставлять отзывы, пока у них не будет полный ответ и идентификатор трассировки.

Реализация серверной части с помощью SSE

Вот как реализовать потоковую передачу с доставкой идентификатора трассировки в конце потока:

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

Реализация внешнего интерфейса для потоковой передачи

Обработайте события потоковой передачи и включите обратную связь только после получения идентификатора трассировки:

// 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 className="typing-indicator">...</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">
          <span>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 && <span className="feedback-thanks">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>
  );
}

Основные рекомендации по потоковой передаче

При реализации сбора отзывов с потоковыми ответами следует учитывать следующие моменты:

  1. Тайминг идентификатора трассировки: Идентификатор трассировки доступен только после завершения потоковой передачи. Настройте пользовательский интерфейс для корректной обработки этого, отключив элементы управления обратной связью до получения идентификатора трассировки.

  2. Структура событий: используйте согласованный формат событий с type полем, чтобы различать маркеры содержимого, события завершения и ошибки. Это делает синтаксический анализ и обработку событий более надежным.

  3. Управление состоянием: отдельно отслеживайте как содержимое потоковой передачи, так и идентификатор трассировки. Сбросьте все состояние в начале каждого нового взаимодействия, чтобы предотвратить проблемы с устаревшими данными.

  4. Обработка ошибок. Включите события ошибок в поток для корректной обработки сбоев. Убедитесь, что ошибки регистрируются в трассировку, когда это возможно для отладки.

  5. Управление буферами:

    • Отключение буферизации прокси-сервера с помощью X-Accel-Buffering: no заголовка
    • Реализация правильной буферизации строк в интерфейсе для обработки частичных сообщений SSE
    • Рассмотрите возможность реализации логики повторного подключения для прерываний сети
  6. Оптимизация производительности:

    • Добавьте небольшие задержки между токенами (asyncio.sleep(0.01)), чтобы не перегружать клиентов.
    • Сгруппируйте токены, если они поступают слишком быстро
    • Рассмотрите возможность реализации механизмов обратного давления для медленных клиентов

Анализ данных обратной связи

После сбора отзывов вы можете проанализировать его, чтобы получить аналитические сведения о качестве и удовлетворенности пользователей приложения.

Просмотр обратной связи в Trace UI

Получение трассировок с обратной связью с помощью пакета SDK

Просмотр обратной связи в Trace UI

Обратная связь трассировки

Получение трассировок с обратной связью с помощью пакета SDK

Сначала извлеките трассировки из выделенного периода времени.

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

Анализ шаблонов обратной связи с помощью пакета SDK

Извлечение и анализ обратной связи из трассировок:

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

Анализ многомерной обратной связи

Для получения более подробных отзывов с рейтингами:

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

Факторы, учитываемые при производстве

Сведения о рабочих развертываниях см. в руководстве по наблюдаемости рабочей среды с трассировкой , которая охватывает:

  • Реализация конечных точек сбора отзывов
  • Связывание отзывов с трассировками с помощью идентификаторов запросов клиента
  • Настройка мониторинга качества в режиме реального времени
  • Лучшие практики для обработки отзывов в условиях большого объема

Дальнейшие шаги

Продолжайте свое путешествие с помощью этих рекомендуемых действий и руководств.

Справочные руководства

Ознакомьтесь с подробной документацией по концепциям и функциям, упомянутым в этом руководстве.