AIサービスを作成する上で効果のあったプロンプトエンジニアリング

## はじめに

AI製品の開発のために大規模言語モデルやそれをどう開発するかの勉強をはじめました。
実際にSemanticKernalを用いて、AIサービスを作成してみますが、プロンプトエンジニアリングも勉強しないとなかなか期待する回答が得られないと感じるため試して効果のありそうなプロンプト指示を備忘録も兼ねて記事にしておきます。

基本的に、プロンプトテンプレートとFunction Callingから呼ばれるサービス内の振る舞いを開発し、ユーザ入力が曖昧でも期待する振る舞いを達成させるという方針がベースです。

## 参考

GPTと壁打ちして改善していくこともやっていますが、以下のような基本を抑えるのも重要だと実感します。

・[OpenAI社が推奨するプロンプトのベストプラクティス8つ](https://help.openai.com/en/articles/6654000-best-practices-for-prompt-engineering-with-the-openai-api)

・[各種Prompt Engineeringの日本語実例集（Zero-CoT、mock、ReAct、ToT、Metacog、Step Back、IEPなど）](https://qiita.com/YutaroOgawa2/items/aca32f8fd7d551596cf8)

## 効果のあった手法

### Use the latest model（手法と関係ないですがOpenAIのプラクティスでもあるので）

最新のモデルを利用とありますが、当然利用するモデルで、性能はまったく違います。
GPT-4o-miniでは、なかなか期待する回答をしない場合もGPT-4oでは安定した回答をするなど多いです。
しかし、使用量の観点から高性能なモデルを利用して良い結果が得られても市販化するAIサービスとしては、あまり意味はないため、意図的に軽量モデルを使用し、軽量モデルでも安定した回答が得られるようにしていくべきです。

### 希望する文脈、結果、長さ、形式、スタイルなどについて、具体的で、説明的で、できるだけ詳細に記述

ここで、どれくらい具体的に書くかというのが難しいと思いますが、例えばGitHubCopilotでコードの補助をお願いすると、ソフトウェア開発者という役割定義から始まり60行くらいびっしり、選択したスラッシュコマンドの関数のルールや選んだ場合の結果、出力サンプルなどを沿えてLLMに送信しています。
実際に私も当初は指示5行、出力仕様5行くらいのプロンプトテンプレートでなかなかうまくいかないなと悩んでいましたが、より詳細に事細かに示すことで精度が上がっていったのを実感しました。
「具体的する」なんて一般的に言われますが、どれくらいまで書いていいかというと思っていた5倍くらいは詳細に書いても大丈夫です。

### プロンプトの先頭に指示を配置し、### または """ を使用して指示とコンテキストを区切ります

単純ですが、文章の区切りの認識精度は思ったより悪いです。明確に分けるよう見出しといった部分はプロンプトに意図的に含めていくと入力の認識精度は高まります。
区切りというのは、見出しなど以外にもつけることができ、次のロールでも説明します。

### LLMに送信する際のメッセージのロール

OpenAIのAPIなどを使わないと関係ないかもしれませんが、プロンプト入力含め、LLMにどういったroleでメッセージを送るのか、同じroleでもメッセージとしては分けて送ったほうが認識精度は高まると経験則ですが感じます。

```
 "messages": [
    {
      "role": "system",
      "content": "# 指示 ~~~"
    },
    {
      "role": "user",
      "content": "モデルのデータ構造は ~~~"
    },
    {
      "role": "user",
      "content": "AAAモデルとはなんですか？"
    },
```

role指定できずともrole:userでメッセージの内部で、疑似的にさらにroleをつけて、情報を渡すといった手法も見受けられるので、APIでrole指定が難しい場合でもなんとか精度を上げることはできそうです。
しかし、私が試した限りでは、疑似的なroleよりも明確に分けたほうが、認識精度や出力の安定性が良い結果でした。

```
 "messages": [
    {
      "role": "user",
      "content": "
                "#role": "system"
                "content": "# 指示 ~~~"
                
                "role": "user",
                "content": "モデルのデータ構造は ~~~"
                
                "role": "user",
                "content": "AAAモデルとはなんですか？"
                "
    }
```

`system`,`user`,`assistant`はAPI説明にもあるので、一般的かもしれませんが、通信を確認すると、Function Callingは`tool`でこれは`assistant`と同じくらい強力に回答に影響すると感じます。
`system`,`user`の違いは正直あまり体感できません。対話を続けていくと`user`はすぐに揮発する気もしますが、結局`system`も長い対話が続くと揮発する感じがあります。とはいえワンショット的なプロンプトでも、分ける効果があると考えており、`system`のほうが`user`よりも優先度が高い点を活かし、`system`でプロンプトの方針を送信し、`user`も分けて、ユーザ入力とその処理で活用するドメイン知識を送ると、1つのメッセージにまとめた場合より、体感できるレベルで認識精度高く回答してくれます。

また、SemanticKernelだからなのかもしれませんが、Function Calling(SemanticKernel的にはKernelFunction)は、メッセージとは別の"tools"として送信しているようなので、独自のコマンド定義を考えてメッセージで送るより、関数を正しく呼び出す認識精度が高いのはここが要因なのかもしれません。

### 出力形式を明確にする

見出しの件と合わせてプロンプトテンプレートに期待する出力を明確化しておくのもトークン消費少なく、効果が高い手法です。

```
## 出力仕様
* 出力は、次の形式で出力してください
* {設立年} {事業内容} {会社本拠地住所} {資本金} 
```

### Few-shot Learning (類似して推論例だったり、実行例などを示す手法全般)

プロンプトにいくつか実際の質問と回答例のペアであったり、LLMどのような推論や実行をさせるサンプルを記載する手法です。
サンプルをベースとすることで、期待する方向の回答精度が高まりますが、注意点もあります。

ワンショットの結果にはかなり寄与しますが、それ以降の対話には効果が薄いですし、サンプル数を増やしても性能(精度向上や出力安定)もそこまで上がっていかないです。
また、サンプルに引っ張られ、聞いてもいないのにサンプルの内容で回答したりと扱いに注意する点があります。
プロンプトに単純に指示を追加し、「サンプルの内容はそのまま利用せず、設計情報から適切な値を利用して」などでもある程度抑制できると思いますが、時々サンプルの結果で回答するのをどう許容するかは難しいところです。

### KernelFunctionの関数名をプロンプトに書いてしまう

SemanticKernelで実装したKernelFunctionですが、Descriptionの内容でいい感じに関係する場合のみ関数を呼び出して便利なのですが、作成する機能によっては、KernelFunctionをほぼ確実に実行してほしい場合があると思います。
LLMがどう解釈するかにもよるので、絶対とは言えませんが、プロンプトに「モデルを追加する場合は"ABCApp_AddModel"を実行してください」などKernelFunction属性で定義している関数名を明示することで実行精度を上げることができます。

## さいごに

プロンプトエンジニアリングにかかわる部分だけ挙げてみましたが、実際のAIサービスの開発では、プロンプト以外にもKernelFunctionの定義やユーザ入力を補正するあるいは誘導し、動作する入力を与えることも非常に回答精度にかかわってきます。
あんまり業務のことを書くのは難しいですが、可能なら設計なども今後記事にしていければと思います。