RAG検索拡張生成ディープ実戦:Naive RAGからAgentic RAGへの3世代進化とエンタープライズ実装

技术架构本番環境の AI Agent

なぜ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は最先端、そして評価フレームワークは継続的改善の基盤である。

#RAG#检索增强生成#向量数据库#Embedding#Agentic RAG#Spring AI