Bedrockを活用したブログレビューAPIをローコードで実装してみた

Bedrockを活用したブログレビューAPIをローコードで実装してみた

Lambdaを使わなくても簡単にブログレビューのAPIが作れました
Clock Icon2024.09.30

リテールアプリ共創部@大阪の岩田です。

先日こんなブログを書きました。

https://dev.classmethod.jp/articles/devio-autoreview-by-bedrock/

最初は上記のシステムでメディアポリシー準拠をチェックしているだけだったのですが、ここにタイポチェックも組み込めないかという要望を頂き機能追加をすることになりました。さらにその後ブログ公開後にPULL型でレビューするのではなく、ブログ公開前にAIによる自動レビューを通せないか?という要望も頂きました。

ブログ公開前のレビューについては著者が内容をコピペしてレビュー依頼のプロンプトを書けば実現できる話ではあるのですが、ブラウザのタブを行き来したり、プロンプトを試行錯誤したりする手間を考えるとブログ執筆環境にレビューボタンのようなものが統合されていた方が便利です。ということでブログ執筆環境を整えるためのバックエンドAPIを実装してみました。

構成

今回作成するシステムの概要です。ざっくりこんな構成を作ります。

BedrockによるブログレビューAPIの構成図

  • API Gatewayのサービス統合でStep FunctionsのExpress Workflowを起動
  • Step FunctionsのWorkflowはParallelステートとサービス統合を利用して以下を実行
    • Bedrockにメディアポリシー準拠のレビューを依頼
    • Bedrockにタイポチェックを依頼

実装

ここからは実装を紹介していきます。各種リソースはSAMを使って定義しています。

まずSAMテンプレートです。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Parameters:
  Environment:
    Type: String
    Default: dev
    AllowedValues:
      - dev
      - stg      
      - prd
Resources:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      StageName: !Ref Environment
      DefinitionBody:
        Fn::Transform:
          Name: AWS::Include
          Parameters:
            Location: oas.yaml
      Auth:
        UsagePlan:
          CreateUsagePlan: PER_API
          Description: !Sub ${Environment} ブログ自動レビューAPI用使用料プラン
          Quota:
            Limit: 1000
            Period: DAY
          Throttle:
            RateLimit: 10
            BurstLimit: 20
          UsagePlanName: !Sub ${Environment}-blog-review-usage-plan
  ApiGwRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - apigateway.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: sfn-integration
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - states:StartSyncExecution
              Resource: '*'
  BlogReviewStateMachine:
    Type: AWS::Serverless::StateMachine
    Properties:
      Name: !Sub "${Environment}-blog-review"
      Type: EXPRESS
      DefinitionUri: blog-review.asl.yaml
      DefinitionSubstitutions:
        Environment: !Ref Environment
      Role: !GetAtt BlogReviewStateMachineRole.Arn
      Tracing:
        Enabled: true
      Logging:
        Level: ALL
        IncludeExecutionData: True
        Destinations:
          - CloudWatchLogsLogGroup:
              LogGroupArn: !GetAtt BlogReviewStateMachineLog.Arn
  BlogReviewStateMachineLog:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName : !Sub "/aws/states/${Environment}-blog-review-api"
      RetentionInDays: 30
  BlogReviewStateMachineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - states.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/CloudWatchLogsFullAccess
      Policies:
        - PolicyName: bedrock
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
            - Effect: Allow
              Action:
              - bedrock:InvokeModel
              Resource: '*'

あまり大したことはやっていなくて、API GWやIAMロールを作成しています。どちらかというと別ファイルに切り出しているステートマシンの定義とOpen APIの定義が重要です。

ステートマシンを定義しているblog-review.asl.yamlは以下の通りです。

