Scrivere un valutatore Genkit

Puoi estendere Firebase Genkit per supportare la valutazione personalizzata utilizzando un LLM come giudice o una valutazione programmatica (euristica).

Definizione di valutatore

I valutatori sono funzioni che valutano la risposta di un LLM. Esistono due approcci principali alla valutazione automatica: la valutazione basata su regole euristiche e la valutazione basata su LLM. Nell'approccio basato su heurismi, definisci una funzione deterministica. Al contrario, in una valutazione basata su LLM, i contenuti vengono reintrodotti in un LLM, e a questo viene chiesto di assegnare un punteggio all'output in base ai criteri impostati in un prompt.

Il metodo ai.defineEvaluator, che utilizzi per definire un'azione di valutatore in Genkit, supporta entrambi gli approcci. Questo documento illustra un paio di esempi di come utilizzare questo metodo per le valutazioni basate su euristiche e LLM.

Valutatori basati su LLM

Un valutatore basato su LLM sfrutta un LLM per valutare input, context e output della funzionalità di AI generativa.

I valutatori basati su LLM in Genkit sono costituiti da tre componenti:

  • Un prompt
  • Una funzione di punteggio
  • Un'azione di valutazione

Definisci il prompt

Per questo esempio, il valutatore utilizza un LLM per determinare se un alimento (output) è delizioso o meno. Innanzitutto, fornisci il contesto all'LLM, poi descrivi cosa vuoi che faccia e infine fornisci alcuni esempi su cui basare la sua risposta.

L'utilità definePrompt di Genkit offre un modo semplice per definire prompt con convalida di input e output. Il seguente codice è un esempio di configurazione di un prompt di valutazione con definePrompt.

import { z } from "genkit";

const DELICIOUSNESS_VALUES = ['yes', 'no', 'maybe'] as const;

const DeliciousnessDetectionResponseSchema = z.object({
  reason: z.string(),
  verdict: z.enum(DELICIOUSNESS_VALUES),
});

function getDeliciousnessPrompt(ai: Genkit) {
  return  ai.definePrompt({
      name: 'deliciousnessPrompt',
      input: {
        schema: z.object({
          responseToTest: z.string(),
        }),
      },
      output: {
        schema: DeliciousnessDetectionResponseSchema,
      }
    },
    `You are a food critic. Assess whether the provided output sounds delicious, giving only "yes" (delicious), "no" (not delicious), or "maybe" (undecided) as the verdict.

    Examples:
    Output: Chicken parm sandwich
    Response: { "reason": "A classic and beloved dish.", "verdict": "yes" }

    Output: Boston Logan Airport tarmac
    Response: { "reason": "Not edible.", "verdict": "no" }

    Output: A juicy piece of gossip
    Response: { "reason": "Metaphorically 'tasty' but not food.", "verdict": "maybe" }

    New Output: {{ responseToTest }}
    Response:
    `
  );
}

Definisci la funzione di punteggio

Definisci una funzione che prende un esempio che include output come richiesto dal prompt e assegna un punteggio al risultato. I test case di Genkit includono input come campo obbligatorio, con output e context come campi facoltativi. È responsabilità del valutatore verificare che siano presenti tutti i campi richiesti per la valutazione.

import { ModelArgument, z } from 'genkit';
import { BaseEvalDataPoint, Score } from 'genkit/evaluator';

/**
 * Score an individual test case for delciousness.
 */
export async function deliciousnessScore<
  CustomModelOptions extends z.ZodTypeAny,
>(
  judgeLlm: ModelArgument<CustomModelOptions>,
  dataPoint: BaseEvalDataPoint,
  judgeConfig?: CustomModelOptions
): Promise<Score> {
  const d = dataPoint;
  // Validate the input has required fields
  if (!d.output) {
    throw new Error('Output is required for Deliciousness detection');
  }

  // Hydrate the prompt and generate an evaluation result
  const deliciousnessPrompt = getDeliciousnessPrompt(ai);
  const response = await deliciousnessPrompt(
    {
      responseToTest: d.output as string,
    },
    {
      model: judgeLlm,
      config: judgeConfig,
    }
  );

  // Parse the output
  const parsedResponse = response.output;
  if (!parsedResponse) {
    throw new Error(`Unable to parse evaluator response: ${response.text}`);
  }

  // Return a scored response
  return {
    score: parsedResponse.verdict,
    details: { reasoning: parsedResponse.reason },
  };
}

