Como gerenciar comandos com o Dotprompt

A engenharia de comandos é a principal maneira pela qual você, como desenvolvedor de apps, influencia a saída de modelos de IA generativa. Por exemplo, ao usar LLMs, você pode criar comandos que influenciam o tom, o formato, a duração e outras características das respostas dos modelos.

A forma como você escreve essas instruções depende do modelo que está usando. Uma instrução escrita para um modelo pode não funcionar bem quando usada com outro modelo. Da mesma forma, os parâmetros de modelo definidos (temperatura, top-k etc.) também afetam a saída de maneira diferente, dependendo do modelo.

Fazer com que esses três fatores (o modelo, os parâmetros do modelo e o comando) funcionem juntos para produzir a saída desejada raramente é um processo trivial e, muitas vezes, envolve iteração e experimentação substanciais. O Genkit oferece uma biblioteca e um formato de arquivo chamado Dotprompt, que tem como objetivo tornar essa iteração mais rápida e conveniente.

O Dotprompt foi desenvolvido com base na premissa de que comandos são código. Você define as instruções e os modelos e parâmetros de modelo para os quais elas são destinadas separadamente do código do aplicativo. Em seguida, você (ou talvez alguém que nem esteja envolvido na criação do código do aplicativo) pode iterar rapidamente os comandos e os parâmetros do modelo usando a interface do desenvolvedor do Genkit. Quando as instruções estiverem funcionando da maneira que você quer, elas poderão ser importadas para o aplicativo e executadas usando o Genkit.

As definições de comando são colocadas em um arquivo com a extensão .prompt. Confira um exemplo de como esses arquivos ficam:

---
model: googleai/gemini-1.5-flash
config:
  temperature: 0.9
input:
  schema:
    location: string
    style?: string
    name?: string
  default:
    location: a restaurant
---

You are the world's most welcoming AI assistant and are currently working at {{location}}.

