<p>VBA/PowerShellからAzure OpenAI APIを操る:深層まで踏み込む連携術</p>
<h2 class="wp-block-heading">導入(問題設定)</h2>
<p>Microsoft Office製品群の中核を担うVBA、そしてシステム管理や自動化でその真価を発揮するPowerShellは、今なお多くのビジネス現場で現役のツールです。しかし、現代のデータドリブンな業務プロセスにおいて、これらのレガシーと目されがちな環境から最新のAI技術、特にAzure OpenAI Service (AOAI) の力を借りられないか、という課題意識を持つ方は少なくないでしょう。</p>
<p>「ExcelのマクロからAIによるテキスト要約や分類をしたい」「PowerShellスクリプトで自動生成されたログをAIで解析したい」――そういったニーズは高まる一方です。本稿では、VBAおよびPowerShellという、一見するとAI連携とは縁遠い環境から、Azure OpenAI ServiceのREST APIを直接叩き、その強力な自然言語処理能力を最大限に引き出す方法を、<strong>表層的なHowToに留まらず、内部動作、境界条件、落とし穴、そして堅牢化の戦略</strong>まで、マニアックに深掘りしていきます。</p>
<h2 class="wp-block-heading">理論の要点</h2>
<p>Azure OpenAI Serviceへの連携は、HTTPプロトコルに基づいたREST APIを介して行われます。クライアント(VBA/PowerShellスクリプト)は、所定のエンドポイントに対してHTTPリクエストを送信し、JSON形式で構成されたレスポンスを受信・解析します。</p>
<h3 class="wp-block-heading">REST APIの基本原則とAOAIの特有仕様</h3>
<ol class="wp-block-list">
<li><strong>エンドポイント</strong>: AOAIのエンドポイントURLは、Azureリソース名、デプロイメント名、APIバージョンによって一意に決まります。
例: <code>https://{resource_name}.openai.azure.com/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}</code>
<code>api-version</code>は非常に重要であり、省略するとAPI仕様の変更により予期せぬエラーや動作となる可能性があります。常に最新の安定版を指定しましょう。</li>
<li><strong>HTTPメソッド</strong>: テキスト生成などの場合は、データを送信するため<code>POST</code>メソッドを使用します。</li>
<li><strong>ヘッダー</strong>:
<ul>
<li><code>Content-Type: application/json</code>: リクエストボディがJSON形式であることを示します。これは必須です。</li>
<li><code>api-key: {YOUR_API_KEY}</code>: Azure OpenAI Serviceへの認証情報です。<code>Authorization: Bearer {YOUR_API_KEY}</code>形式でも可能ですが、AOAIでは<code>api-key</code>ヘッダーが推奨されます。</li>
</ul></li>
<li><strong>リクエストボディ (JSON)</strong>: APIに送信するデータ本体です。特に<code>chat/completions</code>エンドポイントでは、<code>messages</code>配列が主要な要素となります。
<ul>
<li><code>messages</code>: 会話の履歴を保持する配列。各要素は<code>role</code>(<code>system</code>, <code>user</code>, <code>assistant</code>のいずれか)と<code>content</code>(メッセージ本文)を持ちます。<code>system</code>ロールはAIの振る舞いを指示するプロンプト、<code>user</code>はユーザーからの入力、<code>assistant</code>はAIからの応答です。</li>
<li><code>temperature</code>: 応答のランダム性を制御します。0.0(決定的)から2.0(創造的)の範囲で設定します。</li>
<li><code>max_tokens</code>: 生成される応答の最大トークン数を指定します。</li>
</ul></li>
<li><strong>レスポンスボディ (JSON)</strong>: APIからの応答もJSON形式です。成功時には生成されたテキストや使用されたトークン数などが含まれます。エラー時にはエラーの詳細が返されます。</li>
<li><strong>HTTPステータスコード</strong>: リクエストの成否を示します。
<ul>
<li><code>200 OK</code>: 成功。</li>
<li><code>400 Bad Request</code>: リクエストボディのJSON形式が不正、必須パラメータ不足など。</li>
<li><code>401 Unauthorized</code>: APIキーが不正、または存在しない。</li>
<li><code>403 Forbidden</code>: 認証されているが、リソースへのアクセス権がない。</li>
<li><code>404 Not Found</code>: エンドポイントURLが間違っている、デプロイメント名が存在しないなど。</li>
<li><code>429 Too Many Requests</code>: レート制限超過。<code>Retry-After</code>ヘッダーで再試行までの秒数が示されることがあります。</li>
<li><code>500 Internal Server Error</code>: AOAI側の問題。</li>
</ul></li>
</ol>
<h3 class="wp-block-heading">内部動作:HTTP通信のレイヤー</h3>
<p><code>Invoke-RestMethod</code>や<code>WinHttpRequest</code>のようなCOMオブジェクトは、その背後でTCP/IPソケット通信、SSL/TLSハンドシェイク、HTTPプロトコルの組み立て・解析を自動的に行っています。これらを意識せずとも利用できますが、例えばTLSバージョンに起因する接続エラー(古いOS環境など)や、ネットワークレベルでのタイムアウトは、この深層の知識がなければデバッグが困難になることがあります。</p>
<h2 class="wp-block-heading">実装(最小→堅牢化)</h2>
<p>ここでは、<code>gpt-3.5-turbo</code>または<code>gpt-4</code>モデルを例に、チャット補完API (<code>chat/completions</code>) を呼び出す実装をVBAとPowerShellそれぞれで進めます。</p>
<h3 class="wp-block-heading">API仕様(chat/completions)</h3>
<p><code>POST /openai/deployments/{deployment_id}/chat/completions?api-version={api_version}</code></p>
<ul class="wp-block-list">
<li><strong>リクエストヘッダー</strong>:
<ul>
<li><code>Content-Type</code>: <code>application/json</code> (必須)</li>
<li><code>api-key</code>: <code>{Azure OpenAI Service API Key}</code> (必須)</li>
</ul></li>
<li><strong>リクエストボディ (JSON)</strong>:
<ul>
<li><code>messages</code>: 配列 (必須)
<ul>
<li><code>role</code>: 文字列 (<code>system</code>, <code>user</code>, <code>assistant</code>) (必須)</li>
<li><code>content</code>: 文字列 (メッセージ本文) (必須)</li>
</ul></li>
<li><code>temperature</code>: 数値 (0.0 ~ 2.0, デフォルト1.0)</li>
<li><code>max_tokens</code>: 整数 (生成されるトークン数の上限)</li>
<li><code>stop</code>: 文字列配列 (生成を停止するシーケンス)</li>
<li><code>top_p</code>: 数値 (0.0 ~ 1.0, デフォルト1.0, temperatureと排他的)</li>
<li><code>presence_penalty</code>: 数値 (-2.0 ~ 2.0, デフォルト0.0, 既存のトピックに対するペナルティ)</li>
<li><code>frequency_penalty</code>: 数値 (-2.0 ~ 2.0, デフォルト0.0, 頻出するトークンに対するペナルティ)</li>
<li><code>seed</code>: 整数 (再現性を保証するシード値, <code>system_fingerprint</code>と共に)</li>
</ul></li>
<li><strong>レスポンスボディ (JSON)</strong>:
<ul>
<li><code>id</code>: 文字列 (APIコールのID)</li>
<li><code>object</code>: 文字列 (<code>chat.completion</code>)</li>
<li><code>created</code>: 整数 (Unixタイムスタンプ)</li>
<li><code>model</code>: 文字列 (使用されたモデル名)</li>
<li><code>choices</code>: 配列
<ul>
<li><code>index</code>: 整数</li>
<li><code>message</code>: オブジェクト
<ul>
<li><code>role</code>: 文字列 (<code>assistant</code>)</li>
<li><code>content</code>: 文字列 (生成されたテキスト)</li>
</ul></li>
<li><code>finish_reason</code>: 文字列 (<code>stop</code>, <code>length</code>, <code>content_filter</code>など)</li>
</ul></li>
<li><code>usage</code>: オブジェクト
<ul>
<li><code>prompt_tokens</code>: 整数 (プロンプトに使用されたトークン数)</li>
<li><code>completion_tokens</code>: 整数 (応答に使用されたトークン数)</li>
<li><code>total_tokens</code>: 整数 (合計トークン数)</li>
</ul></li>
<li><code>system_fingerprint</code>: 文字列 (再現性情報, <code>seed</code>とセット)</li>
</ul></li>
</ul>
<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"># 設定値 (環境に応じて変更してください)
$resourceName = "your-openai-resource-name" # Azure OpenAIリソース名
$deploymentName = "your-deployment-name" # デプロイしたモデルの名前 (例: gpt-35-turbo)
$apiKey = "YOUR_AZURE_OPENAI_API_KEY" # Azure OpenAI APIキー
$apiVersion = "2024-02-15" # APIバージョン
$uri = "https://$resourceName.openai.azure.com/openai/deployments/$deploymentName/chat/completions?api-version=$apiVersion"
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey
}
$body = @{
messages = @(
@{ role = "system"; content = "あなたは有能なアシスタントです。" },
@{ role = "user"; content = "日本の首都はどこですか?" }
)
temperature = 0.7
max_tokens = 150
} | ConvertTo-Json -Depth 4 # -Depthでネストされたオブジェクトも適切にJSON化
Write-Host "Sending request to: $uri"
try {
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body
Write-Host "Response received."
Write-Host "Generated Text: $($response.choices[0].message.content)"
Write-Host "Total Tokens: $($response.usage.total_tokens)"
}
catch {
Write-Error "API call failed: $($_.Exception.Message)"
if ($_.Exception.Response) {
$errorBody = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorBody)
$errorResponseText = $reader.ReadToEnd()
$reader.Close()
Write-Error "Error Details: $errorResponseText"
}
}
</pre>
<h4 class="wp-block-heading">堅牢化とベストプラクティス</h4>
<p>実際の運用では、APIキーのセキュアな管理、エラーハンドリング、リトライメカニズムが不可欠です。</p>
<pre data-enlighter-language="generic"># 設定値 (環境変数からの取得を推奨)
# $env:AZURE_OPENAI_RESOURCE_NAME, $env:AZURE_OPENAI_DEPLOYMENT_NAME, $env:AZURE_OPENAI_API_KEY を設定してください
$resourceName = $env:AZURE_OPENAI_RESOURCE_NAME
$deploymentName = $env:AZURE_OPENAI_DEPLOYMENT_NAME
$apiKey = $env:AZURE_OPENAI_API_KEY
$apiVersion = "2024-02-15"
# 設定値のバリデーション
if (-not ($resourceName -and $deploymentName -and $apiKey)) {
Write-Error "環境変数 AZURE_OPENAI_RESOURCE_NAME, AZURE_OPENAI_DEPLOYMENT_NAME, AZURE_OPENAI_API_KEY が設定されていません。"
exit 1
}
$uri = "https://$resourceName.openai.azure.com/openai/deployments/$deploymentName/chat/completions?api-version=$apiVersion"
function Invoke-AzureOpenAIChatCompletion {
param(
[Parameter(Mandatory=$true)]
[string]$SystemPrompt,
[Parameter(Mandatory=$true)]
[string]$UserPrompt,
[Parameter(Mandatory=$false)]
[int]$MaxTokens = 150,
[Parameter(Mandatory=$false)]
[double]$Temperature = 0.7,
[Parameter(Mandatory=$false)]
[int]$MaxRetries = 5,
[Parameter(Mandatory=$false)]
[int]$InitialDelaySeconds = 1
)
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey
}
$body = @{
messages = @(
@{ role = "system"; content = $SystemPrompt },
@{ role = "user"; content = $UserPrompt }
)
temperature = $Temperature
max_tokens = $MaxTokens
} | ConvertTo-Json -Depth 4
for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
Write-Host "Attempt $attempt of $MaxRetries to call Azure OpenAI API..."
try {
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body -ContentType "application/json" -TimeoutSec 60
return $response.choices[0].message.content
}
catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $attempt): [$statusCode] $errorMessage"
if ($_.Exception.Response) {
$errorStream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorStream)
$errorResponseText = $reader.ReadToEnd()
$reader.Close()
Write-Warning "Error Details: $errorResponseText"
# レート制限 (429) の場合は Retry-After ヘッダーをチェック
if ($statusCode -eq 429 -and $_.Exception.Response.Headers['Retry-After']) {
$retryAfter = [int]$_.Exception.Response.Headers['Retry-After']
Write-Warning "Rate limit hit. Retrying after $retryAfter seconds (from server)."
Start-Sleep -Seconds $retryAfter
continue
}
}
# 4xx系のクライアントエラーはリトライしない
if ($statusCode -ge 400 -and $statusCode -lt 500 -and $statusCode -ne 429) {
Write-Error "Client-side error ($statusCode). Aborting retries."
throw "Azure OpenAI API Client Error: $errorResponseText"
}
# それ以外のエラー (主に5xx系) やネットワークエラーは指数バックオフでリトライ
if ($attempt -lt $MaxRetries) {
$delay = $InitialDelaySeconds * [math]::Pow(2, $attempt - 1)
Write-Warning "Retrying in $delay seconds..."
Start-Sleep -Seconds $delay
} else {
Write-Error "Max retries reached. Failing."
throw "Azure OpenAI API Failed after multiple retries: $errorResponseText"
}
}
}
return $null # 全てのリトライが失敗した場合
}
# 使用例
try {
$result = Invoke-AzureOpenAIChatCompletion `
-SystemPrompt "あなたは優秀なデータ分析アシスタントです。ユーザーの質問に簡潔に答えてください。" `
-UserPrompt "2023年の日本のGDP成長率は?"
if ($result) {
Write-Host "AIの回答: $result"
} else {
Write-Error "AIからの回答を取得できませんでした。"
}
}
catch {
Write-Error "スクリプト全体でエラーが発生しました: $($_.Exception.Message)"
}
</pre>
<h3 class="wp-block-heading">VBAによる実装</h3>
<p>VBAでは、<code>Microsoft WinHTTP Services</code>または<code>Microsoft XML, v6.0</code>などのCOMオブジェクトを利用してHTTPリクエストを送信します。ここでは、広く利用可能な<code>WinHttp.WinHttpRequest.5.1</code>オブジェクト(あるいはより新しい<code>MSXML2.ServerXMLHTTP60</code>など)を使用します。</p>
<h4 class="wp-block-heading">最小実装</h4>
<pre data-enlighter-language="generic">' 参照設定は不要だが、IntelliSenseを有効にする場合は
' 「ツール」->「参照設定」で「Microsoft WinHTTP Services, version 5.1」にチェック
Sub CallAzureOpenAIApi_Minimal()
Dim objHTTP As Object ' WinHttp.WinHttpRequest.5.1
Dim sURL As String
Dim sAPIKey As String
Dim sRequestBody As String
Dim sResponse As String
'--- 設定値 (環境に応じて変更してください) ---
Const RESOURCE_NAME As String = "your-openai-resource-name" ' Azure OpenAIリソース名
Const DEPLOYMENT_NAME As String = "your-deployment-name" ' デプロイしたモデルの名前
sAPIKey = "YOUR_AZURE_OPENAI_API_KEY" ' Azure OpenAI APIキー
Const API_VERSION As String = "2024-02-15"
'-------------------------------------------
sURL = "https://" & RESOURCE_NAME & ".openai.azure.com/openai/deployments/" & _
DEPLOYMENT_NAME & "/chat/completions?api-version=" & API_VERSION
' JSONボディの組み立て
' VBAでJSONを直接構築するのは手間がかかるが、ここでは手動でシンプルに構成
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": ""あなたは有能なアシスタントです。""}," & _
"{""role"": ""user"", ""content"": ""日本の首都はどこですか?""}]," & _
"""temperature"": 0.7, ""max_tokens"": 150}"
Set objHTTP = CreateObject("WinHttp.WinHttpRequest.5.1") ' または "MSXML2.XMLHTTP"
With objHTTP
.Open "POST", sURL, False ' Falseで同期呼び出し
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", sAPIKey
.Send sRequestBody
If .Status = 200 Then
sResponse = .ResponseText
Debug.Print "Response Received: " & sResponse
' レスポンスJSONから必要な情報を抽出 (簡易版)
' 実際のアプリケーションではJSONパーサーの利用を検討
Dim jsonParts As Variant
jsonParts = Split(sResponse, """content"":""")
If UBound(jsonParts) > 0 Then
Dim contentPart As String
contentPart = Split(jsonParts(1), """")(0)
Debug.Print "Generated Text: " & contentPart
Else
Debug.Print "Content not found in response."
End If
Else
Debug.Print "API Call Failed. Status: " & .Status & ", StatusText: " & .StatusText
Debug.Print "Response Text: " & .ResponseText ' エラー詳細
End If
End With
Set objHTTP = Nothing
End Sub
</pre>
<h4 class="wp-block-heading">堅牢化とベストプラクティス</h4>
<p>VBAでの堅牢化は、エラーハンドリング、リトライ、タイムアウト設定、そしてクラスモジュールによる抽象化が中心になります。JSONの構築と解析には、VBA-JSONなどの外部ライブラリも選択肢に入りますが、ここでは外部ライブラリに極力依存しないという要件に沿って、文字列操作による簡易的なアプローチを示します。</p>
<p><strong>VBAにおける64bit対応/PtrSafe/LongPtrについて</strong>:
VBAの64bit対応は主に<code>Declare Function</code>でWindows API(WinAPI)を直接呼び出す際に重要になります。ポインタやハンドルなど、メモリ上のアドレスを扱う引数や戻り値の型を<code>LongPtr</code>に、そして<code>Declare</code>ステートメントに<code>PtrSafe</code>キーワードを追加することが必須です。
<code>WinHttp.WinHttpRequest.5.1</code>のようなCOMオブジェクトを利用する場合、COMレイヤーが32bit/64bit間のアーキテクチャの違いを吸収するため、スクリプト内で直接<code>PtrSafe</code>や<code>LongPtr</code>を使用する場面は稀です。COMオブジェクトのメソッド引数にポインタを渡すような特殊なケースを除き、既存のCOMオブジェクトを利用する限りは、ほとんど意識する必要はありません。しかし、もしVBAからWinAPIを直接呼び出して高度なネットワーク操作や低レイヤー処理を行う場合は、これらのキーワードの理解と適用が不可欠です。本稿のスコープでは、COMオブジェクトによる高レベルAPI呼び出しに限定するため、<code>PtrSafe</code>/<code>LongPtr</code>の具体的なコード例は示しませんが、VBA開発者としては常にその存在と必要性を意識しておくべきです。</p>
<p>以下は、AOAI連携機能をクラスモジュールにカプセル化し、リトライロジックを追加した例です。</p>
<p><strong>クラスモジュール <code>cAzureOpenAIClient</code></strong></p>
<pre data-enlighter-language="generic">' クラスモジュール名: cAzureOpenAIClient
' 参照設定: 不要 (CreateObjectを使用)
Private Const CLASS_NAME As String = "cAzureOpenAIClient"
Private pResourceName As String
Private pDeploymentName As String
Private pAPIKey As String
Private pAPIVersion As String
Private pMaxRetries As Long
Private pInitialDelaySeconds As Long
Private pRequestTimeoutSeconds As Long
'*****************************************************
' Public Properties
'*****************************************************
Public Property Let ResourceName(ByVal sName As String)
pResourceName = sName
End Property
Public Property Get ResourceName() As String
ResourceName = pResourceName
End Property
Public Property Let DeploymentName(ByVal sName As String)
pDeploymentName = sName
End Property
Public Property Get DeploymentName() As String
DeploymentName = pDeploymentName
End Property
Public Property Let APIKey(ByVal sKey As String)
pAPIKey = sKey
End Property
Public Property Get APIKey() As String
APIKey = pAPIKey
End Property
Public Property Let APIVersion(ByVal sVersion As String)
pAPIVersion = sVersion
End Property
Public Property Get APIVersion() As String
APIVersion = pAPIVersion
End Property
Public Property Let MaxRetries(ByVal lRetries As Long)
pMaxRetries = lRetries
End Property
Public Property Get MaxRetries() As Long
MaxRetries = pMaxRetries
End Property
Public Property Let InitialDelaySeconds(ByVal lDelay As Long)
pInitialDelaySeconds = lDelay
End Property
Public Property Get InitialDelaySeconds() As Long
InitialDelaySeconds = pInitialDelaySeconds
End Property
Public Property Let RequestTimeoutSeconds(ByVal lTimeout As Long)
pRequestTimeoutSeconds = lTimeout
End Property
Public Property Get RequestTimeoutSeconds() As Long
RequestTimeoutSeconds = pRequestTimeoutSeconds
End Property
'*****************************************************
' Initialize
'*****************************************************
Private Sub Class_Initialize()
' デフォルト値の設定
pAPIVersion = "2024-02-15"
pMaxRetries = 3
pInitialDelaySeconds = 2
pRequestTimeoutSeconds = 60
End Sub
'*****************************************************
' Public Methods
'*****************************************************
Public Function ChatCompletion(ByVal sSystemPrompt As String, _
ByVal sUserPrompt As String, _
Optional ByVal dTemperature As Double = 0.7, _
Optional ByVal lMaxTokens As Long = 150) As String
Dim objHTTP As Object ' WinHttp.WinHttpRequest.5.1
Dim sURL As String
Dim sRequestBody As String
Dim sResponse As String
Dim lAttempt As Long
Dim lDelay As Long
Dim lStatusCode As Long
On Error GoTo ErrorHandler
' 設定値のバリデーション
If pResourceName = "" Or pDeploymentName = "" Or pAPIKey = "" Then
Err.Raise vbObjectError + 1000, CLASS_NAME, "Required properties (ResourceName, DeploymentName, APIKey) are not set."
End If
sURL = "https://" & pResourceName & ".openai.azure.com/openai/deployments/" & _
pDeploymentName & "/chat/completions?api-version=" & pAPIVersion
' JSONボディの組み立て (簡易版、必要に応じてJSONパーサーを導入)
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": """ & EscapeJson(sSystemPrompt) & """}," & _
"{""role"": ""user"", ""content"": """ & EscapeJson(sUserPrompt) & """}]," & _
"""temperature"": " & Replace(CStr(dTemperature), ",", ".") & "," & _
"""max_tokens"": " & lMaxTokens & "}"
Set objHTTP = CreateObject("WinHttp.WinHttpRequest.5.1")
For lAttempt = 1 To pMaxRetries + 1 ' MaxRetries + 1回のリトライ試行
Debug.Print CLASS_NAME & ": Attempt " & lAttempt & " of " & (pMaxRetries + 1)
With objHTTP
.Open "POST", sURL, False ' 同期呼び出し
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", pAPIKey
.SetTimeouts pRequestTimeoutSeconds * 1000, pRequestTimeoutSeconds * 1000, pRequestTimeoutSeconds * 1000, pRequestTimeoutSeconds * 1000 ' resolve, connect, send, receive timeouts (ms)
On Error Resume Next ' Send中にエラーが発生する可能性もある
.Send sRequestBody
lStatusCode = .Status
sResponse = .ResponseText
On Error GoTo ErrorHandler
If lStatusCode = 200 Then
ChatCompletion = ExtractContentFromJson(sResponse)
Exit Function
Else
Debug.Print CLASS_NAME & ": API Call Failed (Attempt " & lAttempt & "). Status: " & lStatusCode & ", StatusText: " & .StatusText
Debug.Print CLASS_NAME & ": Error Response: " & sResponse
If lStatusCode >= 400 And lStatusCode < 500 And lStatusCode <> 429 Then
' クライアントサイドエラー (4xx系、レート制限429以外) はリトライしない
Err.Raise vbObjectError + 1001, CLASS_NAME, "Client-side API error: " & lStatusCode & " - " & sResponse
End If
If lAttempt <= pMaxRetries Then ' MaxRetries回まではリトライを試みる
lDelay = pInitialDelaySeconds * (2 ^ (lAttempt - 1)) ' 指数バックオフ
' Retry-Afterヘッダーがあればそちらを優先 (VBA WinHttpRequestでは直接取得がやや面倒なので簡易化)
' 実際には.GetResponseHeader("Retry-After")で取得を試みるべき
Debug.Print CLASS_NAME & ": Retrying in " & lDelay & " seconds..."
Application.Wait Now + TimeValue("00:00:" & lDelay)
DoEvents ' UIフリーズ防止
Else
' 最大リトライ回数を超えた
Err.Raise vbObjectError + 1002, CLASS_NAME, "Max retries exceeded. Last status: " & lStatusCode & " - " & sResponse
End If
End If
End With
Next lAttempt
' ここに到達した場合、エラーハンドラにジャンプしていなければnull/空文字列を返す
ChatCompletion = ""
Exit Function
ErrorHandler:
Debug.Print CLASS_NAME & ": An error occurred: " & Err.Description
ChatCompletion = "" ' エラー時は空文字列を返す
Err.Clear
End Function
' JSON文字列内の特殊文字をエスケープする簡易関数
Private Function EscapeJson(ByVal sText As String) As String
sText = Replace(sText, "\", "\\")
sText = Replace(sText, """", "\""")
sText = Replace(sText, Chr(8), "\b") ' Backspace
sText = Replace(sText, Chr(12), "\f") ' Form feed
sText = Replace(sText, Chr(10), "\n") ' Newline
sText = Replace(sText, Chr(13), "\r") ' Carriage return
sText = Replace(sText, Chr(9), "\t") ' Tab
EscapeJson = sText
End Function
' 応答JSONからcontentを抽出する簡易関数
' 堅牢な実装にはJSONパーサーの使用を推奨
Private Function ExtractContentFromJson(ByVal sJson As String) As String
Dim re As Object ' RegExp
Set re = CreateObject("VBScript.RegExp")
re.Pattern = """content"":\s*""([^""]*)"""
re.Global = False
If re.Test(sJson) Then
ExtractContentFromJson = re.Execute(sJson)(0).SubMatches(0)
Else
ExtractContentFromJson = ""
End If
Set re = Nothing
End Function
</pre>
<p><strong>標準モジュールでの使用例</strong></p>
<pre data-enlighter-language="generic">Sub TestAzureOpenAIClient()
Dim oAIClient As cAzureOpenAIClient
Dim sResult As String
'--- 環境変数から取得するなどのセキュアな方法を推奨 ---
Const AZURE_OPENAI_RESOURCE_NAME As String = "your-openai-resource-name"
Const AZURE_OPENAI_DEPLOYMENT_NAME As String = "your-deployment-name"
Const AZURE_OPENAI_API_KEY As String = "YOUR_AZURE_OPENAI_API_KEY"
'----------------------------------------------------
Set oAIClient = New cAzureOpenAIClient
With oAIClient
.ResourceName = AZURE_OPENAI_RESOURCE_NAME
.DeploymentName = AZURE_OPENAI_DEPLOYMENT_NAME
.APIKey = AZURE_OPENAI_API_KEY
.MaxRetries = 5 ' リトライ回数を設定
.InitialDelaySeconds = 3 ' 初期遅延秒数を設定
.RequestTimeoutSeconds = 90 ' リクエストタイムアウト秒数を設定
End With
On Error GoTo ErrorHandler
Debug.Print "Calling Azure OpenAI API..."
sResult = oAIClient.ChatCompletion("あなたは優秀なデータ分析アシスタントです。ユーザーの質問に簡潔に答えてください。", _
"2023年の日本のGDP成長率は?")
If sResult <> "" Then
Debug.Print "AIの回答: " & sResult
Else
Debug.Print "AIからの回答を取得できませんでした。詳細はデバッグウィンドウを確認してください。"
End If
Exit Sub
ErrorHandler:
Debug.Print "An error occurred in TestAzureOpenAIClient: " & Err.Description
MsgBox "エラーが発生しました: " & Err.Description, vbCritical
Set oAIClient = Nothing
End Sub
</pre>
<h3 class="wp-block-heading">Azure OpenAI API連携フロー</h3>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["VBA/PowerShellスクリプト開始"] --> B("API設定読み込み: Resource, Deployment, Key, Version")
B --> C{"プロンプトとパラメータでリクエストボディを構築"}
C --> D("HTTPヘッダー設定: Content-Type, api-key")
D --> E("APIエンドポイントURL構築")
E --> F["HTTP POSTリクエスト送信"]
F -- ネットワークエラー/タイムアウト --> G{"リトライ判定?"}
F -- HTTPステータス 200 OK --> H("レスポンスJSON受信")
F -- HTTPステータス 4XX/5XX --> I{"エラー種別判定"}
G -- リトライ可能 --> J("指数バックオフ待機")
J --> F
G -- リトライ不可 --> K("処理失敗/エラー報告")
I -- 429 Too Many Requests --> L("Retry-Afterヘッダーチェック")
L -- 指定あり --> J
L -- 指定なし/その他リトライ可能なエラー --> J
I -- 4XXクライアントエラー(429以外) --> K
I -- 5XXサーバーエラー --> J
H --> M("レスポンスJSON解析: 生成テキスト、トークン数など抽出")
M --> N["結果の利用/表示"]
N --> O["スクリプト終了"]
K --> O
</pre></div>
<h2 class="wp-block-heading">ベンチ/検証</h2>
<p>AOAI連携コードの品質を担保するためには、単なる動作確認だけでなく、性能と堅牢性の検証が不可欠です。</p>
<ul class="wp-block-list">
<li><strong>機能テスト</strong>:
<ul>
<li><strong>正常系</strong>: 短いプロンプト、長いプロンプト、複雑なプロンプトで期待通りの応答が得られるか。</li>
<li><strong>異常系</strong>:
<ul>
<li>無効なAPIキー、存在しないデプロイ名、不正なJSONボディで<code>401</code>/<code>404</code>/<code>400</code>エラーが正しくハンドリングされるか。</li>
<li>レート制限(意図的に短時間で多数リクエストを送信)時にリトライが機能し、最終的に応答を得られるか、または適切なエラーで終了するか。</li>
<li>ネットワーク切断時やDNS解決失敗時にタイムアウト・リトライが働くか。</li>
</ul></li>
</ul></li>
<li><strong>性能テスト</strong>:
<ul>
<li><strong>応答時間</strong>: プロンプトの長さ、<code>max_tokens</code>の値、モデル(<code>gpt-3.5-turbo</code> vs <code>gpt-4</code>)の違いが応答時間にどう影響するかを計測。複数回実行して平均値を算出。</li>
<li><strong>トークン消費量</strong>: 同様のプロンプトで何度か実行し、<code>usage.total_tokens</code>が変動するかどうかを確認。</li>
</ul></li>
<li><strong>境界条件の確認</strong>:
<ul>
<li><code>max_tokens</code>を極端に小さな値(例: 1)や大きな値に設定した場合の挙動。</li>
<li><code>temperature</code>を0.0(決定的)や2.0(創造的)に設定した際の応答の変化。</li>
<li>非常に長いプロンプト(コンテキストウィンドウの最大値に近づける)を送信した場合の動作。</li>
</ul></li>
</ul>
<p><strong>計測方法の簡略例</strong>:</p>
<ul class="wp-block-list">
<li><strong>VBA</strong>:
<pre data-enlighter-language="generic">Dim StartTime As Double
StartTime = Timer ' 秒単位の現在時刻
' API呼び出しコード
Dim EndTime As Double
EndTime = Timer
Debug.Print "処理時間: " & (EndTime - StartTime) & "秒"
</pre></li>
<li><strong>PowerShell</strong>:
<pre data-enlighter-language="generic">$startTime = Get-Date
# API呼び出しコード
$endTime = Get-Date
$duration = $endTime - $startTime
Write-Host "処理時間: $($duration.TotalSeconds)秒"
</pre></li>
</ul>
<h2 class="wp-block-heading">応用例/代替案</h2>
<h3 class="wp-block-heading">応用例</h3>
<ol class="wp-block-list">
<li><strong>Excelでのレポート自動生成</strong>: VBAからAOAIを呼び出し、特定のデータセットに基づいてサマリーや洞察を生成し、Excelシートに直接書き込む。</li>
<li><strong>PowerShellによるログ分析</strong>: サーバーログやイベントログをPowerShellで収集・フィルタリングし、AOAIで異常検出やカテゴリ分類を行う。</li>
<li><strong>社内Q&Aシステム</strong>: VBA/PowerShellスクリプトでユーザーの質問を受け付け、既存ドキュメントを参照させつつAOAIで回答を生成、Teamsなどへ通知する。</li>
<li><strong>定型業務の自動化</strong>: 例えば、メールの件名や本文からキーワードを抽出し、それに基づいて次のアクション(タスク作成、ファイル分類など)をAIに判断させる。</li>
</ol>
<h3 class="wp-block-heading">代替案</h3>
<ol class="wp-block-list">
<li><strong>Python等の中間層の利用</strong>:
VBA/PowerShellで複雑なJSON操作や高度なエラーハンドリング、非同期処理を実装するのは困難が伴います。PythonにはOpenAIのSDKや豊富なライブラリが存在するため、VBA/PowerShellからPythonスクリプトを呼び出し、Python側でAOAI連携の全てを担わせるという方法も有効です。
<ul>
<li><strong>VBA</strong>: <code>Shell</code>関数でPythonスクリプトを実行し、標準出力/ファイル経由で結果を受け取る。</li>
<li><strong>PowerShell</strong>: <code>Start-Process</code>や<code>python.exe</code>を直接呼び出す。</li>
</ul></li>
<li><strong>Azure Functions/Logic Appsなどの利用</strong>:
VBA/PowerShellから直接AOAIを呼び出すのではなく、Azure FunctionsやLogic Appsなどのサーバーレスサービスを間に挟むことで、認証情報の管理を一元化し、レート制限やリトライロジックをクラウド側で堅牢に実装できます。これによりVBA/PowerShell側のコードは非常にシンプルになります。</li>
<li><strong>データブリッジングツール</strong>:
Power Automate DesktopのようなRPAツールを利用し、AI連携部分をGUIで設定し、PowerShellやVBAと連携させるアプローチも可能です。</li>
</ol>
<h2 class="wp-block-heading">まとめ</h2>
<p>本稿では、VBAおよびPowerShellという既存環境からAzure OpenAI ServiceのREST APIを連携させる手法について、その最小実装から堅牢化、そして内部動作や落とし穴まで踏み込んで解説しました。</p>
<ul class="wp-block-list">
<li><strong>VBA</strong>: <code>WinHttp.WinHttpRequest.5.1</code>オブジェクトを使ったHTTP通信、簡易的なJSON構築・解析、クラスモジュールによる抽象化とエラー・リトライハンドリング。64bit環境における<code>PtrSafe</code>/<code>LongPtr</code>の要点についても言及しました。</li>
<li><strong>PowerShell</strong>: <code>Invoke-RestMethod</code>による簡潔なHTTPリクエスト、環境変数を用いたAPIキー管理、<code>try/catch/finally</code>と指数バックオフを用いた堅牢なリトライロジック。</li>
</ul>
<p>VBAやPowerShellは、手元のExcelファイルや既存システムとの連携において、依然として強力なツールです。これらにAzure OpenAI Serviceの力を組み合わせることで、従来手作業で行っていたテキスト処理や意思決定プロセスにAIの知見を導入し、業務の高度化・自動化を劇的に推進できる可能性を秘めています。</p>
<p>しかし、その実装にはAPIキーの厳重な管理、レート制限への対策、JSONの厳密な取り扱いといった細心の注意が求められます。本稿で紹介した堅牢化のポイントと運用チェックリストを参考に、セキュアで信頼性の高い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/overview">Azure OpenAI Service の概要 – Azure Cognitive Services | Microsoft Learn</a></li>
<li><a href="https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference#chat-completions">Chat Completions – REST API リファレンス – Azure Cognitive Services | Microsoft Learn</a></li>
</ul>
VBA/PowerShellからAzure OpenAI APIを操る:深層まで踏み込む連携術
導入(問題設定)
Microsoft Office製品群の中核を担うVBA、そしてシステム管理や自動化でその真価を発揮するPowerShellは、今なお多くのビジネス現場で現役のツールです。しかし、現代のデータドリブンな業務プロセスにおいて、これらのレガシーと目されがちな環境から最新のAI技術、特にAzure OpenAI Service (AOAI) の力を借りられないか、という課題意識を持つ方は少なくないでしょう。
「ExcelのマクロからAIによるテキスト要約や分類をしたい」「PowerShellスクリプトで自動生成されたログをAIで解析したい」――そういったニーズは高まる一方です。本稿では、VBAおよびPowerShellという、一見するとAI連携とは縁遠い環境から、Azure OpenAI ServiceのREST APIを直接叩き、その強力な自然言語処理能力を最大限に引き出す方法を、表層的なHowToに留まらず、内部動作、境界条件、落とし穴、そして堅牢化の戦略まで、マニアックに深掘りしていきます。
理論の要点
Azure OpenAI Serviceへの連携は、HTTPプロトコルに基づいたREST APIを介して行われます。クライアント(VBA/PowerShellスクリプト)は、所定のエンドポイントに対してHTTPリクエストを送信し、JSON形式で構成されたレスポンスを受信・解析します。
REST APIの基本原則とAOAIの特有仕様
- エンドポイント: AOAIのエンドポイントURLは、Azureリソース名、デプロイメント名、APIバージョンによって一意に決まります。
例:
https://{resource_name}.openai.azure.com/openai/deployments/{deployment_id}/chat/completions?api-version={api_version}
api-version
は非常に重要であり、省略するとAPI仕様の変更により予期せぬエラーや動作となる可能性があります。常に最新の安定版を指定しましょう。
- HTTPメソッド: テキスト生成などの場合は、データを送信するため
POST
メソッドを使用します。
- ヘッダー:
Content-Type: application/json
: リクエストボディがJSON形式であることを示します。これは必須です。
api-key: {YOUR_API_KEY}
: Azure OpenAI Serviceへの認証情報です。Authorization: Bearer {YOUR_API_KEY}
形式でも可能ですが、AOAIではapi-key
ヘッダーが推奨されます。
- リクエストボディ (JSON): APIに送信するデータ本体です。特に
chat/completions
エンドポイントでは、messages
配列が主要な要素となります。
messages
: 会話の履歴を保持する配列。各要素はrole
(system
, user
, assistant
のいずれか)とcontent
(メッセージ本文)を持ちます。system
ロールはAIの振る舞いを指示するプロンプト、user
はユーザーからの入力、assistant
はAIからの応答です。
temperature
: 応答のランダム性を制御します。0.0(決定的)から2.0(創造的)の範囲で設定します。
max_tokens
: 生成される応答の最大トークン数を指定します。
- レスポンスボディ (JSON): APIからの応答もJSON形式です。成功時には生成されたテキストや使用されたトークン数などが含まれます。エラー時にはエラーの詳細が返されます。
- HTTPステータスコード: リクエストの成否を示します。
200 OK
: 成功。
400 Bad Request
: リクエストボディのJSON形式が不正、必須パラメータ不足など。
401 Unauthorized
: APIキーが不正、または存在しない。
403 Forbidden
: 認証されているが、リソースへのアクセス権がない。
404 Not Found
: エンドポイントURLが間違っている、デプロイメント名が存在しないなど。
429 Too Many Requests
: レート制限超過。Retry-After
ヘッダーで再試行までの秒数が示されることがあります。
500 Internal Server Error
: AOAI側の問題。
内部動作:HTTP通信のレイヤー
Invoke-RestMethod
やWinHttpRequest
のようなCOMオブジェクトは、その背後でTCP/IPソケット通信、SSL/TLSハンドシェイク、HTTPプロトコルの組み立て・解析を自動的に行っています。これらを意識せずとも利用できますが、例えばTLSバージョンに起因する接続エラー(古いOS環境など)や、ネットワークレベルでのタイムアウトは、この深層の知識がなければデバッグが困難になることがあります。
実装(最小→堅牢化)
ここでは、gpt-3.5-turbo
またはgpt-4
モデルを例に、チャット補完API (chat/completions
) を呼び出す実装をVBAとPowerShellそれぞれで進めます。
API仕様(chat/completions)
POST /openai/deployments/{deployment_id}/chat/completions?api-version={api_version}
- リクエストヘッダー:
Content-Type
: application/json
(必須)
api-key
: {Azure OpenAI Service API Key}
(必須)
- リクエストボディ (JSON):
messages
: 配列 (必須)
role
: 文字列 (system
, user
, assistant
) (必須)
content
: 文字列 (メッセージ本文) (必須)
temperature
: 数値 (0.0 ~ 2.0, デフォルト1.0)
max_tokens
: 整数 (生成されるトークン数の上限)
stop
: 文字列配列 (生成を停止するシーケンス)
top_p
: 数値 (0.0 ~ 1.0, デフォルト1.0, temperatureと排他的)
presence_penalty
: 数値 (-2.0 ~ 2.0, デフォルト0.0, 既存のトピックに対するペナルティ)
frequency_penalty
: 数値 (-2.0 ~ 2.0, デフォルト0.0, 頻出するトークンに対するペナルティ)
seed
: 整数 (再現性を保証するシード値, system_fingerprint
と共に)
- レスポンスボディ (JSON):
id
: 文字列 (APIコールのID)
object
: 文字列 (chat.completion
)
created
: 整数 (Unixタイムスタンプ)
model
: 文字列 (使用されたモデル名)
choices
: 配列
index
: 整数
message
: オブジェクト
role
: 文字列 (assistant
)
content
: 文字列 (生成されたテキスト)
finish_reason
: 文字列 (stop
, length
, content_filter
など)
usage
: オブジェクト
prompt_tokens
: 整数 (プロンプトに使用されたトークン数)
completion_tokens
: 整数 (応答に使用されたトークン数)
total_tokens
: 整数 (合計トークン数)
system_fingerprint
: 文字列 (再現性情報, seed
とセット)
PowerShellによる実装
最小実装
Invoke-RestMethod
はHTTPリクエストとJSONパースを簡潔に扱えるため、最小実装に最適です。
# 設定値 (環境に応じて変更してください)
$resourceName = "your-openai-resource-name" # Azure OpenAIリソース名
$deploymentName = "your-deployment-name" # デプロイしたモデルの名前 (例: gpt-35-turbo)
$apiKey = "YOUR_AZURE_OPENAI_API_KEY" # Azure OpenAI APIキー
$apiVersion = "2024-02-15" # APIバージョン
$uri = "https://$resourceName.openai.azure.com/openai/deployments/$deploymentName/chat/completions?api-version=$apiVersion"
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey
}
$body = @{
messages = @(
@{ role = "system"; content = "あなたは有能なアシスタントです。" },
@{ role = "user"; content = "日本の首都はどこですか?" }
)
temperature = 0.7
max_tokens = 150
} | ConvertTo-Json -Depth 4 # -Depthでネストされたオブジェクトも適切にJSON化
Write-Host "Sending request to: $uri"
try {
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body
Write-Host "Response received."
Write-Host "Generated Text: $($response.choices[0].message.content)"
Write-Host "Total Tokens: $($response.usage.total_tokens)"
}
catch {
Write-Error "API call failed: $($_.Exception.Message)"
if ($_.Exception.Response) {
$errorBody = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorBody)
$errorResponseText = $reader.ReadToEnd()
$reader.Close()
Write-Error "Error Details: $errorResponseText"
}
}
堅牢化とベストプラクティス
実際の運用では、APIキーのセキュアな管理、エラーハンドリング、リトライメカニズムが不可欠です。
# 設定値 (環境変数からの取得を推奨)
# $env:AZURE_OPENAI_RESOURCE_NAME, $env:AZURE_OPENAI_DEPLOYMENT_NAME, $env:AZURE_OPENAI_API_KEY を設定してください
$resourceName = $env:AZURE_OPENAI_RESOURCE_NAME
$deploymentName = $env:AZURE_OPENAI_DEPLOYMENT_NAME
$apiKey = $env:AZURE_OPENAI_API_KEY
$apiVersion = "2024-02-15"
# 設定値のバリデーション
if (-not ($resourceName -and $deploymentName -and $apiKey)) {
Write-Error "環境変数 AZURE_OPENAI_RESOURCE_NAME, AZURE_OPENAI_DEPLOYMENT_NAME, AZURE_OPENAI_API_KEY が設定されていません。"
exit 1
}
$uri = "https://$resourceName.openai.azure.com/openai/deployments/$deploymentName/chat/completions?api-version=$apiVersion"
function Invoke-AzureOpenAIChatCompletion {
param(
[Parameter(Mandatory=$true)]
[string]$SystemPrompt,
[Parameter(Mandatory=$true)]
[string]$UserPrompt,
[Parameter(Mandatory=$false)]
[int]$MaxTokens = 150,
[Parameter(Mandatory=$false)]
[double]$Temperature = 0.7,
[Parameter(Mandatory=$false)]
[int]$MaxRetries = 5,
[Parameter(Mandatory=$false)]
[int]$InitialDelaySeconds = 1
)
$headers = @{
"Content-Type" = "application/json"
"api-key" = $apiKey
}
$body = @{
messages = @(
@{ role = "system"; content = $SystemPrompt },
@{ role = "user"; content = $UserPrompt }
)
temperature = $Temperature
max_tokens = $MaxTokens
} | ConvertTo-Json -Depth 4
for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
Write-Host "Attempt $attempt of $MaxRetries to call Azure OpenAI API..."
try {
$response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body -ContentType "application/json" -TimeoutSec 60
return $response.choices[0].message.content
}
catch {
$statusCode = $_.Exception.Response.StatusCode.value__
$errorMessage = $_.Exception.Message
Write-Warning "API call failed (Attempt $attempt): [$statusCode] $errorMessage"
if ($_.Exception.Response) {
$errorStream = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorStream)
$errorResponseText = $reader.ReadToEnd()
$reader.Close()
Write-Warning "Error Details: $errorResponseText"
# レート制限 (429) の場合は Retry-After ヘッダーをチェック
if ($statusCode -eq 429 -and $_.Exception.Response.Headers['Retry-After']) {
$retryAfter = [int]$_.Exception.Response.Headers['Retry-After']
Write-Warning "Rate limit hit. Retrying after $retryAfter seconds (from server)."
Start-Sleep -Seconds $retryAfter
continue
}
}
# 4xx系のクライアントエラーはリトライしない
if ($statusCode -ge 400 -and $statusCode -lt 500 -and $statusCode -ne 429) {
Write-Error "Client-side error ($statusCode). Aborting retries."
throw "Azure OpenAI API Client Error: $errorResponseText"
}
# それ以外のエラー (主に5xx系) やネットワークエラーは指数バックオフでリトライ
if ($attempt -lt $MaxRetries) {
$delay = $InitialDelaySeconds * [math]::Pow(2, $attempt - 1)
Write-Warning "Retrying in $delay seconds..."
Start-Sleep -Seconds $delay
} else {
Write-Error "Max retries reached. Failing."
throw "Azure OpenAI API Failed after multiple retries: $errorResponseText"
}
}
}
return $null # 全てのリトライが失敗した場合
}
# 使用例
try {
$result = Invoke-AzureOpenAIChatCompletion `
-SystemPrompt "あなたは優秀なデータ分析アシスタントです。ユーザーの質問に簡潔に答えてください。" `
-UserPrompt "2023年の日本のGDP成長率は?"
if ($result) {
Write-Host "AIの回答: $result"
} else {
Write-Error "AIからの回答を取得できませんでした。"
}
}
catch {
Write-Error "スクリプト全体でエラーが発生しました: $($_.Exception.Message)"
}
VBAによる実装
VBAでは、Microsoft WinHTTP Services
またはMicrosoft XML, v6.0
などのCOMオブジェクトを利用してHTTPリクエストを送信します。ここでは、広く利用可能なWinHttp.WinHttpRequest.5.1
オブジェクト(あるいはより新しいMSXML2.ServerXMLHTTP60
など)を使用します。
最小実装
' 参照設定は不要だが、IntelliSenseを有効にする場合は
' 「ツール」->「参照設定」で「Microsoft WinHTTP Services, version 5.1」にチェック
Sub CallAzureOpenAIApi_Minimal()
Dim objHTTP As Object ' WinHttp.WinHttpRequest.5.1
Dim sURL As String
Dim sAPIKey As String
Dim sRequestBody As String
Dim sResponse As String
'--- 設定値 (環境に応じて変更してください) ---
Const RESOURCE_NAME As String = "your-openai-resource-name" ' Azure OpenAIリソース名
Const DEPLOYMENT_NAME As String = "your-deployment-name" ' デプロイしたモデルの名前
sAPIKey = "YOUR_AZURE_OPENAI_API_KEY" ' Azure OpenAI APIキー
Const API_VERSION As String = "2024-02-15"
'-------------------------------------------
sURL = "https://" & RESOURCE_NAME & ".openai.azure.com/openai/deployments/" & _
DEPLOYMENT_NAME & "/chat/completions?api-version=" & API_VERSION
' JSONボディの組み立て
' VBAでJSONを直接構築するのは手間がかかるが、ここでは手動でシンプルに構成
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": ""あなたは有能なアシスタントです。""}," & _
"{""role"": ""user"", ""content"": ""日本の首都はどこですか?""}]," & _
"""temperature"": 0.7, ""max_tokens"": 150}"
Set objHTTP = CreateObject("WinHttp.WinHttpRequest.5.1") ' または "MSXML2.XMLHTTP"
With objHTTP
.Open "POST", sURL, False ' Falseで同期呼び出し
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", sAPIKey
.Send sRequestBody
If .Status = 200 Then
sResponse = .ResponseText
Debug.Print "Response Received: " & sResponse
' レスポンスJSONから必要な情報を抽出 (簡易版)
' 実際のアプリケーションではJSONパーサーの利用を検討
Dim jsonParts As Variant
jsonParts = Split(sResponse, """content"":""")
If UBound(jsonParts) > 0 Then
Dim contentPart As String
contentPart = Split(jsonParts(1), """")(0)
Debug.Print "Generated Text: " & contentPart
Else
Debug.Print "Content not found in response."
End If
Else
Debug.Print "API Call Failed. Status: " & .Status & ", StatusText: " & .StatusText
Debug.Print "Response Text: " & .ResponseText ' エラー詳細
End If
End With
Set objHTTP = Nothing
End Sub
堅牢化とベストプラクティス
VBAでの堅牢化は、エラーハンドリング、リトライ、タイムアウト設定、そしてクラスモジュールによる抽象化が中心になります。JSONの構築と解析には、VBA-JSONなどの外部ライブラリも選択肢に入りますが、ここでは外部ライブラリに極力依存しないという要件に沿って、文字列操作による簡易的なアプローチを示します。
VBAにおける64bit対応/PtrSafe/LongPtrについて:
VBAの64bit対応は主にDeclare Function
でWindows API(WinAPI)を直接呼び出す際に重要になります。ポインタやハンドルなど、メモリ上のアドレスを扱う引数や戻り値の型をLongPtr
に、そしてDeclare
ステートメントにPtrSafe
キーワードを追加することが必須です。
WinHttp.WinHttpRequest.5.1
のようなCOMオブジェクトを利用する場合、COMレイヤーが32bit/64bit間のアーキテクチャの違いを吸収するため、スクリプト内で直接PtrSafe
やLongPtr
を使用する場面は稀です。COMオブジェクトのメソッド引数にポインタを渡すような特殊なケースを除き、既存のCOMオブジェクトを利用する限りは、ほとんど意識する必要はありません。しかし、もしVBAからWinAPIを直接呼び出して高度なネットワーク操作や低レイヤー処理を行う場合は、これらのキーワードの理解と適用が不可欠です。本稿のスコープでは、COMオブジェクトによる高レベルAPI呼び出しに限定するため、PtrSafe
/LongPtr
の具体的なコード例は示しませんが、VBA開発者としては常にその存在と必要性を意識しておくべきです。
以下は、AOAI連携機能をクラスモジュールにカプセル化し、リトライロジックを追加した例です。
クラスモジュール cAzureOpenAIClient
' クラスモジュール名: cAzureOpenAIClient
' 参照設定: 不要 (CreateObjectを使用)
Private Const CLASS_NAME As String = "cAzureOpenAIClient"
Private pResourceName As String
Private pDeploymentName As String
Private pAPIKey As String
Private pAPIVersion As String
Private pMaxRetries As Long
Private pInitialDelaySeconds As Long
Private pRequestTimeoutSeconds As Long
'*****************************************************
' Public Properties
'*****************************************************
Public Property Let ResourceName(ByVal sName As String)
pResourceName = sName
End Property
Public Property Get ResourceName() As String
ResourceName = pResourceName
End Property
Public Property Let DeploymentName(ByVal sName As String)
pDeploymentName = sName
End Property
Public Property Get DeploymentName() As String
DeploymentName = pDeploymentName
End Property
Public Property Let APIKey(ByVal sKey As String)
pAPIKey = sKey
End Property
Public Property Get APIKey() As String
APIKey = pAPIKey
End Property
Public Property Let APIVersion(ByVal sVersion As String)
pAPIVersion = sVersion
End Property
Public Property Get APIVersion() As String
APIVersion = pAPIVersion
End Property
Public Property Let MaxRetries(ByVal lRetries As Long)
pMaxRetries = lRetries
End Property
Public Property Get MaxRetries() As Long
MaxRetries = pMaxRetries
End Property
Public Property Let InitialDelaySeconds(ByVal lDelay As Long)
pInitialDelaySeconds = lDelay
End Property
Public Property Get InitialDelaySeconds() As Long
InitialDelaySeconds = pInitialDelaySeconds
End Property
Public Property Let RequestTimeoutSeconds(ByVal lTimeout As Long)
pRequestTimeoutSeconds = lTimeout
End Property
Public Property Get RequestTimeoutSeconds() As Long
RequestTimeoutSeconds = pRequestTimeoutSeconds
End Property
'*****************************************************
' Initialize
'*****************************************************
Private Sub Class_Initialize()
' デフォルト値の設定
pAPIVersion = "2024-02-15"
pMaxRetries = 3
pInitialDelaySeconds = 2
pRequestTimeoutSeconds = 60
End Sub
'*****************************************************
' Public Methods
'*****************************************************
Public Function ChatCompletion(ByVal sSystemPrompt As String, _
ByVal sUserPrompt As String, _
Optional ByVal dTemperature As Double = 0.7, _
Optional ByVal lMaxTokens As Long = 150) As String
Dim objHTTP As Object ' WinHttp.WinHttpRequest.5.1
Dim sURL As String
Dim sRequestBody As String
Dim sResponse As String
Dim lAttempt As Long
Dim lDelay As Long
Dim lStatusCode As Long
On Error GoTo ErrorHandler
' 設定値のバリデーション
If pResourceName = "" Or pDeploymentName = "" Or pAPIKey = "" Then
Err.Raise vbObjectError + 1000, CLASS_NAME, "Required properties (ResourceName, DeploymentName, APIKey) are not set."
End If
sURL = "https://" & pResourceName & ".openai.azure.com/openai/deployments/" & _
pDeploymentName & "/chat/completions?api-version=" & pAPIVersion
' JSONボディの組み立て (簡易版、必要に応じてJSONパーサーを導入)
sRequestBody = "{""messages"": [{""role"": ""system"", ""content"": """ & EscapeJson(sSystemPrompt) & """}," & _
"{""role"": ""user"", ""content"": """ & EscapeJson(sUserPrompt) & """}]," & _
"""temperature"": " & Replace(CStr(dTemperature), ",", ".") & "," & _
"""max_tokens"": " & lMaxTokens & "}"
Set objHTTP = CreateObject("WinHttp.WinHttpRequest.5.1")
For lAttempt = 1 To pMaxRetries + 1 ' MaxRetries + 1回のリトライ試行
Debug.Print CLASS_NAME & ": Attempt " & lAttempt & " of " & (pMaxRetries + 1)
With objHTTP
.Open "POST", sURL, False ' 同期呼び出し
.SetRequestHeader "Content-Type", "application/json"
.SetRequestHeader "api-key", pAPIKey
.SetTimeouts pRequestTimeoutSeconds * 1000, pRequestTimeoutSeconds * 1000, pRequestTimeoutSeconds * 1000, pRequestTimeoutSeconds * 1000 ' resolve, connect, send, receive timeouts (ms)
On Error Resume Next ' Send中にエラーが発生する可能性もある
.Send sRequestBody
lStatusCode = .Status
sResponse = .ResponseText
On Error GoTo ErrorHandler
If lStatusCode = 200 Then
ChatCompletion = ExtractContentFromJson(sResponse)
Exit Function
Else
Debug.Print CLASS_NAME & ": API Call Failed (Attempt " & lAttempt & "). Status: " & lStatusCode & ", StatusText: " & .StatusText
Debug.Print CLASS_NAME & ": Error Response: " & sResponse
If lStatusCode >= 400 And lStatusCode < 500 And lStatusCode <> 429 Then
' クライアントサイドエラー (4xx系、レート制限429以外) はリトライしない
Err.Raise vbObjectError + 1001, CLASS_NAME, "Client-side API error: " & lStatusCode & " - " & sResponse
End If
If lAttempt <= pMaxRetries Then ' MaxRetries回まではリトライを試みる
lDelay = pInitialDelaySeconds * (2 ^ (lAttempt - 1)) ' 指数バックオフ
' Retry-Afterヘッダーがあればそちらを優先 (VBA WinHttpRequestでは直接取得がやや面倒なので簡易化)
' 実際には.GetResponseHeader("Retry-After")で取得を試みるべき
Debug.Print CLASS_NAME & ": Retrying in " & lDelay & " seconds..."
Application.Wait Now + TimeValue("00:00:" & lDelay)
DoEvents ' UIフリーズ防止
Else
' 最大リトライ回数を超えた
Err.Raise vbObjectError + 1002, CLASS_NAME, "Max retries exceeded. Last status: " & lStatusCode & " - " & sResponse
End If
End If
End With
Next lAttempt
' ここに到達した場合、エラーハンドラにジャンプしていなければnull/空文字列を返す
ChatCompletion = ""
Exit Function
ErrorHandler:
Debug.Print CLASS_NAME & ": An error occurred: " & Err.Description
ChatCompletion = "" ' エラー時は空文字列を返す
Err.Clear
End Function
' JSON文字列内の特殊文字をエスケープする簡易関数
Private Function EscapeJson(ByVal sText As String) As String
sText = Replace(sText, "\", "\\")
sText = Replace(sText, """", "\""")
sText = Replace(sText, Chr(8), "\b") ' Backspace
sText = Replace(sText, Chr(12), "\f") ' Form feed
sText = Replace(sText, Chr(10), "\n") ' Newline
sText = Replace(sText, Chr(13), "\r") ' Carriage return
sText = Replace(sText, Chr(9), "\t") ' Tab
EscapeJson = sText
End Function
' 応答JSONからcontentを抽出する簡易関数
' 堅牢な実装にはJSONパーサーの使用を推奨
Private Function ExtractContentFromJson(ByVal sJson As String) As String
Dim re As Object ' RegExp
Set re = CreateObject("VBScript.RegExp")
re.Pattern = """content"":\s*""([^""]*)"""
re.Global = False
If re.Test(sJson) Then
ExtractContentFromJson = re.Execute(sJson)(0).SubMatches(0)
Else
ExtractContentFromJson = ""
End If
Set re = Nothing
End Function
標準モジュールでの使用例
Sub TestAzureOpenAIClient()
Dim oAIClient As cAzureOpenAIClient
Dim sResult As String
'--- 環境変数から取得するなどのセキュアな方法を推奨 ---
Const AZURE_OPENAI_RESOURCE_NAME As String = "your-openai-resource-name"
Const AZURE_OPENAI_DEPLOYMENT_NAME As String = "your-deployment-name"
Const AZURE_OPENAI_API_KEY As String = "YOUR_AZURE_OPENAI_API_KEY"
'----------------------------------------------------
Set oAIClient = New cAzureOpenAIClient
With oAIClient
.ResourceName = AZURE_OPENAI_RESOURCE_NAME
.DeploymentName = AZURE_OPENAI_DEPLOYMENT_NAME
.APIKey = AZURE_OPENAI_API_KEY
.MaxRetries = 5 ' リトライ回数を設定
.InitialDelaySeconds = 3 ' 初期遅延秒数を設定
.RequestTimeoutSeconds = 90 ' リクエストタイムアウト秒数を設定
End With
On Error GoTo ErrorHandler
Debug.Print "Calling Azure OpenAI API..."
sResult = oAIClient.ChatCompletion("あなたは優秀なデータ分析アシスタントです。ユーザーの質問に簡潔に答えてください。", _
"2023年の日本のGDP成長率は?")
If sResult <> "" Then
Debug.Print "AIの回答: " & sResult
Else
Debug.Print "AIからの回答を取得できませんでした。詳細はデバッグウィンドウを確認してください。"
End If
Exit Sub
ErrorHandler:
Debug.Print "An error occurred in TestAzureOpenAIClient: " & Err.Description
MsgBox "エラーが発生しました: " & Err.Description, vbCritical
Set oAIClient = Nothing
End Sub
Azure OpenAI API連携フロー
graph TD
A["VBA/PowerShellスクリプト開始"] --> B("API設定読み込み: Resource, Deployment, Key, Version")
B --> C{"プロンプトとパラメータでリクエストボディを構築"}
C --> D("HTTPヘッダー設定: Content-Type, api-key")
D --> E("APIエンドポイントURL構築")
E --> F["HTTP POSTリクエスト送信"]
F -- ネットワークエラー/タイムアウト --> G{"リトライ判定?"}
F -- HTTPステータス 200 OK --> H("レスポンスJSON受信")
F -- HTTPステータス 4XX/5XX --> I{"エラー種別判定"}
G -- リトライ可能 --> J("指数バックオフ待機")
J --> F
G -- リトライ不可 --> K("処理失敗/エラー報告")
I -- 429 Too Many Requests --> L("Retry-Afterヘッダーチェック")
L -- 指定あり --> J
L -- 指定なし/その他リトライ可能なエラー --> J
I -- 4XXクライアントエラー(429以外) --> K
I -- 5XXサーバーエラー --> J
H --> M("レスポンスJSON解析: 生成テキスト、トークン数など抽出")
M --> N["結果の利用/表示"]
N --> O["スクリプト終了"]
K --> O
ベンチ/検証
AOAI連携コードの品質を担保するためには、単なる動作確認だけでなく、性能と堅牢性の検証が不可欠です。
- 機能テスト:
- 正常系: 短いプロンプト、長いプロンプト、複雑なプロンプトで期待通りの応答が得られるか。
- 異常系:
- 無効なAPIキー、存在しないデプロイ名、不正なJSONボディで
401
/404
/400
エラーが正しくハンドリングされるか。
- レート制限(意図的に短時間で多数リクエストを送信)時にリトライが機能し、最終的に応答を得られるか、または適切なエラーで終了するか。
- ネットワーク切断時やDNS解決失敗時にタイムアウト・リトライが働くか。
- 性能テスト:
- 応答時間: プロンプトの長さ、
max_tokens
の値、モデル(gpt-3.5-turbo
vs gpt-4
)の違いが応答時間にどう影響するかを計測。複数回実行して平均値を算出。
- トークン消費量: 同様のプロンプトで何度か実行し、
usage.total_tokens
が変動するかどうかを確認。
- 境界条件の確認:
max_tokens
を極端に小さな値(例: 1)や大きな値に設定した場合の挙動。
temperature
を0.0(決定的)や2.0(創造的)に設定した際の応答の変化。
- 非常に長いプロンプト(コンテキストウィンドウの最大値に近づける)を送信した場合の動作。
計測方法の簡略例:
- VBA:
Dim StartTime As Double
StartTime = Timer ' 秒単位の現在時刻
' API呼び出しコード
Dim EndTime As Double
EndTime = Timer
Debug.Print "処理時間: " & (EndTime - StartTime) & "秒"
- PowerShell:
$startTime = Get-Date
# API呼び出しコード
$endTime = Get-Date
$duration = $endTime - $startTime
Write-Host "処理時間: $($duration.TotalSeconds)秒"
応用例/代替案
応用例
- Excelでのレポート自動生成: VBAからAOAIを呼び出し、特定のデータセットに基づいてサマリーや洞察を生成し、Excelシートに直接書き込む。
- PowerShellによるログ分析: サーバーログやイベントログをPowerShellで収集・フィルタリングし、AOAIで異常検出やカテゴリ分類を行う。
- 社内Q&Aシステム: VBA/PowerShellスクリプトでユーザーの質問を受け付け、既存ドキュメントを参照させつつAOAIで回答を生成、Teamsなどへ通知する。
- 定型業務の自動化: 例えば、メールの件名や本文からキーワードを抽出し、それに基づいて次のアクション(タスク作成、ファイル分類など)をAIに判断させる。
代替案
- Python等の中間層の利用:
VBA/PowerShellで複雑なJSON操作や高度なエラーハンドリング、非同期処理を実装するのは困難が伴います。PythonにはOpenAIのSDKや豊富なライブラリが存在するため、VBA/PowerShellからPythonスクリプトを呼び出し、Python側でAOAI連携の全てを担わせるという方法も有効です。
- VBA:
Shell
関数でPythonスクリプトを実行し、標準出力/ファイル経由で結果を受け取る。
- PowerShell:
Start-Process
やpython.exe
を直接呼び出す。
- Azure Functions/Logic Appsなどの利用:
VBA/PowerShellから直接AOAIを呼び出すのではなく、Azure FunctionsやLogic Appsなどのサーバーレスサービスを間に挟むことで、認証情報の管理を一元化し、レート制限やリトライロジックをクラウド側で堅牢に実装できます。これによりVBA/PowerShell側のコードは非常にシンプルになります。
- データブリッジングツール:
Power Automate DesktopのようなRPAツールを利用し、AI連携部分をGUIで設定し、PowerShellやVBAと連携させるアプローチも可能です。
まとめ
本稿では、VBAおよびPowerShellという既存環境からAzure OpenAI ServiceのREST APIを連携させる手法について、その最小実装から堅牢化、そして内部動作や落とし穴まで踏み込んで解説しました。
- VBA:
WinHttp.WinHttpRequest.5.1
オブジェクトを使ったHTTP通信、簡易的なJSON構築・解析、クラスモジュールによる抽象化とエラー・リトライハンドリング。64bit環境におけるPtrSafe
/LongPtr
の要点についても言及しました。
- PowerShell:
Invoke-RestMethod
による簡潔なHTTPリクエスト、環境変数を用いたAPIキー管理、try/catch/finally
と指数バックオフを用いた堅牢なリトライロジック。
VBAやPowerShellは、手元のExcelファイルや既存システムとの連携において、依然として強力なツールです。これらにAzure OpenAI Serviceの力を組み合わせることで、従来手作業で行っていたテキスト処理や意思決定プロセスにAIの知見を導入し、業務の高度化・自動化を劇的に推進できる可能性を秘めています。
しかし、その実装にはAPIキーの厳重な管理、レート制限への対策、JSONの厳密な取り扱いといった細心の注意が求められます。本稿で紹介した堅牢化のポイントと運用チェックリストを参考に、セキュアで信頼性の高いAI連携システムを構築してください。
参考リンク
コメント