0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RAGパイプラインでは、完全停止より「成功したように見える劣化」の方が厄介です。

HTTP 200が返り、処理件数も増え、回答生成まで完了します。しかし、保存先は空のままかもしれません。入力欠落やリランカー停止も見逃しやすくなります。

本稿では、Qdrant、Ollama、リランカーを対象にします。実運用で遭遇した3種類の失敗と対策を紹介します。

1. Qdrantへ送った件数を「保存できた件数」にしていた

Qdrantクライアントが、サーバーより2つ以上新しいマイナーへ更新されていました。Qdrantが保証する範囲は、1マイナー以内です。

この構成でも、upsertは成功を返しました。ところが、後から対象ポイントを取得すると、結果は空でした。

さらに、アプリは送信配列の長さを成功件数として加算していました。

await client.upsert(collectionName, {
  wait: true,
  points,
});

upserted += points.length;

この実装で分かる値は送信件数だけです。保存結果は対象IDから確認します。

const pointIds = [...new Set(points.map(({ id }) => id))];
const storedPoints = await client.retrieve(collectionName, {
  ids: pointIds,
  with_payload: false,
  with_vector: false,
});

if (storedPoints.length !== pointIds.length) {
  throw new Error(`Qdrant verification failed: ${storedPoints.length}/${pointIds.length}`);
}

この事象をQdrant一般の不具合とは断定しません。ここで重要なのは、送信件数と保存件数を分ける設計です。

依存関係は同じマイナーへ固定します。Qdrantは隣接マイナー間の互換性を保証します。今回は運用ルールをさらに厳しくしました。

@qdrant/js-client-rest = 1.<same-minor>.<patch>
qdrant/qdrant           = v1.<same-minor>.<patch>

Dockerイメージ側も環境変数で固定できます。

compose.yaml
services:
  qdrant:
    image: qdrant/qdrant:${QDRANT_VERSION}

互換性警告はCIで失敗として扱います。

読み戻しは追加通信を発生させます。全バッチへの常時適用は避けました。CI、移行直後、バージョンドリフト検出時などに限定しています。

2. モデル最大長とOllama実コンテキスト長のずれ

BGE-M3の仕様上限は8192トークンです。検証時のOllamaは4kコンテキストでモデルをロードしていました。

根本対策はランタイム側の上限を8192へ引き上げることです。十分なVRAMも必要になります。

OLLAMA_CONTEXT_LENGTH=8192 ollama serve

設定後はollama psを実行します。CONTEXT列から割り当て値を確認できます。

NAME           ID            SIZE      PROCESSOR    CONTEXT    UNTIL
bge-m3:latest  example-id    example   100% GPU     8192       ...

ただし、上限を広げても入力超過は起こり得ます。アプリ側には切り詰め処理も残します。役割は最後の防波堤です。

境界テストでは、英語とコード断片から作った合成入力を使いました。文字数だけでは判断しません。BGE-M3 tokenizerでトークン数も確認しました。

count_tokens.py
import sys

from transformers import AutoTokenizer

text = sys.stdin.read()
tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-m3')
print(len(tokenizer.encode(text, add_special_tokens=True)))

この値は目安です。最終判断には実際の埋め込み処理を使います。

文字数による上限は、tokenizerを組み込めない箇所向けの保険です。

const MAX_EMBED_CHARS = 8_000;
let truncateCount = 0;

const truncateForEmbedding = (text: string): string => {
  if (text.length <= MAX_EMBED_CHARS) {
    return text;
  }

  truncateCount += 1;

  if (truncateCount <= 5 || truncateCount % 100 === 0) {
    log.warn('embedder: oversized input was truncated', {
      originalLength: text.length,
      truncatedTo: MAX_EMBED_CHARS,
      truncateCount,
    });
  }

  return text.slice(0, MAX_EMBED_CHARS);
};

8,000文字は英語とコード中心の合成入力から得た暫定値です。日本語中心の文章には流用できません。

文字数とトークン数は一致しません。利用中のモデルとランタイムで境界を測ってください。

重要なのは、切り詰め自体より観測可能性です。無記録で削ると、検索精度の低下しか見えません。

3. リランカー障害時に「似た処理」で埋めなかった

リランカーのモデルロードが失敗しても、生成モデルを代役にすれば処理は継続できます。ただし、スコアの意味と再現性は別物です。

利用不能時はベクトル検索の順序へ戻します。フォールバック発生も記録します。

if (!model) {
  log.error('reranker unavailable; using vector ranking');
  metrics.rerankerFallback.inc();

  return hits.slice(0, topK);
}

精度改善用コンポーネントの停止後に別方式を挟むと、原因まで隠れます。

品質を偽装しません。劣化モードは明示します。こちらの方が調査しやすくなります。

共通して効いた3つのルール

静かな失敗 防止策
送信件数だけで成功判定 保存先から対象IDを読み戻す
モデルカードだけで上限決定 ランタイム設定を確認後、境界は実測
障害時に代替処理で補完 劣化先を固定し、ログへ記録

RAGの品質問題には、例外を伴わないケースもあります。

受信側で確認する。実環境を測る。劣化を隠さない。

この3点を導入した後、「検索精度が何となく悪い」という調査不能な状態は大きく減りました。

参考資料

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?