PowerShellスクリプト (.ps1)

PowerAutomate

VBA/PowerShellでAzure OpenAI API連携を極める:HTTP通信の深淵と堅牢化

導入(問題設定)

ビジネスの現場では、いまだにVBAやPowerShellスクリプトが多様な自動化、データ処理、レポート生成に活用されています。特にVBAはExcelやAccess、Outlookと密接に連携し、PowerShellはWindowsサーバー環境での管理タスクに不可欠です。これらの環境は、Pythonのような最新言語に比べて外部ライブラリの導入に制約がある、あるいはセキュリティポリシー上許可されないケースも少なくありません。

しかし、ChatGPTに代表される大規模言語モデル(LLM)の登場は、ビジネスプロセスの自動化と効率化に革命的な可能性をもたらしました。既存のVBA/PowerShell資産から、最先端のAI機能(テキスト生成、要約、翻訳、分類など)を活用したいというニーズは増大しています。

本記事では、VBAおよびPowerShellスクリプトからAzure OpenAI ServiceのREST APIを直接叩き、外部ライブラリに極力依存しない形でAI連携を実現する方法を解説します。単なるHowToに留まらず、HTTP通信の内部動作、JSONのハンドリング、エラー処理、セキュリティ、そして64bit環境への考慮といった、実運用で直面するであろう「濃く、マニアックな」論点に深く切り込みます。あなたの既存資産が、AIの力で新たな価値を生み出すための道筋を示すことが、本記事の狙いです。

理論の要点

