LabHub
はじめる
学ぶ 学習パス コース

音声 AI エージェント — 聞いて、調べて、話すパイプライン

声で聞く検索 — 誤認識・聞き返し・拒否、そして根拠チェックを通った誤答

LabHubで続きを見る

一言でいうと

音声クエリは、短く、口語的で、ASRが間違って書き起こしたまま入ってきます。そのため、検索の前にクエリを整え(フィラーの除去・数字の単語の整理・前の質問に依存した聞き返しの解決)、検索スコアで答えのない質問を先に断り、モデルが出した答えは引用したドキュメントで検査したあとにのみ話します。検査でわかるのは、数字がドキュメントにあるかどうかまでです。根拠を通過した誤答が残ることを知ったうえで評価する必要があります(モジュール9)。

なぜ必要なのか

LLMエンジニアリングコースのRAGは、文章で書かれた質問を受け取ります。電話は違います。このモジュールの12個のクエリは、合成音声をこのイメージのストリーミングASRが実際に書き起こした結果です(fixtures/voice/rag/queries.jsonl)。「What time do you guys open on Saturday?」は「OHI WHAT TIME DO YOU GUISE OPEN ON SATURDAY」に、「How long does a refill take?」は「HOW LONG DOES ORIFYL TAKE」になって届きました。そして2つ目の質問は「O K AND WHAT ABOUT SUNDAY」で、前の質問がなければ、何を聞いているのかわかりません。

どう動くのか

段落単位のエンベディング。電話で読み上げる答えは1、2文なので、ドキュメントを段落に分けて、段落ごとにエンベディングします。このイメージのエンベディングモデルはall-MiniLM-L6-v2(qint8 ONNX)です。モデルカードどおりに、トークンベクトルをattention maskで覆って平均(mean pooling)し、長さ1に正規化すると、内積がそのままコサイン類似度になります。

クエリの書き換え。3つのルールで、この素材の大部分を扱えます。(1)フィラーのリスト(um・uh・oh・like・okay・o k …)を除きます。リストには、このASRが実際に出した「ohi」も入れました。観測した誤認識を辞書にためていくのが、現場のやり方です。(2)数字の単語を数字にします(twenty four → 24)。ドキュメントには「24 hours」と書かれています。(3)「and what about Sunday」のような聞き返しは、前の質問を持ってきて曜日だけを変えます。「ORIFYL」のような誤認識は、ルールでは直せません。エンベディングが意味で近い段落を見つけてくれるのを期待するだけです。

測ってみると。このドキュメント14個(段落41個)では、書き換え前でもtop-1再現率が10個中10個でした。エンベディングが「ORIFYL」を含む質問を、処方の再発行の段落の近くに置いたのです。ところがBM25と混ぜたハイブリッド(RRF、k=60)に変えると、top-1が元のクエリで10個中8個、書き換えたクエリで7個に下がりました。「VITIO」・「ORIFYL」のような誤認識の単語はどのドキュメントにもなく、BM25にシグナルを与えられず、残った一般的な単語(how、long、the)が順位をぼやかしたのです(このイメージで測った値)。ハイブリッドがいつも勝つわけではありません。書き換えが大きく役立ったのは、検索ではなく質問の意味です。「and what about sunday」をそのままモデルに渡すと、何を聞いたのかわかりません。

拒否のしきい値。答えのない質問(ピザ店のおすすめ、Wi-Fiのパスワード)でも、検索は何かを見つけてきます。最も近い段落のスコアがしきい値τより低ければ、モデルに尋ねもせずに断ります。小さなモデルに、でっち上げる機会を与えないのです。この素材では、答えのある質問の最低スコアは0.345、答えのない質問の最高スコアは0.260なので、その間にしきい値があります。書き換えルールに注意すべき理由も、ここで見えました。一時期「you → the clinic」の置換を入れたところ、「can you recommend a good pizza place」が「can the clinic recommend…」になってスコアが0.397に上がり、どんなしきい値でも、答えのある質問と分けられなくなりました。そのため、そのルールは外しました。

形式は文法で、内容は検査で。0.5Bモデルに「出典を[kb-hours]のように書いて」と言うと、形式から破ります。llama-serverのresponse_format(json_schema)で出力を{"answer": …, "source": …}に縛り、sourceを今回見つけたドキュメントidとnoneだけを許可するenumにすれば、モデルは見つけていないドキュメントを引用できません。そのあとコードが検査します。答えに含まれる数字(時刻・金額・日数)が、引用したドキュメントにそのままあるかどうかです。実際に、このモデルは「refill takes 10 business days」を出しながら血液検査のドキュメントを引用しました。ドキュメントには3 business daysがあります。検査で落ちた答えは話さず、引用ドキュメントから、質問に最も近い文をそのまま読み上げます(抽出)。間違えることがない代わりに、不自然になります。

根拠を通過した誤答。数字の検査は、「土曜日は何時に開くか」に平日の時間(8:30 a.m. to 6 p.m.)を答えても通過させます。数字がドキュメントにあるからです。実際に、このパイプラインは土曜日の質問と日曜日の聞き返しに、平日の時間を答えました。このような誤答は検証器では防げず、正解表で評価してはじめて見えます(モジュール9の事実の正確さ)。

現場での姿

音声アシスタントの引用は、画面の脚注とは違います。「[kb-hours]」を声に出して読んではいけません(モジュール7の読み上げ用テキストの作成で消します)。その代わり、引用はログと評価に残して、「何を根拠に話したか」を後から問えるようにします。

次のラボですること

KBドキュメントを段落に分けてエンベディングし、コサイン検索関数を作ります。クエリの書き換えルールは、隠しケースで採点ツールの試験を受けます。元のクエリと書き換えたクエリで再現率とスコアを測ったあと、答えのない質問を分けるしきい値を決めます。しきい値を超えた質問だけをLLMにJSONで尋ね、検証器でふるい、通過できなかった答えは抽出に置き換えて、最終的な発話を作ります。