Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

ChatGPTでPythonのdiagramsを使ってAWSのサービス構成図生成コードを作ってもらう

夏の暑さもだいぶ落ち着いてきていよいよ秋めいてきました、そろそろサンマがおいしい季節ですね、菅野です。

AWSを用いて様々なアーキテクチャを作成することが可能ですが、どういった構成になっているのかを一目で表すには図が効果的です。
手動でPowerPointや、draw.io等の作図ツールを用いて作成することも多いのではないか、と思いますが、ChatGPTで簡単に出力できたら便利ですよね。

Advanced Data Analysysの動作環境ではDiagramsライブラリがインストールされていないため、Pythonコードを直接ChatGPTで動かして構成図を出力してもらうことはできませんでした。
なので、今回はChatGPTにPythonのライブラリDiagramsを用いてクラウドの構成図を作成するPythonコードを作成してもらいましょう。

今回の検証ではGPT-4モデルを利用します。

Diagrams

Pythonのライブラリで、AWS、Azure、k8sといった様々なクラウド、サービスの構成図を作成するためのライブラリです。 diagrams.mingrammer.com

Diagramsを用いる準備

Diagramsを用いて構成図を作成するため、ローカル環境に以下の準備を実施します。

diagramsのインストール

以下のコマンドを実行してdiagramsをインストールします。

pip install diagrams

graphvisのインストール

diagramsでは、graphvisを用いて図の作成をします。
以下のサイトからgraphvisをインストールしましょう。

graphviz.gitlab.io

Windowsでのインストール実行時にはPathに追加するオプションがデフォルトオフになっていますが、追加するようにするとスムーズに利用できます。
入れ忘れてしまった場合には手動で実行ディレクトリをPathに追加しましょう。

クラウドアーキテクチャ図を作成してもらう

下準備がととのったので、さっそくChatGPTでクラウドの構成図を作成してもらいましょう。

Webアプリケーションの例

WebアプリケーションをAWSで作成した場合の構成図を作成してもらいましょう。

(中略)

出力されたコードを実行したところエラーが発生したため、何度か修正を実施してもらいました。
(中略)
ただしこの出力でもVPCGatewayEndpointのimportエラーが発生しました。このあと何パターンか試したのですが、ChatGPTの出力ですと、importするモジュールのパッケージを間違えるケースが散発しました。このあともう1回やり取りして、最終的に修正されたものが次のコードです 。

from diagrams import Cluster, Diagram
from diagrams.aws.network import Route53, ELB, VPC, Endpoint  # <-- 修正箇所
from diagrams.aws.compute import EC2
from diagrams.aws.storage import S3
from diagrams.aws.database import RDS

with Diagram("Web Application Architecture", show=False):
    dns = Route53("dns")
    
    with Cluster("VPC"):
        with Cluster("ELB"):
            load_balancer = ELB("ELB")
            dns >> load_balancer
            
            with Cluster("EC2 Cluster"):
                ec2_1 = EC2("EC2 AZ1")
                ec2_2 = EC2("EC2 AZ2")
                load_balancer >> ec2_1
                load_balancer >> ec2_2

        db = RDS("RDS")
        ec2_1 >> db
        ec2_2 >> db
        
    s3_bucket = S3("S3 Bucket")
    vpc_endpoint = Endpoint("VPC Endpoint")  # <-- 修正箇所
    
    ec2_1 >> vpc_endpoint >> s3_bucket
    ec2_2 >> vpc_endpoint >> s3_bucket

実行した結果以下のファイルが出力されました。

矢印が変なところもありますが、おおむね期待した形に出力できていますね。

機械学習を行う構成

AWSでは機械学習もサポートしています。
そちらを用いた構成図を作成してもらいましょう。

ただしこの出力でもまたimportエラーが発生しました。SageMakerのところです
ChatGPTにSageMakerのImport元を修正する案を出してもらいましたが、それでもエラーは解消しませんでした。

SageMakerモジュールは現在のドキュメントを見ると、diagrams.aws.ml には存在しているみたいですがSageMakerではなくSagemakerとなっている模様です。
DiagramsのGithubからSageMaker部分のコードを確認しても、SageMakerのモジュール名は2020年時点ですでにSagemakerであったのでChatGPTのモデルの学習データにないから出力できないというわけではなさそうです。
考えうる可能性としては一般的に利用されているSageMakerという語に引っ張られてしまっている可能性がありますね。

手動で修正した結果は以下です。

from diagrams import Diagram
from diagrams.aws.compute import Lambda
from diagrams.aws.network import APIGateway
from diagrams.aws.ml import Sagemaker
from diagrams.aws.storage import S3

with Diagram("Machine Learning App on AWS", show=False):
    api_gateway = APIGateway("API Gateway")
    lambda_function = Lambda("Lambda")
    sagemaker = Sagemaker("SageMaker")
    s3 = S3("Model in S3")

    api_gateway >> lambda_function >> sagemaker >> s3

実行して出力した結果が以下です。

APIGatewayを介して学習を実行、実行に用いたデータ、出力結果等を保存するアーキテクチャ図が作成されました。

Fargate(ECS)を用いた構成図

Fargate(ECS)を用いたAWSの構成も作ってもらいましょう。

追加でChatGPTに以下の修正をしてもらいました

  • コンテナのアイコンにFargateのアイコンを使っていたのでコンテナのアイコンを使うようにした。
  • Fargateのクラスター内にFargateのアイコンを置いてもらった。
  • ECSのクラスター内にECSのアイコンを置いてもらった。

また、DynamoDBのモジュール名(Dynamodb)がサービス名のDynamoDBに引っ張られて間違っているため手動で直しました。
最終的な出力はこちら。

from diagrams import Cluster, Diagram
from diagrams.aws.compute import ECS, Fargate, ElasticContainerServiceContainer
from diagrams.aws.network import CloudFront, ELB
from diagrams.aws.database import Dynamodb

with Diagram("Web Application Architecture with AWS Fargate and ECS", show=False):
    
    # CloudFront
    cloudfront = CloudFront("CloudFront")
    
    # LoadBalancer
    load_balancer = ELB("LoadBalancer")
    cloudfront >> load_balancer

    with Cluster("ECS Cluster"):
        ecs_icon = ECS("ECS Service")
        
        # Fargate
        with Cluster("Fargate Cluster"):
            fargate_icon = Fargate("Fargate Service")
            containers = [ElasticContainerServiceContainer("Container 1"), 
                          ElasticContainerServiceContainer("Container 2"), 
                          ElasticContainerServiceContainer("Container 3")]
            
            fargate_icon >> containers
            load_balancer >> fargate_icon
        
    # DynamoDB
    dynamodb = Dynamodb("DynamoDB")
    containers >> dynamodb

実行した結果の画像がこちら。

幾つか指示を出して途中で修正してもらいましたが、意図通りの出力ができました。

まとめ

ChatGPTを用いてAWSの構成図を作成するPythonコードを作成してもらいました。
正式サービス名と異なる一部のモジュールに関しては、正式サービス名に引っ張られエラーになる可能性が高いです。 エラーが出た場合はおとなしくDiagramsのドキュメントを参照し手動で修正した方が早そうでした。
今後もChatGPTでどういった使い方ができるか探求していこうと思います。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

ChatGPT (Advanced Data Analysis)でBNF式からパーサーを生成する

こんにちは、最近ピアノを習い始めた安部です。

今回は、ChatGPTのAdvanced Data Analysis (旧Code Interpreter)にBNF式を与えてパーサーを作成してもらおうと思います。

BNF式のように機械的に解釈可能なものであれば、正確にコードを生成してくれるのではないでしょうか?

BNFでうまくいけば、その他の様々な形式のデータやフォーマットからパーサーを自動生成してくれることが期待できそうです。

1. BNFとは

 BNFバッカス・ナウア記法)とは、プログラムの構文規則(文脈自由文法)を記述するための記法です。

 正確な定義よりも具体例を見た方が早く理解できると思うので、例を示します。

 『プログラム意味論』(横内寛文 著)の冒頭に登場する、非常に単純なプログラムを許容する言語の定義です。

