概要

NVIDIA AI Enterpriseを導入し、推論基盤としてNIM(NVIDIA Inference Microservices)のコンテナを運用している企業では、「RAGの埋め込みとリランクも、すでに動いているNIMで賄いたい」という要望がよく挙がります。GPUの管理やコンテナの運用手順をLLMと揃えられ、サポートの窓口も一本化できるためです。

NIMの埋め込みはOpenAI互換の /v1/embeddings を提供しているため、接続先のURLを差し替えるだけで動くように見えます。ところが実際には、非対称の埋め込みモデルは input_type を指定しないとリクエストを拒否する、リランカーのエンドポイントと形式がvLLMと異なる といった違いがあり、そのままでは動きません。

埋め込みとリランクが検索の中でどの位置にあるかを確認したうえで、NIMに切り替えたときに何が変わるか、運用で気をつけるべきモデルと次元の一貫性、モデル変更時の再埋め込みについて解説します。


検索の中での埋め込みとリランクの位置

社内RAGの検索は、一般的に2段で構成します。

質問 ↓ 埋め込み(質問をベクトル化) [1段目:候補を広く集める] ├ ベクトル検索(意味の近さ) └ キーワード検索(型番・固有名詞の一致) ↓ 統合(RRFなど)して候補を数十件に絞る [2段目:リランク] └ クロスエンコーダが「質問と候補」を1組ずつ読んで再評価 ↓ 上位k件 LLMに根拠として渡す

埋め込みモデル は、文書と質問をそれぞれ独立にベクトルへ変換します。事前に全文書を計算しておけるため高速ですが、質問と文書を同時に見ていないぶん、細かな関連の判定は苦手です。

リランカー は、質問と候補の文章を1組にして読み、関連度を直接評価します。精度は高いものの、候補ごとに計算するため、全文書に対しては使えません。そこで、1段目で広く集めた候補だけを2段目で並べ替えます。

NIMを使う場合も、この構成自体は変わりません。変わるのは、それぞれのAPIを呼ぶときのリクエストとレスポンスの形です。


NIMにすると何が変わるか

埋め込み:非対称モデルは input_type が必須

NIMで提供される検索向けの埋め込みモデル(nv-embedqa系など)の多くは、非対称(asymmetric)モデル です。検索に使う質問と、登録する文書とで、異なる前処理をしてベクトル化します。そのため、リクエストに「これは質問か、文書か」を示す input_type が必要です。

// 質問を埋め込むとき {"model": "nv-embedqa-...", "input": "主軸の異音の原因は?", "input_type": "query"} // 文書(チャンク)を登録するとき {"model": "nv-embedqa-...", "input": "主軸ベアリングの摩耗により…", "input_type": "passage"}

input_type を付けないと、NIMは400番台のエラーで拒否します。一方、vLLMやOllamaでは、このフィールドは必要ありません。同じコードで両方の接続先を扱うには、この差を吸収する必要があります。

リランカー:エンドポイントと形式が異なる

vLLMのリランカーは /v1/rerank で提供されますが、NIMのリランカーは /v1/ranking で提供され、リクエストとレスポンスの形も異なります。

項目vLLM(/v1/rerank)NIM(/v1/ranking)
質問"query": "文字列""query": {"text": "文字列"}
候補"documents": ["…", "…"]"passages": [{"text": "…"}, …]
返す件数"top_n" で指定全候補を順位付けして返す
結果results[].index と relevance_scorerankings[].index と logit

スコアの尺度も異なる

見落としやすいのがスコアの尺度です。vLLMの relevance_score が0〜1程度の範囲に収まるモデルが多いのに対し、NIMの logit は正規化されていない値で、負の値も取ります。リランクのスコアに「0.5以上だけ採用」のような固定の閾値を設けている場合、接続先を切り替えると、すべての候補が落ちる、あるいはすべて通るといった事態が起きます。リランクのスコアは並べ替えにだけ使い、足切りには使わない設計にしておくと、接続先を替えても挙動が安定します。


接続側の実装パターン

接続先がNIMかvLLMかを設定で明示的に切り替える方法もありますが、運用中の環境では「どの接続先がどちらか」を人が正しく設定し続けるのは負担になります。そこで、まず標準の形で呼び、接続先が拒否したときだけNIMの形に切り替えて、その結果を覚えておく という方法が扱いやすくなります(説明用の汎用コードです)。

_INPUT_TYPE_REQUIRED: set[tuple[str, str]] = set() # (接続先URL, モデル名) async def embed(text: str, url: str, model: str, input_type: str) -> list[float]: payload = {"model": model, "input": text} if (url, model) in _INPUT_TYPE_REQUIRED: payload["input_type"] = input_type resp = await client.post(f"{url}/v1/embeddings", json=payload) if ("input_type" not in payload and resp.status_code in (400, 422) and "input_type" in resp.text.lower()): _INPUT_TYPE_REQUIRED.add((url, model)) # 以後はこの組にだけ付ける payload["input_type"] = input_type resp = await client.post(f"{url}/v1/embeddings", json=payload) resp.raise_for_status() return resp.json()["data"][0]["embedding"]

リランカーも同じ考え方で、/v1/rerank が404または405を返したら /v1/ranking にNIMの形で送り直し、成功した接続先を覚えておきます。どちらでも失敗した場合は、リランクを諦めて1段目の検索の順で回答を続け、利用者を待たせないようにします。

