13
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SlackとZapierとKnowledge Bases for Amazon Bedrockで社内Q&Aシステム構築完全手順 - 2024年8月版

Last updated at Posted at 2024-08-16

【注】ステップバイステップでこれでもかこれでもかと完全な手順を記載しましたので、分量多めです。右側の目次を使いながらご覧ください。2時間あれば構築できます。

社内生成AIツール使ってくれない問題

「社内で使える生成AIツールを展開したのに、全然ユーザーが使ってくれない...」
誰も見てないダッシュボード同様、DX推進あるあるではないでしょうか?

このようなツールはリリースして満足しがちですが、最も重要かつ難しいのは、日々の業務に使ってもらうことです。以下のようなアプローチが考えられます。

  1. 業務プロセスに組み込んで、強制的に使われるようにする
  2. ユーザーが日々使うツールから利用できるようにし、ツールのアクセスコスト、学習コストを(ほぼ)ゼロにする

今回は日頃の業務コミュニケーションで活用しているツールSlackを窓口としたQ&A生成AIチャットボットを構築するという2.のアプローチをとり、「ユーザーに使われる生成AIツール」を構築します。Slackを窓口とすることで、同僚に相談する感覚で手軽に質問可能です。

今回構築するSlackを窓口にしたQ&Aシステム

image.png

<ツールの特徴>

  • Slackを窓口とすることでユーザーはアプリケーションへのアクセスコストなし
  • Slackのスレッドに回答するので、トピックがまとまり後で確認しやすい
  • スレッド内の投稿履歴が利用されるので、チャットボットとインタラクティブに深掘りができる
  • 格納ドキュメントは英語でも、日本語で回答してくれる
  • コストは月数万円レベル(ほぼOpenSearch Serverless代)

アーキテクチャ

image.png

<事前準備>

  1. PDFファイルを Amazon Simple Storage Service(Amazon S3) に格納します。
  2. Amazon Bedrock Knowledge base の機能にて、PDFのベクトル化を行います。
  3. PDFベクトルは Amazon OpenSearch Serverless のVector Storeに格納します。

<処理の流れ>

  1. ユーザーはSlackに質問を投稿します。
  2. Slack投稿をトリガーにzapierが発火し、zapierはAWS Lambdaを起動します。
  3. Amazon Bedrock Knowledge baseの機能にて、質問文をベクトル化、Amazon OpenSearch Serverlessによるベクトル検索を実施し、検索結果から回答を生成します。
  4. AWS LambdaはAWS Secrets ManagerからSlack API用のシークレット情報を取得し、Slack APIを利用して生成された回答をSlackのスレッドに投稿します。

社内Q&Aシステム構築手順

このシステムを構築する手順を、(これでもかと)詳細に記載します。

1. RAGを構築する

まずは検索結果から回答を生成するRAGの仕組みを構築します。AWSの Knowledge Bases for Amazon Bedrock を利用します。作業対象は以下の赤枠です。

image.png

Knowledge base に格納するファイルは、ServiceNow Xanadu(ザナドゥ)バージョンのAI機能に関するプロダクトドキュメントです。

1-1. ファイルダウンロード

ドキュメントをPDF形式でダウンロードしていきます。

image.png

2024/8/11時点では日本語ドキュメントはありませんので、英語ドキュメントをダウンロードします。赤枠の Save entire publicationを押すと、セクション全体のPDFを取得できます。

image.png

548ページあります。かなりのボリュームですね。ServiceNowプロダクト全体のdocsは、数万ページにもなります。これはクラウドサービスの開発者ドキュメントと同様ですね。

image.png

1-2. ファイル容量削減

PDFファイルをダウンロードして、容量を確認します。

image.png

ファイルサイズが50MBを超えてしまいました。50MBを超えるとAmazon Bedrockで扱うことができないので、サイズを削減します。

今回は、MacBook Pro のPreviewアプリのサイズ削減機能を使います。
File > Export から、Quartz Filterで Reduce File Sizeを選択します。

image.png

46.9MBになり、50MB以下に抑えることができました。50MBに収まらない場合は、ファイルの分割などを検討します。

