Zarządzanie promptami za pomocą Dotprompt

Inżynieria promptów to główny sposób, w jaki deweloper aplikacji może wpływać na wyniki modeli generatywnej AI. Na przykład korzystając z modeli LLM, możesz tworzyć prompty, które wpływają na ton, format, długość i inne cechy odpowiedzi modeli.

Sposób zapisywania tych promptów zależy od używanego modelu. Prompt napisany dla jednego modelu może nie działać dobrze w przypadku innego modelu. Podobnie ustawienia parametrów modelu (temperatura, Top-K itp.) będą wpływać na dane wyjściowe w różny sposób w zależności od modelu.

Uzyskanie pożądanych wyników przy użyciu wszystkich tych czynników – modelu, jego parametrów i promptu – rzadko jest prostym procesem i często wymaga wielu iteracji oraz eksperymentów. Genkit udostępnia bibliotekę i format pliku Dotprompt, które mają na celu przyspieszenie i ułatwienie tej iteracji.

Dotprompt opiera się na założeniu, że prompty to kod. Prompty definiujesz razem z modelami i parametrami modeli, dla których są przeznaczone, niezależnie od kodu aplikacji. Następnie Ty (lub ktoś, kto nie jest nawet zaangażowany w tworzenie kodu aplikacji) możesz szybko iterować prompty i parametry modelu za pomocą interfejsu dla programistów Genkit. Gdy prompty będą działać zgodnie z oczekiwaniami, możesz je zaimportować do aplikacji i uruchamiać za pomocą Genkit.

Definicje promptów są zapisywane w plikach o rozszerzeniu .prompt. Oto przykład wyglądu tych plików:

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

Część w trójkropkach to dane wstępne YAML, podobne do formatu danych wstępnych używanego przez GitHub Markdown i Jekyll. Reszta pliku to prompt, który może opcjonalnie używać szablonów Handlebars. W następnych sekcjach znajdziesz więcej informacji o poszczególnych częściach pliku .prompt oraz o sposobach ich wykorzystania.

Zanim zaczniesz

Zanim zaczniesz czytać tę stronę, zapoznaj się z treściami na stronie Generowanie treści za pomocą modeli AI.

Jeśli chcesz uruchomić przykłady kodu na tej stronie, najpierw wykonaj czynności opisane w przewodniku dla początkujących. We wszystkich przykładach zakładamy, że masz już zainstalowaną bibliotekę Genkit jako zależność w projekcie.

Tworzenie plików prompt

Chociaż Dotprompt udostępnia różne sposoby tworzenia i wczytywania promptów, jest zoptymalizowany pod kątem projektów, w których prompty są zorganizowane w plikach .prompt w jednym katalogu (lub podkatalogach). W tym rozdziale znajdziesz instrukcje tworzenia i wczytywania promptów przy użyciu tej zalecanej konfiguracji.

Tworzenie katalogu promptów

Biblioteka Dotprompt spodziewa się, że prompty będą znajdować się w katalogu w głównym katalogu projektu, i automatycznie wczytuje wszystkie prompty, które tam znajdzie. Domyślnie ta katalog ma nazwę prompts. Na przykład przy użyciu domyślnej nazwy katalogu struktura projektu może wyglądać tak:

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

Jeśli chcesz użyć innego katalogu, możesz go określić podczas konfigurowania Genkit:

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

Tworzenie pliku promptu

Plik .prompt można utworzyć na 2 sposoby: w edytorze tekstu lub za pomocą interfejsu dla programistów.

Korzystanie z edytora tekstu

Jeśli chcesz utworzyć plik prompta w edytorze tekstu, utwórz plik tekstowy z rozszerzeniem .prompt w katalogu promptów, np. prompts/hello.prompt.

Oto minimalny przykład pliku prompta:

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

