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?

AI検索エージェントの曖昧クエリをPythonで止める

0
Posted at

AI検索エージェントの曖昧クエリをPythonで止める

2026-07-05にThe DecoderでDiscoBenchの記事を読んだ。211タスク、463個の曖昧ポイントを入れた検索エージェント評価で、トップでもend-to-end accuracyが43.1%。しかも「検索を増やす」だけでは良くならず、曖昧なまま何度も検索して最後に外す、という話だった。

これ、検索エージェントを作っていると地味に刺さる。

自分も今日、手元の小さい検索ラッパーを触っていて、「OpenAI API 料金」みたいな入力をそのまま検索に投げる癖がまだ残っていた。料金は日付で変わるし、比較系の質問は基準が抜ける。なのに検索APIに渡すと、きれいなクエリ文字列になってしまう。見た目は進んでいる。中身はズレている。

ここで欲しいのは賢い検索ではなく、検索前の足止めなんだよね。

AI検索エージェントは検索前に曖昧クエリを止める

自分はここで、検索APIの手前に小さい clarify gate を置くことにした。

やることは単純で、入力を3種類だけ見る。

  • エンティティが曖昧か
  • 比較基準が抜けているか
  • 料金やAPI制限みたいに日付やバージョンが必要か

LLMに判断させる前に、安いルールで止める。全部を救う気はない。むしろ雑でいい。検索を1回増やすより、質問を1回返したほうが事故が小さいケースを拾うための門番。

Pythonで最小のclarify gateを書く

Python 3.11で動かしたコード。外部APIは使っていない。

from dataclasses import dataclass
import re
import unittest
from typing import Literal

Action = Literal["ask", "search"]

AMBIGUOUS_ENTITIES = {
    "mercury": "Mercury は惑星、元素、決済会社など複数あります。どれですか?",
    "jaguar": "Jaguar は車、動物、スポーツチームなど複数あります。どれですか?",
    "go": "Go は言語名、ゲーム、移動の一般語で曖昧です。どの Go ですか?",
    "rust": "Rust は言語名、ゲーム、錆の一般語で曖昧です。どの Rust ですか?",
}

CRITERIA_PAT = re.compile(
    r"(best|top|fastest|cheapest|compare|ranking|おすすめ|最適|一番|最速|最安|比較|ランキング)",
    re.I,
)
CRITERIA_HINT = re.compile(
    r"(price|cost|latency|accuracy|license|region|budget|料金|価格|精度|速度|遅延|用途|予算|商用|リージョン)",
    re.I,
)
VOLATILE_PAT = re.compile(
    r"(api|pricing|price|料金|価格|quota|limit|制限|latest|最新|today|今日)",
    re.I,
)
VERSION_OR_DATE = re.compile(
    r"(20\d{2}|v?\d+(?:\.\d+){0,2}|today|今日|最新|latest)",
    re.I,
)

@dataclass
class Decision:
    action: Action
    reasons: list[str]
    question: str | None
    normalized_query: str

def decide(query: str, *, today: str = "2026-07-06") -> Decision:
    q = " ".join(query.strip().split())
    low = q.casefold()
    reasons: list[str] = []
    questions: list[str] = []

    for word, question in AMBIGUOUS_ENTITIES.items():
        if re.search(rf"(?<![a-z]){re.escape(word)}(?![a-z])", low):
            reasons.append("entity")
            questions.append(question)
            break

    if CRITERIA_PAT.search(q) and not CRITERIA_HINT.search(q):
        reasons.append("criteria")
        questions.append("何を基準に比較しますか? 例: 料金、精度、速度、商用利用、運用コスト")

    if VOLATILE_PAT.search(q) and not VERSION_OR_DATE.search(q):
        reasons.append("version_or_date")
        questions.append(f"いつ時点の情報として調べますか? 今日なら {today} 時点で固定します。")

    if reasons:
        return Decision("ask", reasons, questions[0], q)
    return Decision("search", [], None, q)

