Amazon Bedrockを使ったAPIを作ってみた件

# 記事を書こうと思ったきっかけ
勉強会のネタとしてBedrockを使ったAPIを作ることを実施しようと考えている。実施するために事前にやってみようと思った次第。

# システムの全体像
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272113/b09653a2-17c2-f481-40ea-80fcdcc51e49.png)

かなりシンプルな構成。1時間半いかないくらいでのボリュームで勉強会を実施したいので、なるべくシンプルに。

# 準備
今回はBedrockが東京リージョンでしか使えないので、API GatewayやLambdaも東京リージョンに作成。
Bedrockのモデルは「Claude 3.5 Sonnet v1」を利用

## Lambdaの設定
「Lambda」＞「関数の作成」を選択。
Pythonを指定し、IAMロールは名前を付けて新規作成。
まずは、基本設定などを編集。おそらくタイムアウトは3秒だと短いので30秒に設定。また、作成したIAMロールにBedrockを利用するためのIAMポリシー（AmazonBedrockFullAccess）を付与。

一旦動作確認のために以下のページを参考にコードを作成し、適当なプロンプトに対して正しく応答が得られるかを試してみた。

https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-runtime/client/invoke_model.html

https://docs.aws.amazon.com/bedrock/latest/userguide/model-parameters-anthropic-claude-messages.html

``` Python
import json
import boto3

bedrock_runtime = boto3.client('bedrock-runtime')
model_id = 'anthropic.claude-3-5-sonnet-20240620-v1:0'

request_body = json.dumps({
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 1000,
    "messages":[
        {
            "role":"user",
            "content":"Amazon Bedrockについて日本語で教えて。"
        }
    ]
})

def lambda_handler(event, context):
    # TODO implement

    response = bedrock_runtime.invoke_model(
        body = request_body,
        modelId = model_id,
        contentType = 'application/json'
    )

    parsed_response = json.loads(response.get('body').read())
    output_text = parsed_response['content'][0]['text']
    return {
        'statusCode': 200,
        'body': output_text
    }
```

以下のようなリターンが返ってきた。いけてそう。
``` json
Response:
{
  "statusCode": 200,
  "body": "Amazon Bedrockは、AWSが提供する完全マネージド型のサービスで、大規模言語モデル（LLM）や生成AI用のAPIを簡単に利用できるプラットフォームです。以下にBedrockの主な特徴を挙げます：\n\n1. 多様なAIモデル：\n   Anthropic、AI21 Labs、Stability AI、Amazon等の様々なプロバイダーのAIモデルにアクセスできます。\n\n2. サーバーレス:\n   インフラストラクチャの管理が不要で、使用した分だけ料金を支払う方式です。\n\n3. プライバシーとセキュリティ:\n   データはAWS内で暗号化され、プライベートエンドポイントの使用も可能です。\n\n4. カスタマイズ機能:\n   ファインチューニングやプロンプトエンジニアリングにより、モデルをカスタマイズできます。\n\n5. 統合性:\n   他のAWSサービスとシームレスに統合できます。\n\n6. スケーラビリティ:\n   需要に応じて自動的にスケールします。\n\n7. コスト効率:\n   必要な時に必要なだけリソースを使用できます。\n\nBedrockを使用することで、企業は複雑なAI技術を簡単に自社のアプリケーションやサービスに組み込むことができます。これにより、チャットボット、コンテンツ生成、データ分析など、様々な用途にAIを活用することが可能になります。"
}
```

