医療機関の文書を大量に電子化し、AIで構造化して活用する基盤を検討しているアーキテクトやバックエンドエンジニアに向けた内容です。スキャン文書をFHIR(医療情報交換のための標準規格)準拠のデータに変換するパイプラインは、一見よくあるOCR(画像から文字を読み取る技術)連携の延長に見えます。ところが本番運用に入った途端、コンプライアンスと性能のトレードオフが同時に牙をむく設計領域です。
何が起きるかというと、サーバーレス構成で組んだ請求処理パイプラインが、ピーク時に応答遅延でSLA(サービス品質の合意水準)を割り込む事象です。原因はAzure FunctionsのConsumptionプラン(従量課金で自動スケールする実行環境)が抱えるコールドスタート(休止中の関数が呼び出されて起動するまでの遅延)にあります。この遅延は2〜3秒に達することがあり、請求書類の即時判定を求められる業務では致命的です。
何が起きるか:スケーリング方式のミスマッチ
月間で退院サマリー2万5千件、検査報告書8千件、画像PDF1万2千件を処理するような中規模病院の運用を想定します。請求チームはクレーム却下を避けるため、構造化された診断・処置コードを30秒以内に受け取る必要があります。
ここでAzure Functions Consumptionを選ぶと、リクエストが途切れた直後にインスタンスが休止し、次の呼び出しでコールドスタートが発生します。同時に、単一の埋め込みフィールドに異種文書(退院サマリーと検査報告書とレントゲン画像PDF)をまとめて格納すると、類似検索の精度が落ちるという副作用も重なります。
さらに厄介なのは、PDFをLLM(大規模言語モデル)に投げる際のトークン上限です。32,000トークンの制限を意識せずに長大な文書をそのまま送ると、途中で切り詰められて抽出漏れが発生します。これは監査ログ上「処理済み」と記録されるため、発見が遅れやすい落とし穴です。
なぜ起きるか:3層に分解して考える
原因は大きく3つの層に分けられます。
- インフラ層: サーバーレスの弾力性とリアルタイムSLAが根本的に相性が悪い。Consumptionプランは休止インスタンスの再起動コストを利用者側に転嫁する設計
- データ層: ベクトルインデックス(類似検索用の数値表現の集合)を「一時的な副産物」として扱い、埋め込みモデルのバージョン管理を怠る
- プロセス層: バッチ処理でトレーサビリティ(処理の追跡可能性)を犠牲にし、監査要件を後付けで満たそうとする
これらは独立した問題ではなく、連鎖して悪化します。たとえばバッチサイズを大きくしてLLM呼び出しのトークン消費を抑えようとすると、1リクエストあたりの処理時間が伸び、結果としてコールドスタートと重なったときの遅延が二重に積み上がります。
OpenAIの埋め込みモデルが更新されると、既存のベクトル空間がずれて類似検索の結果が変わってしまう問題も見逃せません。バージョンを固定せずに運用していると、ある日突然「似ている症例が検索に出てこなくなった」という報告が上がってきます。
自分のプロジェクトが該当するか確認する方法
以下の観点で、現状の構成を点検してみてください。
# Azure Functionsのホスティングプランを確認する
az functionapp show --name <FUNCTION_APP名> --resource-group <RG名> --query "sku"
# 出力が Dynamic ならConsumptionプラン(コールドスタートのリスクあり)
# ElasticPremium ならPremiumプランAzure Cognitive Search(Azureのハイブリッド検索サービス)を使っている場合は、インデックス定義でベクトルフィールドが文書種別ごとに分かれているか確認します。管理ポータルの「インデックス」画面、または以下のようなREST APIで定義を取得できます。
# インデックス定義を取得してフィールド構成を確認する
curl -X GET "https://<検索サービス名>.search.windows.net/indexes/<インデックス名>?api-version=2023-11-01" \
-H "api-key: <管理キー>"もう一つの確認ポイントは、埋め込みモデルのバージョンがコード内やインデックス名に明示されているかどうかです。text-embedding-3-small-v1 のようにバージョンを含めた命名になっていなければ、モデル更新時に無停止移行できません。
BAA(Business Associate Agreement、医療情報を扱う委託先との契約)についても、利用中の各サービス(検索エンジン、LLM API、ストレージ)が個別に締結対象かどうかをベンダーの契約ドキュメントで確認する必要があります。PineconeやQdrantのような外部ベクトルDBを使う場合、Azure単体構成とは別にBAAが必要になる点も見落とされがちです。
対策の手順
ステップ1: リアルタイムSLAが求められる処理を切り分ける
請求処理のように30秒以内の応答が必須な処理と、夜間バッチで十分な処理を分離します。前者はAzure Functions Premiumプラン、またはAzure Container Apps(コンテナを実行するマネージドサービス)に移行します。Premiumプランは事前ウォームアップされたインスタンスを維持するため、コールドスタートをほぼ解消できます。
ステップ2: 文書種別ごとにベクトルフィールドを分離する
退院サマリー、検査報告書、画像PDFのOCR結果を、それぞれ別のベクトルフィールドまたは別インデックスに格納します。検索時にフィルタ条件で文書種別を絞り込めるようにすることで、類似検索の精度低下を防げます。
ステップ3: トークン上限を超える文書を事前に分割する
PDFをLLMに渡す前に、ページ単位またはセクション単位でチャンク分割し、各チャンクが上限内に収まっているかをコード側で検証します。分割結果は後で結合できるよう、文書IDと連番を必ず付与します。
ステップ4: 埋め込みバージョンをインデックス名に含める
たとえば discharge-summary-embed-v3 のように命名し、モデル更新時は新バージョンのインデックスを並行構築してから切り替えます。これによりゼロダウンタイムでの移行が可能になります。
ステップ5: 監査ログを変換ステップごとに残す
バッチ処理を導入する場合でも、各文書がどのステップをいつ通過したかをログに残す設計にします。30日間の保持ポリシーとフォレンジック調査に耐えられるよう、ログ自体の改ざん防止(WORMストレージなど)も検討対象になります。
まとめ
医療文書処理パイプラインの落とし穴は、AIモデルの精度よりも周辺のオーケストレーション設計に潜んでいます。
- Azure Functionsのプランを az functionapp show で確認し、Consumptionのままリアルタイム処理を回していないか点検する
- ベクトルインデックスの定義をAPIで取得し、文書種別ごとにフィールドが分かれているか確認する
- 埋め込みモデルのバージョンがインデックス名やコードに明示されているか見直す
- 外部ベクトルDBを使う場合、BAAの締結範囲を契約書ベースで再確認する
これらは一度に全部やり切る必要はありません。まずはリアルタイム処理とバッチ処理の切り分けから着手すると、影響範囲が把握しやすくなります。