プロンプトエンジニアリングの技法をまとめる

こんにちは。エンジニアの山下です。

今回はプロンプトエンジニアリングの技法について書きたいと思います。

フレクトでは LLM 関連技術の業務活用と実装に力を入れており、ありがたいことに LLM を使用したサービスの開発案件も少しずつ増えてきました。

それ自体は大変喜ばしいことなのですが、一方で現在のような新技術へのパラダイムシフトの過渡期には開発者の関連技術への造詣に大きな差が生じやすく、案件の数が増えると成果物の品質を高い水準で維持することが難しくなってきます。

特に LLM を使用したサービスの開発ではプロンプトの品質は開発者の知識に依存しがちです。プロンプトは自然言語ゆえに記述の自由度が極めて高く、かつ入力に対して得られる出力が確率的で対応関係が掴みづらいため、個々の開発者が思い思いにプロンプトを記述してしまうと概ね悲惨な結果に繋がります。この例に漏れず、フレクトでも何度かこの種の問題を経験しました。

実のところ、この手の問題はプロンプトエンジニアリングのごく基礎的な知識さえあれば比較的容易に回避できます。しかしながら、人に知識を伝授するという作業はそこそこ手間がかかり、特に自身が別の仕事を抱えた状態でそれをやるのは非常に面倒大変です。こういう時に「これ読んでおいて」と言える簡単なガイドが欲しくなりますね。

というわけで、先日それを社内向けに作成したのですが、世の皆様も同じ悩みを持っているかもしれないので、今回はその内容を記事としてまとめたいと思います。

前提

この記事では OpenAI・Anthropic・Google の LLM ベンダー代表3社が公開しているプロンプトエンジニアリングガイドの内容から、最低でも2社以上で紹介されている技法を取り上げて紹介するという方針を採ります。各社ベンダーのガイドは自社のモデルを念頭に書かれている節があり、今回は初学者向けにできるだけ汎用的な技法を紹介できるようにこのような方針を採っています。

上記の LLM ベンダーの代表3社は LLMArena Leaderboard のテキスト生成部門のトップ3社から選んでいます。

各社が公開しているプロンプトエンジニアリングガイドは記事の末尾に参考資料として掲載しておくので、気になる方はそちらを参照してください。

また、GIGO やコンテキストエンジニアリングなど、プロンプト技法以前の内容として初学者に知っておいてもらいたい事柄が色々ありますが、この記事ではプロンプト技法に絞って解説し、これらについては取り扱わないのでご注意ください。

各社ベンダーが述べていること

最初に各社ベンダーのガイドで述べられている事柄の一覧を示します。

なお、一覧にはガイドが暗黙に仮定していると解釈して筆者が追加した項目も含まれており、必ずしも各社のガイドの項目名に一致するわけではありません。

今回は上記の太字の項目を重点的に紹介するわけですが、太字の項目の中にも重要度のグラデーションがあり、かつ細字の項目にも知っておくと有益なものがあるため、これらを以下のように分類して順を追って紹介したいと思います。

  • 初級:太字のうちプロンプトを書く際にほぼ必須になるもの
  • 中級:太字のうち必須ではないが知っておいてほしいもの
  • 上級:細字のうち知っておくと有益なもの

具体的には、プロンプトのレビュー依頼を出す際に最低限守ってほしいものを初級に、それ以外を中級と上級に割り振るようなイメージで書いてます。特に初級は本当にごく基礎的な内容なので、初学者の方はまずこの内容を遵守することから始めることを推奨します。

なお、この記事の中で示すプロンプトの例は英語で書いてあったり日本語で書いてあったりしますが、基本的にプロンプトはどちらの言語で書いても問題ありません。ただし、利用するプラットフォームによってはプロンプトに使用する言語に制約があることがあるので、その点にはご注意ください。

初級:最低限押さえておいてほしい内容

というわけで、プロンプトを書く際にほぼ必須になる技法から説明します。項目は以下の6つです。

  • 目的・指示・例・文脈情報を書く
  • 役割を与える
  • Few-shot example を活用する
  • XML タグで文章を装飾する
  • 指示は明確かつ具体的にする
  • コンテキストに必要な情報を含める