image.png

1-3. Amazon S3 にPDFファイルを格納

サイズを小さくしたPDFファイルを、Amazon S3に格納します。

image.png

1-4. Amazon Bedrock Knowledge bases の構築

Amazon BedrockのKnowledge basesサービス画面に移動し、Create knowledge baseを選択します。

image.png

<設定>
Step1は、デフォルトのままNextをクリックします。

image.png

Step2は、S3 URIを入力します。

image.png

赤枠のように入力しました。

image.png

Step3は、Embeddings modelとして Titan Embeddings G1 - Text v1.2 を選択します。

Vector databaseはデフォルトのQuick create a new vector storeを選択します。Amazon OpenSearch Serverless vector storeが構築されます。

image.png

Step4は、設定した項目を確認し、Create knowledge baseをクリックします。

image.png

作成中になり、右下がくるくる回っています。しばし待ちましょう......

image.png

待つこと5分ほど、Knowledge baseが構築されました。

Knowledge base ID

は、後ほどLambda関数内で利用しますので、メモしておきましょう。

image.png

データソースを選択し、Syncを押します。Amazon S3のPDFが Titan Embeddings G1 - Text v1.2 によってベクトル化され、Amazon OpenSearch Serverless の vector storeに格納されます。

image.png

Syncが始まりました。

image.png

今回は1ファイルなので、数分で完了しました。
データソースをクリックして詳細画面で、Sync historyが確認できます。

image.png

右側の Test Knowledge baseで動作確認してみましょう。

image.png

モデルは、AnthropicのClaude 3 Sonnetを選択します。

image.png

<質問>
ServiceNowのNLU modelを削除するために必要なロールは何ですか?またその手順は?

image.png

プロダクトドキュメントは英語なのですが、日本語で回答生成してくれました!!内容も正しいです。
次に、Knowledge baseを利用しない場合の回答も確認してみます。

image.png

nlu_adminロールが正解なので、この回答は誤りです。

手順も遠くから薄めでぼんやり見ると合っている風!ですが、近くで冷静にみたら間違っておりました。
他のLLMでも試しましたが、Knowledge baseのような具体的な回答はできませんでした。
ドキュメントを基にした具体的な回答ができるのは、RAGの価値ですね。

2. Slackを窓口にしたシステムを作る

では、Amazon Bedrockで作成したRAGを、Slackから利用するシステムを作っていきます。
アーキテクチャの赤枠が対象になります。

image.png

2-1. Slackの設定

まずは、RAGチャットボットが応答するチャネルを作成します。

image.png

デフォルトのままPublicとします。
image.png

Channel IDをメモしておきます。後でAWS Lambdaのコード内で利用します。

image.png

Slackにアプリケーションを追加する

Slack APIから、Slackアプリの新規作成ダイアログを表示します。
Create New Appを押して、新しいアプリを作成します。
From scratchを選択します。

image.png

App Nameを入力し、workspaceを選択します。

image.png

Basic Informationの、Permissionsをクリックします。

image.png

Bot Token Scopesと、User Token Scopesを設定します。

image.png

Bot Token Scopesに chat:write、
User Token Scopesに channels:historyを設定します。

image.png

ワークスペースにインストールします。

image.png

Allowを選択します。

image.png

User OAuth Token
Bot User OAuth Token
が発行されました。

image.png

このコードを、後ほど AWS Secrets Manager に登録します。

Slackチャネルにアプリケーションを統合

次に、作成したアプリケーションをSlackチャネルに登録します。
Slackチャネル詳細のIntegrationsから Add an App をクリックします。

image.png

作成したアプリケーションのAddをクリックします。

image.png

追加されました。

image.png

2-2. AWS Secrets Manager の設定

AWS LambdaでSlack APIのシークレットキーを使います。Slackのシークレットキーを安全に利用するために、AWS Secrets Manager を利用します。
Store a new secretをクリックします。

image.png

Secret typeでOther type of secretを選択し、
Slackの2つのキーを設定します。その後、Nextをクリックします。

image.png

Secret nameのみ入力してNextをクリックします。

