RAG検索拡張生成ディープ実戦:Naive RAGからAgentic RAGへの3世代進化とエンタープライズ実装
なぜRAGがエンタープライズAIの必修科目なのか
大規模言語モデルがどれほど強力でも、3つの根本的な弱点がある。2026年になっても、生のLLM APIコールでエンタープライズQAを構築しているなら、必ずこれらの壁にぶつかっているはずだ:
| 弱点 | 現れ | ビジネスへの影響 |
|---|---|---|
| 知識のカットオフ | 学習データが特定の時点で止まっている | 最新の政策や製品情報に回答できない |
| ハルシネーション | もっともらしいが存在しない事実を捏造 | 意思決定の誤導、法的リスク |
| プライベート知識の欠落 | 社内文書、プロセス、用語に不慣れ | 汎用モデルはドメイン特化QAに代替不可 |
RAG(Retrieval-Augmented Generation)の本質:LLMに回答前に「資料を調べさせる」こと。検索結果で生成を制約し、3つの弱点を根本から解決する。
データ:2026年のエンタープライズAI導入プロジェクトの92%がRAGアーキテクチャを採用。純粋なPrompt手法は5%未満。
┌─────────────────────────────────────────────────────────┐
│ RAGなし vs RAGあり:根本的な違い │
├──────────────────────┬──────────────────────────────────┤
│ 生のLLMコール │ RAG拡張コール │
├──────────────────────┼──────────────────────────────────┤
│ ユーザー質問 → LLM → │ ユーザー質問 → 検索 → コンテキスト│
│ 回答(記憶から) │ 注入 → LLM → 回答+引用元 │
│ │ (調査後に回答) │
├──────────────────────┼──────────────────────────────────┤
│ 知識:カットオフ │ 知識:リアルタイム、更新可能 │
│ 精度:制御不能 │ 精度:検索結果で制約 │
│ トレーサビリティ:無 │ トレーサビリティ:全主張に引用付与 │
└──────────────────────┴──────────────────────────────────┘
Naive RAG → Advanced RAG → Agentic RAG:3世代の進化
RAGは静的ではない。2023年から2026年にかけて、RAGアーキテクチャは3世代の進化を遂げた:
┌───────────────────────────────────────────────────────────────┐
│ RAG 3世代進化ロードマップ │
├───────────────────┬───────────────────┬───────────────────────┤
│ Naive RAG │ Advanced RAG │ Agentic RAG │
│ (2023) │ (2024-2025) │ (2026) │
├───────────────────┼───────────────────┼───────────────────────┤
│ Query → 検索 → │ クエリ書き換え → │ Agent自律的意思決定 → │
│ 生成 │ ハイブリッド検索 → │ マルチターン+自己評価 │
│ │ リランク → 生成 │ 動的ツール呼び出し │
├───────────────────┼───────────────────┼───────────────────────┤
│ 問題:検索品質が低い│ 問題:自律性不足 │ 問題:複雑性が高い │
│ ハルシネーション │ ハルシネーション │ ハルシネーション <5% │
│ 30%+ │ 10-15% │ 能動的推論+自己修正 │
│ 制御不能 │ 制御可能だが受動的 │ │
└───────────────────┴───────────────────┴───────────────────────┘
3世代の比較
| 次元 | Naive RAG | Advanced RAG | Agentic RAG |
|---|---|---|---|
| クエリ処理 | 生クエリを直接検索 | クエリ書き換え/拡張/HyDE | Agentがサブ問題に分解 |
| 検索戦略 | 単一ベクトル検索 | ハイブリッド検索+リランク | マルチターン検索+ツール呼び出し |
| 生成戦略 | コンテキストを直接結合 | 厳選コンテキスト+引用 | 自己評価+反復最適化 |
| 典型的ハルシネーション率 | 30-40% | 10-15% | <5% |
| エンドツーエンド遅延 | 1-2s | 2-4s | 5-15s |
| ユースケース | デモ/プロトタイプ | 本番環境 | 複雑な推論 |
| 実装複雑性 | 低 | 中 | 高 |
Embeddingモデルの選定と評価
EmbeddingはRAGの「目」——間違ったモデルを選ぶと、検索品質は即座に低下する。2026年の主要Embeddingモデル比較:
| モデル | 次元 | MTEBスコア | 中国語MTEB | 価格/1M tokens | デプロイ方式 |
|---|---|---|---|---|---|
| text-embedding-3-large | 3072 | 68.4 | 64.2 | $0.13 | API |
| text-embedding-3-small | 1536 | 62.3 | 58.7 | $0.02 | API |
| bge-m3 | 1024 | 65.8 | 70.1 | 無料 | ローカル/API |
| gte-Qwen2-1.5B | 1536 | 67.2 | 72.5 | 無料 | ローカル/API |
| gte-Qwen2-7B | 3584 | 70.1 | 75.8 | 無料 | ローカル(GPU必要) |
| Cohere embed-v4 | 1024 | 66.1 | 61.3 | $0.10 | API |
| Voyage-3 | 1024 | 67.8 | 63.9 | $0.12 | API |
選定ガイド
| シナリオ | 推奨モデル | 理由 |
|---|---|---|
| 中国語中心のエンタープライズKB | gte-Qwen2-1.5B | 中国語MTEB最高、無料ローカルデプロイ |
| 多言語混在 | bge-m3 | ネイティブ多言語対応、1024次元でコスパ良 |
| 最高品質追求 | gte-Qwen2-7B | 7Bパラメータ、最良結果、GPU必要 |
| 迅速なローンチ、運用なし | text-embedding-3-small | API呼び出し、$0.02/1M tokens |
| 予算あり、安定性優先 | text-embedding-3-large | OpenAIエコシステム、3072次元高精度 |
Java Embeddingサービス実装
public class EmbeddingService {
private final OpenAiChatModel chatModel;
private final RestTemplate restTemplate;
private final String embeddingApiUrl;
private final String embeddingModel;
public EmbeddingService(OpenAiChatModel chatModel, String apiUrl, String model) {
this.chatModel = chatModel;
this.restTemplate = new RestTemplate();
this.embeddingApiUrl = apiUrl;
this.embeddingModel = model;
}
public float[] embed(String text) {
Map<String, Object> request = Map.of(
"model", embeddingModel,
"input", text
);
ResponseEntity<Map> response = restTemplate.postForEntity(
embeddingApiUrl + "/embeddings",
request,
Map.class
);
List<Double> embedding = (List<Double>) ((Map) ((List<?>) response.getBody().get("data")).get(0)).get("embedding");
float[] result = new float[embedding.size()];
for (int i = 0; i < embedding.size(); i++) {
result[i] = embedding.get(i).floatValue();
}
return result;
}
public List<float[]> embedBatch(List<String> texts) {
Map<String, Object> request = Map.of(
"model", embeddingModel,
"input", texts
);
ResponseEntity<Map> response = restTemplate.postForEntity(
embeddingApiUrl + "/embeddings",
request,
Map.class
);
List<?> dataList = (List<?>) response.getBody().get("data");
return dataList.stream()
.map(item -> {
List<Double> embedding = (List<Double>) ((Map) item).get("embedding");
float[] arr = new float[embedding.size()];
for (int i = 0; i < embedding.size(); i++) {
arr[i] = embedding.get(i).floatValue();
}
return arr;
})
.collect(Collectors.toList());
}
public static float cosineSimilarity(float[] a, float[] b) {
float dotProduct = 0.0f;
float normA = 0.0f;
float normB = 0.0f;
for (int i = 0; i < a.length; i++) {
dotProduct += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dotProduct / (float) (Math.sqrt(normA) * Math.sqrt(normB));
}
}
ベクトルデータベース比較:PGVector vs Milvus vs Qdrant
2026年、ベクトルデータベースは「必要かどうか」ではなく「どれを選ぶか」の問題だ。3つの主要ソリューションの深い比較:
| 次元 | PGVector | Milvus | Qdrant |
|---|---|---|---|
| 基盤 | PostgreSQL拡張 | スタンドアロン分散システム | スタンドアロンRustサービス |
| 最大ベクトル数 | 数千万 | 数百億 | 数十億 |
| クエリ遅延(1Mベクトル) | 25-40ms | 15-25ms | 10-18ms |
| ハイブリッド検索 | tsvectorと併用必要 | ネイティブ対応 | ネイティブ対応 |
| 分散 | PG論理レプリケーション依存 | ネイティブ分散 | シャード対応 |
| 運用複雑性 | 低(PG再利用) | 高 | 中 |
| トランザクション | ACID | 限定 | 限定 |
| フィルタリング | フルSQL | スカラーフィルタ | Payloadフィルタ |
| エコシステム | Spring Data JPA | Java SDK | Java SDK |
| ユースケース | 既存PG、中小規模 | 超大規模、高同時実行 | 高性能、中規模 |
アーキテクチャ比較
┌────────────────────────────────────────────────────────────────┐
│ PGVector アーキテクチャ │
├────────────────────────────────────────────────────────────────┤
│ Application ──→ PostgreSQL (pgvector拡張) │
│ ├── ベクトルインデックス (HNSW/IVFFlat) │
│ ├── 全文検索 (tsvector) │
│ ├── リレーショナルデータ (行ストア) │
│ └── トランザクション/ACID │
│ メリット:追加運用ゼロ、フルSQL機能 │
│ デメリット:大規模で性能制限、分散が弱い │
├────────────────────────────────────────────────────────────────┤
│ Milvus アーキテクチャ │
├────────────────────────────────────────────────────────────────┤
│ Application ──→ Proxy ──→ Coordinator │
│ ├── Query Node (検索) │
│ ├── Data Node (書き込み) │
│ └── Index Node (インデックス構築) │
│ ストレージ:MinIO/S3 + etcd │
│ メリット:数十億規模、ネイティブ分散、クラウドネイティブ │
│ デメリット:運用が複雑、リソース消費大 │
├────────────────────────────────────────────────────────────────┤
│ Qdrant アーキテクチャ │
├────────────────────────────────────────────────────────────────┤
│ Application ──→ Qdrant Service (Rust) │
│ ├── HNSWインデックス │
│ ├── Payloadフィルタリング │
│ ├── WAL永続化 │
│ └── シャードクラスタ │
│ メリット:Rust高性能、低遅延、シンプルなAPI │
│ デメリット:極端な大規模ではMilvusに劣る │
└────────────────────────────────────────────────────────────────┘
Spring Boot Qdrant統合
@Configuration
public class QdrantConfig {
@Bean
public QdrantClient qdrantClient() {
return new QdrantClient(
QdrantGrpcClient.newBuilder("localhost", 6334, false).build()
);
}
}
@Service
public class QdrantVectorStore {
private final QdrantClient qdrantClient;
private final EmbeddingService embeddingService;
private static final String COLLECTION_NAME = "knowledge_base";
private static final int VECTOR_SIZE = 1536;
public QdrantVectorStore(QdrantClient qdrantClient, EmbeddingService embeddingService) {
this.qdrantClient = qdrantClient;
this.embeddingService = embeddingService;
}
public void createCollection() throws ExecutionException, InterruptedException {
qdrantClient.createCollectionAsync(
CollectionInfo.newBuilder()
.setCollectionName(COLLECTION_NAME)
.setVectorsConfig(VectorsConfig.newBuilder()
.setParams(VectorParams.newBuilder()
.setSize(VECTOR_SIZE)
.setDistance(Distance.Cosine)
.build())
.build())
.setOptimizersConfig(OptimizersConfigDiff.newBuilder()
.setIndexingThreshold(20000)
.build())
.setHnswConfig(HnswConfigDiff.newBuilder()
.setM(16)
.setEfConstruct(100)
.build())
.build()
).get();
}
public void upsertDocuments(List<DocumentChunk> chunks) throws ExecutionException, InterruptedException {
List<float[]> embeddings = embeddingService.embedBatch(
chunks.stream().map(DocumentChunk::getContent).collect(Collectors.toList())
);
List<PointStruct> points = new ArrayList<>();
for (int i = 0; i < chunks.size(); i++) {
DocumentChunk chunk = chunks.get(i);
points.add(PointStruct.newBuilder()
.setId(PointId.newBuilder().setUuid(UUID.randomUUID().toString()).build())
.setVectors(Vectors.newBuilder().setVector(Vector.newBuilder()
.addAllData(FloatVector.newBuilder()
.addAllData(toFloatList(embeddings.get(i)))
.build().getDataList())
.build()).build())
.putAllPayload(Map.of(
"content", Value.newBuilder().setStringValue(chunk.getContent()).build(),
"source", Value.newBuilder().setStringValue(chunk.getSource()).build(),
"section", Value.newBuilder().setStringValue(chunk.getSection()).build()
))
.build());
}
qdrantClient.upsertAsync(COLLECTION_NAME, points).get();
}
public List<SearchResult> search(String query, int topK) throws ExecutionException, InterruptedException {
float[] queryVector = embeddingService.embed(query);
List<ScoredPoint> results = qdrantClient.searchAsync(
SearchPoints.newBuilder()
.setCollectionName(COLLECTION_NAME)
.setVector(Vector.newBuilder().addAllData(toFloatList(queryVector)).build())
.setLimit(topK)
.setWithPayload(true)
.build()
).get();
return results.stream()
.map(point -> new SearchResult(
point.getPayload().get("content").getStringValue(),
point.getPayload().get("source").getStringValue(),
point.getScore()
))
.collect(Collectors.toList());
}
private List<Float> toFloatList(float[] arr) {
List<Float> list = new ArrayList<>(arr.length);
for (float v : arr) {
list.add(v);
}
return list;
}
}
Spring Boot PGVector統合
@Entity
@Table(name = "documents")
public class DocumentEntity {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
private String content;
private String source;
private String section;
@Column(columnDefinition = "vector(1536)")
private float[] embedding;
@Column(columnDefinition = "tsvector")
private String searchText;
}
@Mapper
public interface DocumentMapper extends BaseMapper<DocumentEntity> {
@Select("SELECT *, embedding <=> #{embedding} AS distance " +
"FROM documents " +
"WHERE embedding <=> #{embedding} < #{threshold} " +
"ORDER BY embedding <=> #{embedding} " +
"LIMIT #{limit}")
List<DocumentEntity> vectorSearch(@Param("embedding") float[] embedding,
@Param("threshold") float threshold,
@Param("limit") int limit);
@Select("SELECT *, ts_rank(search_text, plainto_tsquery(#{query})) AS rank " +
"FROM documents " +
"WHERE search_text @@ plainto_tsquery(#{query}) " +
"ORDER BY rank DESC " +
"LIMIT #{limit}")
List<DocumentEntity> fullTextSearch(@Param("query") String query,
@Param("limit") int limit);
}
Advanced RAG実戦:クエリ書き換え + ハイブリッド検索 + リランク
これは2026年の本番RAG環境における標準アーキテクチャだ。単一ベクトル検索ではもう不十分——ハイブリッド検索+リランクこそが正解。
┌──────────────────────────────────────────────────────────────────┐
│ Advanced RAG 完全パイプライン │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ユーザークエリ:「会社の2025年Q4収益成長の要因は何か?」 │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 1. クエリ書き換え │ │
│ │ 元のクエリ → 3つの書き換え+HyDE │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ┌─────────┴─────────┐ │
│ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ │
│ │ ベクトル │ │ BM25 │ │
│ │ 検索 │ │ 検索 │ │
│ │(意味的) │ │(キーワード)│ │
│ └────┬─────┘ └────┬─────┘ │
│ │ │ │
│ └────────┬─────────┘ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 2. RRF融合(Reciprocal Rank │ │
│ │ Fusion) │ │
│ │ ベクトル0.7 + キーワード0.3 │ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 3. Cross-Encoderリランク │ │
│ │ 精細なquery-doc関連性スコアリング│ │
│ └──────────────┬──────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────┐ │
│ │ 4. コンテキスト注入 + LLM生成 │ │
│ │ 引用付きの正確な回答 │ │
│ └─────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
クエリ書き換え実装
@Service
public class QueryRewriteService {
private final OpenAiChatModel chatModel;
public QueryRewriteService(OpenAiChatModel chatModel) {
this.chatModel = chatModel;
}
public List<String> rewriteQuery(String originalQuery) {
String prompt = """
以下のユーザークエリを3つの異なる角度から検索クエリに書き換え、検索リコールを向上させてください。
要件:
1. 元のクエリのコア意図を保持する
2. 同じニーズを異なる視点から表現する
3. 可能な専門用語や同義語を追加する
4. JSON形式で出力:{"rewrites": ["クエリ1", "クエリ2", "クエリ3"]}
元のクエリ:%s
""".formatted(originalQuery);
ChatResponse response = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.3).build()
));
String content = response.getResult().getOutput().getContent();
Map<String, Object> result = new ObjectMapper().readValue(content, Map.class);
return (List<String>) result.get("rewrites");
}
public String generateHyde(String query) {
String prompt = """
以下の質問に詳細な回答を提供してください。確信がない場合でもできる限り答えてください。
この回答は関連ドキュメントの検索に使用されるため、できるだけ多くの
関連詳細と専門用語を含めてください。
質問:%s
""".formatted(query);
ChatResponse response = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.5).build()
));
return response.getResult().getOutput().getContent();
}
}
ハイブリッド検索 + RRF融合
@Service
public class HybridRetrievalService {
private final QdrantVectorStore vectorStore;
private final DocumentMapper documentMapper;
private final EmbeddingService embeddingService;
private static final double VECTOR_WEIGHT = 0.7;
private static final double KEYWORD_WEIGHT = 0.3;
private static final int RRF_K = 60;
public HybridRetrievalService(QdrantVectorStore vectorStore,
DocumentMapper documentMapper,
EmbeddingService embeddingService) {
this.vectorStore = vectorStore;
this.documentMapper = documentMapper;
this.embeddingService = embeddingService;
}
public List<SearchResult> hybridSearch(String query, int topK) {
List<SearchResult> vectorResults = vectorSearch(query, topK * 2);
List<SearchResult> keywordResults = keywordSearch(query, topK * 2);
List<SearchResult> fused = reciprocalRankFusion(
List.of(vectorResults, keywordResults),
List.of(VECTOR_WEIGHT, KEYWORD_WEIGHT)
);
return fused.stream().limit(topK).collect(Collectors.toList());
}
private List<SearchResult> vectorSearch(String query, int limit) {
try {
return vectorStore.search(query, limit);
} catch (Exception e) {
return Collections.emptyList();
}
}
private List<SearchResult> keywordSearch(String query, int limit) {
List<DocumentEntity> results = documentMapper.fullTextSearch(query, limit);
return results.stream()
.map(doc -> new SearchResult(doc.getContent(), doc.getSource(), 0.0))
.collect(Collectors.toList());
}
private List<SearchResult> reciprocalRankFusion(
List<List<SearchResult>> resultSets,
List<Double> weights) {
Map<String, Double> scoreMap = new HashMap<>();
Map<String, SearchResult> resultMap = new HashMap<>();
for (int setIndex = 0; setIndex < resultSets.size(); setIndex++) {
List<SearchResult> results = resultSets.get(setIndex);
double weight = weights.get(setIndex);
for (int rank = 0; rank < results.size(); rank++) {
String key = results.get(rank).getContent();
double score = weight / (rank + 1 + RRF_K);
scoreMap.merge(key, score, Double::sum);
resultMap.putIfAbsent(key, results.get(rank));
}
}
return scoreMap.entrySet().stream()
.sorted(Map.Entry.<String, Double>comparingByValue().reversed())
.map(entry -> {
SearchResult result = resultMap.get(entry.getKey());
return new SearchResult(result.getContent(), result.getSource(), entry.getValue());
})
.collect(Collectors.toList());
}
}
Cross-Encoderリランク
@Service
public class RerankService {
private final OpenAiChatModel chatModel;
public RerankService(OpenAiChatModel chatModel) {
this.chatModel = chatModel;
}
public List<SearchResult> rerank(String query, List<SearchResult> candidates, int topK) {
List<CompletableFuture<ScoredResult>> futures = candidates.stream()
.map(candidate -> CompletableFuture.supplyAsync(() -> scoreRelevance(query, candidate)))
.collect(Collectors.toList());
List<ScoredResult> scored = futures.stream()
.map(CompletableFuture::join)
.sorted(Comparator.comparingDouble(ScoredResult::score).reversed())
.limit(topK)
.collect(Collectors.toList());
return scored.stream()
.map(sr -> new SearchResult(sr.content(), sr.source(), sr.score()))
.collect(Collectors.toList());
}
private ScoredResult scoreRelevance(String query, SearchResult candidate) {
String prompt = """
以下のドキュメントとクエリの関連性を評価してください。0から10の整数のみ出力してください。
クエリ:%s
ドキュメント:%s
スコア:
""".formatted(query, candidate.getContent());
ChatResponse response = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.0).build()
));
double score = Double.parseDouble(response.getResult().getOutput().getContent().trim());
return new ScoredResult(candidate.getContent(), candidate.getSource(), score / 10.0);
}
}
完全なAdvanced RAGパイプライン
@Service
public class AdvancedRagPipeline {
private final QueryRewriteService queryRewriteService;
private final HybridRetrievalService hybridRetrievalService;
private final RerankService rerankService;
private final EmbeddingService embeddingService;
private final OpenAiChatModel chatModel;
public RagResponse query(String userQuery) {
List<String> allQueries = new ArrayList<>();
allQueries.add(userQuery);
allQueries.addAll(queryRewriteService.rewriteQuery(userQuery));
String hydeAnswer = queryRewriteService.generateHyde(userQuery);
allQueries.add(hydeAnswer);
List<SearchResult> allResults = new ArrayList<>();
for (String q : allQueries) {
allResults.addAll(hybridRetrievalService.hybridSearch(q, 10));
}
List<SearchResult> deduplicated = deduplicate(allResults);
List<SearchResult> reranked = rerankService.rerank(userQuery, deduplicated, 5);
String context = buildContext(reranked);
String answer = generateAnswer(userQuery, context);
return new RagResponse(answer, reranked);
}
private List<SearchResult> deduplicate(List<SearchResult> results) {
Map<String, SearchResult> uniqueMap = new LinkedHashMap<>();
for (SearchResult result : results) {
uniqueMap.putIfAbsent(result.getContent(), result);
}
return new ArrayList<>(uniqueMap.values());
}
private String buildContext(List<SearchResult> results) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < results.size(); i++) {
SearchResult r = results.get(i);
sb.append("[%d] 出典:%s\n%s\n\n".formatted(i + 1, r.getSource(), r.getContent()));
}
return sb.toString();
}
private String generateAnswer(String query, String context) {
String systemPrompt = """
あなたはナレッジベースQAアシスタントです。以下の検索ドキュメントに基づいてユーザーの質問に答えてください。
ルール:
1. 検索ドキュメントに基づいてのみ回答し、情報を捏造しない
2. すべての主張に引用元を付与する [1][2]...
3. 検索結果が不十分な場合は明確に述べる
4. 最新かつ最も関連性の高い情報を優先する
検索ドキュメント:
%s
""".formatted(context);
ChatResponse response = chatModel.call(new ChatRequest(
List.of(
new Message("system", systemPrompt),
new Message("user", query)
),
ChatOptions.builder().withTemperature(0.1).build()
));
return response.getResult().getOutput().getContent();
}
}
ドキュメントチャンキング戦略
チャンキングはRAGの基盤——不適切なチャンキングは、どんなに優れた検索も無駄にする。4つの主要戦略の深い比較:
| 戦略 | 原理 | メリット | デメリット | ユースケース | 粒度 |
|---|---|---|---|---|---|
| 固定長 | token/文字数で分割 | シンプル、制御可能 | 意味的完全性を損なう | ログ、表データ | 固定chunk_size |
| 意味チャンキング | Embedding類似度ブレークポイント検出 | 意味的完全性が良い | 計算コスト高 | 技術文書、論文 | 適応的 |
| 構造チャンキング | 見出し/セクション/段落で分割 | 文書構造を保持 | パーサーが必要 | Markdown/HTML/PDF | 構造階層別 |
| トピックチャンキング | LLMがトピック境界を識別 | トピックの凝集度が高い | LLM呼び出しコスト高 | 長文書、マルチトピック | トピック別 |
固定長チャンキング(Java)
public class FixedLengthChunker {
private final int chunkSize;
private final int overlapSize;
public FixedLengthChunker(int chunkSize, int overlapSize) {
this.chunkSize = chunkSize;
this.overlapSize = overlapSize;
}
public List<DocumentChunk> chunk(String content, String source) {
List<DocumentChunk> chunks = new ArrayList<>();
int start = 0;
int index = 0;
while (start < content.length()) {
int end = Math.min(start + chunkSize, content.length());
String text = content.substring(start, end);
if (end < content.length()) {
int lastPeriod = text.lastIndexOf('。');
int lastNewline = text.lastIndexOf('\n');
int breakPoint = Math.max(lastPeriod, lastNewline);
if (breakPoint > chunkSize / 2) {
text = text.substring(0, breakPoint + 1);
end = start + breakPoint + 1;
}
}
chunks.add(new DocumentChunk(text, source, "chunk-" + index, index));
start = end - overlapSize;
index++;
}
return chunks;
}
}
意味チャンキング(Java)
@Service
public class SemanticChunker {
private final EmbeddingService embeddingService;
private static final double SIMILARITY_THRESHOLD = 0.85;
public SemanticChunker(EmbeddingService embeddingService) {
this.embeddingService = embeddingService;
}
public List<DocumentChunk> chunk(String content, String source) {
List<String> sentences = splitSentences(content);
if (sentences.isEmpty()) {
return Collections.emptyList();
}
List<float[]> embeddings = embeddingService.embedBatch(sentences);
List<DocumentChunk> chunks = new ArrayList<>();
StringBuilder currentChunk = new StringBuilder(sentences.get(0));
int chunkIndex = 0;
for (int i = 1; i < sentences.size(); i++) {
float similarity = EmbeddingService.cosineSimilarity(
embeddings.get(i - 1), embeddings.get(i)
);
if (similarity >= SIMILARITY_THRESHOLD) {
currentChunk.append(sentences.get(i));
} else {
chunks.add(new DocumentChunk(
currentChunk.toString(), source, "chunk-" + chunkIndex, chunkIndex
));
currentChunk = new StringBuilder(sentences.get(i));
chunkIndex++;
}
}
if (!currentChunk.isEmpty()) {
chunks.add(new DocumentChunk(
currentChunk.toString(), source, "chunk-" + chunkIndex, chunkIndex
));
}
return chunks;
}
private List<String> splitSentences(String text) {
return Arrays.stream(text.split("(?<=[。!?.!?])"))
.map(String::trim)
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
構造チャンキング(Markdown)
@Service
public class MarkdownStructureChunker {
private static final Pattern HEADING_PATTERN = Pattern.compile("^(#{1,6})\\s+(.+)$", Pattern.MULTILINE);
public List<DocumentChunk> chunk(String markdown, String source) {
List<Section> sections = parseSections(markdown);
return sections.stream()
.map(section -> new DocumentChunk(
section.content(),
source,
section.heading(),
section.level()
))
.collect(Collectors.toList());
}
private List<Section> parseSections(String markdown) {
List<Section> sections = new ArrayList<>();
Matcher matcher = HEADING_PATTERN.matcher(markdown);
List<Integer> positions = new ArrayList<>();
List<String> headings = new ArrayList<>();
List<Integer> levels = new ArrayList<>();
while (matcher.find()) {
positions.add(matcher.start());
headings.add(matcher.group(2).trim());
levels.add(matcher.group(1).length());
}
for (int i = 0; i < positions.size(); i++) {
int start = positions.get(i);
int end = (i + 1 < positions.size()) ? positions.get(i + 1) : markdown.length();
String content = markdown.substring(start, end).trim();
sections.add(new Section(headings.get(i), content, levels.get(i)));
}
if (!positions.isEmpty() && positions.get(0) > 0) {
String preamble = markdown.substring(0, positions.get(0)).trim();
if (!preamble.isEmpty()) {
sections.add(0, new Section("前置き", preamble, 0));
}
}
return sections;
}
}
Agentic RAG:Agentが検索のタイミングを決定
Agentic RAGは2026年の最先端アプローチだ。コアアイデア:Agent自身が検索するかどうか、何を検索するか、検索が十分かどうかを決定する。
┌──────────────────────────────────────────────────────────────────┐
│ Agentic RAG ワークフロー │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ユーザークエリ:「製品Aと製品Bの技術アーキテクチャの違いを比較」 │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Agentの思考:製品Aのアーキテクチャ文書が│ │
│ │ 必要 │ │
│ └──────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 検索:製品Aの文書 → コンテキストを取得 │ │
│ └──────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Agentの評価:製品Bの文書もまだ必要 │ │
│ └──────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 検索:製品Bの文書 → コンテキストを取得 │ │
│ └──────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ Agentの評価:情報は十分、比較回答を │ │
│ │ 生成できる │ │
│ └──────────────────┬─────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────────┐ │
│ │ 生成:引用付きのA/B製品アーキテクチャ │ │
│ │ 比較分析 │ │
│ └────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
Agentic RAGコア実装
@Service
public class AgenticRagService {
private final OpenAiChatModel chatModel;
private final HybridRetrievalService retrievalService;
private final RerankService rerankService;
private static final int MAX_ITERATIONS = 5;
public AgenticRagResponse query(String userQuery) {
List<RetrievalStep> steps = new ArrayList<>();
List<SearchResult> allContext = new ArrayList<>();
String currentThought = userQuery;
for (int i = 0; i < MAX_ITERATIONS; i++) {
AgentDecision decision = decideAction(currentThought, allContext);
steps.add(new RetrievalStep(i + 1, decision.thought(), decision.action()));
if ("GENERATE".equals(decision.action())) {
String answer = generateAnswer(userQuery, allContext);
return new AgenticRagResponse(answer, allContext, steps);
}
if ("SEARCH".equals(decision.action())) {
List<SearchResult> results = retrievalService.hybridSearch(decision.searchQuery(), 5);
List<SearchResult> reranked = rerankService.rerank(decision.searchQuery(), results, 3);
allContext.addAll(reranked);
currentThought = decision.thought();
}
if ("INSUFFICIENT".equals(decision.action())) {
return new AgenticRagResponse(
"申し訳ありませんが、複数回の検索を行っても、ご質問にお答えするための十分な情報を見つけることができませんでした。",
allContext, steps
);
}
}
String answer = generateAnswer(userQuery, allContext);
return new AgenticRagResponse(answer, allContext, steps);
}
private AgentDecision decideAction(String query, List<SearchResult> context) {
String contextStr = context.isEmpty() ? "(検索結果なし)" :
context.stream()
.map(r -> "- " + r.getContent().substring(0, Math.min(200, r.getContent().length())))
.collect(Collectors.joining("\n"));
String prompt = """
あなたはRAG Agentで、次のアクションを決定します。
ユーザークエリ:%s
現在のコンテキスト:
%s
次のアクションを決定してください:
- SEARCH: さらなる検索が必要(search_queryを提供)
- GENERATE: 十分な情報がある、回答を生成可能
- INSUFFICIENT: 十分な情報を見つけられない
JSON形式で出力:
{"thought": "あなたの推論", "action": "SEARCH|GENERATE|INSUFFICIENT", "search_query": "検索クエリ(SEARCHの場合のみ)"}
""".formatted(query, contextStr);
ChatResponse response = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.1).build()
));
try {
Map<String, String> result = new ObjectMapper().readValue(
response.getResult().getOutput().getContent(), Map.class
);
return new AgentDecision(
result.get("thought"),
result.get("action"),
result.getOrDefault("search_query", "")
);
} catch (Exception e) {
return new AgentDecision("パース失敗、回答生成を試行", "GENERATE", "");
}
}
private String generateAnswer(String query, List<SearchResult> context) {
String contextStr = context.stream()
.map(r -> "[出典: " + r.getSource() + "]\n" + r.getContent())
.collect(Collectors.joining("\n\n"));
String systemPrompt = """
以下の検索ドキュメントに基づいてユーザーの質問に答えてください。
ルール:
1. 検索ドキュメントのみに基づいて回答し、捏造しない
2. すべての主張に引用元を付与する
3. 情報が不十分な場合は明確に述べる
検索ドキュメント:
%s
""".formatted(contextStr);
ChatResponse response = chatModel.call(new ChatRequest(
List.of(new Message("system", systemPrompt), new Message("user", query)),
ChatOptions.builder().withTemperature(0.1).build()
));
return response.getResult().getOutput().getContent();
}
}
マルチモーダルRAG:画像、表、コードの統一検索
2026年、RAGはもはやテキスト検索だけではない。マルチモーダルRAGにより、画像、表、コードも検索・引用可能になった。
┌──────────────────────────────────────────────────────────────────┐
│ マルチモーダルRAGアーキテクチャ │
├──────────────────────────────────────────────────────────────────┤
│ │
│ 入力ドキュメント(PDF/HTML/Markdown) │
│ │ │
│ ├── テキスト抽出 ──→ テキストEmbedding ──→ ベクトルDB │
│ │ │
│ ├── 表抽出 ──→ 表→テキスト説明 ──→ Embedding ──→ ベクトルDB│
│ │ │
│ ├── 画像抽出 ──→ ビジュアルEmbedding ──→ ベクトルDB │
│ │ (CLIP/Qwen-VL) │
│ │ │
│ └── コード抽出 ──→ コードEmbedding ──→ ベクトルDB │
│ (CodeBERT/専用モデル) │
│ │
│ クエリ時:統一ベクトル検索 → マルチモーダル結果融合 → LLM生成 │
│ │
└──────────────────────────────────────────────────────────────────┘
マルチモーダルドキュメント処理
@Service
public class MultimodalDocumentProcessor {
private final EmbeddingService textEmbeddingService;
private final OpenAiChatModel visionModel;
public List<DocumentChunk> processDocument(Document document) {
List<DocumentChunk> chunks = new ArrayList<>();
chunks.addAll(processText(document.getTextContent(), document.getSource()));
chunks.addAll(processTables(document.getTables(), document.getSource()));
chunks.addAll(processImages(document.getImages(), document.getSource()));
chunks.addAll(processCodeBlocks(document.getCodeBlocks(), document.getSource()));
return chunks;
}
private List<DocumentChunk> processText(String text, String source) {
SemanticChunker chunker = new SemanticChunker(textEmbeddingService);
return chunker.chunk(text, source);
}
private List<DocumentChunk> processTables(List<Table> tables, String source) {
return tables.stream()
.map(table -> {
String description = convertTableToText(table);
return new DocumentChunk(
description, source,
"table-" + table.getIndex(),
table.getIndex(),
"TABLE"
);
})
.collect(Collectors.toList());
}
private String convertTableToText(Table table) {
StringBuilder sb = new StringBuilder();
sb.append("テーブル説明:").append(table.getCaption()).append("\n");
List<String> headers = table.getHeaders();
sb.append("列名:").append(String.join(", ", headers)).append("\n");
for (List<String> row : table.getRows()) {
for (int i = 0; i < headers.size() && i < row.size(); i++) {
sb.append(headers.get(i)).append(": ").append(row.get(i)).append("; ");
}
sb.append("\n");
}
return sb.toString();
}
private List<DocumentChunk> processImages(List<DocumentImage> images, String source) {
return images.stream()
.map(image -> {
String description = describeImage(image);
return new DocumentChunk(
"[画像] " + description, source,
"image-" + image.getIndex(),
image.getIndex(),
"IMAGE"
);
})
.collect(Collectors.toList());
}
private String describeImage(DocumentImage image) {
String prompt = "この画像の内容を詳細に説明してください。グラフデータや重要な情報を含めてください。";
ChatResponse response = visionModel.call(new ChatRequest(
List.of(new Message("user", prompt + "\n[画像base64: " + image.getBase64() + "]")),
ChatOptions.builder().withTemperature(0.1).build()
));
return response.getResult().getOutput().getContent();
}
private List<DocumentChunk> processCodeBlocks(List<CodeBlock> codeBlocks, String source) {
return codeBlocks.stream()
.map(code -> new DocumentChunk(
"[コード] " + code.getLanguage() + "\n" + code.getContent() +
"\n機能説明:" + code.getDescription(),
source,
"code-" + code.getIndex(),
code.getIndex(),
"CODE"
))
.collect(Collectors.toList());
}
}
RAG評価フレームワーク:定量評価
評価なしには最適化はない。RAG評価は検索品質と生成品質の両方の次元で定量化する必要がある:
評価指標フレームワーク
| 次元 | 指標 | 説明 | 計算方法 | 目標値 |
|---|---|---|---|---|
| 検索品質 | Recall@K | 上位K件中の関連文書の割合 | 関連∩検索結果 / 関連文書総数 | > 90% |
| 検索品質 | MRR | 最初の関連文書の順位の逆数の平均 | avg(1/first_relevant_rank) | > 0.8 |
| 検索品質 | nDCG@K | 正規化割引累積利得 | DCG/IDCG | > 0.85 |
| 生成品質 | Faithfulness | 回答の検索内容に対する忠実度 | 裏付けられた主張数 / 総主張数 | > 95% |
| 生成品質 | Relevancy | 回答とクエリの関連性 | LLM評価0-1スコア | > 0.9 |
| 生成品質 | Correctness | 回答と正解の一貫性 | 正解との意味的類似度 | > 0.85 |
| エンドツーエンド | レイテンシ | クエリから回答までの総時間 | P95レイテンシ | < 3s |
| エンドツーエンド | 拒答精度 | 回答不可能な質問を正しく拒否 | 正しい拒答数 / 拒答すべき総数 | > 80% |
評価フレームワーク実装
@Service
public class RagEvaluationService {
private final OpenAiChatModel chatModel;
public EvaluationResult evaluate(RagResponse response, String groundTruth) {
double faithfulness = evaluateFaithfulness(response);
double relevancy = evaluateRelevancy(response);
double correctness = evaluateCorrectness(response, groundTruth);
return new EvaluationResult(faithfulness, relevancy, correctness);
}
private double evaluateFaithfulness(RagResponse response) {
String prompt = """
以下の回答の検索内容に対する忠実度を評価してください。
検索内容:
%s
回答:
%s
回答内の各主張を抽出し、検索内容で裏付けられるかを判断してください。
JSONで出力:{"total_claims": N, "supported_claims": M}
""".formatted(
response.getSources().stream()
.map(SearchResult::getContent)
.collect(Collectors.joining("\n")),
response.getAnswer()
);
ChatResponse llmResponse = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.0).build()
));
try {
Map<String, Integer> result = new ObjectMapper().readValue(
llmResponse.getResult().getOutput().getContent(), Map.class
);
return (double) result.get("supported_claims") / result.get("total_claims");
} catch (Exception e) {
return 0.0;
}
}
private double evaluateRelevancy(RagResponse response) {
String prompt = """
以下の回答とクエリの関連性を評価してください(0-10点)。
クエリ:%s
回答:%s
0から10の整数のみ出力してください。
""".formatted(response.getQuery(), response.getAnswer());
ChatResponse llmResponse = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.0).build()
));
try {
return Double.parseDouble(llmResponse.getResult().getOutput().getContent().trim()) / 10.0;
} catch (Exception e) {
return 0.0;
}
}
private double evaluateCorrectness(RagResponse response, String groundTruth) {
String prompt = """
以下の回答と正解の意味的一貫性を評価してください(0-10点)。
回答:%s
正解:%s
0から10の整数のみ出力してください。
""".formatted(response.getAnswer(), groundTruth);
ChatResponse llmResponse = chatModel.call(new ChatRequest(
List.of(new Message("user", prompt)),
ChatOptions.builder().withTemperature(0.0).build()
));
try {
return Double.parseDouble(llmResponse.getResult().getOutput().getContent().trim()) / 10.0;
} catch (Exception e) {
return 0.0;
}
}
}
プロダクショングレードRAGアーキテクチャとパフォーマンス最適化
プロダクションアーキテクチャ概要
┌──────────────────────────────────────────────────────────────────────┐
│ プロダクショングレードRAGアーキテクチャ │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ API Gateway (Spring Cloud Gateway) │ │
│ │ 認証(JWT) │ レート制限(Sentinel) │ キャッシュ(Redis) │ │
│ └────────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌────────────────────────────▼───────────────────────────────┐ │
│ │ RAG Service (Spring Boot) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │ クエリ │ │ ハイブリッド│ │ リランク │ │ │
│ │ │ 書き換え │→ │ 検索 │→ │ サービス │ │ │
│ │ │ サービス │ │ サービス │ │ │ │ │
│ │ └──────────┘ └──────────┘ └──────────┘ │ │
│ │ │ │ │
│ │ ┌──────────┐ ┌──────────┐ ▼ │ │
│ │ │ 評価 │ │ キャッシュ│ ┌──────────┐ │ │
│ │ │ サービス │ │ サービス │ │ 生成 │ │ │
│ │ └──────────┘ └──────────┘ │ サービス │ │ │
│ │ └──────────┘ │ │
│ └────────────────────────────┬───────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼───────────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Qdrant │ │Elastic- │ │ Redis │ │
│ │ ベクトルDB│ │search │ │ キャッシュ│ │
│ │ │ │ BM25索引 │ │ レイヤー │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ ドキュメントインジェストパイプライン(非同期) │ │
│ │ アップロード → 解析 → チャンク → Embed → インデックス → │ │
│ │ メタデータストア(Kafka駆動、差分更新対応) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 監視とオブザーバビリティ │ │
│ │ Prometheus │ Grafanaダッシュボード │ アラート │ 品質追跡 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘
キャッシュレイヤー最適化
@Service
public class RagCacheService {
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
private static final long CACHE_TTL_HOURS = 24;
public Optional<RagResponse> getCachedResponse(String query) {
String cacheKey = generateCacheKey(query);
String cached = redisTemplate.opsForValue().get(cacheKey);
if (cached != null) {
try {
return Optional.of(objectMapper.readValue(cached, RagResponse.class));
} catch (Exception e) {
return Optional.empty();
}
}
return Optional.empty();
}
public void cacheResponse(String query, RagResponse response) {
String cacheKey = generateCacheKey(query);
try {
String json = objectMapper.writeValueAsString(response);
redisTemplate.opsForValue().set(cacheKey, json, CACHE_TTL_HOURS, TimeUnit.HOURS);
} catch (Exception e) {
// キャッシュ失敗はメインフローに影響しない
}
}
private String generateCacheKey(String query) {
return "rag:cache:" + DigestUtils.md5Hex(query);
}
}
ドキュメントインジェストパイプライン
@Service
public class DocumentIngestionPipeline {
private final DocumentParserService parserService;
private final SemanticChunker semanticChunker;
private final MarkdownStructureChunker structureChunker;
private final EmbeddingService embeddingService;
private final QdrantVectorStore vectorStore;
private final DocumentMapper documentMapper;
@Async("ingestionExecutor")
public CompletableFuture<Void> ingestDocument(MultipartFile file, String source) {
String content = parserService.parse(file);
List<DocumentChunk> chunks;
if (isMarkdown(content)) {
chunks = structureChunker.chunk(content, source);
} else {
chunks = semanticChunker.chunk(content, source);
}
List<float[]> embeddings = embeddingService.embedBatch(
chunks.stream().map(DocumentChunk::getContent).collect(Collectors.toList())
);
for (int i = 0; i < chunks.size(); i++) {
chunks.get(i).setEmbedding(embeddings.get(i));
}
vectorStore.upsertDocuments(chunks);
for (DocumentChunk chunk : chunks) {
DocumentEntity entity = new DocumentEntity();
entity.setContent(chunk.getContent());
entity.setSource(chunk.getSource());
entity.setSection(chunk.getSection());
entity.setEmbedding(chunk.getEmbedding());
entity.setSearchText(chunk.getContent());
documentMapper.insert(entity);
}
return CompletableFuture.completedFuture(null);
}
private boolean isMarkdown(String content) {
return content.contains("# ") || content.contains("## ") || content.contains("```");
}
}
パフォーマンス最適化チェックリスト
| 最適化項目 | 方法 | 効果 |
|---|---|---|
| Embeddingキャッシュ | 同一テキストのEmbedding結果を再利用 | API呼び出し50%以上削減 |
| クエリキャッシュ | Redisで類似クエリ結果をキャッシュ | P95レイテンシ60%削減 |
| バッチEmbedding | 複数テキストを1回の呼び出しに統合 | スループット3-5倍向上 |
| 非同期インジェスト | Kafka駆動の非同期ドキュメント処理 | インジェストがクエリをブロックしない |
| コネクションプール | Qdrant/ES接続プールのチューニング | 同時実行能力2倍向上 |
| HyDE事前計算 | ホットクエリのHyDE Embeddingを事前生成 | ホットクエリのレイテンシ40%削減 |
| インデックス最適化 | HNSWパラメータチューニング(M=16, ef=100) | 精度と速度のバランス |
| シャーディング戦略 | ドキュメントタイプ/時間でシャード | 検索範囲を絞り30%高速化 |
Spring Boot設定
spring:
ai:
openai:
api-key: ${OPENAI_API_KEY}
chat:
options:
model: gpt-4o
temperature: 0.1
embedding:
options:
model: text-embedding-3-small
rag:
vector-store:
type: qdrant
qdrant:
host: localhost
port: 6334
collection: knowledge_base
vector-size: 1536
chunking:
default-strategy: semantic
chunk-size: 512
overlap-size: 64
similarity-threshold: 0.85
retrieval:
hybrid: true
vector-weight: 0.7
keyword-weight: 0.3
rrf-k: 60
top-k: 10
cache:
enabled: true
ttl-hours: 24
ingestion:
async: true
batch-size: 100
pool-size: 4
まとめ
| コンポーネント | 2026年のベストプラクティス | 重要ポイント |
|---|---|---|
| クエリ処理 | クエリ書き換え + HyDE | 多角的検索でリコール向上 |
| 検索戦略 | ハイブリッド(ベクトル+BM25)+ RRF融合 | ベクトル70% + キーワード30% |
| ランキング最適化 | Cross-Encoderリランク | 精細なquery-doc関連性スコアリング |
| ドキュメントチャンキング | 意味 > 構造 > 固定 | チャンキングはRAGの基盤 |
| エージェント化 | Agentic RAGマルチターン+自己評価 | Agentが検索タイミングを決定 |
| マルチモーダル | 画像/表/コードの統一検索 | ビジュアルEmbedding + テキスト説明 |
| 評価 | Faithfulness/Relevancy/Correctness | 評価なくして最適化なし |
| プロダクション化 | キャッシュ+非同期+監視+チューニング | エンジニアリングがRAGの成否を決める |
RAGは「検索+生成」ほど単純ではなく、すべての段階を慎重に設計する必要があるシステムエンジニアリングの課題だ。クエリ書き換えからハイブリッド検索、ドキュメントチャンキングからAgentic推論まで——各コンポーネントが最終的な回答品質を決定する。2026年、Advanced RAGは標準、Agentic RAGは最先端、そして評価フレームワークは継続的改善の基盤である。
ブラウザローカルツールを無料で試す →