<変数> ::= A | B | C | ... | Z
<定数> ::= 0 | 1 | 2 | ... | 9
<式> ::= <変数> | <定数> | <式> + <式> | <式> - <式>
<文> ::= <変数> := <式>
    | if <式> = <式> then <文> else <文>
    | for <式> do <文>
    | noop
    | <文>; <文>

眺めれば何となく分かると思いますが、このBNF式で定義される言語には以下のような特徴があります。

  • 変数として大文字アルファベット1文字のみを使える。
  • 定数は0~9の自然数のみ。
  • 演算は+と-だけ。
  • 代入 / 簡単な条件分岐 / ループ / 何もしない(noop) / 文の連続実行 ができる。

許容できるプログラムの例を示します(見やすいよう、適宜改行・インデンテーションをしています)。

X := 5;
Y := 2;

Z := 0;
for Y do Z := Z + X;

2. 簡単なプログラム言語のパーサーを生成してもらう

ではさっそく、ChatGPTのAdvanced Data Analysis (旧Code Interpreter)で上記のBNF式で定義される言語をパースするプログラムを、Pythonを使って書いてもらいましょう。

まずはシンプルにお願いしてみます。

最初の指示

特に凝った指示は出していませんが、字句解析器を実装してからパーサーを実装してくれるようです。

長いので、まずは字句解析器の実装部分をお見せします。

字句解析器を作成し、動作確認までしてくれた

簡単なテストまで実施してくれて、気が利いてますね!

続けて、抽象構文木(AST)を生成するパーサーの実装に入りました。

こちらも長いですが、ChatGPTからのレスポンスの続きをお見せします。 コード部分は長すぎるので、テキストで載せます。

続けて、パーサーを実装してくれる

class ParserError(Exception):
    pass

class ASTNode:
    pass

class Variable(ASTNode):
    def __init__(self, name: str):
        self.name = name

class Constant(ASTNode):
    def __init__(self, value: int):
        self.value = value

class BinaryOp(ASTNode):
    def __init__(self, left: ASTNode, op: str, right: ASTNode):
        self.left = left
        self.op = op
        self.right = right

class Assignment(ASTNode):
    def __init__(self, var: Variable, expr: ASTNode):
        self.var = var
        self.expr = expr

class IfStatement(ASTNode):
    def __init__(self, condition_left: ASTNode, condition_right: ASTNode, then_branch: ASTNode, else_branch: ASTNode):
        self.condition_left = condition_left
        self.condition_right = condition_right
        self.then_branch = then_branch
        self.else_branch = else_branch

class ForLoop(ASTNode):
    def __init__(self, count: ASTNode, body: ASTNode):
        self.count = count
        self.body = body

class NoOp(ASTNode):
    pass

class Sequence(ASTNode):
    def __init__(self, first: ASTNode, second: ASTNode):
        self.first = first
        self.second = second

class Parser:
    def __init__(self, tokens: List[Tuple[str, Union[str, int]]]):
        self.tokens = tokens
        self.pos = 0

    def consume(self, expected_type: str) -> Tuple[str, Union[str, int]]:
        if self.pos < len(self.tokens) and self.tokens[self.pos][0] == expected_type:
            token = self.tokens[self.pos]
            self.pos += 1
            return token
        else:
            raise ParserError(f"Expected token of type {expected_type}, but got {self.tokens[self.pos]}")

    def variable(self) -> Variable:
        _, name = self.consume(VAR)
        return Variable(name)

    def constant(self) -> Constant:
        _, value = self.consume(CONST)
        return Constant(value)

    def expression(self) -> ASTNode:
        left = self.term()

        # Check if there's a binary operator
        while self.pos < len(self.tokens) and self.tokens[self.pos][0] == OPERATOR:
            op = self.consume(OPERATOR)[1]
            right = self.term()
            left = BinaryOp(left, op, right)

        return left

    def term(self) -> ASTNode:
        # First try to parse a variable
        if self.pos < len(self.tokens) and self.tokens[self.pos][0] == VAR:
            return self.variable()

        # Then try to parse a constant
        elif self.pos < len(self.tokens) and self.tokens[self.pos][0] == CONST:
            return self.constant()

        else:
            raise ParserError(f"Unexpected token {self.tokens[self.pos]}")

    def statement(self) -> ASTNode:
        # Check for an assignment statement
        if self.pos < len(self.tokens) and self.tokens[self.pos][0] == VAR:
            var = self.variable()
            self.consume(ASSIGN)
            expr = self.expression()
            return Assignment(var, expr)

        # Check for an if statement
        elif self.pos < len(self.tokens) and self.tokens[self.pos][1] == "if":
            self.consume(KEYWORD)  # "if"
            condition_left = self.expression()
            self.consume(EQUAL)
            condition_right = self.expression()
            self.consume(KEYWORD)  # "then"
            then_branch = self.statement()
            self.consume(KEYWORD)  # "else"
            else_branch = self.statement()
            return IfStatement(condition_left, condition_right, then_branch, else_branch)

        # Check for a for loop
        elif self.pos < len(self.tokens) and self.tokens[self.pos][1] == "for":
            self.consume(KEYWORD)  # "for"
            count = self.expression()
            self.consume(KEYWORD)  # "do"
            body = self.statement()
            return ForLoop(count, body)

        # Check for a noop
        elif self.pos < len(self.tokens) and self.tokens[self.pos][1] == "noop":
            self.consume(KEYWORD)  # "noop"
            return NoOp()

        # Check for a sequence of statements
        elif self.pos < len(self.tokens) and self.tokens[self.pos][0] == SEMICOLON:
            self.consume(SEMICOLON)
            first = self.statement()
            second = self.statement()
            return Sequence(first, second)

        else:
            raise ParserError(f"Unexpected token {self.tokens[self.pos]}")

    def parse(self) -> ASTNode:
        ast = self.statement()
        if self.pos != len(self.tokens):
            raise ParserError("Extra tokens found after parsing")
        return ast

# Test the parser
parser = Parser(tokenize(test_input))
ast = parser.parse()
ast

こちらも自動でテストをしてくれましたが、失敗しました。

初回実装は失敗

すると、勝手にデバッグしてコードを修正してくれます。 修正過程のコードは省略してお見せします。

デバッグ・修正まで自動でやってくれる

この後もデバッグ・修正が続き、ついにパーサーが完成しました!

エラーが直り、パーサーが完成

さらに、お願いせずとも人間が読みやすいように抽象構文木を表示してくれます。

人間の読める形式でASTを表示してくれる

ここでは、テストとしてA : = 1+ B; if C = 2 then noop else B := 3の解析が行われています。

まだちょっと読みにくいので、もっと見やすくしてくれないかお願いしてみます。

ASTをツリー形式で表示するよう指示を出す

すると、多少見やすく修正してくれました。

ASTを少し見やすくしてくれた

ノードと線で表現された抽象構文木を画像で示してくれたら最高でしたが、さすがにそれは明示的に指示しないとダメでした。 (Advanced Data Analysisではgraphvizも使えるので、お願いしたらやってくれます。)

とはいえある程度見やすく表示してくれたので、今回はこれで良しとしておきましょう!

まとめ

何回かエラーが出ましたが、無事にパーサーができました。

長いやり取りがあったように見えたかもしれませんが、実はこちらが指示を出したのは2回だけです。 テスト/デバッグ/修正をChatGPTが勝手にやってくれるので、大変便利ですね。

自前のパーサーを作りたい場面はそう多くありませんが、自作プログラミング言語で遊びたい人には役に立つかもしれません!

Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Grandmasterと一緒に働きたエンジニアWanted! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com

Azure OpenAI Service 「on your data」 構成でのセキュリティ性を向上させる

こんにちは、igaです。
先日、久しぶりにライブで声を出したらのどが枯れてしまいました。


前回に引き続き、Azure OpenAIのセキュリティを向上させるため、ネットワークのアクセス制限について確認します。
今回は、以前検証した独自データを使用する場合のネットワークのアクセス制限について確認します。

acro-engineer.hatenablog.com

Azure OpenAIの構成

前回の構成で、Azure OpenAIに対してインターネットからのアクセス制限を行いました。

独自データ(原文の表記はon your data)を使用する場合、構築した直後はデータを保持するCognitive Searchがインターネット上のどこからでもREST APIによるリクエストが受信可能な状態になっています。

