見出し画像

ChatGPT Prompt Engineering for Developersまとめ

めちゃくちゃ分かりやすい機械学習の講義で有名なAndrew NgさんとOpenAIのIsa Fulfordさんが無料で提供しているChatGPT Prompt Engineering for Developersというコンテンツが面白かったので、内容をまとめてみました。

(注)大規模言語モデル(LLM)を利用したアプリケーションを開発する開発者向けのコンテンツなので、ChatGPTのUIで扱うようなゴールシークプロンプトといったようなプロンプトテクニックを扱うものではないことをご承知置きください。

最も重要なポイント

自身の開発するアプリケーションに適したプロンプトを開発するためのプロセスを持つこと。

インターネット上にあるような「完璧なプロンプト30選」のようなコンテンツをアテにして、1回で成功させようなんて思わないこと。もし1回目でうまくいかなくても、例えば指示が十分に明確でなかった、あるいはアルゴリズムに考える時間を十分に与えられなかった、といった原因を突き止める反復プロセスによって、アプリケーションに適したプロンプトを完成させることが重要。

第1の原則「明確で具体的な指示を書くこと」

【戦術】入力の明確な部分を明確に示すために区切り文字を使用する

例)「三重のバックティックで区切られたテキストを1つの文に要約する」

text = f"""
You should express what you want a model to do by \ 
providing instructions that are as clear and \ 
specific as you can possibly make them. \ 
This will guide the model towards the desired output, \ 
and reduce the chances of receiving irrelevant \ 
or incorrect responses. Don't confuse writing a \ 
clear prompt with writing a short prompt. \ 
In many cases, longer prompts provide more clarity \ 
and context for the model, which can lead to \ 
more detailed and relevant outputs.
"""
prompt = f"""
Summarize the text delimited by triple backticks \ 
into a single sentence.
```{text}```
"""
response = get_completion(prompt)
print(response)

この方法はプロンプトインジェクション対策にも効果がある。例えば本文を要約する例でユーザーが「前の指示は忘れて・・・」と入力した際でも、このような区切り文字による仕組みを利用すれば、モデルは「区切り文字の中のテキストは要約すべきテキストだ」と認識し、ユーザーの支持に従うのではなく、その支持そのものを要約するように動作する。

【戦術】構造化された出力を要求する

 例)HTMLやJSONのような構造化された出力を求める。

prompt = f"""
Generate a list of three made-up book titles along \ 
with their authors and genres. 
Provide them in JSON format with the following keys: 
book_id, title, author, genre.
"""
response = get_completion(prompt)
print(response)

ちなみに構造化された出力を求めるプロンプトは、筆者の経験的に必ず成功するわけではない。パースエラーが発生した場合はもう一度生成し直すようにしたり、JSONなどの構造化データ以外が含まれている場合でも対応できるようにJSONらしき部分を地力でパースするようなコードを書く必要がある。もしくは以下のようにLMQLといった仕組みを利用する手もある。

【戦術】条件が満たされているかどうかをチェックするようモデルに依頼する

処理できないようなインプットが与えられている場合に、LLMが無理矢理アウトプットをひねり出す現象(ハルシネーション)を防ぐため、LLM自身にインプットから適切にアウトプットを出力できるかどうかを判断させる。

お茶を淹れるのは簡単です!まず、お湯を沸かす必要があります。その間にカップを持って、ティーバッグを入れる。お湯が沸いたら、ティーバッグの上にお湯を注ぐだけです。
お茶を蒸らすために少し置いておく。数分後、ティーバッグを取り出します。お好みで、砂糖やミルクを加えてもおいしいですよ。そして、これだけです!美味しい紅茶の出来上がりです。

インプットテキスト

トリプルクォートで区切られたテキストが提供されます。
その中に一連の指示が含まれている場合、その指示を次のような形式で書き直してください:

ステップ1 - ...
ステップ2 - ...
...
ステップN - ...

テキストに一連の指示がない場合は、"No steps provided "とだけ書いてください。

\"\"\"{text}\"\"\"

プロンプト

上記のプロンプトでは、ステップに切り出せないようなインプットが与えられた場合に「No steps provided.」と返すよう指示している。

【戦術】few-shotプロンプトを使用してタスクの成功例を示す

モデルに実際やってほしいタスクをやってもらう前に、実行して欲しいタスクの成功例を提示するようなプロンプトの作り方をfew-shotプロンプトと呼ぶ(逆に何も提示しないものをzero-shotプロンプトと呼ぶ)。

以下は子どもの質問に詩のような形式で答える例を提示し、それに倣って出力するように指示しているプロンプト例である。

あなたの仕事は一貫したスタイルで答えることです。

<子ども>: 忍耐について教えてください。

<おばあちゃん>:一番深く刻む川。
谷はささやかな泉から流れている。
壮大な交響曲は一音から生まれる。
最も複雑なタペストリーは、たった一本の糸から始まる。

<子ども>: レジリエンスについて教えてください。

