Zarządzanie promptami za pomocą Dotprompt

Projektowanie 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 tworzenia 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, ustawione przez Ciebie parametry modelu (temperatura, Top-K itp.) będą wpływać na dane wyjściowe w zależności od modelu.

Uzyskanie pożądanych wyników przy użyciu wszystkich tych czynników (modelu, parametrów modelu 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.

Narzędzie Dotprompt zostało zaprojektowane na podstawie założenia, że prompty to kod. Prompty wraz z modelami i parametrami modeli, do których są przeznaczone, definiujesz oddzielnie 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 Genkit Developer. 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 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. 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 Początkujący. 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). Z tego sekcji dowiesz się, jak tworzyć i wczytywać prompty 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/
├── lib/
├── node_modules/
├── prompts/
│   └── hello.prompt
├── src/
├── package-lock.json
├── package.json
└── tsconfig.json

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

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

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 informacji wstępnych w formacie używanym 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

Plik prompta możesz też utworzyć 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 { 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=...
    }),
  ],
});

Nie ma problemu, jeśli plik zawiera inny kod, ale to, co jest wymagane, to tylko to, co powyżej.

Załaduj interfejs programisty w tym samym projekcie:

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

W sekcji Modele 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 podczas biegu

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 { 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=...
    }),
  ],
});

Nie ma problemu, jeśli plik zawiera inny kod, ale to, co jest wymagane, to tylko to, co powyżej. Jeśli przechowujesz prompty w katalogu innym niż domyślny, pamiętaj, aby go podać podczas konfigurowania Genkit.

Uruchamianie promptów z poziomu kodu

Aby użyć prompta, najpierw wczytaj go za pomocą metody prompt('file_name'):

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

Po załadowaniu możesz wywołać prompt jak funkcję:

const response = await helloPrompt();

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

Prompt do wywołania przyjmuje 2 opcjonalne parametry: dane wejściowe prompta (patrz sekcja poniżej o określaniu schematów danych wejściowych) oraz obiekt konfiguracji podobny do tego z metody generate(). Przykład:

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

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

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

Opis 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 uruchamiać je w interfejsie dla programistów Genkit, aby szybko iterować prompty i konfiguracje modelu niezależnie od kodu aplikacji.

Załaduj interfejs dla programistów z katalogu projektu:

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

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-1.5-flash
config:
  temperature: 1.4
  topK: 50
  topP: 0.4
  maxOutputTokens: 400
  stopSequences:
    -   "<end>"
    -   "<fin>"
---

Te wartości są mapowane bezpośrednio na parametr config akceptowany przez wywoływalny prompt:

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

Opis 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 danych wejściowych i wyjściowych dla promptu, definiując je w sekcji wstępnej:

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

Te schematy są używane w podobny sposób jak te przekazywane do generate()żądania lub definicji przepływu. Na przykład prompt zdefiniowany powyżej generuje uporządkowane dane wyjściowe:

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

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

W pliku .prompt możesz definiować schematy na kilka sposobów: za pomocą własnego formatu definiowania schematów Dotprompt (Picoschema), w standardowym formacie JSON Schema 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 powyższym przykładzie są zdefiniowane w formacie Picoschema. Picoschema to zwarty format definicji schematu zoptymalizowany pod kątem YAML, który ułatwia 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 interfejsu 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 */

}

Picoschema obsługuje typy skalarne string, integer, number, booleanany. Obiekty, tablice i typy wyliczeniowe 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

Schematy Zod zdefiniowane w kodzie

Oprócz definiowania schematów bezpośrednio w pliku .prompt możesz też odwoływać się do schematu zarejestrowanego w pliku defineSchema() po nazwie. Jeśli używasz TypeScript, to podejście pozwoli Ci korzystać z funkcji statycznego sprawdzania kodu podczas pracy z promptami.

Aby zarejestrować schemat:

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()),
  })
);

W promptzie podaj nazwę zarejestrowanego schematu:

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

Biblioteka Dotprompt automatycznie przekształci nazwę w podstawowy zarejestrowany schemat Zod. Następnie możesz użyć schematu, aby określić typ danych wyjściowych prompta Dotprompt:

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;

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 określić 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-1.5-flash
config:
  temperature: 1.4
  topK: 50
  topP: 0.4
  maxOutputTokens: 400
  stopSequences:
    -   "<end>"
    -   "<fin>"
---

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

const menuPrompt = ai.prompt('menu');
const { data } = await menuPrompt({ 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 miałby zostać 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-1.5-flash
input:
  schema:
    theme?: string
---
Invent a menu item for a {{#if theme}}{{theme}} themed{{/if}} 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 najlepiej jednak wyrażać w postaci kombinacji kilku wiadomości, np. promptu systemowego.

Narzędzie pomocnicze {{role}} umożliwia łatwe tworzenie promptów z kilkoma wiadomościami:

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

Prompty multimodalne

W przypadku modeli, które obsługują dane wejściowe multimodalne, np. obrazy i tekst, możesz użyć pomocnika {{media}}:

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

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

Przykład tworzenia adresu URL data: znajdziesz też w sekcji Wejście multimodalne na stronie Modele.

Częściowe

Elementy częściowe to szablony do wielokrotnego użytku, które można dodawać do dowolnego 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-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}}

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

Funkcje częściowe akceptują zarówno argumenty nazwane jak wyżej, jak i pojedynczy argument pozycyjny reprezentujący kontekst. Może to być przydatne podczas wykonywania takich czynności jak renderowanie elementów listy.

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

Definiowanie częściowych w kodzie

Częściowe komponenty możesz też zdefiniować w kodzie za pomocą tagu definePartial:

ai.definePartial(
  '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 nimi zarządzać w ramach promptu. Pomocnicy są rejestrowani globalnie za pomocą defineHelper:

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

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

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

HELLO, {{shout name}}!!!

Warianty promptów

Ponieważ pliki promptów to tylko tekst, możesz je (a nawet powinieneś) przekazywać do systemu kontroli wersji, co pozwoli Ci łatwo porównywać zmiany w czasie. Często zmodyfikowane wersje promptów można w pełni przetestować tylko w środowisku produkcyjnym, porównując je z dotychczasowymi wersjami. Dotprompt obsługuje to za pomocą funkcji wariantów.

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

  • my_prompt.prompt: prompt „baseline”
  • my_prompt.gemini15pro.prompt: wariant o nazwie gemini15pro

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

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

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 tej konfiguracji, a jego autorzy uważają, że jest to najlepszy sposób na pracę programistów.

Jeśli jednak masz przypadki użycia, które nie są dobrze obsługiwane przez tę konfigurację, możesz zdefiniować prompty w kodzie za pomocą funkcji definePrompt():

Pierwszy parametr tej funkcji jest analogiczny do bloku wstępnego w pliku .prompt. Drugi parametr może być ciągiem znaków szablonu Handlebars, jak w pliku prompt, lub funkcją zwracającą 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?` }],
        },
      ],
    };
  }
);