Amazon Bedrock Prompt optimizationでプロンプト最適化！

## はじめに
この記事は、2024 Japan AWS Jr. Champions に選出されたメンバーで、アウトプット促進の一環として行っているブログリレーの11/25週の記事です。
↓過去の記事はこちらなので、併せてご覧ください！↓
[10/18週 @heiyoo(Yohei Nishikawa)さん【AWS】2024 Japan AWS Jr. Championsに申し込む際に整理したこと](https://qiita.com/heiyoo/items/ae6aed7dc6c8097b4c6f)
[10/25週 @prinprinprrrさん【Amazon ECS】コンソール以外でタスクの停止理由を確認する方法](https://qiita.com/prinprinprrr/items/264547d0c7f7c758c566)
[11/4週 @tumodora5さん[AWSコスト最適化] AWSリソースの夜間停止とその運用課題について](https://qiita.com/tumodora5/items/9a1776cc63811e4ad862)
[11/11週 @nieke_06さん【AWS Jr.Champions】Jr.Championsになって良かったこと3選](https://qiita.com/nieke_06/items/f37ea90f3629a09c64ce)
[11/18週 @jowoooonさん Amazon InspectorでSBOMを始めよう](https://qiita.com/jowoooon/items/7200dc1a0625c4bb43e7)


## 今回の記事の内容
今週（11/21）AWSから新たに発表されたAmazon Bedrockの、prompt optimization（プレビュー版）を使って、簡単なチャットボットを作成してみた、という記事になります。
[こちら](https://aws.amazon.com/jp/about-aws/whats-new/2024/11/prompt-optimization-preview-amazon-bedrock/)が参照記事です。


:::note warn
なお、本機能は現状プレビュー版で、使用可能リージョンも一部に限定されているのでご留意ください。
:::

:::note info
利用可能リージョン、及び本機能が利用可能な基盤モデルは以下です。
（[Amazon Bedrock Optimize a prompt](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management-optimize.html)より引用）
【利用可能リージョン】
- 米国東部（バージニア州北部）
- 米国西部（オレゴン）
- アジア太平洋（ムンバイ）
- アジア太平洋（シドニー）
- カナダ（中部）
- ヨーロッパ（フランクフルト）
- ヨーロッパ (アイルランド) (ゲート付き)
- ヨーロッパ（ロンドン）
- ヨーロッパ（パリ）
- 南アメリカ（サンパウロ）

【利用可能な基盤モデル】
- Amazon Titan Text G1 - Premier
- Anthropic Claude 3 Haiku
- Anthropic Claude 3 Opus
- Anthropic Claude 3 Sonnet
- Anthropic Claude 3.5 Sonnet
- Meta Llama 3 70B Instruct
- Meta Llama 3.1 70B Instruct
- Mistral AI Mistral Large (24.02)
:::


## 検証スタート
今回はバージニア北部リージョンで検証を行ってみます。
プレイグラウンド、boto3で試した上で、Streamlitで簡単なアプリを作る、3ステップで検証を進めます。

### Step1.プレイグラウンドから使う
![スクリーンショット 2024-11-23 13.05.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/3bfcef28-b7d6-d1fb-2179-49fb849c19d3.png)

Chat/Text playgroundの画面の、プロンプト入力画面の左下のボタンが、
「プロンプト最適化」ボタンです。
これを押下すると、画面のようになります。
試してみて気づいたのですが、現状ではプロンプト最適化は、英語のみがサポートされているようです。
![スクリーンショット 2024-11-23 15.58.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/d78e08f8-a520-7e07-91ee-07cf29a2a97d.png)

「この記事要約して」というプロンプトが画面右側のような形で最適化されているようでした。
![スクリーンショット 2024-11-23 15.59.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/1239d185-10d4-a303-9337-eafcce9412ce.png)


### Step2.boto3から使う
[サンプルコード](https://docs.aws.amazon.com/bedrock/latest/userguide/prompt-management-optimize.html#prompt-management-optimize-submit)が公開されているので、まずはboto3からprompt optimizationを試してみます。記載のコードをそのまま実行してみました。
ドキュメントにも記載があるように、
プロンプトの解析が終了した段階で`analyzePromptEvent`が、プロンプトの最適化が完了した段階で、`optimizePromptEvent `イベントが返却されます。
```python
import boto3

# Set values here
TARGET_MODEL_ID = "anthropic.claude-3-sonnet-20240229-v1:0" # Model to optimize for. For model IDs, see https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html
PROMPT = "Please summarize this text: " # Prompt to optimize

def get_input(prompt):
    return {
        "textPrompt": {
            "text": prompt
        }
    }
 
def handle_response_stream(response):
    try:
        event_stream = response['optimizedPrompt']
        for event in event_stream:
            if 'optimizedPromptEvent' in event:
                print("========================== OPTIMIZED PROMPT ======================\n")
                optimized_prompt = event['optimizedPromptEvent']
                print(optimized_prompt)
            else:
                print("========================= ANALYZE PROMPT =======================\n")
                analyze_prompt = event['analyzePromptEvent']
                print(analyze_prompt)
    except Exception as e:
        raise e
 
 
if __name__ == '__main__':
    client = boto3.client('bedrock-agent-runtime')
    try:
        response = client.optimize_prompt(
            input=get_input(PROMPT),
            targetModelId=TARGET_MODEL_ID
        )
        print("Request ID:", response.get("ResponseMetadata").get("RequestId"))
        print("========================== INPUT PROMPT ======================\n")
        print(PROMPT)
        handle_response_stream(response)
    except Exception as e:
        raise e
```
結果は以下のような形になりました。
```text
Request ID: 479df9f9-6aa6-432f-896e-6e99dd66a3d0
========================== INPUT PROMPT ======================

Please summarize this text:
========================= ANALYZE PROMPT =======================

{'message': 'Analysis of your prompt is complete'}
========================== OPTIMIZED PROMPT ======================

{'optimizedPrompt': {'textPrompt': {'text': '"Your task is to provide a concise summary of the given text enclosed in <context> tags. Follow these guidelines to produce a high-quality summary:\\n\\n<summarization_guidelines>\\n- Read the text carefully and thoroughly to understand its main points and key details\\n- Identify the central topic, main arguments, and most important supporting information\\n- Omit unnecessary details, examples, or redundant information to keep the summary concise\\n- Rephrase the key points using your own words rather than copying verbatim from the original text\\n- Aim for a summary length around 20% of the original text or less\\n- Ensure your summary accurately captures the essence of the text without omitting crucial information\\n</summarization_guidelines>\\n\\nText to summarize:\\n<context>\\n{{context}}\\n</context>\\n\\nPlease provide your concise summary within the following tags:\\n<summary>\\n[Your summary here]\\n</summary>"'}}}
```
訳してみると、こんな感じになりました。
Claudeモデルのパフォーマンス最適化において重要であると言われている[XMLタグの使用](https://docs.anthropic.com/ja/docs/build-with-claude/prompt-engineering/use-xml-tags)がなされたプロンプトに生まれ変わっていますね。

```text
「あなたのタスクは、<context>タグで囲まれた与えられたテキストの簡潔な要約を提供することです。以下のガイドラインに従って、高品質な要約を作成してください：

<summarization_guidelines>

テキストを注意深く、そして徹底的に読み、主なポイントと重要な詳細を理解すること
中心となるトピック、主張、最も重要な補足情報を特定すること
要約を簡潔に保つために、不必要な詳細、例、または冗長な情報を省くこと
元のテキストをそのままコピーするのではなく、自分の言葉で要点を言い換えること
要約の長さは元のテキストの約20%以内を目安とすること
重要な情報を省略せず、テキストの本質を正確に反映する要約を作成すること </summarization_guidelines>
要約対象のテキスト： <context> {{context}} </context>

以下のタグ内に簡潔な要約を提供してください：

<summary> [ここに要約を記載してください] </summary>
```


### Step3.Streamlitと組み合わせてみる
今回はフロントエンドをPythonコードで簡単に作成できる[Streamlit](https://streamlit.io/)を使って、
プロンプト最適化前と後でプロンプトがどのように変わるのか、それによってどのように回答が変わるのかを比較できるようなチャットボットを作成しました。
コードは以下のような形です。（必要に応じてプロファイル指定などはして下さい）


#### コード
コードは以下です。

:::note info
※ streamlitの[`st.session_state`](https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state)を使い、ボタン押下時に、入力値を保持できるようにしています。（通常だとボタン押下時、ページ全体が再読み込みされ、変数が全て初期化されてしまうため）
:::

```app.py
import boto3
import streamlit as st


# AWS Bedrockで使用するmodelID（Claude3 Sonnet）
modelId = "anthropic.claude-3-sonnet-20240229-v1:0"

# converseAPI利用のための各種設定値
temperature = 0.5
top_k = 200

inference_config = {"temperature": temperature}
additional_model_fields = {"top_k": top_k}

# AWSクライアントの初期化
def initialize_boto3_client():
    return boto3.client('bedrock-agent-runtime')

# 入力プロンプトを整形する関数
def get_input(prompt):
    return {"textPrompt": {"text": prompt}}

# プロンプト最適化を実行する関数
def optimize_prompt(client, prompt):
    try:
        response = client.optimize_prompt(
            input=get_input(prompt),
            targetModelId=modelId
        )
        return response
    except Exception as e:
        st.error(f"Error: {e}")
        return None

# 最適化されたプロンプトを抽出する関数
def extract_optimized_prompt(response):
    try:
        optimized_prompts = []
        event_stream = response['optimizedPrompt']
        for event in event_stream:
            if 'optimizedPromptEvent' in event:
                optimized_prompts.append(event['optimizedPromptEvent']['optimizedPrompt']['textPrompt']['text'])
        return " ".join(optimized_prompts)
    except Exception as e:
        st.error(f"Error extracting optimized prompt: {e}")
        return None

# モデルからレスポンスを取得する関数
def get_model_response(client, prompt):
    #モデル呼び出しのためbedrock-runtimeクライアントを生成
    client = boto3.client('bedrock-runtime')
    messages = [{
        "role": "user",
        "content": [{"text": prompt}],
    }]
    response = client.converse(
        modelId=modelId,
        messages=messages,
        inferenceConfig=inference_config,
        additionalModelRequestFields=additional_model_fields
    )
    return response["output"]["message"]["content"][0]["text"]


# Streamlitアプリの中身
def main():
    st.title("Amazon Bedrock Prompt Optimizer")

    # 初期化
    if "prompt" not in st.session_state:
        st.session_state.prompt = ""
    if "optimized_prompt" not in st.session_state:
        st.session_state.optimized_prompt = None
    if "original_response" not in st.session_state:
        st.session_state.original_response = None
    if "optimized_response" not in st.session_state:
        st.session_state.optimized_response = None

    # 入力フィールド
    st.session_state.prompt = st.text_area("プロンプトを入力", st.session_state.prompt)

    if st.button("プロンプトを最適化"):
        st.write("プロンプトを最適化しています...")

        # boto3クライアント初期化
        client = initialize_boto3_client()

        # プロンプト最適化
        response = optimize_prompt(client, st.session_state.prompt)
        if response:
            st.write("最適化が完了しました")

            # 最適化されたプロンプトの取得
            st.session_state.optimized_prompt = extract_optimized_prompt(response)
            st.session_state.original_prompt = st.session_state.prompt  # 保存

    # 最適化前と後のプロンプトの表示
    if st.session_state.optimized_prompt:
        st.subheader("最適化前のプロンプト:")
        st.text(st.session_state.original_prompt)
        st.subheader("最適化後のプロンプト:")
        st.text(st.session_state.optimized_prompt)

    # 比較ボタンの処理
    if st.session_state.optimized_prompt and st.button("回答を比較"):
        with st.spinner("回答を生成中..."):
            client = initialize_boto3_client()
            st.session_state.original_response = get_model_response(client, st.session_state.original_prompt)
            st.session_state.optimized_response = get_model_response(client, st.session_state.optimized_prompt)

    # 結果の表示
    if st.session_state.original_response and st.session_state.optimized_response:
        st.subheader("Claude3 Sonnetによる回答(最適化前のプロンプト):")
        st.text(st.session_state.original_response)
        st.subheader("Claude3 Sonnetによる回答(最適化後のプロンプト):")
        st.text(st.session_state.optimized_response)

if __name__ == "__main__":
    main()

```
:::note info
前提として
```bash
$ pip install boto3 streamlit
```
を実施して、boto3及びstreamlitが使用できるようにしておく必要があります。
:::

app.pyがあるディレクトリにて、下記を実行すると、`localhost:8501`でstreamlitの画面が立ち上がります。
```bash 
$ path/to/your/directory streamlit run app.py
```
初期画面はこんな感じです。デザインは簡素ですが、機能に焦点を当ててご紹介しています。

![スクリーンショット 2024-11-23 165139.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/710609fa-c423-3a49-25af-6df2473b4190.png)


初期画面が表示されたら、準備は完了です。今回は知る人ぞ知るSteve Jobsの、スタンフォード大学のスピーチ（英語）を要約してもらうこととします。
※[「ハングリーであれ。愚か者であれ」　ジョブズ氏スピーチ全訳（日経電子版）](https://www.nikkei.com/article/DGXZZO35455660Y1A001C1000000/)から引用させて頂きました。


#### ＜プロンプトを最適化＞ボタンを押下後の画面
`プロンプトを最適化`ボタンを押下すると、プロンプトの最適化処理が走ります。

その後、`回答を比較`ボタンが表示されるので、押下すると、最適化前（ユーザーが入力したプロンプト）、最適化後それぞれのプロンプトがモデルに渡され、ユーザーのリクエストに対する回答が表示される、という仕組みです。
![スクリーンショット 2024-11-23 165433.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/07bcdce2-8d67-4faf-9bd4-557fa3b488ef.png)


![スクリーンショット 2024-11-23 165521.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/315670c7-f158-45ed-2d2f-670bf149cc69.png)
![スクリーンショット 2024-11-23 165546.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/a911412c-1e2e-f7be-3db4-f5e9827a67aa.png)


元の文章が長く、どんなプロンプトになっているか分かりづらいため、以下に最適化前と後のプロンプトを載せておきます。
最適化後では大分構造的なプロンプトになっていることが分かります。
##### 最適化前
```text
Please summarize the following text.
（以下要約される文章が続く）
```

##### 最適化後
①タスクの概要
```text
Your task is to generate a concise summary of the following text enclosed in <context> tags:
```

②要約させる文章の提示
```text
<context>
[要約される文章]
</context>

```

③要約をする上でのガイドラインの指示
```text
<summarization_guidelines>
- Read the given text carefully to fully understand its content and main points
- Identify the key information, central ideas, and important supporting details
- Omit unnecessary details, examples, or repetitive statements to keep the summary concise
- Rephrase the main points using your own words rather than copying verbatim from the original text
- Aim for a summary length around 20% of the original text while preserving the crucial information and overall meaning
- Check that your summary is accurate, coherent, and conveys the essence of the original text without adding your own interpretation or opinions
</summarization_guidelines>

```

④出力形式の指示
```text
<summary>
[Your summary here]
</summary>
```


#### ＜回答を比較＞ボタンを押下後の画面
このような形で、回答を生成中・・・という表示が出るようにしました。
![スクリーンショット 2024-11-23 163824.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/3964001d-1e30-fe67-3023-f610d94fcf73.png)


#### 最終形態
最終形態はこんな感じです。上が最適化前のプロンプト、下が最適化後のプロンプトを渡した際の生成AI（Claude3 Sonnet）による回答となります。
![スクリーンショット 2024-11-23 163913.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/7332bc14-870b-c352-a30b-a288acf3ccb8.png)
どちらが良い要約か、というのは一概に判断が付きませんが、最適化後の方がより完結に、内容をまとめてくれている気がします。


添付画像のように、StreamlitとAmazon Bedrock Prompt optimizationを組み合わせることで
最適化前と後のプロンプトの違い、およびプロンプトによる回答の違いを比較することができました。そのため、一旦検証はここまでとします。

:::note warn
プレビュー版である本機能は、1日10個、アカウント単位で100個のプロンプト最適化までしか行うことができない事にご注意下さい。
※筆者はブログ執筆中に何度も使っていて途中で下記エラーレスポンスが返って来てしまい、あえなくその日の検証は終了。という形になってしまいました（笑）
![スクリーンショット 2024-11-23 145715_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3623937/2015b17d-da70-da5c-0133-0c4a149dca7d.png)

:::
## 最後に
今回は、Amazon Bedrockの最新機能であるprompt optimizationを試して、ちょっとした検証をしてみました。
本機能は、AIアプリケーション開発を行うエンジニアにとって頭を悩ませる「プロンプト設計」を考える上で、大いに役立つ機能だなと感じました。
2023年9月にGAされてから1年と少し経つAmazon Bedrockですが、日々新しい機能が増えていって便利になっていって、活用の幅もどんどん拡大しているなと感じます。もっと使いこなして、生粋のBedrockerになっていきたいなと思います！


***
次週（11月最終週）は@T-Hashi-motoさんの記事が投稿されます！お楽しみに！
併せて、Jr.Championsでは[Japan AWS Jr. Champions Advent Calendar 2024](https://qiita.com/advent-calendar/2024/japan-aws-jr-champions) も12月から実施しますのでこちらもご期待下さい！