powered by TechFeed
表示モード
ハウツー

RAGへの曖昧な質問に「一度だけ確認して学習する」パターン — 確認不要なデフォルト値を自動獲得する仕組み

6月22日、Towards Data Scienceが「When RAG Users Ask Vague Questions: Clarify Once, Learn the Default」と題した記事を公開した。RAGシステムで曖昧な質問を受けたとき、毎回確認するのでも毎回無視するのでもなく、「一度だけ確認し、その回答を学習してデフォルト化する」という発想は、UXと精度を両立させるうえで実践的なアプローチだ。記事では保険ブローカー向けシステムの実トラフィックを素材に、このパターンをPydanticスキーマとPythonコードで具体的に示している。

6月22日、Towards Data Scienceが「When RAG Users Ask Vague Questions: Clarify Once, Learn the Default」と題した記事を公開した。RAGシステムで曖昧な質問を受けたとき、毎回確認するのでも毎回無視するのでもなく、「一度だけ確認し、その回答を学習してデフォルト化する」という発想は、UXと精度を両立させるうえで実践的なアプローチだ。記事では保険ブローカー向けシステムの実トラフィックを素材に、このパターンをPydanticスキーマとPythonコードで具体的に示している。


問題の本質:「happy path」以外のトラフィック

RAG(Retrieval-Augmented Generation)とは、大規模言語モデルの生成能力と外部知識ベースの検索を組み合わせたアーキテクチャで、企業内文書への質問応答システムとして広く採用されている。エンタープライズRAGの本番環境では、ユーザーが意図を完全に伝えた質問(happy path)は少数派だ。保険ブローカー向けシステムの実トラフィックを例に挙げると、以下のような曖昧な質問が頻繁に発生する。

  • フィールドの曖昧さ: 「limitは何か?」→ 契約書には発生ごとの上限額、総支払上限額、サブリミットなど複数存在する
  • ページスコープ欠落: 「何と書いてある?」→ 200ページの文書のどこを見ればよいかわからない
  • 日付スコープの曖昧さ: 旧スケジュールと更新特約が混在する契約書でどちらが適用されるか不明
  • 意図の曖昧さ: 「保証条項」→ 引用したいのか、要約したいのか、条件を抽出したいのか
  • 暗黙のエンティティ: 「保険契約者」→ 企業被保険者、受益者、追加記名被保険者のどれか

これらはすべて、ユーザーがすでに特定した1つの文書に関する質問だ。システムがParsedQuestionの型付きフィールドを持っていても、ユーザーが値を与えなかった場合のループが欠けている。


コアパターン:2つのPydanticスキーマ

記事の核心は2つのスキーマで完結する。PydanticはPythonの型ヒントを活用したデータバリデーションライブラリで、LLMの出力を構造化データとして受け取る用途でも広く使われている。

from datetime import datetime
from pydantic import BaseModel, Field

class ClarificationRequest(BaseModel):
    """ParsedQuestionのフィールドが信頼度閾値を下回った際に発行する"""
    target_field: str                          # 埋めるべきParsedQuestionのフィールド
    question_to_user: str                      # ユーザーに見せる自然文の質問
    candidate_values: list[str]                # システムが提案できる値の一覧
    proposed_default: str | None = None        # システムが選ぶデフォルト値
    proposed_default_reason: str | None = None # 1文での理由
    audit: dict = Field(default_factory=dict)  # request_id, model, prompt_version

class ClarificationDefault(BaseModel):
    """多くのリクエストから学習した回答。継続的に更新される"""
    target_field: str
    doctype: str                               # broker_contract, invoice, ...
    sub_conditions: dict = Field(default_factory=dict)
    candidate_votes: dict[str, float]          # 値 -> 重み付き投票数
    confidence: float                          # 0..1、ask/apply判断に使用
    sample_size: int
    last_refreshed: datetime

ClarificationRequestはユーザーへの問い合わせを表し、ClarificationDefaultはその蓄積から学習したデフォルト値を保持する。これら2つのスキーマと1つのテーブルが、曖昧な質問への対処コスト全体だ。


具体例:ブローカーシステムでの3ケース展開

記事が示す実例は、保険ブローカーの新人クレーム担当者が契約書をアップロードし「*qui est l'assureur?*(保険者は誰か?)」と質問するシナリオだ。フランス語のクエリを例示に使っているのは、多言語環境を含む実務上の多様なユーザー層を想定した例示であり、このパターンが特定言語に依存しないことを示している。

