Gestione dei prompt con Dotprompt

Firebase Genkit fornisce il plug-in Dotprompt e il formato di testo per aiutarti a scrivere e organizzare i tuoi prompt di IA generativa.

Dotprompt è progettato sulla premessa che i prompt sono codice. Scrivi e gestisci i prompt in file con formato speciale chiamati file dotprompt, ne monitori le modifiche utilizzando lo stesso sistema di controllo delle versioni che utilizzi per il codice e li esegui insieme al codice che chiama i modelli di AI generativa.

Per utilizzare Dotprompt, crea prima una directory prompts nella directory principale del progetto, quindi crea un file .prompt in quella directory. Ecco un semplice esempio che potresti chiamare greeting.prompt:

---
model: vertexai/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}}.

Per utilizzare questo prompt, installa il plug-in dotprompt:

go get github.com/firebase/genkit/go/plugins/dotprompt

Quindi, carica il prompt utilizzando Open:

import "github.com/firebase/genkit/go/plugins/dotprompt"
dotprompt.SetDirectory("prompts")
prompt, err := dotprompt.Open("greeting")

Puoi chiamare il metodo Generate del prompt per eseguire il rendering del modello e passarlo all'API del modello in un solo passaggio:

ctx := context.Background()

// Default to the project in GCLOUD_PROJECT and the location "us-central1".
vertexai.Init(ctx, nil)

// The .prompt file specifies vertexai/gemini-1.5-flash, which is
// automatically defined by Init(). However, if it specified a model that
// isn't automatically loaded (such as a specific version), you would need
// to define it here:
// vertexai.DefineModel("gemini-1.0-pro-002", &ai.ModelCapabilities{
// 	Multiturn:  true,
// 	Tools:      true,
// 	SystemRole: true,
// 	Media:      false,
// })

type GreetingPromptInput struct {
	Location string `json:"location"`
	Style    string `json:"style"`
	Name     string `json:"name"`
}
response, err := prompt.Generate(
	ctx,
	&dotprompt.PromptRequest{
		Variables: GreetingPromptInput{
			Location: "the beach",
			Style:    "a fancy pirate",
			Name:     "Ed",
		},
	},
	nil,
)
if err != nil {
	return err
}

fmt.Println(response.Text())

In alternativa, puoi semplicemente eseguire il rendering del modello in una stringa:

renderedPrompt, err := prompt.RenderText(map[string]any{
	"location": "a restaurant",
	"style":    "a pirate",
})

La sintassi di Dotprompt si basa sul linguaggio di creazione di modelli Handlebars. Puoi utilizzare gli aiuti if, unless e each per aggiungere parti condizionali al prompt o eseguire l'iterazione dei contenuti strutturati. Il formato del file utilizza il frontmatter YAML per fornire i metadati di un prompt in linea con il modello.

Definizione di schemi di input/output con Picoschema

Dotprompt include un formato compatto di definizione dello schema basato su YAML chiamato Picoschema per semplificare la definizione degli attributi più importanti di uno schema per l'utilizzo di LLM. Ecco un esempio di schema per un articolo:

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

Lo schema precedente è equivalente al seguente schema JSON:

{
  "properties": {
    "metadata": {
      "properties": {
        "updatedAt": {
          "type": "string",
          "description": "ISO timestamp of last update"
        },
        "approvedBy": {
          "type": "integer",
          "description": "id of approver"
        }
      },
      "type": "object"
    },
    "title": {
      "type": "string"
    },
    "subtitle": {
      "type": "string"
    },
    "draft": {
      "type": "boolean",
      "description": "true when in draft state"
    },
    "date": {
      "type": "string",
      "description": "the date of publication e.g. '2024-04-09'"
    },
    "tags": {
      "items": {
        "type": "string"
      },
      "type": "array",
      "description": "relevant tags for article"
    },
    "authors": {
      "items": {
        "properties": {
          "name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        },
        "type": "object",
        "required": ["name"]
      },
      "type": "array"
    }
  },
  "type": "object",
  "required": ["title", "date", "tags", "authors"]
}

Picoschema supporta i tipi scalari string, integer, number, boolean e any. Per oggetti, array ed enum, sono indicati tra parentesi dopo il nome del campo.

Gli oggetti definiti da Picoschema hanno tutte le proprietà richieste, a meno che non siano contrassegnate come facoltative da ?, e non consentono proprietà aggiuntive. Quando una proprietà è contrassegnata come facoltativa, viene anche impostata come nullable per consentire ai modelli LLM di restituire un valore nullo anziché omettere un campo.

In una definizione di oggetto, la chiave speciale (*) può essere utilizzata per dichiarare una definizione di campo "wildcard". Verranno associate eventuali proprietà aggiuntive non fornite da una chiave esplicita.

Picoschema non supporta molte delle funzionalità dello schema JSON completo. Se hai bisogno di schemi più solidi, puoi fornire uno schema JSON:

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

Sostituzione dei metadati del prompt

Sebbene i file .prompt ti consentano di incorporare metadati come la configurazione del modello nel file stesso, puoi anche sostituire questi valori in base alla chiamata:

// Make sure you set up the model you're using.
vertexai.DefineModel("gemini-1.5-flash", nil)

response, err := prompt.Generate(
	context.Background(),
	&dotprompt.PromptRequest{
		Variables: GreetingPromptInput{
			Location: "the beach",
			Style:    "a fancy pirate",
			Name:     "Ed",
		},
		Model: "vertexai/gemini-1.5-flash",
		Config: &ai.GenerationCommonConfig{
			Temperature: 1.0,
		},
	},
	nil,
)

Prompt con più messaggi

Per impostazione predefinita, Dotprompt crea un singolo messaggio con un ruolo "user". Alcuni prompt sono meglio espressi come una combinazione di più messaggi, ad esempio un prompt di sistema.

L'helper {{role}} fornisce un modo semplice per creare prompt con più messaggi:

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

Prompt multimodali

Per i modelli che supportano input multimodali, come immagini insieme a testo, puoi utilizzare l'helper {{media}}:

---
model: vertexai/gemini-1.5-flash
input:
  schema:
    photoUrl: string
---

Describe this image in a detailed paragraph:

{{media url=photoUrl}}

L'URL può essere un URI https:// o data: codificato in Base64 per l'utilizzo di immagini "in linea". Nel codice, si tratta di:

dotprompt.SetDirectory("prompts")
describeImagePrompt, err := dotprompt.Open("describe_image")
if err != nil {
	return err
}

imageBytes, err := os.ReadFile("img.jpg")
if err != nil {
	return err
}
encodedImage := base64.StdEncoding.EncodeToString(imageBytes)
dataURI := "data:image/jpeg;base64," + encodedImage

type DescribeImagePromptInput struct {
	PhotoUrl string `json:"photo_url"`
}
response, err := describeImagePrompt.Generate(
	context.Background(),
	&dotprompt.PromptRequest{Variables: DescribeImagePromptInput{
		PhotoUrl: dataURI,
	}},
	nil,
)

Varianti del prompt

Poiché i file prompt sono solo di testo, puoi (e devi) eseguirne il commit nel tuo sistema di controllo della versione, in modo da confrontare facilmente le modifiche nel tempo. Spesso, le versioni modificate dei prompt possono essere testate completamente solo in un ambiente di produzione, affiancate alle versioni esistenti. Dotprompt supporta questa funzionalità tramite la funzionalità varianti.

Per creare una variante, crea un file [name].[variant].prompt. Ad esempio, se utilizzi Gemini 1.5 Flash nel tuo prompt, ma vuoi verificare se Gemini 1.5 Pro ha un rendimento migliore, puoi creare due file:

  • my_prompt.prompt: il prompt "baseline"
  • my_prompt.geminipro.prompt: una variante denominata "geminipro"

Per utilizzare una variante del prompt, specificala al momento del caricamento:

describeImagePrompt, err := dotprompt.OpenVariant("describe_image", "geminipro")

Il caricamento della richiesta tenterà di caricare la variante del nome e farà ricorso al valore di riferimento se non esiste. Ciò significa che puoi utilizzare il caricamento condizionale in base ai criteri più adatti alla tua applicazione:

var myPrompt *dotprompt.Prompt
var err error
if isBetaTester(user) {
	myPrompt, err = dotprompt.OpenVariant("describe_image", "geminipro")
} else {
	myPrompt, err = dotprompt.Open("describe_image")
}

Il nome della variante è incluso nei metadati delle tracce di generazione, pertanto puoi confrontare e mettere a confronto il rendimento effettivo tra le varianti nell'ispettore delle tracce Genkit.