Como criar um avaliador do Genkit

É possível estender o Firebase Genkit para oferecer suporte à avaliação personalizada, usando um LLM como juiz ou por avaliação programática (heurística).

Definição do avaliador

Os avaliadores são funções que avaliam a resposta de um LLM. Há duas abordagens principais para a avaliação automatizada: avaliação heurística e avaliação baseada em LLM. Na abordagem heurística, você define uma função determinística. Por outro lado, em uma avaliação baseada em LLM, o conteúdo é enviado de volta a um LLM, e o LLM é solicitado a pontuar a saída de acordo com os critérios definidos em um comando.

O método ai.defineEvaluator, que você usa para definir uma ação de avaliador no Genkit, aceita as duas abordagens. Este documento apresenta alguns exemplos de como usar esse método para avaliações heurísticas e baseadas em LLM.

Avaliadores baseados em LLM

Um avaliador baseado em LLM usa um LLM para avaliar o input, context e output 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 o comando

Neste exemplo, o avaliador usa um LLM para determinar se um alimento (output) é delicioso ou não. Primeiro, forneça contexto ao LLM, depois descreva o que você quer que ele faça e, por fim, dê alguns exemplos para basear a resposta.

O utilitário definePrompt do Genkit oferece uma maneira fácil de definir comandos com validação de entrada e saída. O código abaixo é um exemplo de como configurar uma solicitação de avaliação com 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,
      }
      prompt: `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:
      `
  });
}

Definir a função de pontuação

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

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

/**
 * Score an individual test case for delciousness.
 */
export async function deliciousnessScore<
  CustomModelOptions extends z.ZodTypeAny,
>(
  ai: Genkit,
  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 },
  };
}

Definir a ação do avaliador

A etapa final é escrever uma função que defina o EvaluatorAction.

import { 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(ai, judge, datapoint, judgeConfig);
      return {
        testCaseId: datapoint.testCaseId,
        evaluation: score,
      };
    }
  );
}

O método defineEvaluator é semelhante a outros construtores do Genkit, como defineFlow e defineRetriever. Esse método exige que um EvaluatorFn seja fornecido como um callback. O método EvaluatorFn aceita um objeto BaseEvalDataPoint, que corresponde a uma única entrada em um conjunto de dados em avaliação, junto com um parâmetro de opções personalizadas opcional, se especificado. A função processa o ponto de dados e retorna um objeto EvalResponse.

Os esquemas Zod para BaseEvalDataPoint e EvalResponse são os seguintes.

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

O objeto defineEvaluator permite que o usuário forneça um nome, um nome de exibição legível pelo usuário e uma definição para o avaliador. O nome de exibição e a definição são mostrados junto com os resultados da avaliação na interface para desenvolvedores. Ele também tem um campo isBilled opcional que marca se esse avaliador pode resultar em faturamento (por exemplo, se ele usa um LLM ou uma API faturada). Se um avaliador for cobrado, a interface solicitará uma confirmação do usuário na CLI antes de permitir que ele execute uma avaliação. Essa etapa ajuda a evitar gastos indesejados.

Avaliadores heurísticos

Um avaliador heurístico pode ser qualquer função usada para avaliar o input, context ou output 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 no avaliador baseado em LLM, defina a função de pontuação. Nesse caso, a função de pontuação não precisa de um LLM de juiz.

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

Definir a ação do avaliador

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

Em resumo

Definição do plug-in

Os plug-ins são registrados no framework instalando-os no momento da iniciação do Genkit. Para definir um novo plug-in, use o método auxiliar genkitPlugin para instanciar todas as ações do Genkit no contexto do plug-in.

Este exemplo de código mostra dois avaliadores: o avaliador de delícias baseado em LLM e o avaliador de número de telefone dos EUA baseado em regex. A instanciação desses avaliadores no contexto do plug-in os registra com o 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;

Configurar o Genkit

Adicione o plug-in myCustomEvals à 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 { gemini15Pro } from '@genkit-ai/googleai';

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

Como usar avaliadores personalizados

Depois que você instanciar os avaliadores personalizados no contexto do app do Genkit (por um plug-in ou diretamente), eles estarão prontos para uso. O exemplo a seguir ilustra como testar o avaliador de gostosura com algumas entradas e saídas de amostra.

  • 1. Crie um arquivo JSON "deliciousness_dataset.json" com o seguinte conteúdo:
[
  {
    "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. Use a CLI do Genkit para executar o avaliador nesses casos de teste.
# Start your genkit runtime
genkit start -- <command to start your app>
genkit eval:run deliciousness_dataset.json --evaluators=myCustomEvals/deliciousnessEvaluator
  • 3. Acesse "localhost:4000/evaluate" para conferir os resultados na interface do Genkit.

É importante observar que a confiança nos avaliadores personalizados aumenta à medida que você os compara com conjuntos de dados ou abordagens padrão. Itere os resultados desses comparativos para melhorar a performance dos avaliadores até que ela atinja o nível de qualidade desejado.