项目概述

本文档设计了一套基于Spring Boot和Spring AI框架的检索增强生成(Retrieval-Augmented Generation, RAG)搜索服务。该服务通过融合Elasticsearch关键词检索和Qdrant向量检索,实现对文档知识库的高相关性查询,并结合大语言模型(LLM)生成最终回答。系统支持在本地模型(通过Ollama运行开源模型)与OpenAI云接口之间灵活切换推理方式,并支持使用本地 HuggingFace 嵌入模型或OpenAI Embedding API生成文本向量。设计涵盖总体架构、核心模块、接口定义、数据流程、关键类配置和示例控制层,并提供完整的Docker Compose环境配置,确保方案可复现且结构清晰。

整体架构

image-20250429120643387

系统整体架构采用分层模块化设计,核心包括以下部分:

  • 文档向量存储(Vector Store):使用 Qdrant 作为向量数据库存储文档的语义向量表示,用于语义相似度检索。Spring AI 提供了抽象的 VectorStore 接口来封装向量数据库操作 。通过引入 Spring AI 的 Qdrant 集成模块,可以将 Qdrant 用作 VectorStore,在应用中执行高效的相似度查询。
  • 全文检索引擎:使用 Elasticsearch 存储文档的原始文本或关键词索引,用于基于关键词的布尔/全文检索。Elasticsearch 提供倒排索引能够有效匹配用户查询中的关键词。
  • 混合检索器(Hybrid Retriever):RAG服务的检索层组件,负责将关键词检索与向量语义检索结果结合。查询时同时对ES执行关键词匹配检索,对Qdrant执行向量相似度检索,获取两方面的文档列表,并进行结果融合。通过混合检索,既能利用关键词精确匹配,又能利用向量检索捕获语义相关内容,提升召回率和准确性。Spring AI 提供了 DocumentRetriever 接口表示抽象的文档检索器,以及向量检索实现 VectorStoreDocumentRetriever ;本方案可自定义实现一个HybridRetriever,内部组合调用 ES 和 Qdrant 的检索接口,然后用 文档合并器将结果合并(可简单去重拼接,或按分数融合)。例如,可使用 Spring AI 的 ConcatenationDocumentJoiner 将多数据源的结果集合并为一个文档列表 。
  • RAG服务层:封装整个“检索-生成”流程的核心服务(可称为 RagService)。它负责接收用户查询,调用Hybrid Retriever检索相关文档,将检索到的文档作为上下文提交给LLM,并返回生成的答案。RAG服务通过 Spring AI 提供的 ChatClient 与底层LLM模型交互 。ChatClient抽象出与AI模型交互的通用接口,支持同步或流式地发送提示并获得回复 。借助Spring AI的模块化设计,可以方便地更换底层使用的LLM或向量库,而无需大改业务代码 。
  • LLM推理层:大语言模型用于根据用户查询和检索到的文档上下文生成回答。通过Spring AI的ChatModel与ChatClient抽象,我们可以无缝切换不同的模型和提供商。例如,当使用OpenAI时,可配置ChatClient调用OpenAI的GPT模型;当使用本地模型时,利用Ollama提供本地LLM推理服务。ChatClient屏蔽了底层差异,以统一的API发送Prompt并获取模型响应 。
  • 嵌入模型:将文本转换为向量表示的嵌入模型组件。系统支持两种方案:
    1. 本地 HuggingFace 模型:通过Spring AI的Transformers ONNX支持,加载本地预训练的Transformer模型(例如all-MiniLM-L6-v2)以生成文本嵌入 。此方式保证数据不出本地,响应速度快且无需调用外部API。
    2. OpenAI Embedding API:利用OpenAI的embedding接口(如text-embedding-ada-002)获取文本的向量表示,需要网络调用和API密钥。Spring AI 提供了开箱即用的 OpenAiEmbeddingModel 实现,我们只需提供 OpenAI API key 即可使用 。

上述组件通过Spring Boot进行配置和组织,最终以RESTful API形式提供服务,包括文档入库接口和问答查询接口。下图展示了系统各组件的交互关系:

(架构说明:用户通过Controller发出请求,RAG Service调用Hybrid Retriever分别与ES和Qdrant交互检索文档;嵌入模型用于向量生成供Qdrant存储与检索;ChatClient封装LLM模型调用(本地或云);最终将检索到的相关文档作为提示上下文,交由LLM生成答案并返回给用户。)

核心模块设计

向量存储与混合检索模块

Qdrant 向量存储配置:系统使用Qdrant来持久化文档向量及元数据。通过引入依赖 spring-ai-qdrant-store,Spring AI能够自动配置Qdrant相关的Bean 。我们需要提供Qdrant客户端以及VectorStore Bean,例如:

@Bean
public QdrantClient qdrantClient() {
    // 构建连接到 Qdrant 的 gRPC 客户端
    QdrantGrpcClient.Builder grpcClientBuilder = 
        QdrantGrpcClient.newBuilder("qdrant-host", 6334, false);
    grpcClientBuilder.withApiKey("<QDRANT_API_KEY>");
    return new QdrantClient(grpcClientBuilder.build());
}

@Bean
public VectorStore vectorStore(QdrantClient client, EmbeddingModel embeddingModel) {
    return QdrantVectorStore.builder(client, embeddingModel)
            .collectionName("documents")       // 指定集合名称
            .initializeSchema(true)            // 若未预建集合则自动创建
            .build();
}

上述配置创建了VectorStore接口的Qdrant实现,用于存储向量及执行相似度搜索。 其中注入的 EmbeddingModel 用于在存储文档时生成其向量表示。注意:可以提前在Qdrant中创建集合并指定向量维度和相似度度量方式,或通过 initializeSchema(true) 由程序自动创建(默认使用 Cosine 距离,维度取决于所用嵌入模型) 。确保Qdrant实例已启动并可访问,比如通过Docker启动 Qdrant 容器。

Elasticsearch 全文检索配置:Elasticsearch用于存储文档文本索引,实现关键词匹配检索。可采用Spring Data Elasticsearch或Rest API进行集成。配置上,需要提供ES的地址(例如localhost:9200)、索引名称(如documents)以及索引mapping(包含文档ID、内容等字段)。为了简化,实现上可以利用Spring Data Elasticsearch定义一个文档实体及对应的Repository。例如定义DocumentEntity(id, content, metadata)并建立全文索引。

文档索引结构:为了方便混合检索结果的合并,我们为每个文档片段分配唯一ID,并在Qdrant和Elasticsearch中共享该ID作为主键。每份文档在入库时通常会被拆分成若干片段 (chunks),每个片段是一段适合检索和上下文长度的文本。我们将每个片段作为独立的Document进行索引和向量存储。这样在检索阶段,无论通过向量还是关键词找到某片段,都可以根据ID确定对应的内容。可以将文档内容存储在ES索引中,而在Qdrant中主要存储向量和必要的元信息(如文档ID、来源等)作为 payload。

Hybrid Retriever 实现:创建一个混合检索器,例如 HybridRetriever implements DocumentRetriever。其retrieve(Query query)方法内部执行以下步骤:

  • 使用ES执行关键词查询:如构造一个match或bool查询,匹配content字段包含查询文本,取Top K结果。可借助ES的REST客户端或Repository方法实现,得到一组Document片段列表(含ID和内容)。
  • 使用Qdrant执行相似度查询:调用上面配置的vectorStore.similaritySearch()方法,对用户查询文本进行向量查询,获取Top K相似文档片段 。Spring AI 提供了 SearchRequest 可指定相似度阈值和topK等参数 。例如:
 List<Document> vectorDocs = vectorStore.similaritySearch(
       SearchRequest.builder()
           .query(userQuery)
           .topK(5)
           .build()
 );

