OpenAI API のプロンプトエンジニアリング基礎
概要
LLM を使ったアプリケーションを作るとき、モデルの性能そのものより「どう指示するか」で結果が大きく変わります。同じ GPT モデルでも、プロンプトの設計次第で精度・安定性・コストがまるで違ってきます。
この記事では OpenAI API を題材に、プロンプトエンジニアリングの基礎を入門者向けに整理します。API の呼び出し方から、実務で効くテクニック(役割分担・Few-shot・構造化出力・温度制御)までを、動くコード例つきで解説します。
対象読者は「API キーは取得できたが、思ったような回答が返ってこない」という段階の方です。
環境
- Python 3.11
openaiv1 系 Python SDK- モデルは
gpt-4o-mini相当(安価で入門に十分)を想定
pip install openai export OPENAI_API_KEY="(自分のキー)"
APIキーはコードに直書きせず、必ず環境変数やシークレット管理から読み込みます。
なぜプロンプトが重要なのか
LLM は「次に来る確率の高いトークン」を生成しているだけで、こちらの意図を読み取っているわけではありません。曖昧な指示には曖昧な出力で応じます。
こちらが暗黙に期待している「出力形式」「対象読者」「制約」を、モデルは明示されない限り知りません。プロンプトエンジニアリングとは、この暗黙の期待を言語化してモデルに渡す作業です。
入門者が最初につまずく問題
- 回答がブレる — 同じ質問でも毎回違う形式・粒度で返ってくる
- 指示を無視する — 「JSONで返して」と書いたのに前置きの文章が付く
- 余計に長い / 短すぎる — 出力量を制御できずコストが読めない
- 前提知識を勝手に補完する — 与えていない情報を創作する(ハルシネーション)
これらの多くは、モデルの能力不足ではなく指示の設計不足が原因です。
基礎の4原則
1. 役割(system メッセージ)で振る舞いを固定する
system ロールでモデルの立ち位置・トーン・制約を宣言します。会話全体に効く「設定」なので、毎回の質問に書くより効率的です。
from openai import OpenAI
client = OpenAI() # OPENAI_API_KEY を環境変数から読む
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "あなたは丁寧な技術ライターです。専門用語には短い補足を添えます。"},
{"role": "user", "content": "REST APIを一言で説明してください。"},
],
temperature=0.3,
)
print(response.choices[0].message.content)
2. 具体的に、期待する出力形式まで書く
「要約して」ではなく「3つの箇条書きで、各30文字以内で要約して」のように、粒度・形式・制約を明示します。
# ❌ 曖昧
bad = "この文章を要約して。"
# ✅ 明確
good = """次の文章を要約してください。
制約:
- 箇条書きで3点
- 各30文字以内
- 技術者向けの語彙で
文章: {text}"""
3. Few-shot(例示)で形式を学ばせる
言葉で説明しづらい形式は、入出力の例を1〜2個見せるのが最短です。
messages = [
{"role": "system", "content": "問い合わせ文をカテゴリに分類します。出力はカテゴリ名のみ。"},
{"role": "user", "content": "ログインできません"},
{"role": "assistant", "content": "認証"},
{"role": "user", "content": "請求書を再発行したい"},
{"role": "assistant", "content": "請求"},
{"role": "user", "content": "パスワードを変更したい"},
]
res = client.chat.completions.create(
model="gpt-4o-mini", messages=messages, temperature=0,
)
print(res.choices[0].message.content) # → 認証
4. 温度(temperature)で振れ幅を制御する
temperature は出力のランダム性です。分類や抽出など正解が決まっているタスクは低め(0〜0.3)、アイデア出しなど発散したいタスクは高め(0.7〜1.0)にします。
構造化出力(JSON)を確実に受け取る
後続処理でパースするなら、response_format で JSON を強制するのが安全です。
res = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "抽出結果を指定スキーマのJSONで返します。説明文は付けません。"},
{"role": "user", "content": "田中さんは30歳、東京在住のエンジニアです。"},
],
response_format={"type": "json_object"},
temperature=0,
)
import json
data = json.loads(res.choices[0].message.content)
print(data)
JSON モードを使うと「前置きの文章が混ざってパースが壊れる」事故を防げます。
動作確認のポイント
- 再現性 — 同じ入力を数回投げ、出力がブレていないか
- 形式適合 — JSON なら
json.loadsが例外なく通るか - 境界ケース — 空入力・想定外入力でも壊れないか
- トークン量 —
response.usageで入出力トークンを確認しコストを見積もる
print(res.usage) # prompt_tokens / completion_tokens / total_tokens
注意点
- APIキーは絶対にコードやリポジトリに含めない。環境変数・シークレット管理から読む
- ハルシネーション対策:「わからない場合は『不明』と答える」と明示する
- プロンプトインジェクション:ユーザー入力を system 指示と同格に扱わない。データとして囲む
- モデル更新で挙動が変わる:モデル名を固定し、更新時は回帰テストを回す
- コストは出力トークンに強く効く:不要に長い出力を求めず
max_tokensで上限を設ける
まとめ
プロンプトエンジニアリングの基礎は、突き詰めると「暗黙の期待を明示する」ことに尽きます。
systemで役割・制約を固定する- 出力形式・粒度・件数まで具体的に書く
- 例示(Few-shot)で形式を学ばせる
temperatureでタスクに応じて振れ幅を制御する- JSON モードと
usage確認で実務品質に仕上げる
まずは手元の1タスクを選び、「曖昧なプロンプト → 制約を足したプロンプト」の前後で出力を比べてみてください。効果を実感するのが上達の近道です。
