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

Firebase Genkit には、生成 AI プロンプトの作成と整理に役立つ Dotprompt プラグインとテキスト形式が用意されています。

Dotprompt は、「プロンプトはコードである」という考え方で設計されています。プロンプトは、dotprompt ファイルと呼ばれる特別な形式のファイルで作成、管理します。そして、コードに使用するのと同じバージョン管理システムを使用してプロンプトの変更を追跡し、生成 AI モデルを呼び出すコードとともにデプロイします。

Dotprompt を使用するには、まずプロジェクトのルートに prompts ディレクトリを作成し、そのディレクトリに .prompt ファイルを作成します。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}}.

このプロンプトを使用するには、dotprompt プラグインをインストールします。

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

次に、Open を使用してプロンプトを読み込みます。

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

プロンプトの Generate メソッドを呼び出すと、1 つのステップで、テンプレートからの生成を行い、結果をモデル API に渡すことができます。

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

または、テンプレートから文字列を生成します。

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

Dotprompt の構文は、Handlebars テンプレート言語をベースにしています。ifunlesseach の各ヘルパーを使用すると、条件付きの部分をプロンプトに追加したり、構造化された内容について反復処理したりできます。このファイル形式では、YAML frontmatter を使用して、テンプレートを使いインラインでプロンプトにメタデータを与えます。

Picoschema で入力 / 出力スキーマを定義する

Dotprompt には 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

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

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

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

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

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

プロンプト メタデータをオーバーライドする

.prompt ファイルでは、モデル構成などのメタデータをファイル自体に埋め込むことができますが、これらの値を呼び出しごとにオーバーライドすることもできます。

// 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,
)

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

デフォルトでは、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 を使用できます。コードでは、次のように記述します。

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

プロンプトのバリアント

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

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

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

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

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

プロンプト ローダーが、その名前のバリアントを読み込もうとします。存在しない場合は、ベースラインを読み込みます。つまり、アプリに適した条件に基づく条件付き読み込みを使用できます。

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

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