なお、上記の上から4つは具体的ですぐに実践できる技法になっており、下2つは抽象的でどちらかというとマインドセットのような感じになっています。そういったわけで、初めてプロンプトを書く人はまず上4つを確実にマスターするというところを目指すのがよいです。

目的・指示・例・文脈情報を書く

プロンプトは基本的に目的・指示・例・文脈情報の4つから構成されており、初学者はまずこれらを書くことを手癖にすることから始めるのがよいです。

以下は OpenAI が掲載しているプロンプトの例です。

# Identity

You are coding assistant that helps enforce the use of snake case variables in JavaScript code, and writing code that will run in Internet Explorer version 6.

# Instructions

* When defining variables, use snake case names (e.g. my_variable) instead of camel case names (e.g. myVariable).
* To support old browsers, declare variables using the older "var" keyword.
* Do not give responses with Markdown formatting, just return the code as requested.

# Examples

<user_query>
How do I declare a string variable for a first name?
</user_query>

<assistant_response>
var first_name = "Anna";
</assistant_response>

Markdown のヘッダー記法で書かれている Identity・Instructions・Examples がそれぞれ目的・指示・例に該当します。こんな感じでそれぞれを書く枠を用意して書き始めると必要な項目が整理できてよいです。なお、この例では文脈情報が省略されていますが、文脈情報には RAG で取得したドキュメントの内容や、回答生成の際に知っておいてほしい業務知識などが入ります。

実際のプロンプトでは上記のように枠で区切られていないことも多いですが、基本的に構成は同じです。以下に別の例を示します。

You are an assistant AI for record search. Your job is to extract records related to a given query from a given list. Please select the records relevant to a given query.

Please follow the below instructions when you generate output:

- Please select the best ten records. But you can return less than ten records if there are no relevant records.
- If there is no records relevant to the query, you can return empty list.
- When you select records, don't follow the score of the vector search and decide by your own intention.

Please use the below format for your answer:
{
   "records": [
     "Put Source Record ID here",
     "Put Source Record ID here",
     ...
   ]
}

The query is:
{!$Input:query}

And the list is:
{!$EinsteinSearch:XXXXXXXXXXX_XXXXXXXXXXX.results}

Now, select the records relevant to the query.

これは筆者が社内サービスを構築する際に書いたプロンプトを調整したものです。項目ごとの枠はありませんが、実際に書いてある事柄は目的・指示・例・文脈情報に準拠しています。以下のように項目分けするとわかりやすいと思います。

--- 目的
You are an assistant AI for record search. Your job is to extract records related to a given query from a given list. Please select the records relevant to a given query.

--- 指示
Please follow the below instructions when you generate output:

- Please select the best ten records. But you can return less than ten records if there are no relevant records.
- If there is no records relevant to the query, you can return empty list.
- When you select records, don't follow the score of the vector search and decide by your own intention.

--- 例
Please use the below format for your answer:
{
   "records": [
     "Put Source Record ID here",
     "Put Source Record ID here",
     ...
   ]
}

--- 文脈情報
The query is:
{!$Input:query}

And the list is:
{!$EinsteinSearch:XXXXXXXXXXX_XXXXXXXXXXX.results}

Now, select the records relevant to the query.

例の部分がフォーマットの指定になっていますが、これは One-shot example という例示のテクニックです。これについては別の項で説明します。

また、文脈情報の部分に RAG 等の内容を差し込むためのプレースホルダが使用されていますが、文脈情報は動的に変化する内容であることも多く、このような書き方はよく見られるので覚えておきましょう。

役割を与える

LLM に役割を明示的に与えることで望ましい回答が得られやすくなります。先の OpenAI のプロンプトの例の Identity の項を再掲します。

# Identity

You are coding assistant that helps enforce the use of snake case variables in JavaScript code, and writing code that will run in Internet Explorer version 6.

ここに書かれている内容はそのまま LLM に果たしてほしい役割ですね。こんな感じで目的の項を書く際に書き添えておけば OK です。

Few-shot example を活用する