Cognitive Searchを利用するためには、通常、APIキーが必要になります。
APIキー自体、ランダムな数字と文字で生成されるもので、これだけでもある程度セキュリティは確保できるものの、もし、APIキーが外部に知られてしまうと、想定していない人や環境から、自社のAzure OpenAIにアクセスされてしまいます。

APIキーが万が一漏洩してしまった場合に、前回のAzure OpenAIと同様にCognitive Searchを不正に利用されないようにするため、ネットワークのアクセスを制限します。


ネットワークアクセス制限のポイント

2023年9月時点では、Azure OpenAIがon your dataで利用するCognitive Searchのネットワークアクセスを制限するには、Microsoftへの申請が必要になります。
(以下のページの内容で、「仮想ネットワークのサポート & プライベート エンドポイントのサポート」を参照してください。)
learn.microsoft.com


申請に対しての審査が行われ、審査がOKであれば5営業日以内にメールでAzure OpenAIからのアクセス先URLがメールで連絡されます。

利用までの流れは次のようになります。

①Azure OpenAIをデプロイする
②Azure OpenAI Studioからon your dataの投入を行う
③ネットワークアクセス制限の申請を行う
④Cognitive Searchのネットワークアクセスを制限する
⑤Azure OpenAIのネットワークアクセスを制限する

①Azure OpenAIをデプロイする

こちらの記事を参考に、Azure OpenAIをデプロイします。
Azure OpenAI Service のはじめ方 - Taste of Tech Topics

②Azure OpenAI Studioからon your dataの投入を行う

こちらの記事を参考に、Azure OpenAIにon your dataでデータを登録します。
Azure OpenAI Service 「on your data」 で独自データを使ったチャットを実現する - Taste of Tech Topics

③ネットワークアクセス制限の申請を行う

以下のヘルプにあるように、on your dataを投入したCognitive SearchとAzure OpenAIにアクセス制限を行うための申請を行います。
learn.microsoft.com


申請画面には、Cognitive SearchとAzure OpenAIの情報を記入するようになっています。

Cognitive SearchのリソースIDを指定します。
Cognitive Searchの「プロパティ」を選択して、要点のIDに表示される値をFormに入力します。

Azure OpenAIの場所を指定します。
Azure OpenAIの「概要」を選択して、基本の場所に表示される値をFormに入力します。

これで、申請が通るまで待ちます。

④Cognitive Searchのネットワークアクセスを制限する

Cognitive Searchの「ネットワーク」から「パブリックアクセス」を選択します。
作成した状態では「すべてのネットワーク」が選択された状態になっているため、インターネットからアクセスできる状態になっています。

これを、「選択されたネットワーク」にすることで限られたネットワークからのアクセスに制限します。
「クライアントIPアドレスの追加」を押すことで、Azure Portalを操作しているネットワークからの接続が許可されるので、「保存」を押して設定した情報を反映します。

続いて、「プライベートアクセス」タブを選択すると、申請したCognitive Searchへのプライベートエンドポイントが表示されます。

接続状態が「保留中」になっているため、利用可能な状態にするため「承認」を押します。
承認の確認ダイアログが表示されるので、説明を入力して「OK」を押すとプライベートエンドポイントの接続状態が「承認済み」になり利用できるようになります。


⑤Azure OpenAIのネットワークアクセスを制限する

前回の内容を元にして、Azure OpenAIのネットワークアクセスを制限します。
acro-engineer.hatenablog.com

Azure OpenAIにアクセスするPythonのプログラムで、on your dataを利用するため、dataSourcesオプションを指定します。

import os
import math
import json
import requests

def chat_to_ai():
  api_type = "azure"
  api_base = "https://xxxxx.openai.azure.com"
  api_version = "2023-06-01-preview"  # on your data に対応したAPIバージョンを指定する
  api_key = os.getenv("OPENAI_API_KEY")
  deployment_id = "ChatTest"

  api_url = f'{api_base}/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}'

  req_headers = {
    "Content-Type": "application/json",
    "api-key": f'{api_key}'
  }

  # 問い合わせ内容
  messages = [
      {
          "role":"system",
          "content":"You are an AI assistant that helps people find information."
      },
      {
          "role": "user",
          "content": "SQLインジェクションによる脅威と根本的な対策を教えてください"
      }
  ]

  # on your dataの指定
  dataSources = [
      {
          "type": "AzureCognitiveSearch",
          "parameters": {
              "endpoint": "https://xxxxxxx.search.windows.net",
              "key": "xxxxxxxxxxx",
              "indexName": "on-your-data-index",
              "semanticConfiguration": "",
              "queryType": "simple",
              "fieldsMapping": None,
              "inScope": True,
              "roleInformation": "You are an AI assistant that helps people find information."
          }
      }
  ]

  req_body = {
    "messages": messages,
    "dataSources": dataSources  # On your dataを利用する
  }

  response = requests.post(api_url, headers=req_headers, data=json.dumps(req_body))
  if response.status_code == 200:
    # Azure OpenAIから正常応答が返ってきた場合は、レスポンスBodyをJSON形式に変換して返す
    return json.loads(response.text)
  else:
    # Azure OpenAIから正常応答が返ってこない場合は、Azure OpenAIのレスポンスと同じ構造でHTTPステータスコードを返す
    return {
      'choices': [
        {
          'message': {
            'content': f'error status: {response.status_code}'
          }
        }
      ]
    }

if __name__ == '__main__':
  ai_response = chat_to_ai()

  print(ai_response.get('choices', [{}])[0].get('message', {}).get('content', 'NO_CONTENT'))

このプログラムを動かすと、on your dataに含まれる内容を要約して返してくれます。

プライベートアクセス時の制限

Azure OpenAIのネットワークアクセス制限を行うと、Azure OpenAI StudioからAzure OpenAIに対する操作ができなくなります。
そのため、on your dataのデータ投入を行ってからネットワークアクセスの制限を行うことになります。

まとめ

Azure OpenAIでon your dataを利用するネットワークの制限を行う方法を確認しました。
アクセス制限を行うことで、意図しない利用を避けることができるため、要件に応じて導入を検討してください。



Acroquest Technologyでは、キャリア採用を行っています。

  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

 
少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

Kaggle Grandmasterと一緒に働きたエンジニアWanted! - Acroquest Technology株式会社のデータサイエンティストの採用 - Wantedlywww.wantedly.com

ChatGPT/Bing Chat Enterprise で AWS CDK v2 のコードを作成する

先週1週間、リフレッシュ休暇(当社の年次休暇)を頂き、宮古島旅行に行ってきました。
天気にも恵まれ、海もとてもキレイだったので、テンションが上がりっぱなしだった菅野です。

最近、AWSのリソースを作成する際に、CDKを利用することが増えてきました。
AWSのリソース定義としては、CloudFormationがありますが、CDKは、以下のようなメリットがあります。

  • TypeScriptやPython等、慣れたプログラミング言語AWSのリソースを定義できる。
  • CloudFormationと比べると、大幅に記述量が減り、分かりやすくなる。

今回はAWS CDKに不慣れな私が生成AIを利用してどこまで簡単に作成できるか試してみます。
最初に、ChatGPTを利用して、CDKのコード出力を行おうとしたのですが、CDKの最新バージョン(v2)は2021年12月にリリースされており、2021年9月までの情報を学習しているChatGPTのGPT-4ではCDKv2のコードを生成できません。

そこで今回は、Bing Chat Enterprise を利用して、CDKv2のコード生成を行ってみました。

AWS CDKとは

AWS Cloud Development Kit の略です。
Pyton,TypeScript等様々な言語でAWSのリソースをソースコードとして管理できるフレームワークです。 ソースコードで作成したリソースはAWS CDK Cliを用いることでデプロイすることができます。

docs.aws.amazon.com

作成するAWSリソースの説明

本記事ではPetShopのペットを管理する簡単なWebアプリケーションを例として以下のようなリソースを作成してもらいます。

  • S3の静的ホスティングでWebクライアントを作成。
  • APIGateway、Lambda、DynamoDBを用いたAPIを作成。
  • 上記Webクライアント、API双方の前段にCloudFrontを配置する。

Bing Chat EnterpriseでCDKのリソースを作成

では早速Bing Chat EnterpriseにCDKコードを作成してもらいましょう。
(中略)

