re:Invent 2023でGAされたAgents for Amazon BedrockとAmazon Kendraを組み合わせてRAGを構築

この記事は[Japan AWS Jr. Champions Advent Calendar 2023](https://qiita.com/advent-calendar/2023/jr-champions) 16日目の記事です！

## Agents for Amazon Bedrockとは
re:Invent 2023よりGAが発表されたAmazon Bedrockの新機能です。

通常の基盤モデルの場合、質問すると回答してくれるのみのため、実際のタスクはその回答を読んだ人間が行う必要がありました。

これに対し、**Agents for Amazon Bedrock** では、質問に対して何をすべきかを基盤モデル自身が**思考**し、**計画**し、**実行**してくれます。さらに事前に関数を設定することで、質問に対し必要であれば関数を実行する、ということまで自律して行ってくれます。

[LangChain Agent](https://zenn.dev/umi_mori/books/prompt-engineer/viewer/langchain_agents)に機能としては似ています。

![IMG_9357.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/21788388-5725-56bc-b7d5-43e95465f8a2.jpeg)
*Photo by 石原*

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

また、**Knowldge base for Amazon Bedrock**もAgents for Amazon Bedrockと同時にGAが発表されたBedrockの新機能です。

S3にデータを配置し、「Create Knowledge base」を選択するだけで簡単に**RAG（Retrieval Argumented Genaration、検索拡張性）** を構築することができます。
情報を参照するためのインデックスとしては、Amazon OpenSearch Serveless, Pinecone, Redis Enterprise Cloud のいずれかから選択が可能です。

さらにAgents for Amazon Bedrockと統合されており、GUIによる設定のみでエージェントを介したRAGを構築することができます。

https://www.ibm.com/blogs/solutions/jp-ja/retrieval-augmented-generation-rag/

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/knowledge-base.html

## Amazon Kendraとは
**Amazon Kendra** とはエンタープライズサーチの機能を有したAWSのサービスです。
昨今Bedrockと組み合わせたRAGの用途ととしても非常に注目されています。

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

## Bedrock+KendraによるRAGと、Knowledge baseの違い
LLMが情報を参照するインデックスが異なるため、コスト、検索能力、データソースの種類などが異なります。

検索機能に関しては、Amazon Kendraは**全文検索とセマンティック検索**、Knowledge Base for Amazon Bedrockでは**ベクター検索**が用いられます。

また、仕様としてKnowledge Baseが参照できるデータソースは**S3のみ**であり、Amazon Kendraは**約40種類**ものデータソースを提供しております。

さらに、Amazon Kendraは書き込む際に、コンテンツに**メタ情報を付与**したり、パスを指定のURLに**変更**したりと自由にコンテンツをカスタマイズすることが可能です。
Knowledge baseがインデックスに書き込む情報はテキストとそのテキストのベクトル、そしてS3パスの3つのみです。

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/knowledge-base-setup.html

https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/hiw-data-source.html

以上のことから、エージェントを介したRAGを構築したい場合にKnowledge baseより**Kendraを採用したい**ケースが存在すると考えられることから、その検証を行った結果をご紹介いたします。

# 検証
今回はAgents for Amazon BedrockがKendraから検索して情報を取得して回答するRAGを構築しました。
その際に、Kendraの機能を使ってコンテンツのパスを指定のURLに変更し、**RAGが回答する際に指定したURLも添えてくれる**、ということを目指しました。

## Bedrockが参照するコンテンツの用意
今回は私が以前書いた記事のHTMLファイルを使用します。

https://qiita.com/Naoki_Ishihara/items/7bfefc5a4750aa50c58e

S3に用意したHTMLファイルと、そのコンテンツをカスタマイズするためのメタデータファイルを用意します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/3513daac-92e8-23af-711f-e442070a1363.png)

メタデータファイル
```
{
    "Attributes": {
        "_source_uri": "https://qiita.com/Naoki_Ishihara/items/7bfefc5a4750aa50c58e"
    },
    "Title": "Amazon KendraにおけるWebクローラーコネクタの有用な機能とその設定方法（追記：Amazon Q For Business UseにおけるWebクローラーについて）",
    "ContentType": "HTML"
}
```
そして、KendraのS3コネクタを使用して同期します。

検証としてKendraのインデックスに検索をかけてみると、ちゃんとコンテンツにタイトルが追加されたり、パスが指定のURLに変更されていることが分かります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/f4ed3477-8e0f-a4b7-84fe-a1e71f95d86e.png)

