Scrivere un plug-in di valutazione Genkit

Firebase Genkit può essere esteso per supportare la valutazione personalizzata dell'output del caso di test utilizzando un LLM come giudice o in modo puramente programmatico.

Definizione dello strumento di valutazione

I valutatori sono funzioni che valutano i contenuti forniti e generati da un LLM. Ci sono due approcci principali alla valutazione automatizzata (test): valutazione euristica e valutazione basata su LLM. Nell'approccio euristico, definisci una funzione deterministica come quelle dello sviluppo software tradizionale. In una valutazione basata su LLM, il contenuto viene restituito a un LLM e all'LLM viene chiesto di assegnare un punteggio all'output in base ai criteri impostati in un prompt.

Valutatori basati su LLM

Un valutatore basato su LLM sfrutta un LLM per valutare l'input, il contesto o l'output della funzionalità di IA generativa.

I valutatori basati su LLM in Genkit sono composti da 3 componenti:

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

Definisci il prompt

Per questo esempio, il prompt chiederà all'LLM di giudicare la bontà dell'output. Innanzitutto, fornisci il contesto all'LLM, poi descrivi cosa vuoi che faccia e, infine, fornisci alcuni esempi su cui basare la sua risposta.

Genkit viene fornito con dotprompt, che fornisce un modo semplice per definire e gestire i prompt con funzionalità come la convalida dello schema di input/output. Ecco come puoi utilizzare dotprompt per definire un prompt di valutazione.

import { defineDotprompt } from '@genkit-ai/dotprompt';

// Define the expected output values
const DELICIOUSNESS_VALUES = ['yes', 'no', 'maybe'] as const;

// Define the response schema expected from the LLM
const DeliciousnessDetectionResponseSchema = z.object({
  reason: z.string(),
  verdict: z.enum(DELICIOUSNESS_VALUES),
});
type DeliciousnessDetectionResponse = z.infer<
  typeof DeliciousnessDetectionResponseSchema
>;

const DELICIOUSNESS_PROMPT = defineDotprompt(
  {
    input: {
      schema: z.object({
        output: z.string(),
      }),
    },
    output: {
      schema: DeliciousnessDetectionResponseSchema,
    },
  },
  `You are a food critic with a wide range in taste. Given the output, decide if it sounds delicious and provide your reasoning. Use only "yes" (if delicous), "no" (if not delicious), "maybe" (if you can't decide) as the verdict.

Here are a few examples:

Output:
Chicken parm sandwich
Response:
{ "reason": "This is a classic sandwich enjoyed by many - totally delicious", "verdict":"yes"}

Output:
Boston logan international airport tarmac
Response:
{ "reason": "This is not edible and definitely not delicious.", "verdict":"no"}

Output:
A juicy piece of gossip
Response:
{ "reason": "Gossip is sometimes metaphorically referred to as tasty.", "verdict":"maybe"}

Here is a new submission to assess:

Output:
{{output}}
Response:
`
);

Definisci la funzione di punteggio

Ora definisci la funzione che prenderà un esempio che include output come richiesto dal prompt e assegnerà un punteggio al risultato. Gli scenari di test Genkit includono input come campo obbligatorio, con campi facoltativi per output e context. È responsabilità del valutatore verificare che siano presenti tutti i campi obbligatori per la valutazione.

/**
 * Score an individual test case for delciousness.
 */
export async function deliciousnessScore<
  CustomModelOptions extends z.ZodTypeAny,
