こんにちは。とまと🍅好きエンジニアです。
この記事では、プロンプト最適化フレームワークであるDSPyを、Bedrockのモデルを使って動かす方法をBedrock連携 → DSPy導入 → 最適化デモ の順にハンズオン形式で解説します。
想定読者
最近話題の DSPyをAWS Bedrockのモデルを使って触ってみたい!って思っている人
DSPyとは
DSPyは、モジュール型AIソフトウェアを構築するための宣言型フレームワークです。脆弱な文字列ではなく構造化されたコードを高速に反復処理でき、 AIプログラムを言語モデルに効果的なプロンプトと重みにコンパイルするアルゴリズムを提供します。シンプルな分類器、高度なRAGパイプライン、エージェントループなど、あらゆる用途に使用できます。
公式サイトから引用
DSPyに関してはいろんな方が記事を書いているので詳しくは以下を参照
AWS Bedrockとは
AWS Bedrock は、AWS上でさまざまなLLM(Claude、Titan、Llamaなど)を安全に使えるマネージドサービス。
詳しくは公式サイトから
この記事で学べること
- Bedrockモデルを使ったDSPyプロンプト最適化の実例
- Claude モデルを用いた簡単なテキスト生成最適化
前提・準備
必要なもの
- AWSアカウント(Bedrock利用権限あり)
- Python 3.10 以上
ステップ1:Bedrock連携
API キーの作成
初めにBedrockと接続するために、APIキーを発行します
BedrockのAPIキーには短期APIキーと長期APIキーが存在しますが、今回はとりあえず試すだけなので、短期APIキーを使いたいと思います。
注意
ここで作成する短期APIキーの有効期限は12時間なので、それを過ぎると接続できなくなります。
まず、Bedrockを開いて、左側のメニューから「見つける」→「APIキー」をクリックします
すると以下のような画面が表示されるので、「短期APIキーを生成」をクリックします。

短期API keyが生成されたら、上側の「APIキーをコピー」クリックします。