LLM に対して入出力の例を提示すると回答精度が劇的に向上することが知られています。この手法は例示の数に応じて XXX-shot example と呼ばれます。例が1件のみなら One-shot で、複数であれば Few-shot という感じです。また、例示をしない場合は Zero-shot と呼ばれます。

これまで示した例はいずれも One-shot example でしたが、例は以下のように複数書いても構いません。

# Examples

<product_review id="example-1">
I absolutely love this headphones — sound quality is amazing!
</product_review>

<assistant_response id="example-1">
Positive
</assistant_response>

<product_review id="example-2">
Battery life is okay, but the ear pads feel cheap.
</product_review>

<assistant_response id="example-2">
Neutral
</assistant_response>

<product_review id="example-3">
Terrible customer service, I'll never buy from them again.
</product_review>

<assistant_response id="example-3"> Negative </assistant_response>

例示をする際はシステムが想定する入出力のパターンを網羅できるように例を選ぶのが基本です。いつもの開発でテストケースを書く際と同じように考えれば OK です。

XML タグで文章を装飾する

ほとんどのモデルは XML タグを使用した指示に対応しています。XML タグを使用することで以下の恩恵が受けられ、これにより精度向上に繋がります。

  • 記述の区切りが明確になる
  • タグ名により記述内容の意図が明確になる

これまでの例でも Few-shot example などで利用されてきたテクニックですね。以下に該当箇所を再掲します。

# Examples

<user_query>
How do I declare a string variable for a first name?
</user_query>

<assistant_response>
var first_name = "Anna";
</assistant_response>

なお、使用する XML タグのタグ名には特に縛りはなく、タグ名は自分で定義しても問題ありません。例えば、Few-shot example で使用するタグは <example> などでも OK です。

指示は明確かつ具体的にする

ここからはやや抽象的なマインドセットに近い話になります。

LLM に与える指示は具体的であるほど効果が高いとされています。人間にプロンプトを渡して作業してもらった際に、誰が作業しても同じ結果になるような指示が書けるとベストです。

例として、LLM に個人情報をマスクしてもらうプロンプトを考えてみます。

Please remove all personally identifiable information from these customer feedback messages: {{FEEDBACK_DATA}}

削除対象を all personally identifiable information と指示しており、どのような情報が該当するのかがやや曖昧です。これをより具体的に書き直すと以下のようになります。

Your task is to anonymize customer feedback for our quarterly review.

Instructions:
1. Replace all customer names with “CUSTOMER[ID]” (e.g., “Jane Doe” → “CUSTOMER_001”).
2. Replace email addresses with “EMAIL
[ID]@example.com”.
3. Redact phone numbers as “PHONE_[ID]“.
4. If a message mentions a specific product (e.g., “AcmeCloud”), leave it intact.
5. If no PII is found, copy the message verbatim.
6. Output only the processed messages, separated by ”---”.

Data to process: {{FEEDBACK_DATA}}

メールアドレス等が具体的な削除対象として挙げられている他、単なる削除ではなくマスクしてほしい旨も明記され、より実行すべきタスクが明確になりました。

実際にプロンプトを書く際は、最初から全てを揺らぎなく具体的に書くことは難しいため、トライ&エラーを繰り返して解釈の揺らぎが生じる箇所を少しずつ特定して具体化・明確化していくことが多いです。

コンテキストに必要な情報を含める

LLM が回答を生成するにあたって知っておく必要のある情報は、全て LLM のコンテキストに注入しておく必要があります。このような情報の例を以下に示します。

  • 会話の履歴
  • ユーザの情報
  • RAG で取得したドキュメントの内容
  • Web 検索の結果
  • 業界の常識などの前提知識

基本的にはこちらもトライ&エラーを繰り返して必要な情報を書き足していくことが多いです。

必要な情報を含めようなど当たり前だと思うかもしれませんが、コンテキストに必要な情報が含まれるかどうかには RAG のデータ検索精度等も絡んでくるため、真面目に取り組むと総合格闘技の様相を呈して結構難しいです。さしあたり初学者の方は「回答に必要な情報はできるだけ盛り込むようにしよう」と意識しておくくらいでよいと思います。


