😺
【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
用語についてわからない場合はこちらを参照してください。
準備
モデルアクセスの解除
下記のモデルアクセスの変更
から使用するモデルのアクセス権を付与しましょう。
実装
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