Greet a guest{{#if name}} named {{name}}{{/if}}{{#if style}} in the style of {{style}}{{/if}}.

A parte com três traços é o front-end YAML, semelhante ao formato de front-end usado pelo GitHub Markdown e pelo Jekyll. O restante do arquivo é o comando, que pode usar modelos Handlebars. As seções a seguir vão detalhar cada uma das partes que compõem um arquivo .prompt e como usá-las.

Antes de começar

Antes de ler esta página, você precisa conhecer o conteúdo abordado na página Como gerar conteúdo com modelos de IA.

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.

Como criar arquivos de comando

Embora o Dotprompt ofereça várias maneiras diferentes de criar e carregar comandos, ele é otimizado para projetos que organizam os comandos como arquivos .prompt em um único diretório (ou subdiretórios). Esta seção mostra como criar e carregar comandos usando essa configuração recomendada.

Como criar um diretório de comando

A biblioteca Dotprompt espera encontrar suas instruções em um diretório na raiz do projeto e carrega automaticamente as instruções encontradas. Por padrão, esse diretório é chamado prompts. Por exemplo, usando o nome de diretório padrão, a estrutura do projeto pode ficar assim:

your-project/
├── lib/
├── node_modules/
├── prompts/
│   └── hello.prompt
├── src/
├── package-lock.json
├── package.json
└── tsconfig.json

Se você quiser usar um diretório diferente, especifique-o ao configurar o Genkit:

const ai = genkit({
  promptDir: './llm_prompts',
  // (Other settings...)
});

Como criar um arquivo de comando

Há duas maneiras de criar um arquivo .prompt: usando um editor de texto ou com a interface para desenvolvedores.

Como usar um editor de texto

Se você quiser criar um arquivo de comando usando um editor de texto, crie um arquivo de texto com a extensão .prompt no diretório de comandos: por exemplo, prompts/hello.prompt.

Confira um exemplo mínimo de um arquivo de comando:

---
model: vertexai/gemini-1.5-flash
---
You are the world's most welcoming AI assistant. Greet the user and offer your assistance.

A parte com traços é o front-end YAML, semelhante ao formato de front-end usado pelo GitHub Markdown e pelo Jekyll. O restante do arquivo é o comando, que pode usar opcionalmente modelos do Handlebars. A seção de textos introdutórios é opcional, mas a maioria dos arquivos de comando contém pelo menos metadados que especificam um modelo. O restante desta página mostra como ir além e usar os recursos do Dotprompt nos arquivos de comando.

Como usar a interface para desenvolvedores

Também é possível criar um arquivo de comando usando o executor de modelo na interface para desenvolvedores. Comece com o código do aplicativo que importa a biblioteca do Genkit e a configura para usar o plug-in de modelo de seu interesse. Exemplo:

import { genkit } from 'genkit';

// Import the model plugins you want to use.
import { googleAI } from '@genkit-ai/googleai';

const ai = genkit({
  // Initialize and configure the model plugins.
  plugins: [
    googleAI({
      apiKey: 'your-api-key', // Or (preferred): export GOOGLE_GENAI_API_KEY=...
    }),
  ],
});

Não há problema se o arquivo contiver outro código, mas o código acima é tudo o que é necessário.

Carregar a interface para desenvolvedores no mesmo projeto:

genkit start -- tsx --watch src/your-code.ts

Na seção "Modelos", escolha o modelo que você quer usar na lista de modelos fornecidos pelo plug-in.

Executor de modelo da interface do desenvolvedor do Genkit

Em seguida, teste o comando e a configuração até conseguir resultados satisfatórios. Quando estiver tudo pronto, pressione o botão "Exportar" e salve o arquivo no diretório de comandos.

Como executar avisos

Depois de criar arquivos de comando, é possível executá-los no código do aplicativo ou usar as ferramentas fornecidas pelo Genkit. Independentemente de como você quer executar os comandos, comece com o código do aplicativo que importa a biblioteca do Genkit e os plug-ins de modelo em que você tem interesse. Exemplo:

import { genkit } from 'genkit';

// Import the model plugins you want to use.
import { googleAI } from '@genkit-ai/googleai';

const ai = genkit({
  // Initialize and configure the model plugins.
  plugins: [
    googleAI({
      apiKey: 'your-api-key', // Or (preferred): export GOOGLE_GENAI_API_KEY=...
    }),
  ],
});

Não há problema se o arquivo contiver outro código, mas o código acima é tudo o que é necessário. Se você estiver armazenando as instruções em um diretório diferente do padrão, especifique-o ao configurar o Genkit.

Executar comandos de código

Para usar um comando, primeiro carregue-o usando o método prompt('file_name'):

const helloPrompt = ai.prompt('hello');

Depois de carregado, você pode chamar o comando como uma função:

const response = await helloPrompt();

// Alternatively, use destructuring assignments to get only the properties
// you're interested in:
const { text } = await helloPrompt();

Uma solicitação acionável recebe dois parâmetros opcionais: a entrada para a solicitação (consulte a seção abaixo sobre como especificar esquemas de entrada) e um objeto de configuração, semelhante ao do método generate(). Exemplo:

const response2 = await helloPrompt(
  // Prompt input:
  { name: 'Ted' },

  // Generation options:
  {
    config: {
      temperature: 0.4,
    },
  }
);

Todos os parâmetros transmitidos para a chamada de comando vão substituir os mesmos parâmetros especificados no arquivo de comando.

Consulte Gerar conteúdo com modelos de IA para ver descrições das opções disponíveis.

Como usar a interface para desenvolvedores

Ao refinar as solicitações do app, você pode executá-las na interface do desenvolvedor do Genkit para iterar rapidamente as solicitações e as configurações do modelo, independentemente do código do aplicativo.

Carregue a interface do desenvolvedor no diretório do projeto:

genkit start -- tsx --watch src/your-code.ts

Executor de comandos da interface do desenvolvedor do Genkit

Depois de carregar comandos na interface do desenvolvedor, é possível executá-los com diferentes valores de entrada e testar como as mudanças na redação do comando ou nos parâmetros de configuração afetam a saída do modelo. Quando estiver satisfeito com o resultado, clique no botão Export prompt para salvar a solicitação modificada no diretório do projeto.

Configuração do modelo

No bloco de prefácio dos arquivos de comando, você pode especificar valores de configuração de modelo para o comando:

---
model: googleai/gemini-1.5-flash
config:
  temperature: 1.4
  topK: 50
  topP: 0.4
  maxOutputTokens: 400
  stopSequences:
    -   "<end>"
    -   "<fin>"
---

Esses valores são mapeados diretamente para o parâmetro config aceito pelo comando chamável:

const response3 = await helloPrompt(
  {},
  {
    config: {
      temperature: 1.4,
      topK: 50,
      topP: 0.4,
      maxOutputTokens: 400,
      stopSequences: ['<end>', '<fin>'],
    },
  }
);

Consulte Gerar conteúdo com modelos de IA para ver descrições das opções disponíveis.

Esquemas de entrada e saída

É possível especificar esquemas de entrada e saída para o comando definindo-os na seção de informações gerais:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    theme?: string
  default:
    theme: "pirate"
output:
  schema:
    dishname: string
    description: string
    calories: integer
    allergens(array): string
---
Invent a menu item for a {{theme}} themed restaurant.

Esses esquemas são usados da mesma forma que os transmitidos para uma solicitação generate() ou uma definição de fluxo. Por exemplo, o comando definido acima produz uma saída estruturada:

const menuPrompt = ai.prompt('menu');
const { data } = await menuPrompt({ theme: 'medieval' });

const dishName = data['dishname'];
const description = data['description'];

Você tem várias opções para definir esquemas em um arquivo .prompt: o próprio formato de definição de esquema do Dotprompt, Picoschema, esquema JSON padrão ou como referências a esquemas definidos no código do aplicativo. As seções a seguir descrevem cada uma dessas opções em mais detalhes.

Picoschema

Os esquemas no exemplo acima são definidos em um formato chamado Picoschema. O Picoschema é um formato de definição de esquema compacto otimizado para YAML que facilita a definição dos atributos mais importantes de um esquema para uso do LLM. Confira um exemplo mais longo de um esquema, que especifica as informações que um app pode armazenar sobre um artigo:

schema:
  title: string # string, number, and boolean types are defined like this
  subtitle?: string # optional fields are marked with a `?`
  draft?: boolean, true when in draft state
  status?(enum, approval status): [PENDING, APPROVED]
  date: string, the date of publication e.g. '2024-04-09' # descriptions follow a comma
  tags(array, relevant tags for article): string # arrays are denoted via parentheses
  authors(array):
    name: string
    email?: string
  metadata?(object): # objects are also denoted via parentheses
    updatedAt?: string, ISO timestamp of last update
    approvedBy?: integer, id of approver
  extra?: any, arbitrary extra data
  (*): string, wildcard field

O esquema acima é equivalente à seguinte interface do TypeScript:

interface Article {
  title: string;
  subtitle?: string | null;
  /** true when in draft state */
  draft?: boolean | null;
  /** approval status */
  status?: 'PENDING' | 'APPROVED' | null;
  /** the date of publication e.g. '2024-04-09' */
  date: string;
  /** relevant tags for article */
  tags: string[];
  authors: {
    name: string;
    email?: string | null;
  }[];
  metadata?: {
    /** ISO timestamp of last update */
    updatedAt?: string | null;
    /** id of approver */
    approvedBy?: number | null;
  } | null;
  /** arbitrary extra data */
  extra?: any;
  /** wildcard field */

}

O Picoschema oferece suporte aos tipos escalares string, integer, number, boolean e any. Objetos, matrizes e tipos enumerados são indicados por um parêntese após o nome do campo.

Os objetos definidos pelo Picoschema têm todas as propriedades necessárias, a menos que sejam indicadas como opcionais por ? e não permitam outras propriedades. Quando uma propriedade é marcada como opcional, ela também é anulável, proporcionando mais tolerância para que os LLMs retornam um valor nulo em vez da omissão de um campo.

Em uma definição de objeto, a chave especial (*) pode ser usada para declarar uma definição de campo "caractere curinga". Isso corresponderá a todas as propriedades adicionais não fornecidas por uma chave explícita.

Esquema JSON

O Picoschema não é compatível com muitos dos recursos do esquema JSON completo. Se você precisar de esquemas mais robustos, poderá fornecer um esquema JSON:

output:
  schema:
    type: object
    properties:
      field1:
        type: number
        minimum: 20

Esquemas Zod definidos no código

Além de definir esquemas diretamente no arquivo .prompt, é possível fazer referência a um esquema registrado com defineSchema() por nome. Se você estiver usando TypeScript, essa abordagem vai permitir que você aproveite os recursos estáticos de verificação de tipo da linguagem ao trabalhar com comandos.

Para registrar um esquema:

import { z } from 'genkit';

const MenuItemSchema = ai.defineSchema(
  'MenuItemSchema',
  z.object({
    dishname: z.string(),
    description: z.string(),
    calories: z.coerce.number(),
    allergens: z.array(z.string()),
  })
);

No comando, informe o nome do esquema registrado:

---
model: googleai/gemini-1.5-flash-latest
output:
  schema: MenuItemSchema
---

A biblioteca Dotprompt vai resolver automaticamente o nome para o esquema registrado do Zod. Em seguida, use o esquema para definir o tipo da saída de um comando de ponto:

const menuPrompt = ai.prompt<
  z.ZodTypeAny, // Input schema
  typeof MenuItemSchema, // Output schema
  z.ZodTypeAny // Custom options schema
>('menu');
const { data } = await menuPrompt({ theme: 'medieval' });

// Now data is strongly typed as MenuItemSchema:
const dishName = data?.dishname;
const description = data?.description;

Modelos de comandos

A parte de um arquivo .prompt que segue o material de abertura (se presente) é o próprio comando, que será transmitido ao modelo. Embora essa solicitação possa ser uma string de texto simples, muitas vezes é necessário incorporar a entrada do usuário à solicitação. Para fazer isso, especifique o comando usando a linguagem de modelagem Handlebars. Os modelos de comando podem incluir marcadores de posição que se referem aos valores definidos pelo esquema de entrada do comando.

Você já viu isso em ação na seção sobre esquemas de entrada e saída:

---
model: googleai/gemini-1.5-flash
config:
  temperature: 1.4
  topK: 50
  topP: 0.4
  maxOutputTokens: 400
  stopSequences:
    -   "<end>"
    -   "<fin>"
---

Neste exemplo, a expressão Handlebars, {{theme}}, é resolvida como o valor da propriedade theme da entrada quando você executa o comando. Para transmitir a entrada ao prompt, chame o prompt como no exemplo abaixo:

const menuPrompt = ai.prompt('menu');
const { data } = await menuPrompt({ theme: 'medieval' });

Como o esquema de entrada declarou a propriedade theme como opcional e forneceu um padrão, você poderia ter omitido a propriedade, e a solicitação teria sido resolvida usando o valor padrão.

Os modelos de Handlebars também oferecem suporte a alguns construtos lógicos limitados. Por exemplo, como alternativa para fornecer um padrão, é possível definir o comando usando o auxiliar #if do Handlebars:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    theme?: string
---
Invent a menu item for a {{#if theme}}{{theme}} themed{{/if}} restaurant.

Neste exemplo, o comando é renderizado como "Invent a menu item for a restaurant" quando a propriedade theme não é especificada.

Consulte a documentação do Handlebars para informações sobre todos os auxiliares lógicos integrados.

Além das propriedades definidas pelo esquema de entrada, os modelos também podem se referir a valores definidos automaticamente pelo Genkit. As próximas seções descrevem esses valores definidos automaticamente e como usá-los.

Comandos com várias mensagens

Por padrão, o Dotprompt constrói uma única mensagem com a função "user". No entanto, alguns comandos são mais bem expressos como uma combinação de várias mensagens, como um comando do sistema.

O auxiliar {{role}} oferece uma maneira simples de criar comandos de várias mensagens:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    userQuestion: string
---
{{role "system"}}
You are a helpful AI assistant that really loves to talk about food. Try to work
food items into all of your conversations.
{{role "user"}}
{{userQuestion}}

Comandos multimodais

Para modelos com suporte a entrada multimodal, como imagens e texto, é possível usar o auxiliar {{media}}:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    photoUrl: string
---
Describe this image in a detailed paragraph:

{{media url=photoUrl}}

O URL pode ser um URI data: codificado em base64 ou https: para uso de imagem "inline". No código, seria:

const multimodalPrompt = ai.prompt('multimodal');
const { text } = await multimodalPrompt({
  photoUrl: 'https://example.com/photo.jpg',
});

Consulte também Entrada multimodal, na página de modelos, para conferir um exemplo de como criar um URL data:.

Parciais

As parciais são modelos reutilizáveis que podem ser incluídos em qualquer comando. As parciais podem ser úteis para comandos relacionados que compartilham um comportamento comum.

Ao carregar um diretório de comando, qualquer arquivo com um sublinhado (_) como prefixo é considerado parcial. Assim, um arquivo _personality.prompt pode conter:

You should speak like a {{#if style}}{{style}}{{else}}helpful assistant.{{/else}}.

Isso pode ser incluído em outras solicitações:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    name: string
    style?: string
---
{{ role "system" }}
{{>personality style=style}}

{{ role "user" }}
Give the user a friendly greeting.

User's Name: {{name}}

As partes parciais são inseridas usando a sintaxe {{>NAME_OF_PARTIAL args...}}. Se nenhum argumento for fornecido para a parcial, ela será executada com o mesmo contexto que a solicitação pai.

As partes parciais aceitam argumentos nomeados, como acima, ou um único argumento posicional que representa o contexto. Isso pode ser útil para tarefas como renderizar membros de uma lista.

_destination.prompt

- {{name}} ({{country}})

chooseDestination.prompt

---
model: googleai/gemini-1.5-flash-latest
input:
  schema:
    destinations(array):
      name: string
      country: string
---
Help the user decide between these vacation destinations:

{{#each destinations}}
{{>destination this}}
{{/each}}

Como definir parciais no código

Também é possível definir parciais no código usando definePartial:

ai.definePartial(
  'personality',
  'Talk like a {{#if style}}{{style}}{{else}}helpful assistant{{/if}}.'
);

As partes parciais definidas por código estão disponíveis em todas as instruções.

Como definir helpers personalizados

É possível definir auxiliares personalizados para processar e gerenciar dados em um comando. Os helpers são registrados globalmente usando defineHelper:

ai.defineHelper('shout', (text: string) => text.toUpperCase());

Depois que um auxiliar é definido, ele pode ser usado em qualquer comando:

---
model: googleai/gemini-1.5-flash
input:
  schema:
    name: string
---

HELLO, {{shout name}}!!!

Variantes de comando

Como os arquivos de comando são apenas textos, você pode (e deve) enviá-los ao seu sistema de controle de versões, o que facilita a comparação das mudanças ao longo do tempo. Muitas vezes, as versões ajustadas dos comandos só podem ser totalmente testadas em um ambiente de produção lado a lado com as versões atuais. O Dotprompt oferece suporte a isso pelo recurso de variantes.

Para criar uma variante, crie um arquivo [name].[variant].prompt. Por exemplo, se você estava usando o Gemini 1.5 Flash no comando, mas quer saber se o Gemini 1.5 Pro teria um desempenho melhor, você pode criar dois arquivos:

  • my_prompt.prompt: o comando de "valor de referência"
  • my_prompt.gemini15pro.prompt: uma variante chamada gemini15pro

Para usar uma variante de comando, especifique a opção de variante ao carregar:

const myPrompt = ai.prompt('my_prompt', { variant: 'gemini15pro' });

O nome da variante é incluído nos metadados dos traces de geração. Assim, você pode comparar e contrastar o desempenho real entre variantes no inspetor de trace do Genkit.

Como definir comandos no código

Todos os exemplos discutidos até agora presumiram que as solicitações são definidas em arquivos .prompt individuais em um único diretório (ou subdiretórios dele), acessível ao app no momento da execução. O prompt de ponto é projetado em torno dessa configuração, e os autores consideram que ele é a melhor experiência de desenvolvedor em geral.

No entanto, se você tiver casos de uso que não têm suporte a essa configuração, também é possível definir comandos no código usando a função definePrompt():

O primeiro parâmetro dessa função é análogo ao bloco de introdução de um arquivo .prompt. O segundo parâmetro pode ser uma string de modelo do Handlebars, como em um arquivo de comando, ou uma função que retorna um GenerateRequest:

const myPrompt = ai.definePrompt(
  {
    name: 'myPrompt',
    model: 'googleai/gemini-1.5-flash',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  'Hello, {{name}}. How are you today?'
);
const myPrompt = ai.definePrompt(
  {
    name: 'myPrompt',
    model: 'googleai/gemini-1.5-flash',
    input: {
      schema: z.object({
        name: z.string(),
      }),
    },
  },
  async (input): Promise<GenerateRequest> => {
    return {
      messages: [
        {
          role: 'user',
          content: [{ text: `Hello, ${input.name}. How are you today?` }],
        },
      ],
    };
  }
);