それぞれ以下のコードを出力してくれました。
CDKコード

from aws_cdk import core
from aws_cdk import (
    aws_s3 as s3,
    aws_s3_deployment as s3_deploy,
    aws_lambda as _lambda,
    aws_apigateway as api_gw,
    aws_dynamodb as ddb,
    aws_cloudfront as cf
)

class PetShopCdkStack(core.Stack):

    def __init__(self, scope: core.Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # S3 bucket for web hosting
        website_bucket = s3.Bucket(self, "WebsiteBucket",
            website_index_document="index.html",
            removal_policy=core.RemovalPolicy.DESTROY
        )

        # DynamoDB table for pets
        pet_table = ddb.Table(self, "PetTable",
            partition_key=ddb.Attribute(name="id", type=ddb.AttributeType.STRING),
            removal_policy=core.RemovalPolicy.DESTROY
        )

        # Lambda function for CRUD operations
        pet_lambda = _lambda.Function(self, "PetLambda",
            runtime=_lambda.Runtime.PYTHON_3_9,
            handler="handler.handler",
            code=_lambda.Code.asset("lambda"),
            environment={
                "TABLE_NAME": pet_table.table_name
            }
        )

        # Grant the Lambda function permissions to read/write the DynamoDB table
        pet_table.grant_read_write_data(pet_lambda)

        # API Gateway for the Lambda function
        pet_api = api_gw.LambdaRestApi(self, "PetApi",
            handler=pet_lambda,
            default_cors_preflight_options=api_gw.CorsOptions(
                allow_origins=["*"]
            )
        )

        # CloudFront Distribution
        distribution = cf.CloudFrontWebDistribution(self, "Distribution",
            origin_configs=[
                cf.SourceConfiguration(
                    s3_origin_source=cf.S3OriginConfig(
                        s3_bucket_source=website_bucket
                    ),
                    behaviors=[cf.Behavior(is_default_behavior=True)]
                ),
                cf.SourceConfiguration(
                    custom_origin_source=cf.CustomOriginConfig(
                        domain_name=pet_api.domain_name,
                        origin_protocol_policy=cf.OriginProtocolPolicy.HTTPS_ONLY
                    ),
                    behaviors=[cf.Behavior(path_pattern="/api/*")]
                )
            ]
        )

静的ホスティングクライアント

<!DOCTYPE html>
<html>
<head>
    <title>Pet Shop</title>
</head>
<body>
    <h1>Welcome to the Pet Shop!</h1>
    <button onclick="fetchData()">Fetch Data</button>
    <div id="data"></div>

    <script>
    function fetchData() {
        fetch('/api')
            .then(response => response.json())
            .then(data => document.getElementById('data').textContent = JSON.stringify(data));
    }
    </script>
</body>
</html>

ちょっとWebUIがシンプルすぎるのと、S3、APIGatewayはCloudFront側からのみのリクエストを受け付けるようになっていないため、変更を加えてもらいます。
また、Lambda関数も出力してもらえなかったので追加で出力するよう依頼していきます。


(中略)

(中略)

(中略)
(中略)

実装が中途半端なままファイル出力したり、変更点のみの部分的な出力であったので、何度か応答を繰り返して更新していってもらいました。

最終的な結果は以下のようになりました。

CDKコード

from constructs import Construct
from aws_cdk.core import Stack, RemovalPolicy, CfnOutput
from aws_cdk.aws_s3 import Bucket, BlockPublicAccess
from aws_cdk.aws_s3_deployment import BucketDeployment, Source
from aws_cdk.aws_lambda import Function, Runtime, Code
from aws_cdk.aws_apigateway import LambdaRestApi, CorsOptions, MethodOptions, RestApi
from aws_cdk.aws_dynamodb import Table, Attribute, AttributeType
from aws_cdk.aws_cloudfront import Distribution, SourceConfiguration, S3OriginConfig, Behavior, CustomOriginConfig, OriginAccessIdentity

class PetshopCdkStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # DynamoDB Table
        pet_table = Table(self, "PetTable",
            partition_key=Attribute(name="id", type=AttributeType.STRING),
            removal_policy=RemovalPolicy.DESTROY
        )

        # Lambda function
        pet_lambda = Function(self, "PetHandler",
            runtime=Runtime.PYTHON_3_8,
            handler="handler.handler",
            code=Code.from_asset("lambda"),
            environment={
                'PET_TABLE_NAME': pet_table.table_name
            }
        )

        pet_table.grant_read_write_data(pet_lambda)

        # API Gateway
        pet_api = RestApi(self, "Endpoint",
            default_cors_preflight_options=CorsOptions(
                allow_origins=["*"],
                allow_methods=["GET", "POST", "PUT", "DELETE"]
            ),
            handler=pet_lambda,
            proxy=False
        )

        pet_resource = pet_api.root.add_resource('api')

        # S3 bucket for web hosting
        website_bucket = Bucket(self, "WebsiteBucket",
            website_index_document="index.html",
            auto_delete_objects=True,
            removal_policy=RemovalPolicy.DESTROY,
            block_public_access=BlockPublicAccess.BLOCK_ALL
        )

        BucketDeployment(self, "DeployWebsite",
            sources=[Source.asset('./website')],
            destination_bucket=website_bucket,
            distribution_paths=["/*"]
        )

        # CloudFront Distribution
        oai = OriginAccessIdentity(self, "OAI")
        
        website_bucket.grant_read(oai)

        distribution = Distribution(self, "Distribution",
            default_behavior_options={
                "origin": S3Origin(website_bucket),
                "origin_access_identity": oai,
            },
            additional_behaviors={
                "/api/*": {
                    "origin": HttpOrigin(f"{pet_api.rest_api_id}.execute-api.{self.region}.amazonaws.com"),
                    "origin_protocol_policy": OriginProtocolPolicy.HTTPS_ONLY,
                }
            }
        )

        CfnOutput(self, "CloudFrontURL",
            value=distribution.domain_name,
            description="The CloudFront distribution URL"
        )


OAIを用いてアクセス制限をかけるようにしてくれています。

静的ホスティングWebクライアント

<!DOCTYPE html>
<html>

<head>
    <title>Pet Shop</title>
    <script>
        const API_ENDPOINT = "/api";

        async function createPet() {
            const name = document.getElementById('name').value;
            const type = document.getElementById('type').value;
            const data = { id: Date.now().toString(), name, type };
            await fetch(API_ENDPOINT, {
                method: 'POST',
                body: JSON.stringify(data),
                headers: { 'Content-Type': 'application/json' }
            });
            loadPets();
        }

        async function updatePet() {
            const id = document.getElementById('updateId').value;
            const name = document.getElementById('updateName').value;
            const type = document.getElementById('updateType').value;
            const data = { id, name, type };
            await fetch(API_ENDPOINT, {
                method: 'PUT',
                body: JSON.stringify(data),
                headers: { 'Content-Type': 'application/json' }
            });
            loadPets();
        }

        async function deletePet() {
            const id = document.getElementById('deleteId').value;
            await fetch(`${API_ENDPOINT}?id=${id}`, {
                method: 'DELETE'
            });
            loadPets();
        }

        async function loadPets() {
            const petsList = document.getElementById('petsList');
            petsList.innerHTML = '';
            const pets = await getAllPets();
            pets.forEach(pet => {
                const petItem = document.createElement('li');
                petItem.textContent = `${pet.name} (${pet.type}) - ID: ${pet.id}`;
                petsList.appendChild(petItem);
            });
        }

        async function getAllPets() {
            const response = await fetch(API_ENDPOINT);
            return response.json();
        }

        window.onload = () => {
            loadPets();
        };
    </script>
</head>

<body>
    <h1>Welcome to Pet Shop</h1>

    <h2>Add a new pet</h2>
    <div>
        <label>Name: <input type="text" id="name"></label>
        <label>Type: <input type="text" id="type"></label>
        <button onclick="createPet()">Add Pet</button>
    </div>

    <h2>Update a pet</h2>
    <div>
        <label>ID: <input type="text" id="updateId"></label>
        <label>Name: <input type="text" id="updateName"></label>
        <label>Type: <input type="text" id="updateType"></label>
        <button onclick="updatePet()">Update Pet</button>
    </div>

    <h2>Delete a pet</h2>
    <div>
        <label>ID: <input type="text" id="deleteId"></label>
        <button onclick="deletePet()">Delete Pet</button>
    </div>

    <h2>List of pets</h2>
    <ul id="petsList"></ul>
</body>

</html>

CRUD処理も各フォームから出力するようになっています。

Lambdaハンドラ

import json
import boto3
import os

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])