DigitalBaseでもこの方式でNIMに対応しています。埋め込みでは、呼び出し側が常に「質問」か「文書」かを渡し、input_type を拒否した接続先とモデルの組にだけ以後付けて送ります。vLLMやOllamaには送りません。リランカーは /v1/rerank が見つからない接続先を /v1/ranking で呼び直し、いずれにも失敗したときは検索の順に戻して警告を出します。社内アプリから埋め込みを呼ぶOpenAI互換のAPIでも、input_type を任意で指定できるようにしています。


運用上の注意点

1. 登録時と検索時で同じモデルを使う

埋め込みのベクトルは、同じモデルで作ったもの同士でしか比べられません。文書をモデルAで登録し、質問をモデルBでベクトル化すると、次元が同じでも検索結果は意味をなさなくなります。エラーにならないぶん、気づきにくい問題です。

対策として、ナレッジ(文書の集まり)ごとに 登録に使った埋め込みモデルを記録し、検索時はその記録に従って質問をベクトル化する ようにします。利用者や設定が別のモデルを指定していても、ナレッジに記録されたモデルを優先します。

2. 次元を混ぜない

埋め込みモデルによって出力の次元数は異なります。1つのナレッジに異なる次元のベクトルが混ざると、検索自体ができなくなります。保存時に既存の次元と比べ、違えばエラーにして止めるのが安全です。

複数のナレッジをまとめて検索する場合も注意が必要です。ナレッジごとにモデルが異なれば、質問のベクトルもナレッジごとに作り分けなければならず、次元が異なれば同じ索引では検索できません。同時に検索するナレッジは、同じ埋め込みモデルで揃えておくのが運用上は簡単です。

3. モデルを変えたら再埋め込みが必要

NIMのモデルを新しいものに更新する、vLLMで動かしていた埋め込みモデルからNIMのモデルに乗り換える、といった場合は、既存の文書をすべて新しいモデルで埋め込み直す 必要があります。古いベクトルと新しいベクトルは比較できないためです。

移行は次の手順で進めると、利用者への影響を抑えられます。

  1. 新しいモデルで別のナレッジを作り、同じ文書を取り込む
  2. 代表的な質問で、旧ナレッジと新ナレッジの検索結果を比べる
  3. 問題がなければ、利用者が参照するナレッジを新しいものに切り替える
  4. 旧ナレッジを削除する

取込時に本文のハッシュと取込条件(埋め込みモデル・チャンク設定)を記録しておけば、「モデルを変えたので全件を取り込み直す」「中身もモデルも同じなので飛ばす」の判定を自動化できます。

4. input_type の付け間違いに注意する

非対称モデルでは、文書を query として登録したり、質問を passage としてベクトル化したりしても、エラーにはならず検索の精度だけが下がります。取込処理・チャット・社内アプリからのAPI呼び出しなど、埋め込みを呼ぶすべての箇所で、どちらを渡すべきかを決めておきます。

5. 入力長の上限とチャンクの大きさを揃える

埋め込みモデルには入力できるトークン数の上限があり、モデルによって大きく異なります。上限を超えるチャンクは、黙って切り捨てられるか、エラーになります。モデルを切り替えるときは、チャンクの大きさがその上限に収まっているかを確認し、超える場合は理由を示して取込を止めるようにしておくと、検索から一部の内容が抜け落ちることを防げます。

6. リランカーが止まったときの振る舞いを決めておく

リランカーは検索の経路上にあるため、応答が遅い・落ちているといった状態がそのまま回答の遅れにつながります。タイムアウトを設け、超えたら1段目の順で回答を続けるようにしておきます。管理画面などで、リランカーの接続先が応答しているか、指定したモデルが提供されているかを確認できるようにしておくと、切り分けが速くなります。


導入時の確認項目

確認項目内容
埋め込みモデル非対称モデルか(input_type が必要か)
次元数既存のナレッジと一致するか。異なるなら別のナレッジとして作る
入力長の上限現在のチャンクの大きさが収まるか
リランカーのエンドポイント/v1/rerank か /v1/ranking か
スコアの扱いリランクのスコアに固定の閾値を設けていないか
障害時の振る舞いリランカーのタイムアウトと、検索順への切り戻し
移行計画既存ナレッジの再埋め込みの手順と、比較に使う質問

まとめ

NIMの埋め込みとリランカーは、すでにNIMを運用している企業にとって、RAGの推論部分を同じ基盤に揃える有力な選択肢です。ただし、OpenAI互換という言葉どおりにURLを差し替えるだけでは動かない部分があります。

  1. 埋め込み:非対称モデルは input_type(query か passage)が必須。呼び出し側で常に区別して渡す
  2. リランカー:/v1/ranking は形式もスコアの尺度も異なる。スコアは並べ替えにだけ使う
  3. モデルの一貫性:ナレッジごとに登録時のモデルを記録し、検索時もそのモデルを使う
  4. 次元:1つのナレッジ、同時に検索するナレッジの間で次元を混ぜない
  5. 再埋め込み:モデルを変えたら全件を埋め込み直す。別のナレッジで並行して作り、比べてから切り替える

接続先の差を基盤側で吸収しておけば、利用者はNIMかvLLMかを意識せずに同じナレッジを使い続けられます。


関連記事