【AmazonBedrock】ビッグテックの中の人ができないことを考えたらLLMを無駄に集結させるところに行き着いた

本記事は「**Amazon Bedrock Advent Calendar 2023**」、12月20日の記事です。

https://qiita.com/advent-calendar/2023/amazon-bedrock-generative-ai-aws

## はじめに
こんにちは、KDDIアジャイル開発センターのはしもと（仮名）です。
**Amazon Bedrock 大好き！Amazon Bedrock 大好き！**

アドベントカレンダーを書いているということは今年ももうすぐ終わりというわけです。早いですね。

IT業界で今年最も話題になったものといえば、間違いなく「生成AI」でしょう。
一時は OpenAI・Microsoft 連合の一強かと思われましたが、ビッグテック各社、独自の大規模言語モデルを開発するだけでなく、自社サービスに生成AIを統合するといった動きも盛んで、まだまだ競争が収まる気配はありません。

### 待望の Amazon Bedrock の登場
#### 登場...?
ChatGPT が巷を席巻した今年4月、AWS は生成AIサービスである **Amazon Bedrock** を発表しました。
全国のAWSエンジニアは **来たか...！** と胸を躍らせたのではないでしょうか。

https://aws.amazon.com/jp/bedrock/

ただ....ええ....あの...なかなかGAされなくて.......
AWSさーん...おーい....Amazon Bedrockまだうちに来てないです.......
~~Azureつらいのではやくしてくれないかなー...なんて.....~~

待ちに待ちすぎて街ができました

#### 登場！！！！！
re:Invent まで引っ張るつもりかと思い始めた2023年9月28日、**Amazon Bedrock が正式リリースとなりました！！！**

https://aws.amazon.com/jp/blogs/news/amazon-bedrock-is-now-generally-available-build-and-scale-generative-ai-applications-with-foundation-models/

選択できる基盤モデルには、Amazon が開発したものだけでなく、Stable Diffusion で有名な **Stability AI**, Llama2 を開発した **Meta**, 元OpenAI のスタッフが立ち上げた **Anthropic** の Claudeなど、分野を牽引する企業のものから自由に選択して使用することができます。

基盤モデルだけではありません。毎年11-12月ごろに開催される AWS re:Invent では、大注目される中、Bedrock のさまざまなアップデートが発表されました。

https://dev.classmethod.jp/articles/update-reinvent2023-bedrock-summary/

12月に入る前にこれだけアップデートがあれば、ブログのネタには困りませんね。
今年のアドベントカレンダーは大丈夫そうです、おつかれっした！

### アドカレで書くことない( ^ q ^ )
気づいておりました。AWSエンジニア、Bedrock に興味津々なのです。
毎日投稿される検証ブログやSNSの投稿はもちろん、勉強会のテーマになれば数百人が参加するほどの大盛況。

このアドベントカレンダーも公開されるやいなや続々とエントリーが入り、気づけばシーズン2ができています（！）。

サービスの入門記事、re:Invent 2023 で発表された新機能の解説、Bedrock で使用できる言語モデルのプロンプトのお作法などなど。

ここで私はアドベントカレンダーに正面から挑むことを諦めました。

## ビッグテックの中の人ができないことをやろう
稚拙だとかネタだとか思われてもいい、誰もやっていないことをやろう。
もっといえばつよつよエンジニアができないことをやろう。

そして思いつきました、**OpenAI(Microsoft) や Google の 言語モデルも使ってしまおう**と。

:::note info
**Agents for Amazon Bedrock 使うからアドベントカレンダーのテーマ的にもきっとセーフ！**
:::

### やること
1つの質問に対して **AWS (Claude V2)**, **OpenAI (GPT-4)**, **Google (Gemini Pro)** の超精鋭LLMたちから回答を取得できるようにします。

各言語モデルへのアクセスは、新機能「**Agents for Amazon Bedrock**」を使ってよしなにやってもらいます！！！

https://aws.amazon.com/jp/bedrock/agents/

### 構成図
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/0954b433-29ed-9fe2-a7e7-0491631e4984.png" width="450px">

- 操作は AWSマネジメントコンソール > Bedrock のサービス画面から行います。
- Claude V2 をベースの言語モデルとしてエージェントを構成します。
- このエージェントは必要に応じてLambda関数を実行し、Claude V2, GPT-4, Gemini ProのAPIを呼び出し、得られた回答を返します。
- S3バケット内には、APIスキーマを記述したyamlファイルを事前に準備し、アクショングループ作成時にこのスキーマを利用するよう、オブジェクトのURIを指定しておきます。