def handler(event, context):
    try:
        if event['httpMethod'] == 'GET':
            # Retrieve pet by ID or list all
            pet_id = event['queryStringParameters'].get('id')
            if pet_id:
                response = table.get_item(Key={'id': pet_id})
                return {
                    'statusCode': 200,
                    'body': json.dumps(response.get('Item'))
                }
            else:
                response = table.scan()
                return {
                    'statusCode': 200,
                    'body': json.dumps(response['Items'])
                }

        elif event['httpMethod'] == 'POST':
            # Create a new pet
            body = json.loads(event['body'])
            table.put_item(Item=body)
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet created successfully!'})
            }

        elif event['httpMethod'] == 'PUT':
            # Update a pet
            body = json.loads(event['body'])
            table.update_item(Key={'id': body['id']}, UpdateExpression="set info=:r", ExpressionAttributeValues={':r': body['info']})
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet updated successfully!'})
            }

        elif event['httpMethod'] == 'DELETE':
            # Delete a pet
            pet_id = event['queryStringParameters'].get('id')
            if pet_id:
                table.delete_item(Key={'id': pet_id})
                return {
                    'statusCode': 200,
                    'body': json.dumps({'message': 'Pet deleted successfully!'})
                }
            else:
                return {
                    'statusCode': 400,
                    'body': json.dumps({'message': 'Pet ID is required!'})
                }

        else:
            return {
                'statusCode': 400,
                'body': json.dumps({'message': 'Invalid request method!'})
            }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'message': str(e)})
        }

手動での修正

Bing Chat Enterprise で生成したのみで作成したリソースファイルでは一部エラーが出てデプロイできない部分が出たり、疎通できない部分があったので手動で修正をかけてあげましょう。

CDKコード

from constructs import Construct
from aws_cdk import Stack, RemovalPolicy, CfnOutput, Duration
from aws_cdk.aws_s3 import Bucket, BlockPublicAccess
from aws_cdk.aws_s3_deployment import BucketDeployment, Source
from aws_cdk.aws_lambda import Function, Runtime, Code
from aws_cdk.aws_apigateway import LambdaRestApi
from aws_cdk.aws_dynamodb import Table, Attribute, AttributeType
from aws_cdk.aws_cloudfront import CloudFrontWebDistribution, SourceConfiguration, S3OriginConfig, Behavior, CustomOriginConfig, OriginAccessIdentity, CloudFrontAllowedMethods, OriginProtocolPolicy


class PetshopCdkStack(Stack):

    def __init__(self, scope: Construct, id: str, **kwargs) -> None:
        super().__init__(scope, id, **kwargs)

        # DynamoDB Table
        pet_table = Table(self, "PetTable",
            partition_key=Attribute(name="id", type=AttributeType.STRING),
            removal_policy=RemovalPolicy.DESTROY
        )

        # Lambda function
        pet_lambda = Function(self, "PetHandler",
            runtime=Runtime.PYTHON_3_8,
            handler="handler.handler",
            code=Code.from_asset("lambda"),
            environment={
                'PET_TABLE_NAME': pet_table.table_name
            }
        )

        pet_table.grant_read_write_data(pet_lambda)

        # API Gateway
        pet_api = LambdaRestApi(self, "Endpoint",
            handler=pet_lambda,
            proxy=False
        )

        api_resource = pet_api.root.add_resource("api")
        pet_resource = api_resource.add_resource("pet")
        pet_id_resource = pet_resource.add_resource("{id}")

        pet_resource.add_method('GET')    # Read
        pet_resource.add_method('POST')   # Create
        pet_resource.add_method('PUT')    # Update
        pet_resource.add_method('DELETE') # Delete

        pet_id_resource.add_method('GET')

        # S3 bucket for web hosting
        website_bucket = Bucket(self, "WebsiteBucket",
            website_index_document="index.html",
            auto_delete_objects=True,
            removal_policy=RemovalPolicy.DESTROY,
            block_public_access=BlockPublicAccess.BLOCK_ALL
        )


        # CloudFront Distribution
        oai = OriginAccessIdentity(self, "OAI")
        
        website_bucket.grant_read(oai)

        distribution = CloudFrontWebDistribution(self, "Distribution",
            origin_configs=[
                SourceConfiguration(
                    s3_origin_source=S3OriginConfig(
                        s3_bucket_source=website_bucket,
                        origin_access_identity=oai
                    ),
                    behaviors=[Behavior(is_default_behavior=True)]
                ),
                SourceConfiguration(
                    custom_origin_source=CustomOriginConfig(
                        origin_path='/prod',
                        domain_name=f"{pet_api.rest_api_id}.execute-api.{self.region}.amazonaws.com",
                        origin_protocol_policy=OriginProtocolPolicy.HTTPS_ONLY
                    ),
                    behaviors=[Behavior(path_pattern="api/*", allowed_methods=CloudFrontAllowedMethods.ALL,default_ttl=Duration.seconds(0),max_ttl=Duration.seconds(0),min_ttl=Duration.seconds(0))]
                )
            ]
        )

        BucketDeployment(self, "DeployWebsite",
            sources=[Source.asset('./website')],
            destination_bucket=website_bucket,
            distribution_paths=["/*"],
            distribution=distribution
        )

        CfnOutput(self, "CloudFrontURL",
            value=distribution.distribution_domain_name,
            description="The CloudFront distribution URL"
        )

Lambdaハンドラ

  • GetのメソッドについてQueryStrinParameterではなくPathParameterを利用するように修正
  • 判定条件を修正してPathParameterが定義されていない際に参照エラーが出ないように変更
import json
import boto3
import os

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['PET_TABLE_NAME'])

def handler(event, context):
    try:
        if event['httpMethod'] == 'GET':
            # Retrieve pet by ID or list all
            if event['pathParameters'] and event['pathParameters'].get('id'):
                response = table.get_item(Key={'id': event['pathParameters'].get('id')})
                return {
                    'statusCode': 200,
                    'body': json.dumps(response.get('Item'))
                }
            else:
                response = table.scan()
                return {
                    'statusCode': 200,
                    'body': json.dumps(response['Items'])
                }

        elif event['httpMethod'] == 'POST':
            # Create a new pet
            body = json.loads(event['body'])
            table.put_item(Item=body)
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet created successfully!'})
            }

        elif event['httpMethod'] == 'PUT':
            # Update a pet
            body = json.loads(event['body'])
            table.update_item(Key={'id': body['id']}, UpdateExpression="set info=:r", ExpressionAttributeValues={':r': body['info']})
            return {
                'statusCode': 200,
                'body': json.dumps({'message': 'Pet updated successfully!'})
            }

        elif event['httpMethod'] == 'DELETE':
            # Delete a pet
            pet_id = event['queryStringParameters'].get('id')
            if pet_id:
                table.delete_item(Key={'id': pet_id})
                return {
                    'statusCode': 200,
                    'body': json.dumps({'message': 'Pet deleted successfully!'})
                }
            else:
                return {
                    'statusCode': 400,
                    'body': json.dumps({'message': 'Pet ID is required!'})
                }

        else:
            return {
                'statusCode': 400,
                'body': json.dumps({'message': 'Invalid request method!'})
            }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({'message': str(e)})
        }

デプロイ、Webページへのアクセス

以下のコマンドでデプロイしましょう。

cdk bootstrap
cdk deploy

以下のようにコンソール出力されたらデプロイ成功です。

PetshopCdkStack.CloudFrontURLに表示されているURLへアクセスしてみましょう。


必要情報を入力して、Add Petボタンを押すと 無事追加したペットが表示されました。

まとめ

Bing Chat Enterpriseを活用してAWS CDKのコードを作成、デプロイしてAWSのリソースを作成しました。
他のPythonコードとは異なり、1回のプロンプト入力で動作するコードを出力することは難しく、手動での修正や細かい調整が必要になりそうですが、ある程度の形は作成できるのでCDKに不慣れなエンジニアでも作業時間は大幅に短縮できそうです。
今後もChatGPTの可能性に迫っていこうと思います。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

