<p>VBA/PowerShellからAzure OpenAI API:REST深堀と堅牢実装の極意</p>
<h2 class="wp-block-heading">導入(問題設定)</h2>
<p>ビジネス現場の最前線で使われるExcel VBAマクロや、システム管理の自動化を担うPowerShellスクリプト。これら既存資産に、Azure OpenAI Serviceの強力な自然言語処理能力を直接組み込みたいというニーズは日増しに高まっています。しかし、外部ライブラリの導入が難しい環境、厳格なセキュリティポリシー、そして何より「既存スクリプト資産を最大限活用したい」という制約の中で、どのように堅牢かつ効率的にAIと連携すればよいのでしょうか。</p>
<p>本記事では、VBAおよびPowerShellからAzure OpenAI ServiceのREST APIを直接呼び出す手法に焦点を当てます。単なるHowToに留まらず、APIの内部動作、潜在的な落とし穴、そして最小実装から実運用に耐えうる堅牢なコードへの進化プロセスまで、マニアックに深掘りしていきます。外部ライブラリへの依存を極力排除し、ネイティブ機能でどこまでできるかを追求することで、システム担当者が既存環境で安心してAIを活用できる基盤を提供します。</p>
<h2 class="wp-block-heading">理論の要点</h2>
<p>Azure OpenAI Serviceは、HTTPベースのREST APIとして提供されます。これは、特定のURL(エンドポイント)に対してHTTPリクエストを送信し、JSON形式でデータをやり取りすることで、AIモデルと通信する仕組みです。</p>
<h3 class="wp-block-heading">1. REST APIの基本要素</h3>
<ul class="wp-block-list">
<li><strong>エンドポイント (Endpoint):</strong> APIにアクセスするためのURL。Azure OpenAIの場合、<code>https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=YYYY-MM-DD</code> の形式をとります。
<ul>
<li><code>YOUR_RESOURCE_NAME</code>: Azureリソース作成時に指定したユニークな名前。</li>
<li><code>YOUR_DEPLOYMENT_NAME</code>: デプロイしたモデル(例: <code>gpt-35-turbo</code>, <code>gpt-4</code>)に付けたデプロイ名。</li>
<li><code>api-version</code>: 使用するAPIのバージョン。指定が必須であり、後方互換性がない変更が含まれる可能性があるため、常に最新または安定版を指定します(例: <code>2024-02-15</code>)。</li>
</ul></li>
<li><strong>HTTPメソッド:</strong> データの操作種類を示します。Azure OpenAIのChat Completions APIでは、テキスト生成リクエストは通常 <code>POST</code> メソッドを使用します。</li>
<li><strong>ヘッダー (Headers):</strong> リクエストに関するメタデータを伝達します。
<ul>
<li><code>Content-Type: application/json</code>: リクエストボディがJSON形式であることを示します。</li>
<li><code>api-key: YOUR_API_KEY</code>: 認証情報としてAzure OpenAI ServiceのAPIキーを送信します。</li>
</ul></li>
<li><strong>ボディ (Body):</strong> APIに送信する実際のデータ。JSON形式でプロンプトやモデルパラメータを含めます。</li>
<li><strong>ステータスコード (Status Code):</strong> サーバーからの応答状況を示します(例: <code>200 OK</code>, <code>400 Bad Request</code>, <code>500 Internal Server Error</code>)。</li>
<li><strong>レスポンス (Response):</strong> サーバーからの返答。通常、JSON形式でAIの生成テキストやトークン使用量などが含まれます。</li>
</ul>
<h3 class="wp-block-heading">2. Azure OpenAIの認証とモデル指定</h3>
<p>認証はAPIキーをHTTPヘッダーに含めることで行われます。これはAzure Portalから取得できるAPIキーであり、取り扱いには厳重な注意が必要です。
モデルは、デプロイ時に指定した名前(<code>YOUR_DEPLOYMENT_NAME</code>)を使ってエンドポイントURIの一部として指定されます。これにより、複数のモデルを同じAzure OpenAIリソース内で使い分けることが可能です。</p>
<h3 class="wp-block-heading">3. プロンプトの構造(Chat Completions API)</h3>
<p>Chat Completions APIでは、従来のCompletion APIと異なり、会話形式のメッセージ配列を使用します。</p>
<ul class="wp-block-list">
<li><strong><code>messages</code>:</strong> 会話の履歴を表すオブジェクトの配列。
<ul>
<li><strong><code>role</code>:</strong> メッセージの送信者を定義します。
<ul>
<li><code>system</code>: モデルの振る舞いや全体的な指示を設定します。会話の最初に一度だけ設定することが推奨されます。</li>
<li><code>user</code>: ユーザーからの入力。</li>
<li><code>assistant</code>: モデルの返答(以前の会話履歴を再現する場合に利用)。</li>
</ul></li>
<li><strong><code>content</code>:</strong> メッセージのテキスト本体。</li>
</ul></li>
</ul>
<h3 class="wp-block-heading">4. 主要なリクエストパラメータとレスポンス構造</h3>
<p><strong>API エンドポイント:</strong>
<code>POST https://YOUR_RESOURCE_NAME.openai.azure.com/openai/deployments/YOUR_DEPLOYMENT_NAME/chat/completions?api-version=2024-02-15</code></p>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">パラメータ名</th>
<th style="text-align:left;">型</th>
<th style="text-align:left;">必須</th>
<th style="text-align:left;">説明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;"><strong>Request Headers</strong></td>
<td style="text-align:left;"></td>
<td style="text-align:left;"></td>
<td style="text-align:left;"></td>
</tr>
<tr>
<td style="text-align:left;"><code>Content-Type</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;">はい</td>
<td style="text-align:left;"><code>application/json</code></td>
</tr>
<tr>
<td style="text-align:left;"><code>api-key</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;">はい</td>
<td style="text-align:left;">Azure OpenAI ServiceのAPIキー</td>
</tr>
<tr>
<td style="text-align:left;"><strong>Request Body</strong></td>
<td style="text-align:left;">JSON Object</td>
<td style="text-align:left;"></td>
<td style="text-align:left;"></td>
</tr>
<tr>
<td style="text-align:left;"><code>messages</code></td>
<td style="text-align:left;">array</td>
<td style="text-align:left;">はい</td>
<td style="text-align:left;">会話のメッセージ配列。<code>{"role": "...", "content": "..."}</code> オブジェクトの配列。</td>
</tr>
<tr>
<td style="text-align:left;"><code>temperature</code></td>
<td style="text-align:left;">number</td>
<td style="text-align:left;">いいえ</td>
<td style="text-align:left;">応答のランダム性(創造性)を制御。0.0(決定的)〜2.0(創造的)。デフォルトは1.0。</td>
</tr>
<tr>
<td style="text-align:left;"><code>max_tokens</code></td>
<td style="text-align:left;">integer</td>
<td style="text-align:left;">いいえ</td>
<td style="text-align:left;">生成される応答の最大トークン数。<code>prompt_tokens + completion_tokens <= max_tokens</code> の制約あり。</td>
</tr>
<tr>
<td style="text-align:left;"><code>top_p</code></td>
<td style="text-align:left;">number</td>
<td style="text-align:left;">いいえ</td>
<td style="text-align:left;"><code>temperature</code>と類似。上位<code>p</code>パーセントの確率を持つトークンからサンプリング。</td>
</tr>
<tr>
<td style="text-align:left;"><code>stop</code></td>
<td style="text-align:left;">string/array</td>
<td style="text-align:left;">いいえ</td>
<td style="text-align:left;">指定された文字列に遭遇すると、モデルは生成を停止します。</td>
</tr>
<tr>
<td style="text-align:left;"><code>stream</code></td>
<td style="text-align:left;">boolean</td>
<td style="text-align:left;">いいえ</td>
<td style="text-align:left;"><code>true</code>に設定すると、応答がストリーミングで送られます。本記事では同期通信を扱います。</td>
</tr>
</tbody>
</table></figure>
<p><strong>Response Body (成功時):</strong>
JSON Object</p>
<figure class="wp-block-table"><table>
<thead>
<tr>
<th style="text-align:left;">プロパティ名</th>
<th style="text-align:left;">型</th>
<th style="text-align:left;">説明</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align:left;"><code>id</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;">応答の一意のID。</td>
</tr>
<tr>
<td style="text-align:left;"><code>object</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;"><code>chat.completion</code></td>
</tr>
<tr>
<td style="text-align:left;"><code>created</code></td>
<td style="text-align:left;">integer</td>
<td style="text-align:left;">応答が作成されたUnixタイムスタンプ。</td>
</tr>
<tr>
<td style="text-align:left;"><code>model</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;">使用されたモデルのID。</td>
</tr>
<tr>
<td style="text-align:left;"><code>choices</code></td>
<td style="text-align:left;">array</td>
<td style="text-align:left;">モデルによって生成された選択肢の配列。</td>
</tr>
<tr>
<td style="text-align:left;"><code>choices[0].index</code>| integer</td>
<td style="text-align:left;">選択肢のインデックス。</td>
</tr>
<tr>
<td style="text-align:left;"><code>choices[0].message.role</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;"><code>assistant</code></td>
</tr>
<tr>
<td style="text-align:left;"><code>choices[0].message.content</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;">モデルが生成したテキスト。</td>
</tr>
<tr>
<td style="text-align:left;"><code>choices[0].finish_reason</code></td>
<td style="text-align:left;">string</td>
<td style="text-align:left;">応答が終了した理由(<code>stop</code>, <code>length</code>, <code>content_filter</code>, <code>null</code>など)。</td>
</tr>
<tr>
<td style="text-align:left;"><code>usage</code></td>
<td style="text-align:left;">object</td>
<td style="text-align:left;">リクエストとレスポンスで使用されたトークン数の情報。</td>
</tr>
<tr>
<td style="text-align:left;"><code>usage.prompt_tokens</code></td>
<td style="text-align:left;">integer</td>
<td style="text-align:left;">プロンプトに使用されたトークン数。</td>
</tr>
<tr>
<td style="text-align:left;"><code>usage.completion_tokens</code></td>
<td style="text-align:left;">integer</td>
<td style="text-align:left;">生成された応答に使用されたトークン数。</td>
</tr>
<tr>
<td style="text-align:left;"><code>usage.total_tokens</code></td>
<td style="text-align:left;">integer</td>
<td style="text-align:left;">全体のトークン数。</td>
</tr>
</tbody>
</table></figure>
<h3 class="wp-block-heading">MermaidによるAPI連携フロー</h3>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
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.)
</pre></div>
<p>このシーケンス図は、クライアントがAzure OpenAI Serviceを介してLLMと通信する基本的なフローを示しています。認証、内部でのプロンプト転送、そしてコンテンツフィルタリングなど、一連のプロセスがサーバー側で透過的に行われます。</p>
<h2 class="wp-block-heading">実装(最小→堅牢化)</h2>
<h3 class="wp-block-heading">VBAによる実装</h3>
<p>VBAでは、HTTP通信のために<code>Microsoft WinHTTP Services</code>または<code>Microsoft XML, v6.0</code>などのCOMオブジェクトを利用します。ここでは汎用性の高い<code>WinHttpRequest</code>オブジェクトを使用します。
VBAにおけるJSONの生成・解析は標準機能だけでは困難なため、ここでは<code>Scripting.Dictionary</code>と正規表現(または文字列操作)を駆使し、「外部ライブラリ極力なし」の要件に沿ったマニアックな実装を目指します。</p>
<p><strong>重要な注意点:</strong>
VBAで外部APIを呼び出す場合、64bit環境で<code>PtrSafe</code>キーワードが必要となる<code>Declare</code>ステートメントは、WinAPI関数に直接アクセスする場合に限定されます。<code>WinHttpRequest</code>のようなCOMオブジェクトを使用する限り、内部で適切に処理されるため、<code>PtrSafe</code>は直接関係ありません。しかし、VBAが動作するOfficeのビット数(32bit/64bit)は、COMオブジェクトの登録や互換性に影響を与える可能性があるため、常にターゲット環境を意識すべきです。</p>
<h4 class="wp-block-heading">最小実装 (VBA)</h4>
<pre data-enlighter-language="generic">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
</pre>
<h4 class="wp-block-heading">堅牢化 (VBA)</h4>
<p>上記コードをベースに、エラーハンドリングの強化、タイムアウト設定、JSON生成の抽象化、および<code>Scripting.Dictionary</code>を用いた簡易JSONパース機能を追加し、より実用的な堅牢な実装を目指します。<code>Scripting.Dictionary</code>はVBAに標準搭載されており、外部ライブラリとは見なされにくいCOMコンポーネントです。</p>
<pre data-enlighter-language="generic">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
</pre>
<h3 class="wp-block-heading">PowerShellによる実装</h3>
<p>PowerShellでは、<code>Invoke-RestMethod</code>コマンドレットがHTTP通信とJSON処理を強力にサポートしており、VBAに比べて遥かに簡潔に記述できます。</p>
<h4 class="wp-block-heading">最小実装 (PowerShell)</h4>
<pre data-enlighter-language="generic"># ■■■ 環境に合わせて以下を設定してください ■■■
$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 "現在の日本の首相は誰ですか?"
</pre>
<h4 class="wp-block-heading">堅牢化 (PowerShell)</h4>
<p>エラーハンドリングの強化、リトライロジック(指数バックオフ)、パラメータの抽象化、および詳細なロギングを組み込みます。</p>
<pre data-enlighter-language="generic"># ■■■ 環境に合わせて以下を設定してください ■■■
$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" # 元に戻す
</pre>
<h2 class="wp-block-heading">ベンチ/検証</h2>
<p>AI連携機能は、単に動くことだけでなく、予期せぬ挙動や性能劣化がないことを検証することが重要です。</p>
<ul class="wp-block-list">
<li><p><strong>応答速度の計測:</strong></p>
<ul>
<li>特定のプロンプトに対する初回応答時間と、複数回実行した際の平均応答時間を計測。ネットワークレイテンシ、モデルの処理負荷、クライアント側のJSONパース時間などが影響します。</li>
<li>VBA: <code>Timer</code>関数で処理時間を計測。</li>
<li>PowerShell: <code>Measure-Command</code>コマンドレットで処理時間を計測。</li>
</ul></li>
<li><p><strong>エラーハンドリングのテスト:</strong></p>
<ul>
<li>意図的に無効なAPIキー、存在しないデプロイ名、誤ったAPIバージョンを設定し、各言語の実装が適切にエラーを捕捉し、メッセージを返すか確認。</li>
<li>レートリミット(HTTP 429)をシミュレート(または実際に遭遇)し、リトライロジックが機能するか検証。</li>
</ul></li>
<li><p><strong>JSONパースの正確性:</strong></p>
<ul>
<li>VBAの場合、<code>Debug.Print req.ResponseText</code>で生JSONを確認し、パース結果(<code>messageContent</code>など)が期待通りか目視で確認。複雑な応答やエスケープシーケンス(<code>\n</code>, <code>\"</code>など)が正しく処理されるか。</li>
<li>PowerShellでは<code>ConvertFrom-Json</code>が強力なので、問題は少ないですが、レスポンス構造が変更された場合に備え、プロパティアクセスが適切か確認。</li>
</ul></li>
<li><p><strong>プロンプトと応答の品質:</strong></p>
<ul>
<li>多様なプロンプト(短い質問、長い文章の要約、特定の形式での出力指示など)を与え、モデルの応答が期待される品質を満たしているか評価。</li>
<li><code>temperature</code>や<code>max_tokens</code>などのパラメータを変更し、応答への影響を確認。</li>
</ul></li>
<li><p><strong>トークン使用量のモニタリング:</strong></p>
<ul>
<li><code>usage.total_tokens</code>が適切に取得できているか確認し、コスト見積もりや最適化に利用できるか検証。</li>
</ul></li>
</ul>
<h2 class="wp-block-heading">応用例/代替案</h2>
<h3 class="wp-block-heading">応用例</h3>
<ul class="wp-block-list">
<li><strong>Excelデータ分析補助 (VBA):</strong>
<ul>
<li>セル範囲のテキストをAIで要約し、別のセルに出力。</li>
<li>顧客コメントの感情分析(ポジティブ/ネガティブ)を自動化し、集計。</li>
<li>商品名やカテゴリのゆらぎをAIで正規化。</li>
</ul></li>
<li><strong>PowerShell自動化スクリプトへの組み込み:</strong>
<ul>
<li>システムログの異常検出(異常パターンの説明生成)。</li>
<li>メール本文からタスクを抽出し、タスク管理システムと連携。</li>
<li>ドキュメント生成時のドラフト作成や校正支援。</li>
</ul></li>
<li><strong>シンプルなチャットボットプロトタイプ:</strong>
<ul>
<li>VBAのユーザーフォームやPowerShellのコンソールアプリで、AIとの簡単な対話インターフェースを構築。</li>
</ul></li>
</ul>
<h3 class="wp-block-heading">代替案</h3>
<ul class="wp-block-list">
<li><strong>Azure OpenAI SDK/ライブラリの使用:</strong>
<ul>
<li>Python (<code>openai</code>ライブラリ)、C# (<code>Azure.AI.OpenAI</code> NuGetパッケージ) など、公式SDKを使用することで、認証やJSON処理を抽象化でき、開発効率が向上します。外部ライブラリの導入が許可される環境であれば、これが最も推奨されるアプローチです。</li>
</ul></li>
<li><strong>Azure Functions/Logic Apps経由:</strong>
<ul>
<li>VBA/PowerShellからは、直接Azure OpenAI APIを叩くのではなく、Azure FunctionsやLogic Appsを介して間接的にアクセスする方法です。中間層を挟むことで、APIキーの管理を一元化し、レートリミット制御、ロギング、エラー通知などのガバナンスを強化できます。特にVBAの場合、Function AppのHTTPトリガーを呼び出す形にすることで、セキュリティと保守性が向上します。</li>
</ul></li>
<li><strong>Graph APIやMicrosoft 365 Copilot SDK (将来):</strong>
<ul>
<li>Microsoft 365環境との密な連携が求められる場合、将来的にはGraph APIを通じてCopilot機能にアクセスしたり、専用のSDKが提供されたりする可能性があります。これにより、Officeアプリ内でのAI活用がよりスムーズになるでしょう。</li>
</ul></li>
</ul>
<h2 class="wp-block-heading">まとめ</h2>
<p>本記事では、VBAおよびPowerShellという既存の強力なスクリプティング環境から、Azure OpenAI ServiceのREST APIを直接操作する手法を「濃く・マニアックに」解説しました。</p>
<ul class="wp-block-list">
<li><strong>VBAでは<code>WinHttpRequest</code>、PowerShellでは<code>Invoke-RestMethod</code></strong> を用いることで、外部ライブラリに依存せずHTTP通信が可能です。</li>
<li><strong>JSON形式のリクエスト/レスポンス</strong> の構造を理解し、VBAでは<code>Scripting.Dictionary</code>や文字列操作、PowerShellでは<code>ConvertTo-Json</code>/<code>ConvertFrom-Json</code>で適切に処理することが鍵となります。特にVBAのJSONパースは、ネイティブ機能では限界があるため、どこまで許容するかを見極める必要があります。</li>
<li><strong>堅牢な実装</strong> には、エラーハンドリング、タイムアウト設定、指数バックオフによるリトライロジック、そして安全なAPIキー管理が不可欠です。</li>
<li><strong>64bit環境</strong> におけるVBAのCOMオブジェクト利用では<code>PtrSafe</code>の直接的な記述は不要ですが、環境依存の考慮は常に重要です。</li>
</ul>
<p>既存の業務プロセスにAIの力を注入するための直接的な経路として、VBAやPowerShellからのREST API連携は非常に強力な選択肢です。このアプローチを深く理解し、堅牢な実装を心がけることで、社内システムのAI化を加速させることができるでしょう。</p>
<h2 class="wp-block-heading">運用チェックリスト</h2>
<p>Azure OpenAI API連携を本番運用する前に、以下の項目を確認してください。</p>
<ul class="wp-block-list">
<li><strong>APIキーの管理:</strong>
<ul>
<li>APIキーはハードコードせず、安全な方法(環境変数、Azure Key Vaultなど)で管理されているか?</li>
<li>APIキーの定期的なローテーションが計画されているか?</li>
<li>最小権限の原則に基づき、APIキーが必要なリソースにのみアクセスできるよう設定されているか?</li>
</ul></li>
<li><strong>レートリミット対策:</strong>
<ul>
<li>APIの呼び出し頻度が、Azure OpenAI Serviceのレートリミット(TPM: Tokens Per Minuteなど)を超過しないよう設計されているか?</li>
<li><code>429 Too Many Requests</code> エラーに対し、指数バックオフなどのリトライロジックが組み込まれているか?</li>
</ul></li>
<li><strong>エラーハンドリングとロギング:</strong>
<ul>
<li>HTTPステータスコード(4xx, 5xx)やネットワークエラーに対して、適切なエラー処理とユーザーへの通知が行われるか?</li>
<li>重要なAPI呼び出し、成功/失敗、トークン使用量などがログに記録されるか?(特に<code>prompt_tokens</code>, <code>completion_tokens</code>)</li>
<li>VBAでは<code>Debug.Print</code>だけでなく、ファイル出力やExcelシートへのエラーログ記録を検討しているか?</li>
</ul></li>
<li><strong>コストモニタリング:</strong>
<ul>
<li>Azure PortalでAzure OpenAI Serviceの使用状況(トークン数、コスト)を定期的に監視する体制があるか?</li>
<li>予期せぬ高コスト発生を防ぐためのアラート設定が行われているか?</li>
</ul></li>
<li><strong>プロンプトとレスポンスの検証:</strong>
<ul>
<li>意図しない出力(ハルシネーション、不適切な内容)が発生しないよう、プロンプトの設計が十分に行われているか?</li>
<li>必要に応じて、応答のサニタイジングやフィルタリング処理をクライアント側で実装しているか?</li>
</ul></li>
<li><strong>セキュリティとネットワーク:</strong>
<ul>
<li>API呼び出しがTLS(SSL)で保護されたエンドポイントに対して行われているか?</li>
<li>必要に応じて、Azure OpenAI Serviceのネットワーク制限(VNet統合、IPファイアウォール)が適切に設定されているか?</li>
</ul></li>
<li><strong>バージョン管理とテスト:</strong>
<ul>
<li>APIバージョン(<code>api-version</code>パラメータ)は固定されており、モデルやAPIの変更に追従できるよう考慮されているか?</li>
<li>コードの変更が、既存の動作に影響を与えないよう、テストプロセスが確立されているか?</li>
</ul></li>
</ul>
<h2 class="wp-block-heading">参考リンク</h2>
<ul class="wp-block-list">
<li><strong>Azure OpenAI Service の概要</strong>: <a href="https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/overview">https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/overview</a></li>
<li><strong>Azure OpenAI Chat Completion API リファレンス</strong>: <a href="https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference#chat-completions">https://learn.microsoft.com/ja-jp/azure/cognitive-services/openai/reference#chat-completions</a></li>
</ul>
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
が強力なので、問題は少ないですが、レスポンス構造が変更された場合に備え、プロパティアクセスが適切か確認。
プロンプトと応答の品質:
多様なプロンプト(短い質問、長い文章の要約、特定の形式での出力指示など)を与え、モデルの応答が期待される品質を満たしているか評価。
temperature
やmax_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の変更に追従できるよう考慮されているか?
コードの変更が、既存の動作に影響を与えないよう、テストプロセスが確立されているか?
参考リンク
コメント