社内問い合わせ対応やサポート業務にRAG(検索拡張生成、文書を検索してから回答を生成する仕組み)を導入したものの、想定外の質問への回答精度が伸び悩んでいるチームに向けた内容です。原因の多くは設計段階の分岐構造の欠如にあり、根拠の整理と確認方法をまとめました。
RAGは「質問を埋め込みベクトルに変換し、類似文書を検索し、プロンプトに埋め込んでLLM(大規模言語モデル)に回答させる」という一本道のパイプラインです。手順自体は書き手が持つ手順書に忠実に答えを返せるため、操作手順の質問には強く機能します。ところが、この一本道の設計には分岐がありません。
何が起きるか あらゆる質問を「検索」で処理してしまう
「月次締め処理のやり方」も「サーバーが起動時にクラッシュする」も、同じ検索パイプラインを通ります。
前者は手順書に答えがあるので問題なく回答できます。後者は文書に答えがないことが多く、無関係な文書の断片を無理やりつなぎ合わせた回答を返してしまいます。
さらに、ドキュメントに存在しない未知の障害やインシデントに対しても同じ検索処理が走ります。結果として「それらしいが的外れな回答」が生成され、利用者の信頼を損ないます。これは幻覚(ハルシネーション、事実に基づかない回答をもっともらしく生成する現象)とは別の問題で、検索対象そのものが質問の性質に合っていないという設計上のミスマッチです。
なぜ起きるか 分岐のないアーキテクチャが原因
原因は技術的な精度の問題ではなく、アーキテクチャの構造にあります。RAGパイプラインは「質問→埋め込み→検索→プロンプト→LLM→回答」という直線的な流れで固定されており、質問の性質を判断する層が存在しません。
実際の技術サポート担当者は、質問を受けた瞬間に「これは手順の質問か」「既知のエラーコードか」「未知の障害か」を頭の中で分類し、対応方針を変えています。分類の後に初めて「文書を調べる」「診断する」「人にエスカレーションする」を選んでいます。
クラシックなRAGにはこの「対応方針を決める」判断層がありません。すべての質問が同じ検索処理という一つの穴に押し込まれるため、手順書に答えがない質問ほど品質が崩れます。
これはソフトウェアアーキテクチャの観点で見ると、単一責務のパイプラインに複数の異なる要求(手順照会・障害診断・エスカレーション判断)を無理に押し込んだ設計と言えます。マイクロサービスの世界で「一つのサービスに責務を詰め込みすぎる」問題と構造的に同じです。責務ごとに分岐させる仕組みが必要になります。
自分のプロジェクトが該当するか確認する方法
次の観点で、現在運用しているRAG実装が同じ落とし穴にはまっていないか確認できます。
- パイプライン構成を確認する: 質問の種類によって処理経路が分岐する条件分岐(if文やルーティング処理)が実装に存在するか
- ログを確認する: 直近の問い合わせログで「ドキュメントに答えがなかった質問」に対して、どのような回答を返しているか抽出する
- 応答の根拠を確認する: 回答に引用元(ソース)が明示されているか、無関係な文書を無理に引用していないか
- エスカレーション経路を確認する: 「わからない」「人に確認してほしい」と判断して処理を止める仕組みがあるか
- 状態管理を確認する: 一連のやり取りの中で「これまで何を試したか」を保持する構造(会話履歴以上の情報)があるか
これらのうち複数が「ない」場合、今回取り上げているような一本道パイプラインの構造的な限界に直面している可能性が高いです。
対策の手順 分類層とエージェント分岐を追加する
対策の中心は、検索の前に「質問を分類し、対応方針を決める層」を挿入することです。LangGraph(LangChainが提供する、状態を持つエージェントをグラフ構造で接続するライブラリ)を使うと、この分岐構造を明示的なグラフとして表現できます。
まず、全エージェントが共有する状態(ステート)を定義します。TypedDictで質問文・実行計画・これまでの処理履歴・対応レベルなどを持たせます。
class AgentState(TypedDict):
messages: Annotated[Sequence[BaseMessage], operator.add]
question: str
plan: List[str]
past_steps: List[str]
response: str
next_agent: str
niveau_support: int # 1=解決 2=部分解決 3=人へのエスカレーション必要niveau_support(対応レベル)のようなフィールドを持たせておくと、あるエージェントが「これは自分の手に負えない」と判断した際に、値を3に設定するだけで後続の処理が自動的にエスカレーション経路へ切り替わります。エージェント間の意思疎通を、コードのif分岐ではなく共有状態の値で行う設計です。
次に、分類器(クラシファイア)を用意し、質問を「手順照会」「既知エラー」「未知の障害」に振り分けます。既存のRAGパイプラインは「手順照会」担当のエージェントとしてそのまま流用できます。
def rag_agent(state: AgentState) -> AgentState:
docs = vectorstore.similarity_search(state["question"], k=3)
if not docs:
return {**state, "niveau_support": 3, "past_steps": state["past_steps"] + ["RAG: no results"]}
answer = llm.invoke(f"Answer from context only.
Context: ...
Question: {state['question']}")
return {**state, "response": answer, "niveau_support": 1}このエージェントに加えて、既知のエラーコードを検索する診断エージェントと、人へのエスカレーション用チケットを生成するエージェントを追加し、LangGraphの条件付きエッジ(条件分岐を伴うグラフの辺)で接続します。既存のRAG資産を捨てる必要がなく、判断層を一枚足すだけで拡張できる点が実務上の利点です。
導入にあたっては、いきなり全問い合わせを対象にせず、まずログから「文書に答えがなかった質問」だけを抽出し、分類ルールの精度を検証してから本番の分岐に組み込む進め方が現実的です。分類ミスが起きると誤った経路に流れてしまうため、分類器自体の精度検証を後回しにしないことが重要です。
まとめ
RAGが一本道のパイプラインである以上、質問の性質を問わず同じ検索処理が走るのは設計上当然の挙動です。
まず自分のシステムのログを見て、ドキュメントに答えがない質問にどう応答しているかを確認してください。
条件分岐やエスカレーション経路がなければ、LangGraphのような状態共有型のエージェント構成を検討する余地があります。
対応レベルを表すフィールドを状態に持たせるだけでも、エージェント間の連携と人への引き継ぎ判断を整理しやすくなります。