設定値 (環境に応じて変更してください)

EXCEL

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の特有仕様

  1. エンドポイント: AOAIのエンドポイントURLは、Azureリソース名、デプロイメント名、APIバージョンによって一意に決まります。 例: https://{resource_name}.openai.azure.com/openai/deployments/{deployment_id}/chat/completions?api-version={api_version} api-versionは非常に重要であり、省略するとAPI仕様の変更により予期せぬエラーや動作となる可能性があります。常に最新の安定版を指定しましょう。
  2. HTTPメソッド: テキスト生成などの場合は、データを送信するためPOSTメソッドを使用します。
  3. ヘッダー:
    • Content-Type: application/json: リクエストボディがJSON形式であることを示します。これは必須です。
    • api-key: {YOUR_API_KEY}: Azure OpenAI Serviceへの認証情報です。Authorization: Bearer {YOUR_API_KEY}形式でも可能ですが、AOAIではapi-keyヘッダーが推奨されます。
  4. リクエストボディ (JSON): APIに送信するデータ本体です。特にchat/completionsエンドポイントでは、messages配列が主要な要素となります。
    • messages: 会話の履歴を保持する配列。各要素はrolesystem, user, assistantのいずれか)とcontent(メッセージ本文)を持ちます。systemロールはAIの振る舞いを指示するプロンプト、userはユーザーからの入力、assistantはAIからの応答です。
    • temperature: 応答のランダム性を制御します。0.0(決定的)から2.0(創造的)の範囲で設定します。
    • max_tokens: 生成される応答の最大トークン数を指定します。
  5. レスポンスボディ (JSON): APIからの応答もJSON形式です。成功時には生成されたテキストや使用されたトークン数などが含まれます。エラー時にはエラーの詳細が返されます。
  6. 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-RestMethodWinHttpRequestのような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間のアーキテクチャの違いを吸収するため、スクリプト内で直接PtrSafeLongPtrを使用する場面は稀です。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)秒"
    

応用例/代替案

応用例

  1. Excelでのレポート自動生成: VBAからAOAIを呼び出し、特定のデータセットに基づいてサマリーや洞察を生成し、Excelシートに直接書き込む。
  2. PowerShellによるログ分析: サーバーログやイベントログをPowerShellで収集・フィルタリングし、AOAIで異常検出やカテゴリ分類を行う。
  3. 社内Q&Aシステム: VBA/PowerShellスクリプトでユーザーの質問を受け付け、既存ドキュメントを参照させつつAOAIで回答を生成、Teamsなどへ通知する。
  4. 定型業務の自動化: 例えば、メールの件名や本文からキーワードを抽出し、それに基づいて次のアクション(タスク作成、ファイル分類など)をAIに判断させる。

代替案

  1. Python等の中間層の利用: VBA/PowerShellで複雑なJSON操作や高度なエラーハンドリング、非同期処理を実装するのは困難が伴います。PythonにはOpenAIのSDKや豊富なライブラリが存在するため、VBA/PowerShellからPythonスクリプトを呼び出し、Python側でAOAI連携の全てを担わせるという方法も有効です。
    • VBA: Shell関数でPythonスクリプトを実行し、標準出力/ファイル経由で結果を受け取る。
    • PowerShell: Start-Processpython.exeを直接呼び出す。
  2. Azure Functions/Logic Appsなどの利用: VBA/PowerShellから直接AOAIを呼び出すのではなく、Azure FunctionsやLogic Appsなどのサーバーレスサービスを間に挟むことで、認証情報の管理を一元化し、レート制限やリトライロジックをクラウド側で堅牢に実装できます。これによりVBA/PowerShell側のコードは非常にシンプルになります。
  3. データブリッジングツール: 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連携システムを構築してください。

参考リンク

ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました