LLMのおかげで、FAQを作るのも探すのも、ずいぶん楽になりました。
FAQが少ないうちは、全部LLMに渡して「この質問に答えるFAQを探して」と頼むだけでも動きます。では、1,000件、2,000件と増えても、そのままでよいのでしょうか。
社内FAQが増えたら、どんな検索の仕組みにすればよいのか。公開されている自治体FAQ 1,786件で試してみました。
全件をGeminiに読ませると、50問中47問で正解FAQが1位に入りました。一方、ベクトル検索で10件に絞ってから読ませても、同じ50問では同じ正解数になりました。
ただし、10件に絞る方法には、はっきりした限界もあります。
候補の中に正解があれば、LLMで順位を改善できる余地がある。候補になければ、並べ直しても届かない。
この違いが見えてくると、FAQ検索をどう育てればよいかも考えやすくなります。
まずは、FAQを全部渡してみる
使ったのは、LocalgovFAQ研究に由来する公開データです。自治体FAQが1,786件、検索用の問い合わせ文が749問あります。
749問のうち、正解のFAQが分かっている587問を使って、検索の精度を調べました。LLMには、問い合わせに合うFAQを選び、そのIDを返してもらいます。
精度は、正解のFAQが何位までに入ったかで比べます。
| 指標 | 何を数えるか |
|---|---|
| Hit@1 | 1位に正解FAQが入った問い合わせの数・割合 |
| Hit@3 | 上位3件に正解FAQが1件以上入った問い合わせの数・割合 |
| Hit@10 | 上位10件に正解FAQが1件以上入った問い合わせの数・割合 |
まずは、FAQ全件の質問文と回答文をGemini 3.8 Flashへ渡します。FAQと検索の指示を合わせると497,436トークンでした。トークンは、LLMが扱う文章量の単位です。今回の1,786件は、一度に渡せる量に収まりました。
実験を始める前に、587問からランダムに50問を選びました。この50問で試した結果がこちらです。
| 方法 | Hit@1 | Hit@3 | Hit@10 |
|---|---|---|---|
| FAQ全1,786件をGeminiに渡す | 47/50 | 48/50 | 49/50 |
50問での結果とはいえ、かなり当たります。FAQを渡すだけで始められるのも魅力です。
FAQ全件を渡せるなら、まずこの方法を試す価値はありそうです。
ただ、続けて使うことを考えると、気になるのが待ち時間と料金でした。
このうち20問で、Gemini APIに問い合わせてから結果が返るまでの時間を測りました。中央値は2.28秒、最大は5.62秒です。共通のFAQを保存して再利用する「コンテキストキャッシュ」は、20回すべてで使われていました。
50問分の費用をAPIの使用量から計算すると、キャッシュを2時間保存する料金も含めて約392円でした。
ときどき使うなら十分かもしれません。でも、検索画面で何度も使うなら、もう少し速く、安くしたくなります。
ベクトル検索は速い。ただ、正解が1位とは限らない
そこで、ベクトル検索を試しました。文章を数値の並びに変換し、意味が近いものを探す方法です。
今回はFAQの質問文だけを gemini-embedding-2 でベクトルに変換し、保存しておきます。使ったベクトルは768次元です。検索時には問い合わせ文だけを新しくベクトルに変換し、保存済みのFAQベクトルと比べて上位10件を選びます。
587問すべてで検索の精度を調べると、こうなりました。
| 方法 | Hit@1 | Hit@3 | Hit@10 |
|---|---|---|---|
| 質問文のベクトル検索 | 394/587(67.1%) | 496/587(84.5%) | 553/587(94.2%) |
速さも30問で測ってみました。問い合わせをベクトルに変換し、上位10件を選ぶまでの時間は、中央値で0.50秒でした。その大半は、問い合わせをベクトルに変換するAPIの応答を待つ時間でした。
候補を返すまでが速い。しかも、上位10件まで見れば94.2%の問い合わせで正解が入っています。
一方、1位に正解が入ったのは67.1%です。
正解は見つかっている。でも、上に出せていない。
それなら、この10件だけをLLMに読ませたらどうでしょうか。
10件だけ読ませると、50問では全件読みと同じ正解数に
全件読みと同じ50問を使い、ベクトル検索が選んだ10件の質問文・回答文をGeminiへ渡しました。候補は増やさず、10件の順番だけを並べ直してもらいます。この処理をリランキングと呼びます。
| 方法 | Hit@1 | Hit@3 | Hit@10 |
|---|---|---|---|
| FAQ全1,786件をGeminiに渡す | 47/50 | 48/50 | 49/50 |
| ベクトル検索の上位10件をGeminiで並べ直す | 47/50 | 48/50 | 49/50 |
| ベクトル検索のみ | 38/50 | 40/50 | 49/50 |
この50問では、全件を読ませても、10件だけを読ませても、正解数は同じでした。当たった問い合わせには違いがありますが、10件に絞ってもよい結果が出ています。
Geminiへ渡す文章量を減らすと、費用も下がりました。
| 50問分のGemini利用費用 | 使用量からの計算額 |
|---|---|
| 全件読み(キャッシュ保存2時間分を含む) | 約392円 |
| 上位10件の並べ直し | 約21円 |
金額は、記録したAPI使用量に当時の公開単価をかけ、1ドル165円で換算しました。約21円はGeminiによる並べ直しの費用です。FAQや問い合わせをベクトルに変換する費用は別にかかります。
LLMに読ませる範囲を絞ると、その分の費用を抑えられる。 その効果が見えてきました。
587問でも改善した。残った失敗は2種類ある
手応えがあったので、上位10件を並べ直す方法を587問すべてに広げました。
| 方法 | Hit@1 | Hit@3 | Hit@10 |
|---|---|---|---|
| ベクトル検索のみ | 394/587(67.1%) | 496/587(84.5%) | 553/587(94.2%) |
| 上位10件をGeminiで並べ直す | 497/587(84.7%) | 546/587(93.0%) | 553/587(94.2%) |
1位に正解が入った問い合わせは103問増えました。上位3件では50問増えています。
ただし、並べ直したことで、かえって正解が下がった問い合わせもあります。
正解が新たに1位になったのは121問。逆に、1位から外れたのは18問でした。上位3件で見ると、新たに入ったのが52問、外れたのが2問です。
改善した数だけでなく、それまで当たっていたのに外れた数も見ておきたいところです。
ここで、並べ直したあとも上位3件に正解が入らなかった41問を分けてみます。
| 残った失敗 | 問い合わせ数 |
|---|---|
| 候補10件には正解があるが、上位3件に上がらなかった | 7問 |
| 候補10件に正解が入っていなかった | 34問 |
この2つでは、次に直す場所が違います。
最初の7問は、順位の付け方を見直す余地があります。残る34問は、どれだけうまく並べ直しても正解に届きません。正解を候補に入れられるよう、検索の方法を見直す必要があります。
順位の問題なのか、候補をひろう段階の問題なのか。
検索の精度が足りないと感じたら、まずここを分けて見ると、次に試すことが絞れます。
なお、全件読みを試したのは先ほどの50問です。587問では、ベクトル検索と、その上位10件を並べ直す方法を比較しています。
速く見せて、あとからLLMの判断を添える
Geminiで並べ直すには、追加の待ち時間がかかります。
同じ30問で、それぞれの処理を別々に測ると、次の結果になりました。
| 処理 | 中央値 | p95 |
|---|---|---|
| 問い合わせのベクトル化から上位10件の取得まで | 0.50秒 | 0.55秒 |
| 10件をGeminiで並べ直すAPI呼び出し | 1.29秒 | 1.50秒 |
p95は、かかった時間を短い順に並べたとき、全体の95%の位置にあたる値です。今回は2つの処理を別々に測りました。問い合わせから最終結果が返るまでを、通して測った時間ではありません。
それでも、画面の作り方を考える材料にはなります。
この結果を使ってFAQ検索のWebアプリを作るなら、私は次のようにしたいと思います。ここからは、まだ試していない画面の案です。
- ベクトル検索で候補を選ぶ
- まず上位3件を画面に表示する
- 裏で上位10件をGeminiに読ませる
- 結果が返ったら、有力な候補に印を付ける
最初の候補が出れば、利用者は読み始められます。そのあとで、
AIが確認したおすすめの候補です
と案内するイメージです。
読んでいる途中でFAQの位置が動くと戸惑いそうなので、突然並べ替えるより、候補に印を添える方が使いやすそうです。
先にFAQを読み始めてもらい、あとからLLMの判断を添える。そんな見せ方も考えられます。
方法を足せば、よくなるとは限らなかった
ほかの方法も試しました。以下はすべて、同じ587問で正解FAQが上位に入った件数です。
| 方法 | Hit@1 | Hit@3 | Hit@10 |
|---|---|---|---|
| 質問文のベクトル検索 | 394 | 496 | 553 |
| 回答文のBM25検索と組み合わせる | 249 | 358 | 482 |
| FAQごとに想定質問を5件追加する | 379 | 506 | 558 |
| BGEで上位10件を並べ直す | 353 | 474 | 553 |
| Geminiで上位10件を並べ直す | 497 | 546 | 553 |
BM25は、文字や単語の一致を手がかりに探す方法です。今回は回答文の文字BM25と質問文のベクトル検索を、同じ重みで組み合わせました(RRF)。この設定では正解数が減りました。
FAQごとに、利用者が尋ねそうな質問をAIで5件ずつ作り、検索対象に加える方法では、上位3件の正解数が10問増えました。ただ、1位の正解数は15問減り、保存するベクトルは6倍になります。
並べ直し専用のモデル BAAI/bge-reranker-v2-m3 も試しましたが、今回の設定では上位1件・3件ともに正解数が減りました。
今回試した範囲では、仕組みを足せば精度が上がるとは限りませんでした。
私なら、こう育てる
まず、FAQ全件をLLMに渡すところから始めます。全部を一度に渡せて、待ち時間と料金にも問題がなければ、そのまま使えます。
そこから先は、実際に困ったことに合わせて考えます。
| 困ったこと | 次に試すこと |
|---|---|
| 全件読みの待ち時間や料金が気になる | ベクトル検索で候補を絞る |
| 候補には正解があるのに、上位に出ない | 候補の質問文・回答文をLLMで並べ直す |
| 並べ直しを待つ時間が気になる | 候補を先に表示し、LLMの判断をあとから添える |
| 候補に正解が入らない | 漏れた問い合わせを調べ、候補の選び方を見直す |
| 答えになるFAQ自体がない | FAQを追加する |
候補をひろえていないときには、制度名や型番の一致を重視する、FAQの見出しを直す、よくある言い換えを追加する、といった方法が考えられます。失敗の具体例に合わせて試し、改善と悪化の両方を確認します。
そのために残しておきたいのは、実際の問い合わせと、人が確認した正解FAQです。これがあれば、変更のたびに「本当によくなったか」を確かめられます。
評価の範囲と実験データ
正解の判定には、元の研究のデータを使いました。問い合わせに対する答えが書かれているFAQを正解とし、それが検索結果の上位に入ったかを数えています。
利用者がFAQを読んで問題を解決できたか、答えがないときに「回答できない」と案内できるかは、今回調べていません。
元の研究で正解かどうかを調べていないFAQにも、役に立つ回答があるかもしれません。また、今回は自治体FAQで試しています。社内FAQに使うときは、自分たちの問い合わせでも確かめたいところです。
実験コード、問い合わせごとのFAQの順位、保存したベクトル、集計結果は、GitHubに公開しています。詳しい実験条件は、各レポートにまとめました。
待ち時間は、今回の実行環境で測ったものです。料金はAPIの使用量から計算しており、請求書の金額を確認したものではありません。モデルの設定や、渡した文章の違いも、リンク先に記録しています。
元データは Sakata らの FAQ Retrieval using Query-Question Similarity and BERT-Based Query-Answer Relevance(SIGIR 2019)に由来します。FAQ本文や生成した想定質問の一括収録はしていません。元のFAQと問い合わせは、再現手順に従って配布元から取得できます。
おわりに
1,786件のFAQは、本当に全部LLMへ渡せました。そして、10件に絞ってから読ませる方法にも、十分に試す価値がありました。
全件読みが遅い・高いと感じたら、候補を絞る。候補に正解があるのに順位が低ければ、LLMに並べ直してもらう。候補に入らなければ、検索側を見直す。
まず単純に作り、困ったことが見えたら、その問題に合う方法を足す。
FAQが増えたときも、この順番で育てていこうと思います。