— 設定値 (ハードコード) —

EXCEL

VBA/PowerShellからAzure OpenAI APIを極める:RESTの深淵と64bit対応

導入(問題設定)

レガシーなVBAやPowerShellスクリプトが跋扈する企業システム。日々、定型業務の自動化やデータ処理に汗を流す中で、「このルーティンワーク、もっと賢くできないか?」「この大量のテキストデータから、サクッと要約や分類を抽出できないか?」と、一度は頭をよぎったことがあるのではないでしょうか。その答えの一つが、Azure OpenAI Serviceです。

しかし、「VBAやPowerShellから、どうやってモダンなREST APIを叩けばいいのか?」「JSONって何?」「認証ってどうやるの?」といった疑問にぶつかることもあるでしょう。本記事では、そうした疑問に対し、単なるHowToで終わらせず、その内部動作や境界条件、そして実務における落とし穴まで踏み込み、VBAとPowerShellの両方からAzure OpenAI ServiceのREST APIを連携させる方法を、最小実装から堅牢化まで段階的に解説します。既存資産を最大限に活用し、あなたの業務にLLMの知能を注入する、その第一歩を踏み出しましょう。

理論の要点

Azure OpenAI Serviceへの連携は、基本的にHTTPSを介したREST APIコールによって行われます。ここで核となるのは、HTTPリクエストの構造とJSON形式のデータ交換です。

1. Azure OpenAI Serviceの基本

  • OpenAI APIとの違い: Azure OpenAI Serviceは、OpenAIが提供するAPIをMicrosoft Azureのインフラ上で提供するものです。セキュリティ、コンプライアンス、プライベートネットワークでの利用、データプライバシーなどの点で企業利用に適しています。利用にはAzureサブスクリプションとサービスへのアクセス許可が必要です。
  • デプロイ: Azure OpenAI Serviceでは、利用したいモデル(例: gpt-3.5-turbo, gpt-4)を特定のリージョンに「デプロイ」する必要があります。このデプロイされたリソースが、APIエンドポイントのURLの一部となります。
  • APIキー認証: APIリクエストの認証には、デプロイされたリソースのAPIキーを使用します。これは通常、HTTPヘッダーの api-key または Authorization (Bearerトークン形式) で送信されます。本記事では api-key ヘッダーを使用します。

2. REST APIの構造

Azure OpenAI ServiceのAPIは、標準的なRESTの原則に従います。 * HTTPメソッド: テキスト生成などのリクエストは、通常 POST メソッドを使用します。 * エンドポイント: https://<your-resource-name>.openai.azure.com/openai/deployments/<your-deployment-name>/chat/completions?api-version=2023-07-01-preview のような形式です。 * <your-resource-name>: Azure OpenAIリソース名 * <your-deployment-name>: デプロイしたモデル名(例: gpt-35-turbo) * api-version: 必須のクエリパラメータ。APIのバージョンを指定。 * ヘッダー: * Content-Type: application/json: リクエストボディがJSON形式であることを示します。 * api-key: <your-api-key>: 認証情報。 * リクエストボディ (JSON): POST リクエストの本体として、要求する処理の内容をJSON形式で記述します。特に chat/completions エンドポイントでは、messages 配列が重要です。

Chat Completions APIのリクエストボディ(抜粋)

プロパティ名 必須 説明
messages 配列 Yes 会話の履歴を構成するメッセージオブジェクトの配列。各メッセージは rolecontent を持ちます。
messages[].role 文字列 Yes メッセージの送信者。system, user, assistant, tool のいずれか。
system: 全体的な指示、振る舞いの定義に使用。
user: ユーザーからの質問や入力。
assistant: モデルの応答。
messages[].content 文字列 Yes メッセージの実際のテキスト内容。
temperature 数値 No 0から2までのサンプリング温度。高い値ほど多様な出力、低い値ほど決定論的な出力になります(デフォルト: 1)。
max_tokens 整数 No 生成される最大トークン数。入力トークンと出力トークンの合計がモデルのコンテキストウィンドウを超えないように注意が必要です。長い応答が必要な場合に設定します。
top_p 数値 No 核サンプリングパラメータ。temperature と組み合わせて、トークン選択の多様性を制御します(デフォルト: 1)。
stream ブール値 No true に設定すると、サーバーはイベントストリーム形式で部分的なメッセージを送信します。リアルタイム応答が求められる場合に有効です(デフォルト: false)。

3. レスポンスボディ (JSON)

モデルからの応答もJSON形式で返されます。特に choices 配列の中に生成されたテキストが含まれます。

Chat Completions APIのレスポンスボディ(抜粋)