Część w nawiasach to informacje wstępne w formacie YAML, podobne do formatu informacji wstępnych używanego przez GitHub Markdown i Jekyll. Pozostała część pliku to prompt, który może opcjonalnie używać szablonów Handlebars. Sekcja wstępna jest opcjonalna, ale większość plików promptów zawiera co najmniej metadane określające model. W dalszej części tej strony dowiesz się, jak pójść dalej i wykorzystać funkcje Dotprompt w plikach prompt.

Korzystanie z interfejsu dla programistów

Możesz też utworzyć plik prompta za pomocą narzędzia do uruchamiania modelu w interfejsie programisty. Zacznij od kodu aplikacji, który importuje bibliotekę Genkit i konfiguruje ją do używania interesującego Cię wtyczka modelu. Przykład:

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

Załaduj interfejs programisty w tym samym projekcie:

genkit start -- go run .

W sekcji Model wybierz z listy modeli udostępnionych przez wtyczkę model, którego chcesz użyć.

Genkit – model interfejsu użytkownika dla dewelopera

Następnie eksperymentuj z promptem i konfiguracją, aż uzyskasz zadowalające wyniki. Gdy wszystko będzie gotowe, naciśnij przycisk Eksportuj i zapisz plik w katalogu promptów.

Prompty dotyczące biegania

Po utworzeniu plików promptów możesz je uruchamiać z poziomu kodu aplikacji lub za pomocą narzędzi udostępnionych przez Genkit. Niezależnie od tego, jak chcesz uruchamiać prompty, zacznij od kodu aplikacji, który importuje bibliotekę Genkit i interesujące Cię wtyczki modelu. Przykład:

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

Jeśli przechowujesz prompty w katalogu innym niż domyślny, pamiętaj, aby podać go podczas konfigurowania Genkit.

Uruchamianie promptów z poziomu kodu

Aby użyć prompta, najpierw załaduj go za pomocą funkcji genkit.LookupPrompt():

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

Prompt wykonalny ma podobne opcje do genkit.Generate(), a wiele z nich można zastąpić w czasie wykonywania, w tym dane wejściowe (patrz sekcja określanie schematów danych wejściowych), konfigurację i inne:

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

Wszystkie parametry przekazane do wywołania prompta zastąpią te same parametry określone w pliku prompta.

Opisy dostępnych opcji znajdziesz w artykule Generowanie treści za pomocą modeli AI.

Korzystanie z interfejsu dla programistów

Podczas ulepszania promptów w aplikacji możesz je uruchamiać w interfejsie dla programistów Genkit, aby szybko iterować prompty i konfiguracje modelu niezależnie od kodu aplikacji.

Załaduj interfejs programisty z katalogu projektu:

genkit start -- go run .

Genkit – prompt w interfejsie dla programistów

Po załadowaniu promptów do interfejsu programisty możesz uruchamiać je z różnymi wartościami wejściowymi i eksperymentować z tym, jak zmiany w sformułowaniu promptu lub parametry konfiguracji wpływają na dane wyjściowe modelu. Gdy uzyskasz zadowalający efekt, możesz kliknąć przycisk Eksportuj prompt, aby zapisać zmodyfikowany prompt w katalogu projektu.

Konfiguracja modelu

W bloku wstępnym w plikach promptów możesz opcjonalnie określić wartości konfiguracji prompta:

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

Te wartości są mapowane bezpośrednio na opcję WithConfig() akceptowaną przez prompt wykonalny:

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

Opisy dostępnych opcji znajdziesz w artykule Generowanie treści za pomocą modeli AI.

Schematy danych wejściowych i wyjściowych

Możesz określić schematy wejściowe i wyjściowe promptu, definiując je w sekcji wstępnej. Te schematy są używane w podobny sposób jak te przekazywane do żądania genkit.Generate() lub definicji przepływu:

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

Ten kod generuje następujące dane wyjściowe w formacie ustrukturyzowanym:

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"])

Schematy możesz definiować w pliku .prompt na kilka sposobów: za pomocą własnego formatu definicji schematu Dotprompt (Picoschema), standardowego schematu JSON lub jako odwołania do schematów zdefiniowanych w kodzie aplikacji. W następnych sekcjach opisujemy każdą z tych opcji bardziej szczegółowo.