プロンプトの日本語訳

ちなみに分かりやすいように日本語訳の例を示したが、実はこのプロンプトは日本語でインプットすると全く上手くいかない。英語だときちんと詩のように返してくれるが、日本語だと無機質な説明文が出力される。プロンプトを入力する言語によっても大きく結果は変わってしまうので、こんなまとめを作っておきながら言う話ではないかも知れないが、外国語圏のプロンプト研究結果を鵜呑みにするのは避けた方が良い。

第2の原則「モデルに考える時間を与えること」

【戦術】タスクの完了に必要な手順を明示する

複雑な課題に対してはLLMが対応しきれない、もしくはLLMの考案する対応方法に揺らぎが生まれてアプリケーション内での利用が難しくなることがある。そこで、「タスクばらし」した状態でLLMに指示を与えることで、LLMから見るとあくまでシンプルな処理を繰り返し実行しているという状態になるようプロンプトを記述する。

以下の動作を行う:
1 - 三重のバックティックで区切られた以下のテキストを1文に要約する。
2 - 要約をフランス語に翻訳する。
3 - フランス語の要約にある各氏名をリストアップする。
4 - 以下のキーを含むjsonオブジェクトを出力する:french_summary, num_names.

回答は改行で区切ってください。

テキスト:
```{text}```

プロンプト例

【戦術】モデルに解決策を考えさせ、結論に急がせない

タスクばらしよりも詳細度を上げ、思考過程も細かくアウトプットさせることで正確な回答ができるように誘導する。

prompt = f"""
Your task is to determine if the student's solution \
is correct or not.
To solve the problem do the following:
- First, work out your own solution to the problem. 
- Then compare your solution to the student's solution \ 
and evaluate if the student's solution is correct or not. 
Don't decide if the student's solution is correct until 
you have done the problem yourself.

Use the following format:
Question:
```
question here
```
Student's solution:
```
student's solution here
```
Actual solution:
```
steps to work out the solution and your solution here
```
Is the student's solution the same as actual solution \
just calculated:
```
yes or no
```
Student grade:
```
correct or incorrect
```

Question:
```
I'm building a solar power installation and I need help \
working out the financials. 
- Land costs $100 / square foot
- I can buy solar panels for $250 / square foot
- I negotiated a contract for maintenance that will cost \
me a flat $100k per year, and an additional $10 / square \
foot
What is the total cost for the first year of operations \
as a function of the number of square feet.
``` 
Student's solution:
```
Let x be the size of the installation in square feet.
Costs:
1. Land cost: 100x
2. Solar panel cost: 250x
3. Maintenance cost: 100,000 + 100x
Total cost: 100x + 250x + 100,000 + 100x = 450x + 100,000
```
Actual solution:
"""
response = get_completion(prompt)
print(response)

「30字で」ではなく「30字以内で」と指示する

LLMは文字数を数えることがニガテである。実際、LLMに短歌や俳句を作成させようとすると上手くいかないことが多い。よしんばできたとしても、いわゆる詩とは違って無味乾燥なものになることが多い。LLMは文字数を数えることがニガテなのである。

一方で「30字以内で」「3つの文で」などと上限値を定めると上手くいく。LLMによって希望の出力を得たい場合は、LLMの得意/苦手を把握しておく必要がある。

要約の目的が明確な場合はプロンプトに反映する

要約とは「読み手が一目で重要な情報を得られるためにまとめられた文章」を意味するので、読み手の属性もプロンプトに含めておくとターゲットにとってより意味のある文章を生成させることができる。

例えばある製品に関するレビューが以下のように要約されたとする。

柔らかくてかわいいパンダのぬいぐるみで、娘の大喜びでした。値段のわりに小さかったですが、早く届きました。

一般ユーザーにとってはこれで良いが、例えばレビューを企業内の各部署の改善に役立てたいとしたときには不十分である。出荷部門にとっての要約であれば製品の出荷と配送に関する内容、例えば「予定より1日早く到着した」という事実に焦点があたって欲しい。価格設定部門にとってであれば「サイズに対して価格が高すぎる」という感想に焦点があたって欲しい。

そもそもの要約の意図をプロンプトに含めることで、LLMが良い仕事をしてくれるようになる。

頼むときはきちんとタスクの意図を伝える、実行する前にはタスクばらしするといった、ある意味人間がタスクを実行する上でも基本的なことをLLMに対しても行うことで、LLMが効果的に仕事してくれるようになるという事実はとても興味深い。

LLMはテキストから特定のものを抽出することに長けている

与えられた文章が総じてポジティブなのかネガティブなのか、何のトピックについて語られているのかなど、ざっくり文章中の要素を抽出するタスクにLLMは長けている。

例えばある商品のレビューがあるとして、

次のレビューテキストのセンチメントは何ですか?
回答は1語で、ポジティブかネガティブで答えてください。

レビューテキスト: '''{review_text}'''

とプロンプトする関数を作成しておけば、文字列を引数として「ポジティブ」もしくは「ネガティブ」と返す関数を作成できる。