image.png

そのままNextをクリックします。

image.png

review画面に遷移します。Python3のコードは後のLambdaで利用します。
Storeをクリックします。

image.png

表示されたPython3コードは以下です。(参考)

# Use this code snippet in your app.
# If you need more information about configurations
# or implementing the sample code, visit the AWS docs:
# https://aws.amazon.com/developer/language/python/

import boto3
from botocore.exceptions import ClientError

def get_secret():

    secret_name = "slack_app_servicenow_expert"
    region_name = "us-east-1"

    # Create a Secrets Manager client
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager',
        region_name=region_name
    )

    try:
        get_secret_value_response = client.get_secret_value(
            SecretId=secret_name
        )
    except ClientError as e:
        # For a list of exceptions thrown, see
        # https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html
        raise e

    secret = get_secret_value_response['SecretString']

    # Your code goes here.

登録されました。

image.png

2-3. AWS Lambdaの設定

処理の中核である、AWS Lambda を構築していきます。Create a functionをクリックします。

image.png

Function nameと、Runtimeを入力します。

image.png

以下のソースコードをlambda_function.pyにコピペします。

<以下は環境に合わせて修正してください>

  • cid = 'C07H38BSFRN' # 要修正1:SlackチャンネルID
  • region_name = "us-east-1" # 要修正2:リージョン設定
  • region = "us-west-2" # 要修正3:リージョン設定
  • kb_id = 'XUP335MAD3' # 要修正4:Knowledge base ID
  • region = "us-east-1" # 要修正5:リージョン設定

AWS Lambdaのコード: lambda_function.py

import json
import os
import re
import urllib.request
from datetime import datetime
from urllib.parse import urlparse

import boto3
from botocore.exceptions import ClientError

# Lambda関数のメインハンドラー
def lambda_handler(event, context):
    cid = 'C07H38BSFRN'  # 要修正1:SlackチャンネルID
    key_to_retrieve = 'thread_ts'
    
    # イベントにthread_tsが含まれているかチェック
    if key_to_retrieve in event:
        thread_ts = event[key_to_retrieve]

        # Slackユーザートークンを取得
        slack_user_token = get_secret(
            "slack_app_servicenow_expert", 
            "User_OAuth_Token"
        )
        # Slackスレッドの履歴を取得
        response = get_slack_thread_history(cid, thread_ts, slack_user_token)
        # 会話を抽出
        conversation = extract_conversation(response)
        # 会話をフォーマット
        formatted_conversation = format_conversation(conversation)
        # タグ付きセクションを削除
        formatted_conversation_wo_ref = remove_tagged_sections(
            formatted_conversation
        )

        # プロンプトを作成
        prompt_pre = (
            f"あなたはBotです。Userから今回入力された質問を補正してください。"
            f"補正した質問は、生成AIの入力に利用されます。補正する際、"
            f"これまでの会話履歴を考慮して、Userの質問意図を十分に理解して補正してください。"
            f"時間をかけてじっくり考えてください。"
            f"<今回入力された質問>"
            f"{event['raw_text']}"
            f"</今回入力された質問>"
            f"<これまでの会話履歴>"
            f"{formatted_conversation_wo_ref}"
            f"</これまでの会話履歴>"
            f"解説は不要です。補正した質問のみを出力すること!"
        )
        
        # Bedrock Sonnetを使用してプロンプトを処理
        prompt = use_bedrock_sonnet(prompt_pre)
        
    else:
        # thread_tsがない場合は、raw_textをそのまま使用
        prompt = event['raw_text']
    
    # Bedrock RAG Sonnetを使用してメッセージを生成
    message = use_bedrock_rag_sonnet(prompt)
    
    # Slackにメッセージを投稿
    ts = event['ts']
    slack_access_token = get_secret(
        "slack_app_servicenow_expert", 
        "Bot_User_OAuth_Token"
    )
    post_message_to_slack(cid, message, slack_access_token, ts)

    # 正常終了を示すレスポンスを返す
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

