RAG检索增强生成深度实战:从Naive RAG到Agentic RAG的三代进化与企业级落地

技术架构

RAG为什么是企业AI必修课

大模型再强,也有三大硬伤。2026年了,如果你还在裸调LLM API做企业问答,那一定踩过这些坑:

硬伤 表现 业务影响
知识截止 模型训练数据停留在某个时间点 无法回答最新政策、产品信息
幻觉(Hallucination) 编造看似合理但不存在的事实 误导决策,法律风险
私有知识缺失 不懂企业内部文档、流程、术语 通用模型无法替代专业问答

RAG(Retrieval-Augmented Generation)的本质:让大模型在回答前先"查资料",用检索结果约束生成,从根源上解决三大硬伤。

数据:2026年企业AI落地项目中,92%采用了RAG架构,纯Prompt方案占比不足5%。

┌─────────────────────────────────────────────────────────┐
│              没有RAG vs 有RAG 的本质区别                   │
├──────────────────────┬──────────────────────────────────┤
│     纯LLM调用         │         RAG增强调用              │
├──────────────────────┼──────────────────────────────────┤
│  用户提问 → LLM → 答  │  用户提问 → 检索 → 上下文注入 →  │
│  (凭记忆回答)        │  LLM → 答案+引用来源              │
│                      │  (查资料后回答)                   │
├──────────────────────┼──────────────────────────────────┤
│  知识:训练数据截止    │  知识:实时检索,可随时更新         │
│  准确性:不可控        │  准确性:检索结果约束生成           │
│  可追溯:无           │  可追溯:每个答案附带引用           │
└──────────────────────┴──────────────────────────────────┘

Naive RAG → Advanced RAG → Agentic RAG三代进化

RAG不是一成不变的,从2023年到2026年,RAG架构经历了三代进化:

┌───────────────────────────────────────────────────────────────┐
│                     RAG 三代进化路线图                          │
├───────────────────┬───────────────────┬───────────────────────┤
│   Naive RAG       │  Advanced RAG     │   Agentic RAG         │
│   (2023)          │  (2024-2025)      │   (2026)              │
├───────────────────┼───────────────────┼───────────────────────┤
│ Query → 检索 → 生成│ 查询改写 → 混合检索 │ Agent自主决策         │
│                   │ → 重排序 → 生成    │ → 多轮检索+自我评估    │
│                   │                   │ → 动态工具调用          │
├───────────────────┼───────────────────┼───────────────────────┤
│ 问题:检索质量差    │ 问题:缺乏自主性    │ 问题:复杂度高          │
│ 幻觉率30%+        │ 幻觉率10-15%      │ 幻觉率<5%             │
│ 不可控             │ 可控但被动         │ 主动推理+自主纠错       │
└───────────────────┴───────────────────┴───────────────────────┘

三代RAG对比

维度 Naive RAG Advanced RAG Agentic RAG
查询处理 原始查询直接检索 查询改写/扩展/HyDE Agent自主分解子问题
检索策略 单路向量检索 混合检索+重排序 多轮检索+工具调用
生成策略 直接拼接上下文 精选上下文+引用 自我评估+迭代优化
典型幻觉率 30-40% 10-15% <5%
端到端延迟 1-2s 2-4s 5-15s
适用场景 Demo/原型 生产环境 复杂推理场景
实现复杂度

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

选型建议

场景 推荐模型 理由
中文为主的企业知识库 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年向量数据库已经不是"有没有"的问题,而是"选哪个"的问题。三大主流方案深度对比:

维度 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 完整Pipeline                         │
├──────────────────────────────────────────────────────────────────┤
│                                                                  │
│  用户查询:"公司2025年Q4营收增长原因是什么?"                      │
│       │                                                          │
│       ▼                                                          │
│  ┌─────────────────────────────────┐                            │
│  │  1. 查询改写(Query Rewrite)    │                            │
│  │  原始 → 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 Pipeline

@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 = """
            你是知识库问答助手。基于以下检索到的文档回答用户问题。

            规则:
            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的地基,切得不好,检索再强也白搭。四种主流策略深度对比:

策略 原理 优点 缺点 适用场景 切分粒度
固定长度 按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 ──→ 向量库                   │
│       │                                                          │
│       ├── 表格提取 ──→ 表格→文本描述 ──→ Embedding ──→ 向量库     │
│       │                                                          │
│       ├── 图片提取 ──→ 视觉Embedding ──→ 向量库                   │
│       │              (CLIP/Qwen-VL)                              │
│       │                                                          │
│       └── 代码提取 ──→ 代码Embedding ──→ 向量库                   │
│                       (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 答案与ground truth的一致性 与标准答案的语义相似度 > 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) │ 日志(ELK)    │     │
│  └────────────────────────────┬───────────────────────────────┘     │
│                               │                                      │
│  ┌────────────────────────────▼───────────────────────────────┐     │
│  │                    RAG Service (Spring Boot)                │     │
│  │    ┌──────────┐  ┌──────────┐  ┌──────────┐               │     │
│  │    │查询改写   │  │混合检索   │  │重排序     │               │     │
│  │    │Service   │→ │Service   │→ │Service   │               │     │
│  │    └──────────┘  └──────────┘  └──────────┘               │     │
│  │                                        │                    │     │
│  │    ┌──────────┐  ┌──────────┐          ▼                    │     │
│  │    │评估Service│  │缓存Service│  ┌──────────┐               │     │
│  │    └──────────┘  └──────────┘  │生成Service│               │     │
│  │                                └──────────┘               │     │
│  └────────────────────────────┬───────────────────────────────┘     │
│                               │                                      │
│       ┌───────────────────────┼───────────────────────┐             │
│       ▼                       ▼                       ▼             │
│  ┌──────────┐          ┌──────────┐          ┌──────────┐          │
│  │ Qdrant   │          │Elastic-  │          │ Redis    │          │
│  │ 向量库    │          │search    │          │ 缓存层    │          │
│  │          │          │ BM25索引  │          │          │          │
│  └──────────┘          └──────────┘          └──────────┘          │
│                                                                      │
│  ┌────────────────────────────────────────────────────────────┐     │
│  │                文档摄入Pipeline (异步)                       │     │
│  │    文件上传 → 解析 → 切分 → Embedding → 索引 → 元数据存储    │     │
│  │    (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);
    }
}

文档摄入Pipeline

@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结果 减少50%+ API调用
查询缓存 Redis缓存相似查询结果 P95延迟降低60%
批量Embedding 合并多个文本一次调用 吞吐量提升3-5x
异步摄入 Kafka驱动的异步文档处理 摄入不影响查询
连接池优化 Qdrant/ES连接池调优 并发能力提升2x
预计算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的地基
Agent化 Agentic RAG多轮检索+自我评估 Agent自主决定何时检索
多模态 图片/表格/代码统一检索 视觉Embedding + 文本描述
评估 Faithfulness/Relevancy/Correctness 没有评估就没有优化
生产化 缓存+异步+监控+调优 工程化决定RAG成败

RAG不是"检索+生成"这么简单,而是一个需要精心设计每个环节的系统工程。从查询改写到混合检索,从文档切分到Agentic推理——每个环节都决定了最终答案的质量。2026年,Advanced RAG已是标配,Agentic RAG是前沿,而评估体系是持续优化的基石。

本站提供浏览器本地工具,免注册即可试用 →

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