https://docs.aws.amazon.com/ja_jp/kendra/latest/dg/s3-metadata.html

## エージェント作成
Bedrockのナビゲーションペインから「Agenta」→「Create Agent」を選択します。
### Step1：エージェントの詳細
エージェント名以外の設定はデフォルトを設定し、Nextを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/52d1367c-607e-7522-606d-36e7c2b93ef9.png)

### Step2：モデル選択
モデルはClaude v2を選び、
「Instructions for the Agent」に、システムプロンプトを入力します。
どのようなプロセスで対応してほしいかをステップバイステップで記述します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/715d8549-9c1f-e962-5691-63ecf9c172a8.png)

```
あなたは、Amazon KendraのWebクローラーの設定に関するエキスパートです。質問に対し、以下の手順で対応してください。各ステップは厳格に実施し、指示に従ってください。
1. 質問に対し、あなたは適切なキーワードを決定し、Amazon KendraのWebクローラーに関するドキュメントに対し検索を行います。検索の実行は、私の指示を仰ぐことなく実行してください。 
2. 検索結果の出力： 検索を行って情報を取得した場合、ソースURLは必ず私に表示をしてください。これは例外なく守ってください。	
```
### Step3：アクショングループ追加
アクショングループとしてLambda関数と、その入力と出力を定義するOpneAPI.ymlテンプレートを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/2206d5b7-c14c-2145-68f9-705c5adae3eb.png)

#### Lambda関数

アクショングループでLambdaを呼び出す際のイベントコンテキストは下記ドキュメントで定義されています。

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

こちらを利用して、Kendraの検索結果を取得する下記Lambda関数を作成しました
```python
import boto3  # boto3ライブラリをインポート
import json  # jsonライブラリをインポート

kendra = boto3.client('kendra')  # AWS Kendraサービスのクライアントを生成

def search(search_keyword):  # 検索キーワードを引数にとる検索関数
    query_text = search_keyword  # 検索キーワードをクエリテキストとして設定
    index_id = '<INDEX_ID>'  # KendraのインデックスIDを設定

    # Kendraにクエリを送信し、レスポンスを受け取る
    response = kendra.retrieve(
        QueryText=query_text,
        IndexId=index_id,
        # 日本語(_language_code = "ja")のドキュメントに絞り込むための属性フィルターを設定
        AttributeFilter={
            "AndAllFilters": [
                {
                    "EqualsTo": {
                        "Key": "_language_code",
                        "Value": {"StringValue": "ja"}
                    }
                },
            ]
        }
    )

    extracted_results = []  # 結果を格納するリストを初期化
    # response['ResultItems']の最初のアイテムだけを抽出して保存
    for item in response['ResultItems'][:1]:
        extracted_results.append({
            'DocumentTitle': item.get('DocumentTitle'),  # ドキュメントのタイトル
            'Content': item.get('Content'),  # ドキュメントの内容
            'DocumentURI': item.get('DocumentURI')  # ドキュメントのURI
        })
        
    # 検索結果を含む辞書を返す
    return {
        "QueryId": response['QueryId'],  # クエリID
        "Results": extracted_results  # 抽出された検索結果
    }
    
def lambda_handler(event, context):  # Lambdaのメインハンドラ
    print(event)  # イベントデータをプリント

    parameters = event.get("parameters", [])  # イベントからパラメータを取得
    if not parameters:  # パラメータがない場合はエラーメッセージを返す
        return {
            "statusCode": 400,
            "body": json.dumps({"message": "No parameters provided."})
        }

    # parametersの最初の要素のvalueをsearch_keywordとして使用
    search_keyword = parameters[0]["value"]
    print(f'Search keyword: {search_keyword}')  # 検索キーワードを出力

    api_path = event.get("apiPath", "")  # イベントからAPIパスを取得
    body = None  # レスポンスボディの初期化

    if api_path == "/search":  # APIパスが/searchの場合は検索関数を実行
        body = search(search_keyword)

    elif api_path == "/submit":  # /submitパスはこの例では実装されていない
        # submitアクションが実装されていないため、エラーメッセージを返す
        return {
            "statusCode": 400,
            "body": json.dumps({"message": "Submit action not implemented."})
        }

    if body is None:  # APIパスが不正な場合はエラーメッセージを返す
        return {
            "statusCode": 400,
            "body": json.dumps({"message": "Invalid API path."})
        }

    # レスポンスボディをJSON形式でエンコーディングして返す
    response_body = {
        "application/json": {
            "body": json.dumps(body, ensure_ascii=False)
        }
    }

    # アクションレスポンスを作成
    action_response = {
        "actionGroup": event.get("actionGroup"),  # アクショングループ
        "apiPath": api_path,  # APIパス
        "httpMethod": event.get("httpMethod"),  # HTTPメソッド
        "httpStatusCode": 200,  # HTTPステータスコード
        "responseBody": response_body,  # レスポンスボディ
    }

    # APIレスポンスを作成
    api_response = {
        "messageVersion": "1.0",  # メッセージバージョン
        "response": action_response  # アクションレスポンス
    }

    return api_response  # 最終的なレスポンスを返す
```
クエリAPIは戻り値がシンプルなretrieve APIを使用しました。

