ATAU

2026年8月20日

「知らないことは知らないと言う」の続き——実ログをもとにAIチャットボットの閾値を見直した話

自社サイトのAIチャットボットに寄せられた実際の質問ログを見て、類似度の閾値を0.72から0.65へ調整しました。その過程で見つかった「会話をそのまま連結すると検索精度が落ちる」問題と、それを踏まえて追加した「聞き返す」設計について紹介します。

前回の宿題

以前、サイトにAIチャットボットを実装した話を書きました。根拠のある記事が見つかった場合だけ回答し、見つからなければ正直に「わかりません」と伝える——そういう設計にこだわった、という内容です。

その記事の最後に、こう書いていました。

現時点では類似度の閾値は初期値のままです。今後は実際の質問ログを見ながら、「本来答えられたのに該当なしになっていないか」「見当違いの記事で誤答していないか」を確認し、調整していく予定です。

公開から1週間ほど経ち、実際の質問ログがたまってきたので、その宿題に手をつけました。

ログを見て気づいたこと

チャットログには、想定より短い質問が多く並んでいました。「会社概要」「問い合わせ先は?」「金額は?」——ユーザーは律儀に一文で聞いてくれるとは限りません。

当時の閾値(0.72)で照らし合わせると、次のような質問が軒並み「該当なし」で弾かれていました。

スコア質問本来マッチすべき記事
0.6998「会社概要を教えてください」About
0.7089「問い合わせ先は」Contact
0.7336「分析の自動化について」月次HP分析自動化の記事
0.6868「映像分析」PlayRecapの記事

どれも、記事タイトルを見れば人間には一目瞭然の質問です。0.72という閾値は、初期値としては手堅い一方、短い質問に対しては厳しすぎたということです。

一方で、答えられた質問(実際に回答を返せたケース)のスコアはすべて0.744以上に固まっていました。この二つの分布を見比べて、閾値を0.65まで下げる判断をしました。

多少緩めても、検索でヒットした記事本文をClaudeに渡した後、Claude自身が「この文脈では答えられない」と判断すればnoContext: trueを返す2段目のチェックが効きます。閾値を下げることのリスクは、この仕組みが吸収してくれると考えました。

二択では足りなかった

閾値を下げても、まだ引っかからない質問がありました。「資本金は?」は0.62でした。丁寧に「ATAUの資本金はいくらですか?」と聞いた別のログは0.747で正しく答えられていたので、同じ内容でも聞き方次第でスコアが大きく変わることがわかります。

ここをさらに閾値で拾おうとすると、今度は無関係な質問まで巻き込むリスクが上がります。「マッチする/しない」の二択で調整できる範囲は、もう使い切っていました。

そこで、二択の間に**「聞き返す」**という第三の状態を追加することにしました。

  • スコアが十分高い → そのまま回答
  • スコアがそこそこ(グレーゾーン)→ 候補記事を踏まえて確認質問を返す
  • スコアが低い → 従来通り「該当なし」

「資本金は?」のような短い質問には、候補記事のタイトルをもとに「ATAUの企業情報について知りたいのか、それとも資本金など会社概要の詳細をお探しですか?」と聞き返し、ユーザーの返答を待ってから答える流れです。

こだわったポイント1:会話をそのまま連結すると、むしろ精度が落ちる

最初に試した実装はシンプルでした。確認質問への返答が来たら、元の質問と返答を連結して、もう一度埋め込みベクトルを計算し直す——という方法です。

質問: 資本金は?
確認質問への返答: はい、金額を知りたいです
再検索クエリ: "資本金は? はい、金額を知りたいです"

ブラウザで実際に試したところ、想定と逆の結果になりました。

  • 「資本金は?」単体のスコア:0.6198
  • 連結後のスコア:0.6115

スコアが上がるどころか、下がってしまったのです。原因は単純で、「はい、金額を知りたいです」という相槌のような文には「資本金」という言葉ほどの情報量がなく、埋め込みベクトル全体が薄まってしまうからでした。会話履歴を持たせれば精度が上がるだろう、という直感は、埋め込みベースの検索には必ずしも当てはまりません。

そこで設計を変えました。確認質問を出した時点で提示した候補記事を、そのまま次のターンの回答根拠として使い回す方式です。ユーザーの返答は再検索のクエリにはせず、Claudeへの質問文としてのみ使います。再埋め込みが不要になった分、APIコールも1回減らせました。

// 確認質問への返答が来たら、再検索はせず
// 提示済みの候補記事をそのまま文脈として使う
const relevant = clarifySlugs
  .map((slug) => index.find((entry) => entry.slug === slug))
  .filter(Boolean);

const context = relevant.map((m) => `【${m.title}】\n${m.fullText}`).join('\n\n---\n\n');
const llmResult = await callClaude(context, `${priorQuestion} ${question}`);

「文脈を引き継ぐ」を実装するとき、検索用のクエリと生成用の質問文を同じ変数で扱うと、こういう落とし穴にはまります。役割ごとに使う場所を分けたことで、素直に解決しました。

こだわったポイント2:グレーゾーンの下限も、実際に試す

聞き返す機能を入れた直後、「今日の天気は?」と聞いてみたところ、「テニス挑戦や伴走型IT支援についてのご質問でしょうか?」という、明らかに見当違いの確認質問が返ってきました。

スコアを調べると0.568。グレーゾーンの下限を0.55に設定していたため、範囲に入ってしまっていたのです。試しに他の無関係な質問も測ってみると、次のような結果でした。

質問スコア
今日の天気は?0.568
おすすめのラーメン屋を教えて0.581
こんにちは0.643
猫の飼い方0.596

サイトの内容と何の関係もない質問でも、0.55〜0.64あたりのスコアは普通に出てしまう——これは今回使っている埋め込みモデル・コーパスの組み合わせで起きる「類似度の底上げ」で、多くの埋め込みモデルに共通する既知の傾向です。閾値だけで「関連あり/なし」をきれいに線引きできるわけではないことを、身をもって確認しました。

グレーゾーンの下限を0.6まで引き上げたところ、無関係な質問は素直に「該当なし」に戻り、拾いたかった短い質問(「資本金は?」など)はそのまま聞き返しの対象として残りました。最終的な閾値は次の通りです。

  • 0.65以上:そのまま回答
  • 0.6〜0.65:候補記事を踏まえて聞き返す
  • 0.6未満:該当なし

実際の動作

「資本金は?」と聞くと、まず確認質問が返ります。

ATAUの企業情報全般について知りたいのか、それとも資本金など会社概要の詳細をお探しですか?

ここで「はい、金額を知りたいです」と返すと、

ATAUの資本金は100万円です。

と、Aboutページを根拠に正しく回答されます。一方で「今日の天気は?」は、以前と変わらず「該当する情報が見つかりませんでした」とお問い合わせフォームへの誘導に落ち着きます。

おわりに

前回の記事で立てた「知らないことは知らないと言う」という原則は、今回も変えていません。変えたのは、「知らない」と即断する前に、もう一段階「聞き返す」余地を持たせたことです。

閾値のチューニングは、数字を動かして終わりではありませんでした。実際にブラウザで質問を打ち込み、想定外の挙動が出るたびにログとスコアに戻って原因を確認する——地味な繰り返しでしたが、そのぶん「なぜその数字にしたか」を説明できる状態になったと思います。

チャットボットは引き続きサイトの右下から使えます。今回の調整で、短い質問にも少し賢く反応するようになっているはずなので、ぜひ試してみてください。