promptfoo でお手軽プロンプト検証

こんにちは! kintone 開発チームの福田(@man_2_fork)です。

kintone では AI を使った RAG 機能をベータ版として提供しています。機能自体については、プレスリリースをご覧ください。

さて、AI 機能の開発のためには複数のモデルやプロンプトの検証が欠かせません。このときに promptfoo という LLM 検証用のツールを使用しました。このツールを使うと簡単な検証が行えたり、複雑な検証をプラグイン機構で行えたりと柔軟かつ非常に便利でした。

この記事では promptfoo の基本的な部分から少し複雑なユースケースまでご紹介していこうと思います。

モチベーション

LLM アプリケーションを開発する上で欠かせないのがモデルの検証やプロンプトエンジニアリングです。

LLM の進化は日進月歩であり、より性能が高いモデルや、より安価なモデルが次々と登場します。アプリケーションを開発する上では、そうした変化に追従し、性能やコストのために新しいモデルを検証する必要があります。また、クラウドプロバイダのクオータ制限もあり、複数のモデルを使用できると嬉しい場面もあります。

単一のモデルを使用する場合においても、プロンプトエンジニアリングによって既存のモデルを使用しての性能向上にも努める必要があります。

こうした状況下で、LLM の検証をコストを抑えて行えることは必要不可欠と言っても過言ではないでしょう。

基本的な使い方

まずは promptfoo の基本的な使い方をご紹介します。

promptfoo は YAML ファイルにモデルとプロンプトを記述することによってモデルごとの回答生成を試すというのが基本的な使い方になります。お気に入りのエディタで編集できるので良いですね。

例として以下のような YAMLを 考えてみましょう。以下のファイルを promptfooconfig.yaml として保存します。

providers:
  - id: bedrock:anthropic.claude-3-5-sonnet-20240620-v1:0 # Claude 3.5 Sonnet v1
  - id: bedrock:anthropic.claude-3-5-sonnet-20241022-v2:0 # Claude 3.5 Sonnet v2
    config:
      region: us-west-2

prompts:
  - "あなたは料理の専門家です。次の食材を使った簡単なレシピを 1 つ提案してください。食材: {{ ingredient }}"

tests:
  - vars:
      ingredient: トマト
  - vars:
      ingredient: 鶏肉

これは Amazon Bedrock の Claude 3.5 Sonnet v1 と Claude 3.5 Sonnet v2 に対してレシピを提案するプロンプトを試すという例になっています。{{ ingredient }} として食材をパラメータ化しており、ひとつのプロンプトをパラメータを変えて検証します。

実際に実行してみましょう。この例では Amazon Bedrock を使用するため、AWS のアクセスキーを環境変数に設定し、AWS の API を実行できるようにしておく必要があります。

promptfoo は npm でインストール可能なので、ターミナルで npm install -g promptfoo を実行してインストールします。その上で promptfoo eval コマンドを実行します。すると並列で回答の生成が走り、ターミナルに内容が表示されます。

promptfoo eval を実行した結果のターミナル出力のスクリーンショット

この画面でもざっくりした生成結果は確認できますが、もっと便利に確認する方法があります。それが promptfoo の Web UI です。promptfoo view コマンドを実行します。

promptfoo view で表示されるブラウザ画面のスクリーンショット

生成結果の評価

LLM の検証とは切っても切れないのが出力の評価です。promptfoo には出力を評価する機能がいくつかあるので、ご紹介します。

手動での評価

最も単純な方法は手動ですべての出力を見て、出力が妥当か判断することでしょう。promptfoo view コマンドを実行してブラウザで結果を表示できることはすでに説明しましたが、その画面で出力結果の評価も行うことができます。

例えば、先ほどの例で Claude 3.5 Sonnet v1 の例が基準を満たしていないと判断したとしましょう。セルをホバーすると右上にいくつかアイコンが表示されます。各セルの「👍️」、「👎️」アイコンをクリックすることで、出力が合格だったか不合格だったかを設定することができます。今回は不合格にしたいので「👎️」をクリックします。

出力を不合格として評価したセルのスクリーンショット。右上に不合格にするアイコンがある。PASS の表示が FAIL に変わっている。

合格か不合格の二択ではなく、もっと詳細にスコアを設定したい場合は、「🔢」 アイコンをクリックすることで、0--1 でスコアをつけることもできます。「✏️」アイコンをクリックするとコメントを追加できるので、評価を決めたポイントなどをメモしておくとよいでしょう。

機械的な評価

少数の出力の評価であれば手動での評価で十分なこともありますが、たくさんのプロンプトやパラメータで検証したい場合は限界が出てくるでしょう。promptfoo は出力結果を機械的に評価する仕組みがあります。

「文字列が含まれているか」や「出力が決まった JSON Schema に則っているか」といった単純なテストはあらかじめ用意されている assertion を使って検証することができます。 www.promptfoo.dev

「トマトを使ったレシピ」の回答に「トマト」という文字列が含まれていないと失敗とみなす、というアサーションがこちらです。

tests:
  - vars:
      ingredient: トマト
    assert:
      - type: contains
        value: トマト

簡単なアサーションのため全部 PASS 扱いになってしまいますが、単純なアサーションで意味のある評価ができるのであれば有用でしょう。アサーションは複数指定できるので JSON の出力を期待するプロンプトでは JSON 形式の出力になっているか判定するアサーションをとりあえず入れておくと良いかもしれません。