blog-review.asl.yaml
---
Comment: (${Environment}) DevIOのブログをレビューするステートマシン
StartAt: Parallel
States:
  Parallel:
    Type: Parallel
    End: true
    Branches:
    - StartAt: ReviewBlogMediaPolicy
      States:
        ReviewBlogMediaPolicy:
          Type: Task
          Resource: arn:aws:states:::bedrock:invokeModel
          Parameters:
            ModelId: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-haiku-20240307-v1:0
            Body:
              anthropic_version: bedrock-2023-05-31
              max_tokens: 2000
              system: |
                あなたは企業ブログのレビュワーです

                メディアポリシーに従ってブログ内に不適切な表現がないかチェックする必要があります。
                ...messages:
                - role: user
                  content:
                    - type: text
                      text.$: $.article
              temperature: 0
          OutputPath: $.Body.content[0].text
          End: true          
    - StartAt: ReviewBlogTypo
      States:
        ReviewBlogTypo:
          Type: Task
          Resource: arn:aws:states:::bedrock:invokeModel
          Parameters:
            ModelId: arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-3-5-sonnet-20240620-v1:0
            Body:
              anthropic_version: bedrock-2023-05-31
              max_tokens: 2000
              system: |
                あなたは企業ブログのレビュワーです
                ...略
              messages:
                - role: user
                  content:
                    - type: text
                      text.$: $.article
              temperature: 0
          OutputPath: $.Body.content[0].text
          End: true
    ResultSelector:
      mediaPolicy.$: States.ArrayGetItem($,0)
      typo.$: States.ArrayGetItem($,1)

Bedrockのモデルやプロンプトは様子を見ながら適宜調整する想定です。

ステートマシンへの入力は以下のような入力を期待しています,。

{
  "article": "レビュー対象となる記事の内容"
}

Bedrockにレビューを経て最終的な出力はParallelステートのResultSelectorの指定により以下のような形式となります。

{
  "mediaPolicy":"このブログに特に問題はありませんでした",
  "typo":"タイポは特に見つかりませんでした"
}

作成されるステートマシンの定義は以下のようになります。

ブログレビュー用ステートマシンの定義

oas.yamlの中身は以下の通りです。

oas.yaml
openapi: 3.0.1
info:
  title:
    Fn::Sub: ${Environment} Blog Review API
  version: '1.0'
x-amazon-apigateway-request-validators:
  body-only:
    validateRequestBody: true
    validateRequestParameters: false
x-amazon-apigateway-request-validator: body-only
paths:
  /review:
    post:
      security:
        - api_key: []
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                article:
                  type: string
                  # 1文字だけレビューしても有効なレビューはもらえないので、本来はもっと桁数を大きくした方が良い
                  minLength: 1
                  # TODO maxLengthも設定する
                  nullable: false
              required:
                - article
      responses:
        '200':
          description: Review created
          content:
            application/json:
              schema:
                type: object
                properties:
                  typo:
                    type: string
                    example: タイポ警察だ!                    
                  mediaPolicy:
                    type: string
                    example: 特に問題ありません
      x-amazon-apigateway-integration:
        credentials: 
          Fn::GetAtt: ApiGwRole.Arn
        httpMethod: "POST"
        uri:
          Fn::Sub: "arn:aws:apigateway:${AWS::Region}:states:action/StartSyncExecution"
        responses:
          200:
            statusCode: "200"
            responseTemplates:
              application/json: |
                #set($inputRoot = $input.path('$'))
                #if($input.path('$.status').toString().equals("FAILED"))
                  #set($context.responseOverride.status = 500)
                  {
                    "error": "$input.path('$.error')",
                    "cause": "$input.path('$.cause')"
                  }
                  #else
                    $input.path('$.output')
                  #end
        requestTemplates:
          application/json: 
            Fn::Sub:
            - |
                #set($inputRoot = $input.path('$'))
                {
                  "input": "$util.escapeJavaScript($input.json('$')).replaceAll("\\'","'")",
                  "stateMachineArn": "${BlogReviewStateMachineArn}"
                }
            - BlogReviewStateMachineArn: 
                Fn::GetAtt: BlogReviewStateMachine.Arn
        passthroughBehavior: "when_no_templates"
        type: "aws"
components:
  securitySchemes:
    api_key:
      type: "apiKey"
      name: "x-api-key"
      in: "header"

ポイントをいくつか紹介します。

securityにはapi_keyを指定してAPIキーの利用を必須としています。SAMテンプレートで以下のように使用料プランを指定しており、この使用料プランに紐づくAPIキーを利用してもらう想定です。何度もAPIを呼び出されるとBedrockの利用料が高く付いてしまうので...。

