0. はじめに
今回作成したのは、製造業の情報システム部門で利用することを想定した、社内IT問い合わせ向けのRAGサンプルアプリケーションです。
このテーマを選んだ理由は2つあります。
1つ目は、RAGの構築と評価を試すためのサンプルデータが必要だったことです。実在する社内マニュアルや問い合わせ履歴をそのまま検証に利用することは難しいため、Microsoft 365、Slack、Backlog、kintone、Monday.comなどの社内IT利用ガイドを模したデータを用意しました。
2つ目は、私たちの会社には製造業の情報システム部門のお客様が多く、社内ヘルプデスクや操作マニュアル検索が、RAGの活用場面としてイメージしやすかったためです。製造業の情報システム部門では、Microsoft 365の設定、アカウント管理、業務システムの操作方法など、繰り返し発生する問い合わせへの対応が少なくありません。
このRAGは、もともと私がリーダーをするJIMUC(日本IBMミドルウェアユーザー会)の生成AI分科会コミュニティ活動で実施する「RAG評価ワークショップ」の題材として準備していました。ワークショップでは、次の5種類、合計20件の質問を用意し、正確性(accuracy)、根拠性(groundedness)、回答関連性(relevance)の3軸で、人間とLLMの双方がRAG回答を評価するシナリオを想定しています。
- 単一文書から直接回答できる質問
- 複数文書の統合が必要な質問
- 条件や手順を整理する質問
- 参照文書に答えが存在しない質問
- 質問内容が曖昧で確認を必要とする質問
この評価セットは、単純な一問一答だけではなく、RAGが実際のヘルプデスクで直面しそうな失敗を確認するために作成したものです。
しかし、20件の回答を人間が読み、3つの観点で採点し、コメントを記録する作業には、想定以上に時間がかかります。さらに、アプリケーションを変更するたびに同じ評価を繰り返すのは、改善サイクルの速度を落とす要因になります。
そこで今回は、ワークショップでの人間評価に先立ち、フロンティアモデルを評価者として使い、RAGアプリケーションの改善を進めることにしました。
質問、期待回答、検索された文書、RAGの回答をフロンティアモデルへ渡し、正確性、根拠性、回答関連性を0から2点で評価します。その結果から問題のあるテストケースを特定し、プロンプト、検索件数、スコア閾値、サブクエリ生成などを変更しながら、V1からV11まで更新を行い検証しました。
最終的に採用したのは、最新のV11ではなくV8でした。
本記事では、RAGアプリケーションと評価基盤をどのように作り、評価結果から何を変更し、なぜV8が最もバランスのよい構成だと判断したのかを紹介します。
なお、人間による評価とLLMによる評価がどの程度一致するかについては、ワークショップ実施後、別の記事で取り上げる予定です。
1. 今回作ったAIヘルプデスクRAG
1.1 想定ユースケース
今回のサンプルでは、製造業の情報システム部門が社員から受ける問い合わせを想定しました。
対象としたのは、Microsoft 365、Slack、Backlog、kintone、Monday.comなどの社内ITサービスです。利用者からの質問に対し、社内の利用ガイドや運用ルールを根拠として回答します。
例えば、次のような質問です。
- Microsoft 365への初回サインインでは、どの設定が必要か
- Slackで共有した正式文書は、最終的にどこへ保存するか
- Backlogの新規プロジェクトで、どのワークフローを設定するか
- kintoneへCSVを取り込む際、どの文字コードを使用するか
- 資料に記載がない問い合わせに対して、推測せず回答できるか
1.2 システム全体の構成
セットされた質問から20通りの回答を生成する検証用RAG本体に加えて、人間評価用のWebUI、LLMによる自動評価、外部評価用のExcelを含めた評価基盤も用意しました。
RAGの回答生成には、IBM watsonx.ai上のGPT-OSS-120Bを採用しました。
選定時にはモデルの性能だけでなく、トークンコスト、長期的な利用継続性、モデル変更に伴うアプリケーションの更改頻度も考慮しました。RAGでは、回答生成モデルを変更すると、プロンプト、出力形式、検索コンテキストの与え方、評価結果などへ影響する可能性があります。
そこで今回は回答生成モデルを固定し、検索条件やプロンプトを段階的に変更することで、RAGの品質をどこまで改善できるかを検証しました。回答品質の評価にはフロンティアモデルを使用し、生成と評価でモデルの役割を分けています。
2. サンプルデータをMilvusへ登録する
2.1 サンプル文書の構造
データには、本文だけでなく、文書ID、タイトル、対象システム、カテゴリなどのメタデータを持たせます。
{
"doc_id": "slack-003",
"title": "ファイル共有とバージョン管理",
"system": "Slack",
"category": "運用ルール",
"content": "Slack上でのファイル共有は一時的な利用を前提とし、正式文書はSharePointまたはBacklogに保存します。"
}
2.2 Embeddingとベクトル登録
Embeddingモデルには回答生成モデルと同じくwatsonx.aiで利用可能な、intfloat/multilingual-e5-largeを使用しました。文書を1024次元のベクトルに変換し、Milvusへ登録します。
embedding = get_embedding(document_text)
collection.insert([
[doc_id],
[title],
[document_text],
[embedding]
])
Milvusでは、ベクトル間の近さを測る方法としてCOSINE類似度を使用し、検索を高速化するためのインデックスとしてHNSWを使用しました。
COSINE類似度は、質問と文書をそれぞれベクトルに変換し、二つのベクトルがどれだけ同じ方向を向いているかを測る方法です。値が高いほど、質問と文書の意味が近いと判断します。
例えば、「Slackで受けた問い合わせの管理方法」という質問と、「Slackの問い合わせをBacklogへ記録する」という文書は、使用している単語が完全には一致しなくても、内容が近いため高い類似度になることが期待されます。
一方、HNSWは、大量のベクトルの中から質問に近いベクトルを高速に探すためのインデックスです。すべての文書と順番に比較するのではなく、ベクトル同士を階層的なグラフ構造で結び、近い候補へ移動しながら検索します。
今回の構成では、HNSWを使って類似する文書候補を高速に探し、その候補をCOSINE類似度の高い順に取得しています。
サンプルプログラムでは、データの登録処理、検索処理、RAG回答生成、評価結果の保存を分離しました。これにより、検索処理だけを変更した場合と、プロンプトだけを変更した場合を比較しやすくしています。
3. まずは最小構成のRAGを作る
最初のバージョンでは、次の単純な流れで回答を生成しました。
- ユーザーの質問をEmbeddingする
- Milvusで類似文書を検索する
- 上位文書をコンテキストとしてまとめる
- 質問とコンテキストをwatsonx.aiへ渡す
- 回答と検索結果を評価用JSONへ保存する
query_vector = get_embedding(question)
results = collection.search(
data=[query_vector],
anns_field="embedding",
limit=3,
param={
"metric_type": "COSINE",
"params": {"ef": 100}
},
output_fields=["doc_id", "title", "content"]
)
初期構成では、SEARCH_TOP_K=3、回答生成時のtemperature=0.0とし、回答の安定性を重視しました。
SEARCH_TOP_K=3は、質問に近い文書を上位3件に絞る設定です。関係の薄い文書がコンテキストへ混ざることを抑え、回答の冗長化や判断の揺れを減らすことを狙いました。
temperature=0.0は、回答生成時のランダム性を抑える設定です。今回の目的は創造的な回答ではなく、社内IT利用ガイドに基づく一貫した回答の生成であるため、低い値を採用しました。また、同じ条件での回答差を小さくすることで、RAGを変更した際の評価結果も比較しやすくしています。
ただし、TOP_Kを小さくすると、複数文書にまたがる質問で必要な情報を取りこぼす可能性があります。この点は、後続バージョンで取得件数やサブクエリを調整しながら検証しました。
評価用JSONには、最終回答だけでなく、期待回答、正解とする文書ID、実際に検索されたコンテキスト、人間評価、LLM評価を保存します。
{
"question": "Slackで共有した正式文書はどこへ保存しますか。",
"expected_answer": "SharePointまたはBacklogへ保存します。",
"reference_doc_ids": ["slack-003"],
"search_context": [],
"rag_answer": "...",
"human_eval": {},
"llm_eval": {}
}
検索結果も保存することで、問題が発生したときに、次の2つを分けて確認できます。
- 必要な文書を検索できなかったのか
- 文書は検索できたが、回答生成モデルが利用できなかったのか
この切り分けは、その後の改善で重要になりました。
4. RAGの品質をどう測るか
4.1 20件のテストケース
テストケースは、実際のヘルプデスクで発生しやすい質問を意識して5種類に分類しました。
| 質問タイプ | 主な確認ポイント |
|---|---|
| 単一文書から直接回答 | 文書中の事実を正確に回答できるか |
| 複数文書の統合 | 複数の記述を組み合わせられるか |
| 条件・手順 | 順序、条件、例外を落とさないか |
| 文書に答えがない | 推測せず「記載なし」と回答できるか |
| 曖昧な質問 | 必要に応じて確認質問ができるか |
正解が明確な質問だけで評価すると、RAGは高いスコアを出しやすくなります。しかし、実際の問い合わせには、根拠文書に答えがないものや、製品名しか書かれていない曖昧な質問も含まれます。
そのため、あえて「答えなし」と「曖昧な質問」を評価セットへ加えました。
4.2 3つの評価軸
評価は次の3軸で実施しました。
Accuracy: 正確性
期待する内容が含まれているか、事実関係や手順に誤りがないかを評価します。
Groundedness: 根拠性
回答内容が検索された文書に基づいているか、文書にない情報を補っていないかを評価します。
Relevance: 回答関連性
質問へ直接答えているか、不要な説明や繰り返しがないかを評価します。
4.3 評価尺度を0から2点にした理由
当初は1から5点の評価を検討しました。しかし、5段階では「3と4の違い」など、評価者による判断の揺れが生じやすくなります。
そこで、評価尺度を次の3段階に変更しました。
| 点数 | 判断基準 |
|---|---|
| 2 | 要件を満たしている |
| 1 | 一部不足、余計な情報、軽微な問題がある |
| 0 | 誤り、根拠なし、または質問への回答になっていない |
この尺度であれば、完全、部分的、不適切という区別が明確になり、人間とLLMの双方が採点しやすくなります。
5. 人間評価とフロンティアモデル評価を準備する
5.1 StreamlitとExcelによる人間評価UI
人間による評価にはStreamlitアプリを用意しました。
また、コミュニティに参加している社外の方にも評価してもらいやすいように、Excelのファイルでも用意しました。
主な機能は次のとおりです。
- 評価者名の登録
- ケース単位の回答、期待回答、検索文書の確認
- 正確性、根拠性、回答関連性の採点
- コメント入力
- 評価結果の個別保存
- LLM評価との差分表示
- 評価軸ごとの平均スコア表示
ただし、20件すべてを読み、3軸で採点するには相応の時間がかかります。アプリケーションを変更するたびに、同じ人間評価を繰り返すのは現実的でありません。
そこで、今回の改善作業ではフロンティアモデルによる評価を先行させました。
5.2 LLM-as-a-Judge
今回はanthropic/claude-3.5-sonnetにスコアと評価理由をまとめて生成させましたが、採点部分だけを見れば、以前検証したJevを使う方法も考えられます。Jevは定義した選択肢や順序尺度から型付きの判断を返せるため、正確性、根拠性、回答関連性を0から2点で判定する処理とも相性がよさそうです。confidenceを使い、判断に迷った回答だけを人間が確認する構成も試してみたいところです。
評価モデルには、次の情報を渡します。
- ユーザーの質問
- 期待回答
- 正解とする文書
- 実際に検索されたコンテキスト
- RAGが生成した回答
- 0から2点の採点基準
評価モデルは、3軸のスコアと理由をJSONで返します。
{
"accuracy": {
"score": 2,
"reason": "期待回答に含まれる必要事項を満たしています。"
},
"groundedness": {
"score": 2,
"reason": "回答内容は検索コンテキスト内で確認できます。"
},
"relevance": {
"score": 1,
"reason": "質問には回答していますが、補足説明がやや冗長です。"
}
}
5.3 評価モデルの出力も常に安定するとは限らない
LLM-as-a-Judgeを使えば、評価作業が完全に自動化されるわけではありません。
実際にT05では、JSONを指定したにもかかわらず、英語の散文が返り、評価結果の抽出に失敗しました。このため、次の対策を追加しました。
- JSON以外を出力しないようプロンプトで明示する
- JSONパースに失敗した場合は再試行する
- 未評価のケースだけを再評価する
- 評価不能なケースを識別して保存する
- 評価結果の理由も保持し、後から確認できるようにする
評価モデル自体の失敗を隠さず記録することも、評価基盤の一部です。
6. V1の結果と見えてきた問題
V1の平均スコアは次のとおりでした。
| 評価軸 | V1 |
|---|---|
| Accuracy | 1.10 |
| Groundedness | 1.30 |
| Relevance | 0.85 |
| 総合平均 | 約1.08 |
一見すると自然な文章を返すケースもありましたが、テストケース単位で確認すると、複数の問題が見つかりました。
- 正しい文書を検索できているのに「資料に記載がない」と回答する
- 同じ文言を繰り返す
- 英語の思考過程のような文章が回答へ混入する
- 回答が途中で切れる
- 複数文書の情報を統合できない
- 文書にない手順を補ってしまう
- 質問が曖昧でも確認せずに回答する
特に重要だったのは、「検索に成功したケース」と「回答生成に成功したケース」が一致しなかったことです。
必要な文書が検索結果に含まれていても、生成モデルが該当部分を使わないことがあります。反対に、十分な文書が取得できていないのに、一般知識からもっともらしい回答を作る場合もあります。
この時点で、プロンプトだけでなく、検索設定と回答生成を分けて改善する必要があると判断しました。
7. V1からV8までの改善
すべてのバージョンを均等に説明するのではなく、改善の転換点となった変更を中心に紹介します。
7.1 APIとプロンプトの改善
V2では、次の変更を行いました。
-
chat()APIへ移行 -
max_tokens=512へ設定 - 回答プロンプトを見直す
-
TOP_K=5へ変更 - 検索スコア閾値を0.75に設定
これにより、直接回答できる質問や「資料に答えがない質問」が大きく改善しました。
プロンプトでは、文書に根拠がない場合に推測しないこと、回答に利用した情報をコンテキスト内へ限定することを明確にしました。
7.2 答えがない質問と曖昧質問への対応
V3では、次の指示を追加しました。
- 関連情報がない場合は、資料に記載がないことを明示する
- 必要に応じて確認先を案内する
- 質問が曖昧な場合は確認質問を返す
- 現在のコンテキストで回答可能な範囲も併記する
この変更によってGroundednessは1.70まで上昇しました。
単純に「分かりません」と答えるのではなく、どの情報が不足しているかを示すことで、ヘルプデスクとしての使いやすさも意識しました。
7.3 回答切断への対策
V4では、T10の回答が途中で切れる問題に対し、max_tokensを512から768へ変更しました。

