Como gerar conteúdo com modelos de IA

A IA generativa tem como base os modelos de IA. Atualmente, os dois exemplos mais importantes de modelos generativos são modelos de linguagem grandes (LLMs) e modelos de geração de imagens. Esses modelos recebem uma entrada, chamada de comando (mais comumente texto, imagem ou uma combinação de ambos) e produzem como saída texto, imagem ou até áudio ou vídeo.

A saída desses modelos pode ser surpreendentemente convincente: os LLMs geram texto que parece ter sido escrito por um ser humano, e os modelos de geração de imagens podem produzir imagens muito próximas de fotografias reais ou artes criadas por humanos.

Além disso, os LLMs mostraram capacidade para tarefas além da geração de texto simples:

  • Como escrever programas de computador
  • Planejar subtarefas necessárias para concluir uma tarefa maior
  • Como organizar dados desorganizados
  • Entender e extrair dados de informações de um corpus de texto
  • Seguir e realizar atividades automatizadas com base em uma descrição de texto da atividade

Há muitos modelos disponíveis de vários provedores. Cada modelo tem pontos fortes e fracos, e um modelo pode se destacar em uma tarefa, mas não em outras. Os apps que usam a IA generativa podem se beneficiar do uso de vários modelos diferentes, dependendo da tarefa em questão.

Como desenvolvedor de apps, você normalmente não interage diretamente com modelos de IA generativa, mas sim por meio de serviços disponíveis como APIs da Web. Embora esses serviços geralmente tenham funcionalidades semelhantes, todos eles as fornecem por APIs diferentes e incompatíveis. Se você quiser usar vários serviços de modelo, use cada um dos SDKs proprietários, que podem ser incompatíveis entre si. E se você quiser fazer upgrade de um modelo para o mais recente e mais capaz, talvez seja necessário criar essa integração de novo outra vez.

O Genkit resolve esse desafio fornecendo uma única interface que abstrai os detalhes de acesso a qualquer serviço de modelo de IA generativa, com várias implementações predefinidas já disponíveis. Criar seu app com tecnologia de IA com o Genkit simplifica o processo de fazer a primeira chamada de IA generativa e facilita a combinação de vários modelos ou a troca de um modelo por outro à medida que novos modelos surgem.

Antes de começar

Se você quiser executar os exemplos de código nesta página, primeiro conclua as etapas do guia Primeiros passos. Todos os exemplos assumem que você já instalou o Genkit como uma dependência no seu projeto.

Modelos com suporte do Genkit

O Genkit foi projetado para ser flexível o suficiente para usar qualquer serviço de modelo de IA generativa. As bibliotecas principais definem a interface comum para trabalhar com modelos, e os plug-ins de modelo definem os detalhes de implementação para trabalhar com um modelo específico e a API dele.

A equipe do Genkit mantém plug-ins para trabalhar com modelos fornecidos pela Vertex AI, IA generativa do Google e Ollama:

  • Família de LLMs do Gemini, pelo plug-in da Vertex AI do Google Cloud
  • Família de LLMs Gemini, pelo plug-in da IA do Google
  • Modelos de geração de imagens Imagen2 e Imagen3, usando a Vertex AI do Google Cloud
  • Família de LLMs Claude 3 da Anthropic, pelo modelo de jardim da Vertex AI do Google Cloud
  • Gemma 2, Llama 3 e muitos outros modelos abertos, pelo plug-in Ollama (você precisa hospedar o servidor do Ollama)

Além disso, há vários plug-ins com suporte da comunidade que fornecem interfaces para esses modelos:

Para saber mais, pesquise pacotes com a tag genkit-model em npmjs.org.

Como carregar e configurar plug-ins de modelo

Antes de usar o Genkit para começar a gerar conteúdo, é necessário carregar e configurar um plug-in de modelo. Se você veio do guia "Primeiros passos", você já fez isso. Caso contrário, consulte o guia Primeiros passos ou a documentação do plug-in individual e siga as etapas antes de continuar.

O método generate()

No Genkit, a interface principal em que você interage com modelos de IA generativa é o método generate().

A chamada generate() mais simples especifica o modelo que você quer usar e um comando de texto:

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