以上で初級の内容は終わりです。

初学者の方は当面の目標として、ここまでの内容を手癖にして何も考えずとも実践できることを目指すのがよいです。上記の内容がちゃんと実施できていればプロンプトをレビューに出した際に大きな問題になることは少ないと思います。

中級:状況次第で利用する汎用テクニック

続いて中級です。ここでは常に利用するわけではないものの、よく使用する技法として知っておきたいものを紹介します。

なお、ここで扱う技法には何らかのトレードオフがあるものもあるため、ちゃんと特性を理解した上で使用するのが大切です。

Chain of Thought (CoT) を活用する

LLM に思考過程を出力させることで回答精度を向上させるという技法があり、これを CoT と呼びます。なお、CoT はプロンプト中の指示で実現できますが、特にプロンプトで指示せずとも CoT を実施してくれるモデルもあり、これは推論モデルと呼ばれます。基本的にはどちらで実現しても効能は同じです。

CoT のプロンプトの例を以下に示します。

What is the most likely interpretation of this sentence? Think step by step and print out the thinking process.

Please output in JSON format with final answer in 'answer', and thinking steps in 'think' fields.

The sentence: "The chef seasoned the chicken and put it in the oven because it looked pale."

上のプロンプトでは、提示された文章の適切な解釈を思考過程と共に出力するように指示しています。太字で示している Think step by step(順を追って考えてください)は CoT を使用する際の常套句として有名です。

このプロンプトから得られる出力の例が以下になります。

{
  "answer": "The chef seasoned the chicken to enhance its flavor and put it in the oven to cook it because it appeared to be undercooked.",
  "think": [{
    "step": 1,
    "thought": "The chef seasoned the chicken. This suggests that the chef is preparing the chicken for cooking."
  }, {
    "step": 2,
    "thought": "The chef put the chicken in the oven. This indicates that the chef is cooking the chicken."
  }, {
    "step": 3,
    "thought": "The sentence states that the chicken looked pale. Pale chicken is often undercooked."
  }, {
    "step": 4,
    "thought": "Combining these observations, it is most likely that the chef seasoned the chicken to enhance its flavor and put it in the oven to cook it because it appeared to be undercooked."
  }]
}

詳細は割愛しますが、思考過程と回答が出力されているのがわかります。

CoT はじっくり考えることで回答精度を高めることができるタスクに対して特に有用です。参考までに OpenAI の推論モデルのガイドでは、以下の用途で推論機能が役立つとされています。

  • 大量のデータから条件に適合したものを見つける
  • 大量のデータの中に含まれる関係性の抽出
  • 複雑な画像の解釈
  • レビュー・デバッグ・コード改善

ただし、CoT は回答精度への貢献も大きく便利な技法ではありますが、思考過程の分だけ出力が増大するため、レイテンシの悪化やトークン消費による費用増大などの問題があります。常に利用できる技法というわけではないので注意しましょう。

余談ですが、CoT が線形な思考過程を出力する点を木構造に拡張し、更に探索的な思考ができるようにした Tree of Thought (ToT) という手法があり、これをプロンプトのみで実現する方法が考案されています。以下の GitHub で紹介されているので、興味のある方は参照してみてください。

github.com

複雑なプロンプトは分割する

単一のプロンプトであまりに複雑な指示をしている場合、LLM の回答精度が下がる傾向があります。このような場合、元のタスクをサブタスクに分解し、サブタスクごとにプロンプトを書いて LLM に実行させた方が回答精度は高くなります。プロンプトに指示を詰め込み過ぎてしまうのは割とよく陥りがちなパターンなので、特に初学者の方は心に留めておくとよいです。

例えば、以下のようなプロンプトを考えてみます。

あなたはドキュメントの活用をサポートするアシスタントAIです。与えられた提案書から必要な情報を抽出してまとめるのがあなたの仕事です。

回答を生成する際は以下の指示を遵守してください:

- 与えられたドキュメントが提案書かどうかを判断してください
- 与えられたドキュメントが提案書の場合は<format-A>の形式のJSONを使用して回答生成してください
- 与えられたドキュメントが提案書でない場合は<format-B>の形式のJSONを使用して回答を生成してください