>(
  judgeLlm: ModelArgument<CustomModelOptions>,
  dataPoint: BaseDataPoint,
  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
  const finalPrompt = DELICIOUSNESS_PROMPT.renderText({
    output: d.output as string,
  });

  // Call the LLM to generate an evaluation result
  const response = await generate({
    model: judgeLlm,
    prompt: finalPrompt,
    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 l'azione di valutazione stessa.

/**
 * Create the Deliciousness evaluator action.
 */
export function createDeliciousnessEvaluator<
  ModelCustomOptions extends z.ZodTypeAny,
>(
  judge: ModelReference<ModelCustomOptions>,
  judgeConfig: z.infer<ModelCustomOptions>
): EvaluatorAction {
  return defineEvaluator(
    {
      name: `myAwesomeEval/deliciousness`,
      displayName: 'Deliciousness',
      definition: 'Determines if output is considered delicous.',
    },
    async (datapoint: BaseDataPoint) => {
      const score = await deliciousnessScore(judge, datapoint, judgeConfig);
      return {
        testCaseId: datapoint.testCaseId,
        evaluation: score,
      };
    }
  );
}

Valutatori di regole euristiche

Una valutazione euristica può essere qualsiasi funzione utilizzata per valutare l'input, il contesto o l'output della tua funzionalità di IA generativa.

I valutatori euristici in Genkit sono costituiti da due componenti:

  • Una funzione di punteggio
  • Un'azione del valutatore

Definisci la funzione di punteggio

Come per il valutatore basato su LLM, definisci la funzione di punteggio. In questo caso, la funzione di punteggio non deve conoscere l'LLM del giudice o la relativa configurazione.

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

/**
 * Scores whether an individual datapoint matches a US Phone Regex.
 */
export async function usPhoneRegexScore(
  dataPoint: BaseDataPoint
): 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 regex ${regex.source}`
    : `Output did not match regex ${regex.source}`;
  return {
    score: matches,
    details: { reasoning },
  };
}

Definisci l'azione del valutatore

/**
 * Configures a regex evaluator to match a US phone number.
 */
export function createUSPhoneRegexEvaluator(
  metrics: RegexMetric[]
): EvaluatorAction[] {
  return metrics.map((metric) => {
    const regexMetric = metric as RegexMetric;
    return defineEvaluator(
      {
        name: `myAwesomeEval/${metric.name.toLocaleLowerCase()}`,
        displayName: 'Regex Match',
        definition:
          'Runs the output against a regex and responds with 1 if a match is found and 0 otherwise.',
        isBilled: false,
      },
      async (datapoint: BaseDataPoint) => {
        const score = await regexMatchScore(datapoint, regexMetric.regex);
        return fillScores(datapoint, score);
      }
    );
  });
}

Configurazione

Opzioni del plug-in

Definisci il PluginOptions che verrà utilizzato dal plug-in di valutazione personalizzato. Questo oggetto non ha requisiti rigorosi e dipende dai tipi di valutatori definiti.

Come minimo, dovrà includere la definizione delle metriche da registrare.

export enum MyAwesomeMetric {
  WORD_COUNT = 'WORD_COUNT',
  US_PHONE_REGEX_MATCH = 'US_PHONE_REGEX_MATCH',
}

export interface PluginOptions {
  metrics?: Array<MyAwesomeMetric>;
}

Se questo nuovo plug-in utilizza un LLM come giudice e supporta la sostituzione dell'LLM da utilizzare, definisci parametri aggiuntivi nell'oggetto PluginOptions.

export enum MyAwesomeMetric {
  DELICIOUSNESS = 'DELICIOUSNESS',
  US_PHONE_REGEX_MATCH = 'US_PHONE_REGEX_MATCH',
}

export interface PluginOptions<ModelCustomOptions extends z.ZodTypeAny> {
  judge: ModelReference<ModelCustomOptions>;
  judgeConfig?: z.infer<ModelCustomOptions>;
  metrics?: Array<MyAwesomeMetric>;
}

Definizione del plug-in

I plug-in vengono registrati nel framework tramite il file genkit.config.ts in un progetto. Per poter configurare un nuovo plug-in, definisci una funzione che definisce un GenkitPlugin e lo configuri con il PluginOptions definito sopra.

In questo caso abbiamo due valutatori, DELICIOUSNESS e US_PHONE_REGEX_MATCH. È qui che i valutatori vengono registrati con il plug-in e con Firebase Genkit.

export function myAwesomeEval<ModelCustomOptions extends z.ZodTypeAny>(
  params: PluginOptions<ModelCustomOptions>
): PluginProvider {
  // Define the new plugin
  const plugin = genkitPlugin(
    'myAwesomeEval',
    async (params: PluginOptions<ModelCustomOptions>) => {
      const { judge, judgeConfig, metrics } = params;
      const evaluators: EvaluatorAction[] = metrics.map((metric) => {
        // We'll create these functions in the next step
        switch (metric) {
          case DELICIOUSNESS:
            // This evaluator requires an LLM as judge
            return createDeliciousnessEvaluator(judge, judgeConfig);
          case US_PHONE_REGEX_MATCH:
            // This evaluator does not require an LLM
            return createUSPhoneRegexEvaluator();
        }
      });
      return { evaluators };
    }
  );

  // Create the plugin with the passed params
  return plugin(params);
}
export default myAwesomeEval;

Configura Genkit

Aggiungi il plug-in appena definito alla configurazione Genkit.

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

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

export default configureGenkit({
  plugins: [
    ...
    myAwesomeEval({
      judge: gemini15Flash,
      judgeConfig: {
        safetySettings: [
          {
            category: 'HARM_CATEGORY_HATE_SPEECH',
            threshold: 'BLOCK_NONE',
          },
          {
            category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
            threshold: 'BLOCK_NONE',
          },
          {
            category: 'HARM_CATEGORY_HARASSMENT',
            threshold: 'BLOCK_NONE',
          },
          {
            category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
            threshold: 'BLOCK_NONE',
          },
        ],
      },
      metrics: [
        MyAwesomeMetric.DELICIOUSNESS,
        MyAwesomeMetric.US_PHONE_REGEX_MATCH
      ],
    }),
  ],
  ...
});

Test

Gli stessi problemi che si applicano alla valutazione della qualità dell'output di una funzionalità di IA generativa si applicano alla valutazione della capacità di giudizio di un valutatore basato su LLM.

Per capire se il valutatore personalizzato funziona al livello previsto, crea un insieme di casi di test con una risposta chiara corretta e sbagliata.

Un esempio di bontà potrebbe avere il seguente aspetto in un file JSON deliciousness_dataset.json:

[
  {
    "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."
  }
]

Questi esempi possono essere generati da persone fisiche oppure puoi chiedere a un modello LLM di aiutarti a creare un insieme di casi di test che possono essere selezionati. Esistono anche molti set di dati di benchmark che è possibile utilizzare.

Quindi, utilizza l'interfaccia a riga di comando Genkit per eseguire lo strumento di valutazione su questi casi di test.

genkit eval:run deliciousness_dataset.json

Visualizza i risultati nell'interfaccia utente di Genkit.

genkit start

Vai a localhost:4000/evaluate.