const ai = genkit({
  plugins: [googleAI()],
  model: gemini15Flash,
});

(async () => {
  const { text } = await ai.generate(
    'Invent a menu item for a pirate themed restaurant.'
  );
  console.log(text);
})();

Quando você executa esse exemplo breve, ele mostra algumas informações de depuração seguidas pela saída da chamada generate(), que geralmente é um texto Markdown, como no exemplo a seguir:

## The Blackheart's Bounty

**A hearty stew of slow-cooked beef, spiced with rum and molasses, served in a
hollowed-out cannonball with a side of crusty bread and a dollop of tangy
pineapple salsa.**

**Description:** This dish is a tribute to the hearty meals enjoyed by pirates
on the high seas. The beef is tender and flavorful, infused with the warm spices
of rum and molasses. The pineapple salsa adds a touch of sweetness and acidity,
balancing the richness of the stew. The cannonball serving vessel adds a fun and
thematic touch, making this dish a perfect choice for any pirate-themed
adventure.

Execute o script novamente e você vai receber uma saída diferente.

O exemplo de código anterior enviou a solicitação de geração para o modelo padrão, especificado ao configurar a instância do Genkit.

Também é possível especificar um modelo para uma única chamada generate():

const { text } = await ai.generate({
  model: gemini15Pro,
  prompt: 'Invent a menu item for a pirate themed restaurant.',
});

Este exemplo usa uma referência de modelo exportada pelo plug-in. Outra opção é especificar o modelo usando um identificador de string:

const { text } = await ai.generate({
  model: 'googleai/gemini-1.5-pro-latest',
  prompt: 'Invent a menu item for a pirate themed restaurant.',
});

Um identificador de string de modelo tem a aparência de providerid/modelid, em que o ID do provedor (neste caso, googleai) identifica o plug-in, e o ID do modelo é um identificador de string específico do plug-in para uma versão específica de um modelo.

Alguns plug-ins de modelo, como o Ollama, fornecem acesso a dezenas de modelos diferentes e, portanto, não exportam referências de modelo individuais. Nesses casos, só é possível especificar um modelo para generate() usando o identificador de string dele.

Esses exemplos também ilustram um ponto importante: quando você usa generate() para fazer chamadas de modelos de IA generativa, mudar o modelo que você quer usar é simplesmente uma questão de transmitir um valor diferente para o parâmetro do modelo. Ao usar generate() em vez dos SDKs de modelo nativo, você tem a flexibilidade de usar vários modelos diferentes no app e mudar de modelo no futuro.

Até agora, você só viu exemplos das chamadas generate() mais simples. No entanto, generate() também fornece uma interface para interações mais avançadas com modelos generativos, que você vai conhecer nas seções a seguir.

Comandos do sistema

Alguns modelos oferecem suporte a um comando do sistema, que fornece ao modelo instruções sobre como você quer que ele responda às mensagens do usuário. É possível usar o comando do sistema para especificar um perfil que você quer que o modelo adote, o tom das respostas, o formato das respostas e assim por diante.

Se o modelo que você está usando for compatível com solicitações do sistema, forneça uma com o parâmetro system:

const { text } = await ai.generate({
  system: 'You are a food industry marketing consultant.',
  prompt: 'Invent a menu item for a pirate themed restaurant.',
});

Parâmetros do modelo

A função generate() usa um parâmetro config, em que você pode especificar configurações opcionais que controlam como o modelo gera conteúdo:

const { text } = await ai.generate({
  prompt: 'Invent a menu item for a pirate themed restaurant.',
  config: {
    maxOutputTokens: 400,
    stopSequences: ['<end>', '<fin>'],
    temperature: 1.2,
    topP: 0.4,
    topK: 50,
  },
});

Os parâmetros exatos que são aceitos dependem do modelo e da API de modelo. No entanto, os parâmetros no exemplo anterior são comuns a quase todos os modelos. Confira a seguir uma explicação desses parâmetros:

Parâmetros que controlam o comprimento da saída

maxOutputTokens