回答に使用するJSONの形式は以下の通りです:

<format-A>
{
  ...
}
</format-A>

<format-B>
{
  “error”: “提案書でないと判断した理由を書いてください”
}
</format-B>

このタスクは明らかに以下の2つの処理を行っています。

  1. 与えらえたドキュメントが提案書かどうか判断する
  2. 提案書から必要な情報を抽出する

これをサブタスク単位に分割すると以下のようになります。

あなたはドキュメントの活用をサポートするアシスタントAIです。与えられたドキュメントが提案書かどうかを判断するのがあなたの仕事です。

回答を生成する際は以下の指示を遵守してください:

- 与えられたドキュメントが提案書かどうかを判断してください
- 回答は<format>で指定した形式のJSONで行ってください

回答に使用するJSONの形式は以下の通りです:

<format>
{
  “is-proposal”: true | false,
  “reason”: “そう判断した理由を書いてください”
}
</format>


あなたはドキュメントの活用をサポートするアシスタントAIです。与えられた提案書から必要な情報を抽出してまとめるのがあなたの仕事です。

回答を生成する際は以下の指示を遵守してください:

  • 回答は<format>で指定した形式のJSONで行ってください

回答に使用するJSONの形式は以下の通りです:

<format>
{
  ...
}
</format>

最近のモデルは高性能なので、実際はこれくらいのタスクなら特に分割せずとも適切に処理してくれることも多いですが、あまり詰め込み過ぎないようにした方がよいのは間違いないので、常に意識しておくのが吉です。

なお、分割したサブタスクを実行する方式は2つあり、

  1. 分割したサブタスクを直列に接続し順次実行する
  2. 分割したサブタスクを並列実行する

のいずれかを選ぶ形になります。

これらの方式にはそれぞれ利点と欠点があり、1 は実行に時間がかかる反面、各サブタスクで発生した意思決定の内容を次のサブタスクに引き継げることから、タスク全体での一貫性が確保しやすいという利点があります。他方、2 は並列処理により実行時間が短縮できる一方、他のサブタスクで発生した意思決定をコンテキストに引き継げず、全体の一貫性を確保するのが難しいという欠点があります。

これらの方式のどちらを使用すべきかは上述のトレードオフや実行するタスクの性質によって決まります。例えば、上で挙げた例では最初のサブタスクの結果をもとに次のサブタスクの実行要否を判断するので、順次実行が前提になります。

大きいドキュメントは最初に置く

コンテキストに含める情報の格納順序が LLM の性能に影響を与えることが知られており、特に Anthropic と Google は大きいドキュメントをその他の指示よりも先に格納することを推奨しています。

これらの LLM ベンダーのモデルはコンテキストウィンドウのサイズがかなり大きく、RAG で取得したデータをとにかくコンテキストに詰め込むといった用途で選ばれることも多いので、推奨の格納順序を心に留めておくことは有用です。

上級:その他紹介しておきたい技法

ここからの内容はやや発展的で、LLM ベンダー1社のガイドにのみ表れている内容になります。

ここで紹介する技法はできるだけ汎用的なものを抜き出したつもりではありますが、固有のモデルにしか通用できない技法である可能性があるため、どの LLM ベンダーが述べている内容かも含めて提示します。これらの技法を適用する際は、その効果については注意深く観察するようにしてください。

LLM のレスポンスを途中まで書く(Anthropic)

LLM からのレスポンスの内容の一部を先回りして記述することで出力内容を半固定することができます。これは JSON などの特定の出力形式を LLM に強制させたい場合に便利です。

通常、LLM に対して「出力は JSON にしてください」と指示しても、JSON の前に「以下が出力の JSON です」などの無駄な文言が付与されるという問題が発生する可能性がありますが、これは以下のような記述によって抑制することが可能です。

User: Extract the name, size, price, and color from this product description as a JSON object:


The SmartHome Mini is a compact smart home assistant available in black or white for only $49.99. At just 5 inches wide, it lets you control lights, thermostats, and other connected devices via voice or app—no matter where you place it in your home. This affordable little hub brings convenient hands-free control to your smart devices.

