■■■ 環境に合わせて以下を設定してください ■■■

EXCEL

VBA/PowerShellからAzure OpenAI API:REST深堀と堅牢実装の極意

導入(問題設定)

ビジネス現場の最前線で使われるExcel VBAマクロや、システム管理の自動化を担うPowerShellスクリプト。これら既存資産に、Azure OpenAI Serviceの強力な自然言語処理能力を直接組み込みたいというニーズは日増しに高まっています。しかし、外部ライブラリの導入が難しい環境、厳格なセキュリティポリシー、そして何より「既存スクリプト資産を最大限活用したい」という制約の中で、どのように堅牢かつ効率的にAIと連携すればよいのでしょうか。

本記事では、VBAおよびPowerShellからAzure OpenAI ServiceのREST APIを直接呼び出す手法に焦点を当てます。単なるHowToに留まらず、APIの内部動作、潜在的な落とし穴、そして最小実装から実運用に耐えうる堅牢なコードへの進化プロセスまで、マニアックに深掘りしていきます。外部ライブラリへの依存を極力排除し、ネイティブ機能でどこまでできるかを追求することで、システム担当者が既存環境で安心してAIを活用できる基盤を提供します。

理論の要点

Azure OpenAI Serviceは、HTTPベースのREST APIとして提供されます。これは、特定のURL(エンドポイント)に対してHTTPリクエストを送信し、JSON形式でデータをやり取りすることで、AIモデルと通信する仕組みです。

1. REST APIの基本要素

  • エンドポイント (Endpoint): APIにアクセスするためのURL。Azure OpenAIの場合、https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=YYYY-MM-DD の形式をとります。
    • YOUR_RESOURCE_NAME: Azureリソース作成時に指定したユニークな名前。
    • YOUR_DEPLOYMENT_NAME: デプロイしたモデル(例: gpt-35-turbo, gpt-4)に付けたデプロイ名。
    • api-version: 使用するAPIのバージョン。指定が必須であり、後方互換性がない変更が含まれる可能性があるため、常に最新または安定版を指定します(例: 2024-02-15)。
  • HTTPメソッド: データの操作種類を示します。Azure OpenAIのChat Completions APIでは、テキスト生成リクエストは通常 POST メソッドを使用します。
  • ヘッダー (Headers): リクエストに関するメタデータを伝達します。
    • Content-Type: application/json: リクエストボディがJSON形式であることを示します。
    • api-key: YOUR_API_KEY: 認証情報としてAzure OpenAI ServiceのAPIキーを送信します。
  • ボディ (Body): APIに送信する実際のデータ。JSON形式でプロンプトやモデルパラメータを含めます。
  • ステータスコード (Status Code): サーバーからの応答状況を示します(例: 200 OK, 400 Bad Request, 500 Internal Server Error)。
  • レスポンス (Response): サーバーからの返答。通常、JSON形式でAIの生成テキストやトークン使用量などが含まれます。

2. Azure OpenAIの認証とモデル指定

認証はAPIキーをHTTPヘッダーに含めることで行われます。これはAzure Portalから取得できるAPIキーであり、取り扱いには厳重な注意が必要です。 モデルは、デプロイ時に指定した名前(YOUR_DEPLOYMENT_NAME)を使ってエンドポイントURIの一部として指定されます。これにより、複数のモデルを同じAzure OpenAIリソース内で使い分けることが可能です。

3. プロンプトの構造(Chat Completions API)

Chat Completions APIでは、従来のCompletion APIと異なり、会話形式のメッセージ配列を使用します。

  • messages: 会話の履歴を表すオブジェクトの配列。
    • role: メッセージの送信者を定義します。
      • system: モデルの振る舞いや全体的な指示を設定します。会話の最初に一度だけ設定することが推奨されます。
      • user: ユーザーからの入力。
      • assistant: モデルの返答(以前の会話履歴を再現する場合に利用)。
    • content: メッセージのテキスト本体。

4. 主要なリクエストパラメータとレスポンス構造

API エンドポイント: POST https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2024-02-15

パラメータ名 必須 説明
Request Headers
Content-Type string はい application/json
api-key string はい Azure OpenAI ServiceのAPIキー
Request Body JSON Object
messages array はい 会話のメッセージ配列。{"role": "...", "content": "..."} オブジェクトの配列。
temperature number いいえ 応答のランダム性(創造性)を制御。0.0(決定的)〜2.0(創造的)。デフォルトは1.0。
max_tokens integer いいえ 生成される応答の最大トークン数。prompt_tokens + completion_tokens <= max_tokens の制約あり。
top_p number いいえ temperatureと類似。上位pパーセントの確率を持つトークンからサンプリング。
stop string/array いいえ 指定された文字列に遭遇すると、モデルは生成を停止します。
stream boolean いいえ trueに設定すると、応答がストリーミングで送られます。本記事では同期通信を扱います。

Response Body (成功時): JSON Object