这将返回与查询最相似的5个文档片段。

  • 合并结果:将关键词结果与向量结果合并。可以用文档ID去重,避免同一片段重复。简单策略是直接合并列表;如需更精细的融合,可根据ES相关度分值和向量相似度分值对结果重新排序或加权。由于不同检索的分数不直接可比,一个实际工程策略是保留各自Top K的结果,然后让上层LLM基于这些候选片段自行判断相关性。这里我们采取简单去重合并策略,并限制总片段数(如取不超过6-10个片段)以控制提示长度。
  • 输出:返回合并后的Document列表。每个Document包含内容文本,以及可能的元数据(例如来源标识用于输出引用)。 通过HybridRetriever,我们将语义匹配和精确匹配结合,最大程度找到相关内容。例如,当用户查询中包含与文档不同的措辞时,向量检索可找到语义相似的内容,而关键词检索可保证若有精确匹配的术语不会被遗漏。

LLM 推理与切换模块

ChatClient与ChatModel抽象:Spring AI 提供了 ChatModel 接口表示具体的大语言模型(如OpenAI ChatGPT、Anthropic Claude、本地Llama等),ChatClient 则封装了与这些模型交互的通用客户端 。本服务通过注入一个ChatClient实例来调用LLM进行回答生成。得益于ChatClient的抽象设计,我们可以灵活地更换背后的ChatModel,而业务代码保持不变。例如,可以在配置中切换使用OpenAI的GPT-4模型或本地的Llama2模型 。

  1. OpenAI 模型集成:若使用OpenAI云服务,添加依赖spring-ai-openai,并在应用配置中提供OpenAI的API密钥和所选模型名称。例如在application.yml中配置:
spring:
  ai:
    openai:
      api-key: YOUR_OPENAI_API_KEY
      chat:
        model: gpt-4

这将启用Spring AI对OpenAI Chat API的自动配置。注入的ChatClient将默认使用OpenAI指定的模型。通过ChatClient的fluent API,可以很方便地发送对话消息并获取回复。例如:

ChatResponse response = chatClient.prompt()
    .user("请解释RAG的作用")
    .call()
    .chatResponse();
String answer = response.content();

上述调用会将用户消息发送到配置的OpenAI模型并获得回答 。

  1. 本地Ollama模型集成:若采用本地LLM模型,需安装并运行 Ollama 服务。Ollama可以管理和提供本地模型的推理服务,并通过REST接口与应用交互 。引入依赖spring-ai-ollama后,Spring AI提供了 OllamaChatModel 和 OllamaApi 来对接本地Ollama实例 。配置上,可在application.yml中指定Ollama服务地址及所用模型,例如:
spring:
  ai:
    ollama:
      base-url: http://localhost:11434
      default-model: llama2

假设已通过“Ollama pull”命令下载了名称为“llama2”的模型,Ollama将以该模型提供推理。应用启动时,会自动创建连接到Ollama的API客户端 (OllamaApi) 和对应的 ChatModel 实现。ChatClient随即可使用本地模型进行对话。

使用Ollama时,调用方式与OpenAI类似,只是由本地服务产生结果。例如:

ChatResponse response = chatClient.prompt()
    .system("你是资深Java助理")   // 可以设定初始系统提示
    .user("给出RAG搜索服务的优势")
    .call()
    .chatResponse();

ChatClient将向本地Ollama发送请求并获取模型回复。

  1. 推理方式切换:为了在本地模型和云API之间切换,设计上可以使用Spring Profiles或配置开关。例如定义llm.mode配置项,可取值local或openai。通过条件配置,在local模式下注入Ollama的ChatClient Bean,在openai模式下注入OpenAI的ChatClient Bean。借助Spring AI,对不同ChatModel的支持是开箱即用的,开发者只需更改配置即可无缝切换LLM提供方,大大减少切换带来的代码改动 。

文本嵌入生成模块

  1. HuggingFace 本地嵌入:为了确保数据安全和降低依赖,本方案支持使用本地Embedding模型。利用Spring AI的Transformer ONNX支持,我们可以在Java中直接加载HuggingFace提供的embedding模型(转换为ONNX格式)并生成向量。 例如选择sentence-transformers/all-MiniLM-L6-v2模型,该模型输出768维句向量。首先将ONNX模型文件和tokenizer文件准备好(可通过Spring AI自动下载缓存)。然后配置EmbeddingModel Bean:
