RareJob Tech Blog

レアジョブテクノロジーズのエンジニア・デザイナーによる技術ブログです

Amazon Bedrock Prototyping Campに参加してきました!(BedrockとKendraの簡易ハンズオン付き)

はじめに

SMART Method・PROGOS開発グループの奥山です。

京都に引っ越して3ヶ月経ったころ、友人が京都に来たと言うので坐禅に誘いました。

友人は「パートナーがこっちにできたからついでに会いに来た。パートナーと予定があるから坐禅は行かぬ。」と返事をくれました。

さて、私の心もこの頃の気温も急に冷え込んできましたが、生成系AI界隈は非常にアツいです。

そんな中、AWS Japan様から招待いただき、「Amazon Bedrock Prototyping Camp」に参加してきました!

以下、参加レポートとなります。

コンテンツ

  1. Amazon Bedrock Prototyping Campのコンテンツ概要
  2. Bedrock・Kendraのハンズオン付き説明
  3. 開発する際によかったリポジトリやドキュメント
  4. 感想

Amazon Bedrock Prototyping Campのコンテンツ概要

Amazon Bedrock Prototyping Campは

  1. 生成系AIをアプリケーションに組み込む際のビジネス的・エンジニア的な観点
  2. Bedrockの説明
  3. プロンプトエンジニアリングとRAGについて
  4. Bedrockハンズオン
  5. Kendraの説明
  6. Bedrock、Kendraを利用したプロトタイピング

を7時間でやるイベントでした。

Bedrock・Kendraのハンズオン付き説明

実際に受けた説明をもとに、BedrockやKendraの説明をしていきます。

Bedrock

特徴(+解釈)として

  • 主要な基盤モデルをサポート
  • 独自のデータで基盤モデルを非公開でカスタマイズできる
  • GDPRへの準拠やHIPAAの適格性などの標準への対応

があります。

SDKなどを通じてAPIコールすることで利用可能です。

Kendra

データ自体が構造化データ・非構造化データなのかを気にせず放り込めて、それを検索可能にするサービスです。

検索エンジンには機械学習を使っているとのことです。

コネクタが非常に強力で、GoogleDrive、Confluence、S3、RDSなどに対応しているだけでなく、手軽さもあります。こちらがデータ構造をやたら気にする必要なく様々なデータソースから取り入れられるというのが非常に魅力的です。

また、KendraはQueryAPIとRetrieveAPIがあります。

詳細な説明は公式ドキュメントに譲りますが、RetrieveAPIの方がより多く意味的に関連するドキュメントを取得してきます。QueryAPIもRetrieveAPIも意味的に関連するドキュメントを取得しますが、QueryAPIは1つのドキュメントから1つの抜粋なのに対して、RetrieveAPIは1つのドキュメントから関連性の高いものを複数抜粋できるところが特徴となります。

BedrockとKendraをどう組み合わせて使うか?

つまるところ、RAGが実装できます。

Prompt Engineering Guideから引用してみます。

RAG は入力を受け取り、ソース(例えばウィキペディア)が与えられた関連/証拠立てる文書の集合を検索します。文書は文脈として元の入力プロンプトと連結され、最終的な出力を生成するテキストジェネレータに供給されます。これにより RAG は、事実が時間とともに変化するような状況にも適応できます。LLM のパラメトリック知識は静的であるため、これは非常に有用です。RAG は言語モデルが再学習を回避することを可能にし、検索ベースの(文章)生成によって信頼性の高い出力を生成するための最新情報へのアクセスを可能にします。

質問などの入力を受けた際に、参照データを検索した結果を結合して、それをもとに回答を生成してもらう仕組み、という理解で行きます。

KendraにQueryやRetrieveのAPIを用いて取得したデータをBedrockに渡すプロンプトと合わせて質問することで、RAGを実現可能です。

弊社のブログだと、LangChain(OpenAPIのGPT3.5-turboモデル利用)とChromaDBでハンズオンの記事がそれっぽいことをやってたなと思います。

BedrockとKendraのハンズオン

東京リージョン(ap-northeast-1)にて、Bedrock・Kendra共に、AWSコンソールからセットアップし、実際に呼び出して動作するところまでを試してみます。

ぜひお手元で試してみてください!

Bedrockのセットアップ

AWSのBedrockコンソール「Model Access」をクリックします。

「Manage model access」をクリックし、モデルを利用できるようリクエストします。

これで完了です。

リージョンによって利用可能モデルが違うので、公式ドキュメントをご確認ください。

ハンズオン環境

イベントで利用したSageMakerのノートブックを紹介します。

つまるところ、「AWSのサービスを利用する際に利用する諸々の変数セットアップをすっ飛ばして利用可能なJupyterNotebook」です。

SageMakerのコンソールを開き、メニューバーから「ノートブックインスタンス」を選択します。

作成には少々時間がかかります。

作成されたら、メニューから作成したノートブックを選択して「アクセス許可と暗号化」を探します。

IAMロールでの設定を通じて、ノートブックにBedrockとKendraのロールを全許可で追加します。

