Dotprompt でプロンプトを管理する

プロンプト エンジニアリングは、アプリ デベロッパーが生成 AI モデルの出力に影響を与える主要な方法です。たとえば、LLM を使用する場合は、モデルのレスポンスのトーン、形式、長さなどの特性に影響を与えるプロンプトを作成できます。

これらのプロンプトの作成方法は、使用しているモデルによって異なります。1 つのモデル用に作成されたプロンプトは、別のモデルで使用するとパフォーマンスが低下する可能性があります。同様に、設定するモデル パラメータ(temperature、top-k など)も、モデルによって出力に異なる影響を与えます。

モデル、モデル パラメータ、プロンプトの 3 つの要素をすべて組み合わせて、目的の出力を生成することは、簡単なプロセスではありません。多くの場合、大幅な反復処理とテストが必要になります。Genkit には、この反復処理をより迅速かつ便利にすることを目的とした Dotprompt というライブラリとファイル形式が用意されています。

Dotprompt は、「プロンプトはコードである」という考え方で設計されています。プロンプトは、アプリケーション コードとは別に、対象のモデルとモデル パラメータとともに定義します。その後、デベロッパー(またはアプリケーション コードの作成に携わっていないユーザー)は、Genkit デベロッパー UI を使用して、プロンプトとモデル パラメータを迅速に反復処理できます。プロンプトが適切に動作したら、アプリケーションにインポートして Genkit を使用して実行できます。

プロンプトの定義は、それぞれ .prompt 拡張子のファイルに格納します。これらのファイルの例を次に示します。

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

3 つのダッシュで囲まれた部分は、GitHub Markdown と Jekyll で使用される frontmatter 形式に似た YAML frontmatter です。ファイルの残りの部分はプロンプトです。必要に応じて Handlebars テンプレートを使用できます。以降のセクションでは、.prompt ファイルを構成する各部分とその使用方法について詳しく説明します。

始める前に

このページを読む前に、AI モデルによるコンテンツの生成のページで説明されている内容を理解しておく必要があります。

このページのコードサンプルを実行する場合は、まずスタートガイドの手順を完了してください。以下の例では、Genkit がプロジェクトの依存関係としてすでにインストールされていることを前提としています。

プロンプト ファイルの作成

Dotprompt には、プロンプトを作成して読み込むためのさまざまな方法が用意されていますが、1 つのディレクトリ(またはそのサブディレクトリ)内にプロンプトを .prompt ファイルとして整理するプロジェクト用に最適化されています。このセクションでは、この推奨設定を使用してプロンプトを作成して読み込む方法について説明します。

プロンプト ディレクトリを作成する

Dotprompt ライブラリは、プロジェクトのルートにあるディレクトリにプロンプトがあることを前提としており、そこで見つかったプロンプトを自動的に読み込みます。デフォルトでは、このディレクトリの名前は prompts です。たとえば、デフォルトのディレクトリ名を使用すると、プロジェクト構造は次のようになります。

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

別のディレクトリを使用する場合は、Genkit の構成時に指定します。

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

プロンプト ファイルの作成

.prompt ファイルを作成するには、テキスト エディタを使用する方法と、デベロッパー UI を使用する方法の 2 つがあります。

テキスト エディタを使用する

テキスト エディタを使用してプロンプト ファイルを作成する場合は、プロンプト ディレクトリに .prompt 拡張子のテキスト ファイルを作成します(例: prompts/hello.prompt)。

プロンプト ファイルの最小限の例を次に示します。

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

ダッシュで囲まれた部分は、GitHub マークダウンと Jekyll で使用されるフロントマター形式に似た YAML フロントマターです。ファイルの残りの部分はプロンプトです。必要に応じて、Handlebars テンプレートを使用できます。前書きセクションは省略可能ですが、ほとんどのプロンプト ファイルには、少なくともモデルを指定するメタデータが含まれています。このページの後半では、これを超えて、プロンプト ファイルで Dotprompt の機能を利用する方法について説明します。

デベロッパー UI の使用

デベロッパー UI のモデルランナーを使用してプロンプト ファイルを作成することもできます。まず、Genkit ライブラリをインポートし、目的のモデル プラグインを使用するように構成するアプリコードから始めます。例:

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

ファイルに他のコードが含まれていてもかまいませんが、上記のコードのみが必要です。

同じプロジェクトでデベロッパー UI を読み込みます。

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

[モデル] セクションで、プラグインから提供されるモデルのリストから使用するモデルを選択します。

Genkit デベロッパー UI モデルランナー

満足のいく結果が得られるまで、プロンプトと構成をテストします。準備ができたら、[Export] ボタンを押して、ファイルをプロンプト ディレクトリに保存します。

実行プロンプト

プロンプト ファイルを作成したら、アプリケーション コードから、または Genkit が提供するツールを使用して実行できます。プロンプトの実行方法にかかわらず、まず、Genkit ライブラリと目的のモデル プラグインをインポートするアプリコードから始めます。例:

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

