📚

【Amazon Bedrock・SAM】Zennの.mdファイル記事をAIでレビューするVScord拡張機能をつくってみた

に公開

はじめに

日ごろZennの記事を書いた後、投稿前に見直しをして誤字や脱字をしないよう努めていますが、どうしても見逃してしまうことがあります。
ChatGPTなどに張り付けてレビューしてもらえれば済む話ではありますが、プロンプトを書いて貼り付けての作業など毎回行う作業は自動化したいものです。
そこで下記の記事を見つけ、参考にして実際に作成してみることにしました。
最終的にVScord拡張機能で動作するようにしています。

https://dev.classmethod.jp/articles/create-blog-review-api-with-bedrock/

実装

各種リソースはSAMを使って定義しています。
AWS CLIは設定しておいてください。

template.yamlについて

AWS SAMのメインテンプレートファイルで、アプリケーションのインフラストラクチャ全体を定義しています。

1. API Gateway関連

  • Api: REST APIの定義

    • OpenAPI仕様書(oas.yaml)を参照
    • 環境に応じたステージ名を設定
  • BlogReviewUsagePlan: API使用量プラン

    • 1日あたり1000リクエストの制限
    • レート制限: 10リクエスト/秒
    • バースト制限: 20リクエスト
  • BlogReviewApiKey: APIキー

    • APIへのアクセス認証用
  • BlogReviewUsagePlanKey: APIキーと使用量プランの関連付け

2. IAMロール

  • ApiGwRole: API Gateway用ロール

    • Step Functionsの同期実行権限を付与
  • BlogReviewStateMachineRole: Step Functions用ロール

    • CloudWatch Logsへのフルアクセス権限
    • Bedrock(AI)モデルの呼び出し権限

3. Step Functions

  • BlogReviewStateMachine: ブログレビュー用ステートマシン
    • タイプ: EXPRESS(同期実行)
    • 定義ファイル: blog-review.asl.yaml
    • トレーシングとロギングを有効化

4. ロググループ

  • BlogReviewStateMachineLog: Step Functions実行ログ
    • 保持期間: 30日

Outputs(出力)

  • ApiUrl: デプロイされたAPIのエンドポイントURL
  • ApiKeyId: 作成されたAPIキーのID
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

  # Usage Plan を別リソースとして作成
  BlogReviewUsagePlan:
    Type: AWS::ApiGateway::UsagePlan
    DependsOn: Api
    Properties:
      UsagePlanName: !Sub ${Environment}-blog-review-usage-plan
      Description: !Sub ${Environment} ブログ自動レビューAPI用使用料プラン
      Quota:
        Limit: 1000
        Period: DAY
      Throttle:
        RateLimit: 10
        BurstLimit: 20
      ApiStages:
        - ApiId: !Ref Api
          Stage: !Ref Environment

  # API Key
  BlogReviewApiKey:
    Type: AWS::ApiGateway::ApiKey
    Properties:
      Name: !Sub ${Environment}-blog-review-api-key
      Enabled: true

  # Usage Plan Key の関連付け
  BlogReviewUsagePlanKey:
    Type: AWS::ApiGateway::UsagePlanKey
    Properties:
      KeyId: !Ref BlogReviewApiKey
      KeyType: API_KEY
      UsagePlanId: !Ref BlogReviewUsagePlan

  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: '*'

Outputs:
  ApiUrl:
    Description: API Gateway URL
    Value: !Sub 'https://${Api}.execute-api.${AWS::Region}.amazonaws.com/${Environment}'
  ApiKeyId:
    Description: API Key ID
    Value: !Ref BlogReviewApiKey

oas.yamlについて

API Gatewayのエンドポイント定義とリクエスト/レスポンスの仕様を記述しています。

API仕様

セキュリティ

  • APIキー認証(x-api-key ヘッダー)が必須

リクエスト仕様

{
  "article": "レビューする記事の本文(文字列)"
}
  • articleフィールドは必須
  • 最小文字数: 1文字

レスポンス仕様

{
  "typo": "誤字脱字チェック結果",
  "mediaPolicy": "メディアポリシーチェック結果"
}

API Gateway統合設定

Step Functionsとの統合

  • 統合タイプ: AWS サービス統合
  • アクション: StartSyncExecution(同期実行)
  • IAMロール: ApiGwRoleを使用

リクエスト変換

  • 受信したJSONをStep Functionsの入力形式に変換
  • ステートマシンARNを動的に取得

レスポンス変換

  • Step Functions実行結果をAPIレスポンスに変換
  • エラー時は500ステータスコードを返却
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"

blog-review.asl.yamlについて

Step Functionsのステートマシン定義で、ブログレビューの実行フローを記述しています。
Bedrockの呼び出しモデルはStep Functionsがデプロイされているリージョン固有になります。
(例:東京リージョン:Step Functions → オレゴンリージョン:Claude 3.7 Sonnet は不可)

処理フロー

Parallel State(並列処理)

2つのレビュータスクを同時に実行:

1. ReviewBlogMediaPolicy(メディアポリシー審査)