Azure OpenAI Serviceは、OpenAIの強力なモデルをMicrosoft Azureのインフラ上で提供し、エンタープライズ向けのセキュリティ、コンプライアンス、信頼性を兼ね備えています。その核となるのが、HTTP REST APIを通じたアクセスです。

  1. エンドポイントと認証:

    • ベースURL: https://{your-resource-name}.openai.azure.com/openai/deployments/{your-deployment-name}/chat/completions?api-version=2023-05-15
      • {your-resource-name}: Azure OpenAIリソース名
      • {your-deployment-name}: デプロイしたモデルの名前(例: gpt-35-turbo, gpt-4
      • api-version: 必須パラメータ。最新の安定版を指定します。
    • 認証: APIキー認証が一般的です。HTTPヘッダーにapi-key: {your-api-key}を含めます。
    • HTTPメソッド: POST
  2. リクエストとレスポンスの形式 (JSON):

    • APIとの通信はJSON形式で行われます。
    • リクエストボディ:
      • messages: 会話の履歴を保持するオブジェクトの配列。
        • role: system, user, assistant のいずれか。
        • content: メッセージ本体。
      • model: (APIバージョンによっては不要、デプロイ名で指定するため)
      • max_tokens: 生成されるトークンの最大数。
      • temperature: 生成されるテキストのランダム性(0.0~2.0)。0に近いほど決定論的。
      • top_p: temperatureと類似の制御。
      • stop: 生成を停止する文字列の配列。
    • レスポンスボディ:
      • id, object, created, model: メタデータ。
      • choices: 生成されたテキストの候補の配列。
        • index: 候補のインデックス。
        • message: 生成されたメッセージオブジェクト。
          • role: assistant
          • content: 生成されたテキスト。
      • usage: トークン使用量 (prompt_tokens, completion_tokens, total_tokens)。
  3. VBAでのHTTP通信:

    • WinHttpRequestオブジェクト(WinHttp.WinHttpRequest.5.1)が最もモダンで推奨されます。SSL/TLS通信やタイムアウト設定が容易です。MSXML2.XMLHTTPは古い環境での互換性は高いですが、機能面で劣ります。
    • JSONの組み立てとパースは、VBAネイティブの文字列操作関数 (InStr, Mid, Replaceなど) で行うか、簡易的な外部ライブラリを検討することになります。本記事ではネイティブ機能にこだわります。
  4. PowerShellでのHTTP通信:

    • Invoke-RestMethodコマンドレットが最適です。HTTPリクエストの送信、JSONの自動シリアライズ/デシリアライズ、エラーハンドリングが非常に直感的です。
    • Invoke-WebRequestも同様の機能を提供しますが、こちらは詳細なレスポンス情報(ヘッダーなど)を取得するのに適しています。AI連携ではInvoke-RestMethodで十分です。

VBAとPowerShell、それぞれのアプローチにおけるHTTP通信とJSONハンドリングの境界条件を理解することが、堅牢な実装への第一歩となります。


Azure OpenAI API 主要パラメータ一覧 (Chat Completions)

| パラメータ名 | タイプ | 必須 | 説明 | 既定値 | 補足 | | messages | 配列 | messages を参照 | はい | チャットモデルへの入力。各オブジェクトはロールとコンテンツを持つ。 | なし | 配列の要素数には上限があるため、長大な会話履歴は適宜短縮する必要がある。 | | - role | 文字列 | messages を参照 | はい | system, user, assistant のいずれか。 | なし | systemはモデルの振る舞いを定義、userはユーザーの入力、assistantはモデルの応答を表す。 | | - content | 文字列 | messages を参照 | はい | ロールに対応するメッセージの内容。 | なし | | | temperature | 数値 | いいえ | 0.0~2.0の範囲で、生成される応答のランダム性を制御します。高い値ほど多様なテキストが生成されます。 | 1.0 | top_pと同時に設定した場合、片方が有効になり、もう一方は無視されることが多い。 | | top_p | 数値 | いいえ | 0.0~1.0の範囲で、temperatureと類似の制御を行います。確率の高いトークンから順に累積確率がtop_pを超えるまで選択します。 | 1.0 | | | n | 整数 | いいえ | 生成する応答の候補数。 | 1 | 生成数が増えるとトークン消費量も増大する。 | | stream | 真偽値 | いいえ | 応答をストリーミング形式で受け取るかどうか。 | false | trueの場合、応答が逐次返されるため、リアルタイム性が必要なチャットUIなどで有用。 | | stop | 文字列配列 | いいえ | 生成を停止する最大4つの文字列の配列。モデルはこれらの文字列が生成されると停止します。 | なし | 停止文字列は応答には含まれない。 | | max_tokens | 整数 | いいえ | 応答で生成されるトークンの最大数。 | 4096 | モデルの最大コンテキスト長(プロンプト+応答)を超えないように注意が必要。 | | presence_penalty | 数値 | いいえ | -2.0~2.0。トークンの新規性に対するペナルティ。高い値ほど既存のトピックから逸脱しやすくなります。 | 0.0 | | | frequency_penalty | 数値 | いいえ | -2.0~2.0。同じトークンを繰り返すことに対するペナルティ。高い値ほど繰り返されにくくなります。 | 0.0 | | | user | 文字列 | いいえ | エンドユーザーを識別するためのユニークID。レートリミット監視や悪用検知に利用されます。 | なし | 必須ではないが、推奨される。 |


実装(最小→堅牢化)

ここではVBAとPowerShell、それぞれでの実装コードを示します。

VBA (Excel VBA / Access VBA)

VBAでは、外部ライブラリに頼らずHTTP通信とJSON処理を行うため、WinHttpRequestオブジェクトを使い、JSONパースは文字列操作で実現します。

最小実装

シンプルにAPIを呼び出し、最初の応答contentを取得する例。エラー処理は最小限です。

' 標準モジュールに記述 (例: Module1)

Option Explicit

Private Const AZURE_OPENAI_RESOURCE_NAME As String = "your-openai-resource-name" ' Azure OpenAIリソース名
Private Const AZURE_OPENAI_DEPLOYMENT_NAME As String = "gpt-35-turbo-16k" ' デプロイしたモデル名 (例: gpt-35-turbo-16k)
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_AZURE_OPENAI_API_KEY" ' Azure OpenAI APIキー

' !!! 注意: 実際のAPIキーはコードに直接記述せず、よりセキュアな方法で管理してください !!!

Function CallAzureOpenAI(prompt As String) As String
    Dim req As Object ' WinHttpRequest object
    Dim url As String
    Dim jsonBody As String
    Dim responseText As String
    Dim result As String

    On Error GoTo ErrorHandler

    ' 1. URLの構築
    url = "https://" & AZURE_OPENAI_RESOURCE_NAME & ".openai.azure.com/openai/deployments/" & _
          AZURE_OPENAI_DEPLOYMENT_NAME & "/chat/completions?api-version=2023-05-15"

    ' 2. リクエストボディ (JSON) の構築
    ' 簡易的なJSONエスケープ (本番環境ではより堅牢な実装が必要)
    Dim escapedPrompt As String
    escapedPrompt = Replace(prompt, Chr(34), Chr(34) & Chr(34)) ' 二重引用符をエスケープ

    jsonBody = "{""messages"": [{""role"": ""user"", ""content"": """ & escapedPrompt & """}], ""max_tokens"": 500, ""temperature"": 0.7}"

    ' 3. WinHttpRequest オブジェクトの初期化
    Set req = CreateObject("WinHttp.WinHttpRequest.5.1")

    ' 4. HTTPリクエストのオープン
    req.Open "POST", url, False ' False: 同期呼び出し

    ' 5. ヘッダーの設定
    req.SetRequestHeader "Content-Type", "application/json"
    req.SetRequestHeader "api-key", AZURE_OPENAI_API_KEY

    ' 6. リクエストの送信
    req.Send jsonBody

    ' 7. レスポンスの受信と処理
    If req.Status = 200 Then
        responseText = req.ResponseText
        ' Debug.Print "Raw Response: " & responseText ' デバッグ用

        ' 8. JSONレスポンスからのcontent抽出 (簡易版)
        ' "content": "..." の値を取得
        result = ExtractJsonValue(responseText, "content")
        CallAzureOpenAI = result
    Else
        Debug.Print "Error Status: " & req.Status & " - " & req.StatusText
        Debug.Print "Error Response: " & req.ResponseText
        CallAzureOpenAI = "ERROR: API Call Failed. Status " & req.Status
    End If

ExitFunction:
    Set req = Nothing
    Exit Function

ErrorHandler:
    Debug.Print "An unexpected error occurred: " & Err.Description
    CallAzureOpenAI = "ERROR: " & Err.Description
    Resume ExitFunction
End Function

' 簡易的なJSON値抽出関数
' "key": "value" または "key": value の形式で、指定したキーの値を抽出する
Function ExtractJsonValue(jsonString As String, key As String) As String
    Dim startPos As Long
    Dim endPos As Long
    Dim searchPattern As String

    searchPattern = Chr(34) & key & Chr(34) & ":" ' 例: "content":

    startPos = InStr(jsonString, searchPattern)
    If startPos = 0 Then Exit Function

    startPos = startPos + Len(searchPattern) ' キーの直後から検索開始

    ' 値の開始が " (文字列) の場合
    If Mid(jsonString, startPos, 1) = Chr(34) Then
        startPos = startPos + 1 ' 最初の " の次から
        endPos = InStr(startPos, jsonString, Chr(34)) ' 次の " まで
        If endPos > startPos Then
            ExtractJsonValue = Mid(jsonString, startPos, endPos - startPos)
            ' エスケープされた二重引用符 \" の解除 (簡易的)
            ExtractJsonValue = Replace(ExtractJsonValue, Chr(92) & Chr(34), Chr(34))
        End If
    ' 値が文字列でない (数値、booleanなど) 場合
    Else
        endPos = InStr(startPos, jsonString, ",") ' 次のカンマまで
        Dim bracePos As Long
        bracePos = InStr(startPos, jsonString, "}") ' 次の閉じ括弧まで

        If bracePos > 0 And (endPos = 0 Or bracePos < endPos) Then
            endPos = bracePos ' カンマより先に } があればそちらを優先
        End If

        If endPos = 0 Then endPos = Len(jsonString) + 1 ' 最後まで

        If endPos > startPos Then
            ExtractJsonValue = Trim(Mid(jsonString, startPos, endPos - startPos))
        End If
    End If
End Function

' 使用例
Sub TestOpenAICall()
    Dim response As String
    response = CallAzureOpenAI("今日の天気について教えてください。")
    Debug.Print "AI Response: " & response
End Sub

堅牢化

実運用に耐えるVBAコードにするには、以下の点を強化します。

  1. エラー処理の強化: HTTPステータスコードの詳細なチェック、WinHttpRequestの内部エラーハンドリング。
  2. タイムアウト設定: ネットワーク遅延への対応。
  3. JSONパースの改善: ネストされたJSONへの対応(ここではchoices[0].message.contentに特化)。
  4. リトライロジック: 一時的なネットワークエラーやAPIレートリミットへの対応。
  5. セキュリティ: APIキーの直接記述を避ける。
  6. 64bit環境の考慮 (PtrSafe, LongPtr):
    • WinHttpRequestはCOMオブジェクトであるため、VBAのDeclareステートメントによる外部API呼び出しとは異なります。したがって、直接的なPtrSafeLongPtrの適用は不要です。
    • しかし、VBAでWindows APIを直接呼び出す場合(例: レジストリ操作、ファイルパス取得)には、Declare Function ... Lib "..." PtrSafe (... As LongPtr) のようにPtrSafeキーワードとポインタ型のLongPtrの使用が必須です。これは32bit/64bit環境でポインタのサイズが異なるためです。今回は該当しませんが、VBAでWinAPIを扱う際の重要な知識として認識してください。
' 標準モジュールに記述 (例: Module1)

Option Explicit

Private Const AZURE_OPENAI_RESOURCE_NAME As String = "your-openai-resource-name" ' Azure OpenAIリソース名
Private Const AZURE_OPENAI_DEPLOYMENT_NAME As String = "gpt-35-turbo-16k" ' デプロイしたモデル名
' !!! 注意: APIキーは直接コードに記述せず、環境変数やセキュアな設定ファイルから読み込むことを推奨 !!!
' ここでは説明のため、便宜的に定数としていますが、本番環境では避けてください。
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_AZURE_OPENAI_API_KEY"

Private Const MAX_RETRIES As Long = 3          ' 最大リトライ回数
Private Const RETRY_DELAY_SECONDS As Long = 5  ' リトライ間隔(秒)
Private Const REQUEST_TIMEOUT_SECONDS As Long = 60 ' リクエストタイムアウト(秒)

' Azure OpenAI API呼び出し関数(堅牢版)
Function CallAzureOpenAIFortified(prompt As String) As String
    Dim req As Object
    Dim url As String
    Dim jsonBody As String
    Dim responseText As String
    Dim result As String
    Dim retryCount As Long
    Dim httpStatus As Long

    On Error GoTo ErrorHandler

    url = "https://" & AZURE_OPENAI_RESOURCE_NAME & ".openai.azure.com/openai/deployments/" & _
          AZURE_OPENAI_DEPLOYMENT_NAME & "/chat/completions?api-version=2023-05-15"

    ' JSONボディの構築 (メッセージ配列とパラメータ)
    Dim escapedPrompt As String
    escapedPrompt = Replace(prompt, Chr(34), Chr(92) & Chr(34)) ' JSONとして正しくエスケープ
    ' 必要に応じて"system"メッセージや過去の会話履歴を追加
    jsonBody = "{""messages"": [{""role"": ""user"", ""content"": """ & escapedPrompt & """}], ""max_tokens"": 500, ""temperature"": 0.7}"

    For retryCount = 0 To MAX_RETRIES
        Set req = CreateObject("WinHttp.WinHttpRequest.5.1")
        req.Open "POST", url, False ' 同期呼び出し

        ' タイムアウト設定 (接続、送信、受信、解決)
        req.SetTimeouts REQUEST_TIMEOUT_SECONDS * 1000, _
                        REQUEST_TIMEOUT_SECONDS * 1000, _
                        REQUEST_TIMEOUT_SECONDS * 1000, _
                        REQUEST_TIMEOUT_SECONDS * 1000

        req.SetRequestHeader "Content-Type", "application/json"
        req.SetRequestHeader "api-key", AZURE_OPENAI_API_KEY ' 本番では環境変数等から取得

        On Error Resume Next ' エラー発生時に次の行へ進む
        req.Send jsonBody
        On Error GoTo ErrorHandler ' エラーハンドラを再度有効にする

        httpStatus = req.Status
        If httpStatus = 200 Then
            responseText = req.ResponseText
            result = ExtractNestedJsonValue(responseText, "choices", "message", "content")
            CallAzureOpenAIFortified = result
            GoTo ExitFunction ' 成功したらループを抜ける
        ElseIf httpStatus = 429 Then ' Too Many Requests (レートリミット)
            Debug.Print "API Rate Limit hit. Retrying in " & RETRY_DELAY_SECONDS & " seconds. (Retry " & retryCount + 1 & "/" & MAX_RETRIES & ")"
            If retryCount < MAX_RETRIES Then
                Application.Wait Now + TimeValue("00:00:" & RETRY_DELAY_SECONDS)
            Else
                Debug.Print "Max retries reached for 429 error."
                CallAzureOpenAIFortified = "ERROR: Rate limit exceeded after multiple retries. Response: " & req.ResponseText
                GoTo ExitFunction
            End If
        ElseIf httpStatus >= 400 And httpStatus < 500 Then ' クライアントエラー
            Debug.Print "Client Error (" & httpStatus & "): " & req.StatusText
            Debug.Print "Response: " & req.ResponseText
            CallAzureOpenAIFortified = "ERROR: Client Error " & httpStatus & " - " & req.ResponseText
            GoTo ExitFunction
        ElseIf httpStatus >= 500 Then ' サーバーエラー
            Debug.Print "Server Error (" & httpStatus & "): " & req.StatusText
            Debug.Print "Response: " & req.ResponseText
            If retryCount < MAX_RETRIES Then
                Debug.Print "Retrying on server error in " & RETRY_DELAY_SECONDS & " seconds. (Retry " & retryCount + 1 & "/" & MAX_RETRIES & ")"
                Application.Wait Now + TimeValue("00:00:" & RETRY_DELAY_SECONDS)
            Else
                Debug.Print "Max retries reached for server error."
                CallAzureOpenAIFortified = "ERROR: Server Error " & httpStatus & " after multiple retries. Response: " & req.ResponseText
                GoTo ExitFunction
            End If
        Else
            Debug.Print "Unknown HTTP Status: " & httpStatus & " - " & req.StatusText
            Debug.Print "Response: " & req.ResponseText
            CallAzureOpenAIFortified = "ERROR: Unknown Status " & httpStatus & ". Response: " & req.ResponseText
            GoTo ExitFunction
        End If
        Set req = Nothing ' リトライ前にオブジェクトを解放
    Next retryCount

ExitFunction:
    Set req = Nothing
    Exit Function

ErrorHandler:
    Debug.Print "An unhandled error occurred (VBA Error " & Err.Number & "): " & Err.Description
    CallAzureOpenAIFortified = "ERROR: VBA Runtime Error " & Err.Number & " - " & Err.Description
    Resume ExitFunction
End Function

' ネストされたJSON値抽出関数 (例: choices[0].message.content)
Function ExtractNestedJsonValue(jsonString As String, ParamArray keys() As Variant) As String
    Dim currentString As String
    Dim key As Variant
    Dim i As Long

    currentString = jsonString

    For i = LBound(keys) To UBound(keys)
        key = CStr(keys(i))

        ' 配列の最初の要素にアクセスする場合の簡易処理 (例: "choices"[0])
        If key = "choices" Then
            Dim choicesStart As Long
            choicesStart = InStr(currentString, Chr(34) & "choices" & Chr(34) & ":[")
            If choicesStart > 0 Then
                choicesStart = choicesStart + Len(Chr(34) & "choices" & Chr(34) & ":[") ' "[{" の直後
                Dim firstChoiceEnd As Long
                firstChoiceEnd = InStr(choicesStart, currentString, "}") ' 最初の "}" を探す

                ' 配列の最初の要素のオブジェクトを抽出
                Dim objStart As Long
                objStart = InStr(choicesStart, currentString, "{")
                If objStart > 0 And objStart < firstChoiceEnd Then
                    firstChoiceEnd = InStr(objStart, currentString, "}")
                    If firstChoiceEnd > objStart Then
                        currentString = Mid(currentString, objStart, firstChoiceEnd - objStart + 1)
                    Else
                        Exit Function ' 閉じ括弧が見つからない
                    End If
                Else
                    Exit Function ' 開き括弧が見つからない
                End If
            Else
                Exit Function ' "choices"配列が見つからない
            End If
        Else
            ' 通常のキー抽出
            Dim extractedValue As String
            extractedValue = ExtractJsonValue(currentString, key) ' 単一キー抽出関数を再利用
            If extractedValue = "" Then
                Exit Function ' キーが見つからなければ終了
            End If
            currentString = extractedValue ' 次のネストのために、抽出した値を現在の文字列とする (簡易的)
        End If
    Next i

    ExtractNestedJsonValue = currentString ' 最終的に抽出された値
End Function


' 使用例
Sub TestOpenAICallFortified()
    Dim response As String
    response = CallAzureOpenAIFortified("VBAで堅牢なAI連携をするためのポイントを3つ教えてください。")
    Debug.Print "AI Response (Fortified): " & response
End Sub

JSONパースの限界と補足: VBAの文字列操作のみで任意のネストレベルのJSONを完全にパースするのは非常に困難です。上記のExtractNestedJsonValue関数は、特定のパス(例: choices[0].message.content)に特化し、最初の要素に限定する非常に簡易的な実装です。より複雑なJSON構造を扱う場合、VBA-JSONのようなオープンソースライブラリの導入を検討するか、PowerShellなどの別の言語で処理するのが現実的です。

失敗例 → 原因 → 対処 (VBA)

  • 失敗例: TestOpenAICallを実行すると、AI Response: ERROR: API Call Failed. Status 401 と表示され、APIキーが正しいはずなのにcontentが取得できない。
  • 原因: APIキーの誤り、またはAPIキーが期限切れ/無効化されている可能性があります。401 Unauthorizedは認証失敗を意味します。また、リソース名やデプロイ名にタイプミスがある場合も同様のエラーが発生します。
  • 対処:
    1. AzureポータルでAPIキーが正しいことを再確認し、最新のキーに更新します。
    2. Azure OpenAIリソース名およびデプロイ名が正確であることを確認します。
    3. Debug.Print "API Key: " & AZURE_OPENAI_API_KEY のように、実際にコードが使用しているキーを出力して目視確認します。
    4. VBAのWinHttpRequestはOSのSSL/TLS設定に依存します。古いWindows環境ではTLS 1.2が無効になっている場合があり、これが原因でHTTPS通信が失敗することがあります。この場合、レジストリ設定でTLS 1.2を有効化する必要があります。

PowerShell

PowerShellでは、Invoke-RestMethodコマンドレットがHTTP通信とJSON処理を強力にサポートします。

最小実装

# PowerShellスクリプト (.ps1)

# --- 設定変数 ---
$azureOpenAIResourceName = "your-openai-resource-name" # Azure OpenAIリソース名
$azureOpenAIDeploymentName = "gpt-35-turbo-16k"        # デプロイしたモデル名
# !!! 注意: 実際のAPIキーはスクリプトに直接記述せず、よりセキュアな方法で管理してください !!!
$azureOpenAIAPIKey = "YOUR_AZURE_OPENAI_API_KEY"

# Azure OpenAI API呼び出し関数
function Call-AzureOpenAI {
    param(
        [string]$Prompt
    )

    $url = "https://$azureOpenAIResourceName.openai.azure.com/openai/deployments/$azureOpenAIDeploymentName/chat/completions?api-version=2023-05-15"

    $headers = @{
        "Content-Type" = "application/json"
        "api-key"      = $azureOpenAIAPIKey
    }

    $body = @{
        messages = @(
            @{
                role    = "user"
                content = $Prompt
            }
        )
        max_tokens = 500
        temperature = 0.7
    } | ConvertTo-Json

    try {
        $response = Invoke-RestMethod -Uri $url -Method Post -Headers $headers -Body $body
        # Invoke-RestMethod は自動的にJSONをPowerShellオブジェクトに変換する
        $response.choices[0].message.content
    }
    catch {
        Write-Error "API Call Failed: $($_.Exception.Message)"
        Write-Error "Error Details: $($_.Exception.Response.Content)" # 詳細なエラー応答
        return $null
    }
}

# --- 使用例 ---
$aiResponse = Call-AzureOpenAI -Prompt "PowerShellスクリプトでAzure OpenAIを使う利点は何ですか?"
if ($aiResponse) {
    Write-Host "AI Response: $aiResponse"
} else {
    Write-Warning "AI応答を取得できませんでした。"
}

堅牢化

PowerShellの堅牢化では、try/catchによるエラーハンドリング、リトライロジック、セキュリティ強化が中心となります。

# PowerShellスクリプト (.ps1)

# --- 設定変数 ---
$azureOpenAIResourceName = "your-openai-resource-name"
$azureOpenAIDeploymentName = "gpt-35-turbo-16k"

# APIキーのセキュアな管理 (推奨)
# 環境変数から取得する場合
# $azureOpenAIAPIKey = $env:AZURE_OPENAI_API_KEY
# または、SecureStringとしてファイルに保存し、実行時に読み込む
# (初回実行時にセキュアな文字列を作成して保存)
# Read-Host -AsSecureString "Enter Azure OpenAI API Key" | ConvertFrom-SecureString | Out-File C:\Temp\openai_key.txt -Encoding UTF8
# (実行時に読み込み)
$secureApiKey = Get-Content C:\Temp\openai_key.txt | ConvertTo-SecureString
$azureOpenAIAPIKey = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureApiKey))

# --- 定数 ---
$MaxRetries = 3
$RetryDelaySeconds = 5 # リトライ間隔
$RequestTimeoutSeconds = 60 # タイムアウト

# Azure OpenAI API呼び出し関数(堅牢版)
function Invoke-AzureOpenAIChatCompletion {
    param(
        [Parameter(Mandatory=$true)]
        [string]$Prompt,
        [int]$MaxTokens = 500,
        [double]$Temperature = 0.7,
        [string]$SystemMessage = "You are a helpful assistant."
    )

    $url = "https://$azureOpenAIResourceName.openai.azure.com/openai/deployments/$azureOpenAIDeploymentName/chat/completions?api-version=2023-05-15"

    $headers = @{
        "Content-Type" = "application/json"
        "api-key"      = $azureOpenAIAPIKey # セキュアな方法で取得したキーを使用
    }

    $messages = @()
    if ($SystemMessage) {
        $messages += @{ role = "system"; content = $SystemMessage }
    }
    $messages += @{ role = "user"; content = $Prompt }

    $body = @{
        messages    = $messages
        max_tokens  = $MaxTokens
        temperature = $Temperature
    } | ConvertTo-Json -Depth 5 # -Depth でネストの深さを調整

    $retryCount = 0
    while ($retryCount -le $MaxRetries) {
        try {
            Write-Verbose "Calling Azure OpenAI API (Retry $($retryCount+1)/$($MaxRetries+1))" -Verbose

            $response = Invoke-RestMethod -Uri $url `
                                        -Method Post `
                                        -Headers $headers `
                                        -Body $body `
                                        -TimeoutSec $RequestTimeoutSeconds `
                                        -ErrorAction Stop # エラー発生時に即座にcatchブロックへ

            return $response.choices[0].message.content
        }
        catch {
            $errorMessage = $_.Exception.Message
            $statusCode = $_.Exception.Response.StatusCode.value__ # HTTPステータスコード

            Write-Warning "API Call Failed (Status: $statusCode): $errorMessage"
            Write-Verbose "Full Error Details: $($_.Exception | Select-Object * | Format-List)" -Verbose

            # HTTP 429 (Too Many Requests) または 5xx (Server Errors) の場合はリトライ
            if ($statusCode -eq 429 -or $statusCode -ge 500 -and $statusCode -lt 600) {
                if ($retryCount -lt $MaxRetries) {
                    Write-Warning "Retrying in $($RetryDelaySeconds) seconds..."
                    Start-Sleep -Seconds $RetryDelaySeconds
                    $retryCount++
                } else {
                    Write-Error "Max retries ($MaxRetries) reached for status $statusCode. Giving up."
                    return $null
                }
            } else {
                # その他のエラー (4xxクライアントエラーなど) はリトライせず終了
                Write-Error "Non-retryable error ($statusCode). Aborting."
                return $null
            }
        }
    }
    return $null # 全てのリトライが失敗した場合
}

# --- 使用例 ---
$aiResponseFortified = Invoke-AzureOpenAIChatCompletion `
    -Prompt "PowerShellでAPIキーを安全に扱う方法について詳しく教えてください。" `
    -SystemMessage "あなたはセキュリティの専門家です。" `
    -MaxTokens 800

if ($aiResponseFortified) {
    Write-Host "`nAI Response (Fortified):`n$aiResponseFortified"
} else {
    Write-Warning "`nAI応答を取得できませんでした (堅牢版)。"
}

失敗例 → 原因 → 対処 (PowerShell)

  • 失敗例: Invoke-AzureOpenAIChatCompletionを実行すると、Invoke-RestMethod : The remote server returned an error: (400) Bad Request. のようなエラーが発生する。
  • 原因: 400 Bad Requestは、リクエストボディのJSON形式が不正であるか、必須パラメータが不足している場合に発生することが多いです。特に、PowerShellのConvertTo-Jsonが期待通りに動作していない、あるいはプロンプト内の特殊文字がJSONを破壊している可能性があります。
    • PowerShellのバージョンが古いとConvertTo-Json -Depthが使えない、ConvertFrom-SecureStringのエンコーディングが異なる、などの問題も考えられます。
  • 対処:
    1. $body変数の中身をWrite-Host $bodyで出力し、JSONlintなどのオンラインツールで形式が正しいか検証します。特にクォーテーションやカンマの抜けがないか確認。
    2. プロンプト内の特殊文字(改行、タブ、二重引用符など)が正しくエスケープされているか確認します。ConvertTo-Jsonは通常これらを処理しますが、意図しない挙動の場合があります。
    3. Write-Host "URL: $url"$headers | Format-List で送信されるURIとヘッダーも確認し、間違いがないかチェックします。
    4. Invoke-RestMethodErrorActionSilentlyContinueなどに変更し、$Error変数の中身を詳細に確認することで、より具体的なエラーメッセージが得られる場合があります。$_.Exception.Response.Contentを解析すると、Azure OpenAIからの詳細なエラーJSONが得られることがあります。

API呼び出し処理フロー (Mermaid)

Azure OpenAI APIへの一般的な呼び出しフローを視覚化します。

graph TD
    A["VBA/PowerShellスクリプト開始"] --> B{"APIキーと設定ロード"};
    B --> C["リクエストURL構築"];
    C --> D["リクエストヘッダー設定 (api-key, Content-Type)"];
    D --> E["JSONリクエストボディ構築"];
    E --> F{"HTTP POSTリクエスト送信"};
    F -- HTTPステータス 200 OK --> G["JSONレスポンス受信"];
    G --> H["JSONレスポンスパース (choices[0"].message.content)];
    H --> I["結果利用"];
    G -- HTTPステータス 429 Too Many Requests --> J{"リトライ必要?"};
    J -- はい --> K["指定時間待機"];
    K --> F;
    J -- いいえ --> L["エラーログ記録 & 処理終了"];
    F -- HTTPステータス 4xx/5xx エラー --> J;
    F -- ネットワークエラー/タイムアウト --> J;
    I --> M["スクリプト終了"];
    L --> M;

ベンチ/検証

実装したコードの性能や信頼性を確認するための観点です。

  • 応答時間計測:
    • VBA: Timer関数でAPI呼び出し前後の時刻を記録し、差分を測定します。
    • PowerShell: Measure-Command { Invoke-AzureOpenAIChatCompletion ... } を使用して、API呼び出しにかかる時間を簡単に測定できます。
    • 異なるプロンプト長、max_tokens値、temperature値で複数回実行し、平均応答時間と変動を把握します。
  • エラーハンドリングテスト:
    • 意図的に間違ったAPIキーやリソース名を設定し、401 Unauthorizedエラーが適切に捕捉されるか確認します。
    • 無効なJSONを送信し、400 Bad Requestが処理されるか確認します。
    • 短時間に連続してAPIを呼び出し、レートリミット(429 Too Many Requests)発生時のリトライロジックが機能するか確認します。
    • ネットワークを一時的に切断し、タイムアウトやネットワークエラーが処理されるか確認します。
  • JSONパースの正確性:
    • APIからのレスポンスをそのまま出力し、手動でパースした結果と一致するか検証します。
    • 特にVBAの文字列操作によるパースは、エスケープ文字(\", \nなど)や特殊文字(非ASCII文字)が含まれる場合に正確に処理できるか慎重に確認が必要です。
  • トークン使用量の確認:
    • APIレスポンスに含まれるusage情報 (prompt_tokens, completion_tokens, total_tokens) をログに出力し、想定通りのトークンが消費されているか監視します。これはコスト管理に直結します。
  • 環境差異:
    • 複数のOSバージョン(Windows 10/11, Windows Server 2016/2019/2022)、PowerShellのバージョン(5.1/7+)、VBAが動作するOfficeのバージョン(Excel 2016/2019/365)でテストし、予期せぬ互換性問題がないか確認します。特にVBAのWinHttpRequestはOSのTLS設定に大きく影響されます。

応用例/代替案

応用例

  • Excel/CSVデータの要約・分類: Excelシート上の大量のテキストデータをAzure OpenAIに渡し、要約やカテゴリ分類を行い、結果を別の列に書き戻す。
  • 定型メール文案生成: 特定の情報をプロンプトとして渡し、ビジネスメールのドラフトを自動生成する。
  • 議事録からのタスク抽出: 会議の議事録(テキスト)から、決定事項や担当者、期限などのタスクを抽出し、Excelのタスクリストに書き出す。
  • データクレンジング・正規化: 不整合なテキストデータを特定の形式に正規化するよう指示し、一貫性のあるデータに変換する。
  • 簡易チャットボット: ExcelのユーザーフォームやPowerShellのコンソールUIを通じて、簡単な対話インターフェースを構築する。

代替案

VBA/PowerShellでの直接的なAPI連携は、既存資産との親和性や外部ライブラリ制約への対応という点で強みがありますが、より高度な機能や大規模なシステム統合には、以下の選択肢も検討すべきです。

  • Python + openaiライブラリ:
    • 最も推奨される方法です。OpenAI/Azure OpenAIが公式で提供するSDKがあり、非常に扱いやすく、豊富な機能(ストリーミング、非同期処理、Embeddingなど)をサポートします。JSONの処理も容易です。
    • デメリットは、Python実行環境の準備と、VBA/PowerShellからの連携(Pythonスクリプトの呼び出し)が必要になる点です。
  • C# / .NET + HttpClient / Azure.AI.OpenAIライブラリ:
    • .NET環境での開発には強力な選択肢です。VBA/PowerShellと同様にCOM/.NET連携も可能ですが、別途.NETアプリケーションとして構築するのが一般的です。Azure.AI.OpenAIクライアントライブラリを利用すれば、低レイヤーのHTTP通信を意識せずに利用できます。
  • Azure Logic Apps / Power Automate:
    • コードを書かずにGUIベースでワークフローを構築できます。Azure OpenAI Serviceのコネクタが用意されており、他のAzureサービスやSaaSアプリケーションとの連携が容易です。定型的な業務自動化に向きますが、柔軟性や複雑なロジックの実装には限界があります。
  • Web API Gateway 経由:
    • VBA/PowerShellから直接Azure OpenAIを叩くのではなく、中間に専用のWeb API(例: Azure Functions, ASP.NET Core Web API)を構築し、そこをVBA/PowerShellから呼び出す構成です。APIキーの管理を一元化し、より複雑なロジックや高度なセキュリティ対策を中間層で実現できます。

VBA/PowerShellで実現する価値は、あくまで「既存の業務プロセスの中に、最小限の変更でAIを組み込む」点にあることを忘れてはなりません。

まとめ

本記事では、VBAおよびPowerShellスクリプトからAzure OpenAI ServiceのREST APIを直接連携させる方法を、最小実装から堅牢化まで踏み込んで解説しました。

  • VBAではWinHttpRequestオブジェクトとネイティブの文字列操作を駆使し、外部ライブラリに依存せずにHTTP通信とJSON処理を行う手法を学びました。特に64bit環境におけるPtrSafeLongPtrの概念は、直接的な利用は少ないものの、VBAでWindows APIを扱う際の重要な知識として触れました。
  • PowerShellではInvoke-RestMethodコマンドレットの強力な機能を活用し、JSONの自動処理と直感的なエラーハンドリング、セキュリティ強化のポイントを解説しました。
  • いずれの言語においても、APIキーのセキュアな管理、詳細なエラー処理、タイムアウト設定、そしてリトライロジックの実装が、実運用における堅牢性を確保する上で不可欠であることを強調しました。

これらの知識と技術を活用することで、あなたの手元のVBAやPowerShellスクリプトが、最先端のAI機能を取り込み、ビジネスの自動化と効率化を次のレベルへと押し上げるでしょう。

運用チェックリスト

Azure OpenAI API連携を運用する上で、以下の点を確認・定期的に見直しましょう。

  • APIキー管理:
    • APIキーはコードに直接記述せず、環境変数、SecureString(PowerShell)、または安全な設定ファイルから読み込んでいますか?
    • APIキーの有効期限、アクセス権限は適切に管理されていますか? 定期的なローテーションを検討していますか?
  • エラーハンドリング:
    • 4xx系クライアントエラーと5xx系サーバーエラー、およびネットワークエラーを適切に捕捉し、ユーザーまたは管理者への通知、ログ記録を行っていますか?
    • 429 Too Many Requests(レートリミット)発生時に、リトライロジックが正しく機能し、無限ループに陥らないようになっていますか?
  • パフォーマンスとタイムアウト:
    • API呼び出しのタイムアウト値は、ネットワーク状況やAPIの応答速度に合わせて適切に設定されていますか?
    • 長時間の処理が必要なプロンプトの場合、同期呼び出しがUIフリーズなどの問題を引き起こしていませんか? 必要に応じて非同期処理やバックグラウンド実行を検討していますか?
  • トークンとコスト管理:
    • max_tokens設定は、必要な応答の長さに応じて最適化されていますか? 不要に大きな値になっていませんか?
    • usage情報(消費トークン数)をログに記録し、コストのモニタリングを行っていますか?
  • プロンプトエンジニアリング:
    • 利用しているプロンプトは、期待する応答を得るために効果的に設計されていますか?(System Messageの活用、few-shot学習など)
    • プロンプトのバージョン管理やテンプレート化を行っていますか?
  • セキュリティとデータプライバシー:
    • 機密情報や個人情報をAPIに送信する際のデータ保護対策は講じられていますか? (Azure OpenAI Serviceはデータ保護がなされていますが、アプリケーション側での考慮も必要)
  • VBA環境固有:
    • 実行環境のWindows OSでTLS 1.2が有効になっていることを確認していますか? 古い環境ではレジストリ設定が必要です。
    • VBAのWinHttpRequestオブジェクトが、システムで利用可能であることを確認していますか?
  • PowerShell環境固有:
    • PowerShellのバージョンは、利用しているコマンドレットの機能要件を満たしていますか? (例: ConvertTo-Json -Depth)
    • Invoke-RestMethodがプロキシ環境下で適切に動作するよう設定されていますか?

参考リンク

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

コメント

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