ChatGPTでゴールシークプロンプトを使って、バックアップシステムの提案をしてもらう

近頃、いろいろなプロテイン飲料を試しているkonnoです。
今のところバナナ風味が一番好みです!!

ChatGPTは明確な指示を与えるほど、精度の高い回答を返してくれるといわれています。
しかし、そもそも明確な指示を与えられるほど、ゴールがイメージできていないという場合もあるでしょう。

今回は、たとえ私たち人間のゴールイメージが曖昧であっても、ChatGPTから明確なゴールが得られるという「ゴールシークプロンプト」を使ってみたいと思います。

ゴールシークプロンプトとは?

そもそも、ゴールシークプロンプトとは「〇〇〇をしたい」というゴールを設定し、どのようにしたらそのゴールを実現できるのか?という問いを繰り返していき、ゴールを探索するという手法であり、一般的に次のような構造を持ちます。

  1. ゴールを設定する
  2. ゴール達成に必要な手順に分解する
  3. 必要な変数を抽出し、ゴールを再定義する
  4. ゴールを達成するために手順を一つずつ実行

この構造になるよう、プロンプトを作成します。

バックアップシステムを提案してもらう

今回は、Webアプリケーションのバックアップを行うシステムの検討をしてもらおうと思います。
ちなみに、ChatGPTにそのままそれを聞いても、おそらくこのような会話になってしまうのではないでしょうか。

一般的な回答は得られる

それはその通りですが、あくまでごく一般的な内容ですので、残念ながらそのまま利用できる形ではありません。

そこでゴールシークプロンプトを使ってみます。次のようなプロンプトを用意してみました。

次のプロセスに従ってゴールを達成するために適した提案を教えてください。
このプロセスは、より具体的な提案になるよう、私がOKを出すまで繰り返し行ってください。

# ゴール 
バックアップシステムを構築したい

# ステップ1
最初に何に利用するバックアップシステムなのか、私に聞いてください。
私が質問に回答するので、その回答をもとにゴールを達成するために必要な手順に分解してください。
 
# ステップ2
ステップ1で分解した手順を列挙し、それぞれの手順を実行するのに必要な変数を抽出してください。

# ステップ3
ステップ2で抽出した変数を用いて、ゴールを再定義してください。

# ステップ4
ゴール達成のためにステップ2で列挙した手順を一つずつ実行してください。

さて、うまく動くでしょうか?

社内Webツールのバックアップシステムを提案してもらう

先ほどのプロンプトを使って、社内Webツールのバックアップシステムを提案してもらいましょう。
まず、GPT-4を選択します。

GPT-4を使用

プロンプトを入力します。

ゴールシークプロンプトを入力

すると、ステップ1の質問が表示されます。

ステップ1の質問

この問いには、仮に以下のように回答してみます。

バックアップを取るデータの種類は何ですか?(例:ドキュメント、写真、データベースなど):ドキュメント、データベース
バックアップを取るデータの総量はおおよそどのくらいですか?:1TB
バックアップの頻度はどのくらいを想定していますか?(例:毎日、毎週、毎月など):毎日1回
バックアップデータの保存期間はどのくらいを想定していますか?:1年間
バックアップデータの保存場所の希望はありますか?(例:外部ハードドライブ、クラウド、オフサイトなど):オンプレ
バックアップシステムに求める主な機能や要件はありますか?(例:暗号化、圧縮、バージョン管理など):圧縮、暗号化、バージョン管理

すると、ステップ2では、ChatGPTが具体的なゴール設定に必要な手順を教えてくれました。

手順と必要な変数を表示

ステップ3では、抽出した変数を使い、先ほどまで曖昧だったゴールをより詳細に再定義してくれます。

ゴールがより詳細に再定義された

そして、この詳細なゴールに到達するための手順を示します。

ゴール達成までの手順が示される

ここで、もうあと一歩説明が欲しい気がするのですが、このような場合は以下のように「いいね、続けて」と指示するとより詳細に説明してくれることがあります。

一言指示を与える

今回も、こちらの指示を待っていたようで、続けるように指示したところ最終的な詳細手順が出力されました。

最終的な手順が表示された

だいぶゴールが明確になってきたような気がしますね。
あとはゴールに向かって、それぞれの手順をさらにブレイクダウンするようChatGPTに指示するだけです。

手順をChatGPTにブレイクダウンしてもらう

今回は紙面の都合でここまでにしますが、さらにブレイクダウンしていくことで、手順は具体的なものになっていきます。

まとめ

「バックアップシステムを構築したい」という曖昧な指示を出して、あとは質問に答えるだけで、ゴールに至るまでの内容を具体化してくれました。
あたかも、ベテラン経験者や専門家がアドバイスしてくれているかのようです。
システム構築に限らず、ゴールシークプロンプトを使うと、ChatGPTとやり取りをしながら、詳しく知らないことでも、検討を詳細化したり、新しい内容を検討したり、といったことができそうです。

Amazon Kendra と ChatGPT で RAG を実現する

こんにちは、機械学習チーム YAMALEX の駿です。
YAMALEX は Acroquest 社内で発足した、会社の未来の技術を創る、機械学習がメインテーマのデータサイエンスチームです。
(詳細はリンク先をご覧ください。)

今回は Amazon KendraOpenAI ChatGPT を組み合わせてRAGシステムを構築してみます。

RAG とは Retrieval Augmented Generation (検索拡張生成) の略で、 ChatGPT に代表される LLM (大規模言語モデル)でユーザの質問への回答を生成する際に必要な情報(コンテキスト)を事前に検索などを通して取得してから、 コンテキストを踏まえた回答を生成する手法のことです。
言い換えると、RAG は検索そのものの処理ではなく、検索結果を解析し、その内容を分かりやすく要約するものです。

RAG を使用することで、企業内情報やドメイン知識が必要な質問応答において、分かりやすい回答を生成できます。

1. Amazon Kendraとは

Amazon Kendra (以下、 Kendra )は Amazon が提供する、機械学習を利用したインテリジェント検索サービスです。
どのような検索ができるのかは、こちらの記事で紹介しているので、参照してください。

acro-engineer.hatenablog.com

RAG の検索部分に Kendra を使う利点が大きく2つあります。

  1. 自前の実装はゼロで、データ投入、検索を実現できる

    • ドキュメントの取り込み、検索の実行がいかに簡単かは、前回の記事で示した通りです。
  2. LLM が扱うのに最適なチャンクサイズで結果を返してくれる

    • 他の検索サービスを使う場合、 LLM のトークン上限に抵触しないように、検索前後で扱いやすいサイズのチャンクに切り分ける必要がありましたが、 Kendra は LLM で利用されることを想定して、より高い精度で回答できるようなチャンクで検索結果を返します。

    チャンクとは RAG において、検索結果の塊のことを指します。
    一つ一つのチャンクを大きくする/利用するチャンク数を増やすと必要な情報がチャンクに含まれる確率は上がりますが、 その分トークン数が増えてしまうため、金額やトークン上限に響いてきます。
    逆にチャンクを小さくしすぎる/少なすぎると LLM の回答に必要な情報が含まれず、適切な回答ができなくなります。

    • チャンクを作る際には文脈の途中で区切れないようにしないと、など工夫しながらやっていましたが、もうその必要はありません。

2. 構成

AWS Lambda を利用して、 Kendra と ChatGPT を呼び出し、 RAG を実現します。

# 項目 概要
1 Kendra データソース S3: IPA が公開している EC サイト構築・運用セキュリティガイドライン
WebCrawler: Kendra の公式ドキュメント
2 ChatGPT モデル gpt-3.5-turbo

Kendra と ChatGPT を使った構成図

Kendra のインデックスとデータソースの作り方については AWS 公式のドキュメントを参照してください。

docs.aws.amazon.com

3. 実装

順番に実装していきます。

  1. Kendra のインデックスを検索する(上の構成図の 2. 相当)
  2. ChatGPT で回答を生成する(上の構成図の 3. 相当)
  3. 組み合わせて Lambda を作る(上の構成図の 1. 、 4. 相当)

