LLM アプリ開発 #4 プロンプトエンジニアリングの実務
第3回までで呼び出す方法は身につきました。ところが LLM アプリの結果の品質は、コードよりも何をどう尋ねるかに大きく左右されます。同じモデル、同じコードでも、プロンプトの書き方しだいで答えが使えるものにも的外れなものにもなります。今回は、望む結果を安定して引き出すプロンプトの書き方を整理します。
あいまいな指示と具体的な指示 #
LLM は空白を自分で埋めます。指示があいまいだと、モデルが細部を勝手に決めます。長さも、形式も、口調も、そのつど変わります。逆に具体的に書くほど、結果が望む姿に近づきます。
同じ要約の作業を二通りで比べてみます。
- あいまいな指示: 「この文章を要約してください。」
- 具体的な指示: 「この文章を3つの箇条書きで要約してください。各箇条書きは一文で、専門用語はかみくだいて書いてください。」
前者は長さも形式もモデル任せです。後者は箇条書き3つ、一文、やさしい表現という枠が決まっているので、毎回似た結果になります。プロンプトを書くときは、いつも自問してみるとよいです。頭の中に描いた結果を、モデルがこの一文だけ見て同じように思い描けるだろうか、と。
import anthropic
client = anthropic.Anthropic()
prompt = """次の文章を3つの箇条書きで要約してください。
各箇条書きは一文で、専門用語はかみくだいて書いてください。
(ここに要約する文章)"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)出力形式を指定する #
答えをコードで使い直すには、形式が一定でなければなりません。「ポジティブ・ネガティブ・中立のいずれかだけで答えて」「カンマ区切りのリストで」「Markdown の表にまとめて」のように出力形式を決めておくと、その後の処理が楽になります。
とくに分類の作業では、答えを一語に絞ることが大事です。
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=10,
system="あなたはレビュー分類器です。回答は必ずポジティブ、ネガティブ、中立のいずれか一語にしてください。ほかの説明は付けないでください。",
messages=[{"role": "user", "content": "思ったより微妙でした。"}],
)このように出力を絞ると、結果をそのまま if 文や辞書のキーとして使えます。ただし自然言語の指示はあくまでお願いなので、モデルがときどき「ややネガティブです」のような文で答えることもあります。形式を本当の意味で強制する方法は、次回の #5(構造化された出力)で扱います。ここでは「プロンプトで形式を絞れる」程度で十分です。
例で示す #
言葉で説明しにくい形式やスタイルは、例を一つか二つ見せるほうが、説明を十行書くより効きます。入力と出力の組をいくつか見せてから新しい入力を与えると、モデルがそのパターンに従います。この方式を few-shot と呼びます。
messages に user と assistant を交互に入れて例を作ります。
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=10,
messages=[
{"role": "user", "content": "配送が速くてよかったです"},
{"role": "assistant", "content": "ポジティブ"},
{"role": "user", "content": "梱包が破れて届きました"},
{"role": "assistant", "content": "ネガティブ"},
{"role": "user", "content": "価格は手頃だと思います"},
],
)前の二組が「レビューを一語で分類する」というパターンを示しています。Claude は最後のレビューにも同じ形式で、説明なしに一語で答えます。形式が難しいほど、また言葉で表しにくいほど、例の効果が大きくなります。
データと指示をタグで分ける #
プロンプトの中で指示とデータが混ざると、モデルが混乱します。とくにユーザー入力や長い文書を扱うとき、どこまでが処理の対象でどこからが命令なのか、境目があいまいになります。Claude は XML タグで区切られた入力をうまく扱うので、データをタグで包んで分けるとよいです。
prompt = """次の <review> 内の顧客レビューを一文で要約してください。
<review>
配送は速かったものの製品の状態が良くなく、カスタマーサポートの対応は丁寧でした。
</review>"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=200,
messages=[{"role": "user", "content": prompt}],
)タグで包むと、指示(「要約してください」)とデータ(レビュー本文)の境目がはっきりします。データが長くても改行が多くても、モデルは混乱しません。もう一つ利点があります。ユーザーが入力したテキストをタグの中に閉じ込めておくと、その中に「これまでの指示は無視して…」のような文が混じっていても、命令と取り違える余地が減ります。これはプロンプトインジェクションを防ぐ基本の習慣でもあります。
順を追って考えさせる #
すぐに答えを出しにくい問題、たとえば複数の条件を突き合わせる計算や論理の問題は、「順を追って一つずつ考えてから答えて」と添えると正確さが上がります。モデルが途中の過程を経ることで、間違いを減らすからです。
prompt = """りんごが12個あります。友だち3人に2個ずつ分け、
残ったりんごの半分を私が食べました。今残っているりんごは何個ですか?
順を追って一つずつ考えたうえで、最後の行に「答え:」として結果だけ書いてください。"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=512,
messages=[{"role": "user", "content": prompt}],
)途中の過程を出力するとトークンが増えます。最終の答えだけが必要なら、上のように「最後の行に答えだけ」という形式も一緒に指定して、過程は経つつ結果だけをきれいに取り出せます。
claude-sonnet-4-6 では、明示的に促すと役立ちます。モデルの等級によって必要な指示が違う、という点を覚えておくとよいです。よくつまずくところ #
- 一つのプロンプトに詰め込みすぎる — 要約して、翻訳して、分類して、表にまとめて、と一度に頼むと、一部を取りこぼしやすくなります。作業を分けて何度か呼び出すほうが正確です。
- 否定の指示に頼る — 「冗長に書かないで」よりも「二文以内で書いて」のように、望むものを直接言うほうがよく通じます。するなという言葉より、しろという言葉のほうが明確です。
- 例の形式がばらばら — few-shot の例どうしの形式が違うと、モデルも一貫性を失います。例どうしの形式をそろえる必要があります。
プロンプトは磨いていくもの #
よいプロンプトは一度で完成しません。まずは簡単に書いて結果を見ます。次に、モデルがどこでずれるかを確認し、その部分を埋める指示を一行ずつ足していきます。要約が長すぎれば長さ制限を、専門用語がそのまま出れば「かみくだいて書いて」を足す、という具合です。
この過程は、これまで扱った道具とよくかみ合います。同じ入力で何度も試すときは、temperature を 0 に近づけて結果を安定させると(第2回)、プロンプトの変化が結果にどう表れるかを比べやすくなります。最初から完璧なプロンプトを書こうと力むより、すばやく回して失敗を一つずつ潰していくほうが、結局は速いです。
まとめ #
今回は、望む結果を引き出すプロンプトの書き方を整理しました。
- あいまいさを減らして具体的に指示します。長さ、形式、口調まで決めるほど結果が安定します。
- 出力形式を指定すると、結果をコードで使い直しやすくなります。
- 言葉で難しい形式は例(few-shot)で示します。
- データは XML タグで包んで指示と分けます。
- 難しい推論は順を追って考えさせ、最終の答えの形式も一緒に指定します。
プロンプトで出力形式を絞ることはできても、「必ずこの形式」を100%保証することはできません。次回の「LLM アプリ開発 #5 構造化された出力を受け取る」では、JSON スキーマで出力形式を強制し、受け取った結果をそのままコードに差し込んで使う方法を扱います。