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 quase nunca é um processo trivial e, muitas vezes, envolve iteração e experimentação substanciais. O Genkit fornece 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 os comandos com os modelos e parâmetros de modelo para os quais eles são destinados separadamente do código do aplicativo. Em seguida, você (ou talvez alguém que nem mesmo esteja envolvido na criação do código do aplicativo) pode iterar rapidamente nas instruções e nos parâmetros do modelo usando a interface do desenvolvedor do Genkit. Quando os comandos estiverem funcionando como você quer, eles poderão ser importados para o aplicativo e executados usando o Genkit.

As definições de comando vão para 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 da 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 no guia Primeiros passos. Todos os exemplos partem do princípio 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 as instruções em um diretório na raiz do projeto e carrega automaticamente as instruções encontradas. Por padrão, esse diretório é chamado de prompts. Por exemplo, usando o nome de diretório padrão, a estrutura do projeto pode ficar assim:

your-project/
├── prompts/
│   └── hello.prompt
├── main.go
├── go.mod
└── go.sum

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

g, err := genkit.Init(ctx.Background(), ai.WithPromptDir("./llm_prompts"))

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 entre os 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, como alternativa, 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 (
    "context"

    "github.com/firebase/genkit/go/ai"
    "github.com/firebase/genkit/go/genkit"
    "github.com/firebase/genkit/go/plugins/googlegenai"
)

func main() {
    g, err := genkit.Init(context.Background(), ai.WithPlugins(&googlegenai.GoogleAI{}))
    if err != nil {
        log.Fatal(err)
    }

    // Blocks end of program execution to use the developer UI.
    select {}
}

Carregar a interface para desenvolvedores no mesmo projeto:

genkit start -- go run .

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

Executor de modelo da IU 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 comandos

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 seus 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 (
    "context"

      "github.com/firebase/genkit/go/ai"
    "github.com/firebase/genkit/go/genkit"
    "github.com/firebase/genkit/go/plugins/googlegenai"
)

func main() {
    g, err := genkit.Init(context.Background(), ai.WithPlugins(&googlegenai.GoogleAI{}))
    if err != nil {
        log.Fatal(err)
    }

    // Blocks end of program execution to use the developer UI.
    select {}
}

Se você estiver armazenando as instruções em um diretório diferente do padrão, especifique-o ao configurar o Genkit.

Executar comandos do código

Para usar um comando, primeiro carregue-o usando a função genkit.LookupPrompt():

helloPrompt := genkit.LookupPrompt(g, "hello")

Uma solicitação executável tem opções semelhantes às de genkit.Generate(), e muitas delas podem ser substituídas no momento da execução, incluindo elementos como a entrada (consulte a seção sobre como especificar esquemas de entrada), configuração e muito mais:

resp, err := helloPrompt.Execute(context.Background(),
    ai.WithModelName("googleai/gemini-2.0-flash"),
    ai.WithInput(map[string]any{"name": "John"}),
    ai.WithConfig(&googlegenai.GeminiConfig{Temperature: 0.5})
)

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 conferir as descrições das opções disponíveis.

Como usar a interface para desenvolvedores

Ao refinar as solicitações do app, é possível 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 -- go run .

Executor de comando da IU 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 escrita do comando ou nos parâmetros de configuração afetam a saída do modelo. Quando estiver contente com o resultado, clique no botão Exportar comando para salvar a solicitação modificada no diretório do projeto.

Configuração do modelo

No bloco de prefácio dos arquivos de comando, especifique os valores de configuração de modelo para o comando:

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

Esses valores são mapeados diretamente para a opção WithConfig() aceita pela linha de comando executável:

resp, err := helloPrompt.Execute(context.Background(),
    ai.WithConfig(&googlegenai.GeminiConfig{
        Temperature:     1.4,
        TopK:            50,
        TopP:            0.4,
        MaxOutputTokens: 400,
        StopSequences:   []string{"<end>", "<fin>"},
    }))

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

Esquemas de entrada e saída

É possível especificar esquemas de entrada e saída para os comandos definindo-os na seção de informações gerais. Esses esquemas são usados da mesma forma que os transmitidos para uma solicitação genkit.Generate() ou uma definição de fluxo:

---
model: googleai/gemini-2.0-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.

Esse código produz a seguinte saída estruturada:

menuPrompt = genkit.LookupPrompt(g, "menu")
if menuPrompt == nil {
    log.Fatal("no prompt named 'menu' found")
}

resp, err := menuPrompt.Execute(context.Background(),
    ai.WithInput(map[string]any{"theme": "medieval"}),
)
if err != nil {
    log.Fatal(err)
}

var output map[string]any
if err := resp.Output(&output); err != nil {
    log.Fatal(err)
}