Quota:
  Limit: 1000
  Period: DAY
Throttle:
  RateLimit: 10
  BurstLimit: 20

x-amazon-apigateway-request-validatorにはx-amazon-apigateway-request-validatorsで指定したバリデータを指定しており、リクエストボディをAPI GWにバリデーションさせます。これによってリクエストボディが空のJSONの場合や、articleが空文字列の場合はAPI GWから400エラーが返却されることになります。

x-amazon-apigateway-integration配下のrequestTemplatesでは統合リクエストのマッピングテンプレートを指定しています。このテンプレートは以下のようなVTLテンプレートです。

#set($inputRoot = $input.path('$'))
{
  "input": "$util.escapeJavaScript($input.json('$')).replaceAll("\\'","'")",
  "stateMachineArn": "arn:aws:states:us-east-1:123456789012:stateMachine:dev-blog-review"
}

ブログ記事には改行コード等が含まれるため$util.escapeJavaScriptを使って記事をエスケープします。さらにシングルクォーテーションをうまく扱えるようにreplaceAll("\\'","'")でシングルクォーテーションもエスケープします。

これは公式ドキュメントの記載に従って記述しています。

注記

この関数は、通常の一重引用符 (') をエスケープした一重引用符 (\') に変換します。ただし、エスケープした一重引用符は JSON で有効ではありません。したがって、この関数からの出力を JSON のプロパティで使用する場合、エスケープした一重引用符 (\') を通常の一重引用符 (') に戻す必要があります。

API Gateway マッピングテンプレートとアクセスのログ記録の変数リファレンス - Amazon API Gateway

参考までにCDKのStepFunctionsExecutionIntegrationも同様のVTLテンプレートを作成するように実装されています。

https://github.com/aws/aws-cdk/blob/bcf9209fb1b9e9aa295f50c5681201db094b8c00/packages/aws-cdk-lib/aws-apigateway/lib/integrations/stepfunctions.vtl#L74

動作確認

SAMテンプレートの準備ができたのでsam buildsam deployでリソースを一式デプロイし、実際にAPIを叩いて動作確認してみます。

curl  'https://<API GWのID>.execute-api.us-east-1.amazonaws.com/dev/review' \
--header 'x-api-key: APIキー' \
--header 'Content-Type: application/json' \
--data '{
    "article":"ブログの下書きをAIにレビューしてもらうAPIをローコードで実装してみた"
}'

以下のようなレスポンスが返却されてきました。

{
    "typo": "レビューの結果、以下の点を指摘いたします:\n\n1. タイポ:\n   特に見当たりません。\n\n2. 日本語として不自然な言い回し:\n   特に見当たりません。\n\n3. 助詞の使い方:\n   特に不自然な点は見当たりません。\n\n全体的に、タイトルは簡潔で分かりやすく、日本語として自然な表現が使われています。助詞の使用も適切です。",
    "mediaPolicy": "ブログの下書きを確認しました。全体的に適切な内容だと思いますが、以下の点について改善をご検討ください。...略\n\nその他、特に問題となる箇所は見当たりませんでした。メディアポリシーに沿った適切な内容だと評価します。\nご検討ください。"
}

期待通りのレスポンスが返却されました!

まとめ

ブログの下書きをAIにレビューしてもらうAPIをローコードで実装してみました。Lambda無しで簡単にAPIを作れることが分かると思います。このAPIを各ブログ著者の執筆環境と統合して呼び出せるようにVS CodeのExtension等を作りこめば快適な執筆環境が手に入りそうです。

今回APIの認証はAPIキーによる簡易な認証としていますが、公式ドキュメントにも記載されている通りAPIキーだけで認証・認可を行うのは推奨されません。

API キーを、API へのアクセスを制御するための認証または承認に使用しないでください

API Gateway での REST API の使用量プランと API キー - Amazon API Gateway

別途Cognito等を利用してAPI認証・認可を設定するのが良さそうです。ただし、Cognito等で認証・認可する場合もレートリミットをかけるためにAPIキーと使用量プランは利用した方が良いでしょう。

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.