Cómo escribir un evaluador de Genkit

Firebase Genkit se puede extender para admitir la evaluación personalizada de los resultados de los casos de prueba, ya sea con un LLM como juez o de forma puramente programática.

Definición de evaluador

Los evaluadores son funciones que evalúan el contenido que se le proporciona a un LLM y que este genera. 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 determinista como las del desarrollo de software tradicional. En una evaluación basada en LLM, el contenido se vuelve a enviar a un LLM y se le solicita 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 se componen de 3 componentes:

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

Define la instrucción

En 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.

La utilidad definePrompt de Genkit proporciona una forma sencilla de definir instrucciones con validación de entrada y salida. A continuación, te indicamos cómo configurar una instrucción de evaluación con definePrompt.

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

const DeliciousnessDetectionResponseSchema = z.object({
  reason: z.string(),
  verdict: z.enum(DELICIOUSNESS_VALUES),
});
type DeliciousnessDetectionResponse = z.infer<typeof DeliciousnessDetectionResponseSchema>;

const DELICIOUSNESS_PROMPT = ai.definePrompt(
  {
    name: 'deliciousnessPrompt',
    inputSchema: z.object({
      output: z.string(),
    }),
    outputSchema: 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:
  {{output}}
  Response:
  `
);

Define la función de puntuación

Ahora, define la función que tomará un ejemplo que incluya output como lo requiere la instrucción y califica 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 estén presentes todos los campos necesarios para la evaluación.

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
  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 último paso es escribir una función que defina la acción del evaluador.

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

/**
 * 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: BaseEvalDataPoint) => {
      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 en Genkit se componen de 2 componentes:

  • Una función de puntuación
  • Una acción de 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.

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

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: 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 regex ${regex.source}`
    : `Output did not match regex ${regex.source}`;
  return {
    score: matches,
    details: { reasoning },
  };
}

Define la acción del evaluador

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

/**
 * 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: BaseEvalDataPoint) => {
        const score = await regexMatchScore(datapoint, regexMetric.regex);
        return fillScores(datapoint, score);
      }
    );
  });
}

Configuración

Opciones del complemento

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

Como mínimo, deberá 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 nuevo complemento usa un LLM como juez y admite el intercambio de LLM, 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 en el framework a través del archivo genkit.config.ts de un proyecto. Para poder configurar un nuevo complemento, define una función que defina un GenkitPlugin y configúralo con el PluginOptions definido anteriormente.

En este caso, tenemos dos evaluadores: DELICIOUSNESS y US_PHONE_REGEX_MATCH. Aquí es donde esos evaluadores se registran con el complemento y con Firebase Genkit.

export function myAwesomeEval<ModelCustomOptions extends z.ZodTypeAny>(
  options: PluginOptions<ModelCustomOptions>
): PluginProvider {
  // Define the new plugin
  const plugin = (options?: MyPluginOptions<ModelCustomOptions>) => {
    return genkitPlugin(
    'myAwesomeEval',
    async (ai: Genkit) => {
      const { judge, judgeConfig, metrics } = options;
      const evaluators: EvaluatorAction[] = metrics.map((metric) => {
        switch (metric) {
          case DELICIOUSNESS:
            // This evaluator requires an LLM as judge
            return createDeliciousnessEvaluator(ai, judge, judgeConfig);
          case US_PHONE_REGEX_MATCH:
            // This evaluator does not require an LLM
            return createUSPhoneRegexEvaluator();
        }
      });
      return { evaluators };
    })
  }
  // Create the plugin with the passed options
  return plugin(options);
}
export default myAwesomeEval;

Configura Genkit

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

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

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

const ai = genkit({
  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

Los mismos problemas que se aplican a la evaluación de la calidad del resultado de un atributo de 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 al nivel esperado, crea un conjunto de casos de prueba que tengan una respuesta correcta y otra incorrecta.

Como ejemplo de delicia, podría 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 se puedan seleccionar. También hay muchos conjuntos de datos de comparativas disponibles que se pueden usar.

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

genkit eval:run deliciousness_dataset.json

Consulta tus resultados en la IU de Genkit.

genkit start

Navega hacia localhost:4000/evaluate.