プロパティ名 説明
id 文字列 チャット完了応答の一意の識別子。
object 文字列 chat.completion となります。
created 整数 応答が生成されたUnixタイムスタンプ。
model 文字列 使用されたモデルのID。
choices 配列 完了候補の配列。各候補は message オブジェクトと finish_reason を含みます。
choices[].index 整数 choices 配列内での候補のインデックス。
choices[].message オブジェクト モデルによって生成されたメッセージ。rolecontent を含みます。
choices[].message.role 文字列 assistant となります。
choices[].message.content 文字列 モデルが生成したテキスト応答。
choices[].finish_reason 文字列 モデルが応答を停止した理由 (stop, length, content_filter, tool_calls)。
usage オブジェクト リクエストで使用されたトークン数 (prompt_tokens, completion_tokens, total_tokens) に関する情報。料金計算に重要です。

4. VBAでのHTTP通信: WinHttpRequestオブジェクト

VBAでHTTPリクエストを送信する最も一般的な方法は、MSXML2.XMLHTTPまたはMSXML2.ServerXMLHTTP、あるいはWinHttpRequest.WinHttpRequestオブジェクト(Microsoft WinHTTP Services参照設定が必要)を使用することです。これらはCOMコンポーネントであり、Windows OSの機能を利用してHTTP通信を行います。

  • WinHttpRequest: WinHTTP Services はIISなどのサーバー側のWebサービス開発に利用されることが多く、クライアント側の WinInet ベースの XMLHTTP よりも低レベルで柔軟な制御が可能です。特にTLS 1.2などの最新のセキュリティプロトコルへの対応がより確実です。
  • 64bit対応とPtrSafe/LongPtr: WinHttpRequestオブジェクト自体はCOMコンポーネントであるため、VBAコード内で直接 Declare PtrSafe Function を記述する必要は通常ありません。OSが提供するCOMランタイムが32bit/64bit間の互換性を吸収します。しかし、もしVBAから他のWindows API(例: GetPrivateProfileStringなど)を直接呼び出す場合は、64bit環境でポインタを正しく扱うために PtrSafe キーワードと LongPtr 型が必須となります。これは特に、APIの引数や戻り値がポインタ(メモリ上のアドレス)を扱う場合に重要です。

5. PowerShellでのHTTP通信: Invoke-RestMethod

PowerShellでは、Invoke-RestMethodコマンドレットがHTTPリクエストを送信するための最も高機能で便利な方法です。

  • Invoke-RestMethod: HTTPレスポンスを自動的にPowerShellオブジェクトに変換してくれます。JSONレスポンスであれば、ハッシュテーブルやPSCustomObjectとして直接アクセスできるため、JSONパースの手間を大幅に削減できます。内部的には.NETのSystem.Net.WebRequestクラスを利用しています。
  • Invoke-WebRequestとの違い: Invoke-WebRequestは生のレスポンス(HTML、JSON文字列など)を返しますが、Invoke-RestMethodはデータ形式を解釈し、PowerShellオブジェクトとして返します。API連携においては、後者の方が圧倒的に使いやすいです。

graph TD
    A["クライアントスクリプト開始"] --> B{"VBA or PowerShell?"};

    subgraph VBA Path
        B -- VBA --> C_VBA["WinHttpRequestオブジェクト生成"];
        C_VBA --> D_VBA["URL/ヘッダー/ボディ設定"];
        D_VBA --> E_VBA["Sendメソッド実行 (同期/非同期)"];
        E_VBA -- HTTP POST --> F_Azure["Azure OpenAI Service"];
        F_Azure -- JSONレスポンス --> G_VBA["レスポンス受信"];
        G_VBA --> H_VBA["Statusコード確認"];
        H_VBA -- 200 OK --> I_VBA["JSON文字列を手動パース"];
        H_VBA -- 4xx/5xx --> J_VBA["エラー処理"];
        I_VBA --> K_VBA["結果利用"];
        J_VBA --> K_VBA;
    end

    subgraph PowerShell Path
        B -- PowerShell --> C_PS["Invoke-RestMethod準備"];
        C_PS --> D_PS["URI/Headers/Body設定"];
        D_PS --> E_PS["Invoke-RestMethod実行"];
        E_PS -- HTTP POST --> F_Azure;
        F_Azure -- JSONレスポンス --> G_PS["レスポンス受信"];
        G_PS --> H_PS["自動オブジェクト変換"];
        H_PS -- Success --> I_PS["結果利用"];
        H_PS -- Error --> J_PS["Try-Catchエラー処理"];
        I_PS --> K_PS["スクリプト終了"];
        J_PS --> K_PS;
    end

実装(最小→堅牢化)

VBA編

Excel VBAを例に、最小限の機能から始め、エラーハンドリングや設定の外部化など堅牢性を高める手法を見ていきましょう。

1. 準備

  1. 参照設定: VBE (Alt+F11) を開き、「ツール」→「参照設定」から「Microsoft WinHTTP Services, version 5.1」にチェックを入れます。
  2. APIキーとエンドポイントの取得:
    • Azure PortalでAzure OpenAI Serviceリソースに移動。
    • 「キーとエンドポイント」から「キー」と「エンドポイント」のURLをコピーします。
    • デプロイしたモデルの名前も確認しておきます(例: gpt-35-turbo)。

