😺

【Amazon Bedrock】Lambda関数からBedrockを呼び出してLLMアプリを作ってみた

に公開

はじめに

Amazon Bedrock を使ってアプリ開発をしたいと思い、まずはRAG等を使用しないシンプルなLLMアプリを作成してみます。

構成

構成図は下記のような形です。

S3

静的コンテンツ配置用です。Reactをビルドしたものを置いています。

API Gateway

リクエストをPOST送信しています。ターゲットはLambdaです。

Lambda

受け取った入力内容を用いてプロンプトを作成し、Bedrock へ送信します。

Bedrock

APIを介して基盤モデル(FM)を使用します。
今回は以下のモデルを使用しました。

  • 埋め込みモデル
    • Embed Multilingual
  • 基盤モデル
    • Claude 3.5 Sonnet

用語についてわからない場合はこちらを参照してください。
https://zenn.dev/t_oishi/articles/438e218c7aa0ca

準備

モデルアクセスの解除

下記のモデルアクセスの変更から使用するモデルのアクセス権を付与しましょう。

実装

Lambda関数

BedrockへのIAMロールを設定しましょう。
今回はAmazonBedrockFullAccessを設定しています。

import json
import boto3

# Bedrockクライアントの初期化
bedrock_runtime = boto3.client(service_name='bedrock-runtime', region_name='ap-northeast-1')

def lambda_handler(event, context):
    try:
        # API Gatewayからのリクエストボディを取得
        body = json.loads(event['body'])
        user_prompt = body['prompt']

        # Claude 3.5 Sonnetに渡すプロンプトを作成
        prompt_config = {
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 1024,
            "messages": [
                {
                    "role": "user",
                    "content": [{"type": "text", "text": user_prompt}]
                }
            ]
        }
        
        # Bedrockでモデルを呼び出す
        response = bedrock_runtime.invoke_model(
            body=json.dumps(prompt_config),
            modelId='anthropic.claude-3-5-sonnet-20240620-v1:0',
            accept='application/json',
            contentType='application/json'
        )
        
        # レスポンスボディをパース
        response_body = json.loads(response.get('body').read())
        ai_response = response_body['content'][0]['text']
        
        # --- ここが最重要ポイント ---
        # API Gatewayが期待する形式でレスポンスを返す
        return {
            'statusCode': 200,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*' # CORSヘッダーを必ず含める
            },
            # React側が data.body で受け取ることを想定
            'body': json.dumps(ai_response) 
        }

    except Exception as e:
        # エラー発生時もAPI Gatewayの形式で返す
        print(e) # CloudWatchでエラー内容を確認できるようにprintする
        return {
            'statusCode': 500,
            'headers': {
                'Content-Type': 'application/json',
                'Access-Control-Allow-Origin': '*'
            },
            'body': json.dumps({'error': str(e)})
        }

API Gateway

POSTで実装します。
CORSの設定を忘れずにしましょう。

フロント(React)

App.jsx
import { useState } from 'react';

function App() {
  const [prompt, setPrompt] = useState('');
  const [response, setResponse] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState('');

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!prompt) return;

    // --- [LOG 1] ---
    // フォーム送信が開始されたことをログに出力
    console.log('🚀 フォーム送信開始...');
    
    setIsLoading(true);
    setResponse('');
    setError('');

    try {
      const apiEndpoint = 'API Gatewayのエンドポイント';
      const requestBody = { prompt: prompt };

      // --- [LOG 2] ---
      // どのURLに、どんなデータを送るのかをログに出力
      console.log(`📡 APIリクエスト送信:
      - エンドポイント: ${apiEndpoint}
      - メソッド: POST
      - ボディ:`, requestBody);

      const res = await fetch(apiEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(requestBody),
      });

      // --- [LOG 3] ---
      // APIからの生のレスポンスオブジェクトをログに出力
      console.log('📬 APIレスポンス受信:', res);

      if (!res.ok) {
        // HTTPステータスがエラーの場合、エラーを発生させる
        throw new Error(`APIエラー: ${res.status} ${res.statusText}`);
      }

      const data = await res.json();
      
      // --- [LOG 4] ---
      // JSONパース後のデータをログに出力(これが最終的な応答データ)
      console.log('✅ 応答データ:', data);

      setResponse(data);

    } catch (err) {
      // --- [LOG 5] ---
      // エラーが発生した場合、その内容をコンソールにエラーとして出力
      console.error('❌ エラー発生:', err);
      setError(err.message || '不明なエラーが発生しました。');

    } finally {
      // --- [LOG 6] ---
      // 処理が完了したことをログに出力
      console.log('🏁 処理完了');
      setIsLoading(false);
    }
  };

  return (
    <div className="bg-gray-100 min-h-screen flex items-center justify-center p-4">
      <div className="bg-white p-8 rounded-xl shadow-lg w-full max-w-2xl">
        <h1 className="text-3xl font-bold text-center text-gray-800 mb-2">
          Chat with Claude 3.5 Sonnet
        </h1>

        <form onSubmit={handleSubmit}>
          <textarea
            value={prompt}
            onChange={(e) => setPrompt(e.target.value)}
            placeholder="ここにメッセージを入力..."
            className="w-full h-32 p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none transition"
            disabled={isLoading}
          />
          <button
            type="submit"
            disabled={isLoading || !prompt}
            className="w-full mt-4 bg-blue-600 text-white font-bold py-3 px-4 rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors duration-300"
          >
            {isLoading ? '送信中...' : 'POSTリクエストを送信'}
          </button>
        </form>

        {(response || error || isLoading) && (
          <div className="mt-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
            <h2 className="text-lg font-semibold text-gray-700 mb-2">応答結果</h2>
            {isLoading && <p className="text-gray-600 animate-pulse">AIが応答を生成しています...</p>}
            {error && <p className="text-red-500 whitespace-pre-wrap">{error}</p>}
            {response && <p className="text-gray-800 whitespace-pre-wrap">{response}</p>}
          </div>
        )}
      </div>
    </div>
  );
}

export default App;

完成図

おわりに

今回はRAGを使わないので、シンプルでしたが次回はナレッジベースを使用してRAGから回答を得る方法やエージェントを使用したアクションの設定等も作成していきたいと思います。

Discussion