# AWS Secrets Managerからシークレットを取得する関数
def get_secret(secret_name, secret_key):
    region_name = "us-east-1" # 要修正2:リージョン設定
    session = boto3.session.Session()
    client = session.client(
        service_name='secretsmanager', 
        region_name=region_name
    )

    try:
        get_secret_value_response = client.get_secret_value(SecretId=secret_name)
    except ClientError as e:
        raise e

    return json.loads(get_secret_value_response['SecretString'])[secret_key]

# Bedrock RAG Sonnetを使用して回答を生成する関数
def use_bedrock_rag_sonnet(prompt):
    region = "us-west-2" # 要修正3:リージョン設定
    bedrock_client = boto3.client("bedrock-agent-runtime", region_name=region)
    model_id = "anthropic.claude-3-sonnet-20240229-v1:0"
    model_arn = f'arn:aws:bedrock:{region}::foundation-model/{model_id}'
    kb_id = 'XUP335MAD3' # 要修正4:Knowledge base ID
    
    # Bedrock RAG Sonnetを呼び出し
    response = bedrock_client.retrieve_and_generate(
        input={'text': prompt},
        retrieveAndGenerateConfiguration={
            'type': 'KNOWLEDGE_BASE',
            'knowledgeBaseConfiguration': {
                'knowledgeBaseId': kb_id,
                'modelArn': model_arn, 
            }
        }
    )
    
    # 応答を整形
    answer = response['output']['text']
    answer += "\n\n<回答根拠> ==================\n"
    
    for citation in response["citations"]:
        for reference in citation["retrievedReferences"]:
            answer += "<ファイルURI>\n" + reference["location"]["s3Location"]["uri"] + "\n"
            answer += "<該当箇所>\n" + reference["content"]["text"] + "\n"
            answer += "---------------\n"
    
    answer += "</回答根拠>\n"
    
    print(answer)
    return answer

# Bedrock Sonnetを使用してスレッド履歴からユーザーの意図する質問文を生成する関数
def use_bedrock_sonnet(message):
    region = "us-east-1" # 要修正5:リージョン設定
    bedrock = boto3.client('bedrock-runtime', region_name=region)
    body = json.dumps({
        "anthropic_version": "bedrock-2023-05-31",
        "max_tokens": 1000,
        "messages": [{"role": "user", "content": message}]
    })
    model_id = 'anthropic.claude-3-5-sonnet-20240620-v1:0'
    response = bedrock.invoke_model(
        body=body, 
        modelId=model_id, 
        accept='application/json', 
        contentType='application/json'
    )
    response_body = json.loads(response.get('body').read())
    
    answer = response_body["content"][0]["text"]
    print(answer)
    return answer

# Slackにメッセージを投稿する関数
def post_message_to_slack(cid, message, slack_access_token, ts):
    url = "https://slack.com/api/chat.postMessage"
    data = {
        'channel': cid,
        'text': message,
        'thread_ts': ts
    }
    
    post_data = urllib.parse.urlencode(data)
    req = urllib.request.Request(url, data=post_data.encode())
    req.add_header('Authorization', 'Bearer ' + slack_access_token)
    
    with urllib.request.urlopen(req, timeout=1) as response:
        json.loads(response.read())

# Slackスレッドの履歴を取得する関数
def get_slack_thread_history(cid, thread_ts, slack_user_token):
    url = "https://slack.com/api/conversations.replies"
    data = {'channel': cid, 'ts': thread_ts}
    post_data = urllib.parse.urlencode(data).encode()

    req = urllib.request.Request(url, data=post_data)
    req.add_header('Authorization', 'Bearer ' + slack_user_token)

    with urllib.request.urlopen(req, timeout=5) as response:
        response_data = json.loads(response.read().decode('utf-8'))

    return response_data

# Slack応答から会話を抽出する関数
def extract_conversation(response):
    conversation = []

    for message in response['messages']:
        timestamp = datetime.fromtimestamp(float(message['ts']))
        user = 'Bot' if 'bot_id' in message else f"User ({message['user']})"
        text = message['text']
        
        conversation.append({
            'timestamp': timestamp,
            'user': user,
            'text': text
        })

    return conversation

