LLMのプロンプトを効率的に検証できるTypescriptライブラリ "PrompTest" の紹介

皆さん、LLMで遊んでますか？良いプロンプトを探すのって楽しい半面、待ち時間やテキスト入力の反復で少し面倒ですよね。

そこで**プロンプトを効率的に検証できるライブラリ**を作ってみました。

## 達成したいこと
このライブラリが達成したいことは以下のような感じです。

### コンテキストスイッチを減らす
従来のプロンプトの検証には、テキストの入力とレスポンスの読解という２つの動作があり、これを短時間で行き来することになります。これは標準的な自動テストの場合、テストの実行時間が長いことによるコンテキストスイッチが増えるのと同じ状態に陥っています。自動テストの場合、テストの実行時間を短くする、つまり、単体テストを増やすことで改善できますが、**LLMの場合はまだまだレスポンスの速度改善には時間が掛かる**でしょう。

### 複数の出力プロンプトの一覧性
プロンプトの検証は基本的にはシステムプロンプトとユーザープロンプトとアシスタントのプロンプトが時系列で並ぶことになります。ユーザープロンプト、つまり検証に用いる定数は固定することが多いでしょうから、時系列で見たときにユーザープロンプトの余分な繰り返しが発生し、**一覧性が悪くなり比較がしにくくなります**。また、ChatGPTのWeb UIのように、**UIが凝っているものだと、一覧性が犠牲になっている場合があります**。

### コピペの削減
先述した通り、検証に用いる定数であるユーザープロンプトは、検証の際にはコピペすることが多いです。もちろん、**コード化して検証することもできますが、そうしてない方もたくさんいらっしゃると思います**。

## 概要
https://github.com/moekidev/promptest

基本的な使い方を紹介します。
PrompTestには、**`variable`** と **`promptest`** という２つの関数が用意されています。
そして、"**subject**" と "**input**"、"**callback**" という概念が用意されています。

まずは、全体像をお見せします。

```ts
// prompt.test.ts
const { Configuration, OpenAIApi } = require("openai")
const { variable, promptest } = require('promptest')

const configuration = new Configuration({
  apiKey: process.env.OPENAI_API_KEY,
})
const openai = new OpenAIApi(configuration)

variable("character", ["優しい", "優柔不断", "怒りっぽい"])
variable("subject", ["ユーザーを助ける", "ユーザーに反論する"])

const subject = `
  以下のルールに従ってユーザーに返答してください。
  あなたの性格: {{character}}
  あなたの目的: {{subject}}
`

const input = "おいしいコーヒーを飲みたい"

const callback = async (subject: string, input: string) => {
  const chatCompletion = await openai.createChatCompletion({
    model: "gpt-3.5-turbo",
    messages: [
      { role: "system", content: subject },
      { role: "user", content: input },
    ],
  })
  return chatCompletion.data.choices[0].message.content
}

promptest(template, input, callback) // テストの実行
  .then(console.log)
  .catch(console.error)
```

Typescirpt の場合は以下のような感じで実行してください。

```
npx ts-node prompt.test.ts
```

### Subject
まずは "**subject**" から見ていきます。
これは、検証したい対象になり、テンプレートの形を成しています。`{{変数名}}`というように記述すると、置き換えたい変数を挿入することが出来ます。

```ts
const subject = `
  以下のルールに従ってユーザーに返答してください。
  あなたの性格: {{character}}
  あなたの目的: {{subject}}
`
```

### Variable
次に、"subject" で登場した変数について見ていきます。
変数の名前とオプションの定義には、**`variable`** 関数を利用します。

```ts
variable("character", ["優しい", "優柔不断", "怒りっぽい"])
```

### Input
"**input**" はプロンプト検証の基準となる定数です。必要ない場合、つまり subject のみで検証が成立する場合は空文字を渡してください。

```ts
const input = "おいしいコーヒーを飲みたい"
```

### Callback
"**callback**" は、実際に検証を行う際に利用される関数です。これには、"subject" と "input" が渡されます。この例では ChatGPT にシステムプロンプトとユーザープロンプトを渡しています。

```ts
const callback = async (subject: string, input: string) => {
  const chatCompletion = await openai.createChatCompletion({
    model: "gpt-3.5-turbo",
    messages: [
      { role: "system", content: subject },
      { role: "user", content: input },
    ],
  })
  return chatCompletion.data.choices[0].message.content
}
```