ファイルに他のコードが含まれていてもかまいませんが、上記のコードのみが必要です。プロンプトをデフォルト以外のディレクトリに保存する場合は、Genkit の構成時に必ず指定してください。

コードからプロンプトを実行する

プロンプトを使用するには、まず prompt('file_name') メソッドを使用してプロンプトを読み込みます。

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

読み込まれたら、関数のようにプロンプトを呼び出すことができます。

const response = await helloPrompt();

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

呼び出し可能なプロンプトには、プロンプトへの入力(入力スキーマの指定の下のセクションを参照)と、generate() メソッドと同様の構成オブジェクトという 2 つのオプション パラメータがあります。例:

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

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

プロンプト呼び出しに渡すパラメータは、プロンプト ファイルで指定された同じパラメータをオーバーライドします。

使用可能なオプションの説明については、AI モデルでコンテンツを生成するをご覧ください。

デベロッパー UI の使用

アプリのプロンプトを調整する際に、Genkit デベロッパー UI でプロンプトを実行すると、アプリケーション コードとは別に、プロンプトとモデル構成をすばやく反復処理できます。

プロジェクト ディレクトリからデベロッパー UI を読み込みます。

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

Genkit デベロッパー UI プロンプト ランナー

プロンプトをデベロッパー UI に読み込んだら、さまざまな入力値でプロンプトを実行し、プロンプトの文言や構成パラメータの変更がモデルの出力にどのように影響するかをテストできます。結果に満足したら、[Export prompt] ボタンをクリックして、変更したプロンプトをプロジェクト ディレクトリに保存します。

モデル設定

プロンプト ファイルのフロントマター ブロックで、必要に応じてプロンプトのモデル構成値を指定できます。

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

これらの値は、呼び出し可能なプロンプトで受け入れられる config パラメータに直接マッピングされます。

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

使用可能なオプションの説明については、AI モデルでコンテンツを生成するをご覧ください。

入力スキーマと出力スキーマ

プロンプトの入力スキーマと出力スキーマは、前書きセクションで定義することで指定できます。

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

これらのスキーマは、generate() リクエストまたはフロー定義に渡されるスキーマとほぼ同じ方法で使用されます。たとえば、上記で定義したプロンプトでは、構造化された出力が生成されます。

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

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

.prompt ファイルでスキーマを定義するには、Dotprompt 独自のスキーマ定義形式である Picoschema、標準の JSON スキーマ、またはアプリケーション コードで定義されたスキーマへの参照など、いくつかの方法があります。以降のセクションでは、これらのオプションについて詳しく説明します。

Picoschema

上記の例のスキーマは、Picoschema という形式で定義されています。Picoschema は、YAML 用に最適化されたコンパクトなスキーマ定義形式で、LLM の使用に必要なスキーマの最も重要な属性を簡単に定義できます。以下に、アプリが記事について保存する可能性のある情報を指定するスキーマの例を示します。

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

上記のスキーマは、次の 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 は、スカラー型の stringintegernumberbooleanany をサポートしています。オブジェクト、配列、列挙型は、フィールド名の後に括弧で囲んで記述します。

Picoschema で定義されたオブジェクトは、? で省略可能と指定されていない限り、すべてのプロパティが必須であり、また追加のプロパティは許可されません。プロパティが省略可能としてマークされている場合、そのプロパティは null 値許容型にもなるため、LLM はフィールドを省略するかわりに null を返すことも可能になります。

オブジェクト定義では、特殊なキー (*) を使用して「ワイルドカード」フィールド定義を宣言できます。これには、明示的なキーで指定されていない任意の追加プロパティがマッチします。

JSON スキーマ

Picoschema は、フルセットの JSON スキーマが持つ多くの機能をサポートしていません。より堅牢なスキーマが必要な場合は、代わりに JSON スキーマで与えることができます。

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

コードで定義された Zod スキーマ

.prompt ファイルでスキーマを直接定義するだけでなく、defineSchema() に登録されているスキーマを名前で参照することもできます。TypeScript を使用している場合、このアプローチでは、プロンプトを操作するときに言語の静的型チェック機能を利用できます。

スキーマを登録するには:

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

プロンプト内に、登録済みスキーマの名前を指定します。

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

Dotprompt ライブラリは、名前を基盤となる登録済みの Zod スキーマに自動的に解決します。その後、スキーマを使用して 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;

プロンプト テンプレート

.prompt ファイルの前書き(存在する場合)に続く部分は、モデルに渡されるプロンプト自体です。このプロンプトは単純なテキスト文字列にすることもできますが、多くの場合、ユーザー入力をプロンプトに組み込む必要があります。これを行うには、Handlebars テンプレート言語を使用してプロンプトを指定します。プロンプト テンプレートには、プロンプトの入力スキーマで定義された値を参照するプレースホルダを含めることができます。

これは、入力スキーマと出力スキーマのセクションですでに確認しました。

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