Bedrockでモデルを呼び出し実行したい場合は`boto3.client`には`bedrock-runtime`を指定する必要がある。また、`bedrock-runtime`でモデルを呼び出し実行したい場合、`modelId`とモデルごとに決められた`body`を指定する必要がある。
今回ClaudeのMessages APIで必要な`body`において、必須のパラメータは以下の通り。
``` jsonc
{
    "anthropic_version": "bedrock-2023-05-31" //anthoropicのバージョン
    "max_tokens": 1000, //生成するトークンの最大数
    "messages":[ //プロンプトに入力するメッセージ
        {
            "role":"user", // 会話の順番の役割でuserもしくはassistantを指定
            "content":"Amazon Bedrockについて日本語で教えて。" // 会話内容。画像の指定も可能らしい。
        }
    ]
}
```
他にもオプションで回答のランダム性など指定可能。Anthoropic Claudeのバージョン2.1以降であれば、あらかじめ`system`パラメータで特定の目標や役割をインプットできる。例えば「あなたは優秀な校閲者です」的な。


次にAPI Gatewayから受け取ったデータをBedrockに投げる形に変更。
`system`パラメータで事前に役割を指定し、クエリパラメータで受け取った単語に対して応答を返すような形にしてみる。

```python
import json
import boto3
import logging

logger = logging.getLogger()
logger.setLevel("INFO")

bedrock_runtime = boto3.client('bedrock-runtime')
model_id = 'anthropic.claude-3-5-sonnet-20240620-v1:0'

def lambda_handler(event, context):
    # TODO implement

    logger.info(event)

    input_text = event['queryStringParameters']['input_text']

    request_body = json.dumps({
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 100,
    "messages":[
        {
            "role":"user",
            "content":input_text
        }
    ],
    "system":"あなたは有名でとても面白い芸人です。以下の単語に基づいてギャグを生成し、ギャグのみを出力してください。"
})

    response = bedrock_runtime.invoke_model(
        body = request_body,
        modelId = model_id,
        contentType = 'application/json'
    )

    parsed_response = json.loads(response.get('body').read())
    output_text = parsed_response['content'][0]['text']
    return {
        'statusCode': 200,
        'body': json.dumps({
            'output_text': output_text
        }, ensure_ascii=False),
        'isBase64Encoded':False,
        'headers':{}
    }
```

## API Gatewayの設定
AWSコンソールより「API Gateway」＞「API」＞「APIを作成」をクリックし、「REST API」で構築を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272113/bd16c56d-c259-7750-94e9-bfcbcc39af08.png)

API名は適当につけてAPIエンドポイントタイプは「リージョン」を選択。これでリソースが作成されるので、「メソッドを作成」をクリックし、メソッドの作成を進める。
メソッドタイプをPOSTとしLambda関数との統合を設定。Lambdaプロキシ統合もONにした。そして、作成したLambdaを指定し、URLクエリ文字列パラメータでAPI呼び出し時に必要な情報を設定した。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272113/dd4cd828-acd6-43e9-67e0-92563947d434.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272113/a1eeba70-037a-8cf0-391c-37f97a5608e8.png)

# いざテスト・・・！

Postmanを使ってAPIを実行してみる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272113/acc74ddc-22bd-a79b-082b-d7173c48c276.png)

「飴があめぇ」くらいな感じで返すのかと思いきや、意外と一捻りしてきた回答。。。

# 今回詰まったところ
## Boto3のBedrockの使い方
Boto3にはBedrock ClientとBedrockRuntime Clientがあり、最初はBedrock Clientの方を参考にしていた。このBedrock ClientはFine-tuningなどモデルを調整したり評価したりするために使われる様子。

## json.dumpsの扱い
`ensure_ascii=False`を指定しておらず、文字化けしてしまっていた。デフォルトがTrueであるため、ASCII文字を含む場合は自動的にエスケープしないようにFalseの設定が必要とのこと。

# 解決していない部分
連続でリクエストすると、Internal server errorが返ってくる。
ログを見てみると「ThrottlingException」というのが出ている。何度もリクエスト送っているため制限がかかっている模様。これの対処については追々考えようと思う。


# まとめ
とりあえず勉強会用の簡単な構成は作れたので、これをもとに資料を作っていこうと思う。
逆に短すぎるような気もしてきた。。。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/272113/bfd6cf39-6bc9-9af6-2623-fda7e5995659.png)