Como criar um avaliador do Genkit

O Firebase Genkit pode ser estendido para oferecer suporte à avaliação personalizada da saída do caso de teste usando um LLM como juiz ou de forma puramente programática.

Definição do avaliador

Os avaliadores são funções que avaliam o conteúdo fornecido e gerado por um LLM. Há duas abordagens principais para a avaliação automatizada (teste): avaliação heurística e avaliação baseada em LLM. Na abordagem heurística, você define uma função determinística, como as do desenvolvimento de software tradicional. Em uma avaliação baseada em LLM, o conteúdo é enviado de volta a um LLM, que é solicitado a pontuar a saída de acordo com os critérios definidos em um comando.

Avaliadores baseados em LLM

Um avaliador baseado em LLM usa um LLM para avaliar a entrada, o contexto ou a saída do recurso de IA generativa.

Os avaliadores baseados em LLM no Genkit são compostos por três componentes:

  • Um comando
  • Uma função de pontuação
  • Uma ação do avaliador

Definir a solicitação

Neste exemplo, o comando vai pedir ao LLM para julgar o quão deliciosa é a saída. Primeiro, forneça contexto ao LLM, depois descreva o que você quer que ele faça e, por fim, dê alguns exemplos para embasar a resposta.

O utilitário definePrompt do Genkit oferece uma maneira fácil de definir comandos com validação de entrada e saída. Saiba como configurar uma solicitação de avaliação com 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:
  `
);

Definir a função de pontuação

Agora, defina a função que vai receber um exemplo que inclui output, conforme exigido pelo comando, e avalie o resultado. Os casos de teste do Genkit incluem input como um campo obrigatório, com campos opcionais para output e context. É responsabilidade do avaliador validar se todos os campos necessários para a avaliação estão presentes.

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

Definir a ação do avaliador

A etapa final é escrever uma função que defina a própria ação do avaliador.

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

Avaliadores heurísticos

Um avaliador heurístico pode ser qualquer função usada para avaliar a entrada, o contexto ou a saída do recurso de IA generativa.

Os avaliadores heurísticos no Genkit são compostos por dois componentes:

  • Uma função de pontuação
  • Uma ação do avaliador

Definir a função de pontuação

Assim como o avaliador baseado em LLM, defina a função de pontuação. Nesse caso, a função de pontuação não precisa saber sobre o LLM do juiz ou a configuração dele.

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

Definir a ação do avaliador

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

Configuração

Opções do plug-in

Defina o PluginOptions que o plug-in do avaliador personalizado vai usar. Esse objeto não tem requisitos rígidos e depende dos tipos de avaliadores definidos.

No mínimo, ele precisa ter a definição de quais métricas registrar.

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

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

Se esse novo plug-in usar um LLM como juiz e tiver suporte para trocar o LLM a ser usado, defina outros parâmetros no 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>;
}

Definição do plug-in

Os plug-ins são registrados no framework pelo arquivo genkit.config.ts em um projeto. Para configurar um novo plug-in, defina uma função que defina um GenkitPlugin e o configure com o PluginOptions definido acima.

Nesse caso, temos dois avaliadores, DELICIOUSNESS e US_PHONE_REGEX_MATCH. É aqui que esses avaliadores são registrados no plug-in e no 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;

Configurar o Genkit

Adicione o plug-in recém-definido à configuração do Genkit.

Para a avaliação com o Gemini, desative as configurações de segurança para que o avaliador possa aceitar, detectar e classificar o conteúdo potencialmente nocivo.

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

Teste

Os mesmos problemas que se aplicam à avaliação da qualidade da saída de um recurso de IA generativa também se aplicam à capacidade de julgamento de um avaliador baseado em LLM.

Para ter uma ideia se o avaliador personalizado tem o desempenho esperado, crie um conjunto de casos de teste com uma resposta certa e errada clara.

Como exemplo de delícia, um arquivo JSON deliciousness_dataset.json pode ter esta aparência:

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

Esses exemplos podem ser gerados por humanos ou você pode pedir a um LLM para ajudar a criar um conjunto de casos de teste que podem ser selecionados. Há muitos conjuntos de dados de comparação disponíveis que também podem ser usados.

Em seguida, use a CLI do Genkit para executar o avaliador nesses casos de teste.

genkit eval:run deliciousness_dataset.json

Confira seus resultados na interface do Genkit.

genkit start

Acesse localhost:4000/evaluate.