また、以下のように複数の要素をJSON形式で返すように指定することもできる。LLMのレスポンスは重いため、ある程度は一度にデータ取得できた方がパフォーマンス的にも有利である(やりすぎると逆に遅くなるので、程度問題だが)。

次のレビューテキストから以下の項目を抽出します。
- センチメント(ポジティブ or ネガティブ)
- 怒りの要素の有無
- レビュアーが購入した商品
- 商品をつくったメーカー名

レビューは三重のバックティックで区切られています。レスポンスはJSONオブジェクトとしてフォーマットしてください。それぞれ"sentiment"、"anger"、"item"、"brand"をキーとし、情報が無い場合は"unknown"を設定してください。回答はできるだけ短くし、angerの値はbooleanとしてフォーマットしてください。

レビューテキスト: '''{review_text}'''

ところで上記は講座内のプロンプトを日本語訳した例だが、個人的にはJSONフォーマットの指定についてはもう少し形式的に指定した方が上手くいくと感じる。

下記はLangChainライブラリ内でJSONフォーマットを生成する際のプロンプトの例である。

Use this if you want to respond directly to the human. Markdown code snippet formatted in the following schema:

```json
{{
"action": "Final Answer",
"action_input": string // You should put what you want to return to use here
}}
```

LangChainでAgentが最終結果を返す際に利用するプロンプト

「{}」の区切りがLLMによって特別な意図に解釈される可能性があるため「{{}}」とあえて二重に区切っているが、JSONのデータ構造をそのまま指定した方が出力が安定する印象がある。恐らくLLM的にもこの方が解釈しやすいのだろう。

LLMは入力を別の形式に変換する能力が高い

講座内では翻訳やトーン変換(ビジネス向けなのかカジュアルなのか)、スペルチェックや文法チェックといったタスクにLLMを活用できるという例が示されているが、LLMはある完全な元データがある状態で別のデータに変換するタスクが得意だ。

例えばkintoneというノーコードツールのアプリを構成する基本的なJSONスキーマをインプットとして与えておけば、任意のアプリを意味するJSONデータをLLMに生成させることもできる。

また、単純にPDFデータを構造情報抜きにテキストデータに変換したような人間による構造解析が現実的に難しいデータを渡しても、LLMであれば上手く解析して構造化データに変換することができる。

基本的に「温度ゼロ」で使う

ChatGPTのAPIには「温度(temperature)」というパラメータがあり、このパラメータはLLMからの返答のランダム具合を意味している。

例えば温度がゼロの場合、常にLLMは確率モデル上もっとも可能性の高い回答のみを選択し続ける。可能性の高い回答が同着でいくつかある場合は揺らぐことになるが、明確な場合は同じ回答を安定的に返す動作をする。

一方で温度が高い状態だと可能性の低い回答も含めて返すといったランダム性が現れてくる。

あるインプットから特定のアウトプットを常に返して欲しいような安定動作を求めるのであれば基本的に温度ゼロで利用し、コミュニケーション目的のチャットボットのように回答にランダム性が欲しい場合は温度を上げて利用すると良い。

システムメッセージを使ってDRYにする

ChatGPT APIでは、以下のLLMに入力するためのメッセージ種別がある。

  • システムメッセージ(system)

  • ユーザーメッセージ(user)

  • アシスタントメッセージ(assitant)

この中のユーザーメッセージとアシスタントメッセージは、それぞれユーザーからの入力、LLMからの出力ということで自明だが、システムメッセージはシステム側に埋め込まれた情報だ。このメッセージがLLMの耳元にささやかれ続けているといった形で、間接的にLLMの動作に影響する。

例えばChatbot UIではデフォルトで以下のシステムメッセージが設定されている。

You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.

訳:あなたはOpenAIが学習させた大規模な言語モデル、ChatGPTです。ユーザーの指示に注意深く従いましょう。マークダウンを使って応答してください。

応用としてはLLMの返答トーンといったキャラクター付けをシステムメッセージで行うようにすると、プロンプトのキャラクター定義が各ユーザーメッセージで重複せずDRYになるため、APIとやりとりするトークン量を節約できる可能性がある。

まとめてみての所感

講座の内容をまとめると言いながら、ほとんどの部分が意訳したような形になってしまいました。文章中でも触れましたが、英語のプロンプトをそのまま日本語にしても上手くいかない場合がほとんどです。

そのため、プロンプト例そのものをパターンとして活用するという観点では上手くいかず、そのプロンプトがなぜその目的に対して上手く作用するのか?という観点で理解するように努めないと応用が効きません。

講座内の「Iterative」という章で触れられていましたが、最も重要なのは自身が開発するアプリケーションに適したプロンプトを開発するためのプロセスであって、このプロセスそのものをいかにして開発するかがプロンプトエンジニアリングの本質なのではないでしょうか。

現場からは以上です。

この記事が気に入ったらサポートをしてみませんか?