Os LLMs operam em unidades chamadas tokens. Um token geralmente, mas não necessariamente, é associado a uma sequência específica de caracteres. Quando você transmite uma solicitação a um modelo, uma das primeiras etapas é tokenizar a string de solicitação em uma sequência de tokens. Em seguida, o LLM gera uma sequência de tokens da entrada tokenizada. Por fim, a sequência de tokens é convertida de volta em texto, que é a saída.

O parâmetro de tokens de saída máximos simplesmente define um limite de quantos tokens gerar usando o LLM. Cada modelo pode usar um tokenizador diferente, mas uma boa regra geral é considerar que uma única palavra em inglês é composta por dois a quatro tokens.

Como mencionado anteriormente, alguns tokens podem não ser mapeados para sequências de caracteres. Um exemplo é que geralmente há um token que indica o fim da sequência: quando um LLM gera esse token, ele para de gerar mais. Portanto, é possível e, muitas vezes, um LLM gera menos tokens do que o máximo porque gerou o token "stop".

stopSequences

É possível usar esse parâmetro para definir os tokens ou as sequências de tokens que, quando gerados, indicam o fim da saída do LLM. Os valores corretos a serem usados aqui geralmente dependem de como o modelo foi treinado e geralmente são definidos pelo plug-in do modelo. No entanto, se você tiver solicitado que o modelo gere outra sequência de parada, poderá especificar aqui.

Você está especificando sequências de caracteres, não tokens. Na maioria dos casos, você vai especificar uma sequência de caracteres que o tokenizer do modelo mapeia para um único token.

Parâmetros que controlam a "criatividade"

Os parâmetros temperatura, top-p e top-k controlam juntos o nível de "criatividade" que você quer que o modelo tenha. Confira abaixo explicações breves sobre o significado desses parâmetros, mas o ponto mais importante é este: esses parâmetros são usados para ajustar o caractere da saída de um LLM. Os valores ideais para eles dependem das suas metas e preferências e provavelmente serão encontrados apenas por meio de experimentos.

temperatura

Os LLMs são máquinas de previsão de tokens. Para uma determinada sequência de tokens (como o comando), um LLM prevê, para cada token no vocabulário, a probabilidade de que o token venha a seguir na sequência. A temperatura é um fator de dimensionamento pelo qual essas previsões são divididas antes de serem normalizadas para uma probabilidade entre 0 e 1.

Valores de temperatura baixos, entre 0,0 e 1,0, ampliam a diferença nas probabilidades entre os tokens, o que faz com que o modelo tenha ainda menos probabilidade de produzir um token que já foi avaliado como improvável. Isso geralmente é percebido como uma saída menos criativa. Embora 0, 0 não seja tecnicamente um valor válido, muitos modelos o tratam como um indicador de que o modelo precisa se comportar de forma determinística e considerar apenas o token mais provável.

Valores de temperatura alta (maiores que 1,0) comprimem as diferenças de probabilidade entre os tokens, com o resultado de que o modelo tem mais probabilidade de produzir tokens que ele havia avaliado anteriormente como improváveis. Isso é muitas vezes percebido como uma saída mais criativa. Algumas APIs de modelo impõem uma temperatura máxima, geralmente 2,0.

topP

Top-p é um valor entre 0,0 e 1,0 que controla o número de tokens possíveis que você quer que o modelo considere, especificando a probabilidade cumulativa dos tokens. Por exemplo, um valor de 1,0 significa considerar todos os tokens possíveis, mas ainda levar em conta a probabilidade de cada token. Um valor de 0,4 significa considerar apenas os tokens mais prováveis, cujas probabilidades somam 0,4, e excluir os tokens restantes da consideração.

topK

Top-k é um valor inteiro que também controla o número de tokens possíveis que você quer que o modelo considere, mas dessa vez especificando explicitamente o número máximo de tokens. Especificar um valor de 1 significa que o modelo deve se comportar de forma determinística.

Testar parâmetros do modelo

É possível testar o efeito desses parâmetros na saída gerada por diferentes combinações de modelo e comando usando a IU do desenvolvedor. Inicie a interface para desenvolvedores com o comando genkit start, que vai carregar automaticamente todos os modelos definidos pelos plug-ins configurados no projeto. É possível testar rapidamente diferentes comandos e valores de configuração sem precisar fazer essas mudanças repetidamente no código.

