Como criar um plug-in de avaliação 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 maneira programática.

Definição de avaliador

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 (testes): 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 é retornado a um LLM, que precisa pontuar o resultado 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 seu 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 comando solicitará que o LLM avalie a qualidade da saída. Primeiro, forneça contexto ao LLM, descreva o que você quer que ele faça e, por fim, dê alguns exemplos para basear a resposta.

O Genkit vem com o dotprompt, que oferece uma maneira fácil de definir e gerenciar comandos com recursos como validação de esquema de entrada/saída. Veja como usar dotprompt para definir um comando de avaliação.

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

Definir a função de pontuação

Agora, defina a função que vai usar um exemplo que inclui output conforme exigido pelo comando e atribuir uma pontuação ao 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 a presença de todos os campos obrigatórios da avaliação.

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

Definir a ação do avaliador

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

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

Avaliadores heurísticos

Um avaliador heurístico pode ser qualquer função usada para avaliar a entrada, o contexto ou a saída do seu 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 dos jurados nem a configuração dele.

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

Definir a ação do avaliador

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

Configuração

Opções de plug-in

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

No mínimo, será necessário 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 o novo plug-in usar um LLM como juiz e o plug-in oferecer suporte à troca de qual LLM usar, 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 com o 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.

Neste caso, temos dois avaliadores, DELICIOUSNESS e US_PHONE_REGEX_MATCH. Os avaliadores são registrados com o plug-in e o 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 o Genkit

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

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

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

export default configureGenkit({
  plugins: [
    ...
    myAwesomeEval({
      judge: geminiPro,
      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
      ],
    }),
  ],
  ...
});

testes

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

Para ter uma noção do desempenho do avaliador personalizado no nível esperado, crie um conjunto de casos de teste que tenham uma resposta certa e errada.

Por exemplo, pode parecer com um arquivo 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."
  }
]

Esses exemplos podem ser gerados por humanos ou você pode pedir para um LLM ajudar a criar um conjunto de casos de teste que podem ser selecionados. Há muitos conjuntos de dados de comparativo de mercado 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

Veja os resultados na interface do Genkit.

genkit start

Acesse localhost:4000/evaluate.