この例では、プロンプトを実行すると、Handlebars 式 {{theme}} が入力の theme プロパティの値に解決されます。入力をプロンプトに渡すには、次の例のようにプロンプトを呼び出します。

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

入力スキーマで theme プロパティが省略可能として宣言され、デフォルトが指定されているため、プロパティを省略しても、デフォルト値を使用してプロンプトが解決されます。

Handlebars テンプレートは、一部の制限付き論理構文もサポートしています。たとえば、デフォルトを指定する代わりに、Handlebars の #if ヘルパーを使用してプロンプトを定義できます。

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

この例では、theme プロパティが指定されていない場合、プロンプトは「レストランのメニュー項目を作成」と表示されます。

組み込みの論理ヘルパーの詳細については、Handlebars のドキュメントをご覧ください。

テンプレートでは、入力スキーマで定義されたプロパティに加えて、Genkit によって自動的に定義された値を参照することもできます。以降のセクションでは、これらの自動定義値とその使用方法について説明します。

複数メッセージのプロンプト

デフォルトでは、Dotprompt は「user」ロールで単一のメッセージを作成します。ただし、システム プロンプトなどのプロンプトは、複数のメッセージを組み合わせて表現するのが最善です。

{{role}} ヘルパーは、複数のメッセージのプロンプトを作成する簡単な方法を提供します。

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

マルチモーダル プロンプト

テキストと一緒に画像などのマルチモーダル入力をサポートするモデルでは、{{media}} ヘルパーを使用できます。

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

{{media url=photoUrl}}

URL には、画像を「インライン」で使用するために、https: URI または base64 でエンコードされた data: URI を使用できます。コードでは次のように記述します。

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

data: URL の作成例については、モデル ページのマルチモーダル入力もご覧ください。

パーシャル

パーシャルは、任意のプロンプト内に含めることができる再利用可能なテンプレートです。部分プロンプトは、共通の動作を共有する関連プロンプトの場合特に便利です。

プロンプト ディレクトリを読み込むときに、接頭辞にアンダースコア(_)が付いているファイルは部分ファイルと見なされます。たとえば、ファイル _personality.prompt には次の内容が含まれます。

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

これを他のメッセージに含めることができます。

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

部分は {{>NAME_OF_PARTIAL args...}} 構文を使用して挿入されます。部分プロンプトに引数を指定しない場合は、親プロンプトと同じコンテキストで実行されます。

部分テンプレートは、上記の名前付き引数の両方、またはコンテキストを表す単一の位置引数の両方を受け入れます。これは、リストのメンバーのレンダリングなどのタスクに役立ちます。

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

コードで部分定義を定義する

definePartial を使用して、コード内で部分を定義することもできます。

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

コード定義の部分は、すべてのプロンプトで使用できます。

カスタム ヘルパーの定義

プロンプト内のデータを処理して管理するカスタム ヘルパーを定義できます。ヘルパーは defineHelper を使用してグローバルに登録されます。

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

定義したヘルパーは、任意のプロンプトで使用できます。

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

HELLO, {{shout name}}!!!

プロンプトのバリアント

プロンプト ファイルはテキストにすぎないため、バージョン管理システムに commit できます(また、そうすべきです)。これにより、変更点を簡単に比較できます。調整されたバージョンのプロンプトは、本番環境で既存のバージョンと同居させた状態でテストする以外に、完全にテストできる方法がない場合がよくあります。Dotprompt は、バリアント機能でこれをサポートしています。

バリアントを作成するには、[name].[variant].prompt ファイルを作成します。たとえば、プロンプトで Gemini 1.5 Flash を使用していて、Gemini 1.5 Pro の方がパフォーマンスが高いかどうかを確認したい場合は、次の 2 つのファイルを作成します。

  • my_prompt.prompt: 「ベースライン」のプロンプト
  • my_prompt.gemini15pro.prompt: gemini15pro という名前のバリアント

プロンプトのバリアントを使用するには、読み込み時にバリアント オプションを指定します。

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

バリアント名は生成トレースのメタデータに含まれるため、Genkit トレース インスペクタを使い、バリアント間で実際のパフォーマンスを比較および対比できます。

コードでプロンプトを定義する

ここまで説明した例では、プロンプトが 1 つのディレクトリ(またはそのサブディレクトリ)内の個々の .prompt ファイルで定義され、アプリが実行時にアクセスできることを前提としています。Dotprompt は、この設定を中心に設計されており、その作成者は、全体的にデベロッパー エクスペリエンスが最適であると考えています。

ただし、この設定で十分にサポートされていないユースケースがある場合は、definePrompt() 関数を使用してコードでプロンプトを定義することもできます。

この関数の最初のパラメータは、.prompt ファイルのフロントマター ブロックに似ています。2 番目のパラメータは、プロンプト ファイルの場合と同様に Handlebars テンプレート文字列、または 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?` }],
        },
      ],
    };
  }
);