チェック項目
一般的なメディアポリシーを調べて入れています。

使用AIモデル: Claude 3 Sonnet (Bedrock)
出力形式:

  • 問題あり: ❌と具体的な問題点
  • 問題なし: ✅メッセージ

2. ReviewBlogTypo(誤字脱字チェック)

チェック項目

  • 誤字・脱字の検出
  • 文法上の問題の指摘

使用AIモデル: Claude 3 Sonnet (Bedrock)
出力形式:

  • 問題あり: ❌と修正提案
  • 問題なし: ✅メッセージ

結果の集約

ResultSelectorで両方のレビュー結果を統合:

  • mediaPolicy: メディアポリシーチェック結果
  • typo: 誤字脱字チェック結果
blog-review.asl.yaml
Comment: (${Environment}) Zennのブログをレビューするステートマシン
StartAt: Parallel
States:
  Parallel:
    Type: Parallel
    End: true
    Branches:
    - StartAt: ReviewBlogMediaPolicy
      States:
        ReviewBlogMediaPolicy:
          Type: Task
          Resource: arn:aws:states:::bedrock:invokeModel
          Parameters:
            ModelId: apac.anthropic.claude-3-7-sonnet-20250219-v1:0
            Body:
              anthropic_version: bedrock-2023-05-31
              max_tokens: 2000
              system: |
                あなたは企業ブログのメディアポリシー審査担当者です。

                以下の6つの観点から記事をチェックし、問題があれば指摘してください:

                ## チェック項目
                1. **機密情報・個人情報の開示**: 氏名、メールアドレス、電話番号、社内システム情報、API キーなどの機密情報が含まれていないか
                2. **著作権侵害コンテンツの投稿**: 他社の著作物、画像、コードの無断使用がないか
                3. **虚偽の情報の拡散**: 事実と異なる情報、根拠のない主張、誤解を招く表現がないか
                4. **差別的・攻撃的な発言**: 特定の個人・団体・属性に対する差別的・攻撃的な表現がないか
                5. **競合他社への不適切な言及**: 競合他社への誹謗中傷、不当な批判、営業妨害にあたる表現がないか
                6. **未公開の製品・サービス情報の漏洩**: 社内限定情報、未発表の製品情報、機密のロードマップなどが含まれていないか

                ## 出力形式
                問題が見つかった場合:
                「❌ 問題を発見しました:[具体的な問題点と該当箇所を明記]」

                問題がない場合:
                「✅ メディアポリシーの観点から問題ありません」
              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: apac.anthropic.claude-3-7-sonnet-20250219-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)

デプロイ・テスト

ここまで作成したら、sam deployでデプロイしましょう。
sam deploy --guidedは割愛)

デプロイされたAPIエンドポイントでテストしてみましょう。
使用するモデルのアクセスを有効化しておくことを忘れずに。

curl  'https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/review' \
--header 'x-api-key: xxxxxxxxx' \
--header 'Content-Type: application/json' \
--data '{
    "article":"今日は朝から快晴で、街中には活気があふれていた。カフェの前を通ると、コーヒーの香りが漂い、思わず立ち寄りたくなる。通勤途中の人々はそれぞれのペースで歩き、スマートフォンを見たり、友人と会話を楽しんだりしている。公園のベンチには読書をする人や散歩を楽しむ犬連れの人々がいて、日常の穏やかさが感じられた。ふと空を見上げると、雲ひとつない青空が広がり、遠くの山並みくっきりと見える。こんな日には、仕事や勉強の合間に少し外の空気を吸うだけでも、気持ちがリフレッシュされることに気づく。"
}'

うまくいけば以下のように回答が生成されます。

{"typo":"✅ 誤字・脱字・文法上の問題はありません","mediaPolicy":"✅ メディアポリシーの観点から問題ありません"}

試しにあえて誤字をしてみました。
うまく回答されてますね。

 {"typo":"❌ 以下の修正が必要です:\n• [カフの前] → [カフェの前] (「カフェ」の表記が不完全です)\n• [山並みくっきりと見える] → [山並みがくっきりと見える] (助詞「が」が抜けています)","mediaPolicy":"✅ メディアポリシーの観点から問題ありません"}

これでAPIは完了したので、次にVScordで拡張機能を作成します。

VScord拡張機能の設定

VScord拡張機能をカスタマイズして作成すれば以下のように、VScordでAPIが簡単に実行できます。

  1. VSIXファイルから直接インストール(推奨)
    cd zenn-review-extension
    code --install-extension zenn-blog-review-1.0.0.vsix

または VS Codeで:

  • コマンドパレット(Ctrl+Shift+P)
  • 「Extensions: Install from VSIX...」
  • zenn-blog-review-1.0.0.vsixを選択
  1. 開発モードで実行
    cd zenn-review-extension
    npm install
    npm run compile

このような形でレビュー画面を表示することができました!

おわりに

ブログレビューだけでなく、社内文書やレポートの自動チェックなど、さまざまな文章レビューの自動化にも応用していけたらいいなと感じています。
AIを社内外のDXで活用していきたいですね。

GitHubで編集を提案

Discussion