Definisci l'azione dell'valutatore

Il passaggio finale consiste nello scrivere una funzione che definisce EvaluatorAction.

import { Genkit, z } from 'genkit';
import { BaseEvalDataPoint, EvaluatorAction } from 'genkit/evaluator';

/**
 * Create the Deliciousness evaluator action.
 */
export function createDeliciousnessEvaluator<
  ModelCustomOptions extends z.ZodTypeAny,
>(
  ai: Genkit,
  judge: ModelArgument<ModelCustomOptions>,
  judgeConfig?: z.infer<ModelCustomOptions>
): EvaluatorAction {
  return ai.defineEvaluator(
    {
      name: `myCustomEvals/deliciousnessEvaluator`,
      displayName: 'Deliciousness',
      definition: 'Determines if output is considered delicous.',
      isBilled: true,
    },
    async (datapoint: BaseEvalDataPoint) => {
      const score = await deliciousnessScore(judge, datapoint, judgeConfig);
      return {
        testCaseId: datapoint.testCaseId,
        evaluation: score,
      };
    }
  );
}

Il metodo defineEvaluator è simile ad altri costruttori Genkit come defineFlow e defineRetriever. Questo metodo richiede che venga fornito un EvaluatorFn come callback. Il metodo EvaluatorFn accetta un oggetto BaseEvalDataPoint, che corrisponde a una singola voce in un set di dati in fase di valutazione, insieme a un parametro opzionale custom-options, se specificato. La funzione elabora il punto dati e restituisce un oggetto EvalResponse.

Gli schemi Zod per BaseEvalDataPoint e EvalResponse sono riportati di seguito.

BaseEvalDataPoint
export const BaseEvalDataPoint = z.object({
  testCaseId: z.string(),
  input: z.unknown(),
  output: z.unknown().optional(),
  context: z.array(z.unknown()).optional(),
  reference: z.unknown().optional(),
  testCaseId: z.string().optional(),
  traceIds: z.array(z.string()).optional(),
});

export const EvalResponse = z.object({
  sampleIndex: z.number().optional(),
  testCaseId: z.string(),
  traceId: z.string().optional(),
  spanId: z.string().optional(),
  evaluation: z.union([ScoreSchema, z.array(ScoreSchema)]),
});
ScoreSchema
const ScoreSchema = z.object({
  id: z.string().describe('Optional ID to differentiate multiple scores').optional(),
  score: z.union([z.number(), z.string(), z.boolean()]).optional(),
  error: z.string().optional(),
  details: z
    .object({
      reasoning: z.string().optional(),
    })
    .passthrough()
    .optional(),
});

L'oggetto defineEvaluator consente all'utente di fornire un nome, un nome visualizzato leggibile dall'utente e una definizione per l'valutatore. Il nome visualizzato e la definizione vengono visualizzati insieme ai risultati della valutazione nell'interfaccia utente di sviluppo. Dispone inoltre di un campo facoltativo isBilled che indica se questo valutatore può comportare la fatturazione (ad es. utilizza un'API o un LLM fatturato). Se viene addebitato un valutatore, l'interfaccia utente chiede all'utente una conferma nella CLI prima di consentirgli di eseguire una valutazione. Questo passaggio aiuta a evitare spese indesiderate.

Valutatori di euristiche

Un valutatore di euristiche può essere qualsiasi funzione utilizzata per valutare input, context o output della funzionalità di AI generativa.

I valutatori euristici in Genkit sono costituiti da due componenti:

  • Una funzione di punteggio
  • Un'azione di valutazione

Definisci la funzione di punteggio

Come per l'valutatore basato su LLM, definisci la funzione di punteggio. In questo caso, la funzione di punteggio non ha bisogno di un LLM giudice.

import { EvalResponses } from 'genkit';
import { BaseEvalDataPoint, Score } from 'genkit/evaluator';

const US_PHONE_REGEX =
  /[\+]?[(]?[0-9]{3}[)]?[-\s\.]?[0-9]{3}[-\s\.]?[0-9]{4}/i;

/**
 * Scores whether a datapoint output contains a US Phone number.
 */
export async function usPhoneRegexScore(
  dataPoint: BaseEvalDataPoint
): Promise<Score> {
  const d = dataPoint;
  if (!d.output || typeof d.output !== 'string') {
    throw new Error('String output is required for regex matching');
  }
  const matches = US_PHONE_REGEX.test(d.output as string);
  const reasoning = matches
    ? `Output matched US_PHONE_REGEX`
    : `Output did not match US_PHONE_REGEX`;
  return {
    score: matches,
    details: { reasoning },
  };
}