ケース1(初回): insurer_nameフィールドのデフォルトなし。システムは確認を発行する。

「通常ブローカー契約書では1ページ目に保険者名が記載されています。そこから確認してよいですか?」

ユーザーが「Yes」と回答。1ページ目で回答が得られ、ClarificationDefaultに投票+1が記録される。

ケース2(1週間後、別の契約書): source_page = 1の信頼度が12件の実績から0.78に達している。システムは確認せず1ページ目を直接参照し、無言で回答する。この時点で検索スコープも絞り込まれ、コーパス全体を対象とした検索が単一ページのルックアップに縮退する。「保険者は誰か」という質問を、同じ契約書に対して繰り返し1ステップで答えられる仕組みの実体はここにある。

ケース12(表紙ページが1ページ目の契約書): デフォルトの1ページ目に保険者名が見つからない。システムはフィールドがnullになったことを検出し、フォールバックする。

「この契約書の1ページ目には保険者名がありませんでした。目次から探しましょうか、それともページを指定しますか?」

ユーザーが「目次から」と回答。以降のデフォルトは条件分岐が追加される:page_1_kind = bodyの場合は1ページ目、page_1_kind = coversheetの場合は目次、と学習される。


学習ループの実装

信頼度の更新と「ask/apply」のゲート判定は以下のコードで実装される。

from typing import Literal
from datetime import datetime

Signal = Literal["explicit_yes", "explicit_no", "implicit_ok", "failure"]

def update(default: ClarificationDefault, value: str, signal: Signal) -> ClarificationDefault:
    votes = dict(default.candidate_votes)
    if signal == "explicit_yes":   votes[value] = votes.get(value, 0) + 1.0
    elif signal == "explicit_no":  votes[value] = votes.get(value, 0) - 1.0
    elif signal == "implicit_ok":  votes[value] = votes.get(value, 0) + 0.5
    # "failure": 投票変化なし、層別化の候補として扱う
    n_new = default.sample_size + 1
    top = max(votes.values()) if votes else 0.0
    confidence_new = max(0.0, top) / n_new
    return default.model_copy(update={
        "candidate_votes": votes,
        "confidence": confidence_new,
        "sample_size": n_new,
        "last_refreshed": datetime.now(),
    })

def gate(default: ClarificationDefault) -> Literal["apply", "ask_occasionally", "ask"]:
    if default.confidence > 0.85: return "apply"
    if default.confidence < 0.60: return "ask"
    return "ask_occasionally"

投票の重みは3種類に分かれる。ユーザーが明示的に「Yes」と答えた場合は**+1.0、システムがデフォルトを適用して修正なく受け入れられた暗黙の承認は+0.5、明示的な拒否は-1.0だ。信頼度が0.6未満は必ず確認0.85超は無言で適用**、その中間は信号を更新するため時折確認する。

なお、信頼度の計算式 confidence = max(votes) / sample_size は一見すると「サンプルが増えるほど信頼度が下がる」ように見える。これは分子のmax(votes)が投票の絶対数であるのに対し、分母のsample_sizeは全サンプル数(信号の種類を問わず)が増えるためだ。元記事によれば、これは意図的な設計で、「多数のサンプルがありながら最多得票値が少ない=意見が割れている」状況を低信頼度として正しく表現できる。つまり信頼度は、特定の値への票の集中度を測る指標として機能する。


設計上の重要な境界線

記事はこのパターンがチャットボットのマルチターン対話ではないと明示している。確認は1リクエストにつき1回だけ行われ、会話の中でやり取りを続けるのではなく、学習済みデフォルトが複数のリクエストをまたいで機能するのが本質だ。

また、すべての確認発行とデフォルト適用はストレージ層のquery_logに記録され、監査に対応する。「なぜ1ページ目を参照したのか」という問いへの答えは、SQLの1回のJOINで取得できる。


未解決の課題

記事は、このパターンの未解決の課題として次の3点を明示している。

  • 複数フィールドの同時確認: 2〜3項目を一度に聞くUXの設計
  • 悪意あるユーザー: 常に「Yes」と答えてデフォルトを汚染するケースへの対策
  • テナント間のデフォルト共有: マルチテナント環境でのデフォルト分離と共有の方針

これらはいずれも、本番運用でスケールさせる際に直面しうる実務的な問題だ。特にマルチテナント設計については、エンタープライズRAGの文脈で重要性が高い。

詳細はWhen RAG Users Ask Vague Questions: Clarify Once, Learn the Defaultを参照していただきたい。