@Bean
public EmbeddingModel embeddingModel() {
    TransformersEmbeddingModel model = new TransformersEmbeddingModel();
    // 可选:指定模型和分词器资源路径或URL(否则使用默认的all-MiniLM-L6-v2)
    model.setModelResource("classpath:/onnx/all-MiniLM-L6-v2/model.onnx");
    model.setTokenizerResource("classpath:/onnx/all-MiniLM-L6-v2/tokenizer.json");
    return model;
}

上述 TransformersEmbeddingModel 会加载本地ONNX模型,在调用embed()时对输入文本列表输出对应的向量表示 。作为Spring Bean时,Spring AI会自动调用其afterPropertiesSet()完成初始化 。第一次使用时模型文件可能从指定位置加载或下载并缓存。 之后即可通过 embeddingModel.embed(List texts) 获取嵌入向量。

如果使用Ollama作为本地引擎,另一种方法是利用Ollama的Embedding API。Spring AI提供了OllamaEmbeddingModel封装Ollama的向量生成接口 。通过pull相应的嵌入模型(Ollama支持直接pull HuggingFace上的embedding模型,如ollama pull hf.co/intfloat/e5-small-v2),然后:

@Bean
public EmbeddingModel embeddingModel(OllamaApi ollamaApi) {
    // 假设使用一个名为'e5-small-v2'的embedding模型
    var options = OllamaOptions.builder().model("e5-small-v2").build();
    return new OllamaEmbeddingModel(ollamaApi, options);
}

此方式下,所有文本向量将由Ollama本地服务生成,效果等同于直接使用HuggingFace模型。

  1. OpenAI Embedding API:如需借助OpenAI的预训练模型快速获取嵌入,可以使用OpenAI提供的Embedding接口(例如text-embedding-ada-002模型)。Spring AI的OpenAiEmbeddingModel封装了调用逻辑,只需提供API密钥和可选的模型名称即可使用。例如:
@Bean
public EmbeddingModel embeddingModel() {
    return new OpenAiEmbeddingModel(new OpenAiApi(System.getenv("OPENAI_API_KEY")));
}

默认情况下,将使用Ada模型生成1536维的嵌入向量 。这种方式适合对接OpenAI强大的向量质量,但需考虑网络延迟和调用成本。

  1. 嵌入模式切换:和LLM类似,可通过配置切换嵌入模型来源。比如配置embedding.mode为local或openai。当为local时,启用TransformersEmbeddingModel或OllamaEmbeddingModel;为openai时,则使用OpenAiEmbeddingModel。开发时可以提供默认实现,并允许通过配置文件修改。例如默认采用本地模型,若用户提供了OpenAI的apiKey则自动改用OpenAI嵌入服务。

无论哪种实现,我们的VectorStore在初始化时都会绑定一个EmbeddingModel,用于在向量数据库中存储和检索时转换文本 。例如,上述配置的embeddingModel将被注入到QdrantVectorStore中,当调用vectorStore.add(documents)时,每个Document的文本内容会经由EmbeddingModel转为向量并存入Qdrant 。

数据流程说明

文档索引流程 (ETL离线入库)

  1. 文档获取与预处理:通过管理后台或批处理任务,获取需要纳入知识库的原始文档。文档格式可以是纯文本、PDF、Markdown等。使用Spring AI的文档读取器将文档转换为文本内容,并按段落或固定长度对文本进行分块,生成文档片段列表。每个片段可以封装为Spring AI的 Document 对象,其中包含内容文本和元数据(如文档ID、标题、来源等)。
  2. 嵌入向量生成:针对每个文档片段,调用EmbeddingModel生成其语义向量表示。例如利用本地模型将每个片段转换为768维向量。向量通常存于Document对象的embedding字段,或直接在插入向量数据库时计算。 展示了从文档到向量的转换过程:首先将文档分割为小块,然后使用嵌入模型将每个块映射为高维向量表示,捕捉语义信息。
  3. 向量存储入库:调用vectorStore.add(documents)方法,将文档片段批量插入Qdrant向量数据库 。Spring AI 的 VectorStore 接口对底层数据库执行插入操作,并自动处理向量数据和元信息存储。例如对于Qdrant,每个Document的embedding向量和附加payload会存入指定collection中。如果collection尚不存在且initializeSchema=true,VectorStore实现会自动创建集合(相应的维度和索引参数) 。插入成功后,文档的语义表示就持久化在Qdrant中了,可用于相似检索。
  4. 全文索引入库:将文档片段索引到Elasticsearch。对于每个Document对象,从中提取必要字段(如id、content、metadata),调用ES的索引API存储。若使用Spring Data Elasticsearch,可以调用Repository的save()方法批量保存片段实体。如果直接使用ES REST API,则构造批量索引请求。每个片段将成为ES中一条文档记录,内容字段做全文检索索引处理。可以考虑在content字段上应用ES自带的中文分词(如果内容为中文)以提高检索效果。
  5. 索引完成:经过以上过程,知识库文档被有效地存储为向量库+全文索引的混合形式:Qdrant持有每个片段的向量及元数据,Elasticsearch持有片段的可检索文本。后续查询即可利用这两种存储提供的能力。

