私たちのほとんどは、試行錯誤によってLLMを使います。この記事では、LLMの構成要素と、生産環境(プロダクション)でプロンプトを書くための再利用可能なテンプレートという、手順の構造を提供します。
LLMとは何?
基盤モデルは、インターネット上のデータで事前学習された非常に大規模なモデルです。これが生成AIを作る土台です。基盤モデルを使うことで、事前学習済みの1つのモデルを多くのタスクに適応できます。
Large Language Model(LLM)とはテキストのための基盤モデルであり、その中核では、同じFMを多くのタスクに使えます。要約、分類、翻訳、コード生成などです。
つまり、LLMが行うことは、シーケンスの次の単語(次トークン)を予測することです。各ステップで、これまでに見てきたものの周辺文脈を確認し、その上で、あり得る次トークンの確率分布を生成します。これをループで実行すると、流暢で首尾一貫した新しいコンテンツが得られます。要するに、LLMはテキストを受け取り、テキストを返します。入力はプロンプトと呼ばれ、出力は完了(completion)と呼ばれます。
プロンプトに何が入る?
プロンプトとは、望ましい出力を生成するために、生成モデルへ与えるあらゆる入力です。プロンプトエンジニアリングとは、それらのプロンプトを設計し改良して、モデルから可能な限り良い結果を引き出すための実践です。プロンプトを改良するとは、モデルの出力に影響する要因を試すことです。曖昧なプロンプトは、多くのもっともらしい応答につながります。追加する制約が増えるほど、あり得る応答の数は減ります。
よく構造化されたプロンプトは、通常4つのパートから組み立てられます。
| 構成要素 | 役割 |
|---|---|
| 指示(Instruction) | 実行してほしいタスク。 |
| 文脈(Context) | 状況をモデルに理解させるための、関連する背景。 |
| 入力データ(Input data) | タスクが操作すべき具体的な内容。 |
| 出力指示(Output indicator) | 応答が取るべき形式の説明(または例)。 |
ここでの指示と文脈は、2つのテンプレート枠です。Task と Context は、最後にまとめて取り出します。
そして、プロンプトを書くためのベストプラクティスは、4つの次元に整理できます。強いプロンプトは、この4つすべてに配慮します。
| 次元 | 実践 |
|---|---|
| 明確さ(Clarity) | 簡潔で直接的な言葉を使います。曖昧、または過度に複雑な用語は避け、プロンプトが簡単に理解できるようにします。 |
| 文脈(Context) | 関連する背景と具体的な詳細を提供し、モデルが状況を理解するための手がかりにします。 |
| 精度(Precision) | 欲しい応答の種類を明確に述べ、期待する出力を例で示します。 |
| ロールプレイ/ペルソナ(Role-play / Persona) | 特定のキャラクターや専門家の視点からプロンプトを書き、モデルがその役割を効果的に引き受けられるだけの十分な詳細を与えます。 |
プロンプトを検索ビームだと思ってください。曖昧=広いビームで、モデルは広い有効領域のどこかに着地します。制約を1つずつ加えることで、そのビームは狭まります。特異性はモデルへの礼儀ではありません。狙いです。
出力を読むのは誰:コードか人か
出力のほうこそ、私たちにとってより興味深い部分です。LLMの出力には、2種類の観客(オーディエンス)があります。パーサーと人間です。そして、それぞれに対して契約書(contract)を書きます。たとえ人が出力を読んだとしても、「見た感じ大丈夫」は「自分が必要としていたものと一致している」と同じではありません。LLMの出力が目ではなくコードに読まれる場合、それはAPIレスポンスになり、プロンプトはそのスキーマになります。人間は多少ぐちゃぐちゃでも許しますが、json.loads()は許しません。成功するか、例外を投げるかのどちらかです。明示的な仕様がなければ、モデルが形式、長さ、トーン、深さを決め、筋の良いが汎用的なものを選びます。ここでの出力制御とは、その判断をモデルからあなたへ移すことです。
制御には2種類あります:
- 形式の制御: 出力の形(JSONのキーと型、または散文か箇条書きか表か、見出し、セクションなど)。
- 振る舞いの制御: モデルができる/できないこと(例:「有効なTerraformのみ、コメントなし」;または「簡潔なエグゼクティブトーン、技術用語なし」)。
各観客は別々の仕方で失敗し、そして片方は静かに失敗します:
| パーサー(コードにより読まれる出力) | 人(人間により読まれる出力) | |
|---|---|---|
| 何が壊れる | Markdownのフェンス、口調がうるさい前置き、でっち上げ/改名されたキー、文字列としての数値 | 長すぎる、誤ったトーン、セクションの欠落、レベルが合っていない |
| どう壊れるか |
大きく: json.loads() が例外を投げ、パイプラインが止まる。すぐに気づく |
静かに: 出力は流暢で完結して見える。その穴は2回目に読んだときにだけ分かり、誰もそれを指摘しない |
大きな失敗は厄介ですが、静かな失敗はより危険です。微妙に的外れな答えが気づかれないままになり得るからです。
より良い結果を得るために適用できる軽減策がいくつかあります:
パーサーに対して:
- 正確なスキーマを指定する。
- ノイズを禁止する。
- 正確な形の例を1つ示す。
temperature: 0を設定する。- ネイティブの 構造化出力/ツール呼び出し を使う。
人に対して:
- 構造を明示する。
- 長さの予算を与える。
- 観客(オーディエンス)を名前で指定する。
- 必須要素を列挙する。
- 繰り返し形式なら、望ましい出力の例を1つ示す。
ここでの要点は、プロンプトだけによる形式の制御は依頼(request)であり、デコード時の制約は保証(guarantee)だということです。モデルの出力をAPIレスポンスとして扱い、プロンプトをそのスキーマとして扱ってください。
制約 と 出力 もテンプレート枠です。振る舞いを刈り込むルールであり、あなたが取り交わす(契約する)正確な形です。
システムとユーザーの分割
効果的なプロンプトの4つの次元のうちの1つとして、ロールがあることはすでに見ました。多くの人が忘れてしまう部分です。
プロンプトには、3種類のメッセージを含めることができます:システム、ユーザー、アシスタント。
- システムメッセージ: モデルの振る舞い、ルール、全体的な役割を定義します。
- ユーザーメッセージ: 現在のタスクに対する要求または入力を含みます。
- アシスタントメッセージ: モデルからの過去の応答で、会話の文脈として使われたり、例として使われます。
「タイプ」は通常、それぞれのメッセージの role フィールドによって設定されます。
システムメッセージはコンポーネントの設定であり、ユーザーメッセージは特定の呼び出しの入力です。
システムプロンプトは永続的な振る舞いとルールを定義します。ユーザーメッセージは、その特定の要求に対する変数データを提供します。システムがコンポーネントの一般的な振る舞いを決め、ユーザーメッセージが今すぐ何をすべきかを伝えます。
なぜロールは効くのか? 魔法ではありません。
「上級のセキュリティエンジニアとして振る舞って」と言うと、モデルは学習データ内で統計的に関連づけられている、その種の文章のパターンに出力を寄せます。
同様に、「この内容をジュニア開発者に説明して」と言えば、モデルはより単純で、教育的で、より詳しく説明された応答を出す方向に押し出されます。
ロールはモデルに本当の人格を与えません。モデルが生成しそうな種類のテキストの確率分布を変えるだけです。これがRole枠であり、テンプレートの最初の行です。
本番運用において「システム/ユーザー分割」が重要な理由:
- キャッシュ。 システムメッセージは通常、リクエスト間で安定していて再利用されるため、プロンプトキャッシュに最適です。システムプロンプトは一貫させ、変更が多いデータはほとんどをユーザーメッセージに置きましょう。
- テスト容易性。 システムプロンプトは、AIアプリケーションの中でも影響度が非常に大きい部分の一つです。コードのように扱いましょう。バージョン管理し、変更点を比較し、慎重にテストします。
- セキュリティ。 信頼できる指示はシステムメッセージに置くべきです。信頼できないコンテンツ(ユーザー入力、取得したドキュメント、ツール出力)はユーザーメッセージにとどめます。信頼できないテキストが、モデルにとって指示として扱われる場所に着地すると、プロンプトインジェクションが起きます。明確な分離は、最初の防衛ラインです。
モデルの例を示す
良いプロンプトがあれば、かなり前に進めます。いくつかのテクニックで、さらに進めます。最も役立つのは、モデルに例を示すことです。
少数例(few-shot)プロンプトでは、プロンプトにいくつかの具体的なデモが含まれます。モデルは文脈内学習を使ってパターンを推測し、それを新しい入力に適用します。
少数例の例文は、仕様書としても機能するユニットテストです。
なぜ効くのか:モデルはパターンの継続者です。いくつかの入力→出力のペアによって、曖昧さの少ない強いパターンが作られ、モデルはそれを継続します。
どれをいつ使うか:
-
ゼロショット: モデルが明確に何度も見ている、単純で一般的なタスク。
[ { "role": "user", "content": "Summarize this article in 3 bullet points." } ] -
ワンショット:主に形式を固定したいとき。
[ { "role": "user", "content": "Convert countries to JSON. Example: France -> {\"country\": \"France\"} Now convert: Brazil" } ] 少数ショット:分類、構造化された出力、コードの作法、特定のスキーマや、モデルが当てずっぽうで推測できないようなエッジケースが絡むもの。
これらの作り込まれたデモは、Examples(例)の枠です。
以下の例では、最後のメッセージが新しい入力で、モデルがassistantの応答ターンを生成します。
[
{"role": "user", "content": "Today the weather is fantastic"},
{"role": "assistant", "content": "positive"},
{"role": "user", "content": "I don't like your attitude"},
{"role": "assistant", "content": "negative"},
{"role": "user", "content": "That shot selection was awful"},
]
トークンは、あなたが支払うもの
LLMは無料ではなく、トークンが計測器です。LLMにとってトークンは、コストとレイテンシの両方の単位になります。多くの人にとって馴染みのあるイメージはこうです。トークンはネットワーク上のペイロードであり、LLM呼び出しのたびに課金され、レイテンシを伴うAPIリクエストだ、ということです。
出力トークンがレイテンシを支配します。モデルはテキストをトークンごとに生成し、それぞれの新しいトークンは、これまでのすべてに依存するため、生成は逐次的に行われる必要があります。
入力は別の働きをします。プロンプトは単一の並列な「プリフィル(prefill)」処理で処理されるため、比較的速いです(それでもそのトークン分の支払いは発生します)。
主に仕事をするレバーは4つです。
-
入力を圧縮する
ドキュメント全文を送る代わりに、まず要約してから送るか、関連する部分だけを抽出します。多くのプロンプトは、モデルが読まない文脈をそのまま同梱しています。
-
出力を制限し、構造化する
max_tokensを設定し、長い散文の代わりにJSONや配列のような構造化形式を優先します。そして「120トークン未満に保つ」といった、簡潔な要約を求めます。 -
タスクに合ったモデルを使う
分類、ルーティング、抽出のような単純な作業には、小さなモデルのほうが速くて安価です。推論や統合(シンセシス)が本当に必要なタスクには、より強力なモデルを取っておきましょう。
-
キャッシュを使う
キャッシュには2種類あります。
プロンプト/プレフィックスキャッシュ
システムプロンプト、例、参照ドキュメントのような、安定したプロンプト部分を再利用します。プロバイダーがサーバー側でこれをキャッシュするため、高コストな入力処理ステップを再計算せずに済みます。
実務上の含意:キャッシュヒットを最大化するために、安定した内容を先に置き、可変の内容は最後に置きます。
レスポンス/セマンティックキャッシュ
あなた自身のインフラが、過去の回答を保存しておき、同じ、または非常によく似たリクエストが再び現れたときにそれを再利用します。これはプロンプトではなく、出力をキャッシュします。
再利用できるプロンプトテンプレート
ここまでに見てきたことは、本番環境のプロンプトに取り組むときに使うテンプレートを構築するための構造を私たちに与えてくれました。
テンプレートはR-T-C-C-E-Oです:
[ROLE] モデルが果たす役割。出力の分布を設定する。
[TASK] やるべきことを1つに絞って、曖昧さなく書く。
[CONTEXT] 入力、背景、データ。指示と明確に区切る。
[CONSTRAINTS] 空間を削るルール。各ルールは実在する失敗パターンに対応する。
[EXAMPLES] 1〜3個の代表的な入力→出力ペア(構造化・エッジケースのタスク向け)。
[OUTPUT] 応答の正確な形。機械が消費するならスキーマ+例も含める。
すべてのプロンプトが6つ全部を必要とするわけではありませんが、どのプロンプトにも意図した部分集合が必要であり、たまたまそうなった状態にしてはいけません。
最後に、本番チェックリストを作りましょう。プロンプトがリリースされる前の事前確認(プレフライト)です。
- 役割(Role)は設定され、タスクに合っている?
- タスク:有能な無関係の第三者が誤解しないだろうか?
- 制約(Constraints):あなたが実際に見た失敗パターンに、各制約は対応している?飾りのようなものは捨てる。
- 出力契約:明示されている?コードが消費するなら、スキーマ+例はある?
- 形式の保証:単に「こう書け」という依頼だけでなく、デコード時の制約(構造化出力/ツール呼び出し)になっている?
- 例:分類や構造化の作業で提示されている?多様で、一貫していて、最小限になっている?
-
サンプリング:タスクに合わせて
temperatureを調整している? -
トークン予算:
max_tokensで上限を設けている?出力形式はコンパクトになっている? - モデル:大きさは適切で、「強いモデル」ただ一択ではない?
- キャッシュのしやすさ:安定した内容を先に、可変の内容を最後にしている?
- インジェクション対策:システムメッセージに指示、ユーザーメッセージにデータ?
- バージョン管理&テスト済み:ソース管理で、いくつかの回帰ケースと一緒に管理している?
これで6つの枠—Role(役割)、Task(タスク)、Context(文脈)、Constraints(制約)、Examples(例)、Output(出力)—に加えて、プレフライトのチェックリストもカバーしました。すべての本番プロンプトを両方で実行(確認)すれば、希望的な依頼を、テスト済みのコンポーネントに変えられます。プロンプトは会話ではありません。コンポーネント契約です。
