
RAGチャットボットの初回応答を約10秒から約2秒に短縮した話
要約
ある B2B 向け RAG チャットボットの初回応答(TTFT:Time To First Token)に約10秒かかっていた問題を、約2.4秒まで縮めた事例(p50 で約76%短縮)。当初インフラを疑ったが、各処理段の実測で真因は別にあった。以前「文書検索や文書スクロールを多めに行って回答精度を上げよう」と試したアーキテクチャの名残で、全リクエストに直列約4回の LLM 呼び出しがあることが原因だった。ループ自体を撤廃したのではなく、必要な数だけツール呼び出しが起きるように作り替えた。教訓は2つ:推測の前に計測すること、そして過去の最適化の前提が変わったらアーキテクチャ自体を見直すこと。
背景
SolidChat は、企業の Web サイトに RAG 型の生成 AI チャットボットを埋め込むサービスです。
このサービスの応答エンジンは、以前 「文書検索や文書スクロールを多めに行うことで回答精度を上げよう」と試した設計 の上に立っていました。質問1件に対して、manager がエージェントを統括し、ActorAgent がツール使用ループ(最大3反復)を回し、最後に FinishAgent が回答を整える、という多段構成でした。結果として、1リクエストあたり LLM への呼び出しは直列で約4回(リトライを含めると約5.8回)発生していました。当時の目的に対しては意味があったものの、運用していくうちに、ユーザーの要求は「精度よりも、まず早く回答が見たい」にシフトしていきました。精度のために組まれた多段の構造が、回答が遅いという形で問題になってしまいました。
課題
ある法人顧客向けのチャットボットで、利用者が質問を送信してから最初の文字が表示されるまで(TTFT)に約10秒かかっていました。回答が出始めるまでの10秒の沈黙は、利用者にとって「返事が来ない」と感じる長さです。目標は2秒に置きました。
解決策
まず計測する
最初に疑ったのはインフラでした。Lambda のコールドスタート、embedding API のリージョン跨ぎ、ベクトル検索の遅延 — どれもありえる原因です。
しかし、推測で手を動かす前に、各処理段にタイミング計測を仕込み、CloudWatch のログで「何にどの程度の時間がかかっているのか」を実測しました。結果、コールドスタートや、リージョン跨ぎも多少の時間はかかるものの、支配的な要因ではありませんでした。時間の大半はアーキテクチャそのものにありました。
真因:常に生成AIを複数回呼ぶ設計になっていた
直列4回の LLM 呼び出しは、それぞれ数百ミリ秒〜数秒かかります。積み重なれば10秒は説明がついてしまう。さらに計測で分かったのは、次の3点です。
- リクエストの約4〜5割はツールを一度も呼んでいなかった。取得済みドキュメントやプロンプト内の情報だけで答えられる質問が多かった。本来なら1回で終わるはずだが、安易に答えに飛びつかないように設計されていたため、 ActorAgent はほぼ毎回、上限の3回まで呼び出されていた。
- 無条件で走る追加の1呼び出しであるFinishAgentがあった。元々、 マルチエージェントを使った設計でのUI のストリーミング表示の都合で入った、回避策であり、本質的な価値貢献が乏しかった。
このプロダクトには、外部システムへのAPIを必要とする質問も、SolidChatの RAG を必要とする質問もユーザーは投げます。本来は必要な呼び出し分の時間だけがかかるべきですが、LLMの呼び出しが固定の回数だけ発生するようになっていたことが問題でした。
設計変更:必要な数だけLLMを呼び出すように直す
前段の「正規化 → embedding → RAG検索(上位30件) → 回答構築」は維持し、LLM とのやり取りに呼び出し上限をもうけました。ループは次のように動きます。
- モデルがツールを呼ばずに答えれば、その1ラウンドで終了(純粋な RAG リクエスト)。
- モデルがツールを呼べば、「ツール呼び出し → 実行 → 結果を踏まえて続きを返す」のラウンドが、必要な回数だけ繰り返される(ただし呼び出し回数は最大4回)。
ツール呼び出しはモデルが必要としたときにだけ足され、即答できるリクエストはすぐに回答が用意されます。撤去したのは、無条件で走っていた FinishAgent、、多段 manager の間接層です。生成AIの呼び出しは、3回固定ではなく、3回上限となるように変更しました。テキストは生成と同時にリアルタイムでストリームするので、最初の文字が早く出ます。
補足すると、ツール呼び出し時、モデルはツールを叩く前に、短い前置き、たとえば「調べますね」を返しがちです。これが即座にストリームされるため、ツール呼び出しでも最初の文字(TTFT)は速いです。
結果
新構成は本番運用中の実測(合計 N=2,729)を、ツール呼び出しの有無で分割しました。旧構成は、切り替え前にプロダクションで動いていた期間(計装あり)を同じログから集計したものです。
| 構成 | p50 | p90 | p95 | p99 |
|---|---|---|---|---|
| 旧構成(N=73) | 約9.9秒 | 約15.6秒 | 約17.6秒 | 約19.0秒 |
| 新構成・ツール呼び出しなし(N=2,583) | 約2.4秒 | 約3.7秒 | 約4.0秒 | 約5.2秒 |
| 新構成・ツール呼び出しあり(N=146) | 約2.7秒 | 約4.0秒 | 約4.2秒 | 約7.1秒 |
新構成の初回応答(TTFT)の分布(50ms 区切り)。ツール呼び出しあり/なしが混在しているが、ツール呼び出しは全体の約5%で中央値も近いため、2つの山に分かれず、単峰になる(ピークは約1.9〜2.0秒)。右端は ≥5.3s を1本に集約した裾で、最大約42.4秒(稀なコールドスタート)を含む。
旧構成の中央値 約9.9秒に対し、新構成(ツール呼び出しなし、トラフィックの約95%)の中央値は 約2.4秒 — p50 で約76%短縮です(p95 でも 約17.6秒 → 約4.0秒で約77%)。当初目標の2秒に中央値で僅かに届かないのは、ツール呼び出し経路のせいではなく(それは全体の約5%・146件にすぎない)、最速のツール無し経路自体が約2.4秒に底打ちしているためです(その内訳は embedding・ベクトル検索・モデルが最初のトークンを出すまでの時間)。
正直な但し書きを2つ。
- TTFT は「最初の文字が出るまで」の時間です。ツール呼び出し時、その最初の文字は前置き(「調べますね」)であり、本回答はツールの往復のあとに続きます。したがって TTFT は全経路で速い一方、ツール呼び出しリクエストの回答完了までは時間がかかります(例:あるツール呼び出しで TTFT 約2.85秒・回答完了 約9.9秒)。
- 観測された最大は約42.4秒で、ツール無し経路の極端な外れ値(ほかに約16秒・約33秒も数件)。同経路の p99 は約5.2秒なので全体のごく一部で、コールドスタートや一時的なストールと見られます(上図右端の裾)。
教訓
過去の最適化の前提は陳腐化する
旧構成の重いループは、もともと「文書検索や文書スクロールを多めにして精度を上げたい」という目的で組まれたものでした。ただ運用していくうちに、ユーザーの要求は「早く回答が見たい」へとシフトしていき、精度のために組まれた構造が、新しい優先軸では遅延コストとしてそのまま残ることになりました。新機能を足すときには手を入れても、「既にある構造が、いまの目的に照らしてまだ妥当か」を問い直す機会は意外と少ない。今回の改善の半分は、新しい設計を発見したというより、陳腐化した前提に縛られていた構造をリセットしたことです。エージェントループそのものを否定する話ではなく、それを使う・使わないの判断は、いまの目的とコストに照らして定期的に見直すべき、というのが教訓です。
推測の前に計測する
最初に疑ったインフラは主因ではありませんでした。もし推測のままコールドスタート対策に時間を投じていたら、効果の薄い改善に労力を使っていたはずです。各処理の所要時間を実測したことで、初めて真因がわかりました。最適化の前に、まず計測する — 当たり前のようでありながら、見落としやすい原則です。
SolidChat代表・ソフトウェアエンジニア。AIスタートアップの創業メンバーとしてプロダクト開発から上場までを経験。現在は生成AIチャットボット『SolidChat』を開発・運営中。AIや自動化技術を活用したSaaS開発を得意とする。物理と数学書を読むのが好きで、毎朝30分間の読書を日課にしている。