Picoschema

Schematy w przykładzie powyżej są zdefiniowane w formacie Picoschema. Picoschema to zwarty format definicji schematu zoptymalizowany pod kątem YAML, który upraszcza definiowanie najważniejszych atrybutów schematu na potrzeby LLM. Oto dłuższy przykład schematu, który określa informacje, które aplikacja może przechowywać na temat artykułu:

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

Powyższy schemat jest odpowiednikiem tego typu 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
}

Picoschema obsługuje typy skalarne string, integer, number, booleanany. Obiekty, tablice i typy wyliczenia są oznaczane w nawiasach po nazwie pola.

Obiekty zdefiniowane przez Picoschema mają wszystkie wymagane właściwości, chyba że oznaczono je jako opcjonalne za pomocą ?, i nie zezwalają na dodatkowe właściwości. Gdy dana właściwość jest oznaczona jako opcjonalna, staje się też dopuszczalna, co oznacza, że LLM może zwracać wartość null zamiast pomijać pole.

W definicji obiektu klucz specjalny (*) może służyć do zadeklarowania definicji pola „symbol zastępczy”. Dopasuje ona wszystkie dodatkowe właściwości, które nie zostały podane za pomocą klucza docelowego.

Schemat JSON

Picoschema nie obsługuje wielu funkcji pełnego schematu JSON. Jeśli potrzebujesz bardziej rozbudowanych schematów, możesz zamiast tego podać schemat JSON:

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

Szablony promptów

Część pliku .prompt, która następuje po informacjach wstępnych (jeśli występują), to prompt, który zostanie przekazany do modelu. Prośba może być prostym tekstem, ale często warto uwzględnić w niej dane wprowadzane przez użytkownika. Aby to zrobić, możesz podać prompt za pomocą języka szablonów Handlebars. Szablony promptów mogą zawierać zmienne, które odwołują się do wartości zdefiniowanych przez schemat danych promptu.

W sekcji dotyczącej schematów danych wejściowych i wyjściowych pokazaliśmy już, jak to działa:

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

W tym przykładzie wyrażenie Handlebars {{theme}} jest zastępowane wartością właściwości theme wejścia po uruchomieniu promptu. Aby przekazać dane promptowi, wywołaj go w ten sposób:

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

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

Ponieważ w schemacie danych określono, że właściwość theme jest opcjonalna i podano jej wartość domyślną, można było pominąć tę właściwość, a prompt zostałby rozwiązany przy użyciu wartości domyślnej.

Szablony Handlebars obsługują też niektóre ograniczone konstrukcje logiczne. Zamiast wartości domyślnej możesz na przykład zdefiniować prompt za pomocą pomocnika #if w ramach biblioteki Handlebars:

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

W tym przykładzie prompt wyświetla się jako „Wymyśl nazwę pozycji menu dla restauracji”, gdy właściwość theme jest nieokreślona.

Informacje o wszystkich wbudowanych pomocnikach logicznych znajdziesz w dokumentacji Handlebars.

Oprócz właściwości zdefiniowanych w szablonie wejściowym Twoje szablony mogą też odwoływać się do wartości zdefiniowanych automatycznie przez Genkit. W kolejnych sekcjach opisujemy te wartości zdefiniowane automatycznie i sposób ich użycia.

Prompty w kilku wiadomościach

Domyślnie Dotprompt tworzy jedną wiadomość z rolą „użytkownik”. Niektóre prompty, np. prompty systemowe, najlepiej wyrażać za pomocą kombinacji kilku wiadomości.

Narzędzie {{role}} ułatwia tworzenie promptów z wieloma wiadomościami:

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

Prompty multimodalne

W przypadku modeli, które obsługują dane wejściowe multimodalne, takie jak obrazy wraz z tekstem, możesz użyć pomocnika {{media}}:

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

{{media url=photoUrl}}