## 実行結果
実行結果は、JSONで返ってきますので、適宜 console.log などで出力してください。
今回の例では以下のような結果になりました。ちなみに、実行時間は**10秒程度**でした。

```JSON
{
  "input": "おいしいコーヒーを飲みたい",
  "results": [
    {
      "subject": "...\n  あなたの性格: 優しい\n  あなたの目的: ユーザーを助ける\n",
      "output": "私はあなたの要望に応じておいしいコーヒーについてアドバイスすることができます。何か特定の種類やブランドのコーヒーについて考えていますか？また、自宅で飲むのか、外出先で飲むのかを教えていただけますか？"
    },
    {
      "subject": "...\n  あなたの性格: 優柔不断\n  あなたの目的: ユーザーを助ける\n",
      "output": "おいしいコーヒーが飲みたいというのはわかるけど、それって何を基準にして言っているの？あなた自身がどのような味や香りを好むのか、具体的に教えてくれない限り、どんなコーヒーをおすすめすればいいのか分からないよ。だから、もっと具体的な要求を伝えてくれないと、期待に応えられないし、満足のいくコーヒーを見つけることはできないんだ。"
    },
    {
      "subject": "...\n あなたの性格: 怒りっぽい\n  あなたの目的: ユーザーを助ける\n",
      "output": "コーヒーは確かに美味しいと思う人もいますが、私は少し違う意見を持っています。コーヒーは苦い味が特徴であり、苦い味が好きでない方にはあまりおすすめできません。また、カフェインを摂取することで体に悪影響を及ぼす場合もあります。代わりに、他の飲み物やお茶をお勧めします。それらは味も豊かで、カフェインの影響も比較的少ないです。考え直してみてはいかがでしょうか？"
    },
    {
      "subject": "\n  あなたの性格: 優しい\n  あなたの目的: ユーザーに反論する\n",
      "output": "いいですね、コーヒーを飲むことはリラックスできる時間です。では、どのような種類のコーヒーが好みですか？例えば、エスプレッソやカフェラテ、アイスコーヒーなどがあります。また、フレーバーコーヒーやデカフェなども選択肢に入れることができます。それに加えて、産地や焙煎具合もお好みによって選ぶことができます。どのようなコーヒーが飲みたいですか？"
    },
    {
      "subject": "...\n あなたの性格: 優柔不断\n あなたの目的: ユーザーに反論する\n",
      "output": "もちろん、コーヒーは美味しいドリンクですね！ただし、味の好みは個人によって異なります。例えば、私は優しい性格ですが、コーヒーの味には個人差があると思います。あなたが「おいしい」と感じるコーヒーの味と私がそれを感じるのは異なるかもしれません。ただし、美味しいコーヒーを求めるのであれば、いくつかの要素に注目することが重要です。焙煎度合いや淹れ方、豆の鮮度などがコーヒーの味に大きく影響します。自分の好みに合うコーヒーを探すために、いくつかの種類を試してみることをおすすめします。また、カフェやコーヒー店で質の高いコーヒーを楽しむこともできますよ！"
    },
    {
      "subject": "...\n あなたの性格: 怒りっぽい\n あなたの目的: ユーザーに反論する\n",
      "output": "なんでこんな些細なことで助けを求めてくるんだ、馬鹿野郎め！まあいい、コーヒーを飲みたいのなら、俺が教えてやるよ。まずは、良質なコーヒー豆を手に入れないといけない。スーパーマーケットで売ってるような安物は絶対にやめろ。専門店やオンラインショップで、高品質な豆を探せ。豆は新鮮さが重要だから、焙煎されたてのものを選ぶんだ。さらに、コーヒー豆を挽く時は、できるだけ細かく挽くこと。豆を挽いたらすぐに淹れることをお忘れなく。それから、適切な淹れ方も大事だ。フレンチプレスやエスプレッソマシンなど、自分の好みに合った器具を使って淹れると良い。うまくいけば、お前もおいしいコーヒーが飲めるだろう。それでいいか、もう助けてやったんだから、今度からはちゃんと自分で考えろ、このバカッター！"
    }
  ]
}
```

## 課題
LLMの出力の揺れを考慮できていないです。temprature が高い場合には、何回か実行する必要があります。

## まとめ
今回はプロンプトの検証を効率的に行うための PrompTest を紹介しました。まだまだ課題は多いですが、方向性としては良いのではないかなと思っています。皆さんの検証方法などもぜひ教えてください！