注意: 文档入库流程可以离线批处理,也可以通过提供API让用户动态上传文档。在本设计中,可以实现一个/documents/load接口(或使用Stackademic博文中的/load概念 ),接受文件上传请求,服务接收文件后执行上述步骤完成索引。同时返回状态或结果给客户端。为简化,此接口细节在此不展开。

在线检索问答流程

  1. 用户查询输入:用户通过前端或API调用发送查询请求给RAG服务(例如HTTP GET/POST /ask?question=…)。查询内容为自然语言问题,例如:“这个系统如何支持本地模型?”。Controller层接收请求后,将查询字符串封装为Query对象或直接传给服务层。
  2. 混合检索:RAG服务调用 HybridRetriever 来从知识库中检索相关信息:
  • ES关键词检索:对Elasticsearch执行查询,可使用match或multi_match在content字段搜索用户问题中的关键词。如果有匹配,返回相关度最高的前N个片段,例如N=5。
  • Qdrant向量检索:将用户问题字符串通过EmbeddingModel编码为向量查询,调用vectorStore.similaritySearch()获取相似度最高的前M个片段,例如M=5 。可以设置一定的相似度阈值,忽略低于阈值的结果 。
  • 结果融合:将上两步获得的片段集合并集成。假定我们获取了最多10个相关片段。可对这些片段按重要性简单排序(比如根据是否包含关键词将ES结果靠前),或不排序直接传递给LLM。在本设计中,我们将片段文本直接作为上下文传递,因此顺序影响不大,但可以将更相关的放在前面以提高回答质量。
  1. 构造提示 (Prompt):将用户问题和检索到的文档片段组装成提示内容传给LLM。通常采用一个预定义的Prompt模板,例如:
请根据以下文档内容回答问题。如果无法从中找到答案,请回复“未找到相关信息”。

文档内容:
{{{context}}}

问题: {{{question}}}

答案:

其中{{{context}}}占位符由检索到的多个文档片段文本拼接填充,{{{question}}}为用户原始问题。这样生成的完整提示提供了问题所需的参考信息。此步骤可以使用Spring AI的PromptTemplate辅助完成,也可手动拼接字符串。 提到在查询阶段可使用PromptTemplate插入检索到的内容并渲染最终提示,然后发送给模型。 Spring AI 也支持直接使用 QuestionAnswerAdvisor 简化这一过程。如果使用ChatClient.prompt().advisors(new QuestionAnswerAdvisor(vectorStore)),它会自动在调用模型前查询向量库并附加结果 。但由于我们实现了自定义的HybridRetriever(包含ES),我们将自行构造prompt,以便加入两种检索结果。

  1. LLM生成回答:调用LLM模型生成答案。通过前述配置好的ChatClient来执行:
String promptText = promptTemplate.render(Map.of("context", combinedDocsText, "question", userQuestion));
ChatResponse response = chatClient.prompt(promptText).call().chatResponse();
String answer = response.content();