## 最終成果物
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/d684e633-91b7-f04d-82b9-335a0b1a296f.png" width="300px">

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/68e40c0e-698c-f12a-413b-a77755720759.png" width="500px">

3つのLLMたちに不労所得について質問した様子です、なぜこんなことになったのか。

:::note info
「Gemini」という文字列を出力させたかったのですが、`<REDACTED>`にマスクされてしまいました。
大人の事情でしょうか。
:::


## 実装
### Gemini Pro さ〜〜〜ん！！！
初めに、各LLMにリクエストを送信するためのLambda関数を作成します。

リクエスト、レスポンスのフォーマットが決まっているのに注意し、公式ドキュメントや他の方の検証記事を参考に実装を行います。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/agents-create.html

https://qiita.com/nasuvitz/items/c34419150eaee78544cc

実装は以下のとおりです。
`apiPath` が `/ask-gemini`の場合に、Gemini Pro にリクエストを送信します。
**※必要なライブラリは別途レイヤーを作成し、関数に追加しています**

<details><summary>Lambda実行スクリプトとAPIスキーマ</summary><div>

```python:lambda_function.py
import os

PROMPT = """
Answer the following question in Japanese.

Question: {}
"""


def lambda_handler(event, context):
    print(event)

    api_path = event["apiPath"]
    question = event["requestBody"]["content"]["application/json"][
        "properties"
    ][0]["value"]

    content = PROMPT.format(question)

    if api_path == "/ask-gemini":
        import google.generativeai as genai

        genai.configure(api_key=os.getenv("GOOGLE_API_KEY"))
        model = genai.GenerativeModel("gemini-pro")

        response = model.generate_content(content)

        response_body = {"application/json": {"body": response.text}}

        action_response = {
            "actionGroup": event["actionGroup"],
            "apiPath": event["apiPath"],
            "httpMethod": event["httpMethod"],
            "httpStatusCode": 200,
            "responseBody": response_body,
        }
        api_response = {"messageVersion": "1.0", "response": action_response}
        print(api_response)
        return api_response
```

:::note info
実装は Google の公開しているドキュメントを参考にしています
https://ai.google.dev/tutorials/python_quickstart
:::

```yaml
openapi: 3.0.0
info:
  title: "get answer from external LLMs"
  version: 1.0.0
paths:
  /ask-gemini:
    get:
      summary: "Ask Gemini"
      description: "You can get answer from Gemini, which is a large language model created by Google."
      operationId: gemini
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                question:
                  type: string
                  description: question to ask Gemini
              required:
                - question
      responses:
        "200":
          description: "success"
          content:
            application/json:
              schema:
                type: object
                properties:
                  body:
                    type: string
```
</div></details>

#### Lambda実行ログ
Bedrock Agent の設定を行った上で、`日本の大都市を3つ教えて`と聞いてみます。
Lambdaの実行ログに、eventの内容と、LLMが生成した回答を含むレスポンスが確認できます。やったね！！！！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/38e7f049-bfd4-95e8-94f0-b411cb409923.png" width="500px">


このあたりからある感情が湧いてきます。私はなにをやっているんだろうと。

### GPT-4 さ〜〜〜ん！！！
この調子で進めます。後述する Action Group を別で作成した上で、Lambda関数の実装とAPIスキーマを修正します。

GPT-4へのリクエストに対応したLambda関数の実装と、新たに作成したAPIスキーマの詳細はこちらです。
`apiPath`が`/ask-gpt4`の場合の処理を追加しています。

<details><summary>Lambda実行スクリプトとAPIスキーマ</summary><div>

```python:lambda_function.py

　　（中略）

    if api_path == "/ask-gpt4":
        from openai import OpenAI

        client = OpenAI()

        client.api_key = os.environ.get("OPENAI_API_KEY")
        messages = [
            {"role": "user", "content": content},
        ]
        response = client.chat.completions.create(
            model="gpt-4", messages=messages
        )
        response_body = {
            "application/json": {"body": response.choices[0].message.content}
        }
        action_response = {
            "actionGroup": event["actionGroup"],
            "apiPath": event["apiPath"],
            "httpMethod": event["httpMethod"],
            "httpStatusCode": 200,
            "responseBody": response_body,
        }
        api_response = {"messageVersion": "1.0", "response": action_response}
        print(api_response)
        return api_response
```