プロパティ名 説明
id string 応答の一意のID。
object string chat.completion
created integer 応答が作成されたUnixタイムスタンプ。
model string 使用されたモデルのID。
choices array モデルによって生成された選択肢の配列。
choices[0].index| integer 選択肢のインデックス。
choices[0].message.role string assistant
choices[0].message.content string モデルが生成したテキスト。
choices[0].finish_reason string 応答が終了した理由(stop, length, content_filter, nullなど)。
usage object リクエストとレスポンスで使用されたトークン数の情報。
usage.prompt_tokens integer プロンプトに使用されたトークン数。
usage.completion_tokens integer 生成された応答に使用されたトークン数。
usage.total_tokens integer 全体のトークン数。

MermaidによるAPI連携フロー

sequenceDiagram
    participant "C as Client (VBA/PowerShell)"
    participant "A as Azure OpenAI Service"
    participant "L as LLM (gpt-3.5-turbo/gpt-4)"

    C ->> A: 1. HTTP POST Request
    Note over C,A: Headers: api-key, Content-Type: application/json
    Note over C,A: Body: JSON (messages, temperature, max_tokens, etc.)

    A ->> A: 2. Authenticate & Authorize
    A ->> L: 3. Forward Prompt to LLM
    L -->> A: 4. LLM Generates Completion
    A -->> A: 5. Apply Content Filtering
    A -->> C: 6. HTTP 200 OK Response
    Note over A,C: Body: JSON (choices, usage, etc.)

このシーケンス図は、クライアントがAzure OpenAI Serviceを介してLLMと通信する基本的なフローを示しています。認証、内部でのプロンプト転送、そしてコンテンツフィルタリングなど、一連のプロセスがサーバー側で透過的に行われます。

実装(最小→堅牢化)

VBAによる実装

VBAでは、HTTP通信のためにMicrosoft WinHTTP ServicesまたはMicrosoft XML, v6.0などのCOMオブジェクトを利用します。ここでは汎用性の高いWinHttpRequestオブジェクトを使用します。 VBAにおけるJSONの生成・解析は標準機能だけでは困難なため、ここではScripting.Dictionaryと正規表現(または文字列操作)を駆使し、「外部ライブラリ極力なし」の要件に沿ったマニアックな実装を目指します。

重要な注意点: VBAで外部APIを呼び出す場合、64bit環境でPtrSafeキーワードが必要となるDeclareステートメントは、WinAPI関数に直接アクセスする場合に限定されます。WinHttpRequestのようなCOMオブジェクトを使用する限り、内部で適切に処理されるため、PtrSafeは直接関係ありません。しかし、VBAが動作するOfficeのビット数(32bit/64bit)は、COMオブジェクトの登録や互換性に影響を与える可能性があるため、常にターゲット環境を意識すべきです。

最小実装 (VBA)

Attribute VB_Name = "AzureOpenAIChat"
Option Explicit

' ■■■ 環境に合わせて以下を設定してください ■■■
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_AZURE_OPENAI_API_KEY" ' 実際のAPIキーに置き換え
Private Const AZURE_OPENAI_RESOURCE_NAME As String = "YOUR_RESOURCE_NAME" ' リソース名に置き換え
Private Const AZURE_OPENAI_DEPLOYMENT_NAME As String = "YOUR_DEPLOYMENT_NAME" ' デプロイ名(例: gpt-35-turbo)に置き換え
Private Const AZURE_OPENAI_API_VERSION As String = "2024-02-15" ' APIバージョン