ChatClient会将我们提供的完整提示发送给底层ChatModel(OpenAI或Ollama)。模型接收到含有参考内容的提问后,基于提供的上下文进行作答,从而减小幻觉和出错的概率 。生成的答案通过ChatResponse返回,提取其内容字符串,即为最终解答。

  1. 结果返回:将LLM的回答字符串封装为响应,通过Controller返回给调用方。可以只返回答案文本,或者包含一些元信息(如引用的文档来源列表等)。本设计聚焦核心功能,假定仅返回答案文本。在前端界面上,用户即可看到根据其提问生成的回答。
  2. 对话记忆(可选):如果需要多轮对话支持,可结合Spring AI的ChatMemory功能,将之前的问题和答案作为对话历史提供给ChatClient,以实现上下文记忆。但基本的RAG问答场景可不考虑对话历史,每次独立进行。

整个查询流程在用户看来只是提供问题、获得答案,但背后经过了关键词和语义双通道检索,再由大模型综合分析文档给出结果,从而实现“以知识库为基础的问答”。这种RAG技术有效缓解了LLM上下文长度限制、知识截止和幻觉问题 。

关键组件与接口定义

配置与组件摘要

主要依赖:项目引入以下关键依赖:

  • Spring Boot 3.x(基础框架)
  • Spring AI 核心 (spring-ai-bom BOM 和所需starter)
  • Spring AI OpenAI Starter (如使用OpenAI API)
  • Spring AI Ollama Starter (如使用本地Ollama)
  • Spring AI Qdrant Vector Store Starter (spring-ai-qdrant-store-spring-boot-starter)
  • Spring AI ONNX Embedding (如使用本地Transformer模型,可选 spring-ai-embeddings-transformer-onnx)
  • Spring Data Elasticsearch 或 Elasticsearch Java客户端
  • Qdrant Java SDK 或通过Spring AI的 QdrantClient 支持
  • 其他:如需要文档解析,可选 Spring AI 提供的 PDF/Text 文档读取组件。

应用配置:通过application.yml管理上述模式切换参数,例如:

app:
  llm: mode: local        # 本地(local)或openai
  embedding: mode: local  # 本地(local)或openai
spring:
  ai:
    # OpenAI 配置(仅当使用openai模式)
    openai:
      api-key: xxxx
      chat.model: gpt-4
    # Ollama 配置(仅当使用local模式)
    ollama:
      base-url: http://ollama:11434   # 假设Docker容器内服务名
      default-model: llama2-7b
    # Qdrant 向量库配置
    vectorstore:
      qdrant:
        collection-name: documents
        initialize-schema: true

上例展示了自定义的app配置段用于控制模式,以及Spring AI自带的一些配置项。通过profiles或条件注入,可以根据app.llm.mode选择不同的ChatModel配置。

关键Bean:汇总本设计涉及的重要Spring Bean及其配置方式:

  • EmbeddingModel Bean:如上所述,可以是OpenAiEmbeddingModel或TransformersEmbeddingModel,负责文本嵌入。
  • QdrantClient Bean:封装与Qdrant服务的连接,使用gRPC或REST客户端。
  • VectorStore Bean:比如QdrantVectorStore,构造时需注入QdrantClient和EmbeddingModel。
  • ChatClient Bean:通常通过Spring AI自动配置的ChatClient.Builder生成。可直接注入ChatClient或其Builder。在使用多个ChatModel时,也可以注入多个命名的ChatClient。例如:
@Bean
@ConditionalOnProperty(name="app.llm.mode", havingValue="openai")
ChatClient chatClientOpenAI(OpenAiChatModel model) {
    return ChatClient.builder(model).build();
}

@Bean
@ConditionalOnProperty(name="app.llm.mode", havingValue="local")
ChatClient chatClientOllama(OllamaChatModel model) {
    return ChatClient.builder(model).build();
}

上述伪代码表明根据配置选择不同的ChatModel创建ChatClient。实际上若使用Spring Boot Starter和配置文件,大部分情况直接@Autowired ChatClient即可(底层已根据配置选好了模型)。

  • HybridRetriever Bean:如果实现了HybridRetriever类,可将其声明为Bean,内部@Autowired所需的ES客户端和VectorStore,用于执行检索逻辑。
  • RagService Bean:封装RAG流程的服务类。它会@Autowired HybridRetriever和ChatClient,用来实现answerQuestion(String question)方法。

RagService 接口定义

RagService可以定义如下接口以供Controller调用:

public interface RagService {
    /** 根据用户问题返回答案 */
    String getAnswer(String question);
}

其实现类 RagServiceImpl 负责按照在线检索问答流程的步骤完成工作:

@Service
public class RagServiceImpl implements RagService {
    @Autowired private HybridRetriever retriever;
    @Autowired private ChatClient chatClient;
    @Value("${app.maxDocs:6}") private int maxDocs;

    @Override
    public String getAnswer(String question) {
        // 1. 调用混合检索获取相关文档片段列表
        List<Document> docs = retriever.retrieve(new Query(question));
        if (docs.isEmpty()) {
            return "抱歉,未能找到相关信息。";
        }
        // 截取最多maxDocs篇,以防过长
        List<Document> topDocs = docs.size() > maxDocs ? docs.subList(0, maxDocs) : docs;
        // 2. 构造提示上下文文本
        StringBuilder context = new StringBuilder();
        for (Document doc : topDocs) {
            context.append(doc.getContent()).append("\n");
        }
        String prompt = String.format("基于以下内容回答问题:\n%s\n问题:%s\n回答:", context, question);
        // 3. 调用LLM生成回答
        ChatResponse response = chatClient.prompt(prompt).call().chatResponse();
        return response.content();
    }
}

上述代码中:

  • retriever.retrieve返回Document列表,每个Document包含content文本等信息。
  • 将片段内容简单拼接为上下文(真实应用可加入分隔符并注明来源)。
  • 用ChatClient.prompt()发送组装的prompt获取响应。若无相关片段则直接返回无法找到信息的答复。

通过这种实现,RagService对外提供了一个高层接口,隐藏了内部复杂性,Controller只需传入问题字符串即可获得答案。

示例 Controller

最后,提供一个示例控制器来演示接口定义和调用流程:

@RestController
@RequestMapping("/api")
public class QaController {
    @Autowired
    private RagService ragService;

    // 提问接口
    @GetMapping("/ask")
    public ResponseEntity<String> askQuestion(@RequestParam("q") String question) {
        String answer = ragService.getAnswer(question);
        return ResponseEntity.ok(answer);
    }

    // 文档上传接口(可选,实现文档入库)
    @PostMapping(value="/documents", consumes=MediaType.MULTIPART_FORM_DATA_VALUE)
    public ResponseEntity<String> uploadDocument(@RequestPart("file") MultipartFile file) {
        // 调用文档处理服务将文件内容存入ES和Qdrant
        // DocumentLoader.load(file);
        return ResponseEntity.ok("文档已上传并索引完成");
    }
}
  • /api/ask 接口接受查询参数q为用户问题,调用RagService获取答案并直接返回。HTTP方法用GET简单起见,也可用POST提交复杂查询。
  • /api/documents 接口示例展示了如何接收文件并调用文档加载流程,将内容索引到系统中。文档处理可以参考前述文档索引流程实现。

通过上述Controller,前端或用户可以通过HTTP请求与RAG服务交互,实现动态问答。

Docker Compose 环境部署方案

为方便开发和部署,提供Docker Compose配置同时启动所需的外部服务(Qdrant、Elasticsearch、Ollama)和本Spring Boot应用。本项目的docker-compose.yml示例如下:

version: '3.9'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.9.0
    container_name: elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false       # 禁用安全认证
      - ES_JAVA_OPTS=-Xms1g -Xmx1g         # 内存配置,可根据需要调整
    ports:
      - "9200:9200"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9200"] 
      interval: 30s
      retries: 3

  qdrant:
    image: qdrant/qdrant:v1.3.5
    container_name: qdrant
    ports:
      - "6333:6333"   # HTTP API
      - "6334:6334"   # gRPC API
    volumes:
      - qdrant_data:/qdrant/storage

  ollama:
    image: ollama/ollama:0.1.9
    container_name: ollama
    ports:
      - "11434:11434"  # Ollama REST API 默认端口
    volumes:
      - ollama_data:/root/.ollama

  rag-service:
    build: .
    container_name: rag-service
    environment:
      SPRING_PROFILES_ACTIVE: "local"          # 激活本地LLM配置。如切换OpenAI则设为cloud等。
      OPENAI_API_KEY: "${OPENAI_API_KEY:-}"    # 可选,提供OpenAI密钥
      QDRANT_GRPC_HOST: "qdrant"               # 若应用读取环境配置连接Qdrant
      QDRANT_GRPC_PORT: 6334
    depends_on:
      - elasticsearch
      - qdrant
      - ollama
    ports:
      - "8080:8080"