環境変数として設定
動かしたいPythonファイルと同じ階層に、.envファイルを作成します。
そしてそのファイル内に先ほどコピーした短期APIキーと、AWSのリージョンを記入します。
AWS_DEFAULT_REGION="ap-northeast-1" #ここは自分の設定しているリージョンに合わせてください
AWS_BEARER_TOKEN_BEDROCK="コピーしてきた短期APIキー"
ステップ2:DSPyでモデルを設定
ライブラリのインストール
まず初めに、DSPyを動かすのに必要なライブラリdspyと、AWSに接続するのに必要なboto3をインストールします。
pip install dspy boto3
モデルの設定
モデルの設定と言っても以下のコードだけでDSPyは動かせます。
ライブラリをインストールしたら、Pythonファイルを作成して、そこに以下のコードを記述します。
import dspy
#モデルの登録
lm = dspy.LM("bedrock/anthropic.claude-3-haiku-20240307-v1:0")
#モデルの設定
dspy.configure(lm=lm)
predict = dspy.Predict('q -> a')
print(predict(q="美味しいトマトの見極め方は?"))
これを実行してみると、以下のような結果が、、、
Prediction(
a='トマトの見極め方のポイントは以下のようなことが挙げられます:\n\n- 色:深い赤色で均一な色合いが良い。黄色っぽい部分や白っぽい部分は避けましょう。\n- 重さ:手に取った時に重みがあ
、しっかりとした感触があるものが良質です。\n- 形:丸みがあり、ツヤのある形が良い。へこみや変形は避けましょう。\n- 香り:強い香りがするものが新鮮です。\n- 触感:表面がなめらかで、弾 力のあるものが良質です。'
)
美味しいトマトの見極め方が出ましたか?
ステップ3:DSPyの基本操作
ステップ2でどんなことを行っているかの説明をしたいと思います。
まず以下のコードでは、dspyで扱うLLMモデルの設定を行っています。
.LMで"bedrock/使いたいモデルのID"を入力することでそのモデルを使うことができます。
モデルのIDは、Bedrockの「モデルカタログ」→「各モデルのページ」のモデルIDに書かれています。
#モデルの登録
lm = dspy.LM("bedrock/使いたいBedrockのモデルID")
#モデルの設定
dspy.configure(lm=lm)
dspy.Predict()を使うことで、プロンプトの出力を得ることができます。
# dspy.Predict('プロンプト -> 出力結果')
predict = dspy.Predict('q -> a')
print(predict(q="美味しいトマトの見極め方は?"))
より詳細に入出力の構造を定義したい場合はdspy.Signature、ChainOfThoughtを使いたいときはdspy.ChainOfThought等、モジュールが多く用意されているため、LLMの処理を簡単に実装することができます。
class Tomato(dspy.Signature):
info = dspy.InputField(desc="トマトに関する情報")
question = dspy.InputField()
answer = dspy.OutputField(desc="トマトの品種")
generate_answer = dspy.ChainOfThought(Tomato)
info = "黄変果に強く、着果のよさとスタミナが自慢の硬玉桃太郎は「桃太郎ブライト」.スタミナがあり、硬玉で肥大のよい冬春用多収品種は「桃太郎ネクスト」"
question = "黄変果に強いトマトの種類は?"
print(generate_answer(info=info, question=question).answer)
ステップ4:プロンプト最適化を体験
それでは実際にプロンプト最適化を試していきます。
DSPyはどのような場面(データ数やFew-shotの有無など)でLLMの最適化を行うかで用いる方法を変えることができます。
詳しくは公式サイトを参照してください。
今回は少ない回答データを用いてプロンプト最適化を行うので、BootstrapFewShotを使います。
また、プロンプトの評価を行うためにmetricが必要になります。
今回は、dspy.evaluate.answer_exact_matchを使います。これは予測ペアが完全一致するかF1閾値が一致するまで最適化を行うものです。
ステップ3で紹介したdspy.Signatureとdspy.ChainOfThoughtを使ったときと最適化した際の結果を比較してみます。
プロンプト最適化の際の実際のコードは以下のようになります。
import dspy
lm = dspy.LM("bedrock/anthropic.claude-3-haiku-20240307-v1:0")
#モデルの設定
dspy.configure(lm=lm)
class Tomato(dspy.Signature):
info = dspy.InputField(desc="トマトに関する情報")
question = dspy.InputField()
answer = dspy.OutputField(desc="すべて日本語でトマトの品種")
generate_answer = dspy.ChainOfThought(Tomato)
info = "黄変果に強く、着果のよさとスタミナが自慢の硬玉桃太郎は「桃太郎ブライト」.スタミナがあり、硬玉で肥大のよい冬春用多収品種は「桃太郎ネクスト」"
question = "黄変果に強いトマトの種類は?"
print("最適化前")
print(generate_answer(info=info, question=question).answer)
#教師用データを準備
trainset = [
dspy.Example(
info="低温伸長性にすぐれるトマト黄化葉巻病耐病性の早生種は「桃太郎ホープ」",
question="低温伸長性にすぐれるトマトは?",
answer="低温伸長性にすぐれるトマト黄化葉巻病耐病性の早生種である「桃太郎ホープ」です!"
).with_inputs("info", "question"),
dspy.Example(
info="糖度が高く酸味の少ない完熟品種は「桃太郎ファイト」.甘くておいしい、元祖「甘熟」のトマトは「桃太郎」",
question="糖度が高いトマトは?",
answer="糖度が高いトマトは「桃太郎ファイト」や「桃太郎」です!"
).with_inputs("info", "question"),
dspy.Example(
info="青枯病に強く、食味にすぐれた夏秋用完熟トマトは「桃太郎8」",
question="青枯病に強いトマトは?",
answer="青枯病に強いのは、食味にすぐれた夏秋用完熟トマトである「桃太郎8」です!"
).with_inputs("info", "question"),
]
#プロンプト最適化の定義
optimizer = dspy.BootstrapFewShot(metric=dspy.evaluate.answer_exact_match)
#プロンプト最適化を行う
compiled_qa = optimizer.compile(student=generate_answer, trainset=trainset)
#最適化されたモジュールで実行
print("最適化後")
print(compiled_qa(info=info, question=question).answer)
何も変わっていませんね...。
最適化前
黄変果に強いトマトの品種は「桃太郎ブライト」です。
最適化後
黄変果に強いトマトの種類は「桃太郎ブライト」です。
簡単な質問の場合はあまり変わらない結果となっています。
そこで質問を少し複雑にしてみましょう。
question = "黄変果ってどんな感じになるんだっけ?あ、そんなことより春にトマトを育てたいなぁ。なにかおすすめのトマトがあったりする?"
この結果を見てみると、
最適化前
春に収穫できるおすすめのトマト品種は「桃太郎ネクスト」です。この品種は黄変果に強く、硬玉で肥大がよく、スタミナがあるため春の収穫に適しています。
最適化後
春にトマトを育てるのにおすすめなのは、スタミナがあり、硬玉で肥大のよい冬春用多収品種の「桃太郎ネクスト」です。
最適化前は質問に引っ張られてしまったせいか、情報に無い「黄変果に強く」というものが追加されてしまっています。
最適化後は教師用データと似たような最後にトマトの品種名を出してくれる形になっています。
このように近づけたい結果が明確にある場合、それに合わせたプロンプトを作成し、その結果を出力してくれます!
まとめ
この記事では、
- AWS Bedrockとの連携
- 実際のプロンプト最適化の流れ
をハンズオン形式でまとめました。
私も試せば試すほど元のプロンプトを作る作業が面倒になるくらい、使って楽しくなってます。これを機にプロンプト最適化の沼にハマってみませんか?
参考リンク