Saída estruturada

Ao usar a IA generativa como um componente no seu app, geralmente você quer uma saída em um formato diferente do texto simples. Mesmo que você esteja apenas gerando conteúdo para mostrar ao usuário, é possível aproveitar a saída estruturada simplesmente para apresentá-lo de forma mais atraente ao usuário. No entanto, para aplicações mais avançadas de IA generativa, como o uso programático da saída do modelo ou a alimentação da saída de um modelo em outro, a saída estruturada é essencial.

No Genkit, é possível solicitar uma saída estruturada de um modelo especificando um esquema ao chamar generate():

import { z } from 'genkit'; // Import Zod, which is re-exported by Genkit.
const MenuItemSchema = z.object({
  name: z.string(),
  description: z.string(),
  calories: z.number(),
  allergens: z.array(z.string()),
});

const { output } = await ai.generate({
  prompt: 'Invent a menu item for a pirate themed restaurant.',
  output: { schema: MenuItemSchema },
});

Os esquemas de saída do modelo são especificados usando a biblioteca Zod. Além de uma linguagem de definição de esquema, o Zod também oferece verificação de tipo no tempo de execução, que preenche a lacuna entre tipos estáticos do TypeScript e a saída imprevisível de modelos de IA generativa. O Zod permite que você escreva código que possa confiar no fato de que uma chamada de geração bem-sucedida sempre retornará uma saída que se conforma aos seus tipos de TypeScript.

Quando você especifica um esquema em generate(), o Genkit faz várias coisas nos bastidores:

  • Aumenta o comando com mais orientações sobre o formato de saída desejado. Isso também tem o efeito colateral de especificar ao modelo exatamente qual conteúdo você quer gerar. Por exemplo, não apenas sugerir um item de menu, mas também gerar uma descrição, uma lista de alérgenos e assim por diante.
  • Analisa a saída do modelo em um objeto JavaScript.
  • Verifica se a saída está em conformidade com o esquema.

Para receber uma saída estruturada de uma chamada de geração bem-sucedida, use a propriedade output do objeto de resposta:

if (output) {
  const { name, description, calories, allergens } = output;
}

Tratamento de erros

No exemplo anterior, a propriedade output pode ser null. Isso pode acontecer quando o modelo não consegue gerar uma saída que esteja em conformidade com o esquema. A melhor estratégia para lidar com esses erros vai depender do seu caso de uso exato, mas aqui estão algumas dicas gerais:

  • Tente um modelo diferente. Para que a saída estruturada funcione, o modelo precisa ser capaz de gerar saída em JSON. Os LLMs mais poderosos, como Gemini e Claude, são versáteis o suficiente para fazer isso. No entanto, modelos menores, como alguns dos modelos locais que você usaria com o Ollama, podem não conseguir gerar uma saída estruturada de forma confiável, a menos que tenham sido treinados especificamente para fazer isso.

  • Usar as capacidades de coerção do Zod: é possível especificar nos esquemas que o Zod precisa tentar forçar tipos não conformes ao tipo especificado pelo esquema. Se o esquema incluir tipos primitivos diferentes de strings, o uso da coerção do Zod pode reduzir o número de falhas de generate(). A versão a seguir de MenuItemSchema usa a coerção de tipo para corrigir automaticamente situações em que o modelo gera informações de calorias como uma string em vez de um número:

    const MenuItemSchema = z.object({
      name: z.string(),
      description: z.string(),
      calories: z.coerce.number(),
      allergens: z.array(z.string()),
    });
    
  • Tente fazer a chamada generate() novamente. Se o modelo escolhido falhar apenas raramente na geração de saídas conformes, trate o erro como um erro de rede e simplesmente repita a solicitação usando algum tipo de estratégia de espera incremental.

Streaming

Ao gerar grandes quantidades de texto, é possível melhorar a experiência dos usuários apresentando a saída conforme ela é gerada, ou seja, fazendo streaming dela. Um exemplo conhecido de streaming em ação pode ser visto na maioria dos apps de chat com LLM: os usuários podem ler a resposta do modelo à mensagem conforme ela é gerada, o que melhora a capacidade de resposta percebida do aplicativo e aumenta a ilusão de conversar com uma contraparte inteligente.