2. 最小実装

まずは、ハードコードされた情報でテキスト生成を行うシンプルなコードです。

' 標準モジュール (Module1など) に記述

Sub CallAzureOpenAI_Minimal()
    Dim WinHttp As Object
    Dim Url As String
    Dim ApiKey As String
    Dim DeploymentName As String
    Dim JsonBody As String
    Dim ResponseText As String

    ' --- 設定値 (ハードコード) ---
    Const AZURE_OPENAI_RESOURCE_NAME As String = "your-openai-resource-name" ' あなたのリソース名
    DeploymentName = "gpt-35-turbo" ' あなたがデプロイしたモデル名
    ApiKey = "your-api-key" ' あなたのAPIキー

    Url = "https://" & AZURE_OPENAI_RESOURCE_NAME & ".openai.azure.com/openai/deployments/" & _
          DeploymentName & "/chat/completions?api-version=2023-07-01-preview"

    ' --- リクエストボディのJSON生成 ---
    ' プロンプトには「system」ロールで全体の指示、「user」ロールで具体的な質問を設定
    JsonBody = "{""messages"": [{""role"": ""system"", ""content"": ""あなたは親切なAIアシスタントです。""}," & _
               "{""role"": ""user"", ""content"": ""VBAでAzure OpenAI APIを呼び出す方法を教えてください。""}]," & _
               """max_tokens"": 500,""temperature"": 0.7}"

    ' --- WinHttpRequestオブジェクトの初期化とリクエスト送信 ---
    Set WinHttp = CreateObject("WinHttp.WinHttpRequest.5.1")
    With WinHttp
        .Open "POST", Url, False ' Falseで同期実行
        .SetRequestHeader "Content-Type", "application/json"
        .SetRequestHeader "api-key", ApiKey
        .Send JsonBody

        ' --- レスポンス処理 ---
        If .Status = 200 Then
            ResponseText = .ResponseText
            ' 簡易的なJSONパース (contentだけを抽出)
            Dim StartPos As Long, EndPos As Long
            StartPos = InStr(ResponseText, """content"":""")
            If StartPos > 0 Then
                StartPos = StartPos + Len("""content"":""")
                EndPos = InStr(StartPos, ResponseText, """}")
                If EndPos > 0 Then
                    Dim Content As String
                    Content = Mid(ResponseText, StartPos, EndPos - StartPos)
                    ' エスケープ文字の処理 (JSONから文字列抽出する場合の落とし穴)
                    Content = Replace(Content, "\n", vbCrLf)
                    Content = Replace(Content, "\""", """")
                    Content = Replace(Content, "\\", "\") ' 他のエスケープも考慮
                    Debug.Print "AIの応答: " & Content
                Else
                    Debug.Print "エラー: 'content' の終了タグが見つかりません。"
                End If
            Else
                Debug.Print "エラー: 'content' キーが見つかりません。"
            End If
        Else
            Debug.Print "API呼び出し失敗。HTTPステータス: " & .Status & " - " & .StatusText
            Debug.Print "レスポンス: " & .ResponseText
        End If
    End With

    Set WinHttp = Nothing
End Sub

落とし穴と境界条件: * JSONパースの限界: 上記のコードでは、InStrMidを使った非常に単純な文字列抽出を行っています。これはレスポンスの構造が固定でシンプルな場合にしか機能しません。複雑なJSON、ネストされたJSON、配列などを扱うには不十分です。 * エラーハンドリングの不足: On Error GoTo がないため、ネットワーク障害や予期せぬレスポンスでVBAが停止します。 * 同期実行: Open メソッドの第三引数 False は同期実行を意味します。APIの応答待ちの間、Excelがフリーズします。処理時間が長くなる場合、UIの応答性が失われます。 * ハードコードされた設定: APIキーやURLがコードに直書きされているため、環境変更やセキュリティアップデート時にコードの修正が必要です。

3. 堅牢化

実運用に耐えるために、設定の外部化、エラーハンドリング、JSONのより賢いパース(外部ライブラリを極力使わない範囲で)、非同期処理への言及を行います。

' 標準モジュール (Module1など) に記述

' --- 定数宣言 ---
' PtrSafeはここでは直接必要ないが、他のWindows API呼び出しで必要となるため一般的に記述を推奨
Private Const API_VERSION As String = "2023-07-01-preview"

' --- 設定値の保持用クラス (Class Module: clsOpenAISettings) ---
' プロジェクトにClass Moduleを追加し、名前を 'clsOpenAISettings' に変更
' --- clsOpenAISettings.cls ---
' Public ResourceName As String
' Public DeploymentName As String
' Public ApiKey As String
' Public Function GetEndpointUrl() As String
'     GetEndpointUrl = "https://" & ResourceName & ".openai.azure.com/openai/deployments/" & _
'                      DeploymentName & "/chat/completions?api-version=" & API_VERSION
' End Function
' -----------------------------

Function GetOpenAISettings() As clsOpenAISettings
    Dim Settings As New clsOpenAISettings
    ' 実運用では、レジストリ、INIファイル、設定シートなどから読み込む
    ' 例: ThisWorkbook.Sheets("Settings").Range("B1").Value など
    Settings.ResourceName = Environ("AZURE_OPENAI_RESOURCE_NAME") ' 環境変数から取得
    Settings.DeploymentName = "gpt-35-turbo" ' モデル名は変更頻度低ければ定数でも可
    Settings.ApiKey = Environ("AZURE_OPENAI_API_KEY") ' 環境変数から取得

    ' 環境変数が設定されていない場合のデフォルトやエラー処理
    If Settings.ResourceName = "" Then Err.Raise 9999, "GetOpenAISettings", "環境変数 AZURE_OPENAI_RESOURCE_NAME が設定されていません。"
    If Settings.ApiKey = "" Then Err.Raise 9999, "GetOpenAISettings", "環境変数 AZURE_OPENAI_API_KEY が設定されていません。"

    Set GetOpenAISettings = Settings
End Function

Function CallAzureOpenAI_Robust(ByVal UserPrompt As String, _
                                Optional ByVal SystemPrompt As String = "あなたは親切なAIアシスタントです。", _
                                Optional ByVal MaxTokens As Long = 500, _
                                Optional ByVal Temperature As Double = 0.7) As String
    Dim WinHttp As Object
    Dim Settings As clsOpenAISettings
    Dim JsonBody As String
    Dim ResponseText As String
    Dim Content As String

    On Error GoTo ErrorHandler

    Set Settings = GetOpenAISettings() ' 設定を読み込む

    ' --- リクエストボディのJSON生成 ---
    ' VBAの文字列結合は面倒なため、より大きなプロンプトは外部ファイルやテンプレート利用も検討
    JsonBody = "{""messages"": [" & _
               "{""role"": ""system"", ""content"": """ & EscapeJsonString(SystemPrompt) & """}," & _
               "{""role"": ""user"", ""content"": """ & EscapeJsonString(UserPrompt) & """}]," & _
               """max_tokens"": " & MaxTokens & "," & _
               """temperature"": " & Replace(CStr(Temperature), ",", ".") & "}" ' 小数点の地域差対応

    ' --- WinHttpRequestオブジェクトの初期化とリクエスト送信 ---
    Set WinHttp = CreateObject("WinHttp.WinHttpRequest.5.1")
    With WinHttp
        ' タイムアウト設定 (ミリ秒): Openメソッドの後、Sendメソッドの前に設定
        .SetTimeouts ResolveTimeout:=10000, ConnectTimeout:=10000, SendTimeout:=30000, ReceiveTimeout:=60000

        .Open "POST", Settings.GetEndpointUrl(), False ' False: 同期実行 (非同期は後述)
        .SetRequestHeader "Content-Type", "application/json"
        .SetRequestHeader "api-key", Settings.ApiKey
        .Send JsonBody

        ' --- レスポンス処理 ---
        If .Status = 200 Then
            ResponseText = .ResponseText
            ' 正規表現でcontentを抽出する (簡易的なJSONパース)
            Dim regEx As Object
            Set regEx = CreateObject("VBScript.RegExp")
            With regEx
                .Pattern = """content"":\s*""(.*?[^\\])""" ' contentキーの値を抽出 (末尾の\"を考慮)
                .IgnoreCase = False
                .Global = False ' 最初の一致のみ
            End With

            If regEx.Test(ResponseText) Then
                Content = regEx.Execute(ResponseText)(0).SubMatches(0)
                ' JSON文字列のエスケープ解除
                Content = Replace(Content, "\n", vbCrLf)
                Content = Replace(Content, "\t", vbTab)
                Content = Replace(Content, "\r", vbCr)
                Content = Replace(Content, "\""", """")
                Content = Replace(Content, "\\", "\") ' 最後に残ったバックスラッシュを解除

                CallAzureOpenAI_Robust = Content
            Else
                Err.Raise 9998, "CallAzureOpenAI_Robust", "レスポンスから 'content' を抽出できませんでした。" & vbCrLf & "Raw Response: " & ResponseText
            End If
        Else
            Err.Raise .Status, "CallAzureOpenAI_Robust", "API呼び出し失敗。HTTPステータス: " & .Status & " - " & .StatusText & vbCrLf & "レスポンス: " & .ResponseText
        End If
    End With

    Set WinHttp = Nothing
    Exit Function

ErrorHandler:
    Debug.Print "エラー発生: " & Err.Source & " - " & Err.Description
    Debug.Print "エラーコード: " & Err.Number
    CallAzureOpenAI_Robust = "エラー: " & Err.Description ' 呼び出し元にエラーを返す
    Set WinHttp = Nothing
End Function

' JSON文字列内の特殊文字をエスケープするヘルパー関数
Function EscapeJsonString(ByVal InputString As String) As String
    ' JSON文字列内でエスケープが必要な文字: ", \, /, CR, LF, TAB
    Dim Temp As String
    Temp = Replace(InputString, "\", "\\") ' 最初にバックスラッシュをエスケープ
    Temp = Replace(Temp, """", "\""")
    Temp = Replace(Temp, vbCrLf, "\n") ' CRLFは\nに変換
    Temp = Replace(Temp, vbCr, "\r")
    Temp = Replace(Temp, vbLf, "\n")
    Temp = Replace(Temp, vbTab, "\t")
    EscapeJsonString = Temp
End Function

' 実行例
Sub Test_CallAzureOpenAI_Robust()
    On Error Resume Next ' エラーをトラップ
    Dim Result As String
    Result = CallAzureOpenAI_Robust("Excel VBAからAzure OpenAI Serviceを利用するメリットを3つ教えてください。", _
                                    SystemPrompt:="あなたは丁寧なプログラミングアシスタントです。箇条書きで分かりやすく説明してください。")
    If Err.Number <> 0 Then
        Debug.Print "テスト実行中にエラー: " & Result ' Resultにエラーメッセージが格納されている
        Err.Clear
    Else
        Debug.Print "AIの堅牢な応答: " & Result
    End If
End Sub

VBA JSONパースの課題と対策: * VBAには標準のJSONパーサーがありません。「外部ライブラリ極力なし」の要件のため、上記ではVBScript.RegExpオブジェクトを使った簡易的な抽出を示しました。 * より本格的なパース: VBA-JSON(GitHubなどで公開されている単一モジュール)のような、VBAコードとして取り込めるJSONパーサーを利用するのが最も現実的で堅牢な方法です。これは「外部ライブラリ」というよりは「外部コード」という解釈で、COMオブジェクトや参照設定を増やさずに利用できるため、現場では頻繁に採用されます。その場合、DictionaryオブジェクトやCollectionオブジェクトを使ってJSON構造をVBAオブジェクトとして扱えます。 * PtrSafe / LongPtr の補足: * WinHttpRequest自体はCOMオブジェクトであり、そのメソッド呼び出しはCOMインターフェースを介して行われるため、VBAコードレベルで直接ポインタ操作を行う必要はありません。したがって、通常 PtrSafeLongPtr は使いません。 * これらは、Declare ステートメントでWindows API(Win32 APIなど)を直接VBAから呼び出す際に、32bit/64bit環境でメモリアドレスのサイズが異なることに対応するために必要となります。例えば、Declare PtrSafe Function GetPrivateProfileString Lib "kernel32" Alias "GetPrivateProfileStringA" (ByVal lpAppName As String, ByVal lpKeyName As String, ByVal lpDefault As String, ByVal lpReturnedString As String, ByVal nSize As Long, ByVal lpFileName As String) As Long のように使われます。 * 本記事の範囲では直接関係ありませんが、VBAでWindows APIを扱う際には常に意識すべき重要な要素です。

PowerShell編

PowerShellはJSON処理に優れており、VBAに比べて遥かに簡潔に記述できます。

1. 準備

  • 特に参照設定などは不要です。PowerShellがインストールされていればOK。
  • APIキーとエンドポイントはVBAと同様に取得します。

2. 最小実装

Invoke-RestMethod を使った最もシンプルな呼び出しです。

# --- 設定値 (ハードコード) ---
$AzureOpenAIResourceName = "your-openai-resource-name" # あなたのリソース名
$DeploymentName = "gpt-35-turbo" # あなたがデプロイしたモデル名
$ApiKey = "your-api-key" # あなたのAPIキー
$ApiVersion = "2023-07-01-preview"

$Uri = "https://$AzureOpenAIResourceName.openai.azure.com/openai/deployments/$DeploymentName/chat/completions?api-version=$ApiVersion"

# --- ヘッダーの定義 ---
$Headers = @{
    "Content-Type" = "application/json"
    "api-key"      = $ApiKey
}

# --- リクエストボディのJSON生成 ---
# PowerShellではハッシュテーブルをConvertToJsonで簡単にJSONに変換できる
$Body = @{
    messages = @(
        @{ role = "system"; content = "あなたは親切なAIアシスタントです。" },
        @{ role = "user"; content = "PowerShellでAzure OpenAI APIを呼び出す方法を教えてください。" }
    )
    max_tokens  = 500
    temperature = 0.7
} | ConvertTo-Json -Depth 4 # -Depthでネストされたオブジェクトも適切にJSON化

# --- Invoke-RestMethod でリクエスト送信 ---
try {
    $Response = Invoke-RestMethod -Uri $Uri -Method Post -Headers $Headers -Body $Body

    # --- レスポンス処理 ---
    # Invoke-RestMethodはJSONを自動的にPSCustomObjectに変換してくれる
    if ($Response.choices.Count -gt 0) {
        $Content = $Response.choices[0].message.content
        Write-Host "AIの応答: $($Content)"
    } else {
        Write-Error "応答にchoicesが見つかりません。"
    }
}
catch {
    Write-Error "API呼び出し失敗: $($_.Exception.Message)"
    if ($_.Exception.Response) {
        $ErrorResponse = $_.Exception.Response.GetResponseStream()
        $Reader = New-Object System.IO.StreamReader($ErrorResponse)
        $ErrorBody = $Reader.ReadToEnd()
        Write-Error "詳細レスポンス: $($ErrorBody)"
    }
}

落とし穴と境界条件: * ハードコードされた設定: VBAと同様、APIキーやURLが直書きされているのは問題です。 * エラー処理の限界: try-catch は基本的なエラーを捕捉しますが、詳細なレスポンスの取得にはもう少し工夫が必要です。 * PSCustomObjectの構造: Invoke-RestMethod は便利ですが、レスポンスのJSON構造を完全に理解していないと、目的のデータ ($Response.choices[0].message.content) に辿り着けないことがあります。

3. 堅牢化

環境変数からの設定読み込み、詳細なエラーハンドリング、パラメータ化を行います。

# --- 設定の外部化 ---
# 環境変数から取得。なければエラーを発生させる。
$AzureOpenAIResourceName = $env:AZURE_OPENAI_RESOURCE_NAME
if (-not $AzureOpenAIResourceName) {
    throw "環境変数 'AZURE_OPENAI_RESOURCE_NAME' が設定されていません。"
}
$DeploymentName = "gpt-35-turbo" # デプロイ名も環境変数や設定ファイルから取得可
$ApiKey = $env:AZURE_OPENAI_API_KEY
if (-not $ApiKey) {
    throw "環境変数 'AZURE_OPENAI_API_KEY' が設定されていません。"
}
$ApiVersion = "2023-07-01-preview"

Function Invoke-AzureOpenAICompletion {
    param(
        [string]$UserPrompt,
        [string]$SystemPrompt = "あなたは親切なAIアシスタントです。",
        [int]$MaxTokens = 500,
        [double]$Temperature = 0.7
    )

    $Uri = "https://$AzureOpenAIResourceName.openai.azure.com/openai/deployments/$DeploymentName/chat/completions?api-version=$ApiVersion"

    $Headers = @{
        "Content-Type" = "application/json"
        "api-key"      = $ApiKey
    }

    $Body = @{
        messages = @(
            @{ role = "system"; content = $SystemPrompt },
            @{ role = "user"; content = $UserPrompt }
        )
        max_tokens  = $MaxTokens
        temperature = $Temperature
    } | ConvertTo-Json -Depth 4

    try {
        # ErrorAction Stop で、HTTPエラーレスポンスもtryブロックで捕捉
        $Response = Invoke-RestMethod -Uri $Uri -Method Post -Headers $Headers -Body $Body -TimeoutSec 60 -ErrorAction Stop

        if ($Response.choices.Count -gt 0) {
            return $Response.choices[0].message.content
        } else {
            throw "APIレスポンスから有効な 'choices' が見つかりませんでした。"
        }
    }
    catch {
        Write-Error "Azure OpenAI API呼び出し中にエラーが発生しました。"
        Write-Error "メッセージ: $($_.Exception.Message)"

        # HTTPレスポンスからの詳細エラー情報の抽出
        if ($_.Exception.Response) {
            $ErrorResponseStream = $_.Exception.Response.GetResponseStream()
            $StreamReader = New-Object System.IO.StreamReader($ErrorResponseStream)
            $ErrorBodyJson = $StreamReader.ReadToEnd()

            # JSON形式のエラーボディをパース
            try {
                $ErrorObject = $ErrorBodyJson | ConvertFrom-Json
                Write-Error "APIエラーコード: $($ErrorObject.error.code)"
                Write-Error "APIエラーメッセージ: $($ErrorObject.error.message)"
            }
            catch {
                Write-Error "エラーレスポンスのパースに失敗しました。Raw Body: $($ErrorBodyJson)"
            }
        }
        # エラーを再スローして呼び出し元でさらに処理できるようにする
        throw $_.Exception
    }
}

# --- 実行例 ---
try {
    $Result = Invoke-AzureOpenAICompletion -UserPrompt "PowerShellでAzure OpenAI Serviceを利用するメリットを3つ教えてください。" `
                                         -SystemPrompt "あなたはプロフェッショナルなIT技術者です。箇条書きで分かりやすく説明してください。"
    Write-Host "AIの堅牢な応答:"
    Write-Host $Result
}
catch {
    Write-Error "スクリプト実行中に致命的なエラーが発生しました: $($_.Exception.Message)"
}

PowerShellの堅牢化における追加考慮点: * TLSバージョン: Invoke-RestMethod は既定で最新のTLSバージョンを使用しようとしますが、古いOSやPowerShellのバージョンではTLS 1.2が有効になっていない場合があります。その際は [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 をスクリプトの先頭に追加することで強制できます。 * プロキシ設定: 企業ネットワーク環境ではプロキシ経由での通信が必要な場合があります。$env:http_proxyInvoke-RestMethod -Proxy パラメータで設定可能です。 * レートリミット: Azure OpenAI Serviceにはレートリミットがあります。大量のリクエストを送信する場合は、リトライロジック(指数バックオフなど)を実装する必要があります。 * ConvertTo-Json -Depth: JSONのネストが深い場合、デフォルトの -Depth では足りないことがあります。必要に応じて値を増やしましょう。

ベンチ/検証

API連携はネットワークI/Oと外部サービスの応答に依存するため、安定性とパフォーマンスの検証が不可欠です。

テスト観点

  1. 正常系:
    • テキスト生成: 指定したプロンプトに対し、意図通りのテキストが生成されるか。
    • プロンプトパラメータ: max_tokens, temperature などのパラメータが応答に影響を与えているか。
    • 複数回実行: 同一のプロンプトで複数回実行し、応答の安定性(一貫性)を確認。
  2. 異常系:
    • APIキー不正: 無効なAPIキーで 401 Unauthorized が返されるか。
    • エンドポイント不正: 存在しないURLで 404 Not Found または接続エラーが発生するか。
    • JSON形式不正: リクエストボディのJSONが不正な場合に 400 Bad Request が返されるか。
    • タイムアウト: ネットワークが遅延または応答しない場合、設定したタイムアウトで処理が終了するか。
    • レートリミット: 短時間に大量のリクエストを送信し、429 Too Many Requests が返されるか。
    • ネットワーク障害: インターネット接続がない状態でエラーハンドリングが適切に動作するか。
  3. パフォーマンス:
    • 応答時間: 平均的なAPI応答時間を計測し、ユーザー体験や業務要件を満たせるか。特にmax_tokensが多い場合や複雑なプロンプトの場合。
    • ピーク時負荷: 同時実行数が増えた場合のパフォーマンス劣化、レートリミットの発生状況。

計測方法

  • VBA: Timer 関数を使用して、API呼び出し前後の時刻差をミリ秒単位で計測します。

    Dim StartTime As Double
    StartTime = Timer ' 開始時刻
    ' API呼び出しコード
    Dim EndTime As Double
    EndTime = Timer ' 終了時刻
    Debug.Print "API応答時間: " & (EndTime - StartTime) * 1000 & " ms"
    
  • PowerShell: Measure-Command コマンドレットが便利です。

    Measure-Command {
        # API呼び出しコード
        Invoke-AzureOpenAICompletion -UserPrompt "テスト"
    } | Select-Object TotalMilliseconds
    

応用例/代替案

応用例

  • Excelデータからの自動要約/分類: 大量の顧客コメントや商品レビューをExcelシートから読み込み、LLMで要約、感情分析、カテゴリ分類を行い、結果を別列に書き出す。
  • Outlookメール処理の自動化: 新着メールの内容をLLMで分析し、重要度判定、返信の下書き生成、特定のメールボックスへの自動振り分けなど。
  • SharePointリストアイテムの補完: SharePointリストのテキストフィールドの内容を基に、不足している情報や関連キーワードをLLMに生成させてアイテムを更新する。
  • 業務レポートのドラフト作成: 複数システムからのデータをLLMに渡し、レポートの骨子や特定セクションのドラフトを自動生成する。
  • データクレンジングと正規化: 揺らぎのあるテキストデータをLLMに投げて、指定された形式に正規化したり、誤字脱字を修正したりする。

代替案

VBA/PowerShell以外でより大規模・堅牢なシステムを構築する場合の選択肢です。

  1. Python + Requestsライブラリ/OpenAIライブラリ:
    • メリット: LLM連携のデファクトスタンダードであり、公式ライブラリや豊富なコミュニティ製ライブラリ、データ処理ライブラリ(Pandasなど)が充実しています。エラーハンドリングや非同期処理も高度に実装可能です。
    • デメリット: Python実行環境の構築が必要。既存のVBA/PowerShell資産との連携には別途ラッパーや接着剤が必要。
  2. Azure Functions / AWS Lambda (サーバーレス関数):
    • メリット: コードの実行環境を意識せず、API呼び出しロジックに集中できます。スケーラビリティ、高可用性、コスト効率に優れています。HTTPトリガーで公開すれば、VBA/PowerShellからも間接的に呼び出すことができます。
    • デメリット: 関数ごとの開発・デプロイが必要。監視やログの仕組みも考慮が必要。
  3. Power Automate (Power Platform):
    • メリット: GUIベースでワークフローを構築できるため、プログラミング知識が浅いユーザーでも利用しやすい。各種Microsoftサービスとの連携が容易。
    • デメリット: カスタムコネクタの作成が必要な場合がある。複雑なロジックや高度なエラーハンドリングには限界がある。実行コストが高くなる場合がある。

失敗例→原因→対処

ケーススタディ:JSON形式不正によるAPIエラー

失敗例: VBAでリクエストを送信したが、HTTPステータス 400 Bad Request が返され、レスポンスボディには {"error": {"code": "invalid_request_format", "message": "The request body is not a valid JSON."}} のようなエラーメッセージが記録された。

原因: VBAの文字列連結でJSONボディを作成する際、ユーザー入力にダブルクォーテーション " や改行文字が含まれており、これらがJSONの構文を壊してしまったため。特に content フィールドのテキストにエスケープすべき文字(", \, 改行)が含まれていた。

' 失敗したJSONボディ生成の例 (エスケープ漏れ)
Dim UserInput As String
UserInput = "これは""テスト""です。改行\します。" ' この入力が問題
JsonBody = "{""messages"": [{""role"": ""user"", ""content"": """ & UserInput & """}]}"
' -> Result: {"messages": [{"role": "user", "content": "これは"テスト"です。改行\します。"}]}
' これでは "テスト" の部分でJSON文字列が閉じてしまい、不正なJSONとなる。

対処: JSONボディに組み込む文字列は、必ずJSONのルールに従って適切にエスケープする必要があります。特にダブルクォーテーション "\" に、バックスラッシュ \\\ に、改行は \n などに変換が必要です。

' 対処後のJSONボディ生成の例 (EscapeJsonString関数を使用)
Function EscapeJsonString(ByVal InputString As String) As String
    Dim Temp As String
    Temp = Replace(InputString, "\", "\\") ' 最初にバックスラッシュをエスケープ
    Temp = Replace(Temp, """", "\""")
    Temp = Replace(Temp, vbCrLf, "\n")
    Temp = Replace(Temp, vbCr, "\r")
    Temp = Replace(Temp, vbLf, "\n")
    Temp = Replace(Temp, vbTab, "\t")
    EscapeJsonString = Temp
End Function

Dim UserInput As String
UserInput = "これは""テスト""です。改行\します。"
JsonBody = "{""messages"": [{""role"": ""user"", ""content"": """ & EscapeJsonString(UserInput) & """}]}"
' -> Result: {"messages": [{"role": "user", "content": "これは\"テスト\"です。\nします。"}]}
' このようにエスケープすることで、JSONとして正しく認識される。

PowerShellの場合はConvertTo-Jsonコマンドレットが自動でエスケープ処理を行うため、この種の問題は発生しにくいですが、手動でJSON文字列を構築する際には同様の注意が必要です。

まとめ

VBAやPowerShellからAzure OpenAI ServiceのREST APIを連携させることは、既存の業務環境にAIの知能を統合し、強力な自動化と効率化を実現する手段となります。 VBAでは WinHttpRequest オブジェクトを、PowerShellでは Invoke-RestMethod コマンドレットを核に、HTTP通信とJSONデータの送受信を行うことが基本です。

VBAはJSONパースに手間がかかる、PowerShellは簡潔に記述できるといった言語特性を理解し、それぞれに合った堅牢化のアプローチが求められます。特にAPIキーの安全な管理、詳細なエラーハンドリング、そして多様なネットワーク環境への適応は、実運用において避けては通れない課題です。

本記事で解説した最小実装から堅牢化までのステップ、そして具体的な失敗例とその対処法を参考に、あなたの業務に合わせたAI連携システムを構築してください。

運用チェックリスト

  • [ ] APIキーのセキュアな管理: 環境変数、Azure Key Vault、セキュアな設定ファイルなど、コードに直書きしない仕組みになっているか?
  • [ ] エラーハンドリング: ネットワークエラー、HTTPエラーコード(4xx, 5xx)、APIレスポンスエラー(JSONパース失敗など)を適切に捕捉し、ログに記録またはユーザーに通知しているか?
  • [ ] タイムアウト設定: API呼び出しが予期せず長時間停止しないよう、適切なタイムアウトを設定しているか?
  • [ ] コスト監視: Azure PortalでOpenAI Serviceの利用状況とコストを定期的に監視しているか? usage フィールドを解析してトークン数を記録しているか?
  • [ ] プロンプトのバージョン管理: 使用するプロンプト(システムプロンプト、ユーザープロンプト)は外部化され、変更履歴を追跡できる状態にあるか?
  • [ ] レートリミット対策: 短期間に大量のリクエストを送信する可能性がある場合、リトライロジック(指数バックオフなど)が実装されているか?
  • [ ] ロギング: APIリクエスト/レスポンス、エラーメッセージ、実行日時などの重要な情報をログに出力しているか?
  • [ ] 64bit環境への対応: VBAでAPI宣言を行う場合、PtrSafeLongPtrを正しく使用しているか(本記事のWinHttpRequestでは直接不要だが一般論として)?
  • [ ] プロキシ設定: 企業ネットワーク環境でプロキシ経由の通信が必要な場合、適切に設定されているか?

参考リンク

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

コメント

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