イベント会場では、「インラインポリシーを作成」から実行しました。

JSON形式だと下記のようになります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Statement1",
            "Effect": "Allow",
            "Action": [
                "bedrock:*",
                "kendra:*"
            ],
            "Resource": []
        }
    ]
}

検証ではこちらを貼り付けてご利用いただければと思います。

ただし、本番ではちゃんと制限をかけて運用する必要があるので、適宜ご変更ください。

Bedrockのコード

import boto3
import json

# bedrockクライアント初期化
# bedrock_client = boto3.client(service_name='bedrock')
bedrock_runtime_client = boto3.client(service_name='bedrock-runtime')

prompt = "日本で一番高い山はなんですか?"

body = json.dumps({
    "prompt": prompt,
    "max_tokens_to_sample": 300,
    "temperature": 0.1,
    "top_k": 250,
    "top_p": 1,
    "stop_sequences": ["\n\nHuman:"]
})
modelId = "anthropic.claude-instant-v1"  # change this to use a different version from the model provider
accept = "application/json"
contentType = "application/json"

# モデル呼び出し
response = bedrock_runtime_client.invoke_model(
    body=body, 
    modelId=modelId,
    accept=accept,
    contentType=contentType
)

response_body = json.loads(response.get('body').read())
print(response_body.get('completion'))


# LangChainパターン
from langchain.chat_models import BedrockChat
from langchain.schema import HumanMessage

llm = BedrockChat(
    client=bedrock_runtime_client,
    model_id="anthropic.claude-instant-v1",
    model_kwargs={
        "temperature":0.1,
        "max_tokens_to_sample": 1000},
)

result = llm([
    HumanMessage(content=prompt)
])

print(result.content)

Bedrock利用する際に重要なこと

端的に言うと、「プロンプトをしっかり検証する。そのためにプロンプトエンジニアリングを把握する。」です。(とはいえ、これはLLM全般を利用する際に言えることだと思います。)

当日配布されたコードをそのまま貼り付けておくので、是非お手元で改善されていく様を観察してみてください。

比較が楽になるよう、メソッドを定義します。

def invoke_claude(text, max_tokens_to_sample=1000): 

    body = json.dumps({
        "prompt": f"\n\nHuman:{text}\n\nAssistant: ",
        "max_tokens_to_sample": max_tokens_to_sample,
        "temperature": 0.1,
        "top_p": 0.9,
    })

    modelId = 'anthropic.claude-instant-v1'
    accept = 'application/json'
    contentType = 'application/json'

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

    response_body = json.loads(response.get('body').read())

    return response_body.get('completion')[1:]

初期プロンプト

prompt_template = """
以下のようなシステムのアーキテクチャを考えてください。  
システム概要: {about}
システム規模: {scale}  
機能: {features}
"""

prompt = prompt_template.format(
    about="衣料品を販売するECサイト", 
    scale="ピーク時には毎分10000リクエストに対応できる必要があります。また、グローバルに利用可能である必要があります。また、応答性が高く高速である必要があります。", 
    features="""次の3つのページを含んでいる必要があります。
    1.製品について解説したランディングページ。 2. 会社概要を説明するページ 3. 採用情報ページ
    """
)

print('Prompt:', prompt)
print('Output:', invoke_claude(prompt))

改善1回目のプロンプト:ロールを与えている

prompt_template = """
あなたは熟達したシステムアーキテクトです。初心者にとっても専門用語を噛み砕いて分かりやすい説明をすることで有名です。
以下のようなシステムのアーキテクチャを考えてください。 
システム概要: {about}
システム規模: {scale}  
機能: {features}
"""

prompt = prompt_template.format(
    about="衣料品を販売するECサイト", 
    scale="ピーク時には毎分10000リクエストに対応できる必要があります。また、グローバルに利用可能である必要があります。また、応答性が高く高速である必要があります。", 
    features="""次の3つのページを含んでいる必要があります。
    1.製品について解説したランディングページ。 2. 会社概要を説明するページ 3. 採用情報ページ
    """
)

print('Prompt:', prompt)
print('Output:', invoke_claude(prompt))

改善2回目のプロンプト:タグを利用し、プロンプトを構造化する

prompt_template = """
<Instruction>
あなたは熟達したシステムアーキテクトです。初心者にとっても専門用語を噛み砕いて分かりやすい説明をすることで有名です。
システムの概要、規模、機能の情報をもとにお客様の要求を分析し、セキュリティ・パフォーマンス・運用性・信用性・コスト最適といった観点でアーキテクチャを考案してください。
また、高校生にもわかるように、専門用語を噛み砕いて説明してください。
</Instruction> 
<Requirements>
    <About>
        {about}
    </About>
    <Scale>
        {scale}
    </Scale>
    <Feature>
        {features}
    </Feature>
</Requirements>
"""

prompt = prompt_template.format(
    about="ECサイトのためのウェブサイト", 
    scale="ピーク時には毎秒10000リクエストに対応できる必要があります。また、グローバルに利用可能である必要があります。また、応答性が高く高速である必要があります。", 
    features="""次の3つのページを含んでいる必要があります。
    1.製品について解説したランディングページ。 2. 会社概要を説明するページ 3. 採用情報ページ
    """
)

