Building with the Claude API 第2章 — プロンプト評価とプロンプトエンジニアリング
Lesson: Prompt evaluation(プロンプト評価とは)
これまで Claude へのアクセス方法を学んできたが、ここからは焦点を変えて2つの新しいトピック、prompt engineering(プロンプトエンジニアリング)と prompt evaluation(プロンプト評価)を扱う。この2つはどちらも「Claude から最良の出力を得られるプロンプトを書くこと」を目的としている。
- Prompt engineering は、プロンプトを書いたり編集したりするたびに使う一連のテクニック。Claude が「何を求められているか」「どう応答すべきか」を理解する助けになる。
- Prompt evaluation は、プロンプトの自動テストを行い、そのプロンプトが効果的かどうかを教えてくれる何らかの客観的な指標を得ることを目的とする。
この節ではまず prompt evaluation に焦点を当てて、プロンプトの効果を測定する方法を理解したあとに、prompt engineering のテクニックを見ていく。
プロンプト評価がプロンプト作成プロセスのどこに位置するか: プロンプトを最初に書いたとき、一般的には3つの道が考えられる。
- 選択肢1: 書いたプロンプトを1〜2回テストして、「もう十分良い」と判断してそのまま本番投入する。
- 選択肢2: 自分で用意したカスタム入力で何回かテストし、気づいたコーナーケースを1つ2つ手直しする。
- 選択肢3: プロンプトを evaluation pipeline に通し、プロンプトのパフォーマンスを示す客観的なスコアを得る。それをもとにプロンプトを少しずつ改善していく。
講師が強調するのは、選択肢1と選択肢2は「エンジニアなら誰もが(講師自身も含めて)陥ってしまう罠」だということ。本番の重要なアプリケーションで使われることになるプロンプトを書き始めても、それがちゃんと期待通り動くかを十分にテストしないまま進んでしまいがちである。そのため、プロンプトを書くときは常に選択肢3、つまり evaluation pipeline を通して客観的なスコアを得るやり方を強く推奨する。そのスコアをもとにプロンプトを反復改善し、できる限り良いパフォーマンスになるようにしていく。
Lesson: A typical eval workflow(典型的なevalワークフロー)
このレッスンでは、典型的な prompt evaluation ワークフローが実装するステップ全体を順を追って見ていく。
まず2点、前提として理解しておくべきことがある。
- ワークフローの組み立て方は複数ある。 業界全体で標準化された唯一の確立された手法があるわけではない。
- オープンソースのパッケージや有料のオンラインサービスも数多く存在する。 このコースおよびこのモジュールでは、Jupyter notebook の中でゼロから自前のカスタムワークフローを実装していく。理由は、これらのワークフローがどう振る舞うかを理解する助けになるだけでなく、prompt eval を行うのに重厚なソリューションを導入する必要はない、ということを理解してもらうためでもある。小さく始めて全体の動きを把握し、そこからスケールアップしていくことができる。
典型的な prompt eval のステップ:
- ステップ1: 初期プロンプトのドラフトを書く。 何らかの形で改善したいプロンプトを書く。例として、非常にシンプルなプロンプト「please answer the user's question」(ユーザーの質問に答えてください)を用意し、そこにユーザー入力を補間する。
- ステップ2: evaluation データセットを作成する。 このデータセットには、プロンプトに投入する可能性のある入力がいくつか含まれる。今回の例のプロンプトは入力が1つ(ユーザーが提供する質問)だけなので、eval データセットには様々な質問のリストを用意する。今回のデモでは3つの質問だけを含むデータセットにするが、実際の eval では数十、数百、あるいは数千ものレコードを含むデータセットになることもある。データセットは手作業で組み立てることもできるし、Claude を使って生成することもできる。
- eval データセットが用意できたら、それぞれの質問をプロンプトに投入し、完成したプロンプトを Claude に送る。例えば「please answer the user's question」+「what's two plus two?(2+2は?)」のようなプロンプト1、これをデータセットの他のレコード(2、3)についても繰り返す。それぞれを Claude に送って実際のレスポンスを得る。1つ目なら「2 plus 2 is 4」+「オートミールの作り方について」+「月までの距離について」のようなレスポンスが返ってくるイメージ。
- ステップ3: grading(採点)する。 実際の回答が得られたら、それらを何らかの方法で採点する。この採点ステップでは、データセットの質問と Claude から得た回答をペアにして1つずつグレーダー(grader)に投入する。グレーダーの実装方法は複数あり、後ほど詳しく見ていく。グレーダーは回答の品質に基づいて、例えば1〜10のスコアを返す。10は完璧な回答(これ以上改善のしようがない)、4のようなスコアは明らかに改善の余地があることを示す。グレーダーがどうやってこのスコアを算出するのかという「隠れた複雑さ」があるが、これも後ほど詳しく扱う。
- ステップ4: スコアを平均する。 得られたスコアをすべて合計し、平均を算出する。例では合計を3で割って平均スコア7.66を得る、というように、プロンプトのパフォーマンスを表す何らかの客観的な方法が手に入る。
- ステップ5: 反復する。 スコアが得られたら、プロンプトを何らかの形で変更し、このプロセス全体を反復・繰り返す。スコアを改善したければ、プロンプトにもう少し詳細を追加してClaudeをより良く導き、どんな出力が欲しいのかを理解しやすくする、といったことを試す。例えば「answer the question with ample detail(十分な詳細を伴って質問に回答してください)」のような一文を末尾に追加する。プロンプトのバージョン2ができたら、再びこのパイプライン全体に通す。バージョン1とバージョン2それぞれのスコアを比較し、どちらのスコアが高いかによって、(「何もないよりはマシ」という程度の客観的なサインではあるが)どちらのバージョンがより優れているかを判断できる。
このワークフロー全体の概要を把握したところで、次のレッスンから Jupyter notebook 内で実際に自前の eval フレームワークを実装していく。
Lesson: Generating test datasets(テストデータセットの生成)
自前の prompt evaluation ワークフローの構築を実際に始める。プロンプトを書き、それを評価するコードを書いていく。
プロンプトのゴール: AWS のユースケースに特化したコードを書くのをユーザーが助けるためのプロンプトを作る。ユーザーがヘルプが必要なタスクを入力すると、次の3種類のうちいずれかの出力で応答する。
- Python
- JSON configuration(JSON設定)
- 生の正規表現(regular expression)
ユーザーがタスクを依頼したら、上記3種類のいずれかの出力だけを、余計な説明・ヘッダー・フッターなしで返す必要がある。これが全体のゴール。
ステップ1: ドラフトプロンプトを書く。 すでに用意済みのV1プロンプトは「please provide a solution to the following task(次のタスクの解決策を提供してください)」というシンプルな内容で、そこにユーザーのタスクを挿入する。
ステップ2: データセットを組み立てる。 データセットには、プロンプトに投入する入力の数々が含まれる。プロンプトと入力のあらゆる組み合わせを実行することになる。今回のケースでは、task プロパティを持つ JSON オブジェクトの配列を用意する。各タスクは Claude にやってほしいことを記述しており、それぞれをプロンプトに投入して Claude に結果を返させる。データセットは手作業で組み立てることも、Claude で自動生成することもできる。Claude を使ってデータセットを生成する場合、Haiku のような高速なモデルを使う絶好の機会でもある。
実装: notebook を開き、generate_dataset という関数を定義する。この関数の中に、かなり大きめのプロンプトを用意しておく(notebook はレクチャーに添付されているのでダウンロードして使い回すことが推奨される)。このプロンプトは Claude にテストケースをいくつか生成させるためのもので、テストケースは task プロパティを持つ JSON オブジェクトの配列として表現される。デモでは3つのオブジェクトを生成するよう指示している。
コードの流れ:
messagesリストを宣言し、user message としてプロンプトを追加する。- assistant message として「```json」(バッククォート3つ + json)を追加する。
chatをメッセージリストと stop sequence(この場合は「```」の3つのバッククォート)を指定して呼び出す。これは前のモジュールで学んだ prefilling + stop sequence の手法をここでも使っている(前述の通り、この prefilling は現行の最新世代モデルではサポートされなくなっている点に注意)。- 最後に
json.loads(text)を返す。
セルを実行して関数を定義し、実際にテストしてデータセットを print してみると、Python 関数を書くケース・JSON configuration を書くケース・正規表現を書くケースの3つのテストケースがちゃんと生成されていることを確認できる。
データセットをファイルへ保存: 後で評価する際に簡単に読み込めるよう、dataset.json というファイル名で write モードで開き、json.dump に indent=2 を指定して書き出す。セルを実行すると、notebook と同じディレクトリに dataset.json ファイルが作成され、その中にタスクのリストが入っている。これで eval データセットの準備が整った。
Lesson: Running the eval(evalの実行)
データセット生成が完了したので、次はデータセット内の各レコード(これを test case(テストケース) と呼ぶ)を1つずつプロンプトとマージし、その結果を Claude に投入し、得られた出力すべてを grader(グレーダー)に通す、という流れを実装する。グレーダーについてはまだ扱っていないが、すぐ後で扱う。
時間短縮のため、以下3つの関数がすでに用意されている。それぞれ何をするかが明確なコメント付きで書かれている。
1. run_prompt 関数: test case(先ほど生成した JSON オブジェクトの1つ)を受け取って呼び出される。この関数の中で、生成したタスクをプロンプトとマージし、Claude でテキストを生成し、結果を返す。
- V1プロンプトはシンプルに「please solve the following task(次のタスクを解決してください)」という f-string に
test_case['task']を埋め込む形。 - メッセージリストを作り、user message を追加し、
chatを呼び出して結果のテキスト(outputと呼ぶ)を取得し、それをそのまま返す。 - 注意: 現時点ではフォーマット指定や指示は何も含まれていないため、想定より遥かに多い出力が返ってくる可能性が高い。プロンプトの本来のゴールは Python か JSON か正規表現「だけ」を返すことなので、これに関してはまだ対応できておらず、後で改善が必要になる。それでも
run_promptとしての第一歩はこれで完成。
2. run_test_case 関数: 個々のテストケースを1つ受け取り、先ほどの run_prompt を呼び出して Claude から出力を得て、その結果を採点し、何が起きたかを説明する dictionary を返す。
outputはrun_prompt(test_case)の呼び出し結果。- 採点部分は現時点では TODO とし、ハードコードされたスコア10を仮に設定しておく(後で本格的な採点ロジックに置き換える)。
- 最後に、
output・test_case・scoreを含む dictionary を返す。
3. run_eval 関数: データセットを引数として受け取る(または読み込む)。データセットをループし、テストケースごとに run_test_case を呼び出して結果をまとめる。
resultsを空リストとして初期化。- データセット内の各 test case に対して
run_test_case(test_case)を呼び出し、結果をresultsに追加。 - 最終的に
resultsを return する(最初は print していたが、return するほうが良い)。
これで3つの主要関数のアウトラインができた。実は、これがすでに eval pipeline の大部分を占めている。唯一まだ実装していないのが grading(採点)ロジックだけである。
テスト実行: dataset.json を開いて JSON としてパースし、run_eval 関数をデータセット全体で呼び出す。結果を results に代入し、すべてのセルを再実行する。初回実行にはかなりの時間がかかる。Haiku を使っていても、このデモでは実行に約31秒かかった(後ほど実行時間を短縮するテクニックを紹介する予定だが、今はそのまま進める)。
results を print(json.dumps(results, indent=2)) で綺麗に出力すると、各テストケースからの出力を表すオブジェクトの配列が得られる。Claude から得た出力、その元になったテストケースの定義、そしてスコア(現時点ではハードコードされた10)が確認できる。
これでデータセットとテストプロンプトをマージし、Claude から出力を得て、それらをまとめ上げるところまで完了した。残る最後のステップは、入力と Claude からの結果を実際の grader に投入することであり、これは次のレッスンから学んでいく。
Lesson: Model based grading(モデルベースの採点)
prompt evaluation ワークフローに grading system(採点システム)を実装していく。グレーダーはモデルからの出力を受け取り、何らかの客観的なシグナル(数値や真偽値など、何でもよい)を返してくれることを期待するものである。非常によく見かけるのは、1〜10の数値で出力されるパターンで、10は非常に高品質な出力、1は非常に低品質な出力を意味する(ただし数値出力が必須というわけではなく、あくまで一般的な慣行)。
3種類のグレーダー:
- Code-based grader(コードベースのグレーダー): モデルからの出力を、自分たちが書いたコードのスニペットに投入する。このコードの中では、プログラム的なチェックを何でも行える。例えば、出力が長すぎたり短すぎたりしないかの検証、特定の単語が含まれているか/いないかの確認、JSON やコードを返す場合の構文検証(syntax validation)をプログラム的に行うこと、さらには生成されたテキストがユースケースに適した読解レベルにあるかを判定する readability score のような複雑なチェックも実装できる。唯一の要件は、このコードを実行した結果として何らかの実際のシグナル(通常は1〜10の数値だが、必須ではない)を返すことだけ。
- Model-based grader(モデルベースのグレーダー): 元のモデル呼び出し(すでに行ったもの)からの出力を、追加のモデルに投入する。つまり、もう1回別の API リクエストを行うということ。モデルグレーダーを使うと非常に高い柔軟性が得られる。応答の全般的な品質、プロンプトの指示にどれだけ忠実に従ったか、応答の完全性など、思いつく限りほぼ何でも評価するようモデルに依頼できる。ここでも唯一の要件は、モデルが何らかの確固たる客観的シグナル(通常は1〜10の数値)を返すことである。
- Human-based grading(人間による採点): モデルからのすべての出力を実際の人間の前に置き、その人がそれらの応答を何らかの形で評価する。人間は非常に柔軟なので、思いつく限りどんな観点・指標でも評価を依頼できる。大きな欠点は、一般的に時間がかかり、非常に地道な作業になること。
評価基準(evaluation criteria)を事前に決める: どのスタイルの grading を使うにしても、事前にどんな評価基準を使うかを決めておく必要がある。今回のユースケースでは、3つの評価基準を設定した。
- Python・JSON・正規表現のいずれかだけが返ってきていて、Claude による余計な説明が含まれていないこと。
- その Python/JSON/正規表現が有効な構文になっている(タイプミスなどがない)こと。
- 一般的なタスク遂行度。モデルがユーザーのタスクに明確に対応し、重大なエラーやロジックミスを含まない、概ね正確なコードで回答しているか。
1つ目と2つ目は code grader で評価できる(フォーマットの評価、コードの構文検証)。3つ目の「一般的な応答とユーザーの質問に明確に対応しているか」は、その柔軟性から model grader のほうが適している。
まず model grader を実装する(実はこちらのほうが組み立てやすい):
run_test_case内の TODO 部分に戻り、その直前にgrade_by_modelという関数を新しいセルとして追加する。この関数は test case dictionary(データセットの各値、つまりテストケース)とオリジナルのモデル呼び出しからのoutputを引数に取る。- この中でモデルを呼び出して出力を採点させる。そのためにはやや長めのプロンプトを書く必要があるので、事前に用意されたプロンプトをペーストして使う。このプロンプトは以下の構成: role(役割)を設定し、AI が生成した solution(解決策)を評価するよう明確に依頼し、task を印字し、モデルが生成した solution を列挙し、そして応答方法についての指示を与える。指示の内容は、AI が生成した solution の strengths(強み)と weaknesses(弱み)のリスト、そこに至る reasoning(理由付け)、そして実際のスコアを求めるというもの。
- 重要な工夫: スコアだけを求めることもできるが、それだけだとほぼ常に6点あたりの「無難な」スコアばかりが返ってくる傾向がある。追加の strengths・weaknesses・reasoning を求めずにスコアだけを聞くと、モデルは「まあ良い面も悪い面もあるだろうから6にしておこう」と判断しがちになる。strengths・weaknesses・reasoning を求めることで、モデルはより具体的なスコアに絞り込むようになる。
- プロンプトを用意したら、grading model を呼び出すコードを書く。
messagesリストを作り、user message を追加。JSON を受け取るので、ここでも prefilled assistant message + stop sequence を使ってクリーンに抽出する。assistant message として「```json」を追加し、eval_textを得るためにchatを stop sequence(閉じの「```」)付きで呼び出す。このeval_textは特定の構造を持つ JSON オブジェクトになっているはずなので、それをパースして返す(return json.loads(eval_text))。これで model grader の完成。 - このグレーダーを実際に呼び出す:
run_test_caseの TODO に戻り、scoreをgrade_by_modelからのmodel_gradeに置き換える。test_caseと、実際にプロンプトを実行したoutputを渡す。model_gradeからscoreを取り出し、さらにスコアの背後にあるreasoning(理由付け)も抽出する。model_gradedictionary には strengths と weaknesses のリストも含まれているが、例をシンプルに保つためここでは抽出しない。scoreとreasoningを最終的な出力 dictionary に含めるようキーを追加する。 - セルを再実行(
run_test_caseの更新と評価の再実行)。実行には少し時間がかかる(このデモでは約22秒)。結果を print すると、生成された出力、モデルによって生成されたスコアとその理由が確認できる。1つ目は8点、2つ目は7点、3つ目は6点、という結果が得られた。
最後の仕上げ: すべてのスコアを合計して平均を出し、最終的な客観的スコアとして print する。run_eval 関数の中で statistics パッケージから mean をインポートし、mean(result['score'] for result in results) のような comprehension で average score を計算し、print(average_score) する。再実行すると、平均スコア 7.00(8点・7点・6点の平均)が得られた。これでようやく実際の客観的な指標が手に入った。グレーダー自体がモデルによるものなので多少気まぐれになることもあり、採点のガイダンスをもっと工夫する余地もあるが、少なくとも今後上げていくべきスコアの土台ができた。
Lesson: Code based grading(コードベースの採点)
続いて code grader を実装する。この code grader は、モデルからの出力を受け取り、余計な説明なしに Python・JSON・正規表現のいずれかだけが返ってきているかを検証する。加えて、実際に得られたコードの種類に応じた有効な構文になっているかも検証する。
構文検証のトリック: 3つのヘルパー関数 validate_json ・validate_python ・validate_regex を定義する。それぞれの中で、モデルから得た出力を JSON としてパースしようとする/Python の抽象構文木(AST: abstract syntax tree)としてパースしようとする/正規表現としてコンパイルしようとする、という処理を行う。パース・ロード等が成功すればスコア10を、パース中にエラーが発生すれば構文チェックに完全に失敗したとみなしてスコア0を返す。
データセットに format キーが必要: どのバリデータ(採点関数)を実行すべきかを知るには、テストケースのデータセットに「この出力がどのフォーマットになるはずか」という期待値を含めておく必要がある。データセットのファイルを手動で編集することもできるが、代わりにデータセットを生成するプロンプト自体を更新して、将来大きなデータセットを生成する際にも対応できるようにする。
実装のチェックリスト:
- JSON・Python・正規表現を検証する関数を追加する。
run_test_caseセルの上に新しいセルを追加し、3つのバリデータ関数を貼り付ける。セルの先頭で必要な2つのヘルパーモジュールをインポートし、下部にgrade_syntaxという汎用の振り分け関数を用意する。grade_syntaxは test case(特にformatプロパティ)を見て、それに応じた適切な validator 関数を呼び出す。 - データセットに
formatキーを追加するよう更新する。generate_datasetのプロンプトの出力例に、taskの後にカンマを追加しformatキー(値は「JSON」「Python」「RegX」のいずれか)を加える。これでセルとその下のデータセット生成セルを再実行すると、生成されたデータセットにformatが含まれるようになる(例: 「JSON configuration を作成する」タスクにはJSON、「Python を書く」タスクにはPython、「正規表現を書く」タスクにはRegX)。 - ドラフトプロンプトのテンプレートを更新する。 現状のドラフトプロンプトは「try to solve the task(タスクを解決してみてください)」としか言っておらず、これでは必ず JSON でも Python でもないコンテンツが返ってきて、構文チェックに毎回失敗してしまう。
run_promptのプロンプトに、「Python・JSON・正規表現のいずれかだけで応答すること」「コメントや解説を一切追加しないこと」という指示を追加する。さらに、本当に欲しい生のコンテンツだけを得るため、prefilled assistant message + stop sequence を再び使う(この prefilling は現行の最新世代モデルでは使えなくなっている点に前述の通り注意)。今回は、事前にどの言語形式(Python/JSON/RegX)が返ってくるかが分からないため、assistant message には具体的な言語名ではなく単に「```」+「code」という一種の裏技的なプリフィルを入れる。これにより Claude に「これからコードを書くのだ」と伝えつつ、具体的な言語を先に指定する必要がなくなる。閉じ側の stop sequence として「```」を追加する。 - model grader と code grader のスコアをマージする。
run_test_caseに戻り、model grader を実行している箇所のすぐ下に syntax grader(code grader)を追加する。syntax_score = grade_syntax(output, test_case)のようにしてoutputとtest_caseを渡す。その後、syntax score と model score をマージする。まず既存のscoreをmodel_scoreにリネームして明確化し、score = (model_score + syntax_score) / 2として2つのスコアの平均を取る。
実行結果: これで code grading の追加は完了。run_eval を呼び出して全体の平均スコアを再計算すると、最終スコアは 8.166 となった。このスコアが「良い」のかどうかは、実はまだ分からない。それを知る唯一の方法は、プロンプトを何らかの形で変更してみて、より良いスコアが得られるかどうかを確かめることである。次の演習では、プロンプトを少し変更してスコアの改善を試みる。
Lesson: Exercise on prompt evals(プロンプト評価の演習)
課題: model grader をもう少し改善する。具体的には、良い solution とはどのようなものかについて、より多くのコンテキストをグレーダーに提供する。一見難しそうに聞こえるが、実際には2つのステップだけで済む。
- ステップ1: データセットを生成しているプロンプトに戻り、各テストケースに
solution criteria(solution の良し悪しを判断する基準)を含めるよう依頼する。生成されるテストケースの出力に、solution_criteriaという新しいキーが追加されるようにする。例えば「良い solution にはこの特徴とこの特徴とこの特徴が含まれているはずだ」というような内容になる。 - ステップ2: この
solution_criteriaをgrade_by_modelのプロンプトに挿入する。プロンプトの中で solution(評価対象)を記載している箇所のすぐ後に、新しく生成された solution criteria を追加する。
解答手順(実装):
generate_dataset関数を見つけ、大きなプロンプトの中で、各テストケースについてtaskとoutput formatに加えてsolution_criteriaも生成するよう依頼する文言を追加する。「solution を評価するための重要な基準(key criteria for evaluating the solution)」という趣旨の説明文字列を添える。セルを再実行し、その下のセルでデータセットを再生成する。dataset.jsonを開くと、formatに加えてsolution_criteriaが新たに含まれていることが確認できる(内容は生成のたびに変わる)。grade_by_model関数のプロンプトを見つけ、この新しく生成された solution criteria を挿入する。すでに元の task と生成された output を含めている箇所の直後に、「ここに solution を評価する際に使うべき criteria がある(here's some criteria that you should use to evaluate the solution)」という一文を追加し、XML tags で囲む(XML tags の詳細は次の prompt engineering のレッスンで説明される)。そこに test case のsolution_criteriaキーの値を補間する。- セルを実行してパイプライン全体を再実行する。
run_evalを実行し、更新後のスコアを取得する。print(json.dumps(results, indent=2))で結果を確認すると、モデルからの実際の出力、テストケース(task と solution criteria)、スコア(この回では 9点)、そして solution criteria を含めたことでより充実したはずの model grader による reasoning セクションが確認できる。
Lesson: Prompt engineering(プロンプトエンジニアリングとは)
prompt evaluation を理解したところで、次は prompt engineering の世界に移る。prompt engineering とは、すでに書いたプロンプトを何らかの形で改善し、より信頼性が高く高品質な出力を得られるようにすることである。
このモジュールの構成:
- まずゴールを設定する(プロンプトに何をしてほしいか)。
- そのプロンプトの初期バージョンを書く(かなり出来の悪い最初の試作)。
- そのプロンプトを eval し、非常に低い評価スコアが出ることをすぐに確認する。
- その後、異なる prompt engineering テクニックを1つずつ学び、適用していく。適用のたびに再度 evaluation を実行し、改善のたびにパフォーマンスが向上していく様子を見る。
evaluation の実行には、前のモジュールで組み立てたのと同じ種類の eval pipeline を使う。ただし1点注意が必要で、講師は元の eval pipeline に改良を加え、特定のプロンプトだけでなくほぼどんなプロンプトにも対応できる、より柔軟な evaluation pipeline を作っている。これに対応する notebook は 001_prompting という名前でダウンロードできる。
今回のプロンプトのゴール: 選手(athlete)の身長・体重・目標(physical goal)・食事制限(dietary restrictions)に基づいて、1日分の食事プラン(meal plan)を生成するプロンプトを作る。選手を記述するサンプル入力(身長・体重・目標・食事制限)を受け取り、それらをすべてプロンプトに補間してモデルに送る、という想定。第1版のプロンプトでは理想の出力とは似ても似つかない結果になるが、様々な prompt engineering テクニックを経て、最終的にはほぼ理想に近い出力を目指す。
notebook 001_prompting のツアー:
- 冒頭にいくつかセットアップ用の折りたたまれたセルがある。少なくとも一度は実行しておく必要がある。
PromptEvaluatorというクラスのインスタンスを作成する。このクラスは、データセット生成・モデルによる採点など、ほぼすべてをラップしている。引数はmax_concurrent_tasksの1つだけで、これは並行処理(concurrency)をサポートする。複数の API 呼び出しを同時に行えるため、eval プロセスやデータセット生成プロセスを劇的に高速化できる。ただし、サービスのレート制限(rate limit)によっては、レートリミットエラーが出る可能性がある点に注意。エラーが出た場合は、この値をデフォルトの1(並行処理なし)まで下げることが推奨される。講師自身は非常に高いレートリミットを持っているため50に設定しているが、通常のユーザーはまず3程度から始め、レートリミットエラーが出たら2、1と下げていくことが推奨される。- データセットの生成:
generate_datasetメソッドを使う。プロンプトの全体的な目的(コンパクトで簡潔な、選手1人分の1日分食事プランを書くこと)を記述し、prompt_input_specという dictionary にプロンプトが必要とするすべての入力(身長・体重・目標・食事制限)を列挙する。これらはデータセットの一部として生成される追加プロパティになり、後でテストケースごとにプロンプトへ補間される。4つのプロパティの記述例: height(CM単位の選手の身長)、weight(kg単位の選手の体重、height とほぼ同じ形式で複製)、goal(選手の目標)、restrictions(選手の食事制限)。テストケース数(number of test cases to generate)は、モジュールをスムーズに進めるため3のままにしておくことが推奨される(実際の eval では大きな数の test case が望ましいが、講師自身のデモでは動作確認のため50まで引き上げている。受講者にはこれは推奨されない)。 - これらを設定してセルを実行するとデータセットが生成される。同じディレクトリに作成される
dataset.jsonを開くと、前のモジュールと似た構造の個々のデータセットが確認できる。 run_prompt関数: プロンプトを書き、今後改善していく場所。テストケースごとに1回呼び出され、そのテストケースのprompt_inputs(先ほどの dictionary)を唯一の引数として受け取る。この dictionary をプロンプトに補間していく。- 初期プロンプト(第1版): 非常にシンプルで稚拙な内容にする。「what should this person eat?(この人は何を食べるべきですか?)」に続けて、height・weight・goal・restrictions を
prompt_inputsから補間したものを列挙する。 - セルを実行し、evaluation を実行する。この evaluation 関数は表には出ていないキーワード引数
extra_criteria(文字列)を追加で受け取る。これは model grading の際にモデルが考慮すべき追加の評価基準を、開発者が指定できる仕組み。今回は「出力には1日の総カロリー・macro nutrient(三大栄養素)の内訳・具体的な食材と分量とタイミングを伴う食事、が含まれていること」という趣旨の criteria を指定する。 - 第1版の評価結果: 「absolutely terrible score(絶対的にひどいスコア)」で 2.32。講師によれば、これは意図的にあまり賢くないモデルを使っているためで、非常に具体的にプロンプトを書かない限りかなり悪い出力になる。このモジュールを通してスコアが伸びていく様子を分かりやすく見せるためにあえてこの設定にしている。受講者は恐らくもっと良いスコアが出るはずだが、それも問題ない。
output.htmlダッシュボード: evaluation を実行するたびに、notebook と同じディレクトリにoutput.htmlというファイルが生成される。ブラウザにドラッグ&ドロップで開くと、実行されたすべてのテストケースについて、スコア・reasoning・solution criteria・実際の出力までを含む見やすいレポートが確認できる。このダッシュボードは、eval の出力を確認してプロンプトをどう改善すべきかを理解するために頻繁に使うことになる。
現状かなり悪いスコアなので、あとはプロンプトを改善していくだけである。次のレッスンから最初の prompt engineering テクニックを見ていく。
Lesson: Being clear and direct(明確かつ直接的であること)
スタート地点のスコア2.32からは、上がっていく一方である。最初の prompt engineering テクニックは being clear and direct(明確かつ直接的であること)。
このルールが特に関わるのは、プロンプトの最初の一行。最初の一行はプロンプトの中で最も重要になりがちである。その最初の一行では、シンプルで直接的な言葉を使い、action verb(動作を表す動詞)とともに、Claude に何をすべきかを正確に伝える。
例:
- 「write three paragraphs about how solar panels work(太陽光パネルの仕組みについて3段落で書いてください)」— Claude に「書く/生成する/作成する」という仕事があることを伝え、期待される出力の内容についても少し明確にしている。
- 「identify three countries that use geothermal energy and for each include generation stats(地熱エネルギーを利用している国を3つ特定し、それぞれについて発電統計を含めてください)」— やはり Claude に何かをするよう伝え、直接的なタスクと期待される出力について少しの情報を与えている。
いずれの例でも、最初の一行で action(動作) を設定し task(タスク) を提供している。
プロンプトへの適用: 現在の1行目を、「generate a one day meal plan for an athlete that meets their dietary restrictions(選手の食事制限を満たす1日分の食事プランを生成してください)」のように更新する。冒頭で action verb を使って直接的にし、シンプルな言葉で Claude に果たすべき直接的なタスクを与えている。
結果: セルを再実行してプロンプトを更新し、再度 evaluation を実行すると、前回の2.32から 3.92 に向上した。確かに改善しているが、まだ良いとは言えない水準。次のレッスンで、もう少しプロンプトを改善するための次の prompt engineering トピックを見ていく。
Lesson: Being specific(具体的であること)
次の prompt engineering トピックは being specific(具体的であること)。プロンプトに何らかのガイドライン(guidelines)や手順(steps)を列挙して、モデルを特定の方向に導くという考え方である。
例(短編小説を書かせるプロンプト): 「隠れた才能を発見するキャラクターについての短編小説を書いてください」というプロンプトをそのまま Claude に投げると、Claude は無限にある方向性のどれにでも進める。物語の長さは大きく変わりうるし、要素を追加したり削除したりするかもしれない。キャラクターが1人だけかもしれないし、5人登場するかもしれない。特定の種類の出力を確実に得たいなら、guidelines のリストを追加する。例えば「1000語以内に収める」「rising action(盛り上がりの展開)を含める」「少なくとも1人の supporting character(脇役)を含める」といったガイドラインを追加することで、特定の種類の短編小説を書くよう Claude を方向づけられる。
ガイドラインの2つのタイプ:
- タイプA(左側): 出力に持たせたい性質(qualities)を列挙するガイドラインのリスト。出力の長さ・構造、あるいは持つべき様々な属性をコントロールしようとするもの。
- タイプB(右側): モデルが従うべき実際の手順(steps)を提供するもの。モデルに特定のことを考えさせたり、異なる方向性の間で選択させたりすることで、出力の品質を高めることを狙う。例(短編小説の例): まず面白そうな特殊な才能を3つブレインストーミングし、その中から最も面白いものを1つ選ぶ。次にその才能を明らかにするような興味深いシーンを考える。さらに、物語をもっと面白くする脇役についても考える、という具合。
タイプAでは出力の属性を導き、タイプBでは Claude が最終成果物にたどり着くまでの過程をより具体的に導く。実務のプロンプトでは、この2つを組み合わせて使うことが非常によく見られる(出力の属性をコントロールするガイドラインのリストと、モデルが従うべき手順のリストの両方を持たせる)。どちらも「具体的であること」の一例である。
プロンプトへの適用(タイプA: 属性のリスト): run_prompt 関数に戻り、出力に見られてほしい属性のリストを貼り付ける。セルを実行し、evalを再度実行すると、最終スコアは 7.86。前回の3.92から驚異的な改善であり、Claude に出力の中に何を見たいのかを正確に伝えるガイダンスを少し追加しただけでこの結果が得られた。
タイプB(手順のバリエーション)を試す: 食事プランを組み立てる際に Claude が従うべき手順を提供する。まず計算をし、次にこれを考え、それから少し計画を立てる、という具合の手順を与える。セルを再実行すると、先ほどの7.86(統計的な異常値かもしれないと講師は述べている)に対して、今回は 7.3。依然として劇的な改善ではあるが、属性のリストを列挙するほうがやや良い結果だった。そのため講師は、属性のリストを列挙するバージョンに戻すことにした。
どちらの手法をいつ使うか: 一般的には、ほぼどんなプロンプトでも「出力に持たせたい性質」を列挙することを推奨する(タイプA)。一方で、Claude により複雑な問題に取り組ませたい場合、つまり Claude が自然には考慮しないかもしれない、より広い視野や追加のトピックを考慮させたい場合には、モデルが従うべき手順を提供する(タイプB)ことが推奨される。例として、あるセールスチームの数字が前四半期で落ち込んだ理由を Claude に調べさせるプロンプトが挙げられている。このようなシナリオでは、Claude に本来考慮しないかもしれない追加の視点やデータを検討させたい場合がある。
Lesson: Structure with XML tags(XMLタグによる構造化)
次に扱うのは、プロンプトに XML tags を使って構造を持たせるという考え方。
背景: プロンプトを書くとき、しばしばある程度のコンテンツを補間することになる(今回の例でも身長・体重・目標・食事制限を補間している)。それぞれの値は比較的小さいものだが、プロンプトに大量のコンテンツを投入する必要がある場合もありうる。例えば、20ページ分の売上記録(sales records)をプロンプトに貼り付けて Claude に何らかの分析をさせるようなケース。大量のコンテンツをプロンプトに詰め込むと、Claude にとってどのテキストが何を意味するのか、あるいはテキストがどうグルーピングされているのかが分かりにくくなることがある。
解決策: 異なるコンテンツの塊を XML tags で囲むことで、プロンプトの構造をより明確にできる。例えば、先ほどの売上記録を <sales_records> タグで囲む。「sales records」という公式に決まった XML タグがあるわけではなく、これは講師が作った名前にすぎないが、その中にあるコンテンツの性質について Claude によりよい手がかりを与えられる。単に「records」や「data」と呼ぶこともできたが、より具体的な名前のほうが確実に良い出力につながる。
なぜXMLタグが必要かを示す誇張された例: 「debug my code below using the provided documentation(下記のコードを、提供されたドキュメントを使ってデバッグしてください)」という先頭行のプロンプトを考える。これは「自分が書いたバグ入りのコードと、ある程度のドキュメントがこの下に続く」ことを暗示しているが、実際にリストされたコンテンツだけを見ても、どこまでがコードでどこからがドキュメントなのかがまったく明確ではない。これを明確にする方法として、コードの各チャンクを適切な XML タグで囲む。例えば、コードを <my_code> タグ(「これが自分のコードだ」と非常に直接的で明確)で囲み、ドキュメントを表すコードを <docs> タグ(同じく明確)で囲む。こうすることで、Claude はどのコードをデバッグすべきで、どのコードがドキュメントのソースなのかをずっと理解しやすくなる。
プロンプトへの適用: 今回のケースには、区切る必要のある大きなコンテンツの塊は実はない。身長・体重・目標・食事制限といった補間されたコンテンツはすべて十分に短く、Claude が混乱する可能性は低い。それでも、これが選手に関する外部入力・情報であることを明確にするために XML tags を使うことができる。補間ブロック全体を <athlete_information>(と閉じタグ)で囲む。
結果: セルを再実行し、eval を再実行する。XML tags を追加する前のスコアは7.3だった。結果は かなり大きく上昇した(具体的な数値は明記されていないが「quite a bit(かなりの幅)」と表現されている)。講師は、意図的にシンプルで基礎的なモデルを使うことで誇張されたリターンを見せているため、受講者は同程度の大きな伸びを見ないかもしれないが、それは全く問題ない、と補足している。
Lesson: Providing examples(例を提供すること)
このレッスンで扱う prompt engineering テクニックは、講師いわく「見つかる中でも恐らく最も効果的なものの1つ」で非常に楽しみにしている技法だという。それは、プロンプトの中に examples(例)を提供すること。これは提供する例が1つか複数かによって、one-shot prompting または multi-shot prompting と呼ばれる。
具体例で理解する: ツイートの sentiment(感情、ポジティブか・ネガティブか)を分類させるプロンプトを考える。サンプル入力ツイート「yeah sure, that was the best movie I've ever seen since Plan 9 from Outer Space(ええ確かに、"プラン9・フロム・アウタースペース"以来の最高の映画だったよ)」を与える。この「Plan 9 from Outer Space」は有名な「ひどい映画」なので、こんなツイートをする人物は実はおそらく皮肉(sarcastic)を言っており、実際にはその映画を全く気に入っていない可能性が高い。つまり本来はネガティブに分類すべきツイートだが、Claude はこれをうまく分類できない可能性がある。
multi-shot prompting による解決: 元のプロンプトに例を追加する。Claude に対して、これからサンプル入力と理想的な応答例を渡す旨を明確に伝える。入力と出力のペアはほぼ常に XML tags で囲み、プロンプトの構造をより明確にし、その入出力ペアの目的を Claude に明確にする。例えば、サンプル入力「great game tonight(今夜の試合最高だった)」(明らかにポジティブ)を与え、直後に理想的な出力として単に「positive」を与える。こうすることで、Claude は将来同様の入力を見たときに「これはポジティブとラベル付けすべきだ」という具体的で客観的な例を得られる。
コーナーケースへの対応: multi-shot prompting は、コーナーケースを扱いたいときに複数の例を用いる場合に活用できる。皮肉を含むツイートのようなケースはまさにコーナーケースであり、Claude に特に強調しておきたい。コーナーケースを強調する例を追加する際は、Claude に「特にこういうシナリオに気をつけるべきだ」という文脈も添えるとよい。例えば「be especially careful with tweets that contain some sarcasm(皮肉を含むツイートには特に注意してください)」と述べたうえで、すぐに例を示す。「oh yeah, I really need a flight delayed tonight, excellent(ああそうだね、今夜フライトが遅延するのが本当に必要だったんだ、最高だね)」というサンプル入力は、皮肉を理解していなければポジティブなセンチメントのツイートに見えてしまうが、実際には皮肉でありネガティブである可能性が高い。この例を見ることで、Claude が元の入力を採点する際にも「これも皮肉っぽいから、おそらくネガティブだ」と気づきやすくなる。
複雑な出力フォーマットの理解にも有効: multi-shot prompting は、コーナーケースの捕捉や Claude への明確化だけでなく、より複雑な出力フォーマットを Claude に理解させる助けにもなる。かなり複雑な構造の JSON オブジェクトを生成させたい場合、サンプル入力とその複雑な JSON 構造の例出力を Claude に見せることで、目指すべき出力の正確な構造をより良く理解させられる。
prompt evals との相性が特に良い: prompt eval を実行するたびに、同じディレクトリに HTML ファイルが作成されることを思い出してほしい。このファイルを漁って完璧な10点(あるいは少なくとも高いスコアのテストケース)を探す。デモでは10点を見つけ(見つからない場合は最もスコアが高いレコードで構わない)、これはモデルグレーダーによって「ほぼこれ以上望めない」と判断された、入力と出力の例である。これをプロンプト内の例として提供することを考える。
プロンプトへの適用:
- この高スコアの入力をコピーし、プロンプトに戻る。guidelines セクションの下に、これからサンプル入力と理想的な出力の例を提供する旨を明示的に Claude に伝える一文を追加する(「ここにサンプル入力と理想的な出力を含む例があります」といった趣旨)。
- サンプル入力を XML tags 内に配置し、理想的な出力も同様に XML tags 内に配置する。理想的な出力の中身には、先ほどコピーした出力を貼り付け、インデントを整える。
- 任意だが効果的な追加ステップ: なぜこれが理想的な出力なのかを Claude に理解させることが非常に有益であることが多い、と講師は述べる。レポートには、なぜこれが良い応答だと判断されたのかを説明する列(reasoning)がある。その説明の前半部分をコピーし、閉じの
<ideal_output>タグの直下に貼り付け、文法を少し整えて「this example meal plan is well structured, etc.(この例の食事プランは構造がよく整っている、など)」のようにする。これにより、Claude は食料の選択と分量に関する詳細情報を含み、何より選手の目標と制限に一致する、構造化された出力を返すべきだという理解をより強く持てるようになる。
結果: セルを再実行して eval を再実行すると、スコアは少しだけ上昇して 7.96 となった。
まとめ(本レッスンの要点): このテクニックは one-shot(1つの例を提供)または multi-shot(複数の例を提供)プロンプティングと呼ばれる。Claude にコーナーケースへの対応を確実にさせたいとき、あるいは特に複雑な出力フォーマットに確実に一致させたいときに、非常によく使われるテクニックである。
Lesson: Exercise on prompting(プロンプティングの演習)
プロンプトエンジニアリングの理解度を試す演習。演習用に 003_exercise という新しい notebook が用意されている(必須のダウンロードではなく、既存の作業用 notebook を自分で書き換えてもよいし、この 003_exercise をダウンロードして講師の進捗に追いつく形でもよい)。
課題: このモジュールで学んだすべての prompt engineering トピックを使って、既存のプロンプトを改善すること。今回扱うデータセットは、学術論文(scholarly article)からのテキストの一節(passage)のシリーズを生成するもの。プロンプトのゴールは、そのテキストの一節を受け取り、そこに含まれるすべてのトピックを JSON array of strings(文字列の JSON 配列)として抽出すること。「トピック」とは、その記事が何について書かれているか、という意味で、例えば太陽光パネルについての記事であれば「solar panels」を含む JSON array of strings が欲しい、それ以外にもテキスト中で言及されている他のトピックがあればそれも含める、という具合。プロンプトへの唯一の入力は content プロパティ(1段落分のテキスト)。
開始状態: すでにデータセット生成セルは実行済み。スターター用のプロンプトも用意されているが、現時点ではかなり出来の悪いプロンプトで、明らかに改善の余地がある。このプロンプトを使って学んだ様々なテクニックを試す。進捗を評価するには evaluation セルを実行する。すでにいくつかの extra criteria が含まれており、正しい結果に到達しているかを確認できるようになっている。デフォルトのシンプルなプロンプトでの平均スコアは 2.8。目標は少なくとも7点あたりまで平均スコアを引き上げること。
解答手順:
- eval を最低1回実行して
output.htmlを生成する。 これを開いて、なぜ出力がそれほど低く評価されているのかの reasoning を確認する。共通するテーマとして、「JSON array of strings で返ってきていない」ことへの不満が繰り返し見られる。 - being clear and direct のテクニックを適用する。 JSON array of strings としてすべてのトピックを返してほしいなら、Claude にシンプルかつ直接的に何が欲しいのかを伝える必要がある。プロンプトの最初の一行を「extract key topics mentioned from a passage of text from a scholarly journal into a JSON array of strings(学術誌のテキストの一節から言及されている主要なトピックを抽出し、JSON array of strings にしてください)」のように、できる限りシンプルかつ直接的な表現に更新する。プロンプトと evaluation を再実行すると、いきなり 9.5 という驚くべきスコアに到達した。他にも試したいテクニックがまだ残っていたためこの結果は予想外だったが、レポートを見るとテキスト中の様々なトピックを含む JSON がきちんと返ってきており、model grader も非常に満足していることが分かる。
- XML tags で構造化する。 ここで終わりにはせず、正しい種類の出力を確実に得るための他のテクニックも追加する。補間するコンテンツの前後を XML tags で囲み、タグ名を単純に
textとする(プロンプトの最初の一行で「テキストの一節(a passage of text)」という言葉をすでに使っているため、その言及と実際に提供するテキストとの結びつきをより明確にする狙いでtextという名前を選んでいる)。 - being specific(具体的な手順)を追加する。 Claude にやってほしいことをステップバイステップで列挙する。「follow these steps(次の手順に従ってください)」に続けて、例えば「提供されたテキストをよく調べる(closely examine the provided text)」「各トピックを特定する(identify each topic mentioned)」「各トピックを JSON array に追加する(add each topic to a JSON array)」「最終的に JSON array で応答する。他のテキストやコメントは一切含めない(respond with the JSON array. Do not provide any other text or commentary)」という手順を列挙する。
- one-shot / multi-shot の例を追加してもよい。 望むなら例を追加してもよいが、すでに十分良いスコアが出ているため、この演習ではそこまでは行わないことにする。
- プロンプトを再実行し、evaluation を再度実行すると、再び 9.5 というスコアが得られた。これは十分に強力なプロンプトであり、記事からトピックのリストを抽出する用途に確実に信頼できる、と結論づけている。
章末まとめ(要点整理)
- プロンプトを書いたら必ず eval pipeline に通す。 「1〜2回テストして終わり」「コーナーケースだけ手直しして終わり」は誰もが陥る罠。客観的なスコアを得て反復改善するのが唯一の正攻法。
- 典型的な eval ワークフローは5ステップ: ①プロンプトのドラフトを書く→②evaluation データセットを組み立てる(手動 or Claude 生成、生成には Haiku のような高速モデルが向く)→③各テストケースをプロンプトとマージして Claude に投げる→④grader で採点する(通常1〜10点)→⑤スコアを平均して客観的指標を得て、プロンプトを改善しては繰り返す。
- grader は3種類: code-based(プログラム的な検証、構文チェックや長さチェックなど、決定的で高速)、model-based(別のモデル呼び出しで柔軟に評価、strengths/weaknesses/reasoning もあわせて求めると採点が安易な「6点」に偏らず具体的になる)、human-based(最も柔軟だが時間がかかり地道)。評価基準ごとに適したグレーダーを使い分け、複数グレーダーのスコアを平均でマージすることもできる(例: model_score と syntax_score の平均)。
- prefilling + stop sequence は grader の JSON 抽出にも同じパターンで再利用される(旧世代モデルのみ)。「```json」でプリフィルし「```」で止める。出力フォーマットが事前に分からない場合は「```code」のような汎用プリフィルも使える。現行の最新世代モデルではこのprefillingがサポートされなくなっており、現行モデルでは structured outputs を使う。
- model grader には solution criteria(良い solution の基準)を明示的に与えると精度が上がる。 データセット生成時に基準を一緒に生成させ、grading プロンプトに XML tags で挿入するのが定番パターン。
- prompt engineering の実測効果(デモの数値、弱いモデルで誇張された伸び): 稚拙なプロンプト 2.32 →「being clear and direct」(最初の一行を action verb + シンプルな言葉にする)で 3.92 →「being specific」でガイドラインのリスト(出力に持たせたい属性)を追加して 7.86(手順のリストのバリエーションでは 7.3 とやや劣った)→「XML tags で構造化」してさらに向上→「examples を1つ提供」(one-shot、理想の出力とその reasoning も併記)して 7.96。
- being specific には2種類ある。 属性のリスト(出力の長さ・構造・持つべき性質、ほぼ常に有効)と、手順のリスト(モデルに広い視野や追加の考慮事項を検討させたいとき、より複雑な問題に有効)。実務では両方を組み合わせることが多い。
- XML tags は大量・複雑なコンテンツをプロンプトに投入するときに特に威力を発揮する。 何が何を意味するかを Claude に明確にする。タグ名は公式な決まりはなく、内容を表す分かりやすい名前を自分でつける(例:
<sales_records>,<my_code>,<docs>,<athlete_information>,<text>)。 - examples の提供(one-shot / multi-shot prompting)は最も効果的なテクニックの1つ。 サンプル入力と理想的な出力のペアを XML tags で囲んで提供する。特にコーナーケース対応や複雑な出力フォーマットの理解に有効。eval のレポート(output.html)から高スコアのテストケースを見つけて、そのまま例として転用するのが実践的なワークフロー。理由(reasoning)も添えるとさらに効果的。
- 演習(トピック抽出プロンプト)では、being clear and direct だけで 2.8 → 9.5 まで一気に向上した。 その後 XML tags と手順の列挙を追加しても 9.5 を維持。1つのテクニックだけで劇的な改善が得られることもあるが、複数のテクニックを重ねることで頑健性を高められる。
情報源は動画の公式英語字幕(トランスクリプト)。具体例・数値・デモ手順・コード例・比喩はできる限り漏らさず反映している。技術用語(tool use, tool schema, tool_result, text edit tool, web search tool 等)は英語表記のまま用いる。
なお「Fine grained tool calling」は本コースの新規追加レッスンであり、他のレッスンと同じ丁寧さで内容を反映している。