3.1. Kendra のインデックスを検索する

def _retrieve_contexts(question: str, page_size=2) -> dict:
    """Retrieve contexts from Kendra."""
    response = kendra.retrieve(
        IndexId=KENDRA_INDEX,
        QueryText=question,
        PageSize=page_size,
        AttributeFilter={
            "EqualsTo": {
                "Key": "_language_code",
                "Value": {"StringValue": "ja"},
            },
        },
    )

    contexts = [{
        "DocumentTitle": item["DocumentTitle"],
        "Content": item["Content"],
    } for item in response.get("ResultItems", [])]
    return contexts

3.2. ChatGPT で回答を生成する

def _generate_answer(question: str, contexts: dict, max_tokens=512) -> str:
    """Generate answer using ChatGPT."""
    system_prompt = dedent("""\
        You are a Bot that answers User's question.
        Be sure to answer using information in Contexts.

        ## Contexts ##
        {contexts}
    """).format(contexts=json.dumps(contexts, ensure_ascii=False))
    messages = [
        {"role": "system", "content": system_prompt},
        {"role": "user", "content": question},
    ]

    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        temperature=0.0,
        messages=messages,
        max_tokens=max_tokens,
        stop=None,
    )

    answer = response.choices[0]["message"]["content"]
    return answer

3.3. 組み合わせて Lambda を作る

def retrieve_and_generate(question: str) -> tuple[dict, str]:
    """Run RAG using User question."""
    contexts = _retrieve_contexts(question)
    answer = _generate_answer(question, contexts)
    return contexts, answer


def lambda_handler(event, context):
    """Handler for RAG Lambda."""
    question = event.get("question")
    contexts, answer = retrieve_and_generate(question)
    return contexts, answer

インポート文やコメント行も含めて、 100 行足らずで RAG システムを実装できてしまいました。

4. 最終結

次に実際に実行してみて、検索結果と回答を確認します。
※見やすさのため、実際の出力を整形して記載しています。

Lambda を実行する際は、 event に {"question": "質問を入力する"} を入れて実行してください。

4.1. Kendra について質問

【質問】
Kendraの特徴を3つ教えて

【回答】
Kendraの特徴を以下に示します:

1. インデックス作成の柔軟性:Kendraは、最大5つのインデックスを作成することができます。
  それぞれのインデックスは、最大50のデータソースを持つことができます。
  これにより、エンタープライズドキュメントライブラリ全体を効率的にインデックス化することができます。
2. 高いクエリ処理能力:Kendraは、1日あたり約8,000のクエリを処理することができます。
  また、1秒あたり0.1のクエリを処理することも可能です。
  これにより、大量のユーザーからの同時アクセスにも対応することができます。
3. 高い可用性と耐久性:Kendraは、3つのアベイラビリティーゾーン(AZ)で実行されます。
  これにより、データセンターの障害やネットワークの問題に対しても高い可用性と耐久性を提供します。
  また、AWSリージョン内のデータセンターを参照しているため、データのセキュリティも確保されています。

【使用したコンテキスト】
[
  {
    "DocumentTitle": "kendra-dg",
    "Content": "• 10,000 件のドキュメントまたは 3 GB の抽出テ キスト • 1 日あたり約 4,000 クエリ、または 1 秒あたり 0.05 クエリ • 1 つのアベイラビリティーゾーン (AZ) で実行 — 「アベイラビリティーゾーン (AWSリージョン内 のデータセンター)」を参照 機能制限 • プロダクションアプリケーションには適してい ません • レイテンシーや可用性の保証なし Amazon KendraEnterprise Edition は、Amazon Kendraプロダクション環境向けに設計されてお り、すべての機能を備えています。 理想的な使用事例 • エンタープライズドキュメントライブラリ全体 のインデックス作成 • 本番環境へのアプリケーションのデプロイ 特徴 • 最大 5 つのインデックス、それぞれ最大 50 の データソース • 100,000 件のドキュメントまたは 30 GB の抽出 テキスト • 1 日あたり約 8,000 クエリ、または 1 秒あたり 0.1 クエリ • 3 つのアベイラビリティーゾーン (AZ) で実行 — 「アベイラビリティーゾーン (AWSリージョン内 のデータセンター)」を参照 Note Service Quotas コンソールを使用して IAM クォータの増加をリクエストできま"
  },
  {
    "DocumentTitle": "kendra-dg",
    "Content": "権に基づいてフィルタリングできます。 ユーザーアクセスの認証と承認はお客様の責任となります。 Amazon Kendraエディション Amazon Kendraには、開発者版とエンタープライズ版の 2 つのバージョンがあります。 次の表は、それぞ れの機能と 2 つの相違点をまとめたものです。 Amazon Kendra開発者版 Amazon Kendraエンタープライズエディション Amazon Kendraデベロッパーエディション は、Amazon Kendraのすべての機能を低コストで 提供します。 理想的な使用事例 • Amazon Kendraドキュメントのインデックス作 成方法を調べる • 機能を試してみる • を使用するアプリケーションの開発 Amazon Kendra 特徴 • 750 時間の使用を含む無料利用枠 • 最大 5 つのインデックス、それぞれ最大 5 つの データソース • 10,000 件のドキュメントまたは 3 GB の抽出テ キスト • 1 日あたり約 4,000 クエリ、または 1 秒あたり 0.05 クエリ • 1 つのアベイラビリティーゾーン (AZ) で実行 — 「アベイラビリティーゾーン (AWSリージョン内 のデータセンター)」を参照"
  }
]

4.2. セキュリティガイドラインについて質問

【質問】
サイバー保険とは何ですか?

【回答】
サイバー保険は、企業や個人がサイバー攻撃やデータ漏洩などのサイバー事件によって生じる損害や費用を補償する保険のことです。  
具体的には、被害者への損害賠償や法的費用、被害復旧やデータ復旧の費用、プライバシー侵害によるリスク管理費用などが補償されます。  
サイバー保険は、企業や組織が顧客情報や機密情報を保有している場合に特に重要であり、被害発生時の経済的なリスクを軽減するために加入されることが推奨されています。

【使用したコンテキスト】
[
  {
    "DocumentTitle": "000109337",
    "Content": "対策をするまでの期 間内にサイバー攻撃を受けることがないよう、応急処置として、WAF を導入することを推 奨しています。 (「コラム 5:(ご紹介)WAF(Web Application Firewall)の活用方法」も参 考にしてください。) 36 要件7 サイバー保険に加入する。 万が一、ECサイトまたは、自社システムがサイバー攻撃による被害を受けた場合に備え て、サイバー保険に加入することを推奨しています。 考にしてください。) 36 要件7 サイバー保険に加入する。 万が一、ECサイトまたは、自社システムがサイバー攻撃による被害を受けた場合に備え て、サイバー保険に加入することを推奨しています。 サイバー保険については、IPA調査でも顧客情報の漏えい事故を発生させてしまった EC サイトの多くが、被害後に加入していますが、損害賠償や事故対応費用の 負担、収益の減 少を補う効果が認められることから、被害が発生していない場合でも被害発生に備えて加 入することを推奨しています。 コ ラ ム 2 (ご紹介)ECサイト向けホスティング付きセキュリティ保守運用サービス セキュリティ対策について、自社または構築事業 者による対応で賄えない部分を補う際に採りうる選択肢"
  },
  {
    "DocumentTitle": "000109337",
    "Content": "要件6 WAFを導入する。 既に 見つかっている脆弱性に対して対応するまでに期間が必要な場合や、必要となるセ キュリティ対策を実装するまでに期間が必要な場合が 想定されます。 対策をするまでの期 間内にサイバー攻撃を受けることがないよう、応急処置として、WAF を導入することを推 奨してい ます。 (「コラム 5:(ご紹介)WAF(Web Application Firewall)の活用方法」も参 考にしてください。) 36 要件7 サイバー保険に 加入する。 万が一、ECサイトまたは、自社システムがサイバー攻撃による被害を受けた場合に備え て、サイバー保険に加入することを推奨しています。 考にしてください。) 36 要件7 サイバー保険に加入する。 万が一、ECサイトまたは、自社システムがサイバー攻撃による被害を受けた場合に備え て、サイバー保険に加入することを推奨しています。 サイバー保険については、IPA調査でも顧客情報の漏え い事故を発生させてしまった EC サイトの多くが、被害後に加入していますが、損害賠償や事故対応費用の負担、収益の減 少を補う効果 が認められることから、被害が発生していない場合でも被害発生に備えて加"
  }
]