Assistant: {

末尾の Assistant: { の部分が LLM からのレスポンスの一部を先回りしてプロンプトに記述したものです。このように書いておくことで、LLM にこの続きを出力するように促すことができます。

上記のプロンプトに対する LLM の出力の例が以下になります。

  “name”: “SmartHome Mini”,
  “size”: “5 inches wide”,
  “price”: “$49.99”,
  “colors”: [ “black”, “white” ]
}

これに先回りして書いた { を補完してあげれば立派な JSON の出来上がりというわけです。

この技法は Anthropic のガイドでのみ紹介されていた内容ですが、他のベンダーが開発しているモデルでも問題なく利用することができる汎用的な技法です。

ドキュメントの参照箇所を出力させる(Anthropic)

ドキュメントの参照を要する作業を LLM に依頼する場合、参照した内容を先に出力させることで回答精度が上がるということが Anthropic のガイドで述べられています。これは参照箇所以外のドキュメントの内容が回答生成のノイズになることがあり、参照した内容を先に出力させることでこの影響が抑制されるという理屈のようです。

例を以下に示します。

You are an AI physician's assistant. Your task is to help doctors diagnose possible patient illnesses.

<documents>
...
</documents>

Find quotes from the patient records and appointment history that are relevant to diagnosing the patient's reported symptoms. Place these in <quotes> tags. Then, based on these quotes, list all information that would help the doctor diagnose the patient's symptoms. Place your diagnostic information in <info> tags.

太字の部分がこの技法を使用している箇所で、最初に参照したドキュメントの内容を <quotes> というタグを使って出力し、その後必要な情報を出力せよと指示しています。

Anthropic のガイドでのみ紹介されている内容のため、汎用性については微妙なところですが、ドキュメントの解釈が安定しない場合などに試してみる価値はありそうです。

マルチモーダルでは画像の内容を説明させる(Google

マルチモーダルで画像を入力する場合、その画像の内容について最初に LLM に説明させることで精度向上が見込める旨が Google のガイドに書かれています。おそらく1つ前の「ドキュメントの参照箇所を出力させる」と同じ理屈と考えられます。

この技法は精度向上のためだけでなく、マルチモーダルにおけるデバッグ手法としても利用できるので知っておくと便利です。どういうことかというと、そもそもマルチモーダルで LLM が回答に失敗した場合、

  1. 画像に写っている対象が認識できなかった
  2. 画像に写っている対象は認識できたが解釈に失敗した

の2通りの可能性があるのですが、画像の内容を説明させることでこのどちらなのかが判別できます。1 の場合はモデルの性能的に厳しいかもしれませんが、2 の場合は CoT などのプロンプトの工夫によって改善可能かもしれません。

推論モデルには達成すべき目標のみを指示する(OpenAI)

推論モデルに対するプロンプトを書く場合、単に達成したい目標と抽象的なガイドラインのみを記述し、具体的な手順等は記載を省いた方が性能を発揮しやすいと OpenAI は述べています。要するに方法は自分で考えるから任せた方がよいということですね。

推論モデルについては他にも以下の諸注意が述べられています。

  • 達成すべき目標は数値等を用いてできるだけ具体的に書く
  • XML タグ等を利用した文章の装飾は通常のモデル同様に有効
  • CoT は標準搭載されているので CoT の指示は不要(むしろノイズになる)

通常のモデルとは微妙に作法が異なるので注意しておきたいですね。

まとめ

というわけで、今回はプロンプトエンジニアリングの技法についてまとめました。

プロンプトは割と書く機会に恵まれていたのですが、あらためて各社のガイドを見直すと色々と実践できていなかったものもあり、筆者にとっても自身を省みるよい機会となりました。

実案件においては RAG まわりの難しさに振り回されることが多く、プロンプトについてはなおざりになりがちですが、こういった細かな技法についてもちゃんと折を見てアップデートしていけるようにしなければいけませんね。開発者の皆様もお気をつけください。

以上です。最後までお読みいただき、ありがとうございました。

参考資料