単純なアサーションでは対応できないものも、機械的に判定できるものであれば JavaScript や Python のプログラムを使った検証も行えます。

LLM-as-a-Judge

「JavaScript や Python のプログラムを使った検証も行える」という部分で勘の良い方は気づいたかもしれませんが、出力の評価を LLM に任せる、LLM-as-a-Judge も使用できます。

なんと、このためにプログラムを書く必要はなく、promptfoo には組み込みで LLM-as-a-judge 用の assertion が含まれています。

www.promptfoo.dev

この中でも汎用的な LLM Rubric という assertion を使ってみましょう。Assertion の設定にはこの出力を合格とする基準を value として記述します。

tests:
  - vars:
      ingredient: トマト
    assert:
      - type: llm-rubric
        provider: bedrock:anthropic.claude-3-5-sonnet-20240620-v1:0
        value: |
          - 材料に{{ ingredient }}が使われていること
          - 調理方法が簡単であること
          - 材料が 5 つ以下であること
  - vars:
      ingredient: 鶏肉
    assert:
      - type: llm-rubric
        provider: bedrock:anthropic.claude-3-5-sonnet-20240620-v1:0
        value: |
          - 材料に{{ ingredient }}が使われていること
          - 調理方法が簡単であること
          - 材料が 5 つ以下であること

これで評価させた結果がこちらです。

LLM-as-a-Judge の結果。

材料が 5 つ以下という後出しの条件を入れたので、ほとんどの回答が不正解になってしまいました……。とはいえ、定型的な出力でなくとも評価できたり、材料に「鶏もも肉」が含まれている場合に「鶏肉」として判定できたりなど、LLM の持っている知識を利用した評価ができていることが分かるかと思います。

これで理想的には出力の評価まで自動化できるわけですが、判定の精度を出すのが思ったより難しく、実際の評価には使用しませんでした。評価の精度を高めるにはプロンプトエンジニアリングのためのプロンプトエンジニアリングが必要になりそうという印象です。

カスタムのモデルに対応させる

promptfoo は、provider という形でたくさんのモデルに対応しています。単に新しいモデルにリクエストを送信するのにカスタムのモデルを使用したい場面はそう多くないでしょう。

ただし、既存の provider では特殊なパラメータには対応できない場合があるほか、プロダクション環境と完全に同じ条件でモデルにリクエストを送信したい場合は、既存の provider では対応できない場合があります。その場合に使用できるのが、custom provider という機能です。

Custom provider を実装する方法は複数ありますが、JavaScript (TypeScript) や Python で記述する方法が各クラウドプロバイダの SDK を使用できるため便利です。今回は、TypeScript で Amazon Bedrock の Converse API を使用してリクエストを送信する provider を実装してみたいと思います。(promptfoo は Amazon Bedrock には対応していますが、API の呼び出しに別の API を使用しています。)

import {BedrockRuntimeClient, ConverseCommand} from '@aws-sdk/client-bedrock-runtime';
import {ProviderResponse} from 'promptfoo';

module.exports = class BedrockConverse {
  private providerId: string;
  private client: BedrockRuntimeClient;
  private modelId: string;

  constructor(options: {id?: string, config: Record<string, any>}) {
    this.providerId = options.id || 'bedrock-converse';
    this.modelId = options.config.modelId;

    this.client = new BedrockRuntimeClient({region: options.config.region ?? 'us-west-2'});
  }

  id(): string {
    return this.providerId;
  }

  async callApi(prompt: string): Promise<ProviderResponse> {
    const response = await this.client.send(new ConverseCommand({
      modelId: this.modelId,
      messages: [
        {
          role: 'user',
          content: [
            {
              text: prompt
            }
          ]
        }
      ],
    }));

    const providerResponse: ProviderResponse = {
      output: response.output!.message!.content![0]!.text!,
      tokenUsage: {
        total: response.usage?.totalTokens,
        prompt: response.usage?.inputTokens,
        completion: response.usage?.outputTokens
      }
    }

    return providerResponse;
  }
};

プロンプトのパラメータを置換した状態で callApi が呼び出されます。中で最低限やることは、それをもとに API を実行し、結果を決まった型のオブジェクトで返却するだけです。

これを bedrock-converse.ts として保存した上で、promptfooconfig.yaml の provider を以下のように設定します。

providers:
  - id: file://bedrock-converse.ts
    label: Claude 3.5 Sonnet
    config:
      modelId: anthropic.claude-3-5-sonnet-20240620-v1:0
  - id: file://bedrock-converse.ts
    label: Claude 3.5 Sonnet
    config:
      modelId: anthropic.claude-3-5-sonnet-20241022-v2:0

これで通常通り promptfoo eval すれば、カスタムの provider を使用した生成を行えます。生成結果はほぼ同等なので省略します。

おわりに

この記事では promptfoo を使用した LLM の生成結果の評価について説明しました。使い方は簡単ながら柔軟に様々な要件に対応できるので、使用できる場面は多いのではないかと思います。

最後に、kintone チームでは生成 AI を使用した機能開発に取り組む仲間を募集しています。

cybozu.co.jp

エントリーをお待ちしています!