T10のAccuracyは1から2へ改善しました。一方で、別のケースが悪化し、Groundedness平均は1.70から1.60へ低下しました。
この結果から分かったのは、特定ケースの修正が、必ずしもシステム全体の改善にはならないということです。
出力上限を増やすと回答切断を防げますが、冗長な説明や文書にない補足が増える可能性もあります。
7.4 サブクエリによる検索改善
V5以降では、元の質問から最大2個のサブクエリを生成し、複数回の検索結果を統合する方式を試しました。
例えば、次の質問を考えます。
Slackで受けた問い合わせをBacklogで管理する場合、どのように記録すればよいですか。
この質問から、次のようなサブクエリを作ります。
- Slackでの問い合わせ対応ルール
- Backlogへの問い合わせ記録方法
元の質問だけでは取得しにくい文書を、異なる表現で検索することが狙いです。
しかしV5では、APIレスポンスからcontentを取得する処理でエラーが発生し、サブクエリが空のまま処理されました。結果として、検索内容は実質的にV4と同じでした。
V6では、APIレスポンス構造を調査し、安全な取得処理とデバッグ情報を追加しました。
V7ではサブクエリ展開が正常に動作し、次の水準まで改善しました。
- Accuracy: 1.65
- Relevance: 1.75
- 総合平均: 1.65
8. なぜV11ではなくV8を選んだのか
8.1 V9では検索対象を増やした
V9では、LLMへ渡すコンテキスト数を増やしました。
一部のケースでは、これまで不足していた文書を取得できるようになりました。例えばT11では、必要なslack-001を検索結果へ含めることができました。
一方で、次の副作用も確認されました。
- T11では必要文書を取得できたが、生成モデルが内容を十分に使わなかった
- T07では、それまで取得できた
m365-002が再び漏れた - T19ではコンテキストが増えすぎ、回答が悪化した
- サブクエリが実行ごとに変化し、検索結果が安定しなかった
V9の平均スコアは次のとおりです。
| 評価軸 | V8 | V9 |
|---|---|---|
| Accuracy | 1.70 | 1.75 |
| Groundedness | 1.80 | 1.75 |
| Relevance | 1.60 | 1.60 |
| 総合平均 | 1.70 | 1.70 |
V9はAccuracyがV8を上回りましたが、Groundednessは低下し、総合平均は同じ1.70でした。
8.2 V10では「すべて網羅する」指示が逆効果になった
V10では、検索された複数文書の情報を回答へ漏れなく反映させるため、プロンプトへ「すべての文書から必要な情報を拾う」という趣旨の指示を追加しました。
この変更により、T07とT19は改善しました。
- T07:1/2/1から2/2/2
- T19:1/1/0から2/2/2
一方で、関係の薄い文書まで回答へ取り込むケースが増えました。
- T09:2/2/2から1/1/1
- T10:2/1/2から1/1/1
- T20:2/2/1から0/0/0
その結果、V10の総合平均はV9の1.70から1.60へ低下しました。Accuracyは1.75から1.55、Groundednessは1.75から1.60まで下がっています。
複数文書を網羅する指示は、必要情報の取りこぼしを減らす一方で、質問との関係が薄い情報まで回答へ混入させる可能性があります。
8.3 V11ではV8をベースに戻した
V11では、V10で追加した「すべて網羅する」という指示を削除し、Groundednessが最も高かったV8のシンプルなプロンプトをベースに戻しました。その上で、曖昧な質問に対する確認質問の指示など、一部の改善だけを残しました。
その結果、V11の総合平均は1.68まで回復しました。
- Accuracy:1.75
- Groundedness:1.70
- Relevance:1.60
- 総合平均:1.68
ただし、T20では無関係なbacklog-003とoutlook-002が検索結果へ入り、スコアが0/1/0まで低下しました。サブクエリの生成結果によって取得文書が変わり、回答品質が安定しない問題も残りました。
結論
V11ではV10からスコアを回復できましたが、総合平均はV8の1.70に届かず、GroundednessもV8の1.80を下回りました。
V9とV11はAccuracyが1.75と高い一方、検索結果やケース単位の回答に不安定さが見られました。社内ITヘルプデスクでは、回答の一部が正しいことだけでなく、社内文書に基づいて安定して回答することが重要です。
そのため、今回の検証では、総合平均、Groundedness、検索結果の安定性、プロンプトの複雑性を総合的に比較し、V8を最終構成として採用しました。
全バージョンのスコア推移
| バージョン | Accuracy | Groundedness | Relevance | 総合平均 |
|---|---|---|---|---|
| V1 | 1.10 | 1.30 | 0.85 | 1.08 |
| V2 | 1.55 | 1.60 | 1.45 | 1.53 |
| V3 | 1.60 | 1.70 | 1.55 | 1.62 |
| V4 | 1.60 | 1.60 | 1.55 | 1.58 |
| V5 | 1.55 | 1.55 | 1.55 | 1.55 |
| V7 | 1.65 | 1.55 | 1.75 | 1.65 |
| V8 | 1.70 | 1.80 | 1.60 | 1.70 |
| V9 | 1.75 | 1.75 | 1.60 | 1.70 |
| V10 | 1.55 | 1.60 | 1.65 | 1.60 |
| V11 | 1.75 | 1.70 | 1.60 | 1.68 |
9. V1からV11まで試して分かったこと
知見1: 検索成功と回答成功は別物
必要な文書を検索できても、その情報を回答生成モデルが採用するとは限りません。
RAGの失敗を分析するときは、次の段階を分けて確認する必要があります。
- 質問が適切にベクトル化されたか
- 必要な文書が検索候補に入ったか
- 必要文書が最終コンテキストに残ったか
- LLMが該当箇所を回答へ反映したか
- 文書にない情報を補っていないか
「正しい回答が出なかった」という事実だけでは、どこを直せばよいか判断できません。
知見2: TOP_Kを増やせばよいとは限らない
TOP_Kとは何か
TOP_Kは、ベクトル検索で類似度が高い文書を上位何件まで取得するかを指定するパラメータです。
例えば、TOP_K=3なら、質問に近いと判定された文書を、類似度の高い順に3件取得します。
results = collection.search(
data=[query_vector],
anns_field="embedding",
limit=3,
param={
"metric_type": "COSINE",
"params": {"ef": 100}
},
output_fields=["doc_id", "title", "content"]
)
この例ではlimit=3がTOP_Kに相当します。
TOP_Kを増やすと、必要な情報を取りこぼしにくくなります。特に、複数の文書を統合して回答する質問では有効です。
一方で、文書数を増やしすぎると、次の問題が起こります。
問題1: 関係の薄い文書が混ざる
例えば、ユーザーが次のように質問したとします。
Slackで対応依頼を受けた場合、どこに記録すればよいですか。
必要な根拠がSlackとBacklogの2文書に分かれている場合、TOP_Kが小さすぎると片方を取得できない可能性があります。
しかし、TOP_Kを増やしすぎると、Slackのファイル共有ルール、Microsoft 365の認証手順、Monday.comのタスク管理など、質問との関係が薄い文書まで混ざる可能性があります。
LLMは、渡された文書を質問へ関連付けようとするため、不要な情報を回答へ含めることがあります。
問題2: 必要な文書を取得しても使われない
検索結果に正しい文書が含まれていても、LLMがその文書を回答へ使うとは限りません。
今回の検証でも、必要なslack-001を取得できたケースで、その内容が最終回答へ十分に反映されませんでした。
検索の再現率を上げることと、回答の正確性を上げることは同じではありません。
問題3: 回答が冗長または不安定になる
コンテキストが増えると、次のような変化が起こる可能性があります。
- 正しいが長すぎる回答になる
- 質問と関係の薄い補足が入る
- 文書間の表現差を矛盾と判断する
- 「資料に記載がない」と答えるべき質問に推測で答える
- 実行ごとに採用する文書が変わる
TOP_Kは、単独で決めるのではなく、質問タイプやスコア閾値と組み合わせて評価します。
| 設定 | メリット | 主なリスク | 適した質問 |
|---|---|---|---|
| TOP_Kが小さい | ノイズが少なく、回答が簡潔 | 必要文書を取りこぼす | 単一文書で回答できる質問 |
| TOP_Kが大きい | 複数文書を取得しやすい | ノイズ、冗長化、回答の混乱 | 複数文書の統合が必要な質問 |
| スコア閾値あり | 関係の薄い文書を除外できる | 必要文書も除外する可能性 | 答えなし質問を含む構成 |
| サブクエリ併用 | 異なる言い換えで検索できる | 結果数と検索コストが増える | 曖昧または複合的な質問 |
TOP_Kは「大きいほど高精度になるパラメータ」ではなく、必要な情報の取りこぼしと、不要な情報の混入を調整するパラメータです。
知見3: 平均スコアだけではバージョンを選べない
平均スコアが同じでも、失敗しているケースは異なります。
バージョン選定では、次の要素も確認する必要があります。
- 問題ケースの重大度
- 実行ごとの安定性
- Groundedness
- 検索コスト
- 推論時間
- 実装の複雑さ
- 将来の保守性
例えば、社内ヘルプデスクでは、少し冗長な回答よりも、文書にない手順を断定する回答の方が危険です。用途に応じて、どの失敗を許容しないかを先に決める必要があります。
知見4: 推論時の調整だけでは限界がある
V1からV11までで主に試したのは、ユーザーから質問を受けて回答を生成する段階、つまり推論時の調整です。
- 回答生成プロンプトの修正
- TOP_Kの変更
- 類似度スコア閾値の変更
- サブクエリの生成
- LLMへ渡すコンテキスト件数の変更
-
max_tokensの変更 - 答えがない場合や質問が曖昧な場合の指示追加
これらはアプリケーションコードから変更しやすく、短いサイクルで試せます。
しかし、必要な情報が適切な単位でベクトルデータベースへ登録されていなければ、推論時の設定だけでは解決できません。
次の改善段階では、検索前のデータ設計や検索結果の並べ替えが必要になります。
1. チャンク設計の見直し
チャンクは、文書をベクトルデータベースへ登録するときの分割単位です。
例えば、1つのSlack運用ガイドに次の内容が含まれているとします。
- チャンネルの作成ルール
- ダイレクトメッセージの利用方針
- ファイル共有ルール
- 正式文書の保存先
- 退職者のアカウント処理
文書全体を1チャンクとして登録すると、質問と直接関係しない情報も同じベクトルへ混ざります。
「問い合わせ対応をDMで行ってよいか」と質問しても、文書全体の主題がSlack運用一般になるため、「DMは避ける」という一部分が検索へ十分に反映されない可能性があります。
そこで、文書全体を1チャンクにするのではなく、1トピックを1チャンクとして分割します。
チャンク1: Slackチャンネルの作成ルール
チャンク2: 問い合わせ対応ではDMを避ける
チャンク3: ファイル共有は一時利用とする
チャンク4: 正式文書はSharePointまたはBacklogへ保存する
期待できる改善は次のとおりです。
- 質問と該当ルールの意味的な距離が近くなる
- 文書内の小さな注意事項を検索しやすくなる
- 不要な情報がコンテキストへ混ざりにくくなる
- 根拠を特定しやすくなる
- Groundednessの向上が期待できる
一方で、細かく分けすぎると手順の前後関係や例外条件が失われます。そのため、親文書ID、対象システム、トピック、チャンク番号などをメタデータとして持たせます。
{
"doc_id": "slack-001-c02",
"parent_doc_id": "slack-001",
"system": "Slack",
"topic": "DM利用方針",
"chunk_index": 2,
"content": "業務上の問い合わせ対応ではDMを避け..."
}
2. メタデータフィルタリング
メタデータは、本文以外に文書へ付与する分類情報です。
- 対象システム: Slack、Backlog、Microsoft 365
- 文書種別: 操作手順、運用ルール、FAQ
- 対象者: 一般社員、管理者、情報システム部門
- 有効期間
- 文書バージョン
- 公開範囲
- 問い合わせカテゴリ
質問から対象システムを判定し、ベクトル検索の対象を絞ります。
search_filter = 'system in ["Slack", "Backlog"]'
results = collection.search(
data=[query_vector],
anns_field="embedding",
limit=5,
expr=search_filter,
output_fields=["doc_id", "system", "title", "content"]
)
期待できる改善は次のとおりです。
- Microsoft 365やMonday.comなどの不要文書を除外できる
- TOP_Kを増やしてもノイズが増えにくくなる
- 同じ単語が複数システムで使われる場合の誤検索を減らせる
- 参照してはいけない文書を制御できる
- 部門や権限に応じた検索範囲を構成できる
ただし、フィルターの判定を誤ると、正しい文書を検索対象から除外します。
例えば「Slackの問い合わせをBacklogに記録する」という質問をSlackだけに絞ると、Backlog側の運用ルールを取得できません。1つに固定せず、複数候補を許容する設計が必要です。
3. HyDEによる検索
HyDEはHypothetical Document Embeddingsの略です。
ユーザーの短い質問をそのままベクトル化するのではなく、LLMに「この質問に対する理想的な回答文」を仮に作らせ、その文章を使って検索します。
例えば、質問が次のように短い場合を考えます。
SlackのDMは?
このままでは、ユーザーが知りたい内容が十分に表現されていません。
HyDEでは、LLMに次のような仮想回答を生成させます。
業務上の問い合わせ対応ではSlackのダイレクトメッセージを避け、関係者が確認できる公開チャンネルまたは指定された問い合わせ管理システムを使用します。
この仮想回答をベクトル化して検索します。
期待できる改善は次のとおりです。
- 短い質問や曖昧な質問を検索しやすくなる
- 質問と文書で使われる言葉が違う場合に有効
- ユーザーの表現を文書寄りの表現へ展開できる
- 単語一致では拾いにくい運用ルールを取得できる可能性がある
一方で、仮想回答が誤った前提を含むと、その誤りに近い文書を検索する可能性があります。また、検索前のLLM呼び出しが増えるため、応答時間、API呼び出し回数、トークン消費、結果の非決定性も増えます。
すべての質問へ適用するのではなく、短文や検索失敗時に限定する構成も考えられます。
4. リランキング
リランキングは、最初のベクトル検索で取得した文書候補を、別のモデルで質問との関係が強い順に並べ直す処理です。
ベクトル検索は、大量の文書から候補を高速に絞ることが得意です。一方で、質問と文書の細かな関係を判定する用途には限界があります。
そこで、まず広めに候補を取得し、その後で精密に並べ替えます。
ベクトル検索では、質問と文書を個別にベクトル化して類似度を比較します。
質問ベクトル ↔ 文書ベクトル
Cross-Encoderは、質問と文書の組み合わせを同時に読み、関連度を評価します。
「質問 + 文書」をまとめて入力 → 関連度スコア
期待できる改善は次のとおりです。
- 意味が近いだけの文書を順位から下げられる
- 質問への答えを実際に含む文書を上位へ移せる
- 複数の似た文書から、より具体的な文書を選べる
- TOP_Kを広めに取得しつつ、LLMへ渡す件数を抑えられる
- AccuracyとRelevanceの改善が期待できる
一方で、候補文書を1件ずつ再評価するため、計算量が増えます。例えば、最初に20件取得して最終的に5件へ絞る場合、Cross-Encoderは20組の「質問と文書」を評価します。応答時間と計算資源を考慮する必要があります。
改善方法の比較
| 改善対象 | 変更するもの | 主に解決したい問題 | 期待する改善 | 主な注意点 |
|---|---|---|---|---|
| チャンク設計 | 文書の分割単位 | 文書内の一部情報が検索されない | Groundedness、検索精度 | 細かすぎると文脈が失われる |
| メタデータフィルター | 検索対象の範囲 | 他システムの文書が混ざる | Relevance、安定性 | 絞り込み誤りで正解を除外する |
| HyDE | 検索に使う質問表現 | 短文、曖昧表現、語彙差 | 検索再現率 | コスト、遅延、仮想回答の誤り |
| リランキング | 検索結果の順序 | 意味は近いが答えを含まない文書が上位になる | Accuracy、Relevance | 計算量と応答時間が増える |
10. 今回の検証で得た結論
V1からV11までの検証では、プロンプト、TOP_K、スコア閾値、サブクエリ、コンテキスト数など、主に推論時のパラメータを調整しました。その結果、初期バージョンと比べて大きな改善が見られました。
一方で、必要な文書を検索できないケースや、検索できても回答へ反映されないケースは残りました。これらは、TOP_Kやプロンプトだけでは解決しにくい問題です。
今回の検証から得たポイントをまとめます。
- RAG本体より先に、評価可能なデータ構造を用意する
- 正解できる質問だけでなく、答えなしや曖昧質問も評価する
- 検索結果と生成回答を分けて保存する
- 人間評価とLLM-as-a-Judgeは役割を分ける
- 変更内容をバージョン単位で記録する
- 平均点だけでなく、失敗ケースの変化を見る
- TOP_Kを増やせば品質が上がるとは限らない
- 最新バージョンが最良とは限らない
- プロンプト調整の次には、チャンク、メタデータ、HyDE、リランキングを検討する
最終的には、Groundedness、検索結果の安定性、実装の複雑性を含めて比較し、V8を採用しました。
RAGの改善は、LLMへの指示を工夫するだけでは完結しません。データ設計、検索、回答生成、評価を一つのパイプラインとして捉え、それぞれの失敗を切り分けることが重要です。
11. 次に試したいこと
今回のRAGの性能改善検証は一旦ここで終わりにしますが、さらなる改善を行うとすると、推論時のパラメータ調整から、データと検索パイプラインの改善へ進みます。
- 1トピック1チャンクを意識して文書を再分割する
- システム名、文書種別、対象者などのメタデータを追加する
- 複合質問では複数システムを検索対象にする
- 短文や検索失敗時にHyDEを適用する
- ベクトル検索の候補をCross-Encoderでリランキングする
- V8と改善版を同じ20件で再評価する
- 応答時間とAPIコストも評価指標へ加える
また、JIMUCのRAG評価ワークショップでは人間による評価を実施し、今回のLLM評価と比較する予定です。
次の記事では、次の観点を扱いたいと考えています。
- 人間とLLMのスコアはどの程度一致するか
- 評価理由の違いはどこに表れるか
- 評価者によってスコアはどの程度変わるか
- LLM-as-a-Judgeを継続的なRAG評価へ利用できるか
- 人間が確認すべきケースをどのように絞り込むか
フロンティアモデルによる評価は、人間評価を不要にするものではありません。改善サイクルを先に回し、人間が確認すべき論点を絞るための仕組みとして利用することに価値があると考えています。
12. スクリプトとサンプルデータ
下記のgithubリポジトリにスクリプトとサンプルデータをアップしています。
ご参考までに。
https://github.com/kmizutalw/rag-evaluation-workshop