class ClarifyGateTest(unittest.TestCase):
    def test_gate(self):
        cases = [
            ("一番速いベクトルDB", "ask", "criteria"),
            ("OpenAI API 料金", "ask", "version_or_date"),
            ("Mercury release date", "ask", "entity"),
            ("Python 3.12 pathlib Path.walk 使い方", "search", None),
            ("2026年7月のOpenAI API 料金", "search", None),
            ("LLM RAG 評価 精度 比較", "search", None),
        ]
        for query, action, reason in cases:
            got = decide(query)
            self.assertEqual(got.action, action, query)
            if reason:
                self.assertIn(reason, got.reasons, query)

if __name__ == "__main__":
    suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClarifyGateTest)
    result = unittest.TextTestRunner(verbosity=1).run(suite)

    print("\n--- demo ---")
    for query in [
        "一番速いベクトルDB",
        "OpenAI API 料金",
        "Python 3.12 pathlib Path.walk 使い方",
    ]:
        d = decide(query)
        print(f"{query} -> {d.action} reasons={d.reasons} question={d.question}")

    raise SystemExit(0 if result.wasSuccessful() else 1)

手元ではこう通った。

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

--- demo ---
一番速いベクトルDB -> ask reasons=['criteria'] question=何を基準に比較しますか? 例: 料金、精度、速度、商用利用、運用コスト
OpenAI API 料金 -> ask reasons=['version_or_date'] question=いつ時点の情報として調べますか? 今日なら 2026-07-06 時点で固定します。
Python 3.12 pathlib Path.walk 使い方 -> search reasons=[] question=None

ここで止める理由

検索エージェントの失敗は、最後の要約だけを見ると分かりにくい。検索結果は並ぶし、引用も付く。ログだけ見るとそれっぽい。

でも最初の分岐で「どのMercuryか」「何を速いと呼ぶか」「いつ時点の料金か」を外すと、その後の検索は全部きれいに間違う。これはバグとして厄介で、検索APIの品質問題に見えたり、RAGのランキング問題に見えたりする。実際には入口の仕様が足りないだけ。

なので、検索ツールを呼ぶ直前に Decision("ask", ...) を返せる場所を作る。ここをモデルのプロンプトだけに任せると、「曖昧なら確認して」と書いてあっても、モデルは検索を始めがち。DiscoBenchの記事でも、曖昧さ検知は上がってもタスク成功率がそこまで伸びない、という観察が出ていた。気づくことと、ユーザーに戻すことは別スキルらしい。

自分の見立てでは、検索エージェントの実装はこの順番が扱いやすい。

  1. ルールで明らかな曖昧さを止める
  2. 止めた理由をログに残す
  3. ユーザー回答を受けて検索クエリを作る
  4. 検索後の要約で引用と日付を確認する

1がないと、4で頑張っても戻れない。検索後に「もしかして別の意味でしたか」と聞くのは、ユーザー体験としてもけっこうしんどい。

実運用で足したいもの

上のコードは雑なので、そのまま本番に入れるなら足すものがある。

まず、曖昧語の辞書はプロダクトごとに持つ。社内検索なら部署名、顧客名、略称、旧サービス名。技術調査ならモデル名、SDK名、クラウドサービス名。ここは一般辞書より、自分たちの検索ログから作ったほうが強い。

次に、ask の数をメトリクスにする。曖昧クエリ率が高い画面は、UI側の入力例が悪い可能性がある。ユーザーが悪いんじゃなくて、聞き方を誘導できていないだけ、というやつ。

あと地味に大事なのが、質問文を短くすること。長い確認文は読まれない。「どのMercuryですか?」くらいでいい。ここで丁寧すぎる文章を出すと、エージェントが急にサポートチャットみたいになる。

おわりに

検索エージェントは、検索が上手いだけでは足りない。曖昧な入力を見つけた瞬間に、いったん止まれるほうが実務では強い。

今回の小さいゲートは、検索品質を上げる魔法ではない。ただ、間違った一本道に入る前にブレーキを踏める。RAGやブラウジングを作っていて「検索回数は多いのに答えがズレる」なら、まず検索前のclarify gateを疑う価値があると思う。

参考: AI search agents don't fail at searching, they fail at asking the right questions when queries get ambiguous

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?