说明:

  • Elasticsearch:使用官方ES 8.9镜像,配置为单节点模式并关闭安全,以便简化开发测试。映射端口9200用于REST访问。
  • Qdrant:使用官方Qdrant镜像,开放6333端口供HTTP API(若使用)和6334端口供gRPC客户端。挂载卷qdrant_data持久化向量数据。
  • Ollama:使用Ollama官方Docker镜像。映射11434端口用于与Spring应用通信(Ollama提供REST API),挂载卷ollama_data保存已下载的模型数据,以避免容器重启后需要重新pull模型 。在启动前可以先运行类似docker exec -it ollama ollama pull llama2的命令下载所需模型,或在应用初始化时通过Ollama API触发模型下载。
  • RAG服务:假设Spring Boot应用已打包为镜像(Compose中通过build构建)。通过环境变量配置应用Profile和所需参数:例如激活local Profile表示使用本地Ollama;如需OpenAI则对应Profile下提供OPENAI_API_KEY等。depends_on确保其他服务先行启动。端口8080用于对外提供HTTP接口。

网络配置:Compose默认会将这些服务置于同一网络下,容器名称就是主机名。因此应用在连接Qdrant和ES时,可以使用qdrant:6334、elasticsearch:9200作为地址。Spring AI Docker Compose集成模块甚至可以根据容器名自动发现服务并配置连接,例如名字包含“ollama/ollama”的容器会被识别为Ollama服务 。

启动:运行docker-compose up -d将后台启动所有容器。待Elasticsearch和Qdrant完成启动(可通过健康检查日志或API验证),Spring Boot应用会自动连接它们。此时:

  • 可以调用POST /api/documents上传文档(或通过其他方式预先索引文档);
  • 然后调用GET /api/ask?q=你的问题获取答案。

通过Docker Compose,一键部署整个RAG系统的依赖,方便在本地或服务器上运行测试。

结论

本设计文档详细描述了一个基于Java Spring生态的RAG搜索服务方案。通过Spring AI框架提供的抽象和集成能力,我们实现了Retriever-VectorStore-RAG Service的模块化架构,支持混合检索、灵活的LLM/Embedding切换以及容器化部署。核心技术选型包括Spring Boot、Spring AI、Elasticsearch、Qdrant和Ollama等,均为当下流行且有良好支持的组件。文档提供了整体架构和数据流程解析,给出了关键配置和代码示例,确保实现细节清晰可行。开发者可以据此搭建起一个可复现的RAG问答系统,在保证回答准确性的同时兼顾部署灵活性和数据私有性,为企业知识库问答、智能搜索等应用场景提供有力支持。

Ge Yuxu • AI & Engineering

脱敏说明:本文所有出现的表名、字段名、接口地址、变量名、IP地址及示例数据等均非真实, 仅用于阐述技术思路与实现步骤,示例代码亦非公司真实代码。 示例方案亦非公司真实完整方案,仅为本人记忆总结,用于技术学习探讨。
    • 文中所示任何标识符并不对应实际生产环境中的名称或编号。
    • 示例 SQL、脚本、代码及数据等均为演示用途,不含真实业务数据,也不具备直接运行或复现的完整上下文。
    • 读者若需在实际项目中参考本文方案,请结合自身业务场景及数据安全规范,使用符合内部命名和权限控制的配置。

版权声明:本文版权归原作者所有,未经作者事先书面许可,任何单位或个人不得以任何方式复制、转载、摘编或用于商业用途。
    • 若需非商业性引用或转载本文内容,请务必注明出处并保持内容完整。
    • 对因商业使用、篡改或不当引用本文内容所产生的法律纠纷,作者保留追究法律责任的权利。

Copyright © 1989–Present Ge Yuxu. All Rights Reserved.