いずれも検索結果に回答に必要な情報が含まれており、かつ検索結果の情報を使って回答できていることが分かります。

しかし、今回取得したチャンクが、質問ごとにそれぞれほぼ同じ部分の検索結果が引っ掛かっているのが気になりました。
特にサイバー保険についての質問では7割方内容が一致しており、ChatGPTへの入力トークンの無駄遣い感は否めません。

範囲がかぶっているチャンクを削除するなど、トークン数を節約するための対策はしておいた方が良さそうです。

5. まとめ

Kendra と ChatGPT を使った RAG システムを Lambda で実装し、いくつか質問と回答のやり取りをしてみました。

実際のシステムとして運用する場合はここに、 APIトークン数計算、会話履歴、ロギングなど、様々な要素を追加する必要がありますが、 最小限の RAG の構成を 100 行以下で実装できる、というのは衝撃でした。

今回は質問文をそのまま Kendra に入力してセマンティック検索を行いましたが、固有表現を抽出してキーワード検索を行うと検索精度が上がるのか、 ChatGPT に入力しているプロンプトを改善して生成精度が上がるか、など検証してみるのも面白いかもしれません。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com

Amazon Kendra で独自文書に対するセマンティック検索(自然言語での検索)を実現する

こんにちは、機械学習チーム YAMALEX の駿です。
YAMALEX は Acroquest 社内で発足した、会社の未来の技術を創る、機械学習がメインテーマのデータサイエンスチームです。
(詳細はリンク先をご覧ください。)

ここ一か月、健康的な食事を心がけ、 1kg 減量しました。
リモートワークだから仕方ないと思っていたのが、間違いでした。

さて、今回は Amazon Kendra での検索について検証していきます。

1. Amazon Kendra とは

Amazon Kendra (以下、 Kendra )は Amazon が提供する、機械学習を利用したインテリジェント検索サービスです。
複数の場所にある複数のフォーマットのドキュメントに対応しており、自前でコードを書くことなく目的のコンテンツを見つけることができます。
AWS の S3 、 RDS などはもちろんのこと、 AWS 外の Google DriveMicrosoft Sharepoint にも接続できるため、既に蓄積されているデータをそのまま活用できます。

また、検索する際にはキーワード検索だけでなく、自然言語を使った「セマンティック検索」も可能で、自然なやり取りで検索をすることができます。

セマンティック検索とは、機械学習自然言語処理の技術を用いて単語の意味や文脈を理解して、より正確な検索結果を提供する技術です。
キーワード検索が単に文字列が文書に含まれているかどうかを検索するのに対し、セマンティック検索は単語やフレーズの意味、文脈、関連性なども考慮します。
例えば従来のキーワード検索だと「サイバー攻撃」のように検索していたところを、セマンティック検索では「サイバー攻撃を防ぐための予防策」のように人に聞くときと同じように自然言語で検索できます。

これにより、ユーザーは求めている情報に短時間でたどり着くことができます。

今回はいくつかある検索方法をそれぞれ試してみました。

2. 環境構築

下に今回利用する構成図を示します。

Kendra のインデックスに2つのデータソースを設定し、それぞれ S3 上のファイル、インターネット上のウェブサイトを参照するようにしました。

構成図

# データソース 内容
1 S3 IPA が公開している EC サイト構築・運用セキュリティガイドライン
2 WebCrawler Kendra の公式ドキュメント

インデックスとデータソースの作り方については AWS 公式のドキュメントを参照してください。

docs.aws.amazon.com

3. 検証

実施した検証内容は下記の通りです。

  1. 複数文書の検索
  2. 高度なクエリ構文による検索
  3. 文書の更新があった場合の確認

今回の検証は Kendra のインデックスとあわせて提供される検索コンソールを利用しました。
検索コンソール右側のスパナアイコンから、ソースの言語を指定しないと日本語での検索はできないため、注意が必要です。
(指定しない場合、関連度の低いドキュメントがヒットするのではなく、検索結果が0件になります)

検索コンソール

Japanese を選択しないと検索できない

3.1. 複数の文書の検索

話題沸騰ポットの仕様書と Kendra のドキュメントがインデックスに含まれている状態で、検索した結果が下記です。

セマンティック検索を試すため、キーワードではなく、自然言語で質問を入力してみました。
いずれも期待するドキュメントが最上位の結果として取得できていることが分かります。

ソースを指定せずに自然に質問するだけで必要な情報が取得できるのがいいですね。

それぞれの検索結果

3.2. 高度なクエリ構文による検索

次に高度なクエリ構文による検索のうち、 AND / OR を試してみます。
その他に対応している検索方法の詳細については公式ドキュメントを参照してください。

まず AND 検索です。
下の結果のように条件を追加すると、ドキュメントの中のさらにピンポイントの部分の検索結果を返すようになります。 しかし、当然絞りすぎると条件にヒットするドキュメントがなくなってしまいます。

AND 検索

次に OR 検索をやってみます。 下の結果のように、いずれかしか含まれないドキュメントが検索できていることが分かります。

OR 検索

特定のドキュメントを見つけたいときや、検索した結果をさらに絞り込みたいときに有用だと思いました。

3.3. 文書の更新があった場合の確認

最後に文書の更新があったときの挙動を確認します。

簡単のため、 データソースは S3 のもののみにして検証します。 またファイルとして、 IPA が公開している EC サイト構築・運用セキュリティガイドライン(以下、 EC サイトガイドライン)と Kendra の公式デベロッパーガイド( PDF )を使用します。

下記の手順で行いました。

# 手順 期待結果
1 S3 バケット(空の状態)に、EC サイトガイドラインを S3 にアップロード。 Sync を手動実行 新規ドキュメントが追加され、検索でヒットするようになること
2 S3 バケットに、 Kendra のデベロッパーガイドをさらにアップロード。 Sync を手動実行 新規ドキュメントが追加され、検索でヒットするようになること
3 S3 バケットに変更を加えず、 Sync を手動実行 データソースに変化がないこと
4 S3 バケットから Kendra のデベロッパーガイドを削除。 Sync を手動実行 ドキュメントが削除され、検索でヒットしなくなること
5 Kendra のデベロッパーガイドのファイル名をEC サイトガイドラインと同じにし、 S3 に上書きアップロード(ファイル名は変えずに内容が変わった状態)。 Sync を手動実行 ドキュメントが更新され、新しいバージョンの内容のみヒットするようになること

下の Sync 履歴を見ると、ファイルが変更されていないときは、更新を行わないこと、同じファイル名でも変更があれば更新が走ることが分かります。

Sync履歴

検索結果を以下に示します。

データソースに含まれるドキュメントが検索出来ているのは当然ですが、
想定通り、 データソースから削除されたドキュメントは結果に含まれないことが分かりました。

「○月○日よりも前の情報が知りたい」といった要件がある場合は、同じファイルを更新していくのではなく、 ファイル名でバージョン分けして、検索時に更新日時でフィルタする、などの処理が必要になりそうです。

1. の後

2. の後

4. の後

5. の後

データソースのSync スケジュールについて

今回は Sync を手動実行しましたが、頻繁に更新されるドキュメントの最新を検索したい場合は、定期的に Sync を自動実行するように設定が可能です。

一時間ごとなどの決められた頻度のほかに、 Custom を選択すると任意の cron 式を使ってスケジュールを定義できます。
ドキュメントの更新タイミングが決まっている際などに、できるだけ早く検索結果に反映できます。

Sync スケジュール設定

4. まとめ

Kendra で利用できる検索を検証しました。

従来は同等のセマンティック検索を実現しようとした場合、文章の分割、ベクトル化、固有表現抽出、類似度計算など、 複数の技術を組み合わせて実装する必要がありました。
しかし、 Kendra を使えば高精度の検索が実装不要で利用できるのが魅力的だと思いました。

Acroquest Technologyでは、キャリア採用を行っています。
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • Elasticsearch等を使ったデータ収集/分析/可視化
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長
  少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。 www.wantedly.com