:::note warn
Kendraのretrieve APIが対応しているboto3のバージョンがデフォルトのpythonランタイムだと対応していなかったので手動でレイヤー追加を行いました。
:::

#### OpenAPIスキーマ
OpenAPIスキーマはこちらを設定しました。
```yml
openapi: 3.0.0
info:
  title: 'Test API'
  description: "APIs for testing Agents' behavior"
  version: '1.0.0'
paths:
  /search:
    get:
      summary: 'Web Crawler Document in Amazon Kendra'
      description: |
        この関数は、Amazon KendraのWebクローラーコネクタに関するドキュメントを検索し、情報を取得するAPIです。検索することで、タイトル、ドキュメントの内容、ドキュメントのURIをJSON形式の配列で必ず返します。
      operationId: searchFaq
      parameters:
        - in: query
          name: q
          description: 検索キーワード
          required: true
          schema:
            type: string
      responses:
        '200':
          description: 検索結果を返します。
          content:
            application/json:
              schema:
                type: object
                properties:
                  QueryId:
                    type: string
                    description: 検索クエリのID
                  Results:
                    type: array
                    description: 検索結果のリスト
                    items:
                      type: object
                      properties:
                        DocumentTitle:
                          type: string
                          description: ドキュメントのタイトル
                        Content:
                          type: string
                          description: ドキュメントの内容
                        DocumentURI:
                          type: string
                          description: ドキュメントのURI
```
このスキーマに基づきエージェントがどの関数を呼び出すか判断を行います。

https://docs.aws.amazon.com/bedrock/latest/userguide/agents-api-schema.html

### Step4：ナレッジベースを追加
Knowledge baseを追加することができますが、今回は必要がないため飛ばします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/4f9a169d-52ef-7a63-e7eb-b89319908a38.png)
以上の設定によりエージェントを作成します。

## テスト
エージェント作成後、右横の画面ですぐにテストすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/d299c2d3-98d0-8efc-1bc0-ae00f359dcbf.png)

質問：KendraのWebクローラーの設定において、クローラ深さはどのように設定すればよいですか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/10c03f8f-d23f-6246-f452-799f2586a61c.png)

ソースを基に回答をしてくれていることが分かります。
しかしながら、プロンプトで命令していたソースURLを表示してくれません。

どのような推論をしているか「Show trace」→「Orchestration and knowledge base」→「Step 2」を選択して中身を覗いてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/abff705f-efc3-f9ca-397c-dea3b4c6445f.png)

エージェントは「Kendra web crawler クローラ深さ 設定」というキーワードで検索していることが分かります。
そしてタイトル、コンテキスト、ドキュメントURIをきちんと渡せていることが分かります。

Step3を覗いてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/613b5a44-a61c-8d32-8d46-ecc5bf11d399.png)

生成された最終的な回答ではURIが表示されていません。

どうやらLLMが取得したURLを表示させることが出来ていないことが分かります。

## Advanced Promptsについて(未検証)

Agents for Amazon Bedrockでは、エージェンが推論を行うためのプロンプトテンプレートを編集することができ、LLMの動作をより詳細に制御することが可能な**Advanced Prompts**という機能が提供されています。

その設定画面を見ると、エージェントは
Pre-processing → Orchestration → KB response generation → Post-processing　という順番で推論していることが分かります。

そして、推論毎にプロンプトを編集したり、Lambda関数を設定することが可能なようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988434/5ee510a6-53c9-9270-2bd2-11718e41ec64.png)

https://docs.aws.amazon.com/ja_jp/bedrock/latest/userguide/advanced-prompts.html

次回は、これをプロンプトエンジニアリングで編集することで、より命令に忠実にLLMが回答させる検証を行ってみようと思います。