なぜ「お願い」より「定義」なの?
LLM(ChatGPT など)に「〜してください」とお願いしても、毎回同じ品質の結果になるとは限りません。
そこで、作りたいもの=関数とみなし、**ルール(仕様)を具体的に指示します。
具体的に書くという意味ではプロンプトエンジニアリングはプログラミングとあまり変わらないとも言えます。
- 関数 = 入力(Input)を受け取り、出力(Output)を返す箱
- プロンプト = その箱が守るべきルール(仕様)
この“ルール”の3つを理解するのがコツです。
1. 3つのルールを超やさしく
1) 事前条件(Precondition / pre)
スタートする前に入力が満たしておくべき条件。
例)テスト点数のリストは空じゃない/各点数は0〜100。
2) 事後条件(Postcondition / post)
ゴールした時に出力が満たすべき条件。
例)平均点は0〜100の範囲/小数第2位まで。出力=点数の和/人数 を満たす
3) 不変条件(Invariant / inv)
処理の途中でもずっと変わらない約束。
例)テストの結果は常に変わらない。
たとえ話
-
カレーづくり:
- 事前条件=材料がそろっている
- 事後条件=完成したカレーが温かく、具が生煮えでない
- 不変条件=鍋に入れた材料の数はずっと同じ(途中で勝手に増えない・減らない)
2. まずは「関数」に言い換える
小さなプログラムも関数として宣言すると、考えるのがラクです。
- 例1:平均点を出す →
average(scores) -> number - 例2:カート合計 →
cart_total(items) -> number - 例3:摂氏⇄華氏変換 →
c_to_f(celsius) -> number
3. コピペで使える“超シンプル・プロンプトテンプレ”
空欄に埋めて、そのまま LLM に投げれば OK。
あなたはプログラマです。以下の仕様を唯一のルールとして、
Python の関数と最小限のテストコードを書いてください。
説明文は不要。コードのみ出力。
[関数名]:
[入力]:
[出力]:
[事前条件(pre)]:
- (例:入力のリストは空でない)
- (例:各要素は0〜100の整数)
[事後条件(post)]:
- (例:戻り値は0〜100の数値)
- (例:小数第2位で丸める)
[不変条件(inv)]:
- (例:合計 = 各要素の和 ※処理途中でも崩れない)
[テスト(サンプル)]:
- 入力:__ → 期待:__
- 入力:__ → 期待:__
4. 例① 平均点(pre と post の最小セット)
仕様を書いてみる
-
関数:
average(scores) -> float -
pre:
-
scoresは空でないリスト - 各要素は0〜100の整数または小数
-
-
post:
- 返り値は0〜100
- 小数第2位に丸める
この仕様をテンプレに埋めたプロンプト(完成形)
あなたはプログラマです。以下の仕様を唯一のルールとして、
Python の関数と最小限のテストコードを書いてください。
説明文は不要。コードのみ出力。
[関数名]:average
[入力]:scores(数値のリスト)
[出力]:平均点(float)
[事前条件(pre)]:
- scores は空でない
- 各要素は 0〜100 の数値(int/float)
[事後条件(post)]:
- 戻り値は 0〜100
- 小数第2位に丸める(例:78.666…→78.67)
[不変条件(inv)]:
- なし(今回の関数は一発計算のため)
[テスト(サンプル)]:
- [100, 80, 60] → 80.00
- [70, 70] → 70.00
- [0, 100] → 50.00
LLM からは、だいたいこんなコードが返ってきます(参考イメージ):
def average(scores):
if not isinstance(scores, list) or len(scores) == 0:
raise ValueError("scores must be a non-empty list")
for s in scores:
if not (isinstance(s, (int, float)) and 0 <= s <= 100):
raise ValueError("score out of range")
val = sum(scores) / len(scores)
return round(val, 2)
# テスト(最小)
assert average([100, 80, 60]) == 80.00
assert average([70, 70]) == 70.00
assert average([0, 100]) == 50.00
5. 例② カート合計(不変条件を入れてみる)
仕様
-
関数:
cart_total(items) -> int -
入力:
items = [{"price": 単価(>=0の整数), "qty": 個数(>=1の整数)}, ...] -
pre:
-
itemsは空でない - すべての
priceは0以上の整数、qtyは1以上の整数
-
-
post:
- 返り値 = Σ(price × qty)(整数)
-
inv:
- 処理の途中でも**「部分合計 = これまでの (price×qty) の和」**が常に成立
(=順番を入れ替えても最終合計は変わらない)
- 処理の途中でも**「部分合計 = これまでの (price×qty) の和」**が常に成立
テンプレ投入用プロンプト(完成形)
あなたはプログラマです。以下の仕様に従って Python の関数と簡単なテストを書いてください。
説明は不要、コードのみ。
[関数名]:cart_total
[入力]:items(各要素に price:int>=0, qty:int>=1 をもつ辞書のリスト)
[出力]:合計金額(int)
[事前条件(pre)]:
- items は空でない
- すべての price は 0 以上の整数、qty は 1 以上の整数
[事後条件(post)]:
- 戻り値 == Σ(price × qty)
[不変条件(inv)]:
- ループのどの時点でも、部分合計 == それまでに処理した (price×qty) の総和
[テスト(サンプル)]:
- [{"price": 120, "qty": 2}, {"price": 80, "qty": 1}] → 320
- [{"price": 0, "qty": 5}] → 0
6. 例③ 摂氏⇄華氏(単位と丸めのルールを明記)
-
関数:
c_to_f(c) -> float -
pre:
cは数値 -
post:
f = c × 9/5 + 32、小数第1位に丸め -
inv:
f_to_c(c_to_f(c))を計算すると**±0.1℃以内**に戻る(丸めのため厳密一致までは要求しない)
プロンプトの書き方はこれまでと同じです。
7. うまくいかないときのチェックリスト
- **入力の型・範囲(pre)**を書いた?(空リスト禁止、0〜100など)
- **出力の形(post)**を書いた?(小数何桁、整数に丸める、並び順)
- **不変条件(inv)**はある?(合計の一貫性、順序を変えても同じ結果)
- テスト例を2〜3個つけた?(普通・境界・ゼロ)
- 「〜してください」より定義の文章になっている?
8. 練習問題(答えは自分でプロンプト化してみよう)
-
最大値を返す:
max_value(nums)(pre:空でない、各要素は整数/post:戻り値は要素のいずれか) -
偶数だけ数える:
count_even(nums)(post:戻り値は0以上の整数/inv:処理済み部分の偶数カウントは正確) -
文字列の単語数:
count_words(text)(pre:空文字でない/post:スペース区切りの個数)
9. まとめ
-
成果物は関数として考えると、必要なルールが見えやすい。
-
**事前条件(pre)・事後条件(post)・不変条件(inv)**は、
- pre=入力の約束
- post=出力の約束
- inv=途中ずっと守る約束
-
この3つをプロンプトに先に書くだけで、LLMの出力は安定し、テストしやすくなります。というか、この条件を元にテストコードが生成できます。