# 抽出した会話をフォーマットする関数
def format_conversation(conversation):
    formatted = "# Extracted Slack Conversation\n\n"
    for i, message in enumerate(conversation, 1):
        formatted += f"{i}. **[{message['timestamp']}] {message['user']}:** {message['text']}\n\n"
    return formatted

# 指定されたタグで囲まれたセクションを削除する関数
def remove_tagged_sections(text, start_tag='&lt;回答根拠&gt;', end_tag='&lt;/回答根拠&gt;'):
    pattern = re.escape(start_tag) + '.*?' + re.escape(end_tag)
    return re.sub(pattern, '', text, flags=re.DOTALL)

Lambdaの環境設定をします。まずはタイムアウトが3秒と短すぎるので、伸ばします。
Configurationから、General configurationのEditをクリックします。

image.png

今回は30秒にしておきます。

image.png

必要なロールを追加します。Configuration -> Permissions から、下の赤枠をクリックします。

image.png

IAMの画面が開きます。

image.png

選択して、Attach policiesを選択します。

image.png

SecretsManagerReadWriteを付与します。

image.png

AmazonBedrockFullAccessを同様に付与します。

image.png

これでAWS Lambdaの設定は完了です。

2-4. Zapierの設定

Zapierの設定をしていきます。

Zaps画面(左のイナズママーク)で、Createを押し、New Zapを選択します。

image.png

トリガーの設定

Triggerをクリックし、設定していきます。

image.png

Slackを選択します。

image.png

New Message Posted to Channelを選択します。

image.png

Continueをクリックして次へ。
アカウント選択し、Continueをクリックします。

image.png

Channelでは、作成したSlackチャネルを選択します。
Trigger for Bot Messages?は、デフォルトのNoのままにします。

image.png

Test Triggerをクリックします。

image.png

Cotinue with selected recordをクリックします。

image.png

次に、アクションを設定していきます。

アクションの設定

AWS Lambdaを選択します。

image.png

Invoke Function(Async)を選択します。

image.png

AWSアカウントを選択します。

image.png

Continueをクリックします。

image.png

補足: AWSアカウントの登録

ZapierにAWSアカウントを未登録の場合は、ユーザー選択ウィンドウにて、Connect a new accountを選択し、AWSアカウントの登録を行います。

IAM consoleから、 AWS Access Key IDと、AWS Secret Access Keyを発行し、登録してください。

image.png

補足終わります。


アクションの設定(続き)

Actionの設定をします。
Argumentsを3つ設定するのがポイントです。Lambda関数内で利用します。

image.png

テストを実行します。
image.png

Publishをクリックします。

image.png

作成したZapが発行されました。

image.png

本記事では設定しませんが、Zapには名前をつけておきましょう。

image.png

以上でシステム構築完了です。実際に試してみましょう!

動作確認: Slackに投稿!

構築したRAGシステムを試してみましょう。

<質問>
ServiceNowのNLU modelを削除するために必要なロールは何ですか?またその手順は?

無事応答が返ってきました!!

image.png

さらに、履歴を元に追加質問もできます。NLU Workbenchについて、そっけなく聞いてみましょう。

image.png

NLU Workbenchのことを聞いていると読み取ってくれました!
以上でRAGシステム構築は完了です。おつかれさまでした!

まとめ

日常業務に使っているSlackに、社内ドキュメントを根拠とした具体的な回答を生成してくれるQ&Aボットを構築する手順を紹介しました。実際に弊社では様々な生成AIアプリケーションを業務に活用していますが、最も利用率が高いのが今回のプロダクトです。いかにアクセスコストと学習コストを下げることが重要か実感しました。
「せっかくRAGアプリケーション作ったのに使われない!」とお悩みの方の参考になれば幸いです。

宣伝:アオラナウ株式会社/人材募集!!

アオラナウ株式会社は、ServiceNow&生成AIでお客様のビジネス課題解決に興味のある方を募集しております。実ビジネスに活用できる生成AIアプリケーション開発に興味ある方もお待ちしています。(応募時にご相談下さい)

13
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?