OpenAIの代替としてAmazon Bedrockを使用する

本記事はLabBaseの[Advent Calendar 2024](https://qiita.com/advent-calendar/2024/labbase) の23日目です。

## はじめに
2024年12月12日にOpenAIで障害発生しましたね。最近ではOpenAIをサービスに組み込んで使用することも増えてきて、障害時の影響が大きくなるケースも増えてきたのではないかと思います。
ちょうどAWSのLambdaでPythonでテキスト要約のサービスを作っていたので、そのような時の代替手段としてAmazon Bedrockを使うパターンを試してみました。

## モデルへのアクセスをリクエストする
Bedrockで用意されているカタログのモデルを使うには、事前にアクセスのリクエストをする必要があります。今回はAnthropicのClaude 3.5 Sonnetのアクセスを使ってみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/252317/fa82ad91-2bf6-1912-01fc-2c5a10317b5b.png)

## Lambdaに割り当てるIAM roleにモデルへのアクセスポリシー付与
terraformでAWSリソースを管理しているので、以下のようにポリシーを追加します。テキスト要約だけであればInvokeModelで十分ですがあればやりたいことに合わせてポリシー追加する必要があります。

```HCL
resource "aws_iam_role" "lambda" {
  name = "lambda-role"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}

resource "aws_iam_policy" "bedrock_policy" {
  name        = "bedrock-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "bedrock:InvokeModel",
        ]
        Resource = "arn:aws:bedrock:ap-northeast-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "bedrock_policy_attachment" {
  role       = aws_iam_role.lambda.name
  policy_arn = aws_iam_policy.bedrock_policy.arn
}
```

## Bedrockを使用したテキスト要約
テキスト要約を行い、結果を返す関数を定義します。

```Python
import boto3
import json
import logging
from botocore.exceptions import ClientError

logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.INFO)

def summarize_with_bedrock(prompt, model_id, summary_length):
    bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='ap-northeast-1')

    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": summary_length,
        "system": "あなたはxxxです。",
        "messages": [{"role": "user", "content": prompt}]
    })

    try:
        response = bedrock_runtime.invoke_model(body=body, modelId=model_id)
        response_body = json.loads(response.get('body').read())
        summary = response_body['content'][0]['text']
        return summary
    except ClientError as err:
        message = err.response["Error"]["Message"]
        logger.error("A client error occurred: %s", message)
        raise
```

## Lambdaハンドラーの実装
OpenAIのエラー時に有効化したモデルを指定してBedrockを使うようにします。

```Python
def lambda_handler(event, context):
    openai_api_key = secrets['OPENAI_API_KEY'] # Secrets Managerから取得
    try:
        openai.api_key = openai_api_key
        summary = []
        response = openai.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": "あなたはxxxです。"},
                {"role": "user", "content": f"{user_template}:\n\n{target_text}"}
            ],
            max_tokens=max_tokens,
            temperature=0,
        )
        summary = response.choices[0].message.content.strip()
    except Exception as e:
            print(f"OpenAI API call failed: {str(e)}")
            summary = summarize_with_bedrock(f"{user_template}:\n\n{full_text}", 'anthropic.claude-3-5-sonnet-20240620-v1:0', summary_length)
    
    return {
        'statusCode': 200,
        'body': json.dumps({'summary': summary})
    }
```

## 要約を検証
検証のため、青空文庫の作品「[赤ずきん](https://www.aozora.gr.jp/cards/001091/card59835.html)」の文章をインプットとして、200文字以内に要約してみました。

```Python
    # プロンプト
    user_template = f"""
    物語を正確に要約してください。要約の際、以下の条件を守ってください。
    1. {summary_length}字以内で要約し、字数を超過しないようにしてください。
    2. 文章が不自然に切れないようにしてください。
    3. 起承転結の流れに沿って記載してください。
    4. 物語の中の情報以外の情報や単語を含めないようにしてください。
    """
```

Open AIが有効な時に実行すると以下の通りの要約結果を取得できます。
```
昔、かわいい女の子がいて、赤いずきんをかぶっていたため「赤ずきんちゃん」と呼ばれていました。ある日、病気のおばあさんにお菓子とブドウ酒を届けるよう母に頼まれます。森でオオカミに出会い、道を逸れて花を摘む間に、オオカミはおばあさんを飲み込みます。赤ずきんちゃんも飲み込まれますが、狩人がオオカミを倒し、二人を救出。赤ずきんちゃんは教訓を得て、もう森の横道に入らないと決意します。
```

Open AIへアクセスできない状態としてAPIキーを無効にした状態で実行すると、以下の通りの要約結果を取得できました。大きな問題はなさそうです。

```
OpenAI API call failed: Error code: 401 - {'error': {'message': 'Incorrect API key provided: k-proj-K***********************************************************************************************************************L38A. You can find your API key at https://platform.openai.com/account/api-keys.', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_api_key'}}

むかし、誰からも愛される赤ずきんちゃんがいました。ある日、病気のおばあさんに食べ物を届けに行く途中、森でオオカミに出会います。オオカミは先回りしておばあさんを食べ、赤ずきんちゃんも飲み込んでしまいます。通りかかった狩人がオオカミの腹を切り、二人を助け出しました。赤ずきんちゃんは母の言いつけを守ることの大切さを学びました。
```