<p>VBA/PowerShellからAzure OpenAI APIを極める:HTTP/JSON層からの徹底解剖</p>
<h2 class="wp-block-heading">導入(問題設定)</h2>
<p>ExcelマクロやPowerShellスクリプトが根付く現場において、業務効率化は永遠の課題です。定型的なテキスト処理、データ分類、要約、あるいは自由形式の質問応答など、これまで人の手で行われてきた作業に、いまや生成AIの力が期待されています。しかし、既存の業務システムや社内規定の制約により、最新のPythonライブラリや高度なSDKの導入が難しいケースは少なくありません。</p>
<p>本記事では、こうした環境下でもAzure OpenAI Serviceの強力なテキスト生成能力を享受できるよう、VBAおよびPowerShellから直接REST APIを呼び出す方法を、<strong>HTTP/JSONの低レベルな視点から徹底的に解説</strong>します。単なるHowToに留まらず、内部動作、境界条件、そして開発者が陥りがちな落とし穴まで深掘りすることで、堅牢で実践的なソリューション構築のための知見を提供します。</p>
<h2 class="wp-block-heading">理論の要点</h2>
<p>Azure OpenAI ServiceのREST APIは、HTTPプロトコル上でJSON形式のデータをやり取りすることで、AIモデルとの対話を可能にします。その核心を理解するために、以下の要素を押さえます。</p>
<h3 class="wp-block-heading">1. エンドポイントと認証</h3>
<p>Azure OpenAI ServiceのエンドポイントURLは、Azureリソース名、デプロイ名、およびAPIバージョンによって構成されます。
<code>https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version={api-version}</code></p>
<ul class="wp-block-list">
<li><code>{your-resource-name}</code>: Azure OpenAI Serviceリソースのホスト名。</li>
<li><code>{your-deployment-name}</code>: デプロイしたモデル(例: <code>gpt-35-turbo</code>)の名前。</li>
<li><code>{api-version}</code>: 使用するAPIのバージョン(例: <code>2023-05-15</code>)。</li>
</ul>
<p>認証には、APIキー認証(推奨)を使用します。HTTPリクエストヘッダに <code>api-key: YOUR_API_KEY</code> を付与します。<code>Authorization: Bearer YOUR_API_KEY</code> も利用可能ですが、Azure OpenAIでは <code>api-key</code> ヘッダが慣例です。</p>
<h3 class="wp-block-heading">2. リクエストとレスポンスの構造</h3>
<p>モデルとの対話は、JSON形式で構成されたリクエストボディをPOSTすることで行われます。</p>
<h4 class="wp-block-heading">リクエストボディの主要プロパティ</h4>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">プロパティ名</th>
<th style="text-align:left;">型</th>
<th style="text-align:left;">必須/任意</th>
<th style="text-align:left;">説明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;"><code>messages</code></td>
<td style="text-align:left;">Array</td>
<td style="text-align:left;">必須</td>
<td style="text-align:left;">モデルへのプロンプト履歴。各要素は <code>role</code> (string) と <code>content</code> (string) を持つオブジェクト。<code>role</code> は <code>system</code> (全体の振る舞い), <code>user</code> (ユーザーの質問), <code>assistant</code> (モデルの過去の回答) があります。</td>
</tr>
<tr>
<td style="text-align:left;"><code>model</code></td>
<td style="text-align:left;">String</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">使用するモデルのID。Azure OpenAIでは<code>{your-deployment-name}</code>で指定されるため、通常は空か、デプロイ名と一致させる。OpenAI APIの<code>model</code>とは挙動が異なる場合があるため、Azure OpenAIではエンドポイントURLのデプロイ名に依存するのが安全です。ただし、一部のAPIバージョンではボディ内<code>model</code>が必須の場合があるため、<strong>公式ドキュメントのAPIバージョンごとの仕様を確認すること。</strong></td>
</tr>
<tr>
<td style="text-align:left;"><code>temperature</code></td>
<td style="text-align:left;">Number</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">出力のランダム性(0.0~2.0)。0.0に近いほど決定論的、2.0に近いほど創造的になります。デフォルトは1.0。</td>
</tr>
<tr>
<td style="text-align:left;"><code>max_tokens</code></td>
<td style="text-align:left;">Integer</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">生成される応答の最大トークン数。入力プロンプトと出力の両方でモデルのコンテキストウィンドウをオーバーしないよう注意が必要です。</td>
</tr>
<tr>
<td style="text-align:left;"><code>top_p</code></td>
<td style="text-align:left;">Number</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">サンプリング戦略。<code>temperature</code> とは排他的に使用されます。累積確率 <code>top_p</code> まで含まれるトークン候補からサンプリングします。</td>
</tr>
<tr>
<td style="text-align:left;"><code>frequency_penalty</code></td>
<td style="text-align:left;">Number</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">生成されるトークンが、すでにプロンプトや出力に存在するトークンである場合にペナルティを課す度合い(-2.0~2.0)。値が大きいほど繰り返しを減らします。</td>
</tr>
<tr>
<td style="text-align:left;"><code>presence_penalty</code></td>
<td style="text-align:left;">Number</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">生成されるトークンが、プロンプトや出力に存在するトークンに関わらず、新しいトピックを導入する度合い(-2.0~2.0)。値が大きいほど新しいトピックを導入しやすくなります。</td>
</tr>
<tr>
<td style="text-align:left;"><code>stream</code></td>
<td style="text-align:left;">Boolean</td>
<td style="text-align:left;">任意</td>
<td style="text-align:left;">レスポンスをストリーミング形式で受け取るかどうか。<code>true</code>の場合、応答が少しずつ返されます。リアルタイムアプリケーション向け。今回は非ストリーミングで解説。</td>
</tr>
</tbody>
</table></figure>
<h4 class="wp-block-heading">レスポンスボディの主要プロパティ</h4>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">プロパティ名</th>
<th style="text-align:left;">型</th>
<th style="text-align:left;">説明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;"><code>id</code></td>
<td style="text-align:left;">String</td>
<td style="text-align:left;">チャット補完セッションのユニークID。</td>
</tr>
<tr>
<td style="text-align:left;"><code>object</code></td>
<td style="text-align:left;">String</td>
<td style="text-align:left;">オブジェクトのタイプ(例: <code>chat.completion</code>)。</td>
</tr>
<tr>
<td style="text-align:left;"><code>created</code></td>
<td style="text-align:left;">Integer</td>
<td style="text-align:left;">タイムスタンプ(Unix時間)。</td>
</tr>
<tr>
<td style="text-align:left;"><code>model</code></td>
<td style="text-align:left;">String</td>
<td style="text-align:left;">使用されたモデルのID。</td>
</tr>
<tr>
<td style="text-align:left;"><code>choices</code></td>
<td style="text-align:left;">Array</td>
<td style="text-align:left;">生成された応答の配列。各要素は <code>message</code> (オブジェクト), <code>finish_reason</code> (String), <code>index</code> (Integer) を持つ。</td>
</tr>
<tr>
<td style="text-align:left;"><code>message</code></td>
<td style="text-align:left;">Object</td>
<td style="text-align:left;"><code>role</code> (String) と <code>content</code> (String) を持つ。モデルの回答が <code>content</code> に含まれます。</td>
</tr>
<tr>
<td style="text-align:left;"><code>usage</code></td>
<td style="text-align:left;">Object</td>
<td style="text-align:left;">トークンの使用状況。<code>prompt_tokens</code>, <code>completion_tokens</code>, <code>total_tokens</code> を含む。</td>
</tr>
</tbody>
</table></figure>
<h3 class="wp-block-heading">3. HTTPクライアントの選択</h3>
<ul class="wp-block-list">
<li><strong>VBA:</strong> <code>WinHttp.WinHttpRequest.5.1</code> (推奨) または <code>MSXML2.XMLHTTP60</code> を使用します。これらはCOMオブジェクトであり、特別な参照設定なしで利用できる場合が多いですが、OSやOfficeのバージョンによっては参照設定が必要な場合があります。</li>
<li><strong>PowerShell:</strong> <code>Invoke-RestMethod</code> コマンドレットを使用します。これはHTTPリクエストの送信とJSONレスポンスのオブジェクト化を自動で行ってくれる、非常に強力なコマンドレットです。</li>
</ul>
<h3 class="wp-block-heading">4. JSONのパース</h3>
<ul class="wp-block-list">
<li><strong>VBA:</strong> <code>WinHttpRequest</code> が返すすべてのレスポンスは生の文字列です。JSON文字列をVBAのオブジェクトとして扱うには、自前でパースロジックを実装するか、外部ライブラリ(JSONConverter.basなど)を利用する必要があります。本記事では外部ライブラリなしで、基礎的なパース方法を解説します。</li>
<li><strong>PowerShell:</strong> <code>Invoke-RestMethod</code> は、<code>Content-Type: application/json</code> ヘッダを持つレスポンスを自動的にPowerShellのオブジェクト(<code>PSCustomObject</code>)に変換してくれます。このため、JSONパースの労力はVBAに比べて格段に少ないです。</li>
</ul>
<h3 class="wp-block-heading">Azure OpenAI API 呼び出しフロー</h3>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["開始"] --> B{"環境変数/設定からAPIキー, エンドポイントURL取得"};
B --> C{"プロンプト構築 (messages配列)"};
C --> D{"JSONリクエストボディ生成"};
D --> E{"HTTPクライアント初期化 (VBA:WinHttpRequest / PS:Invoke-RestMethod)"};
E --> F{"HTTPヘッダ設定 (Content-Type, api-key)"};
F --> G{"POSTリクエスト送信"};
G --> H{"HTTPレスポンス受信"};
H --> I{"ステータスコード確認"};
I -- 200 OK --> J{"JSONレスポンスパース"};
I -- 4xx/5xxエラー --> K{"エラーハンドリング & リトライ"};
J --> L{"生成されたテキスト抽出"};
L --> M["結果表示/利用"];
K --> M;
M --> N["終了"];
</pre></div>
<h2 class="wp-block-heading">実装(最小→堅牢化)</h2>
<p>ここでは、VBAとPowerShellの両方で実装例を示します。環境変数は設定済みと仮定します。</p>
<h3 class="wp-block-heading">前提条件</h3>
<ul class="wp-block-list">
<li>Azure OpenAI Serviceリソースがデプロイ済みであること。</li>
<li><code>gpt-3.5-turbo</code> または <code>gpt-4</code> モデルが <code>your-deployment-name</code> としてデプロイされていること。</li>
<li>APIキーとエンドポイントURLが取得済みであること。</li>
</ul>
<h3 class="wp-block-heading">VBA (Microsoft Excel VBA)</h3>
<h4 class="wp-block-heading">最小実装</h4>
<p>VBAでHTTPリクエストを送信し、JSONレスポンスから必要な情報を抽出する最小限のコードです。エラーハンドリングは含まれていません。</p>
<pre data-enlighter-language="generic">Attribute VB_Name = "Module1"
Option Explicit
Private Const AZURE_OPENAI_ENDPOINT As String = "https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version=2023-05-15"
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_API_KEY" ' ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
Public Sub CallAzureOpenAIChatMinimal()
Dim req As Object ' WinHttpRequest.5.1
Dim sURL As String
Dim sAPIKey As String
Dim sPrompt As String
Dim sRequestBody As String
Dim sResponse As String
Dim sGeneratedText As String
' --- 設定値 ---
sURL = Replace(AZURE_OPENAI_ENDPOINT, "{your-resource-name}", "YOUR_RESOURCE_NAME") ' 例: my-aoai-resource
sURL = Replace(sURL, "{your-deployment-name}", "YOUR_DEPLOYMENT_NAME") ' 例: gpt-35-turbo-deploy
sAPIKey = AZURE_OPENAI_API_KEY
sPrompt = "VBAでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。"
' --- リクエストボディの構築 ---
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": ""You are a helpful assistant.""}," & _
"{""role"": ""user"", ""content"": """ & EscapeJsonString(sPrompt) & """}]," & _
"""temperature"": 0.7," & _
"""max_tokens"": 50}"
Set req = CreateObject("WinHttp.WinHttpRequest.5.1")
With req
.Open "POST", sURL, False ' 同期処理
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", sAPIKey
.Send sRequestBody
sResponse = .ResponseText
End With
' --- レスポンスのパース (簡易版) ---
' "content": "..." の値を取得
Dim iStart As Long, iEnd As Long
iStart = InStr(sResponse, """content"": """)
If iStart > 0 Then
iStart = iStart + Len("""content"": """)
iEnd = InStr(iStart, sResponse, """")
If iEnd > 0 Then
sGeneratedText = Mid(sResponse, iStart, iEnd - iStart)
Debug.Print "生成されたテキスト: " & sGeneratedText
Else
Debug.Print "エラー: contentの終わりが見つかりません。"
Debug.Print "レスポンス: " & sResponse
End If
Else
Debug.Print "エラー: contentが見つかりません。"
Debug.Print "レスポンス: " & sResponse
End If
Set req = Nothing
End Sub
' JSON文字列のエスケープ処理
' ダブルクォーテーションを \" に変換
Private Function EscapeJsonString(ByVal inputString As String) As String
EscapeJsonString = Replace(inputString, """", "\""")
' 他にもエスケープすべき文字(改行、タブなど)があれば追加
End Function
</pre>
<p><strong>VBAの落とし穴と境界条件:</strong>
* <strong>JSONパースの困難さ:</strong> 上記の<code>InStr</code>ベースのパースは、単純な<code>content</code>抽出には使えますが、ネストが深いJSONや、エスケープ文字を含むJSONには対応できません。正規表現 (<code>VBScript.RegExp</code>) を使うか、専用のJSONパーサークラスを自作する必要があります。
* <strong>同期処理のブロック:</strong> <code>.Open "POST", sURL, False</code> は同期処理です。API応答が遅い場合、Excelがフリーズします。非同期 (<code>True</code>) で処理し、<code>OnReadyStateChange</code>イベントをハンドリングする堅牢化が必要です。
* <strong>64bit環境における<code>PtrSafe</code> / <code>LongPtr</code>:</strong> <code>WinHttp.WinHttpRequest</code> はCOMオブジェクトであり、VBAからCOMを呼び出す際に<code>Declare</code>ステートメントは通常使用しません。そのため、<code>PtrSafe</code>や<code>LongPtr</code>は直接関係しません。しかし、もしWinAPIを直接呼び出してHTTP通信を行う場合は、64bit環境でポインタのサイズが変わるため、これらのキーワードが必須となります。COMオブジェクトを介する場合は、VBAがCOMインターフェースを介してDLLを呼び出すため、VBA開発者が直接<code>PtrSafe</code>等を意識する必要はありません。</p>
<h4 class="wp-block-heading">堅牢化</h4>
<p>上記の最小実装に、エラーハンドリング、タイムアウト設定、そして簡素ながらももう少し実用的なJSONパースを追加します。非同期処理はイベントドリブンな構造になるため、ここでは省略し、同期処理の堅牢化に焦点を当てます。</p>
<pre data-enlighter-language="generic">Attribute VB_Name = "Module1"
Option Explicit
Private Const AZURE_OPENAI_ENDPOINT As String = "https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version=2023-05-15"
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_API_KEY" ' ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
' WinHttpRequestの定数 (WinHttpRequest.dllを直接参照しない場合)
Private Const WHR_SETREQUESTHEADER As Long = 0
Private Const WHR_OPTION_ENABLE_REDIRECTS As Long = 6
Private Const WHR_OPTION_TIMEOUT As Long = 0 ' タイムアウト設定用オプション
Public Sub CallAzureOpenAIChatRobust()
Dim req As Object ' WinHttpRequest.5.1
Dim sURL As String
Dim sAPIKey As String
Dim sPrompt As String
Dim sRequestBody As String
Dim sResponse As String
Dim sGeneratedText As String
Dim lAttempts As Long
Const MAX_RETRIES As Long = 3
Const RETRY_DELAY_SEC As Long = 2 ' 初期リトライ遅延秒数
' --- 設定値 ---
sURL = Replace(AZURE_OPENAI_ENDPOINT, "{your-resource-name}", "YOUR_RESOURCE_NAME")
sURL = Replace(sURL, "{your-deployment-name}", "YOUR_DEPLOYMENT_NAME")
sAPIKey = AZURE_OPENAI_API_KEY
sPrompt = "VBAでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。エラーハンドリングとタイムアウトについても触れてください。"
' --- リクエストボディの構築 ---
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": ""You are a helpful assistant.""}," & _
"{""role"": ""user"", ""content"": """ & EscapeJsonString(sPrompt) & """}]," & _
"""temperature"": 0.7," & _
"""max_tokens"": 100}" ' トークン数を増やして少し長めの応答を期待
Set req = CreateObject("WinHttp.WinHttpRequest.5.1")
For lAttempts = 1 To MAX_RETRIES
On Error GoTo ErrorHandler ' エラーハンドラ設定
With req
.Option(WHR_OPTION_TIMEOUT) = 30000 ' 30秒のタイムアウト (ミリ秒指定)
.Open "POST", sURL, False ' 同期処理
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", sAPIKey
.Send sRequestBody
sResponse = .ResponseText
If .Status >= 200 And .Status < 300 Then
' 成功: レスポンスのパース
sGeneratedText = ParseJsonContent(sResponse)
If Len(sGeneratedText) > 0 Then
Debug.Print "生成されたテキスト (試行 " & lAttempts & "): " & sGeneratedText
Exit For ' 成功したらループを抜ける
Else
Debug.Print "警告: レスポンスからテキストを抽出できませんでした。ステータス: " & .Status & ", レスポンス: " & sResponse
GoTo NextAttempt ' パース失敗は次の試行へ
End If
Else
Debug.Print "HTTPエラー発生 (試行 " & lAttempts & "): " & .Status & " " & .StatusText
Debug.Print "レスポンス: " & sResponse
GoTo NextAttempt ' エラー発生は次の試行へ
End If
End With
NextAttempt:
If lAttempts < MAX_RETRIES Then
Debug.Print "リトライします。待機時間: " & (RETRY_DELAY_SEC * (2 ^ (lAttempts - 1))) & "秒..."
Application.Wait Now + TimeSerial(0, 0, RETRY_DELAY_SEC * (2 ^ (lAttempts - 1))) ' 指数バックオフ
Else
Debug.Print "最大リトライ回数を超えました。"
sGeneratedText = "エラー: AI応答を取得できませんでした。"
End If
Next lAttempts
CleanUp:
Set req = Nothing
Exit Sub
ErrorHandler:
Debug.Print "実行時エラー: " & Err.Number & " - " & Err.Description
' ネットワークエラーやタイムアウトの場合など
If Err.Number = -2147012894 Then ' WinHTTP Error 12002: ERROR_WINHTTP_TIMEOUT
Debug.Print "タイムアウトが発生しました。"
End If
Resume NextAttempt ' エラーが発生しても次の試行へ
End Sub
' JSON文字列のエスケープ処理
Private Function EscapeJsonString(ByVal inputString As String) As String
' JSON文字列に含まれる可能性のある特殊文字をエスケープ
' 必要に応じて、\n, \r, \t, \f, \b なども追加
EscapeJsonString = Replace(inputString, "\", "\\")
EscapeJsonString = Replace(EscapeJsonString, """", "\""")
End Function
' 簡易JSONパース関数 (正規表現を使用)
' JSONレスポンスから "content" フィールドの値を抽出
Private Function ParseJsonContent(ByVal jsonString As String) As String
Dim regEx As Object
Dim matches As Object
Set regEx = CreateObject("VBScript.RegExp")
With regEx
.Pattern = """content"":\s*""((?:[^""]|(?<=\\)"")*)""" ' エスケープされたダブルクォーテーションも考慮
.Global = False
.IgnoreCase = False
End With
Set matches = regEx.Execute(jsonString)
If matches.Count > 0 Then
' 最初のマッチの、1つ目のキャプチャグループがcontentの値
ParseJsonContent = Replace(matches(0).SubMatches(0), "\""", """") ' エスケープされたダブルクォーテーションを元に戻す
Else
ParseJsonContent = ""
End If
Set regEx = Nothing
End Function
</pre>
<p><strong>堅牢化のポイント:</strong>
* <strong>エラーハンドリング:</strong> <code>On Error GoTo</code> とエラーコードによる分岐。特にWinHTTPのエラーコードは把握しておくべきです。
* <strong>リトライロジック:</strong> ネットワークの一時的な問題やAPIのレート制限に対応するため、指数バックオフを伴うリトライ処理を実装しました。
* <strong>タイムアウト設定:</strong> <code>req.Option(WHR_OPTION_TIMEOUT) = 30000</code> でタイムアウトを明示的に設定。
* <strong>JSONパースの強化:</strong> <code>VBScript.RegExp</code> オブジェクトを使用して、<code>"content": "..."</code> の値をより確実に抽出します。エスケープされたダブルクォーテーションも考慮に入れています。これにより、より柔軟なJSON構造に対応できますが、汎用的なJSONパーサーではありません。本格的なパースには外部ライブラリ (<code>JsonConverter.bas</code>など) の導入が最善策です。</p>
<h3 class="wp-block-heading">PowerShell</h3>
<h4 class="wp-block-heading">最小実装</h4>
<p><code>Invoke-RestMethod</code>は非常に強力で、HTTPリクエストの送信、JSONの自動パース、エラーハンドリングの基本的な部分を一度に担ってくれます。</p>
<pre data-enlighter-language="generic"># Azure OpenAI設定
$resourceName = "YOUR_RESOURCE_NAME" # 例: my-aoai-resource
$deploymentName = "YOUR_DEPLOYMENT_NAME" # 例: gpt-35-turbo-deploy
$apiVersion = "2023-05-15"
$apiKey = "YOUR_API_KEY" # ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
# エンドポイントURL構築
$endpoint = "https://$($resourceName).openai.azure.com/openai/deployments/$($deploymentName)/chat/completions?api-version=$($apiVersion)"
# プロンプト
$prompt = "PowerShellでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。"
# リクエストボディ
$body = @{
messages = @(
@{ role = "system"; content = "You are a helpful assistant." },
@{ role = "user"; content = $prompt }
)
temperature = 0.7
max_tokens = 50
} | ConvertTo-Json -Compress # JSON文字列に変換
# HTTPヘッダ
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey
}
# API呼び出し
try {
$response = Invoke-RestMethod -Uri $endpoint -Method Post -Headers $headers -Body $body
# レスポンスからテキスト抽出
$generatedText = $response.choices[0].message.content
Write-Host "生成されたテキスト: $($generatedText)"
}
catch {
Write-Error "API呼び出し中にエラーが発生しました: $($_.Exception.Message)"
if ($_.Exception.Response) {
$errorResponse = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$responseContent = $reader.ReadToEnd()
Write-Error "詳細レスポンス: $($responseContent)"
}
}
</pre>
<h4 class="wp-block-heading">堅牢化</h4>
<p><code>Invoke-RestMethod</code>をさらに堅牢にするため、詳細なエラーハンドリング、リトライロジック、セキュアなAPIキー管理(ここでは解説のみ)を追加します。</p>
<pre data-enlighter-language="generic"># Azure OpenAI設定
$resourceName = "YOUR_RESOURCE_NAME"
$deploymentName = "YOUR_DEPLOYMENT_NAME"
$apiVersion = "2023-05-15"
# APIキーは、以下のようにSecureStringとして保存するか、Azure Key Vaultなどから取得するべきです。
# $apiKey = Read-Host -Prompt "Azure OpenAI API Key" -AsSecureString
# $apiKey = (ConvertTo-SecureString "YOUR_API_KEY" -AsPlainText -Force) # テスト目的のみ
$apiKey = "YOUR_API_KEY" # ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
# エンドポイントURL構築
$endpoint = "https://$($resourceName).openai.azure.com/openai/deployments/$($deploymentName)/chat/completions?api-version=$($apiVersion)"
# プロンプト
$prompt = "PowerShellでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。エラーハンドリングとリトライについても触れてください。"
# リクエストボディ
$body = @{
messages = @(
@{ role = "system"; content = "You are a helpful assistant." },
@{ role = "user"; content = $prompt }
)
temperature = 0.7
max_tokens = 100
# stream = $false # 必要に応じて
} | ConvertTo-Json -Compress
# HTTPヘッダ
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey # SecureStringを直接渡すことはできないため、PlainTextにするか、Invoke-WebRequestの-Credentialを使うか検討
}
# リトライ設定
$maxRetries = 3
$initialDelaySeconds = 2
$generatedText = "エラー: AI応答を取得できませんでした。" # デフォルト値
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
Write-Host "API呼び出し試行: $($attempt)/$($maxRetries)" -ForegroundColor Cyan
try {
$response = Invoke-RestMethod -Uri $endpoint -Method Post -Headers $headers -Body $body -TimeoutSec 30 -ErrorAction Stop -StatusCodeVariable 'statusCode'
if ($statusCode -ge 200 -and $statusCode -lt 300) {
# 成功時の処理
$generatedText = $response.choices[0].message.content
Write-Host "生成されたテキスト (試行 $($attempt)): $($generatedText)" -ForegroundColor Green
break # 成功したらループを抜ける
} else {
Write-Warning "HTTPステータスコードエラー (試行 $($attempt)): $($statusCode)"
# Invoke-RestMethodは通常、非2xxステータスで例外を投げるため、このブロックは保険的
}
}
catch {
$errorMessage = $_.Exception.Message
$httpStatusCode = if ($_.Exception.Response) { $_.Exception.Response.StatusCode } else { "N/A" }
Write-Error "API呼び出し中にエラーが発生しました (試行 $($attempt)): $($errorMessage) (HTTP Status: $($httpStatusCode))"
if ($_.Exception.Response) {
try {
# エラーレスポンスボディを読み取る
$errorResponseContent = (New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())).ReadToEnd() | ConvertFrom-Json
Write-Error "詳細エラー: $($errorResponseContent | ConvertTo-Json -Depth 5)"
# 特定のエラーコードに対する追加処理(例: レート制限)
if ($errorResponseContent.error.code -eq "429") {
Write-Warning "レート制限に達しました。リトライを試みます。"
}
}
catch {
Write-Error "エラーレスポンスのパースに失敗しました: $($_.Exception.Message)"
}
}
}
if ($attempt -lt $maxRetries) {
$delay = $initialDelaySeconds * [math]::Pow(2, ($attempt - 1)) # 指数バックオフ
Write-Host "リトライします。待機時間: $($delay)秒..." -ForegroundColor Yellow
Start-Sleep -Seconds $delay
}
}
if ($generatedText -eq "エラー: AI応答を取得できませんでした。") {
Write-Error "Azure OpenAI APIから有効な応答を取得できませんでした。"
}
# 最終結果の表示(エラーだった場合はエラーメッセージ)
Write-Host "`n--- 最終結果 ---`n$($generatedText)"
</pre>
<p><strong>堅牢化のポイント:</strong>
* <strong><code>try/catch</code> ブロック:</strong> PowerShellの例外処理の標準的な方法。<code>Invoke-RestMethod</code>はHTTPステータスコードが200番台以外の場合、自動的に例外を投げます。
* <strong><code>ErrorAction Stop</code>:</strong> <code>Invoke-RestMethod</code>で例外が発生した際に、<code>catch</code>ブロックに処理を移すために<code>Stop</code>を指定します。
* <strong><code>StatusCodeVariable</code>:</strong> HTTPステータスコードを直接捕捉し、条件分岐に利用します。
* <strong>リトライロジック:</strong> VBAと同様に指数バックオフ戦略を採用。レート制限(HTTP 429)など一時的なエラーからの回復を試みます。
* <strong>詳細なエラーレスポンスの取得:</strong> <code>_.Exception.Response.GetResponseStream()</code> を使って、HTTPエラー発生時のレスポンスボディ(JSON形式のことが多い)を取得し、詳細なエラー情報をログ出力します。
* <strong>タイムアウト:</strong> <code>-TimeoutSec</code> パラメータで明示的にタイムアウトを設定します。</p>
<h2 class="wp-block-heading">ベンチ/検証</h2>
<p>作成したスクリプトの性能と信頼性を評価するための基本的な検証観点と計測方法です。</p>
<h3 class="wp-block-heading">計測方法</h3>
<ul class="wp-block-list">
<li><strong>VBA:</strong> <code>Timer</code>関数を用いて開始時刻と終了時刻を記録し、差分で処理時間を算出します。
<pre data-enlighter-language="generic">Dim startTime As Double
startTime = Timer
' --- API呼び出し処理 ---
Debug.Print "処理時間: " & (Timer - startTime) & "秒"
</pre></li>
<li><strong>PowerShell:</strong> <code>Measure-Command</code>コマンドレットを利用すると、スクリプトブロックの実行時間を簡単に計測できます。
<pre data-enlighter-language="generic">Measure-Command {
# --- API呼び出し処理 ---
} | Select-Object TotalSeconds
</pre></li>
</ul>
<h3 class="wp-block-heading">テスト観点</h3>
<ol class="wp-block-list">
<li><strong>基本的な機能:</strong> 正常なプロンプトで期待通りの応答が返されるか。</li>
<li><strong>パフォーマンス:</strong>
<ul>
<li>様々な<code>max_tokens</code>値(短い、長い)でのレスポンスタイム。</li>
<li>連続して複数回APIを呼び出した際の安定した応答速度。</li>
</ul></li>
<li><strong>エラーハンドリング:</strong>
<ul>
<li>無効なAPIキー/エンドポイントでの <code>401 Unauthorized</code> や <code>404 Not Found</code> エラーが適切に処理されるか。</li>
<li>ネットワーク障害(インターネット接続切断など)時の挙動。</li>
<li>タイムアウト設定が機能し、指定時間内に応答がない場合に適切に処理されるか。</li>
<li>APIのレート制限(<code>429 Too Many Requests</code>)が発生した場合にリトライロジックが機能するか。</li>
</ul></li>
<li><strong>プロンプトの複雑性:</strong>
<ul>
<li>非常に長いプロンプト(モデルのトークン上限に近い)を送信した場合。</li>
<li>特殊文字やエスケープが必要な文字(<code>"</code>, <code>\n</code>など)を含むプロンプトでの挙動。</li>
</ul></li>
<li><strong>JSONパースの堅牢性 (VBAのみ):</strong>
<ul>
<li>モデルが予期せぬ形式のJSON(例: <code>content</code>が空、または構造が異なる)を返した場合に、パースが失敗しないか、あるいは適切にエラーを報告するか。</li>
</ul></li>
<li><strong>セキュリティ:</strong>
<ul>
<li>APIキーがコードにハードコードされていないか(本番環境では)。環境変数や設定ファイルからの読み込みが正しく行われるか。</li>
</ul></li>
</ol>
<h2 class="wp-block-heading">応用例/代替案</h2>
<h3 class="wp-block-heading">応用例</h3>
<ol class="wp-block-list">
<li><strong>Excelデータの一括処理:</strong>
<ul>
<li>顧客の問い合わせ履歴(Excelシート)をAIで要約し、次のアクションを推奨する列を追加。</li>
<li>商品レビューを分析し、ポジティブ/ネガティブな評価を自動分類。</li>
<li>定型レポートの草稿を自動生成し、Excelシートに出力。</li>
</ul></li>
<li><strong>PowerShellによる自動化:</strong>
<ul>
<li>ログファイルから異常パターンを抽出し、AIで原因分析のヒントを得る。</li>
<li>メールの件名と本文から、内容を分類し、適切な担当者への振り分けを提案。</li>
<li>ドキュメントの生成(例: PowerShellスクリプトのコメントから概要を生成)。</li>
</ul></li>
</ol>
<h3 class="wp-block-heading">代替案</h3>
<ol class="wp-block-list">
<li><strong>Python + Azure OpenAI SDK (または <code>requests</code> ライブラリ):</strong>
<ul>
<li>最も推奨されるアプローチ。SDKはAPIのラッパーを提供し、認証、リトライ、ストリーミングなどを抽象化してくれます。<code>requests</code>ライブラリもHTTP通信を容易にします。VBA/PowerShellからのPython呼び出しを検討する価値はあります。</li>
</ul></li>
<li><strong>Azure Logic Apps / Power Automate:</strong>
<ul>
<li>ローコード/ノーコードでAzure OpenAIとの連携フローを構築できます。複雑なプログラミングなしに、様々なSaaSやオンプレミスシステムとの連携が可能です。</li>
</ul></li>
<li><strong>Azure Functions / Web Apps:</strong>
<ul>
<li>VBA/PowerShellから直接APIを叩く代わりに、Azure FunctionsなどでAPIをラップするミドルウェア層を構築する方法です。これにより、APIキーの管理を一元化し、レート制限ロジックをサーバー側で実装し、クライアント側(VBA/PowerShell)はよりシンプルな呼び出しで済ませられます。</li>
</ul></li>
<li><strong>既存のRPAツール:</strong>
<ul>
<li>UiPath, Power Automate DesktopなどのRPAツールは、HTTPリクエストアクティビティを提供しており、VBA/PowerShellと同様にAPI連携が可能です。</li>
</ul></li>
</ol>
<h2 class="wp-block-heading">まとめ</h2>
<p>VBAやPowerShellからAzure OpenAI ServiceのREST APIを直接呼び出す手法は、既存の環境に新たな依存関係を持ち込まずにAIの能力を統合する強力な手段です。低レベルなHTTP/JSONの挙動を理解することで、APIキー認証の仕組み、リクエスト/レスポンスの構造、そしてエラー処理の基本を深く把握できます。</p>
<p>本記事では、最小限の実装から始まり、タイムアウト、リトライ、堅牢なJSONパース(VBAでの正規表現活用)といった要素を盛り込みながら、実践的な堅牢化の指針を示しました。特にVBAではJSONパースが課題となりますが、PowerShellの<code>Invoke-RestMethod</code>はその点において非常に優れています。</p>
<p>直接APIを叩くことは、細かな制御が可能であるというメリットがある一方で、認証情報の安全な管理、エラーハンドリング、レート制限への対応など、開発者が多くの責任を負うことになります。しかし、これらの課題を適切に乗り越えれば、あなたのVBA/PowerShellスクリプトは、生成AIによって新たな次元の自動化を実現するでしょう。</p>
<h2 class="wp-block-heading">参考リンク</h2>
<ul class="wp-block-list">
<li><a href="https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference">Azure OpenAI Service REST API リファレンス</a></li>
<li><a href="https://learn.microsoft.com/ja-jp/windows/win32/winhttp/winhttp-services-reference">WinHTTP サービス リファレンス</a></li>
</ul>
VBA/PowerShellからAzure OpenAI APIを極める:HTTP/JSON層からの徹底解剖
導入(問題設定)
ExcelマクロやPowerShellスクリプトが根付く現場において、業務効率化は永遠の課題です。定型的なテキスト処理、データ分類、要約、あるいは自由形式の質問応答など、これまで人の手で行われてきた作業に、いまや生成AIの力が期待されています。しかし、既存の業務システムや社内規定の制約により、最新のPythonライブラリや高度なSDKの導入が難しいケースは少なくありません。
本記事では、こうした環境下でもAzure OpenAI Serviceの強力なテキスト生成能力を享受できるよう、VBAおよびPowerShellから直接REST APIを呼び出す方法を、HTTP/JSONの低レベルな視点から徹底的に解説 します。単なるHowToに留まらず、内部動作、境界条件、そして開発者が陥りがちな落とし穴まで深掘りすることで、堅牢で実践的なソリューション構築のための知見を提供します。
理論の要点
Azure OpenAI ServiceのREST APIは、HTTPプロトコル上でJSON形式のデータをやり取りすることで、AIモデルとの対話を可能にします。その核心を理解するために、以下の要素を押さえます。
1. エンドポイントと認証
Azure OpenAI ServiceのエンドポイントURLは、Azureリソース名、デプロイ名、およびAPIバージョンによって構成されます。
https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version={api-version}
{your-resource-name}
: Azure OpenAI Serviceリソースのホスト名。
{your-deployment-name}
: デプロイしたモデル(例: gpt-35-turbo
)の名前。
{api-version}
: 使用するAPIのバージョン(例: 2023-05-15
)。
認証には、APIキー認証(推奨)を使用します。HTTPリクエストヘッダに api-key: YOUR_API_KEY
を付与します。Authorization: Bearer YOUR_API_KEY
も利用可能ですが、Azure OpenAIでは api-key
ヘッダが慣例です。
2. リクエストとレスポンスの構造
モデルとの対話は、JSON形式で構成されたリクエストボディをPOSTすることで行われます。
リクエストボディの主要プロパティ
プロパティ名
型
必須/任意
説明
messages
Array
必須
モデルへのプロンプト履歴。各要素は role
(string) と content
(string) を持つオブジェクト。role
は system
(全体の振る舞い), user
(ユーザーの質問), assistant
(モデルの過去の回答) があります。
model
String
任意
使用するモデルのID。Azure OpenAIでは{your-deployment-name}
で指定されるため、通常は空か、デプロイ名と一致させる。OpenAI APIのmodel
とは挙動が異なる場合があるため、Azure OpenAIではエンドポイントURLのデプロイ名に依存するのが安全です。ただし、一部のAPIバージョンではボディ内model
が必須の場合があるため、公式ドキュメントのAPIバージョンごとの仕様を確認すること。
temperature
Number
任意
出力のランダム性(0.0~2.0)。0.0に近いほど決定論的、2.0に近いほど創造的になります。デフォルトは1.0。
max_tokens
Integer
任意
生成される応答の最大トークン数。入力プロンプトと出力の両方でモデルのコンテキストウィンドウをオーバーしないよう注意が必要です。
top_p
Number
任意
サンプリング戦略。temperature
とは排他的に使用されます。累積確率 top_p
まで含まれるトークン候補からサンプリングします。
frequency_penalty
Number
任意
生成されるトークンが、すでにプロンプトや出力に存在するトークンである場合にペナルティを課す度合い(-2.0~2.0)。値が大きいほど繰り返しを減らします。
presence_penalty
Number
任意
生成されるトークンが、プロンプトや出力に存在するトークンに関わらず、新しいトピックを導入する度合い(-2.0~2.0)。値が大きいほど新しいトピックを導入しやすくなります。
stream
Boolean
任意
レスポンスをストリーミング形式で受け取るかどうか。true
の場合、応答が少しずつ返されます。リアルタイムアプリケーション向け。今回は非ストリーミングで解説。
レスポンスボディの主要プロパティ
プロパティ名
型
説明
id
String
チャット補完セッションのユニークID。
object
String
オブジェクトのタイプ(例: chat.completion
)。
created
Integer
タイムスタンプ(Unix時間)。
model
String
使用されたモデルのID。
choices
Array
生成された応答の配列。各要素は message
(オブジェクト), finish_reason
(String), index
(Integer) を持つ。
message
Object
role
(String) と content
(String) を持つ。モデルの回答が content
に含まれます。
usage
Object
トークンの使用状況。prompt_tokens
, completion_tokens
, total_tokens
を含む。
3. HTTPクライアントの選択
VBA: WinHttp.WinHttpRequest.5.1
(推奨) または MSXML2.XMLHTTP60
を使用します。これらはCOMオブジェクトであり、特別な参照設定なしで利用できる場合が多いですが、OSやOfficeのバージョンによっては参照設定が必要な場合があります。
PowerShell: Invoke-RestMethod
コマンドレットを使用します。これはHTTPリクエストの送信とJSONレスポンスのオブジェクト化を自動で行ってくれる、非常に強力なコマンドレットです。
4. JSONのパース
VBA: WinHttpRequest
が返すすべてのレスポンスは生の文字列です。JSON文字列をVBAのオブジェクトとして扱うには、自前でパースロジックを実装するか、外部ライブラリ(JSONConverter.basなど)を利用する必要があります。本記事では外部ライブラリなしで、基礎的なパース方法を解説します。
PowerShell: Invoke-RestMethod
は、Content-Type: application/json
ヘッダを持つレスポンスを自動的にPowerShellのオブジェクト(PSCustomObject
)に変換してくれます。このため、JSONパースの労力はVBAに比べて格段に少ないです。
Azure OpenAI API 呼び出しフロー
graph TD
A["開始"] --> B{"環境変数/設定からAPIキー, エンドポイントURL取得"};
B --> C{"プロンプト構築 (messages配列)"};
C --> D{"JSONリクエストボディ生成"};
D --> E{"HTTPクライアント初期化 (VBA:WinHttpRequest / PS:Invoke-RestMethod)"};
E --> F{"HTTPヘッダ設定 (Content-Type, api-key)"};
F --> G{"POSTリクエスト送信"};
G --> H{"HTTPレスポンス受信"};
H --> I{"ステータスコード確認"};
I -- 200 OK --> J{"JSONレスポンスパース"};
I -- 4xx/5xxエラー --> K{"エラーハンドリング & リトライ"};
J --> L{"生成されたテキスト抽出"};
L --> M["結果表示/利用"];
K --> M;
M --> N["終了"];
実装(最小→堅牢化)
ここでは、VBAとPowerShellの両方で実装例を示します。環境変数は設定済みと仮定します。
前提条件
Azure OpenAI Serviceリソースがデプロイ済みであること。
gpt-3.5-turbo
または gpt-4
モデルが your-deployment-name
としてデプロイされていること。
APIキーとエンドポイントURLが取得済みであること。
VBA (Microsoft Excel VBA)
最小実装
VBAでHTTPリクエストを送信し、JSONレスポンスから必要な情報を抽出する最小限のコードです。エラーハンドリングは含まれていません。
Attribute VB_Name = "Module1"
Option Explicit
Private Const AZURE_OPENAI_ENDPOINT As String = "https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version=2023-05-15"
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_API_KEY" ' ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
Public Sub CallAzureOpenAIChatMinimal()
Dim req As Object ' WinHttpRequest.5.1
Dim sURL As String
Dim sAPIKey As String
Dim sPrompt As String
Dim sRequestBody As String
Dim sResponse As String
Dim sGeneratedText As String
' --- 設定値 ---
sURL = Replace(AZURE_OPENAI_ENDPOINT, "{your-resource-name}", "YOUR_RESOURCE_NAME") ' 例: my-aoai-resource
sURL = Replace(sURL, "{your-deployment-name}", "YOUR_DEPLOYMENT_NAME") ' 例: gpt-35-turbo-deploy
sAPIKey = AZURE_OPENAI_API_KEY
sPrompt = "VBAでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。"
' --- リクエストボディの構築 ---
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": ""You are a helpful assistant.""}," & _
"{""role"": ""user"", ""content"": """ & EscapeJsonString(sPrompt) & """}]," & _
"""temperature"": 0.7," & _
"""max_tokens"": 50}"
Set req = CreateObject("WinHttp.WinHttpRequest.5.1")
With req
.Open "POST", sURL, False ' 同期処理
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", sAPIKey
.Send sRequestBody
sResponse = .ResponseText
End With
' --- レスポンスのパース (簡易版) ---
' "content": "..." の値を取得
Dim iStart As Long, iEnd As Long
iStart = InStr(sResponse, """content"": """)
If iStart > 0 Then
iStart = iStart + Len("""content"": """)
iEnd = InStr(iStart, sResponse, """")
If iEnd > 0 Then
sGeneratedText = Mid(sResponse, iStart, iEnd - iStart)
Debug.Print "生成されたテキスト: " & sGeneratedText
Else
Debug.Print "エラー: contentの終わりが見つかりません。"
Debug.Print "レスポンス: " & sResponse
End If
Else
Debug.Print "エラー: contentが見つかりません。"
Debug.Print "レスポンス: " & sResponse
End If
Set req = Nothing
End Sub
' JSON文字列のエスケープ処理
' ダブルクォーテーションを \" に変換
Private Function EscapeJsonString(ByVal inputString As String) As String
EscapeJsonString = Replace(inputString, """", "\""")
' 他にもエスケープすべき文字(改行、タブなど)があれば追加
End Function
VBAの落とし穴と境界条件:
* JSONパースの困難さ: 上記のInStr
ベースのパースは、単純なcontent
抽出には使えますが、ネストが深いJSONや、エスケープ文字を含むJSONには対応できません。正規表現 (VBScript.RegExp
) を使うか、専用のJSONパーサークラスを自作する必要があります。
* 同期処理のブロック: .Open "POST", sURL, False
は同期処理です。API応答が遅い場合、Excelがフリーズします。非同期 (True
) で処理し、OnReadyStateChange
イベントをハンドリングする堅牢化が必要です。
* 64bit環境におけるPtrSafe
/ LongPtr
: WinHttp.WinHttpRequest
はCOMオブジェクトであり、VBAからCOMを呼び出す際にDeclare
ステートメントは通常使用しません。そのため、PtrSafe
やLongPtr
は直接関係しません。しかし、もしWinAPIを直接呼び出してHTTP通信を行う場合は、64bit環境でポインタのサイズが変わるため、これらのキーワードが必須となります。COMオブジェクトを介する場合は、VBAがCOMインターフェースを介してDLLを呼び出すため、VBA開発者が直接PtrSafe
等を意識する必要はありません。
堅牢化
上記の最小実装に、エラーハンドリング、タイムアウト設定、そして簡素ながらももう少し実用的なJSONパースを追加します。非同期処理はイベントドリブンな構造になるため、ここでは省略し、同期処理の堅牢化に焦点を当てます。
Attribute VB_Name = "Module1"
Option Explicit
Private Const AZURE_OPENAI_ENDPOINT As String = "https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version=2023-05-15"
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_API_KEY" ' ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
' WinHttpRequestの定数 (WinHttpRequest.dllを直接参照しない場合)
Private Const WHR_SETREQUESTHEADER As Long = 0
Private Const WHR_OPTION_ENABLE_REDIRECTS As Long = 6
Private Const WHR_OPTION_TIMEOUT As Long = 0 ' タイムアウト設定用オプション
Public Sub CallAzureOpenAIChatRobust()
Dim req As Object ' WinHttpRequest.5.1
Dim sURL As String
Dim sAPIKey As String
Dim sPrompt As String
Dim sRequestBody As String
Dim sResponse As String
Dim sGeneratedText As String
Dim lAttempts As Long
Const MAX_RETRIES As Long = 3
Const RETRY_DELAY_SEC As Long = 2 ' 初期リトライ遅延秒数
' --- 設定値 ---
sURL = Replace(AZURE_OPENAI_ENDPOINT, "{your-resource-name}", "YOUR_RESOURCE_NAME")
sURL = Replace(sURL, "{your-deployment-name}", "YOUR_DEPLOYMENT_NAME")
sAPIKey = AZURE_OPENAI_API_KEY
sPrompt = "VBAでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。エラーハンドリングとタイムアウトについても触れてください。"
' --- リクエストボディの構築 ---
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": ""You are a helpful assistant.""}," & _
"{""role"": ""user"", ""content"": """ & EscapeJsonString(sPrompt) & """}]," & _
"""temperature"": 0.7," & _
"""max_tokens"": 100}" ' トークン数を増やして少し長めの応答を期待
Set req = CreateObject("WinHttp.WinHttpRequest.5.1")
For lAttempts = 1 To MAX_RETRIES
On Error GoTo ErrorHandler ' エラーハンドラ設定
With req
.Option(WHR_OPTION_TIMEOUT) = 30000 ' 30秒のタイムアウト (ミリ秒指定)
.Open "POST", sURL, False ' 同期処理
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", sAPIKey
.Send sRequestBody
sResponse = .ResponseText
If .Status >= 200 And .Status < 300 Then
' 成功: レスポンスのパース
sGeneratedText = ParseJsonContent(sResponse)
If Len(sGeneratedText) > 0 Then
Debug.Print "生成されたテキスト (試行 " & lAttempts & "): " & sGeneratedText
Exit For ' 成功したらループを抜ける
Else
Debug.Print "警告: レスポンスからテキストを抽出できませんでした。ステータス: " & .Status & ", レスポンス: " & sResponse
GoTo NextAttempt ' パース失敗は次の試行へ
End If
Else
Debug.Print "HTTPエラー発生 (試行 " & lAttempts & "): " & .Status & " " & .StatusText
Debug.Print "レスポンス: " & sResponse
GoTo NextAttempt ' エラー発生は次の試行へ
End If
End With
NextAttempt:
If lAttempts < MAX_RETRIES Then
Debug.Print "リトライします。待機時間: " & (RETRY_DELAY_SEC * (2 ^ (lAttempts - 1))) & "秒..."
Application.Wait Now + TimeSerial(0, 0, RETRY_DELAY_SEC * (2 ^ (lAttempts - 1))) ' 指数バックオフ
Else
Debug.Print "最大リトライ回数を超えました。"
sGeneratedText = "エラー: AI応答を取得できませんでした。"
End If
Next lAttempts
CleanUp:
Set req = Nothing
Exit Sub
ErrorHandler:
Debug.Print "実行時エラー: " & Err.Number & " - " & Err.Description
' ネットワークエラーやタイムアウトの場合など
If Err.Number = -2147012894 Then ' WinHTTP Error 12002: ERROR_WINHTTP_TIMEOUT
Debug.Print "タイムアウトが発生しました。"
End If
Resume NextAttempt ' エラーが発生しても次の試行へ
End Sub
' JSON文字列のエスケープ処理
Private Function EscapeJsonString(ByVal inputString As String) As String
' JSON文字列に含まれる可能性のある特殊文字をエスケープ
' 必要に応じて、\n, \r, \t, \f, \b なども追加
EscapeJsonString = Replace(inputString, "\", "\\")
EscapeJsonString = Replace(EscapeJsonString, """", "\""")
End Function
' 簡易JSONパース関数 (正規表現を使用)
' JSONレスポンスから "content" フィールドの値を抽出
Private Function ParseJsonContent(ByVal jsonString As String) As String
Dim regEx As Object
Dim matches As Object
Set regEx = CreateObject("VBScript.RegExp")
With regEx
.Pattern = """content"":\s*""((?:[^""]|(?<=\\)"")*)""" ' エスケープされたダブルクォーテーションも考慮
.Global = False
.IgnoreCase = False
End With
Set matches = regEx.Execute(jsonString)
If matches.Count > 0 Then
' 最初のマッチの、1つ目のキャプチャグループがcontentの値
ParseJsonContent = Replace(matches(0).SubMatches(0), "\""", """") ' エスケープされたダブルクォーテーションを元に戻す
Else
ParseJsonContent = ""
End If
Set regEx = Nothing
End Function
堅牢化のポイント:
* エラーハンドリング: On Error GoTo
とエラーコードによる分岐。特にWinHTTPのエラーコードは把握しておくべきです。
* リトライロジック: ネットワークの一時的な問題やAPIのレート制限に対応するため、指数バックオフを伴うリトライ処理を実装しました。
* タイムアウト設定: req.Option(WHR_OPTION_TIMEOUT) = 30000
でタイムアウトを明示的に設定。
* JSONパースの強化: VBScript.RegExp
オブジェクトを使用して、"content": "..."
の値をより確実に抽出します。エスケープされたダブルクォーテーションも考慮に入れています。これにより、より柔軟なJSON構造に対応できますが、汎用的なJSONパーサーではありません。本格的なパースには外部ライブラリ (JsonConverter.bas
など) の導入が最善策です。
PowerShell
最小実装
Invoke-RestMethod
は非常に強力で、HTTPリクエストの送信、JSONの自動パース、エラーハンドリングの基本的な部分を一度に担ってくれます。
# Azure OpenAI設定
$resourceName = "YOUR_RESOURCE_NAME" # 例: my-aoai-resource
$deploymentName = "YOUR_DEPLOYMENT_NAME" # 例: gpt-35-turbo-deploy
$apiVersion = "2023-05-15"
$apiKey = "YOUR_API_KEY" # ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
# エンドポイントURL構築
$endpoint = "https://$($resourceName).openai.azure.com/openai/deployments/$($deploymentName)/chat/completions?api-version=$($apiVersion)"
# プロンプト
$prompt = "PowerShellでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。"
# リクエストボディ
$body = @{
messages = @(
@{ role = "system"; content = "You are a helpful assistant." },
@{ role = "user"; content = $prompt }
)
temperature = 0.7
max_tokens = 50
} | ConvertTo-Json -Compress # JSON文字列に変換
# HTTPヘッダ
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey
}
# API呼び出し
try {
$response = Invoke-RestMethod -Uri $endpoint -Method Post -Headers $headers -Body $body
# レスポンスからテキスト抽出
$generatedText = $response.choices[0].message.content
Write-Host "生成されたテキスト: $($generatedText)"
}
catch {
Write-Error "API呼び出し中にエラーが発生しました: $($_.Exception.Message)"
if ($_.Exception.Response) {
$errorResponse = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$responseContent = $reader.ReadToEnd()
Write-Error "詳細レスポンス: $($responseContent)"
}
}
堅牢化
Invoke-RestMethod
をさらに堅牢にするため、詳細なエラーハンドリング、リトライロジック、セキュアなAPIキー管理(ここでは解説のみ)を追加します。
# Azure OpenAI設定
$resourceName = "YOUR_RESOURCE_NAME"
$deploymentName = "YOUR_DEPLOYMENT_NAME"
$apiVersion = "2023-05-15"
# APIキーは、以下のようにSecureStringとして保存するか、Azure Key Vaultなどから取得するべきです。
# $apiKey = Read-Host -Prompt "Azure OpenAI API Key" -AsSecureString
# $apiKey = (ConvertTo-SecureString "YOUR_API_KEY" -AsPlainText -Force) # テスト目的のみ
$apiKey = "YOUR_API_KEY" # ⚠️ 本番環境では直接記述せず、より安全な方法で管理してください
# エンドポイントURL構築
$endpoint = "https://$($resourceName).openai.azure.com/openai/deployments/$($deploymentName)/chat/completions?api-version=$($apiVersion)"
# プロンプト
$prompt = "PowerShellでAzure OpenAI APIを呼び出す方法を簡潔に教えてください。エラーハンドリングとリトライについても触れてください。"
# リクエストボディ
$body = @{
messages = @(
@{ role = "system"; content = "You are a helpful assistant." },
@{ role = "user"; content = $prompt }
)
temperature = 0.7
max_tokens = 100
# stream = $false # 必要に応じて
} | ConvertTo-Json -Compress
# HTTPヘッダ
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey # SecureStringを直接渡すことはできないため、PlainTextにするか、Invoke-WebRequestの-Credentialを使うか検討
}
# リトライ設定
$maxRetries = 3
$initialDelaySeconds = 2
$generatedText = "エラー: AI応答を取得できませんでした。" # デフォルト値
for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
Write-Host "API呼び出し試行: $($attempt)/$($maxRetries)" -ForegroundColor Cyan
try {
$response = Invoke-RestMethod -Uri $endpoint -Method Post -Headers $headers -Body $body -TimeoutSec 30 -ErrorAction Stop -StatusCodeVariable 'statusCode'
if ($statusCode -ge 200 -and $statusCode -lt 300) {
# 成功時の処理
$generatedText = $response.choices[0].message.content
Write-Host "生成されたテキスト (試行 $($attempt)): $($generatedText)" -ForegroundColor Green
break # 成功したらループを抜ける
} else {
Write-Warning "HTTPステータスコードエラー (試行 $($attempt)): $($statusCode)"
# Invoke-RestMethodは通常、非2xxステータスで例外を投げるため、このブロックは保険的
}
}
catch {
$errorMessage = $_.Exception.Message
$httpStatusCode = if ($_.Exception.Response) { $_.Exception.Response.StatusCode } else { "N/A" }
Write-Error "API呼び出し中にエラーが発生しました (試行 $($attempt)): $($errorMessage) (HTTP Status: $($httpStatusCode))"
if ($_.Exception.Response) {
try {
# エラーレスポンスボディを読み取る
$errorResponseContent = (New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())).ReadToEnd() | ConvertFrom-Json
Write-Error "詳細エラー: $($errorResponseContent | ConvertTo-Json -Depth 5)"
# 特定のエラーコードに対する追加処理(例: レート制限)
if ($errorResponseContent.error.code -eq "429") {
Write-Warning "レート制限に達しました。リトライを試みます。"
}
}
catch {
Write-Error "エラーレスポンスのパースに失敗しました: $($_.Exception.Message)"
}
}
}
if ($attempt -lt $maxRetries) {
$delay = $initialDelaySeconds * [math]::Pow(2, ($attempt - 1)) # 指数バックオフ
Write-Host "リトライします。待機時間: $($delay)秒..." -ForegroundColor Yellow
Start-Sleep -Seconds $delay
}
}
if ($generatedText -eq "エラー: AI応答を取得できませんでした。") {
Write-Error "Azure OpenAI APIから有効な応答を取得できませんでした。"
}
# 最終結果の表示(エラーだった場合はエラーメッセージ)
Write-Host "`n--- 最終結果 ---`n$($generatedText)"
堅牢化のポイント:
* try/catch
ブロック: PowerShellの例外処理の標準的な方法。Invoke-RestMethod
はHTTPステータスコードが200番台以外の場合、自動的に例外を投げます。
* ErrorAction Stop
: Invoke-RestMethod
で例外が発生した際に、catch
ブロックに処理を移すためにStop
を指定します。
* StatusCodeVariable
: HTTPステータスコードを直接捕捉し、条件分岐に利用します。
* リトライロジック: VBAと同様に指数バックオフ戦略を採用。レート制限(HTTP 429)など一時的なエラーからの回復を試みます。
* 詳細なエラーレスポンスの取得: _.Exception.Response.GetResponseStream()
を使って、HTTPエラー発生時のレスポンスボディ(JSON形式のことが多い)を取得し、詳細なエラー情報をログ出力します。
* タイムアウト: -TimeoutSec
パラメータで明示的にタイムアウトを設定します。
ベンチ/検証
作成したスクリプトの性能と信頼性を評価するための基本的な検証観点と計測方法です。
計測方法
テスト観点
基本的な機能: 正常なプロンプトで期待通りの応答が返されるか。
パフォーマンス:
様々なmax_tokens
値(短い、長い)でのレスポンスタイム。
連続して複数回APIを呼び出した際の安定した応答速度。
エラーハンドリング:
無効なAPIキー/エンドポイントでの 401 Unauthorized
や 404 Not Found
エラーが適切に処理されるか。
ネットワーク障害(インターネット接続切断など)時の挙動。
タイムアウト設定が機能し、指定時間内に応答がない場合に適切に処理されるか。
APIのレート制限(429 Too Many Requests
)が発生した場合にリトライロジックが機能するか。
プロンプトの複雑性:
非常に長いプロンプト(モデルのトークン上限に近い)を送信した場合。
特殊文字やエスケープが必要な文字("
, \n
など)を含むプロンプトでの挙動。
JSONパースの堅牢性 (VBAのみ):
モデルが予期せぬ形式のJSON(例: content
が空、または構造が異なる)を返した場合に、パースが失敗しないか、あるいは適切にエラーを報告するか。
セキュリティ:
APIキーがコードにハードコードされていないか(本番環境では)。環境変数や設定ファイルからの読み込みが正しく行われるか。
応用例/代替案
応用例
Excelデータの一括処理:
顧客の問い合わせ履歴(Excelシート)をAIで要約し、次のアクションを推奨する列を追加。
商品レビューを分析し、ポジティブ/ネガティブな評価を自動分類。
定型レポートの草稿を自動生成し、Excelシートに出力。
PowerShellによる自動化:
ログファイルから異常パターンを抽出し、AIで原因分析のヒントを得る。
メールの件名と本文から、内容を分類し、適切な担当者への振り分けを提案。
ドキュメントの生成(例: PowerShellスクリプトのコメントから概要を生成)。
代替案
Python + Azure OpenAI SDK (または requests
ライブラリ):
最も推奨されるアプローチ。SDKはAPIのラッパーを提供し、認証、リトライ、ストリーミングなどを抽象化してくれます。requests
ライブラリもHTTP通信を容易にします。VBA/PowerShellからのPython呼び出しを検討する価値はあります。
Azure Logic Apps / Power Automate:
ローコード/ノーコードでAzure OpenAIとの連携フローを構築できます。複雑なプログラミングなしに、様々なSaaSやオンプレミスシステムとの連携が可能です。
Azure Functions / Web Apps:
VBA/PowerShellから直接APIを叩く代わりに、Azure FunctionsなどでAPIをラップするミドルウェア層を構築する方法です。これにより、APIキーの管理を一元化し、レート制限ロジックをサーバー側で実装し、クライアント側(VBA/PowerShell)はよりシンプルな呼び出しで済ませられます。
既存のRPAツール:
UiPath, Power Automate DesktopなどのRPAツールは、HTTPリクエストアクティビティを提供しており、VBA/PowerShellと同様にAPI連携が可能です。
まとめ
VBAやPowerShellからAzure OpenAI ServiceのREST APIを直接呼び出す手法は、既存の環境に新たな依存関係を持ち込まずにAIの能力を統合する強力な手段です。低レベルなHTTP/JSONの挙動を理解することで、APIキー認証の仕組み、リクエスト/レスポンスの構造、そしてエラー処理の基本を深く把握できます。
本記事では、最小限の実装から始まり、タイムアウト、リトライ、堅牢なJSONパース(VBAでの正規表現活用)といった要素を盛り込みながら、実践的な堅牢化の指針を示しました。特にVBAではJSONパースが課題となりますが、PowerShellのInvoke-RestMethod
はその点において非常に優れています。
直接APIを叩くことは、細かな制御が可能であるというメリットがある一方で、認証情報の安全な管理、エラーハンドリング、レート制限への対応など、開発者が多くの責任を負うことになります。しかし、これらの課題を適切に乗り越えれば、あなたのVBA/PowerShellスクリプトは、生成AIによって新たな次元の自動化を実現するでしょう。
参考リンク
コメント