```yaml
openapi: 3.0.0
info:
  title: "get answer from external LLMs"
  version: 1.0.0
paths:
  /ask-gpt4:
    get:
      summary: "Ask GPT-4"
      description: "You can get answer from GPT-4, which is a large language model created by OpenAI."
      operationId: gpt4
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                question:
                  type: string
                  description: question to ask GPT-4
              required:
                - question
      responses:
        "200":
          description: "success"
          content:
            application/json:
              schema:
                type: object
                properties:
                  body:
                    type: string

```
</div>
</details>

#### Lambda実行ログ
先ほどと同様の質問をしてみます。
`ask-gemini`と`ask-gpt4`両方のアクショングループが呼び出されています。セッションIDが同一なので、Bedrock Agent の一連の処理の中でこれらが連続して実行されたようです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/dee3c7f5-7244-564c-c99b-02ce2c95e1d0.png" width="500px">

### Claude V2 さ〜〜〜ん！！！
最後はおなじみの Claude V2 です。
説明は割愛し、Pythonスクリプトの追加箇所およびAPIスキーマのみ記載します。

<details><summary>Lambda実行スクリプトとAPIスキーマ</summary><div>

```python:lambda_function.py

　　（中略）

    if api_path == "/ask-claudev2":
        import boto3
        import json

        bedrock_runtime = boto3.client(
            service_name="bedrock-runtime", region_name="us-east-1"
        )
        modelId = "anthropic.claude-v2"
        accept = "application/json"
        contentType = "application/json"

        content = "\n\nHuman:" + content + "\n\nAssistant:"

        body = json.dumps({"prompt": content, "max_tokens_to_sample": 200})

        response = bedrock_runtime.invoke_model(
            body=body, modelId=modelId, accept=accept, contentType=contentType
        )

        decoded_response = response.get("body").read().decode()

        response_body = {
            "application/json": {
                "body": json.loads(decoded_response)["completion"]
            }
        }
        action_response = {
            "actionGroup": event["actionGroup"],
            "apiPath": event["apiPath"],
            "httpMethod": event["httpMethod"],
            "httpStatusCode": 200,
            "responseBody": response_body,
        }
        api_response = {"messageVersion": "1.0", "response": action_response}
        print(api_response)
        return api_response
```

```yaml
openapi: 3.0.0
info:
  title: "get answer from external LLMs"
  version: 1.0.0
paths:
  /ask-claudev2:
    get:
      summary: "Ask claudev2"
      description: "You can get answer from ClaudeV2, which is a large language model created by Anthropic."
      operationId: claudev2
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                question:
                  type: string
      responses:
        "200":
          description: success
          content:
            application/json:
              schema:
                type: object
                properties:
                  body:
                    type: string
```

</div></details>

#### Lambda実行ログ
再度同じ質問を投げると、ClaudeV2にもリクエストしていることが分かります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/ed62342e-a5e6-80ff-aabc-ba625daa2406.png" width="500px">

何やら不穏ですね。ただやりたかったことはこれでできました！

### Bedrock Agents の設定
Bedrock Agents の設定についても、抜粋して記載します。

#### 全体の構成について
`Action groups` を使用する言語モデル単位で作成しています。
当初、ひとつのアクショングループで試していたのですが、複数APIの実行がうまくいかなかったためこのようになりました。
このあたりの仕様について記載されているドキュメント等あれば教えていただけるとありがたいです。

Knowledge basesは不要のため何も設定していません。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/17a8e749-8481-4ef9-d2cb-f5c1a70f936e.png" width="300px">


#### Post-processing template の上書き
出力フォーマットに関する指示を Agent の Instruction に書いたのですが、うまく効きませんでした。
そこで、`Advanced prompts`から、`Post-processing`のテンプレートに一部追記する形でフォーマットの指示を与えたところ、それっぽい出力を得ることができました。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/bea227f1-cad6-9625-d4de-5f5f272fada0.png" width="300px">

## まとめ
Agents for Amazon Bedrock を使って（無理やり）複数のLLMを呼んでみました。

お手軽便利機能だと思っていましたが、ちゃんと触ってみると結構複雑で、一見問題なさそうな入力でも弾かれてしまうなど、使いこなせるようになるにはなかなか骨が折れそうだなと思いました。

まだまだ謎多き Agents for Amazon Bedrock。
アドベントカレンダーにも同機能に関する記事がたくさんあるので、年末年始はじっくり勉強しようと思います。

はしもと（仮名）でした。

## Appendix
何もコメントはしませんが、これを貼って締めたいと思います。
太平の世は訪れるのだろうか...

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/463957/5cb33e01-88a3-7458-8252-932211bc2107.png" width="400px">