log.Println(output["dishname"])
log.Println(output["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, o 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 simplifica 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 ao seguinte tipo Go:

type Article struct {
    Title    string   `json:"title"`
    Subtitle string   `json:"subtitle,omitempty" jsonschema:"required=false"`
    Draft    bool     `json:"draft,omitempty"`  // True when in draft state
    Status   string   `json:"status,omitempty" jsonschema:"enum=PENDING,enum=APPROVED"` // Approval status
    Date     string   `json:"date"`   // The date of publication e.g. '2025-04-07'
    Tags     []string `json:"tags"`   // Relevant tags for article
    Authors  []struct {
      Name  string `json:"name"`
      Email string `json:"email,omitempty"`
    } `json:"authors"`
    Metadata struct {
      UpdatedAt  string `json:"updatedAt,omitempty"`  // ISO timestamp of last update
      ApprovedBy int    `json:"approvedBy,omitempty"` // ID of approver
    } `json:"metadata,omitempty"`
    Extra any `json:"extra"` // Arbitrary extra data
}

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 obrigató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 retornem 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

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-2.0-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.

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:

menuPrompt = genkit.LookupPrompt(g, "menu")

resp, err := menuPrompt.Execute(context.Background(),
    ai.WithInput(map[string]any{"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 algumas construções lógicas limitadas. Por exemplo, como alternativa para fornecer um padrão, é possível definir a solicitação usando o auxiliar #if do Handlebars:

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

Neste exemplo, o comando é renderizado como "Inventar um menu para um restaurante" 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, como um comando do sistema, são melhor expressos como combinações de várias mensagens.

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

---
model: vertexai/gemini-2.0-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-2.0-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:

multimodalPrompt = genkit.LookupPrompt(g, "multimodal")

resp, err := multimodalPrompt.Execute(context.Background(),
    ai.WithInput(map[string]any{"photoUrl": "https://example.com/photo.jpg"}),
)

Consulte também Entrada multimodal na página Como gerar conteúdo com modelos de IA 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 o seguinte:

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

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

---
model: googleai/gemini-2.0-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 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 do comando pai.

As partes aceitam argumentos nomeados 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-2.0-flash
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 genkit.DefinePartial():

genkit.DefinePartial(g, "personality", "Talk like a {{#if style}}{{style}}{{else}}helpful assistant{{/if}}.")

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

Como definir auxiliares personalizados

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

genkit.DefineHelper(g, "shout", func(input string) string {
    return strings.ToUpper(input)
})

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

---
model: googleai/gemini-2.0-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 simplifica 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 por meio do recurso de variantes.

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

  • myPrompt.prompt: o comando de "valor de referência"
  • myPrompt.gemini25pro.prompt: uma variante chamada gemini25pro

Para usar uma variante de comando, especifique-a ao carregar:

myPrompt := genkit.LookupPrompt(g, "myPrompt.gemini25Pro")

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 partiram do princípio de que as solicitações são definidas em arquivos .prompt individuais em um único diretório (ou subdiretórios dele), acessíveis em seu app no momento da execução. O Dotprompt foi 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 se enquadram nessa configuração, também é possível definir comandos no código usando a função genkit.DefinePrompt():

type GeoQuery struct {
    CountryCount int `json:"countryCount"`
}

type CountryList struct {
    Countries []string `json:"countries"`
}

geographyPrompt, err := genkit.DefinePrompt(
    g, "GeographyPrompt",
    ai.WithSystem("You are a geography teacher. Respond only when the user asks about geography."),
    ai.WithPrompt("Give me the {{countryCount}} biggest countries in the world by inhabitants."),
    ai.WithConfig(&googlegenai.GeminiConfig{Temperature: 0.5}),
    ai.WithInputType(GeoQuery{CountryCount: 10}) // Defaults to 10.
    ai.WithOutputType(CountryList{}),
)
if err != nil {
    log.Fatal(err)
}

resp, err := geographyPrompt.Execute(context.Background(), ai.WithInput(GeoQuery{CountryCount: 15}))
if err != nil {
    log.Fatal(err)
}

var list CountryList
if err := resp.Output(&list); err != nil {
    log.Fatal(err)
}

log.Println("Countries: %s", list.Countries)

Também é possível renderizar comandos em um GenerateActionOptions, que pode ser processado e transmitido para genkit.GenerateWithRequest():

actionOpts, err := geographyPrompt.Render(ctx, ai.WithInput(GeoQuery{CountryCount: 15}))
if err != nil {
    log.Fatal(err)
}

// Do something with the value...
actionOpts.Config = &googlegenai.GeminiConfig{Temperature: 0.8}

resp, err := genkit.GenerateWithRequest(ctx, g, actionOpts, nil, nil) // No middleware or streaming

Todas as opções de comando são transferidas para GenerateActionOptions, exceto por WithMiddleware(), que precisa ser transmitido separadamente se Prompt.Render() for usado em vez de Prompt.Execute().