Cómo escribir un complemento de Genkit Evaluator

Firebase Genkit se puede extender para admitir la evaluación personalizada del resultado de un caso de prueba, ya sea mediante el uso de un LLM como juez o de forma puramente programática.

Definición del evaluador

Los evaluadores son funciones que evalúan el contenido proporcionado a un LLM y generado por él. Existen dos enfoques principales para la evaluación automatizada (pruebas): la evaluación heurística y la evaluación basada en LLM. En el enfoque heurístico, defines una función determinística como las del desarrollo de software tradicional. En una evaluación basada en un LLM, el contenido se vuelve a enviar a un LLM y se le pide al LLM que califique el resultado según los criterios establecidos en una instrucción.

Evaluadores basados en LLM

Un evaluador basado en LLM aprovecha un LLM para evaluar la entrada, el contexto o el resultado de tu función de IA generativa.

Los evaluadores basados en LLM en Genkit constan de 3 componentes:

  • Un mensaje
  • Una función de puntuación
  • Una acción del evaluador

Define la instrucción

Para este ejemplo, la instrucción le pedirá al LLM que juzgue qué tan delicioso es el resultado. Primero, proporciona contexto al LLM, luego describe lo que quieres que haga y, por último, dale algunos ejemplos en los que basar su respuesta.

Genkit incluye dotprompt, que proporciona una manera sencilla de definir y administrar instrucciones con funciones como la validación de esquema de entrada y salida. A continuación, se muestra cómo puedes usar dotprompt para definir una instrucción de evaluación.

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:
`
);

Define la función de puntuación

Ahora, define la función que tomará un ejemplo que incluya output según lo requiera la instrucción y calificará el resultado. Los casos de prueba de Genkit incluyen input como un campo obligatorio, con campos opcionales para output y context. Es responsabilidad del evaluador validar que todos los campos necesarios para la evaluación estén presentes.

/**
 * 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 },
  };
}

Define la acción del evaluador

El paso final es escribir una función que defina la acción del evaluador en sí.

/**
 * 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,
      };
    }
  );
}

Evaluadores heurísticos

Un evaluador heurístico puede ser cualquier función que se use para evaluar la entrada, el contexto o el resultado de tu función de IA generativa.

Los evaluadores heurísticos de Genkit constan de 2 componentes:

  • Una función de puntuación
  • Una acción del evaluador

Define la función de puntuación

Al igual que el evaluador basado en LLM, define la función de puntuación. En este caso, la función de puntuación no necesita conocer el LLM del juez ni su configuración.

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

Define la acción del evaluador

/**
 * 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);
      }
    );
  });
}

Configuración

Opciones del complemento

Define el elemento PluginOptions que usará el complemento de evaluador personalizado. Este objeto no tiene requisitos estrictos y depende de los tipos de evaluadores que se definen.

Como mínimo, será necesario tomar la definición de qué métricas registrar.

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

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

Si este complemento nuevo usa un LLM como juez y el complemento admite cambiar el LLM que se usará, define parámetros adicionales en el objeto 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>;
}

Definición del complemento

Los complementos se registran con el framework a través del archivo genkit.config.ts en un proyecto. Para poder configurar un complemento nuevo, define una función que defina un GenkitPlugin y lo configure con el PluginOptions definido anteriormente.

En este caso, tenemos dos evaluadores, DELICIOUSNESS y US_PHONE_REGEX_MATCH. Aquí es donde se registran los evaluadores con el complemento y 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;

Configurar Genkit

Agrega el complemento definido recientemente a tu configuración de Genkit.

Para la evaluación con Gemini, inhabilita la configuración de seguridad de modo que el evaluador pueda aceptar, detectar y calificar el contenido potencialmente dañino.

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
      ],
    }),
  ],
  ...
});

Prueba la configuración

Los mismos problemas que se aplican a la evaluación de la calidad del resultado de una función de la IA generativa se aplican a la evaluación de la capacidad de evaluación de un evaluador basado en LLM.

Para tener una idea de si el evaluador personalizado funciona en el nivel esperado, crea un conjunto de casos de prueba que tengan una respuesta clara y incorrecta.

Como ejemplo de delicia, puede verse como un archivo 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."
  }
]

Estos ejemplos pueden ser generados por humanos o puedes pedirle a un LLM que te ayude a crear un conjunto de casos de prueba que pueden seleccionarse. Hay muchos conjuntos de datos de comparativas disponibles que también se pueden usar.

Luego, usa la CLI de Genkit para ejecutar el evaluador en estos casos de prueba.

genkit eval:run deliciousness_dataset.json

Ve tus resultados en la IU de Genkit.

genkit start

Navega hacia localhost:4000/evaluate.