No Genkit, é possível fazer streaming da saída usando o método generateStream(). A sintaxe é semelhante ao método generate():

const { response, stream } = await ai.generateStream(
  'Suggest a complete menu for a pirate themed restaurant.'
);

O objeto de resposta tem uma propriedade stream, que pode ser usada para iterar a saída de streaming da solicitação conforme ela é gerada:

for await (const chunk of stream) {
  console.log(chunk.text);
}

Você também pode receber a saída completa da solicitação, como em uma solicitação não de streaming:

const completeText = (await response).text;

O streaming também funciona com saída estruturada:

const MenuSchema = z.object({
  starters: z.array(MenuItemSchema),
  mains: z.array(MenuItemSchema),
  desserts: z.array(MenuItemSchema),
});

const { response, stream } = await ai.generateStream({
  prompt: 'Suggest a complete menu for a pirate themed restaurant.',
  output: { schema: MenuSchema },
});

for await (const chunk of stream) {
  // `output` is an object representing the entire output so far.
  console.log(chunk.output);
}

// Get the completed output.
const { output } = await response;

A saída estruturada em streaming funciona de maneira um pouco diferente do streaming de texto: a propriedade output de um bloco de resposta é um objeto construído a partir da acumulação dos blocos que foram produzidos até o momento, em vez de um objeto que representa um único bloco (que pode não ser válido por si só). Cada bloco de saída estruturada, de certa forma, substitui o bloco anterior.

Por exemplo, confira como as cinco primeiras saídas do exemplo anterior podem parecer:

null

{ starters: [ {} ] }

{
  starters: [ { name: "Captain's Treasure Chest", description: 'A' } ]
}

{
  starters: [
    {
      name: "Captain's Treasure Chest",
      description: 'A mix of spiced nuts, olives, and marinated cheese served in a treasure chest.',
      calories: 350
    }
  ]
}

{
  starters: [
    {
      name: "Captain's Treasure Chest",
      description: 'A mix of spiced nuts, olives, and marinated cheese served in a treasure chest.',
      calories: 350,
      allergens: [Array]
    },
    { name: 'Shipwreck Salad', description: 'Fresh' }
  ]
}

Entrada multimodal

Os exemplos que você viu até agora usaram strings de texto como comandos de modelo. Embora essa seja a maneira mais comum de solicitar modelos de IA generativa, muitos modelos também aceitam outras mídias como comandos. Os comandos de mídia são usados com mais frequência em conjunto com comandos de texto que instruem o modelo a realizar alguma operação na mídia, como adicionar legenda a uma imagem ou transcrever uma gravação de áudio.

A capacidade de aceitar entrada de mídia e os tipos de mídia que você pode usar dependem completamente do modelo e da API dele. Por exemplo, a série de modelos Gemini 1.5 pode aceitar imagens, vídeos e áudios como comandos.

Para fornecer uma instrução de mídia a um modelo compatível, em vez de transmitir uma instrução de texto simples para generate, transmita uma matriz que consiste em uma parte de mídia e uma de texto:

const { text } = await ai.generate([
  { media: { url: 'https://example.com/photo.jpg' } },
  { text: 'Compose a poem about this image.' },
]);

No exemplo acima, você especificou uma imagem usando um URL HTTPS acessível publicamente. Também é possível transmitir dados de mídia diretamente codificando-os como um URL de dados. Exemplo:

import { readFile } from 'node:fs/promises';
const b64Data = await readFile('photo.jpg', { encoding: 'base64url' });
const dataUrl = `data:image/jpeg;base64,${b64Data}`;

const { text } = await ai.generate([
  { media: { url: dataUrl } },
  { text: 'Compose a poem about this image.' },
]);