print('Prompt:', prompt)
print('Output:', invoke_claude(prompt))

Kendraのハンズオン

Kendra自体のセットアップやコネクター自体は、コンソールで直感的に操作できるので、注意点だけにとどめます。

Kendraのセットアップ手順はざっくりと

  1. Indexの作成
  2. Indexにコネクターを通じてデータを投入する

です。

QueryやRetrieveのAPIを利用する際、利用する言語をデータ投入時の言語に合わせないと取得が0件になりました。

弊社はデータをjaで入れましたが、QueryAPIは言語指定しない場合、enで取得する仕様です。

そのため、当初SDK経由で利用していたとき、言語設定をjaにしていないことが原因で、取得が0件のケースに遭遇しました。

データ投入の際の言語設定は、下記写真の画面からできます。ご注意ください。

下記は、イベント当日、弊社がプロトタイピングする際に利用したコードになります。

弊社は当日、自社のConfluenceのデータを用いるため、jaでデータを入れました。

KendraのIndexIDが必要なので、対象のIndexを選択して、Index Settingsの項目から探してください。

import os
from typing import Dict, Literal

import boto3

kendra_client = boto3.client("kendra", region_name='ap-northeast-1')

indexId = 'KendraのIndexIDを入れてください'

kwargs = f"""{question}"""

response = kendra_client.query(
    IndexId=indexId,
    QueryText=kwargs,
    AttributeFilter={
        'AndAllFilters': [
            {"EqualsTo":{"Key":"_language_code","Value":{"StringValue":"ja"}}}
        ]
    },
    PageNumber=1,
    PageSize=20,
    DocumentRelevanceOverrideConfigurations=[
        {
            'Name': 'string',
            'Relevance': {
                'RankOrder': 'DESCENDING'
            }
        },
    ],
)

# print(response)
# print(response['ResultItems'])

for result in response['ResultItems']:
    print(result['DocumentId'])
    print(result['DocumentTitle'])
    print(result['DocumentExcerpt']['Text'])
    print(result['DocumentURI'])
    print(result['DocumentAttributes'])
    print(result['ScoreAttributes'])
    print(result['DocumentTitle'])

Kendraにリクエストする箇所のコードに注目します。

response = kendra_client.query(
    IndexId=indexId,
    QueryText=kwargs,
    AttributeFilter={
        'AndAllFilters': [
            {"EqualsTo":{"Key":"_language_code","Value":{"StringValue":"ja"}}}
        ]
    },
    PageNumber=1,
    PageSize=20,
    DocumentRelevanceOverrideConfigurations=[
        {
            'Name': 'string',
            'Relevance': {
                'RankOrder': 'DESCENDING'
            }
        },
    ],
)

特に

AttributeFilter={
        'AndAllFilters': [
            {"EqualsTo":{"Key":"_language_code","Value":{"StringValue":"ja"}}}
        ]
    },

の部分は、「Kendraのデータを日本語で検索する」場合必要となります。

以上、ハンズオンでした。

開発する際によかったリポジトリやドキュメント

Bedrock

正直なところ、Bedrockについてはプロトタイピングする際に困ったことが特にありませんでした。(と言うのも渡されたサンプルが充実してたので・・・)

公式のGithubリポジトリでよくまとまっているのはこちらかと思います。

github.com

Kendra

2つの公式ドキュメントが役立ちます。

1つ目はKendraの該当するAPIのドキュメントです。

2つ目はSDK for Python自体のKendra部分のドキュメントです。

1つ目でKendraのQueryAPIのリクエスト構造を把握した上で、2つ目でどうSDKに落とし込むかを把握できます。

そのほか有用なGithubリポジトリ

AWSは公式のGithubリポジトリを有してます。

実際に紹介されたものをいくつか載せます。

https://github.com/aws-samples/generative-ai-use-cases-jp

https://github.com/aws-samples/bedrock-claude-chat

https://github.com/aws-samples/simple-lex-kendra-jp

イベント終了後に読んでから気付いたのですが、スライドの一番後ろにあったこちらのWorkshopも非常に良いです。(KendraをSDK for Pythonで利用する際のサンプルコードも乗ってます。)

catalog.us-east-1.prod.workshops.aws

感想

イベントに参加したことで、細かいテクニックから自社サービスの見方、開発する際のマインドセットに至るまで、さまざまな気づきを得られました。

個人的に、LLM導入の肝はプロンプトエンジニアリングである、と気づけたのは非常に大きかったです。(今後のLLMの発展でこの見解が覆ることもあるかもしれませんが。)

同時に、プロダクト価値の向上に資する開発とは何か?に対する手の動かし方の見識を広める入り口に立てた気がして、気分が非常に高揚しました。

手を動かして必死に導入しようと考える作業をもっとやりたいなあと思いました。

最後に

We're hiring!
弊社では、一緒に働いてくださるエンジニアを募集しています。
rarejob-tech.co.jp