🔖

promptfooの色々な機能を試す

2024/04/30に公開

promptfooはプロンプトの評価をしていくうえで便利そうでしたが、サンプルのコードでは自分のニーズにマッチするか判断がつきませんでした。色々と試してみたので備忘録としてまとめておきます。

基本

下記のようなyamlファイルで実験を記述していきます。

promptfooconfig.yaml
description: 'My first eval'

prompts:
  - "Write a tweet about {{topic}}"
  - "Write a very concise, funny tweet about {{topic}}"

providers:
  - openai:gpt-3.5-turbo-0613
  - openai:gpt-4

tests:
  - vars:
      topic: bananas

  - vars:
      topic: avocado toast
    assert:
      # For more information on assertions, see https://promptfoo.dev/docs/configuration/expected-outputs
      - type: icontains
        value: avocado
      - type: javascript
        value: 1 / (output.length + 1)  # prefer shorter outputs

  - vars:
      topic: new york city
    assert:
      # For more information on model-graded evals, see https://promptfoo.dev/docs/configuration/expected-outputs/model-graded
      - type: llm-rubric
        value: ensure that the output is funny

Provider

カスタムProvider

ClaudeをVertex AI経由で利用したいケースは下記のようにProviderでPythonファイルを指定します。

providers:
  - id: 'python:models/vertex_claude.py'
    config:
      pythonExecutable: "//path/to/python"

Pythonファイルは下記のように書きます。 call_api 関数がエンドポイントになります。

model/vertex_claude.py
import anthropic

GCP_PROJECT = "your-gcp-project"
HAIKU = "claude-3-haiku@20240307"


def get_client():
    return anthropic.AnthropicVertex(region="us-central1", project_id=GCP_PROJECT)


def call_api(prompt, options, context):
    output, usage = call_llm(prompt, HAIKU)

    result = {
        "output": output,
        "tokenUsage": {
            "total": usage.input_tokens + usage.output_tokens,
            "prompt": usage.input_tokens,
            "completion": usage.output_tokens,
        },
    }

    return result


def call_llm(prompt: str, model: str) -> tuple[str, anthropic.types.Usage]:
    client = get_client()

    message = client.messages.create(
        model=model,
        max_tokens=4096,
        temperature=0.1,
        messages=[
            {"role": "user", "content": prompt},
        ],
    )

    return message.content[0].text, message.usage

LLM Chainを評価する場合の選択肢の1つでもあります。

詳細:

パラメータをチューニングする

Claudeのモデルを変えたり、temperatureやtop_k, top_pをチューニングしたい場合はconfig経由でパラメータを渡せます。ただしlabelを付与しないと、どのProviderで実行した結果なのか見分けがつかなくなるので、合わせて付与しましょう。

providers:
  - id: 'python:models/vertex_claude.py'
    label: 'Haiku temperature 0.1'
    config:
      model: 'haiku'
      temperature: 0.1
      pythonExecutable: "//path/to/python"
  - id: 'python:models/vertex_claude.py'
    label: 'Sonnet temperature 0.1'
    config:
      model: 'sonnet'
      temperature: 0.1
      pythonExecutable: "//path/to/python"

configで指定したパラメータはoptionsに入っています。

model/vertex_claude.py
...

MODELS = {"haiku": "claude-3-haiku@20240307", "sonnet": "claude-3-sonnet@20240229"}


def call_api(prompt, options, context):
    config = options.get("config", None)

    model_name = config.get("model", "haiku")
    model = MODELS[model_name]

    temperature = config.get("temperature", 0.0)

    output, usage = call_llm(prompt, temperature, model)

    result = {
        "output": output,
        "tokenUsage": {
            "total": usage.input_tokens + usage.output_tokens,
            "prompt": usage.input_tokens,
            "completion": usage.output_tokens,
        },
    }

    return result

...

詳細:

Prompts

promptを別ファイルで指定する

プロンプトエンジニアリングで試行錯誤する際には、システムプロンプトを指定したり、few-shotのためにユーザープロンプトを作り込むことがあります。その際に構造がわかりやすいようにJSONファイルに切り出したくなります。その場合は1ファイル、もしくは複数ファイルに切り出すこともできます。