Definisci l'azione dell'valutatore

import { Genkit } from 'genkit';
import { BaseEvalDataPoint, EvaluatorAction } from 'genkit/evaluator';

/**
 * Configures a regex evaluator to match a US phone number.
 */
export function createUSPhoneRegexEvaluator(ai: Genkit): EvaluatorAction {
  return ai.defineEvaluator(
    {
      name: `myCustomEvals/usPhoneRegexEvaluator`,
      displayName: "Regex Match for US PHONE NUMBER",
      definition: "Uses Regex to check if output matches a US phone number",
      isBilled: false,
    },
    async (datapoint: BaseEvalDataPoint) => {
      const score = await usPhoneRegexScore(datapoint);
      return {
        testCaseId: datapoint.testCaseId,
        evaluation: score,
      };
    }
  );
}

Riepilogo

Definizione del plug-in

I plug-in vengono registrati nel framework installandoli al momento dell'inizializzazione di Genkit. Per definire un nuovo plug-in, utilizza il metodo di assistenza genkitPlugin per creare istanze di tutte le azioni Genkit nel contesto del plug-in.

Questo esempio di codice mostra due valutatori: il valutatore di bontà basato su LLM e il valutatore di numeri di telefono statunitensi basato su regex. L'inizializzazione di questi valutatori nel contesto del plug-in li registra nel plug-in.

import { GenkitPlugin, genkitPlugin } from 'genkit/plugin';

export function myCustomEvals<
  ModelCustomOptions extends z.ZodTypeAny
>(options: {
  judge: ModelArgument<ModelCustomOptions>;
  judgeConfig?: ModelCustomOptions;
}): GenkitPlugin {
  // Define the new plugin
  return genkitPlugin("myCustomEvals", async (ai: Genkit) => {
    const { judge, judgeConfig } = options;

    // The plugin instatiates our custom evaluators within the context
    // of the `ai` object, making them available
    // throughout our Genkit application.
    createDeliciousnessEvaluator(ai, judge, judgeConfig);
    createUSPhoneRegexEvaluator(ai);
  });
}
export default myCustomEvals;

Configura Genkit

Aggiungi il plug-in myCustomEvals alla configurazione di Genkit.

Per la valutazione con Gemini, disattiva le impostazioni di sicurezza in modo che il valutatore possa accettare, rilevare e assegnare un punteggio ai contenuti potenzialmente dannosi.

import { gemini15Pro } from '@genkit-ai/googleai';

const ai = genkit({
  plugins: [
    vertexAI(),
    ...
    myCustomEvals({
      judge: gemini15Pro,
    }),
  ],
  ...
});

Utilizzare gli valutatori personalizzati

Una volta che hai creato i valutatori personalizzati nel contesto dell'app Genkit (tramite un plug-in o direttamente), sono pronti per essere utilizzati. L'esempio seguente illustra come provare lo valutatore di bontà con alcuni input e output di esempio.

  • 1. Crea un file JSON "deliciousness_dataset.json" con i seguenti contenuti:
[
  {
    "testCaseId": "delicous_mango",
    "input": "What is a super delicious fruit",
    "output": "A perfectly ripe mango – sweet, juicy, and with a hint of tropical sunshine."
  },
  {
    "testCaseId": "disgusting_soggy_cereal",
    "input": "What is something that is tasty when fresh but less tasty after some time?",
    "output": "Stale, flavorless cereal that's been sitting in the box too long."
  }
]
  • 2. Utilizza l'interfaccia a riga di comando Genkit per eseguire lo strumento di valutazione su questi casi di test.
# Start your genkit runtime
genkit start -- <command to start your app>
genkit eval:run deliciousness_dataset.json --evaluators=myCustomEvals/deliciousnessEvaluator
  • 3. Vai alla pagina "localhost:4000/evaluate" per visualizzare i risultati nell'interfaccia utente di Genkit.

È importante notare che l'affidabilità degli valutatori personalizzati aumenta man mano che li esegui il benchmark con set di dati o approcci standard. Esegui l'iterazione sui risultati di questi benchmark per migliorare il rendimento dei valutatori fino a raggiungere il livello di qualità target.