ストーリー
Naive RAG vs Advanced RAG
Naive RAGの限界
graph LR
Q["ユーザークエリ"] --> VS["単純ベクトル検索"] --> TK["Top-K取得"] --> LLM["LLMに渡す"] --> A["回答"]
style Q fill:#e3f2fd,stroke:#1565c0
style A fill:#e8f5e9,stroke:#2e7d32
問題点:
1. クエリと文書の意味的ギャップ(Semantic Gap)
2. 単一ステージの検索では精度限界がある
3. 関連性の低いチャンクがノイズになる
4. 複雑な質問を分解できない
Advanced RAGの全体像
graph TD
subgraph "Advanced RAG パイプライン"
subgraph クエリ前処理
QR["Query Rewriting<br/>(クエリの書き換え)"]
QD["Query Decomposition<br/>(複雑な質問の分解)"]
HY["HyDE<br/>(仮想ドキュメント生成)"]
end
subgraph 検索戦略
MS["Multi-stage Retrieval<br/>(多段階検索)"]
HS["Hybrid Search<br/>(ベクトル + キーワード)"]
CR["Contextual Retrieval<br/>(コンテキスト補完)"]
end
subgraph 後処理
RR["Re-ranking<br/>(関連度の再評価)"]
CC["Contextual Compression<br/>(不要部分の除去)"]
CE["Citation Extraction<br/>(出典の抽出)"]
end
end
クエリ前処理 --> 検索戦略 --> 後処理
クエリ前処理テクニック
1. Query Rewriting
ユーザーの質問をベクトル検索に最適化された形に書き換えます。
class QueryRewriter {
constructor(private readonly llm: LLMService) {}
async rewrite(originalQuery: string): Promise<string[]> {
const prompt = `あなたは検索クエリの最適化エキスパートです。
以下のユーザーの質問に対して、ベクトル検索に最適な3つの異なるクエリを生成してください。
元の意味は保持しつつ、異なる表現やキーワードを使ってください。
ユーザーの質問: ${originalQuery}
JSON形式で回答してください:
{"queries": ["クエリ1", "クエリ2", "クエリ3"]}`;
const response = await this.llm.complete(prompt);
const parsed = JSON.parse(response);
return parsed.queries;
}
}
// 使用例
// "デプロイ時にDBのマイグレーションはどうすればいい?"
// → ["データベースマイグレーション デプロイ手順",
// "本番環境でのスキーマ変更方法",
// "CI/CDパイプラインでのDB更新手順"]
2. HyDE (Hypothetical Document Embeddings)
クエリの代わりに、LLMに仮想的な回答ドキュメントを生成させ、それを使って検索します。
class HyDERetriever {
constructor(
private readonly llm: LLMService,
private readonly embedder: EmbeddingService,
private readonly vectorStore: VectorStore,
) {}
async retrieve(query: string, topK: number = 5): Promise<RetrievedContext[]> {
// Step 1: LLMに仮想的な回答を生成させる
const hypotheticalDoc = await this.generateHypotheticalDocument(query);
// Step 2: 仮想ドキュメントのエンベディングで検索
const embedding = await this.embedder.embed(hypotheticalDoc);
const results = await this.vectorStore.search(embedding, topK);
return results;
}
private async generateHypotheticalDocument(query: string): Promise<string> {
const prompt = `以下の質問に対する回答を、社内技術ドキュメントの一節として書いてください。
実際のドキュメントがどのような内容かを想像して、それらしい文章を生成してください。
質問: ${query}
ドキュメントの一節:`;
return this.llm.complete(prompt);
}
}
3. Query Decomposition
複雑な質問を複数のサブ質問に分解し、それぞれに対して検索を行います。
class QueryDecomposer {
constructor(private readonly llm: LLMService) {}
async decompose(complexQuery: string): Promise<string[]> {
const prompt = `以下の複雑な質問を、それぞれ独立して検索可能なシンプルなサブ質問に分解してください。
質問: ${complexQuery}
JSON形式で回答:
{"subQueries": ["サブ質問1", "サブ質問2", ...]}`;
const response = await this.llm.complete(prompt);
const parsed = JSON.parse(response);
return parsed.subQueries;
}
}
// 使用例
// "KubernetesとTerraformを使ったマイクロサービスのデプロイ手順と、
// そのときのモニタリング設定方法を教えて"
// → ["Kubernetesでのマイクロサービスデプロイ手順",
// "Terraformによるインフラ構成管理",
// "マイクロサービスのモニタリング設定方法"]
Multi-stage Retrieval
2段階検索パターン
第1段階で広く候補を取得し、第2段階で精密に絞り込みます。
class MultiStageRetriever {
constructor(
private readonly embedder: EmbeddingService,
private readonly vectorStore: VectorStore,
private readonly reranker: RerankerService,
) {}
async retrieve(query: string, finalTopK: number = 5): Promise<RetrievedContext[]> {
// Stage 1: Broad Retrieval(広範囲検索)
// 最終的に必要な数の3-5倍を取得
const queryEmbedding = await this.embedder.embed(query);
const candidates = await this.vectorStore.search(queryEmbedding, finalTopK * 4);
// Stage 2: Re-ranking(精密な関連度評価)
const reranked = await this.reranker.rerank(query, candidates);
// 上位のみ返す
return reranked.slice(0, finalTopK);
}
}
Re-ranking の実装
interface RerankerService {
rerank(query: string, candidates: RetrievedContext[]): Promise<RetrievedContext[]>;
}
// Cross-Encoder ベースの Re-ranker
class CrossEncoderReranker implements RerankerService {
async rerank(
query: string,
candidates: RetrievedContext[]
): Promise<RetrievedContext[]> {
// Cohere Rerank API の例
const response = await fetch('https://api.cohere.ai/v1/rerank', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.COHERE_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'rerank-multilingual-v3.0',
query,
documents: candidates.map(c => c.chunk.content),
top_n: candidates.length,
}),
});
const data = await response.json();
return data.results.map((r: { index: number; relevance_score: number }) => ({
...candidates[r.index],
score: r.relevance_score,
}));
}
}
// LLM ベースの Re-ranker(コストは高いが精度は最高)
class LLMReranker implements RerankerService {
constructor(private readonly llm: LLMService) {}
async rerank(
query: string,
candidates: RetrievedContext[]
): Promise<RetrievedContext[]> {
const prompt = `以下の質問に対して、各ドキュメントの関連度を1-10で評価してください。
質問: ${query}
${candidates.map((c, i) => `[Doc${i + 1}]: ${c.chunk.content.slice(0, 200)}`).join('\n\n')}
JSON形式で回答: {"scores": [{"doc": 1, "score": 8}, ...]}`;
const response = await this.llm.complete(prompt);
const parsed = JSON.parse(response);
return parsed.scores
.sort((a: { score: number }, b: { score: number }) => b.score - a.score)
.map((s: { doc: number; score: number }) => ({
...candidates[s.doc - 1],
score: s.score / 10,
}));
}
}
Agentic RAG
Self-RAGパターン
LLMが自ら検索の必要性を判断し、検索結果の品質を評価するパターンです。
class AgenticRAG {
constructor(
private readonly retriever: MultiStageRetriever,
private readonly llm: LLMService,
) {}
async answer(query: string): Promise<{ answer: string; sources: string[] }> {
// Step 1: 検索が必要か判断
const needsRetrieval = await this.assessRetrievalNeed(query);
if (!needsRetrieval) {
const answer = await this.llm.complete(
`以下の質問に簡潔に回答してください: ${query}`
);
return { answer, sources: [] };
}
// Step 2: 検索実行
let contexts = await this.retriever.retrieve(query);
// Step 3: 検索結果の品質評価
const qualityAssessment = await this.assessContextQuality(query, contexts);
if (qualityAssessment.needsMoreInfo) {
// 追加検索が必要な場合
const additionalQueries = qualityAssessment.suggestedQueries;
for (const additionalQuery of additionalQueries) {
const moreContexts = await this.retriever.retrieve(additionalQuery);
contexts = [...contexts, ...moreContexts];
}
// 重複除去
contexts = this.deduplicateContexts(contexts);
}
// Step 4: 回答生成
const answer = await this.generateAnswer(query, contexts);
// Step 5: 回答の忠実性検証
const isGrounded = await this.verifyGroundedness(answer, contexts);
if (!isGrounded) {
return {
answer: '申し訳ございませんが、お手持ちの情報源からは正確な回答を生成できませんでした。',
sources: [],
};
}
return {
answer,
sources: contexts.map(c => c.chunk.metadata.source as string),
};
}
private async assessRetrievalNeed(query: string): Promise<boolean> {
const prompt = `以下の質問に回答するために、外部知識ベースの検索が必要ですか?
一般知識で回答可能な場合は "no"、社内固有の情報が必要な場合は "yes" と回答してください。
質問: ${query}
回答 (yes/no):`;
const response = await this.llm.complete(prompt);
return response.trim().toLowerCase() === 'yes';
}
private async assessContextQuality(
query: string,
contexts: RetrievedContext[]
): Promise<{ needsMoreInfo: boolean; suggestedQueries: string[] }> {
const prompt = `以下の質問に対して、検索結果は十分な情報を含んでいますか?
質問: ${query}
検索結果:
${contexts.map(c => c.chunk.content.slice(0, 150)).join('\n---\n')}
JSON形式で回答:
{"sufficient": true/false, "missingTopics": ["不足しているトピック"], "suggestedQueries": ["追加検索クエリ"]}`;
const response = await this.llm.complete(prompt);
const parsed = JSON.parse(response);
return {
needsMoreInfo: !parsed.sufficient,
suggestedQueries: parsed.suggestedQueries ?? [],
};
}
private async verifyGroundedness(
answer: string,
contexts: RetrievedContext[]
): Promise<boolean> {
const contextText = contexts.map(c => c.chunk.content).join('\n');
const prompt = `以下の回答が、提供された情報源に基づいているか検証してください。
情報源に含まれない情報が回答に含まれている場合は "no" と回答してください。
情報源:
${contextText}
回答:
${answer}
検証結果 (yes/no):`;
const response = await this.llm.complete(prompt);
return response.trim().toLowerCase() === 'yes';
}
private deduplicateContexts(contexts: RetrievedContext[]): RetrievedContext[] {
const seen = new Set<string>();
return contexts.filter(c => {
if (seen.has(c.chunk.id)) return false;
seen.add(c.chunk.id);
return true;
});
}
private async generateAnswer(
query: string,
contexts: RetrievedContext[]
): Promise<string> {
const contextText = contexts
.map((c, i) => `[出典${i + 1}] ${c.chunk.content}`)
.join('\n\n');
const prompt = `参考情報のみを根拠にして質問に回答してください。
回答に出典番号を含めてください。
参考情報:
${contextText}
質問: ${query}
回答:`;
return this.llm.complete(prompt);
}
}
Contextual Retrieval パターン
Anthropicが提案したContextual Retrievalは、チャンクにドキュメント全体の文脈を埋め込むことで検索精度を向上させるテクニックです。
class ContextualRetrieval {
constructor(private readonly llm: LLMService) {}
async enrichChunks(
document: string,
chunks: Chunk[]
): Promise<Chunk[]> {
const enrichedChunks: Chunk[] = [];
for (const chunk of chunks) {
const contextPrefix = await this.generateContext(document, chunk.content);
enrichedChunks.push({
...chunk,
content: `${contextPrefix}\n\n${chunk.content}`,
});
}
return enrichedChunks;
}
private async generateContext(
fullDocument: string,
chunkContent: string
): Promise<string> {
const prompt = `<document>
${fullDocument}
</document>
上記のドキュメント内の以下のチャンクについて、簡潔なコンテキスト(50-100文字)を生成してください。
このコンテキストは、チャンクの内容をドキュメント全体の中で位置づけるものです。
<chunk>
${chunkContent}
</chunk>
コンテキスト:`;
return this.llm.complete(prompt);
}
}
RAGパターンの選択ガイド
| パターン | 実装コスト | 効果 | 推奨場面 |
|---|---|---|---|
| Query Rewriting | 低 | 中 | 最初に試すべき改善 |
| Hybrid Search | 中 | 高 | キーワードが重要な場面 |
| Re-ranking | 中 | 高 | 検索精度が不十分な場合 |
| HyDE | 中 | 中-高 | 短いクエリが多い場合 |
| Query Decomposition | 中 | 高 | 複雑な質問が多い場合 |
| Contextual Retrieval | 高 | 高 | インデックス時に一手間かけられる場合 |
| Agentic RAG | 高 | 最高 | 品質要求が最も高い場合 |
まとめ
| ポイント | 内容 |
|---|---|
| Naive RAGの限界 | 単純なベクトル検索では本番品質に到達しない |
| クエリ前処理 | Query Rewriting、HyDE、Query Decompositionで検索品質を向上 |
| Multi-stage | 広範囲検索 → Re-rankingの2段階で精度向上 |
| Agentic RAG | LLMが検索の必要性と品質を自律的に判断 |
チェックリスト
- Naive RAG と Advanced RAG の違いを理解した
- クエリ前処理の3つのテクニックを理解した
- Multi-stage Retrieval と Re-ranking を理解した
- Agentic RAG の概念を理解した
次のステップへ
高度なRAGパターンを学びました。次のセクションでは、構築したRAGシステムの品質をどう測定するかを学びます。測れないものは改善できません。
RAGの品質改善は、仮説→検証→改善の繰り返しです。
推定読了時間: 40分