prompts:
  - file://prompt.json
prompt.json
[
  {
    "role": "system",
    "content": "あなたは翻訳のプロフェッショナルです。入力には日本語か英語を与えます。英語の場合は日本語に変換し、日本語の場合は英語に翻訳してください。"
  },
  {
    "role": "user",
    "content": "{{plain_text}}"
  }
]

気をつけるべき点として、カスタムProviderとともにを利用する場合は、きちんとAPIリファレンスに沿って設定しないと失敗します。例えばClaudeのMessage APIを使う場合、ユーザープロンプトのroleはuserかassistantのみで、systemを指定すると失敗します。Anthropicのサンプルを見るとシステムプロンプトを設定していますが、これは裏側でうまくシステムプロンプトを分離してAPIに合わせているようです。

JSONファイルを利用すると、実態としては第一引数に文字列として渡ってきます。そこのため、parseしてよしなに変換すると解決できます(無理矢理感があり、ちょっと読みにくくなりますが...)。

def call_api(prompt_json: str, options, context):
    ...

    _prompts = json.loads(prompt_json)
    system_prompt, user_prompt = _prompts[0]["content"], list(_prompts[1])
    output, usage = call_llm(system_prompt, user_prompt, temperature, model)
    ...

詳細:

Tests file

testを別ファイルで指定する

長文要約を実行したい、長文から情報抽出したいなど、テストケースが長文になる場合、promptfooconfig.yamlから切り出したくなります。その場合は1ファイル、もしくは複数ファイルに切り出すこともできます。またCSVも利用できます。

tests:
  - test.yml

詳細:

model-assisted eval metricsをカスタムProviderで実行する

LLMの出力をLLMで評価したいケースがあります。promptfooではこれをmodel-assisted eval metrics、評価を行うLLMをgraderと呼んでいます。graderのデフォルトはOpenAIのGPT-4です。ここをカスタムProviderで実行する場合は下記のように指定します。

defaultTest:
  options:
    provider: 'python:vertex_claude.py'
    config:
      model: 'haiku'
      temperature: 0.0
      pythonExecutable: "//path/to/python"

tests:
  - description: Use LLM to evaluate output
    assert:
      - type: llm-rubric
        value: Is spoken like a pirate

他にもCLIで指定する方法と、テストケースごとにproviderを指定する方法があります。ニーズに応じて設定します。

詳細:

画像ファイルをテストする

Providerによって設定が異なりますが、基本的にはURLだったりbase64を指定します。LLMのAPIリファレンスを読むのが間違いがなく、そこから逆算してテストケースを作成します。

Gemini 1.0 Proの場合はURLにGCSのパスが利用できるので、今回はそちらでやってみます。ついでにtemperatureによる出力の違いも確認してみました。

promptfooconfig.yaml
description: 'My first eval for Gemini Pro vision'

prompts:
  - "file://prompt.json"

providers:
  - id: vertex:gemini-pro-vision
    label: 'Gemini Pro temperature 0.1'
    config:
      generationConfig:
        temperature: 0.1
  - id: vertex:gemini-pro-vision
    label: 'Gemini Pro temperature 1.0'
    config:
      generationConfig:
        temperature: 1.0

tests:
  - vars:
      uri: "gs://xxx/yyy/zzz/sample.jpg"
prompt.json
[
  {
    "role": "user",
    "parts": [
      { "text": "What is this image?" },
      {
        "fileData": {
          "mimeType": "image/jpeg",
          "fileUri": "{{uri}}"
        }
      }
    ]
  }
]

詳細:

promptfooconfig.yaml

実行時にpromptfooconfig.yamlを指定する

複数人で開発したり、複数プロンプトを評価したい場合、1つのpromptfooconfig.yamlを使い回すのではなく、新しいファイルを作りたいケースもあります。その場合はCLIの -c オプションでyamlファイルを指定できます。

npx promptfoo@latest eval -c experiments/config1.yaml

詳細:

Discussion