RGA-检索增强生成(Retrieval-augmented Generation)

检索增强生成(Retrieval-augmented Generation)

对于基础大模型来说,他只具备通用信息,他的参数都是拿公网进行训练,并且有一定的时间延迟,无法得知一些具体业务数据和实时数据,这些数据往往在各种文件中(比如txt、 word、html、数据库。)

虽然function-call、SystemMessage可以用来解决一部分问题但是它只能少量,并且针对的场景不一样

如果你要提供大量的业务领域信息,就需要给他外接一个知识库:

比如

  1. 我问他退订要多少费用

  2. 这些资料可能都由产品或者需求编写在了文档中:

    a. 所以需要现在需求信息存到向量数据库(这个过程叫Embedding,涉及到文档读取、分词,就是在我们每一次跟大模型对话的时候呢

  3. 去向量数据库中查询"退订费用相关信息"

  4. 将查询到的数据和对话信息再请求大模型

  5. 此时会响应退订需要多少费用

概念向量:

向量通常用来做相似性搜索,比如语义的一维向量,可以表示词语或短语的语义相似性。例如, "你好"、"hello" 和"见到你很高兴"可以通过一维向量来表示它们的语义接近程度。

然而,对于更复杂的对象,比如小狗,无法仅通过一个维度来进行相似性搜索。这时,我们需要提取多个特征,如颜色、大小、品种等,将每个特征表示为向量的一个维度,从而形成一个多维向量。例如,一只棕色的小型泰迪犬可以表示为一个多维向量[棕色,小型,泰迪犬]。

如果需要检索见过更加精准,我们肯定还需要更多维度的向量,组成更多维度的空间,在多维向量空间中,相似性检索变得更加复杂。我们需要使用一些算法,如余弦相似度或欧几里得距离,来计算向量之间的相似性。向量数据库会帮我实现。

文本向量化

通过向量模型即可向量化,这里我们学到了一种新的模型,叫"向量模型"专门用来做文本向量化的。

大语言模型不能做向量化,所以需要单独找一个向量模型

  • 向量模型的本质目标,就是把语义相似的内容用"相近"的向量表示,把"不相关"内容尽量拉远。

  • 所以好的向量模型能够更好的识别语义,进行向量化。

向量数据库

对于向量模型生成出来的向量,我们可以持久化到向量数据库,并且能利用向量数据库来计算两个向量之间的相似度,或者根据一个向量查找跟这个向量最相似的向量。

在SpringAi中,VectorStore 表示向量数据库,目前支持的向量数据库有 • Azure Vector Search - The Azure vector store。

  • Apache Cassandra - The Apache Cassandra vector store。

  • Chroma Vector Store - The Chroma vector store。

  • Elasticsearch Vector Store - The Elasticsearch vector store。 可以"以向量+关键词"方式做混合检索。深度优化更多针对文本,不是专门"向量搜索引擎"。向量存储和检索容量有限制,查询延迟高于 Milvus.

  • GemFire Vector Store - The GemFire vector store.

  • MariaDB Vector Store - The MariaDB vector store.

  • Milvus Vector Store - The Milvus vector store.

  • Neo4j Vector Store - The Neo4j vector store。可以结合结构化图谱查询与向量检索,大规模嵌入检萦(如干万一亿级高维向量)性能明显落后于 Milvus • OpenSearch Vector Store - The OpenSearch vector store。

  • Oracle Vector Store - The Oracle Database vector store。

  • PgVector Store - The PostgreSQL/PGVector vector store。

  • Pinecone Vector Store - PineCone vector store。

  • Odrant Vector Store - Odrant vector store!

  • Redis Vector Store -The Redis vector store。 低门槛实现小规模向量检索。对于高维大规模向量(如几百万到上亿条),性能和存储效率不如专用向量库。

    其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、

  • SimpleVectorstore - A simple implementation of persistent vector storage, good tor educational purposes.

其中有我们熟悉的几个数据库都可以用来存储向量,比如Elasticsearch、MongoDb、Neo4j、 Pgsql, Redis。

我会讲解2种:

  1. SimpleVectorStore 教学版向量数据库
  2. Milvus Vector Store Milvus(国产团队)、文档友好、社区国内活跃、性能最佳、市场占用率大。实战中使用的向量数据库

匹配检索

在这个示例中,我分别存储了预订航班和取消预订 2段说明到向量数据库中然后通过"退票要多少钱"进行查询

SearchRequest

可以利用searchRequest设置检索请求:

  • query 代表要检索的内容

  • topK 设置检素结果的前N条

    • 通常我们查询所有结果查出来,因为查询结果最终要发给人模型,查询过多的结果会:

      • 1.过多的token意味着更长延迟,更多的费用,并且过多上下文会超限;

      • 2.研究表明过多的内容会降低LLM 的召回性能

  • similarityThreshold 设置相似度阈值,可以通关设置分数限制召回内容相似度。从而过滤掉废料。(中文语料要适当降低分数),所以应遵循始终以"业务召回效果"为主,而不是追求网上常说的高分阈值。

Spring AI RAG 使用方式与核心原理详解

项目路径 : 09rag
Spring AI 版本 : 1.0.0
最后更新: 2025年1月


📋 目录

  1. 概述
  2. [RAG 使用方式](#RAG 使用方式)
  3. 核心原理
  4. [RAG vs Function-Call vs SystemMessage](#RAG vs Function-Call vs SystemMessage)
  5. 项目代码分析
  6. 最佳实践
  7. 常见问题

概述

什么是 RAG?

RAG(Retrieval-Augmented Generation,检索增强生成) 是一种将外部知识库与大型语言模型(LLM)结合的技术。它通过以下步骤工作:

  1. 检索(Retrieval):从知识库中检索与用户查询相关的文档片段
  2. 增强(Augmentation):将检索到的文档作为上下文注入到 Prompt 中
  3. 生成(Generation):基于增强后的上下文生成回答

RAG 的核心价值

  • 知识更新:可以随时更新知识库,无需重新训练模型
  • 准确性提升:基于真实文档回答,减少幻觉(Hallucination)
  • 可追溯性:可以追溯到答案的来源文档
  • 领域适应:可以快速适配特定领域(如企业知识库、产品文档)

RAG 应用场景

  1. 企业知识库问答:基于内部文档回答员工问题
  2. 产品文档助手:回答产品使用、配置相关问题
  3. 法律文档分析:基于法律条文回答法律问题
  4. 医疗知识问答:基于医学文献回答医疗问题
  5. 客服系统:基于服务条款、FAQ 回答客户问题

RAG 使用方式

1. 基础使用流程

RAG 的使用通常包括以下步骤:

复制代码
文档读取 → 文档分割 → 向量化 → 向量存储 → 相似度检索 → 上下文增强 → 生成回答

2. 文档处理(ELT:Extract, Load, Transform)

2.1 文档读取(Extract)

Spring AI 提供了多种文档读取器:

文本文件读取

java 复制代码
@Autowired
@Value("classpath:rag/terms-of-service.txt")
Resource resource;

TextReader textReader = new TextReader(resource);
textReader.getCustomMetadata().put("filename", resource.getFilename());
List<Document> documents = textReader.read();

Markdown 文件读取

java 复制代码
@Value("classpath:rag/document.md")
Resource resource;

MarkdownDocumentReaderConfig config = MarkdownDocumentReaderConfig.builder()
    .withHorizontalRuleCreateDocument(false)  // 分割线是否创建新文档
    .withIncludeCodeBlock(false)              // 代码块是否创建新文档
    .withIncludeBlockquote(false)             // 引用是否创建新文档
    .withAdditionalMetadata("filename", resource.getFilename())
    .build();

MarkdownDocumentReader reader = new MarkdownDocumentReader(resource, config);
List<Document> documents = reader.read();

PDF 文件读取

java 复制代码
// 按页读取
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader(
    resource,
    PdfDocumentReaderConfig.builder().build()
);
List<Document> documents = pdfReader.read();

// 按段落读取(需要 PDF 有目录结构)
ParagraphPdfDocumentReader pdfReader = new ParagraphPdfDocumentReader(
    resource,
    PdfDocumentReaderConfig.builder()
        .withReversedParagraphPosition(true)  // 坐标系反转
        .withPageTopMargin(0)                 // 上边距
        .withPageExtractedTextFormatter(
            ExtractedTextFormatter.builder()
                .withNumberOfTopTextLinesToDelete(0)  // 删除前 N 行
                .build()
        )
        .build()
);
List<Document> documents = pdfReader.read();
2.2 文档分割(Transform)

文档分割是将长文档切分成较小的片段,以便:

  • 提高检索精度:小片段更容易匹配到相关查询
  • 控制上下文长度:避免超出模型的上下文窗口限制
  • 提高向量化质量:较小的文本块向量化效果更好

Token 分割器

java 复制代码
// 标准 Token 分割器
TokenTextSplitter splitter = new TokenTextSplitter();
List<Document> splitDocuments = splitter.apply(documents);

// 中文 Token 分割器(支持中文分词)
ChineseTokenTextSplitter chineseSplitter = new ChineseTokenTextSplitter(
    80,    // chunkSize: 每个分块的目标 token 数
    10,    // chunkOverlap: 分块之间的重叠 token 数
    5,     // minChunkSize: 最小分块 token 数
    10000, // maxChunkSize: 最大分块 token 数
    true   // keepSeparator: 是否保留分隔符
);
List<Document> splitDocuments = chineseSplitter.apply(documents);

分割策略建议

  • 不要过度追求按主题分割:企业级知识库文档多样,按 Token 数分割更实用
  • 合理设置 chunkSize:通常 100-500 tokens,根据模型上下文窗口调整
  • 设置适当的重叠:10-20% 的重叠可以保持上下文连贯性
2.3 元数据增强(Enrichment)

元数据增强可以为文档添加额外的信息,提高检索精度:

关键词提取

java 复制代码
KeywordMetadataEnricher enricher = new KeywordMetadataEnricher(chatModel, 5);
List<Document> enrichedDocuments = enricher.apply(documents);

// 可以自定义关键词提取模板
KeywordMetadataEnricher.KEYWORDS_TEMPLATE = """
    给我按照我提供的内容{context_str},生成%s个关键字;
    允许的关键字有这些:
    ['退票','预定']
    只允许在这个关键字范围进行选择。
    """;

摘要生成

java 复制代码
SummaryMetadataEnricher enricher = new SummaryMetadataEnricher(
    chatModel,
    List.of(
        SummaryMetadataEnricher.SummaryType.PREVIOUS,  // 前一个分块的摘要
        SummaryMetadataEnricher.SummaryType.CURRENT,  // 当前分块的摘要
        SummaryMetadataEnricher.SummaryType.NEXT      // 下一个分块的摘要
    )
);
List<Document> enrichedDocuments = enricher.apply(documents);
2.4 向量化(Embedding)

向量化是将文本转换为数值向量,用于相似度计算:

java 复制代码
@Autowired
DashScopeEmbeddingModel embeddingModel;

// 向量化单个文本
float[] embedding = embeddingModel.embed("我叫徐庶");
System.out.println("向量维度: " + embedding.length);
System.out.println("向量值: " + Arrays.toString(embedding));

向量化模型配置

properties 复制代码
# application.properties
spring.ai.dashscope.api-key=${ALI_AI_KEY}
spring.ai.dashscope.embedding.options.model=text-embedding-v4
2.5 向量存储(Load)

向量存储用于保存文档向量,支持相似度检索:

java 复制代码
@Bean
public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
    return SimpleVectorStore.builder(embeddingModel).build();
}

// 存储文档(内部会自动向量化)
@Autowired
VectorStore vectorStore;

vectorStore.add(documents);

3. RAG 检索与生成

3.1 简单 RAG(QuestionAnswerAdvisor)

最简单的 RAG 使用方式:

java 复制代码
@Autowired
DashScopeChatModel chatModel;
@Autowired
VectorStore vectorStore;

ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultAdvisors(SimpleLoggerAdvisor.builder().build())
    .build();

String answer = chatClient.prompt()
    .user("退票需要多少费用?")
    .advisors(
        SimpleLoggerAdvisor.builder().build(),
        QuestionAnswerAdvisor.builder(vectorStore)
            .searchRequest(
                SearchRequest.builder()
                    .topK(5)                      // 返回前 5 个最相似的文档
                    .similarityThreshold(0.6)      // 相似度阈值,低于此值的文档会被过滤
                    .build()
            )
            .build()
    )
    .call()
    .content();

System.out.println(answer);

工作流程

  1. 用户查询:"退票需要多少费用?"
  2. 向量化查询:将查询转换为向量
  3. 相似度检索:在 VectorStore 中检索最相似的文档片段(topK=5)
  4. 过滤低相似度文档:相似度 < 0.6 的文档被过滤
  5. 上下文增强:将检索到的文档注入到 Prompt
  6. 生成回答:基于增强后的上下文生成回答
3.2 高级 RAG(RetrievalAugmentationAdvisor)

高级 RAG 提供了更多定制选项:

java 复制代码
Advisor retrievalAugmentationAdvisor = RetrievalAugmentationAdvisor.builder()
    // 文档检索器
    .documentRetriever(
        VectorStoreDocumentRetriever.builder()
            .similarityThreshold(0.0)
            .topK(5)
            // .filterExpression()  // 基于元数据过滤
            .vectorStore(vectorStore)
            .build()
    )
    // 查询增强器:处理检索为空的情况
    .queryAugmenter(
        ContextualQueryAugmenter.builder()
            .allowEmptyContext(false)  // 检索为空时,false 返回提示,true 正常回答
            .emptyContextPromptTemplate(
                PromptTemplate.builder()
                    .template("用户查询位于知识库之外。礼貌地告知用户您无法回答")
                    .build()
            )
            .build()
    )
    // 查询转换器:重写查询
    .queryTransformers(
        RewriteQueryTransformer.builder()
            .chatClientBuilder(ChatClient.builder(chatModel))
            .targetSearchSystem("航空票务助手")
            .build()
    )
    // 查询转换器:翻译查询
    .queryTransformers(
        TranslationQueryTransformer.builder()
            .chatClientBuilder(ChatClient.builder(chatModel))
            .targetLanguage("english")
            .build()
    )
    // 文档后处理器:检索后对文档进行处理
    .documentPostProcessors((query, documents) -> {
        System.out.println("Original query: " + query.text());
        System.out.println("Retrieved documents: " + documents.size());
        // 可以在这里对文档进行过滤、排序等操作
        return documents;
    })
    .build();

String answer = chatClient.prompt()
    .advisors(retrievalAugmentationAdvisor)
    .user("我今天心情不好,不想去玩了,你能不能告诉我退票需要多少钱?")
    .call()
    .content();

高级特性说明

  1. 查询重写(RewriteQueryTransformer)

    • 将用户自然语言查询重写为更适合检索的查询
    • 例如:"我今天心情不好,不想去玩了" → "退票费用"
  2. 查询翻译(TranslationQueryTransformer)

    • 将查询翻译为其他语言,适用于多语言知识库
  3. 文档后处理(documentPostProcessors)

    • 在检索后对文档进行进一步处理
    • 可以过滤、排序、去重等
  4. 空上下文处理(allowEmptyContext)

    • false:检索为空时返回提示信息
    • true:检索为空时正常回答(可能产生幻觉)
3.3 重排序(Rerank)

重排序可以提高检索精度,将检索到的文档按相关性重新排序:

java 复制代码
@Autowired
DashScopeRerankModel rerankModel;

RetrievalRerankAdvisor retrievalRerankAdvisor = new RetrievalRerankAdvisor(
    vectorStore,
    rerankModel,
    SearchRequest.builder()
        .topK(200)  // 先检索 200 个文档
        .build()
);

String answer = chatClient.prompt()
    .user("退费费用?")
    .advisors(retrievalRerankAdvisor)
    .call()
    .content();

重排序工作流程

  1. 向量检索:从 VectorStore 检索 topK=200 个文档
  2. 重排序:使用 Rerank 模型对 200 个文档重新排序
  3. 选择 Top N:选择重排序后的前 N 个文档
  4. 上下文增强:将选中的文档注入到 Prompt
  5. 生成回答:基于增强后的上下文生成回答

4. 元数据过滤

基于元数据过滤可以提高检索精度:

java 复制代码
// 存储时添加元数据
Document doc = Document.builder()
    .text("退票政策...")
    .metadata(Map.of("category", "退票", "filename", "terms-of-service.txt"))
    .build();

// 检索时使用元数据过滤
FilterExpressionBuilder filterExpression = FilterExpressionBuilder.builder()
    .eq("category", "退票")  // 只检索 category="退票" 的文档
    .and()
    .eq("filename", "terms-of-service.txt")  // 并且 filename="terms-of-service.txt"
    .build();

SearchRequest searchRequest = SearchRequest.builder()
    .topK(5)
    .similarityThreshold(0.6)
    .filterExpression(filterExpression)  // 应用元数据过滤
    .build();

QuestionAnswerAdvisor advisor = QuestionAnswerAdvisor.builder(vectorStore)
    .searchRequest(searchRequest)
    .build();

元数据过滤的优势

  • 提高精度:只检索特定类别的文档
  • 减少噪音:过滤掉不相关的文档
  • 支持复杂查询:可以组合多个过滤条件
  • 提高性能:减少需要处理的文档数量

支持的过滤操作

  • eq: 等于
  • ne: 不等于
  • gt: 大于
  • gte: 大于等于
  • lt: 小于
  • lte: 小于等于
  • in: 在列表中
  • nin: 不在列表中
  • and: 逻辑与
  • or: 逻辑或
  • not: 逻辑非

5. RAG 评估(Evaluation)

RAG 评估用于评估 RAG 系统的质量,包括相关性、准确性等:

java 复制代码
@Autowired
DashScopeChatModel chatModel;
@Autowired
VectorStore vectorStore;

// 构建 RAG Advisor
RetrievalAugmentationAdvisor advisor = RetrievalAugmentationAdvisor.builder()
    .documentRetriever(VectorStoreDocumentRetriever.builder()
        .vectorStore(vectorStore)
        .build())
    .build();

// 执行查询
String query = "退票需要多少费用?";
ChatResponse chatResponse = ChatClient.builder(chatModel)
    .build()
    .prompt(query)
    .advisors(advisor)
    .call()
    .chatResponse();

// 构建评估请求
EvaluationRequest evaluationRequest = new EvaluationRequest(
    query,  // 原始用户查询
    chatResponse.getMetadata().get(RetrievalAugmentationAdvisor.DOCUMENT_CONTEXT),  // 检索到的上下文
    chatResponse.getResult().getOutput().getText()  // AI 生成的答案
);

// 执行相关性评估
RelevancyEvaluator evaluator = new RelevancyEvaluator(
    ChatClient.builder(chatModel)
);
EvaluationResponse evaluationResponse = evaluator.evaluate(evaluationRequest);

System.out.println("评估结果: " + evaluationResponse);

评估指标

  • 相关性(Relevancy):答案与查询的相关程度
  • 准确性(Accuracy):答案的准确性
  • 完整性(Completeness):答案的完整程度
  • 一致性(Consistency):答案与上下文的一致性

核心原理

1. 向量相似度检索原理

1.1 向量化(Embedding)

向量化是将文本转换为高维向量空间中的点:

复制代码
文本: "退票需要多少费用?"
  ↓ Embedding Model
向量: [0.123, -0.456, 0.789, ..., 0.234]  (维度: 1024)

向量化的特点

  • 语义相似性:语义相似的文本在向量空间中距离较近
  • 高维空间:通常使用 512、1024 或更高维度
  • 稠密向量:每个维度都有数值,不是稀疏向量
1.2 相似度计算

常用的相似度计算方法:

余弦相似度(Cosine Similarity)

复制代码
similarity = cos(θ) = (A · B) / (||A|| × ||B||)

点积相似度(Dot Product)

复制代码
similarity = A · B = Σ(Ai × Bi)

欧氏距离(Euclidean Distance)

复制代码
distance = √Σ(Ai - Bi)²
similarity = 1 / (1 + distance)
1.3 向量检索

向量检索是在高维向量空间中快速找到最相似的向量:

暴力搜索(Brute Force)

  • 计算查询向量与所有文档向量的相似度
  • 时间复杂度:O(n),n 为文档数量
  • 适用于小规模数据(< 10万)

近似最近邻(ANN)

  • 使用索引结构加速检索
  • 时间复杂度:O(log n)
  • 适用于大规模数据(> 10万)
  • 常用算法:HNSW、IVF、LSH 等

2. RAG 工作流程详解

2.1 完整流程图
复制代码
用户查询
   ↓
向量化查询
   ↓
向量相似度检索
   ↓
过滤低相似度文档
   ↓
元数据过滤(可选)
   ↓
重排序(可选)
   ↓
文档后处理(可选)
   ↓
构建增强 Prompt
   ↓
AI 生成回答
   ↓
返回答案
2.2 Prompt 构建

RAG 会将检索到的文档注入到 Prompt 中:

复制代码
System: 你是一个智能助手,基于以下文档回答问题。

Context:
文档1: 退票政策规定,经济舱取消费用为 75 美元...
文档2: 取消预订需要在航班起飞前 48 小时...
文档3: 退款将在 7 个工作日内处理...

User: 退票需要多少费用?

Assistant: 根据文档内容,退票费用取决于舱位等级...

Prompt 模板

java 复制代码
String template = """
    基于以下文档回答问题:
    
    {context}
    
    问题:{question}
    
    如果文档中没有相关信息,请说"根据提供的文档,我无法回答这个问题"。
    """;

3. 向量存储原理

3.1 SimpleVectorStore

SimpleVectorStore 是 Spring AI 提供的内存向量存储:

特点

  • 存储在内存中,速度快
  • 适合小规模数据(< 10万文档)
  • 重启后数据丢失
  • 支持持久化到文件

实现原理

java 复制代码
// 内部数据结构
Map<String, List<Float>> vectors;  // 文档ID -> 向量
Map<String, Document> documents;   // 文档ID -> 文档内容
3.2 持久化向量存储

生产环境建议使用持久化向量存储:

选项 1:PostgreSQL + pgvector

java 复制代码
@Bean
public VectorStore vectorStore(PgVectorStore pgVectorStore) {
    return pgVectorStore;
}

选项 2:Milvus

java 复制代码
@Bean
public VectorStore vectorStore(MilvusVectorStore milvusVectorStore) {
    return milvusVectorStore;
}

选项 3:Pinecone

java 复制代码
@Bean
public VectorStore vectorStore(PineconeVectorStore pineconeVectorStore) {
    return pineconeVectorStore;
}

4. 文档分割原理

4.1 Token 分割

Token 分割器将文本按 Token 数量分割:

复制代码
原始文档(1000 tokens)
  ↓ TokenTextSplitter(chunkSize=200, overlap=20)
分块1: tokens 0-200
分块2: tokens 180-380  (重叠 20 tokens)
分块3: tokens 360-560
...

重叠的作用

  • 保持上下文连贯性
  • 避免重要信息被分割
  • 提高检索召回率
4.2 中文分词

ChineseTokenTextSplitter 支持中文分词:

java 复制代码
ChineseTokenTextSplitter splitter = new ChineseTokenTextSplitter(
    80,    // chunkSize
    10,    // chunkOverlap
    5,     // minChunkSize
    10000, // maxChunkSize
    true   // keepSeparator
);

中文分词的优势

  • 更准确的中文 Token 计数
  • 更好的语义分割
  • 支持中文标点符号处理

5. Advisor 底层实现原理

5.1 Advisor 执行流程

RAG Advisor 在 ChatClient 的调用链中执行:

复制代码
ChatClient.prompt().user().call()
   ↓
Advisor.doBefore()  // 检索文档
   ↓
构建增强 Prompt(包含检索到的文档)
   ↓
ChatModel.call(Prompt)  // AI 生成回答
   ↓
Advisor.doAfter()  // 后处理
   ↓
返回结果
5.2 QuestionAnswerAdvisor 实现

QuestionAnswerAdvisor 的底层实现:

java 复制代码
// 伪代码展示核心逻辑
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
    // 1. 提取用户查询
    String query = extractUserQuery(request);
    
    // 2. 向量化查询
    float[] queryVector = embeddingModel.embed(query);
    
    // 3. 向量检索
    List<Document> documents = vectorStore.similaritySearch(
        queryVector,
        searchRequest
    );
    
    // 4. 构建增强 Prompt
    String context = buildContext(documents);
    Prompt enhancedPrompt = buildEnhancedPrompt(request, context);
    
    // 5. 返回增强后的请求
    return request.mutate().prompt(enhancedPrompt).build();
}
5.3 RetrievalAugmentationAdvisor 实现

RetrievalAugmentationAdvisor 提供了更灵活的扩展点:

java 复制代码
// 伪代码展示核心逻辑
public ChatClientRequest before(ChatClientRequest request, AdvisorChain chain) {
    // 1. 查询转换(Query Transformers)
    Query query = applyQueryTransformers(originalQuery);
    
    // 2. 文档检索
    List<Document> documents = documentRetriever.retrieve(query);
    
    // 3. 文档后处理
    documents = applyDocumentPostProcessors(query, documents);
    
    // 4. 查询增强(处理空上下文)
    if (documents.isEmpty() && !allowEmptyContext) {
        return buildEmptyContextResponse();
    }
    
    // 5. 构建增强 Prompt
    Prompt enhancedPrompt = buildEnhancedPrompt(request, documents);
    
    return request.mutate().prompt(enhancedPrompt).build();
}
5.4 上下文注入机制

检索到的文档会被注入到 Prompt 中:

java 复制代码
// 上下文构建模板
String contextTemplate = """
    基于以下文档回答问题:
    
    {context}
    
    问题:{question}
    """;

// 构建上下文字符串
StringBuilder context = new StringBuilder();
for (Document doc : documents) {
    context.append("文档").append(i).append(": ").append(doc.getText()).append("\n\n");
}

// 替换模板变量
String prompt = contextTemplate
    .replace("{context}", context.toString())
    .replace("{question}", query);

RAG vs Function-Call vs SystemMessage

对比表

特性 RAG Function-Call SystemMessage
知识来源 外部知识库 工具/API 预定义提示词
知识更新 动态更新 需要修改代码 需要修改提示词
知识规模 大规模(GB级) 小规模(工具) 小规模(提示词)
准确性 高(基于文档) 高(基于工具) 中(基于模型知识)
可追溯性 ✅ 可追溯 ✅ 可追溯 ❌ 不可追溯
适用场景 知识问答 工具调用 角色设定
实现复杂度
性能开销 中(检索+生成) 低(工具调用) 低(仅生成)

使用场景建议

使用 RAG 的场景

企业知识库问答 :需要基于大量文档回答

产品文档助手 :需要查询产品文档

法律文档分析 :需要基于法律条文回答

客服系统:需要基于服务条款回答

使用 Function-Call 的场景

实时数据查询 :需要查询数据库、API

工具调用 :需要执行操作(发送邮件、创建订单等)

计算任务 :需要执行计算、转换等

系统集成:需要调用外部系统

使用 SystemMessage 的场景

角色设定 :定义 AI 的角色和性格

行为约束 :定义 AI 的行为规则

格式要求 :定义输出格式

简单知识:少量固定的知识

组合使用

在实际应用中,可以组合使用这三种方式:

java 复制代码
ChatClient chatClient = ChatClient.builder(chatModel)
    .defaultSystem("""
        你是一个智能客服助手,请以友好的语气回答用户问题。
        """)
    .defaultAdvisors(
        QuestionAnswerAdvisor.builder(vectorStore).build(),  // RAG
        new SimpleLoggerAdvisor()
    )
    .defaultTools(toolService)  // Function-Call
    .build();

组合优势

  • RAG 提供知识库支持
  • Function-Call 提供工具能力
  • SystemMessage 定义角色和行为

项目代码分析

1. ChatClientRagTest

位置09rag/src/test/java/com/xushu/springai/rag/ChatClientRagTest.java

功能:演示 RAG 的基本使用和高级特性

关键方法

  1. testRag():简单 RAG 使用

    • 使用 `Questi
  2. testRag():简单 RAG 使用

    • 使用 QuestionAnswerAdvisor
    • 配置 topK=5, similarityThreshold=0.6
  3. testRag2():元数据过滤

    • 演示如何使用 filterExpression 过滤文档
  4. testRag3():高级 RAG

    • 使用 RetrievalAugmentationAdvisor
    • 配置查询重写、翻译、后处理等

2. RerankTest

位置09rag/src/test/java/com/xushu/springai/rag/RerankTest.java

功能:演示重排序的使用

关键点

  • 先检索大量文档(topK=200)
  • 使用 Rerank 模型重新排序
  • 选择最相关的文档

3. RagEvalTest

位置09rag/src/test/java/com/xushu/springai/rag/eval/RagEvalTest.java

功能:演示 RAG 评估

关键点

  • 使用 RelevancyEvaluator 评估答案相关性
  • 评估查询、上下文、答案三者之间的关系
  • 评估请求包含:原始查询、检索到的上下文、AI 生成的答案

评估流程

  1. 执行 RAG 查询,获取答案和上下文
  2. 构建 EvaluationRequest,包含查询、上下文、答案
  3. 使用 RelevancyEvaluator 评估相关性
  4. 输出评估结果(评分、理由等)

4. ReaderTest

位置09rag/src/test/java/com/xushu/springai/rag/ELT/ReaderTest.java

功能:演示各种文档读取器的使用

关键方法

  1. testReaderText():文本文件读取

    • 使用 TextReader 读取纯文本文件
    • 适用于简单的文本文档
  2. testReaderMD():Markdown 文件读取

    • 使用 MarkdownDocumentReader 读取 Markdown 文件
    • 配置选项:
      • withHorizontalRuleCreateDocument: 分割线是否创建新文档
      • withIncludeCodeBlock: 代码块是否创建新文档
      • withIncludeBlockquote: 引用是否创建新文档
      • withAdditionalMetadata: 添加元数据
  3. testReaderPdf():PDF 按页读取

    • 使用 PagePdfDocumentReader 按页读取 PDF
    • 每页创建一个 Document
  4. testReaderParagraphPdf():PDF 按段落读取

    • 使用 ParagraphPdfDocumentReader 按段落读取 PDF
    • 需要 PDF 有目录结构
    • 配置选项:
      • withReversedParagraphPosition: 坐标系反转(不同 PDF 工具可能使用不同坐标系)
      • withPageTopMargin: 上边距
      • withPageExtractedTextFormatter: 文本格式化(删除前 N 行等)

5. SplitterTest

位置09rag/src/test/java/com/xushu/springai/rag/ELT/SplitterTest.java

功能:演示文档分割和元数据增强

关键方法

  1. testTokenTextSplitter():标准 Token 分割

    • 使用 TokenTextSplitter 按 Token 数分割
    • 适用于英文文档
  2. testChineseTokenTextSplitter():中文 Token 分割

    • 使用 ChineseTokenTextSplitter 支持中文分词
    • 更准确的中文 Token 计数
  3. testKeywordMetadataEnricher():关键词元数据增强

    • 使用 KeywordMetadataEnricher 为文档提取关键词
    • 关键词可以用于元数据过滤
    • 可以自定义关键词提取模板和允许的关键词列表
  4. testSummaryMetadataEnricher():摘要元数据增强

    • 使用 SummaryMetadataEnricher 为文档生成摘要
    • 支持三种摘要类型:
      • PREVIOUS: 前一个分块的摘要
      • CURRENT: 当前分块的摘要
      • NEXT: 下一个分块的摘要
    • 摘要可以用于提高检索精度

重要提示

  • 不要过度追求按主题分割:企业级知识库文档多样,按 Token 数分割更实用
  • 只要 token 数合理就行:不需要严格按照主题分割

6. VectorStoreTest

位置09rag/src/test/java/com/xushu/springai/rag/VectorStoreTest.java

功能:演示向量存储的基本操作

关键点

  • 创建 SimpleVectorStore Bean
  • 添加文档(内部自动向量化)
  • 相似度检索
  • 查看检索结果的相似度分数

检索结果

  • 每个检索到的文档都包含相似度分数(score)
  • 分数范围通常在 0-1 之间
  • 分数越高,表示与查询越相似

7. EmbaddingTest

位置09rag/src/test/java/com/xushu/springai/rag/EmbaddingTest.java

功能:演示向量化的基本操作

关键点

  • 向量化单个文本
  • 查看向量维度
  • 理解向量化的结果

最佳实践

1. 文档处理最佳实践

1.1 文档分割

推荐做法

java 复制代码
// 使用合适的 chunkSize
ChineseTokenTextSplitter splitter = new ChineseTokenTextSplitter(
    200,   // chunkSize: 根据模型上下文窗口调整
    20,    // chunkOverlap: 10% 重叠
    50,    // minChunkSize: 避免过小的分块
    500,   // maxChunkSize: 避免过大的分块
    true   // keepSeparator: 保留分隔符
);

不推荐

  • chunkSize 过大(> 1000):可能超出模型上下文窗口
  • chunkSize 过小(< 50):可能丢失上下文信息
  • 无重叠:可能导致重要信息被分割
1.2 元数据管理

推荐做法

java 复制代码
Document doc = Document.builder()
    .text("退票政策...")
    .metadata(Map.of(
        "category", "退票",
        "filename", "terms-of-service.txt",
        "version", "1.0",
        "updateDate", "2025-01-01"
    ))
    .build();

元数据字段建议

  • category: 文档类别
  • filename: 文件名
  • version: 版本号
  • updateDate: 更新日期
  • author: 作者
  • tags: 标签列表

2. 检索最佳实践

2.1 相似度阈值

推荐做法

java 复制代码
SearchRequest.builder()
    .topK(5)
    .similarityThreshold(0.6)  // 根据实际效果调整
    .build();

阈值选择

  • 0.7-0.9:高精度,低召回率(严格匹配)
  • 0.5-0.7:平衡精度和召回率(推荐)
  • 0.3-0.5:低精度,高召回率(宽松匹配)
2.2 TopK 选择

推荐做法

java 复制代码
SearchRequest.builder()
    .topK(5)  // 根据模型上下文窗口和文档长度调整
    .build();

TopK 选择建议

  • 小文档(< 500 tokens):topK = 5-10
  • 中等文档(500-1000 tokens):topK = 3-5
  • 大文档(> 1000 tokens):topK = 1-3

3. 性能优化

3.1 向量存储选择

小规模(< 10万文档)

java 复制代码
SimpleVectorStore.builder(embeddingModel).build();

大规模(> 10万文档)

java 复制代码
// 使用 PostgreSQL + pgvector
PgVectorStore.builder(embeddingModel, dataSource).build();

// 或使用 Milvus
MilvusVectorStore.builder(embeddingModel, milvusClient).build();
3.2 缓存机制
java 复制代码
@Cacheable("rag-cache")
public String answer(String query) {
    // RAG 查询逻辑
}
3.3 异步处理
java 复制代码
@Async
public CompletableFuture<String> answerAsync(String query) {
    // 异步执行 RAG 查询
}

4. 错误处理

java 复制代码
try {
    String answer = chatClient.prompt()
        .user(query)
        .advisors(ragAdvisor)
        .call()
        .content();
    return answer;
} catch (Exception e) {
    log.error("RAG 查询失败: query={}", query, e);
    return "抱歉,我无法回答这个问题。请稍后再试。";
}

错误处理策略

  • 向量化失败:重试或使用备用模型
  • 检索失败:返回友好提示
  • 生成失败:记录日志,返回默认回复
  • 超时处理:设置合理的超时时间

5. 监控和日志

java 复制代码
@Slf4j
public class RagService {
    
    public String answer(String query) {
        long startTime = System.currentTimeMillis();
        
        try {
            // 执行 RAG 查询
            String answer = chatClient.prompt()
                .user(query)
                .advisors(ragAdvisor)
                .call()
                .content();
            
            long duration = System.currentTimeMillis() - startTime;
            log.info("RAG 查询成功: query={}, duration={}ms", query, duration);
            
            return answer;
        } catch (Exception e) {
            log.error("RAG 查询失败: query={}", query, e);
            throw e;
        }
    }
}

监控指标

  • 查询响应时间
  • 检索文档数量
  • 相似度分布
  • 错误率
  • 缓存命中率

常见问题

Q1: RAG 检索不到相关文档怎么办?

可能原因

  1. 相似度阈值设置过高
  2. 查询与文档语义不匹配
  3. 文档分割不合理
  4. 向量化模型不适合

解决方案

java 复制代码
// 1. 降低相似度阈值
SearchRequest.builder()
    .similarityThreshold(0.3)  // 从 0.6 降低到 0.3
    .build();

// 2. 增加 TopK
SearchRequest.builder()
    .topK(10)  // 从 5 增加到 10
    .build();

// 3. 使用查询重写
RewriteQueryTransformer.builder()
    .chatClientBuilder(ChatClient.builder(chatModel))
    .targetSearchSystem("航空票务助手")
    .build();

Q2: RAG 回答不准确怎么办?

可能原因

  1. 检索到的文档不相关
  2. 上下文信息不足
  3. 模型理解有误

解决方案

java 复制代码
// 1. 使用重排序
RetrievalRerankAdvisor retrievalRerankAdvisor = new RetrievalRerankAdvisor(
    vectorStore,
    rerankModel,
    SearchRequest.builder().topK(200).build()
);

// 2. 增加检索文档数量
SearchRequest.builder()
    .topK(10)  // 增加检索数量
    .build();

// 3. 使用元数据过滤
FilterExpressionBuilder filterExpression = FilterExpressionBuilder.builder()
    .eq("category", "退票")
    .build();

Q3: RAG 响应速度慢怎么办?

优化方案

java 复制代码
// 1. 使用缓存
@Cacheable(value = "rag-cache", key = "#query")
public String answer(String query) {
    // RAG 查询
}

// 2. 异步处理
@Async
public CompletableFuture<String> answerAsync(String query) {
    // 异步 RAG 查询
}

// 3. 使用更快的向量存储
// SimpleVectorStore(内存)比数据库向量存储快
SimpleVectorStore.builder(embeddingModel).build();

Q4: 如何处理多语言知识库?

解决方案

java 复制代码
// 使用查询翻译
TranslationQueryTransformer.builder()
    .chatClientBuilder(ChatClient.builder(chatModel))
    .targetLanguage("english")  // 翻译为英文检索
    .build();

Q5: 如何更新知识库?

增量更新

java 复制代码
// 添加新文档
vectorStore.add(newDocuments);

// 删除旧文档
vectorStore.delete(List.of(documentId));

// 更新文档
vectorStore.add(updatedDocuments);  // 会覆盖旧文档

Q6: 如何评估 RAG 效果?

评估方法

java 复制代码
// 使用 RelevancyEvaluator
RelevancyEvaluator evaluator = new RelevancyEvaluator(
    ChatClient.builder(chatModel)
);

EvaluationRequest request = new EvaluationRequest(
    query,
    context,
    answer
);

EvaluationResponse response = evaluator.evaluate(request);
System.out.println("相关性评分: " + response.getScore());

Q7: chunkSize 如何选择?

选择建议

  • 小文档(< 1000 tokens):chunkSize = 200-300
  • 中等文档(1000-5000 tokens):chunkSize = 300-500
  • 大文档(> 5000 tokens):chunkSize = 500-1000

考虑因素

  • 模型上下文窗口大小
  • 文档平均长度
  • 检索精度要求

Q8: 相似度阈值如何设置?

设置建议

  • 高精度场景:0.7-0.9(严格匹配)
  • 平衡场景:0.5-0.7(推荐)
  • 高召回场景:0.3-0.5(宽松匹配)

调整方法

  1. 从 0.6 开始测试
  2. 根据实际效果调整
  3. 记录不同阈值下的准确率

总结

RAG 的核心优势

  1. 知识更新灵活:无需重新训练模型,只需更新知识库
  2. 准确性高:基于真实文档回答,减少幻觉
  3. 可追溯性强:可以追溯到答案来源
  4. 领域适应快:快速适配新领域

RAG 的适用场景

适合使用 RAG

  • 企业知识库问答
  • 产品文档助手
  • 法律文档分析
  • 客服系统

不适合使用 RAG

  • 需要实时数据的场景(应使用 Function-Call)
  • 需要执行操作的场景(应使用 Function-Call)
  • 简单对话场景(直接使用 ChatModel)

RAG 最佳实践总结

  1. 文档处理

    • 合理设置 chunkSize(200-500 tokens)
    • 设置适当的重叠(10-20%)
    • 添加有意义的元数据
  2. 检索优化

    • 合理设置相似度阈值(0.5-0.7)
    • 根据文档大小调整 topK
    • 使用重排序提高精度
  3. 性能优化

    • 小规模使用 SimpleVectorStore
    • 大规模使用持久化向量存储
    • 添加缓存机制
  4. 错误处理

    • 完善的异常处理
    • 友好的错误提示
    • 详细的日志记录

参考资料


测试

java 复制代码
\# ali百炼
spring.ai.dashscope.api-key=sk-xxxx
spring.ai.dashscope.embedding.options.model= text-embedding-v4


logging.level.org.springframework.ai=debug
java 复制代码
package com.xushu.springai.rag;

import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import org.junit.jupiter.api.Test;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;

import java.util.List;

@SpringBootTest
public class VectorStoreTest {

    @TestConfiguration
    static class TestConfig {

        /**
         * 使用 DashScopeEmbeddingModel 构建 VectorStore
         * 注意:如果使用 OllamaEmbeddingModel,需要先确保 Ollama 服务运行
         * 并且已经下载了对应的 embedding 模型(如 nomic-embed-text)
         * 
         * 下载 Ollama embedding 模型命令:
         * ollama pull nomic-embed-text
         */
        @Bean
        public VectorStore vectorStore(DashScopeEmbeddingModel embeddingModel) {
            return SimpleVectorStore.builder(embeddingModel).build();
        }
    }



    @Test
    public void testVectorStore(@Autowired VectorStore vectorStore) {
        Document doc = Document.builder()
                .text("""
          预订航班:
          - 通过我们的网站或移动应用程序预订。
          - 预订时需要全额付款。
          - 确保个人信息(姓名、ID 等)的准确性,因为更正可能会产生 25 的费用。
          """)
                .build();
        Document doc2 = Document.builder()
                .text("""
          取消预订:
          - 最晚在航班起飞前 48 小时取消。
          - 取消费用:经济舱 75 美元,豪华经济舱 50 美元,商务舱 25 美元。
          - 退款将在 7 个工作日内处理。
          """)
                .build();

        // 存储向量(内部会自动向量化)
        vectorStore.add(List.of(doc, doc2));

        SearchRequest searchRequest = SearchRequest.builder()
                .query("退票")
                .topK(2)
                .similarityThreshold(0.5)

                .build();

        List<Document> documents = vectorStore.similaritySearch(searchRequest);

        for (Document document : documents) {
            System.out.println(document.getText());
            System.out.println(document.getScore());
        }
    }

}

结果输出:

相关推荐
Chan162 小时前
场景题:如何设计一个分布式ID
java·开发语言·spring boot·java-ee·intellij-idea
@TangXin2 小时前
Jenkins-Pipeline语法示例
java·servlet·jenkins
Geoking.2 小时前
【设计模式】组合模式(Composite)详解
java·设计模式·组合模式
廋到被风吹走2 小时前
【Spring】Spring Cloud Gateway 网关架构深度解析:路由、过滤器、限流与 Sentinel 集成
spring·架构·sentinel
怦怦蓝2 小时前
IDEA 项目打印日志全攻略:从基础使用到高级配置
java·开发语言·debug
Stream_Silver2 小时前
高效并行测试:在IDEA中同时运行多个参数化测试配置(idea2019如何在同一个项目运行多次)
java·ide·intellij-idea
BD_Marathon2 小时前
搭建MyBatis框架之创建mapper接口(四)
java·前端
kaico20182 小时前
ConcurrentHashMap源码分析
java
虫小宝2 小时前
企业微信API接口对接中Java后端的模拟测试(Mock)与单元测试实战技巧
java·单元测试·企业微信