【AWS Skill Builder】『Building Generative AI Applications Using Amazon Bedrock』コースをやってみた - その 3

## はじめに
[AWS Skill Builder](https://aws.amazon.com/jp/training/digital/)にある『[Building Generative AI Applications Using Amazon Bedrock](https://explore.skillbuilder.aws/learn/courses/17904/building-generative-ai-applications-using-amazon-bedrock/lessons)』というコースを紹介する記事の<strong>「その 3」</strong>になります。

### 記事の構成
その 1：[BedrockとLangChainについて](https://qiita.com/tripleO/items/3446fdb2982441399bfd)
その 2：[RAGについて](https://qiita.com/tripleO/items/4c342195027f9e37e1a0)
**その 3：この記事**

:::note
Skill Builderのコース説明や記事の概要などは、その1の記事をご覧ください。
:::

今回のその3では、コース内の*Demo 3*の内容に即して、エージェントに関する知識を取り扱います。

## エージェント
エージェントに関しては、[その1の記事](https://qiita.com/tripleO/items/3446fdb2982441399bfd#%E3%82%A8%E3%83%BC%E3%82%B8%E3%82%A7%E3%83%B3%E3%83%88)でも登場し軽く概要を載せました。
> AIアプリケーションとしてのエージェントとは、定義されたアクションやワークフローを使って複雑なタスクをこなすシステムを指します。エージェントは、関数やAPI、RAG、コーディングなどのツールが渡され、ユーザーからのインプットや状況に応じてそれらを使い分け、求められている回答を自律的に生成します。

その1では、***LangGraph***を使ったエージェントの実装を紹介しましたが、本記事では**Amazon Bedrock Agents**での実装がメインになります。

### エージェントのユースケース
エージェントは概要だけ聞いてもアプリケーションのイメージはほとんど掴めないと思いますが、AWSが代表的なユースケースのコードサンプルを公開しています。

https://github.com/aws-samples/amazon-bedrock-samples/tree/main/agents-and-function-calling/bedrock-agents/use-case-examples

ユースケースごとのフォルダ配下にある`README.md`に、それぞれのアプリケーションの概要やアーキテクチャ図があるので、それを眺めるだけで勉強になると思います。
個人的に分かりやすいと思ったのは、
- [Customer relationship management](https://github.com/aws-samples/amazon-bedrock-samples/tree/main/agents-and-function-calling/bedrock-agents/use-case-examples/customer-relationship-management-agent#customer-relationship-management-crm-bedrock-agent)
- [Text to SQL Bedrock Agent](https://github.com/aws-samples/amazon-bedrock-samples/tree/main/agents-and-function-calling/bedrock-agents/use-case-examples/text-2-sql-agent#text-to-sql-bedrock-agent)

### エージェントの実装例
ユースケースに沿ったコードサンプルの他に、アーキテクチャに重点を置いたコードサンプルも公開しています。Amazon Bedrock Agentsの基本的な各機能がだいたい把握できます。

https://github.com/aws-samples/amazon-bedrock-samples/tree/main/agents-and-function-calling/bedrock-agents/features-examples#amazon-bedrock-agents---features-examples

*Demo 3*で作成するエージェントは、まさに[この例](https://github.com/aws-samples/amazon-bedrock-samples/tree/main/agents-and-function-calling/bedrock-agents/features-examples/05-create-agent-with-knowledge-base-and-action-group#agent-architecture)のような感じです。

## Amazon Bedrock Agents
AWSが提供する、エージェントアプリケーション開発のためのサービスが、**Amazon Bedrock Agents**です。Amazon Bedrock Agentsの機能についてそれぞれ簡単にまとめます。

### ① アクショングループ
アクショングループとは、エージェントが実行できるメソッドの集まりです。エージェントには、メソッドの**名前や引数が定義されたスキーマ**と、メソッドの**説明**が与えられます。ユーザーからのクエリを処理する中で、メソッドの実行が必要だと判断した際は、エージェントがそのメソッドを呼び出しレスポンスを受け取ります。

アクショングループのメソッドの**スキーマ（つまりインターフェース）の記述方法**と、その中身の**処理の実装方法**はそれぞれ2つずつ存在します。

#### スキーマの記述
1. **OpenAPI schema**
    **OpenAPI schema**は、JSONやYAML形式で記述できる、APIや関数の設計図です。
    [*Path Item Object*](https://swagger.io/specification/#path-item-object)ごとに1つのメソッドが定義され、エージェントがそのメソッドを認識します。
    <br>

1. **関数の詳細**
    <strong>関数の詳細 (function details)</strong>を自分で定義できます。マネジメントコンソール上でフォームを使って作成するか、JSONを記述して作成します。
    定義するのは、「名前」「説明（エージェントに渡される）」「パラメータ」「確認の有効化設定」です。
    ```javascript
    {
        "name": "string",
        "description": "string",
        "parameters": [
            {
                "name": "string",
                "description": "string",
                "required": "True" | "False",
                "type": "string" | "number" | "integer" | "boolean" | "array"
            }
        ]
    }
    ```
    ※*ドキュメントの[こちらのページ](https://docs.aws.amazon.com/bedrock/latest/userguide/agents-action-add.html)より引用*

#### 処理の実装
1. **AWS Lambda関数**
    上記で定義されたスキーマを満たす関数を、**AWS Lambda**で実装できます。
    実装したLambdaをアクショングループに紐づければ、エージェントが呼び出した際に自動でそのロジックが実行され、レスポンスがエージェントに返されます。
1. **リターンコントロール**
    リターンコントロールを設定すると、このメソッドが呼ばれたとき、エージェントは**コントロールをアプリケーションに返し**、レスポンスを待機する状態になります。

    そのリターンコントロールのリクエストを受けたアプリケーション側では、実装されているロジックを実行し、スキーマ通りにレスポンスをエージェントに返す必要があります。それを受け、エージェントは元のクエリ処理のセッションに復帰します。

### ② 他機能との統合
#### Knowledge Base
作成した**Knowledge BaseのRAGシステム**をエージェントに追加することができます。
Knowledge Baseの利用場面を適切にエージェントに伝えておけば、エージェントが処理の中で自動的にKnowledge Baseを呼び出し必要な情報を取得します。

#### Guardrails
**Guardrails**をエージェントに追加することもできます。
（[その2の記事](https://qiita.com/tripleO/items/4c342195027f9e37e1a0#3-guardrails)でも登場したので、長く書きませんが）LLMが生成するテキストにセーフガードとなるポリシーを策定し、アプリケーションの信頼性や安全性を守ります。

### ③ エイリアス
作成と設定が完了したエージェントをデプロイする際は、**エージェントエイリアス**を作成する必要があります。
エイリアスは、エージェントの**どれか1つのバージョン**に紐づけられ、エージェントの使用時には、指定されたエイリアスに紐づくバージョンのエージェントが使われます。

エージェントを更新すると作られる新しいバージョンを、既存のエイリアスか新しいエイリアスに紐づけることができます。

:::note
エージェントに紐づいたリソース（Lambda関数など）のリンクを変更した場合も、エージェントの**新しいバージョンになります**。
:::

エイリアスは以下のようなケースで利用できます。
- 環境（開発/テスト/リリース）ごとにバージョンを変える
- エージェントを使用するコード自体を変更することなく、使用するバージョンを切り替える

### ④ ***InvokeAgent***
エージェントにリクエストを送るには、`InvokeAgent`メソッドを使用します。
以下は、Pythonでのコード例です。
```python:example.py
import boto3

bedrock_agent_runtime_client = boto3.client('bedrock-agent-runtime')

...

response = bedrock_agent_runtime_client.invoke_agent(
    inputText = 'How many orders did AnySystem have yesterday?', # ユーザーからのクエリ
    agentId = agentId, # 作成時に生成されるID
    agentAliasId = 'TSTALIASID',
    sessionId = 'abc123'
)

event_stream = response['completion']
for event in event_stream:        
    if 'chunk' in event:
        data = event['chunk']['bytes']
        agent_answer = data.decode('utf8')
        print(agent_answer)
```
ドキュメント → [invoke_agent - Boto3 1.37.9 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/bedrock-agent-runtime/client/invoke_agent.html#invoke-agent)

### ⑤ セッションとメモリー
エージェントが複数回の呼び出しに渡る会話の内容を保持する機能に、**セッション**と**メモリー**があります。

#### セッション
同じセッション内では、エージェントは複数の呼び出しの内容を保持します。前回のリクエストで登場した情報を覚えているので、人間と対話しているときのように質問することができます。

APIでは、引数に`sessionId`を任意の文字列で指定します。（[前セクション](#-invokeagent)のコード例参照）
同一の`sessionId`を持つ呼び出しでは、同じセッションが使用されます。

:::note warn
しかし、セッションの期限が切れるとエージェントは会話記録を削除するので、**同じ文字列をIDに指定しても**以前の会話内容をエージェントは覚えていません。
:::

#### メモリー
メモリーは、複数のセッションにまたがって会話内容を保持させます。
メモリーの利用手順は、以下です。
1. エージェントの設定でメモリー機能を有効にする
1. APIの引数で、`memoryId`を任意の文字列で指定する
1. セッションが終了すると、そのセッションの要約情報がメモリーとして保存される
1. 次の新しいセッションが始まる際に、メモリーに保存された以前までのセッションの内容がエージェントに与えられる

`memoryId`に他の値を指定すると、別の新しいメモリーが作成されます。

また、`GetAgentMemory`メソッドを用いてメモリー内容の要約を取得することもできます。
```python:example.py
response = bedrock_agent_runtime_client.get_agent_memory(
    agentId = agent_id,
    agentAliasId = 'TSTALIASID',
    memoryId = memory_id,
    memoryType = 'SESSION_SUMMARY'
)

if 'memoryContents' in response and response['memoryContents']:
    session_summary = response['memoryContents'][0]['sessionSummary']
    print("Summary: " + session_summary['summaryText'])
```

:::note warn
*Amazon Titan Text G1 - Premier*をエージェントのLLMとして選択すると、メモリー設定は非対応のため有効にできません。
:::

### ⑥ トレース
エージェントの**トレース**とは、クエリを受け取ってからレスポンスを生成するまでの間にエージェントが行った**思考プロセスのログ**です。
トレースはJSON形式で、APIで`enableTrace = True`を指定するとレスポンスの中にトレースが含まれます。

:::note
コンソール上のチャットUIでも、返答ごとにトレースを表示できます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946195/7bfd07fc-56f1-4b6d-a1f4-4a1f210e0571.png" width="600">
※*Demo3のshopping assistantの一例*
:::

トレースは、エージェントが予期した動作をしているかの確認や、間違った生成の原因解明の際に役立ちます。

## Demo 3
### 構成
今回の*Demo 3*では、「shopping assistant」のエージェントを作成し、以下に関するコード例を実施します。

1. `InvokeAgent`
1. トレースの出力
1. Guardrails
1. メモリー

### 補足事項
- プロファイルとリソースのリージョンの一致
    [その2の記事](https://qiita.com/tripleO/items/4c342195027f9e37e1a0#%E8%A3%9C%E8%B6%B3%E4%BA%8B%E9%A0%85)で記載したので詳細は割愛します。
    <br>

- ログ記録の設定
    デモの中で、設定したS3のバケットに出力されたトレースのログをダウンロードする手順があります。しかし自分が実施してみたところ、S3にオブジェクトが何も生成されておらずエラーになりました。

    結局、Bedrockの「*モデル呼び出しのログ記録*」という設定を見つけ有効にしたところ、問題なくログが出力されるようになったので記載します。 
    - AWSマネジメントコンソールの「*Amazon Bedrock*」を開く
    - サイドメニューから「*Bedrock configurations* > *設定*」を選択
    - 「*モデル呼び出しのログ記録*」の設定を以下のように設定
        <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3946195/e053b0cf-f35c-4d50-8425-235a1404f232.png" width="600">
        - S3バケットはデモ内で作成した、`bedrock-logging-us-east-1-{your_account_id}`を選択
    - 「*設定を保存*」ボタンを押す
    
    設定を保存すると、上記のバケット内に`AWSLogs/`というプレフィックスで、オブジェクトが生成します。エージェントのログは、呼び出してから数分後に、そのフォルダ内に作成されます。

    :::note
    S3バケットのライフサイクル設定で、ログのオブジェクトに有効期限を設定すると良いと思います。
        :::
    <br>

- 必要なポリシー
    Bedrockサービスに対するポリシー以外で、デモの実行に必須のポリシーは以下の通りです。[^1]たくさんあるので漏れてたらすみません。
    - S3
        - `s3:ListBucket`
        - `s3:ListAllMyBuckets`
        - `s3:PutEncryptionConfiguration`
        - `s3:GetObject`
        - `s3:CreateBucket`
        - `s3:PutObject`
    - IAM
        - `iam:CreateRole`
        - `iam:CreatePolicy`
        - `iam:CreatePolicyVersion`
        - `iam:AttachRolePolicy`
        - `iam:PassRole`
    - Lambda
        - `lambda:ListFunctions`
        - `lambda:GetFunction`
        - `lambda:CreateFunction`
        - `lambda:UpdateFunctionCode`
    - Logs
        - `logs:DescribeDeliverySources`
        - `logs:DescribeDeliveries`
        - `logs:DescribeConfigurationTemplates`
        - `logs:PutDeliveryDestination`
        - `logs:PutDeliverySource`
        - `logs:CreateDelivery`
        - `logs:GetDeliveryDestination`
        参考：[Logs sent to Amazon S3 ドキュメント](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/AWS-logs-and-resource-policy.html#AWS-logs-infrastructure-V2-S3)
    - STS
        - `sts:GetCallerIdentity`
    <br>

- 自分が遭遇した問題
    - 金額に関するクエリの際に、`ThrottlingException`になる
        - 何度かリトライして解消される場合と永遠にできない場合がありました
    - ~~ログがS3に生成されない~~ 上記の方法で解決済み
        - ~~原因よく分かりませんでした~~

## おわり
長くなりましたが、ここまで3記事に分けて、[Building Generative AI Applications Using Amazon Bedrock](https://explore.skillbuilder.aws/learn/courses/17904/building-generative-ai-applications-using-amazon-bedrock/lessons)コースの紹介をしてきました。

記事に書いた通り、デモを一通りやってみるだけで非常に勉強になるのでオススメです。

[^1]: ローカル開発環境と前回の記事で紹介した*Pinecone*を使う場合