Public Function GetOpenAIChatCompletion(ByVal prompt As String, Optional ByVal systemMessage As String = "You are a helpful assistant.") As String
    Dim req As Object ' WinHttpRequest
    Dim url As String
    Dim jsonBody As String
    Dim responseText As String
    Dim result As String
    Dim posStart As Long, posEnd As Long

    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=" & AZURE_OPENAI_API_VERSION

    ' 2. リクエストボディ (JSON) の構築
    ' 最小限のJSON生成。堅牢化セクションでヘルパー関数を導入
    jsonBody = "{""messages"":["
    jsonBody = jsonBody & "{""role"":""system"",""content"":""" & EscapeJsonString(systemMessage) & """},"
    jsonBody = jsonBody & "{""role"":""user"",""content"":""" & EscapeJsonString(prompt) & """}"
    jsonBody = jsonBody & "],""temperature"":0.7,""max_tokens"":800}"

    ' 3. WinHttpRequestオブジェクトの初期化
    Set req = CreateObject("WinHttp.WinHttpRequest.5.1")
    req.Open "POST", url, False ' 同期通信

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

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

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

        ' 最小限のJSONパース: "choices":[{"message":{"content":"...
        ' マニアックに、InStrとReplaceで直接必要な値を取り出す
        posStart = InStr(responseText, """choices"":[{""message"":{""role"":""assistant"",""content"":""")
        If posStart > 0 Then
            posStart = posStart + Len("""choices"":[{""message"":{""role"":""assistant"",""content"":""")
            posEnd = InStr(posStart, responseText, """}") ' 次の"}"までを探す

            If posEnd > 0 Then
                result = Mid(responseText, posStart, posEnd - posStart)
                result = Replace(result, "\n", vbLf) ' 改行コードを変換
                result = Replace(result, "\t", vbTab) ' タブコードを変換
                result = Replace(result, "\""", """") ' エスケープされた引用符を変換
                GetOpenAIChatCompletion = result
            Else
                GetOpenAIChatCompletion = "Error: Could not parse content (end tag missing)."
            End If
        Else
            GetOpenAIChatCompletion = "Error: Could not find content in response."
        End If
    Else
        GetOpenAIChatCompletion = "API Error: Status " & req.Status & " - " & req.StatusText & vbCrLf & req.ResponseText
    End If

    GoTo CleanUp

ErrorHandler:
    GetOpenAIChatCompletion = "Runtime Error: " & Err.Description
CleanUp:
    Set req = Nothing
End Function

' JSON文字列内の特殊文字をエスケープする関数
Private Function EscapeJsonString(ByVal s As String) As String
    s = Replace(s, "\", "\\")
    s = Replace(s, """", "\""")
    s = Replace(s, vbCrLf, "\n")
    s = Replace(s, vbCr, "\n")
    s = Replace(s, vbLf, "\n")
    EscapeJsonString = s
End Function

' 使い方(イミディエイトウィンドウまたは別のSubから)
' Sub TestOpenAIChat()
'     Dim response As String
'     response = GetOpenAIChatCompletion("現在の日本の首相は誰ですか?")
'     Debug.Print response
' End Sub

堅牢化 (VBA)

上記コードをベースに、エラーハンドリングの強化、タイムアウト設定、JSON生成の抽象化、およびScripting.Dictionaryを用いた簡易JSONパース機能を追加し、より実用的な堅牢な実装を目指します。Scripting.DictionaryはVBAに標準搭載されており、外部ライブラリとは見なされにくいCOMコンポーネントです。

Attribute VB_Name = "AzureOpenAIChatRobust"
Option Explicit

' ■■■ 環境に合わせて以下を設定してください ■■■
Private Const AZURE_OPENAI_API_KEY As String = "YOUR_AZURE_OPENAI_API_KEY"
Private Const AZURE_OPENAI_RESOURCE_NAME As String = "YOUR_RESOURCE_NAME"
Private Const AZURE_OPENAI_DEPLOYMENT_NAME As String = "YOUR_DEPLOYMENT_NAME"
Private Const AZURE_OPENAI_API_VERSION As String = "2024-02-15"

' タイムアウト設定 (ミリ秒)
Private Const WINHTTP_TIMEOUT_RESOLVE As Long = 30000    ' ホスト名解決
Private Const WINHTTP_TIMEOUT_CONNECT As Long = 30000    ' 接続確立
Private Const WINHTTP_TIMEOUT_SEND As Long = 30000       ' データ送信
Private Const WINHTTP_TIMEOUT_RECEIVE As Long = 120000   ' データ受信 (長め推奨)

' リトライ設定
Private Const MAX_RETRY_ATTEMPTS As Long = 3
Private Const BASE_RETRY_DELAY_SECONDS As Long = 2 ' 最初の待機時間 (秒)

' --- 公開関数 ---
Public Function GetOpenAIChatCompletionRobust( _
    ByVal prompt As String, _
    Optional ByVal systemMessage As String = "あなたは親切なAIアシスタントです。", _
    Optional ByVal temperature As Single = 0.7, _
    Optional ByVal maxTokens As Long = 800 _
) As String

    Dim req As Object ' WinHttpRequest
    Dim url As String
    Dim jsonBody As String
    Dim responseText As String
    Dim attempt As Long
    Dim delaySeconds As Long
    Dim dictResponse As Object ' Scripting.Dictionary
    Dim messageContent As String
    Dim totalTokens As Long

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

    ' 2. リクエストボディ (JSON) の構築
    jsonBody = BuildChatCompletionJson(systemMessage, prompt, temperature, maxTokens)

    For attempt = 1 To MAX_RETRY_ATTEMPTS
        On Error GoTo ErrorHandler
        Set req = Nothing ' ループごとに再初期化

        Set req = CreateObject("WinHttp.WinHttpRequest.5.1")

        ' タイムアウト設定
        req.SetTimeouts WINHTTP_TIMEOUT_RESOLVE, WINHTTP_TIMEOUT_CONNECT, WINHTTP_TIMEOUT_SEND, WINHTTP_TIMEOUT_RECEIVE

        req.Open "POST", url, False ' 同期通信
        req.SetRequestHeader "Content-Type", "application/json"
        req.SetRequestHeader "api-key", AZURE_OPENAI_API_KEY

        req.Send jsonBody

        If req.Status = 200 Then
            responseText = req.ResponseText
            ' Debug.Print "Raw Response (Attempt " & attempt & "): " & responseText

            ' Scripting.DictionaryでJSONを簡易的にパース
            Set dictResponse = ParseJsonToDictionary(responseText)
            If Not dictResponse Is Nothing Then
                If dictResponse.Exists("choices") And IsArray(dictResponse("choices")) And UBound(dictResponse("choices")) >= 0 Then
                    Dim choice As Variant
                    Set choice = dictResponse("choices")(0) ' 最初の選択肢を取得
                    If IsObject(choice) Then
                        If choice.Exists("message") And IsObject(choice("message")) Then
                            If choice("message").Exists("content") Then
                                messageContent = choice("message")("content")
                                messageContent = Replace(messageContent, "\n", vbLf)
                                messageContent = Replace(messageContent, "\t", vbTab)
                                messageContent = Replace(messageContent, "\""", """")
                            End If
                        End If
                    End If
                End If

                If dictResponse.Exists("usage") And IsObject(dictResponse("usage")) Then
                    If dictResponse("usage").Exists("total_tokens") Then
                        totalTokens = dictResponse("usage")("total_tokens")
                        Debug.Print "Tokens used: " & totalTokens
                    End If
                End If
            End If

            If messageContent <> "" Then
                GetOpenAIChatCompletionRobust = messageContent
                Exit Function ' 成功したらループを抜ける
            Else
                Debug.Print "Warning: Content not found in response. Status: " & req.Status & ". Response: " & responseText
                GetOpenAIChatCompletionRobust = "Error: Content not found or JSON parse failed."
                GoTo NextAttempt ' パース失敗は再試行しないが、他のエラーケースのためにGoTo
            End If
        ElseIf req.Status = 429 Then ' Too Many Requests (レートリミット)
            Debug.Print "Rate limit hit (Status 429). Retrying in " & delaySeconds & " seconds... (Attempt " & attempt & ")"
            delaySeconds = BASE_RETRY_DELAY_SECONDS * (2 ^ (attempt - 1)) ' 指数バックオフ
            If attempt < MAX_RETRY_ATTEMPTS Then
                Application.Wait Now + TimeSerial(0, 0, delaySeconds)
            End If
        Else
            Debug.Print "API Error (Attempt " & attempt & "): Status " & req.Status & " - " & req.StatusText & vbCrLf & req.ResponseText
            GetOpenAIChatCompletionRobust = "API Error: Status " & req.Status & " - " & req.StatusText & vbCrLf & req.ResponseText
            Exit Function ' レートリミット以外のエラーは再試行せず終了
        End If

NextAttempt:
        If Not req Is Nothing Then Set req = Nothing
    Next attempt

    If GetOpenAIChatCompletionRobust = "" Then
        GetOpenAIChatCompletionRobust = "Error: Failed after " & MAX_RETRY_ATTEMPTS & " attempts."
    End If
    Exit Function

ErrorHandler:
    Debug.Print "Runtime Error (Attempt " & attempt & "): " & Err.Description & ". Line: " & Erl
    delaySeconds = BASE_RETRY_DELAY_SECONDS * (2 ^ (attempt - 1)) ' 指数バックオフ
    If attempt < MAX_RETRY_ATTEMPTS Then
        Application.Wait Now + TimeSerial(0, 0, delaySeconds)
        Resume NextAttempt
    Else
        GetOpenAIChatCompletionRobust = "Runtime Error: " & Err.Description & " (Failed after " & MAX_RETRY_ATTEMPTS & " attempts)"
    End If
    Resume CleanUp ' エラーハンドラから抜ける際の安全策

CleanUp:
    If Not req Is Nothing Then Set req = Nothing
End Function

' --- ヘルパー関数 ---

' JSON文字列内の特殊文字をエスケープする関数
Private Function EscapeJsonString(ByVal s As String) As String
    s = Replace(s, "\", "\\")
    s = Replace(s, """", "\""")
    s = Replace(s, vbCrLf, "\n")
    s = Replace(s, vbCr, "\n")
    s = Replace(s, vbLf, "\n")
    EscapeJsonString = s
End Function

' Chat Completion APIのリクエストJSONボディを構築する関数
Private Function BuildChatCompletionJson(ByVal systemMsg As String, ByVal userPrompt As String, ByVal temp As Single, ByVal maxTok As Long) As String
    Dim json As String
    json = "{""messages"":["
    json = json & "{""role"":""system"",""content"":""" & EscapeJsonString(systemMsg) & """},"
    json = json & "{""role"":""user"",""content"":""" & EscapeJsonString(userPrompt) & """}"
    json = json & "],""temperature"":" & Replace(CStr(temp), ",", ".") & "," ' 小数点対応
    json = json & """max_tokens"":" & CStr(maxTok) & "}"
    BuildChatCompletionJson = json
End Function

' JSON文字列をScripting.Dictionaryオブジェクトに簡易的にパースする関数
' **警告**: このパーサーは非常に単純で、ネストが深く複雑なJSONや配列の要素が文字列以外のJSONオブジェクトである場合には対応していません。
' 本格的なJSONパースにはJsonConverter.basなどの外部モジュールを推奨します。
Private Function ParseJsonToDictionary(ByVal jsonString As String) As Object
    Set ParseJsonToDictionary = Nothing

    Dim regex As Object
    Dim matches As Object
    Dim match As Object
    Dim key As String
    Dim value As String
    Dim tempDict As Object
    Dim tempArray As Object
    Dim i As Long

    Set tempDict = CreateObject("Scripting.Dictionary")

    ' 最上位のオブジェクトをパース (例: {"id": "...", "object": "..."})
    Set regex = CreateObject("VBScript.RegExp")
    regex.Pattern = """([^""]+)""\s*:\s*(?:""([^""]*)""|(\d+\.?\d*)|(true|false)|(\[.*?\])|(\{.*?\})|null)"
    regex.Global = True
    regex.IgnoreCase = True

    Set matches = regex.Execute(jsonString)
    For Each match In matches
        key = match.SubMatches(0)

        If match.SubMatches(1) <> "" Then ' Quoted string
            value = Replace(match.SubMatches(1), "\""", """") ' エスケープされた引用符を元に戻す
            value = Replace(value, "\n", vbLf)
            value = Replace(value, "\t", vbTab)
            tempDict.Add key, value
        ElseIf match.SubMatches(2) <> "" Then ' Number (integer or float)
            tempDict.Add key, CDbl(Replace(match.SubMatches(2), ",", ".")) ' 小数点対応
        ElseIf match.SubMatches(3) <> "" Then ' Boolean
            tempDict.Add key, CBool(match.SubMatches(3))
        ElseIf match.SubMatches(4) <> "" Then ' Array (簡易対応)
            Set tempArray = ParseJsonArray(match.SubMatches(4))
            tempDict.Add key, tempArray.ToArray
        ElseIf match.SubMatches(5) <> "" Then ' Nested Object (簡易対応)
            ' 再帰的にパースするが、これは非常に限定的。
            ' "choices"のようなトップレベルの配列内にオブジェクトがある場合に対応
            Set tempDict.Add key, ParseJsonToDictionary(match.SubMatches(5))
        ElseIf InStr(match.Value, ": null") > 0 Then ' Null
            tempDict.Add key, Null
        Else
            ' 未対応の型、または複雑な構造はスキップ
            ' Debug.Print "Unsupported JSON value type for key: " & key & " Value: " & match.Value
        End If
    Next

    ' "choices"配列の特別処理 (今回はトップレベルに"choices"があり、その中にオブジェクト配列があるケースに対応)
    If tempDict.Exists("choices") Then
        Dim choicesJson As String
        choicesJson = GetJsonValueByKey(jsonString, "choices")
        If choicesJson <> "" Then
            Set tempArray = ParseJsonArray(choicesJson)
            Dim parsedChoices As New Collection
            For Each choiceItem In tempArray
                ' choiceItemは文字列 "{...}" のはずなので、これをDictionaryにパース
                If IsObject(choiceItem) And TypeName(choiceItem) = "String" Then
                    parsedChoices.Add ParseJsonToDictionary(choiceItem)
                ElseIf IsObject(choiceItem) Then
                    parsedChoices.Add choiceItem ' すでにパース済みの可能性
                End If
            Next
            tempDict.Remove "choices"
            tempDict.Add "choices", CollectionToArray(parsedChoices)
        End If
    End If

    Set ParseJsonToDictionary = tempDict
End Function

' JSON配列文字列をVariant配列にパースする関数 (簡易版)
Private Function ParseJsonArray(ByVal arrayString As String) As Object
    Set ParseJsonArray = CreateObject("Scripting.Dictionary") ' 配列として扱うためDictionaryを利用

    If Left(arrayString, 1) <> "[" Or Right(arrayString, 1) <> "]" Then Exit Function
    arrayString = Mid(arrayString, 2, Len(arrayString) - 2) ' [] を除去

    Dim elements() As String
    ' 簡易的にカンマで分割するが、文字列中のカンマやネストされたオブジェクト/配列は考慮しない
    elements = Split(arrayString, "},{") ' オブジェクトの配列であることを前提

    Dim i As Long
    For i = LBound(elements) To UBound(elements)
        Dim element As String
        element = elements(i)
        If i > LBound(elements) Then element = "{" & element ' 最初の要素以外は"{"を付加
        If i < UBound(elements) Then element = element & "}" ' 最後の要素以外は"}"を付加

        ' ここで各要素を再度ParseJsonToDictionaryでパースする
        ParseJsonArray.Add i, ParseJsonToDictionary(element) ' KeyはインデックスとしてDictionaryに格納
    Next
End Function

' JSON文字列から指定キーの値の文字列を抽出する簡易関数 (ネスト対応は限定的)
Private Function GetJsonValueByKey(ByVal jsonString As String, ByVal key As String) As String
    Dim pattern As String
    Dim regex As Object
    Dim match As Object

    ' キーに続く値(文字列、数値、真偽値、配列、オブジェクトなど)を捕捉するパターン
    ' ネストされたオブジェクトや配列内のキーを誤検出しないよう注意が必要だが、ここでは簡易化
    pattern = """" & key & """\s*:\s*(""(?:[^""]|\\"")*""|\d+\.?\d*|true|false|\[.*?\]|\{.*?\})"

    Set regex = CreateObject("VBScript.RegExp")
    regex.Pattern = pattern
    Set match = regex.Execute(jsonString)

    If match.Count > 0 Then
        GetJsonValueByKey = match.Item(0).SubMatches(0)
    Else
        GetJsonValueByKey = ""
    End If
End Function

' CollectionをVariant配列に変換するヘルパー
Private Function CollectionToArray(col As Collection) As Variant()
    If col.Count = 0 Then
        CollectionToArray = Array() ' 空の配列を返す
        Exit Function
    End If
    Dim arr() As Variant
    ReDim arr(0 To col.Count - 1)
    Dim i As Long
    For i = 0 To col.Count - 1
        arr(i) = col.Item(i + 1)
    Next
    CollectionToArray = arr
End Function

' 使い方(イミディエイトウィンドウまたは別のSubから)
' Sub TestOpenAIChatRobust()
'     Dim response As String
'     response = GetOpenAIChatCompletionRobust("現在の日本の首相は誰ですか?")
'     Debug.Print "Response: " & response
' 
'     ' 複数のシステムメッセージとユーザープロンプト
'     response = GetOpenAIChatCompletionRobust( _
'         prompt:="次の文章を要約してください: 日本経済はデフレからの脱却を目指し、政府と日本銀行が連携して金融緩和政策を継続している。しかし、国際情勢の不確実性や原材料価格の高騰が課題となっている。", _
'         systemMessage:="あなたはプロの要約担当者です。重要な点を抽出し、簡潔にまとめてください。", _
'         maxTokens:=100 _
'     )
'     Debug.Print "Summary: " & response
' End Sub

PowerShellによる実装

PowerShellでは、Invoke-RestMethodコマンドレットがHTTP通信とJSON処理を強力にサポートしており、VBAに比べて遥かに簡潔に記述できます。

最小実装 (PowerShell)

# ■■■ 環境に合わせて以下を設定してください ■■■
$AzureOpenAIApiKey = "YOUR_AZURE_OPENAI_API_KEY" # 実際のAPIキーに置き換え
$AzureOpenAIResourceName = "YOUR_RESOURCE_NAME"  # リソース名に置き換え
$AzureOpenAIDeploymentName = "YOUR_DEPLOYMENT_NAME" # デプロイ名(例: gpt-35-turbo)に置き換え
$AzureOpenAIApiVersion = "2024-02-15" # APIバージョン

function Get-OpenAIChatCompletionMinimal {
    param (
        [string]$Prompt,
        [string]$SystemMessage = "You are a helpful assistant."
    )

    $uri = "https://$AzureOpenAIResourceName.openai.azure.com/openai/deployments/$AzureOpenAIDeploymentName/chat/completions?api-version=$AzureOpenAIApiVersion"

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

    $body = @{
        messages = @(
            @{ role = "system"; content = $SystemMessage },
            @{ role = "user"; content = $Prompt }
        )
        temperature = 0.7
        max_tokens  = 800
    } | ConvertTo-Json

    try {
        $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body
        # レスポンスからテキストとトークン数を抽出
        $completion = $response.choices[0].message.content
        $totalTokens = $response.usage.total_tokens

        Write-Host "Completion: $($completion)"
        Write-Host "Tokens Used: $($totalTokens)"
        return $completion
    }
    catch {
        Write-Error "API Error: $($_.Exception.Message)"
        Write-Error "Response Content: $($_.Exception.Response.Content)"
        return $null
    }
}

# 使い方
# Get-OpenAIChatCompletionMinimal -Prompt "現在の日本の首相は誰ですか?"

堅牢化 (PowerShell)

エラーハンドリングの強化、リトライロジック(指数バックオフ)、パラメータの抽象化、および詳細なロギングを組み込みます。

# ■■■ 環境に合わせて以下を設定してください ■■■
$AzureOpenAIApiKey = "YOUR_AZURE_OPENAI_API_KEY"
$AzureOpenAIResourceName = "YOUR_RESOURCE_NAME"
$AzureOpenAIDeploymentName = "YOUR_DEPLOYMENT_NAME"
$AzureOpenAIApiVersion = "2024-02-15"

# リトライ設定
$MaxRetryAttempts = 3
$BaseRetryDelaySeconds = 2 # 最初の待機時間 (秒)

function Invoke-AzureOpenAIChatCompletion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [string]$Prompt,
        [string]$SystemMessage = "あなたは親切なAIアシスタントです。",
        [ValidateRange(0.0, 2.0)]
        [float]$Temperature = 0.7,
        [int]$MaxTokens = 800,
        [string]$StopSequence = $null
    )

    $uri = "https://$AzureOpenAIResourceName.openai.azure.com/openai/deployments/$AzureOpenAIDeploymentName/chat/completions?api-version=$AzureOpenAIApiVersion"

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

    $bodyParams = @{
        messages = @(
            @{ role = "system"; content = $SystemMessage },
            @{ role = "user"; content = $Prompt }
        )
        temperature = $Temperature
        max_tokens  = $MaxTokens
    }

    if ($StopSequence) {
        $bodyParams.stop = $StopSequence
    }

    $body = $bodyParams | ConvertTo-Json -Depth 4 # 可読性のためDepthを高く設定

    Write-Verbose "Request URI: $uri"
    Write-Verbose "Request Body: $body"

    for ($attempt = 1; $attempt -le $MaxRetryAttempts; $attempt++) {
        $delaySeconds = $BaseRetryDelaySeconds * [math]::Pow(2, ($attempt - 1))

        try {
            Write-Verbose "Attempt $attempt of $MaxRetryAttempts..."
            $response = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body -ContentType "application/json" -ErrorAction Stop

            $completion = $response.choices[0].message.content
            $totalTokens = $response.usage.total_tokens

            Write-Verbose "API call successful (Attempt $attempt)."
            Write-Verbose "Tokens Used: $totalTokens"

            # 結果をカスタムオブジェクトとして返す
            [PSCustomObject]@{
                Completion = $completion
                TotalTokens = $totalTokens
                PromptTokens = $response.usage.prompt_tokens
                CompletionTokens = $response.usage.completion_tokens
                FinishReason = $response.choices[0].finish_reason
                Model = $response.model
                RawResponse = $response | ConvertTo-Json -Depth 8
            }
            return # 成功したら関数を終了
        }
        catch {
            $errorCode = $_.Exception.Response.StatusCode.value__
            $errorMessage = $_.Exception.Message
            $responseContent = $_.Exception.Response.Content

            Write-Warning "API Call Failed (Attempt $attempt): Status Code $errorCode - $errorMessage"
            Write-Verbose "Response Content: $responseContent"

            if ($errorCode -eq 429 -and $attempt -lt $MaxRetryAttempts) {
                Write-Warning "Rate limit hit. Retrying in $($delaySeconds) seconds..."
                Start-Sleep -Seconds $delaySeconds
            }
            elseif ($attempt -lt $MaxRetryAttempts) {
                Write-Warning "Non-rate limit error. Retrying in $($delaySeconds) seconds..."
                # 他のエラーもリトライするが、過度なリトライは避けるため間隔を置く
                Start-Sleep -Seconds $delaySeconds
            }
            else {
                Write-Error "Failed after $($MaxRetryAttempts) attempts. Last Error: $errorMessage"
                Write-Error "Last Response Content: $responseContent"
                throw $_.Exception # 最終的な失敗は例外を再スロー
            }
        }
    }

    # 全てのリトライが失敗した場合
    Write-Error "Operation failed to complete after $($MaxRetryAttempts) attempts."
    return $null
}

# 使い方
# $result = Invoke-AzureOpenAIChatCompletion -Prompt "日本の首都はどこですか?" -Verbose
# if ($result) {
#    Write-Host "質問: 日本の首都はどこですか?"
#    Write-Host "回答: $($result.Completion)"
#    Write-Host "使用トークン: $($result.TotalTokens)"
# }

# $summaryResult = Invoke-AzureOpenAIChatCompletion -Prompt "次の文章を要約してください: 日本経済はデフレからの脱却を目指し、政府と日本銀行が連携して金融緩和政策を継続している。しかし、国際情勢の不確実性や原材料価格の高騰が課題となっている。" `
#     -SystemMessage "あなたはプロの要約担当者です。重要な点を抽出し、簡潔にまとめてください。" `
#     -MaxTokens 100 `
#     -Verbose
# if ($summaryResult) {
#    Write-Host "要約: $($summaryResult.Completion)"
# }

# 失敗テスト (存在しないデプロイ名など意図的にエラーを起こす)
# $AzureOpenAIDeploymentName = "invalid-deployment" # 一時的に変更
# Invoke-AzureOpenAIChatCompletion -Prompt "これはエラーテストです。" -ErrorAction SilentlyContinue -Verbose
# $AzureOpenAIDeploymentName = "YOUR_DEPLOYMENT_NAME" # 元に戻す

ベンチ/検証

AI連携機能は、単に動くことだけでなく、予期せぬ挙動や性能劣化がないことを検証することが重要です。

  • 応答速度の計測:

    • 特定のプロンプトに対する初回応答時間と、複数回実行した際の平均応答時間を計測。ネットワークレイテンシ、モデルの処理負荷、クライアント側のJSONパース時間などが影響します。
    • VBA: Timer関数で処理時間を計測。
    • PowerShell: Measure-Commandコマンドレットで処理時間を計測。
  • エラーハンドリングのテスト:

    • 意図的に無効なAPIキー、存在しないデプロイ名、誤ったAPIバージョンを設定し、各言語の実装が適切にエラーを捕捉し、メッセージを返すか確認。
    • レートリミット(HTTP 429)をシミュレート(または実際に遭遇)し、リトライロジックが機能するか検証。
  • JSONパースの正確性:

    • VBAの場合、Debug.Print req.ResponseTextで生JSONを確認し、パース結果(messageContentなど)が期待通りか目視で確認。複雑な応答やエスケープシーケンス(\n, \"など)が正しく処理されるか。
    • PowerShellではConvertFrom-Jsonが強力なので、問題は少ないですが、レスポンス構造が変更された場合に備え、プロパティアクセスが適切か確認。
  • プロンプトと応答の品質:

    • 多様なプロンプト(短い質問、長い文章の要約、特定の形式での出力指示など)を与え、モデルの応答が期待される品質を満たしているか評価。
    • temperaturemax_tokensなどのパラメータを変更し、応答への影響を確認。
  • トークン使用量のモニタリング:

    • usage.total_tokensが適切に取得できているか確認し、コスト見積もりや最適化に利用できるか検証。

応用例/代替案

応用例

  • Excelデータ分析補助 (VBA):
    • セル範囲のテキストをAIで要約し、別のセルに出力。
    • 顧客コメントの感情分析(ポジティブ/ネガティブ)を自動化し、集計。
    • 商品名やカテゴリのゆらぎをAIで正規化。
  • PowerShell自動化スクリプトへの組み込み:
    • システムログの異常検出(異常パターンの説明生成)。
    • メール本文からタスクを抽出し、タスク管理システムと連携。
    • ドキュメント生成時のドラフト作成や校正支援。
  • シンプルなチャットボットプロトタイプ:
    • VBAのユーザーフォームやPowerShellのコンソールアプリで、AIとの簡単な対話インターフェースを構築。

代替案

  • Azure OpenAI SDK/ライブラリの使用:
    • Python (openaiライブラリ)、C# (Azure.AI.OpenAI NuGetパッケージ) など、公式SDKを使用することで、認証やJSON処理を抽象化でき、開発効率が向上します。外部ライブラリの導入が許可される環境であれば、これが最も推奨されるアプローチです。
  • Azure Functions/Logic Apps経由:
    • VBA/PowerShellからは、直接Azure OpenAI APIを叩くのではなく、Azure FunctionsやLogic Appsを介して間接的にアクセスする方法です。中間層を挟むことで、APIキーの管理を一元化し、レートリミット制御、ロギング、エラー通知などのガバナンスを強化できます。特にVBAの場合、Function AppのHTTPトリガーを呼び出す形にすることで、セキュリティと保守性が向上します。
  • Graph APIやMicrosoft 365 Copilot SDK (将来):
    • Microsoft 365環境との密な連携が求められる場合、将来的にはGraph APIを通じてCopilot機能にアクセスしたり、専用のSDKが提供されたりする可能性があります。これにより、Officeアプリ内でのAI活用がよりスムーズになるでしょう。

まとめ

本記事では、VBAおよびPowerShellという既存の強力なスクリプティング環境から、Azure OpenAI ServiceのREST APIを直接操作する手法を「濃く・マニアックに」解説しました。

  • VBAではWinHttpRequest、PowerShellではInvoke-RestMethod を用いることで、外部ライブラリに依存せずHTTP通信が可能です。
  • JSON形式のリクエスト/レスポンス の構造を理解し、VBAではScripting.Dictionaryや文字列操作、PowerShellではConvertTo-Json/ConvertFrom-Jsonで適切に処理することが鍵となります。特にVBAのJSONパースは、ネイティブ機能では限界があるため、どこまで許容するかを見極める必要があります。
  • 堅牢な実装 には、エラーハンドリング、タイムアウト設定、指数バックオフによるリトライロジック、そして安全なAPIキー管理が不可欠です。
  • 64bit環境 におけるVBAのCOMオブジェクト利用ではPtrSafeの直接的な記述は不要ですが、環境依存の考慮は常に重要です。

既存の業務プロセスにAIの力を注入するための直接的な経路として、VBAやPowerShellからのREST API連携は非常に強力な選択肢です。このアプローチを深く理解し、堅牢な実装を心がけることで、社内システムのAI化を加速させることができるでしょう。

運用チェックリスト

Azure OpenAI API連携を本番運用する前に、以下の項目を確認してください。

  • APIキーの管理:
    • APIキーはハードコードせず、安全な方法(環境変数、Azure Key Vaultなど)で管理されているか?
    • APIキーの定期的なローテーションが計画されているか?
    • 最小権限の原則に基づき、APIキーが必要なリソースにのみアクセスできるよう設定されているか?
  • レートリミット対策:
    • APIの呼び出し頻度が、Azure OpenAI Serviceのレートリミット(TPM: Tokens Per Minuteなど)を超過しないよう設計されているか?
    • 429 Too Many Requests エラーに対し、指数バックオフなどのリトライロジックが組み込まれているか?
  • エラーハンドリングとロギング:
    • HTTPステータスコード(4xx, 5xx)やネットワークエラーに対して、適切なエラー処理とユーザーへの通知が行われるか?
    • 重要なAPI呼び出し、成功/失敗、トークン使用量などがログに記録されるか?(特にprompt_tokens, completion_tokens
    • VBAではDebug.Printだけでなく、ファイル出力やExcelシートへのエラーログ記録を検討しているか?
  • コストモニタリング:
    • Azure PortalでAzure OpenAI Serviceの使用状況(トークン数、コスト)を定期的に監視する体制があるか?
    • 予期せぬ高コスト発生を防ぐためのアラート設定が行われているか?
  • プロンプトとレスポンスの検証:
    • 意図しない出力(ハルシネーション、不適切な内容)が発生しないよう、プロンプトの設計が十分に行われているか?
    • 必要に応じて、応答のサニタイジングやフィルタリング処理をクライアント側で実装しているか?
  • セキュリティとネットワーク:
    • API呼び出しがTLS(SSL)で保護されたエンドポイントに対して行われているか?
    • 必要に応じて、Azure OpenAI Serviceのネットワーク制限(VNet統合、IPファイアウォール)が適切に設定されているか?
  • バージョン管理とテスト:
    • APIバージョン(api-versionパラメータ)は固定されており、モデルやAPIの変更に追従できるよう考慮されているか?
    • コードの変更が、既存の動作に影響を与えないよう、テストプロセスが確立されているか?

参考リンク

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

コメント

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