Todos os modelos que oferecem suporte à entrada de mídia são compatíveis com URLs de dados e HTTPS. Alguns plug-ins de modelo oferecem suporte a outras fontes de mídia. Por exemplo, o plug-in da Vertex AI também permite usar URLs do Cloud Storage (gs://).

Gerar mídia

Até agora, a maioria dos exemplos nesta página lidou com a geração de texto usando LLMs. No entanto, o Genkit também pode ser usado com modelos de geração de imagens. O uso de generate() com um modelo de geração de imagens é semelhante ao uso de um LLM. Por exemplo, para gerar uma imagem usando o modelo Imagen2 na Vertex AI:

  1. O Genkit usa URLs data: como o formato de saída padrão para mídia gerada. Esse é um formato padrão com muitas bibliotecas disponíveis para processá-los. Este exemplo usa o pacote data-urls de jsdom:

    npm i --save data-urls
    npm i --save-dev @types/data-urls
  2. Para gerar uma imagem e salvá-la em um arquivo, chame generate(), especificando um modelo de geração de imagem e o tipo de mídia do formato de saída:

    import { imagen3Fast, vertexAI } from '@genkit-ai/vertexai';
    import parseDataURL from 'data-urls';
    import { genkit } from 'genkit';
    
    import { writeFile } from 'node:fs/promises';
    
    const ai = genkit({
      plugins: [vertexAI({ location: 'us-central1' })],
    });
    
    (async () => {
      const { media } = await ai.generate({
        model: imagen3Fast,
        prompt: 'photo of a meal fit for a pirate',
        output: { format: 'media' },
      });
    
      if (media === null) throw new Error('No media generated.');
    
      const data = parseDataURL(media.url);
      if (data === null) throw new Error('Invalid "data:" URL.');
    
      await writeFile(`output.${data.mimeType.subtype}`, data.body);
    })();
    

Próximas etapas

Saiba mais sobre o Genkit

  • Como desenvolvedor de apps, a principal maneira de influenciar a saída de modelos de IA generativa é com comandos. Leia Gerenciamento de comandos para saber como o Genkit ajuda a desenvolver comandos eficazes e gerenciá-los na sua base de código.
  • Embora generate() seja o núcleo de todos os aplicativos com tecnologia de IA generativa, aplicativos reais geralmente exigem mais trabalho antes e depois de invocar um modelo de IA generativa. Para refletir isso, o Genkit introduz o conceito de fluxos, que são definidos como funções, mas adicionam outros recursos, como observabilidade e implantação simplificada. Para saber mais, consulte Como definir fluxos de trabalho.

Uso avançado do LLM

  • Uma maneira de aprimorar os recursos dos LLMs é mostrar uma lista de maneiras de solicitar mais informações ou pedir que você realize alguma ação. Isso é conhecido como chamada de ferramenta ou chamada de função. Os modelos treinados para oferecer suporte a esse recurso podem responder a um comando com uma resposta formatada especialmente, que indica ao aplicativo de chamada que ele precisa realizar alguma ação e enviar o resultado de volta ao LLM com o comando original. O Genkit tem funções de biblioteca que automatizam a geração de comandos e os elementos de repetição de chamada e resposta de uma implementação de chamada de ferramenta. Consulte Chamada de ferramentas para saber mais.
  • A geração aumentada de recuperação (RAG, na sigla em inglês) é uma técnica usada para introduzir informações específicas do domínio na saída de um modelo. Isso é feito inserindo informações relevantes em um comando antes de transmiti-las ao modelo de linguagem. Uma implementação completa da RAG exige que você reúna várias tecnologias: modelos de geração de embedding de texto, bancos de dados vetoriais e modelos de linguagem grandes. Consulte Geração aumentada de recuperação (RAG) para aprender como o Genkit simplifica o processo de coordenação desses vários elementos.

Como testar a saída do modelo

Como engenheiro de software, você está acostumado com sistemas determinísticos em que a mesma entrada sempre produz a mesma saída. No entanto, como os modelos de IA são probabilistas, a saída pode variar com base em nuances sutis na entrada, nos dados de treinamento do modelo e até mesmo na aleatoriedade introduzida deliberadamente por parâmetros como temperatura.

Os avaliadores do Genkit são maneiras estruturadas de avaliar a qualidade das respostas do seu LLM usando várias estratégias. Leia mais na página Avaliação.