Adres URL może być identyfikatorem URI typu https: lub data: zakodowanym w formacie base64, co umożliwia umieszczanie obrazu w tekście. W kodzie wygląda to tak:

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

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

Przykład tworzenia adresu URL data: znajdziesz w sekcji Wejście multimodalne na stronie Generowanie treści za pomocą modeli AI.

Częściowe

Fragmenty to szablony do wielokrotnego użytku, które można uwzględnić w dowolnym promptu. Fragmenty są szczególnie przydatne w przypadku powiązanych promptów, które mają podobne zachowanie.

Podczas wczytywania katalogu promptów każdy plik z preiksem podkreślenia (_) jest uważany za częściowy. Plik _personality.prompt może zawierać:

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

Można je następnie uwzględnić w innych promptach:

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

Częściowe są wstawiane za pomocą składni {{>NAME_OF_PARTIAL args...}}. Jeśli nie podasz argumentów do funkcji częściowej, zostanie ona wykonana w tym samym kontekście co prompt nadrzędny.

Funkcje częściowe akceptują argumenty nazwane lub pojedynczy argument pozycyjny reprezentujący kontekst. Może to być przydatne podczas wykonywania takich zadań jak renderowanie elementów listy.

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

Definiowanie częściowych w kodzie

Częściowe elementy możesz też zdefiniować w kodzie za pomocą funkcji genkit.DefinePartial():

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

Fragmenty zdefiniowane w kodzie są dostępne we wszystkich promptach.

Definiowanie niestandardowych narzędzi pomocniczych

Możesz zdefiniować niestandardowe pomocnicze funkcje, aby przetwarzać dane i zarządzać nimi w ramach promptu. Pomocnicy są rejestrowani globalnie za pomocą genkit.DefineHelper():

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

Po zdefiniowaniu pomocnika możesz go używać w dowolnym prompt:

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

HELLO, {{shout name}}!!!

Warianty promptów

Ponieważ pliki promptów to tylko tekst, możesz (a nawet powinieneś) je zapisać w systemie kontroli wersji, co uprości proces porównywania zmian w czasie. Często zmodyfikowane wersje promptów można w pełni przetestować tylko w środowisku produkcyjnym obok istniejących wersji. Dotprompt obsługuje tę funkcję za pomocą funkcji wersji.

Aby utworzyć wariant, utwórz plik [name].[variant].prompt. Jeśli na przykład używasz w promptach modelu Gemini 2.0 Flash, ale chcesz sprawdzić, czy model Gemini 2.5 Pro będzie działać lepiej, możesz utworzyć 2 pliki:

  • myPrompt.prompt: prompt „baseline”
  • myPrompt.gemini25pro.prompt: wariant o nazwie gemini25pro

Aby użyć wariantu prompta, podczas wczytywania określ opcję wariantu:

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

Nazwa wariantu jest zawarta w metadanych generowanych śladów, dzięki czemu możesz porównywać rzeczywistą skuteczność różnych wariantów w narzędzie Genkit Trace Inspector.

Definiowanie promptów w kodzie

We wszystkich przykładach omówionych do tej pory założono, że prompty są zdefiniowane w pojedynczych plikach .prompt w jednym katalogu (lub podkatalogach) i dostępne dla aplikacji w czasie wykonywania. Narzędzie Dotprompt zostało zaprojektowane z uwzględnieniem tego ustawienia, a jego autorzy uważają, że jest to najlepsze rozwiązanie dla programistów.

Jeśli jednak masz przypadki użycia, które nie są dobrze obsługiwane przez tę konfigurację, możesz też zdefiniować prompty w kodzie za pomocą funkcji 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)

Prompty mogą też być renderowane w formie GenerateActionOptions, która może być przetworzona i przekazana do 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

Pamiętaj, że wszystkie opcje prompta są przenoszone do GenerateActionOptions z wyjątkiem WithMiddleware(), które musi być przekazywane osobno, jeśli używasz Prompt.Render() zamiast Prompt.Execute().