完全未経験がDX職で奮闘する話① RAGとAzureでのつまづき
はじめに
こんにちは。かい(@kaithon_dx)です。
プライム上場企業でDX担当をしている28歳・完全未経験の派遣社員です。
前回の記事(完全未経験がDX職で奮闘する話⓪)では、キャリアと全体像の話を書きました。
今回は技術的に最もつまずいたことに絞って書いていきます。
結論からお伝えします。
Azure AI Foundry周りの情報は、GPT-5.2にもClaude Opus 4.5にもGeminiにも聞いても、軒並み正確に答えてもらえなかった。
Web検索をONにしても、システムプロンプトに「最新のAzure公式ドキュメントを参照せよ」と制約をかけてもダメでした。これはAzure AI Foundryを触った人間なら共感してもらえると思います。
この記事では、私がぶつかった壁を順番に整理し、最後にAzure AI FoundryでデプロイしたモデルをPythonから呼び出す最小構成のコードを紹介します。
初期構想:何を作ろうとしていたか
社内ドキュメント(PDF・PNG)を検索・要約できるRAGシステムです。大きく2つのサブシステムで構成しています。
サブシステム①:OCRパイプライン
複雑な数式や図表を含むPDF・PNGをOCR → Markdown化する処理。Azure AI Foundryからデプロイしたモデル(Claude Opus 4.6等)を使います。
- 数式をLaTeXで正確に表現すること
- 結合セルの多い表を単一セルに分解してテーブルを作成すること
このあたりの実現が非常に大変でした。
※こちらについての詳細は次回の記事でご紹介します。
サブシステム②:チャットUI + 検索基盤
チャットUIの裏側で、OCR済みMarkdownをAzure AI Searchでインデックス化し、セマンティック検索+キーワード検索のハイブリッド検索を行う。頭脳としてGPT-5.2やClaude Opus 4.5を接続します。
構成自体はシンプルです。
しかし、Azureのリソースを立ち上げる段階で下記の4つの壁に阻まれ、つまずきました。
壁①:リージョンによるモデル制限
Azure AI Foundryでモデルをデプロイしようとして、まず判明したのがこれです。
リージョンによってデプロイできるモデルが違う。
結局、GPT-5.2もClaude Opus 4.5も使うにはEast US 2にAI Foundryを構える必要がありました。Japan Eastではダメだったんです。エンタープライズ環境ということもあり「データは日本リージョンに」という形で進めていたのに、結局East US2でのデプロイが必須でした。
壁②:Azure用語の多様性地獄
Azureは1つの概念に対して複数の名前が存在することが非常に多い。
例えば「Azure AI Search」。Azure Portalでリソースを作ろうとすると、**「Search Service(検索サービス)」**という名前で出てきます。初心者だった自分は「あれ、違うもの?」となりました。
さらにAI Foundry自体が「Azure AI Studio → Azure AI Foundry → Microsoft Foundry」と短期間で名前が変わっています。検索しても古い名称の情報がヒットするため、今どの名前が正しいのかすら判断しづらい。
この「名前の多様性」が、後述する「LLMが正確に答えられない」問題の根本原因の1つです。学習データにある旧名称と現在のUI上の名称が一致しないため、モデルが混乱するんじゃないかなと推測しています。
壁③:閉域ネットワーク構成との格闘
エンタープライズ環境なので、Azureリソースをパブリックインターネットに露出させることはできません。ここからVNet(仮想ネットワーク)、Private Endpoint、VPN、DNSリゾルバー、RBACといった、未経験者には完全に未知の領域だったので、理解と実装に非常に苦労しました。
※LLMはこういった「すでに確立された知識」を教えてくれるのが得意なので、ここは大いに助けられました。
最初のイメージではAI Foundryハブを作ってManaged VNetで閉域構成を組む予定でしたが、用意いただいたサブスクリプションでMachine Learningリソースプロバイダーの権限がなく、申請し承認されるまでのタイムラグを待つのならと方向転換。
クラシック構成でVNet → サブネット → Private Endpoint → P2S VPN → プライベートDNSゾーン → DNSリゾルバー → RBAC設定…と1つずつ積み上げて、接続自体は成功させました。
※ RBAC に関しては、「このロールを付ければ良いよ」というおすすめ機能のようなものがなく、膨大なリストから自力で検索して見つけ出す必要があります。さらに Azure 環境を日本語に設定していると、ロール名の一部が日本語化され、一部は英語のままという羅列になっており、英語と日本語の両方で検索を駆使しないと見つけられません。
そして、かくかくしかじかを上長に報告したところ、こう言われました。
「その構成で、情シスじゃない自分たちが運用・保証を続けられるか?」
というのも、私の所属が情報システム部ではなく、事業部付けのDX担当だからです。ネットワーク構成の保証は事業部の責任範囲を超えています。そこで、閉域構成の知見は回収した上で、Azure上に全てを構築してWebでデプロイする方針自体を見直すことになりました。
方向転換:Azure = APIデプロイ基盤に割り切る
結局、Azureの利用目的を「モデルのAPIデプロイ」に絞る判断をしました。
UI(フロントエンド)はOpenWebUI(OSSのチャットUI)を採用し、AzureでデプロイしたモデルにはAPI経由で接続する構成です。ちなみにAI Foundryのプレイグラウンドから簡易にデプロイできるWebアプリUIも試しましたが、カスタマイズ性が低く要件を満たせませんでした。
この方向転換により開発速度は一気に上がりました。
※OpenWebUIの構成については別の記事で詳しく書きます。
壁④(本題):LLMに聞いても正確に答えてもらえない
ここからが本題です。
Azureでデプロイしたモデルを、自作のPythonコードからAPI経由で呼び出す。やりたいことはシンプルです。しかしここでLLMたちが軒並み間違った情報を返してきました。
何が間違っていたか
1. SDKの指定が古い / 存在しない
LLMが存在しないインポートパスを平然と返してくる。Web検索をONにしても、旧バージョンのドキュメントを拾って古いコードを生成する。
Azure OpenAI(標準デプロイ)とサーバーレスAPI(マーケットプレイスモデル)では、使うSDK自体が違います。でも、LLMはこの2つを混ぜた回答を返してくることがありました。
2. エンドポイントURLの形式が間違っている
Azure OpenAI(標準デプロイ)とサーバーレスAPI(マーケットプレイスモデル)ではエンドポイントURLの形式が全く異なる。LLMはこの区別ができず、混在した情報を返す。
さらに、Azure の仕様としてややこしいのが、以下の二つのパターンがある点です:
- Foundry リソース自体のキーとエンドポイントを利用できる場合
- Foundry ポータルから各モデルのキーとターゲット URI を取得する必要がある場合
3. API Versionの不一致
Azure OpenAIのAPIはapi_versionパラメータが必須ですが、LLMがその必要性すら返してくれなかったり、「202601」のような架空のバージョンを回答したりすることがありました。
実際には「2024-12-10」などの正しいバージョンを指定しないと、Azure API を通して GPT-5.2-chat などのモデルを利用することはできないため、ここにも非常に苦労しました。
なぜLLMは正確に答えられないのか
- Azure自体が過渡期の真っ只中: AI Studio → AI Foundry → Microsoft Foundryと名前が変わり、SDKも頻繁に更新されるため、カットオフ以降の情報収集をもってしてもハルシネーションが起こりうる
- 名前の多義性: 同じ概念に複数の名前があるため、モデルが文脈を正しく解釈できない
- 実例記事がWebに少ない: エンタープライズ環境での構成情報は公開されにくく、学習データに入りにくい
実動コード:最小構成
ここからが、LLMに何度聞いても辿り着けず、公式ドキュメントとエラーログの往復でようやく見つけた実際に動いたコードです。
① Azure OpenAIでGPT-5.2-chatを呼び出す
GPT系モデルをAzure OpenAIリソースにデプロイした場合、openaiライブラリのAzureOpenAIクラスを使います。
pip install openai
import os
from openai import AzureOpenAI
client = AzureOpenAI(
azure_endpoint=os.getenv("AZURE_OPENAI_ENDPOINT"),
# 例: "https://my-resource-name.openai.azure.com/"
api_key=os.getenv("AZURE_OPENAI_API_KEY"),
api_version="2025-01-01-preview",
# ↑ 重要。LLMが返すバージョンは高確率で古い。
# 最新は公式ドキュメントで確認すること。
)
response = client.chat.completions.create(
model="gpt-52-chat",
# ↑ 「モデル名」ではなく「デプロイ名」を指定する。
# AI Foundryでデプロイ時に自分がつけた名前。
messages=[
{"role": "system", "content": "あなたはアシスタントです。"},
{"role": "user", "content": "Azureのリージョンについて教えてください。"},
],
temperature=0.0,
)
print(response.choices[0].message.content)
ハマりポイント:
-
modelにはモデル名(gpt-5.2)ではなく、自分がつけたデプロイ名を入れる -
api_versionは公式のAPIバージョン一覧で最新を確認する -
azure_endpointの末尾に/がないとエラーになるケースがある
② AI FoundryでデプロイしたClaude Opus 4.6を呼び出す(サーバーレスAPI)
AI FoundryでデプロイしたClaudeOpus4.6を呼び出すには、Anthropic互換 Messages APIでが答えです。
ポイントは2つだけ:
- FOUNDRY_INFERENCE_ENDPOINT(または AZURE_AI_INFERENCE_ENDPOINT)が resource root でも、コード側で /anthropic/v1/messages に正規化する
- FOUNDRY_INFERENCE_MODEL(または AZURE_AI_INFERENCE_MODEL)が未設定なら claude-opus-4-6 を使う
.env(例)
FOUNDRY_INFERENCE_ENDPOINT=https://<resource-name>.services.ai.azure.com
FOUNDRY_INFERENCE_KEY=<your-key>
# FOUNDRY_INFERENCE_MODEL=claude-opus-4-6 # 任意(未指定なら既定)
【最小Python(標準ライブラリのみ)】
import json
import os
import urllib.request
from urllib.parse import urlparse
ENV_ENDPOINT_KEYS = ("AZURE_AI_INFERENCE_ENDPOINT", "FOUNDRY_INFERENCE_ENDPOINT")
ENV_KEY_KEYS = ("AZURE_AI_INFERENCE_KEY", "FOUNDRY_INFERENCE_KEY")
ENV_MODEL_KEYS = ("AZURE_AI_INFERENCE_MODEL", "FOUNDRY_INFERENCE_MODEL")
DEFAULT_MODEL = "claude-opus-4-6"
DEFAULT_MAX_TOKENS = 2048
DEFAULT_TEMPERATURE = 0.0
def _get_env(*keys: str, default: str = "") -> str:
for key in keys:
value = os.getenv(key)
if value is not None:
value = value.strip()
if value != "":
return value
return default
def load_dotenv(path: str = ".env") -> None:
if not os.path.exists(path):
return
with open(path, "r", encoding="utf-8") as file:
for line in file:
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
if "=" not in stripped:
continue
key, value = stripped.split("=", 1)
key = key.strip()
value = value.strip().strip("\"").strip("'")
os.environ[key] = value
def _is_resource_root_endpoint(endpoint: str) -> bool:
try:
parsed = urlparse(endpoint)
except Exception:
return False
path = (parsed.path or "").strip("/")
return parsed.netloc.endswith(".services.ai.azure.com") and path == ""
def _is_anthropic_messages_endpoint(endpoint: str) -> bool:
try:
parsed = urlparse(endpoint)
except Exception:
return False
path = (parsed.path or "").rstrip("/").lower()
return path.endswith("/anthropic/v1/messages")
def _normalize_anthropic_messages_endpoint(endpoint: str) -> str:
parsed = urlparse(endpoint)
path = parsed.path or ""
stripped = path.rstrip("/")
lower = stripped.lower()
if lower.endswith("/anthropic/v1/messages"):
return endpoint.rstrip("/")
if lower.endswith("/anthropic"):
return endpoint.rstrip("/") + "/v1/messages"
if _is_resource_root_endpoint(endpoint):
return endpoint.rstrip("/") + "/anthropic/v1/messages"
return endpoint.rstrip("/")
# ---- 実行 ----
load_dotenv(".env")
endpoint = _get_env(*ENV_ENDPOINT_KEYS)
key = _get_env(*ENV_KEY_KEYS)
model = _get_env(*ENV_MODEL_KEYS)
if not endpoint or not key:
raise RuntimeError(
"AZURE_AI_INFERENCE_ENDPOINT と AZURE_AI_INFERENCE_KEY(または FOUNDRY_*)を .env に設定してください。"
)
normalized = _normalize_anthropic_messages_endpoint(endpoint)
if not _is_anthropic_messages_endpoint(normalized):
raise RuntimeError(
"このスクリプトは画像入力を使うため、Anthropic互換 Messages エンドポイントが必要です。\n"
"例: https://<resource>.services.ai.azure.com/anthropic/v1/messages\n"
"現在: " + normalized
)
if not model:
model = DEFAULT_MODEL
max_tokens = int(_get_env("AZURE_AI_INFERENCE_MAX_TOKENS", default=str(DEFAULT_MAX_TOKENS)))
temperature = float(_get_env("AZURE_AI_INFERENCE_TEMPERATURE", default=str(DEFAULT_TEMPERATURE)))
payload = {
"model": model,
"max_tokens": max_tokens,
"temperature": temperature,
"messages": [{"role": "user", "content": "疎通テスト:Hello Claude Opus 4.6 on Foundry"}],
}
req = urllib.request.Request(
normalized,
data=json.dumps(payload).encode("utf-8"),
headers={
"content-type": "application/json",
"api-key": key,
},
method="POST",
)
with urllib.request.urlopen(req, timeout=120) as resp:
print(resp.read().decode("utf-8"))
実行:
python main.py
おわりに
Azure AI Foundryは強力な基盤ですが、2026年2月時点では過渡期の真っ只中にあり、LLMがカバーしきれない領域がまだ多く残っています。
「LLMに聞けばわかる」時代に、LLMに聞いてもわからないことがある。
この体験は、逆説的に「人間が手を動かして自分で確かめて検証する価値」を教えてくれた気がします。
同じようにAzure AI Foundryで格闘している方の参考になれば幸いです。
次回は、Claude Opus 4.6を使ったOCRパイプライン(技術書PDF → Markdown変換)の実装について書きます。その次はOpenWebUIの構成について書く予定です。
Discussion