使用 Spring AI 构建 RAG 搜索服务设计
系统整体架构采用分层模块化设计,核心包括以下部分:
文档向量存储(Vector Store):使用 Qdrant 作为向量数据库存储文档的语义向量表示,用于语义相似度检索。Spring AI 提供了抽象的 VectorStore 接口来封装向量数据库操作。通过引入 Spring AI 的 Qdrant 集成模块,可以将 Qdrant 用作 VectorStore,在应用中执行高效的相似度查询。
全文检索引擎:使用 Elasticsearch 存储文档的原始文本或关键词索引,用于基于关键词的布尔/全文检索。Elasticsearch 提供倒排索引能够有效匹配用户查询中的关键词。
混合检索器(Hybrid Retriever):RAG 服务的检索层组件,负责将关键词检索与向量语义检索结果结合。查询时同时对 Elasticsearch 执行关键词匹配检索,对 Qdrant 执行向量相似度检索,获取两方面的文档列表,并进行结果融合。通过混合检索,既能利用关键词精确匹配,又能利用向量检索捕获语义相关内容,提升召回率和准确性。Spring AI 提供了 DocumentRetriever 接口表示抽象的文档检索器,以及向量检索实现 VectorStoreDocumentRetriever;本方案可自定义实现一个 HybridRetriever,内部组合调用 Elasticsearch 和 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 并获取模型响应。
嵌入模型:将文本转换为向量表示的嵌入模型组件。系统支持两种方案:
- 本地 HuggingFace 模型:通过 Spring AI 的 Transformers ONNX 支持,加载本地预训练的 Transformer 模型(例如 all-MiniLM-L6-v2)以生成文本嵌入。此方式保证数据不出本地,响应速度快且无需调用外部 API。
- 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 分别与 Elasticsearch 和 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 进行集成。配置上,需要提供 Elasticsearch 的地址(例如 localhost:9200)、索引名称(如 documents)以及索引 mapping(包含文档 ID、内容等字段)。为了简化,实现上可以利用 Spring Data Elasticsearch 定义一个文档实体及对应的 Repository。例如定义 DocumentEntity(id, content, metadata) 并建立全文索引。
文档索引结构:为了方便混合检索结果的合并,我们为每个文档片段分配唯一 ID,并在 Qdrant 和 Elasticsearch 中共享该 ID 作为主键。每份文档在入库时通常会被拆分成若干片段 (chunks),每个片段是一段适合检索和上下文长度的文本。我们将每个片段作为独立的 Document 进行索引和向量存储。这样在检索阶段,无论通过向量还是关键词找到某片段,都可以根据 ID 确定对应的内容。可以将文档内容存储在 Elasticsearch 索引中,而在 Qdrant 中主要存储向量和必要的元信息(如文档 ID、来源等)作为 payload。
Hybrid Retriever 实现:创建一个混合检索器,例如 HybridRetriever implements DocumentRetriever。其 retrieve(Query query) 方法内部执行以下步骤:
Elasticsearch 关键词查询:如构造一个 match 或 bool 查询,匹配 content 字段包含查询文本,取 Top K 结果。可借助 Elasticsearch 的 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 去重,避免同一片段重复。简单策略是直接合并列表;如需更精细的融合,可根据 Elasticsearch 相关度分值和向量相似度分值对结果重新排序或加权。由于不同检索的分数不直接可比,一个实际工程策略是保留各自 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 模型并获得回答。
2. 本地 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 发送请求并获取模型回复。
3. 推理方式切换:为了在本地模型和云 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
如果使用 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 模型。
2. 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 强大的向量质量,但需考虑网络延迟和调用成本。
3. 嵌入模式切换:和 LLM 类似,可通过配置切换嵌入模型来源。比如配置 embedding.mode 为 local 或 openai。当为 local 时,启用 TransformersEmbeddingModel 或 OllamaEmbeddingModel;为 openai 时,则使用 OpenAiEmbeddingModel。开发时可以提供默认实现,并允许通过配置文件修改。例如默认采用本地模型,若用户提供了 OpenAI 的 apiKey 则自动改用 OpenAI 嵌入服务。
无论哪种实现,我们的 VectorStore 在初始化时都会绑定一个 EmbeddingModel,用于在向量数据库中存储和检索时转换文本。例如,上述配置的 embeddingModel 将被注入到 QdrantVectorStore 中,当调用 vectorStore.add(documents) 时,每个 Document 的文本内容会经由 EmbeddingModel 转为向量并存入 Qdrant。
数据流程说明
文档索引流程 (ETL 离线入库)
文档获取与预处理:通过管理后台或批处理任务,获取需要纳入知识库的原始文档。文档格式可以是纯文本、PDF、Markdown 等。使用 Spring AI 的文档读取器将文档转换为文本内容,并按段落或固定长度对文本进行分块,生成文档片段列表。每个片段可以封装为 Spring AI 的 Document 对象,其中包含内容文本和元数据(如文档 ID、标题、来源等)。
嵌入向量生成:针对每个文档片段,调用 EmbeddingModel 生成其语义向量表示。例如利用本地模型将每个片段转换为 768 维向量。向量通常存于 Document 对象的 embedding 字段,或直接在插入向量数据库时计算。这展示了从文档到向量的转换过程:首先将文档分割为小块,然后使用嵌入模型将每个块映射为高维向量表示,捕捉语义信息。
向量存储入库:调用 vectorStore.add(documents) 方法,将文档片段批量插入 Qdrant 向量数据库。Spring AI 的 VectorStore 接口对底层数据库执行插入操作,并自动处理向量数据和元信息存储。例如对于 Qdrant,每个 Document 的 embedding 向量和附加 payload 会存入指定 collection 中。如果 collection 尚不存在且 initializeSchema=true,VectorStore 实现会自动创建集合(相应的维度和索引参数)。插入成功后,文档的语义表示就持久化在 Qdrant 中了,可用于相似检索。
全文索引入库:将文档片段索引到 Elasticsearch。对于每个 Document 对象,从中提取必要字段(如 id、content、metadata),调用 Elasticsearch 的索引 API 存储。若使用 Spring Data Elasticsearch,可以调用 Repository 的 save() 方法批量保存片段实体。如果直接使用 Elasticsearch REST API,则构造批量索引请求。每个片段将成为 Elasticsearch 中一条文档记录,内容字段做全文检索索引处理。可以考虑在 content 字段上应用 Elasticsearch 自带的中文分词(如果内容为中文)以提高检索效果。
索引完成:经过以上过程,知识库文档被有效地存储为向量库+全文索引的混合形式:Qdrant 持有每个片段的向量及元数据,Elasticsearch 持有片段的可检索文本。后续查询即可利用这两种存储提供的能力。
注意:文档入库流程可以离线批处理,也可以通过提供 API 让用户动态上传文档。在本设计中,可以实现一个 /documents/load 接口(或使用 Stackademic 博文中的 /load 概念),接受文件上传请求,服务接收文件后执行上述步骤完成索引。同时返回状态或结果给客户端。为简化,此接口细节在此不展开。
在线检索问答流程
用户查询输入:用户通过前端或 API 调用发送查询请求给 RAG 服务(例如 HTTP GET/POST /ask?question=...)。查询内容为自然语言问题,例如:"这个系统如何支持本地模型?"。Controller 层接收请求后,将查询字符串封装为 Query 对象或直接传给服务层。
混合检索:RAG 服务调用 HybridRetriever 来从知识库中检索相关信息:
- Elasticsearch 关键词检索:对 Elasticsearch 执行查询,可使用 match 或 multi_match 在 content 字段搜索用户问题中的关键词。如果有匹配,返回相关度最高的前 N 个片段,例如 N=5。
- Qdrant 向量检索:将用户问题字符串通过 EmbeddingModel 编码为向量查询,调用 vectorStore.similaritySearch() 获取相似度最高的前 M 个片段,例如 M=5。可以设置一定的相似度阈值,忽略低于阈值的结果。
- 结果融合:将上两步获得的片段集合并集成。假定我们获取了最多 10 个相关片段。可对这些片段按重要性简单排序(比如根据是否包含关键词将 Elasticsearch 结果靠前),或不排序直接传递给 LLM。在本设计中,我们将片段文本直接作为上下文传递,因此顺序影响不大,但可以将更相关的放在前面以提高回答质量。
构造提示 (Prompt):将用户问题和检索到的文档片段组装成提示内容传给 LLM。通常采用一个预定义的 Prompt 模板,例如:
请根据以下文档内容回答问题。如果无法从中找到答案,请回复“未找到相关信息”。
文档内容:
{{{context}}}
问题: {{{question}}}
答案:
其中 {{{context}}} 占位符由检索到的多个文档片段文本拼接填充,{{{question}}} 为用户原始问题。这样生成的完整提示提供了问题所需的参考信息。此步骤可以使用 Spring AI 的 PromptTemplate 辅助完成,也可手动拼接字符串。这提到在查询阶段可使用 PromptTemplate 插入检索到的内容并渲染最终提示,然后发送给模型。
Spring AI 也支持直接使用 QuestionAnswerAdvisor 简化这一过程。如果使用 ChatClient.prompt().advisors(new QuestionAnswerAdvisor(vectorStore)),它会自动在调用模型前查询向量库并附加结果。但由于我们实现了自定义的 HybridRetriever(包含 Elasticsearch),我们将自行构造 prompt,以便加入两种检索结果。
- 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 返回,提取其内容字符串,即为最终解答。
结果返回:将 LLM 的回答字符串封装为响应,通过 Controller 返回给调用方。可以只返回答案文本,或者包含一些元信息(如引用的文档来源列表等)。本设计聚焦核心功能,假定仅返回答案文本。在前端界面上,用户即可看到根据其提问生成的回答。
对话记忆(可选):如果需要多轮对话支持,可结合 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 所需的 Elasticsearch 客户端和 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 和 Elasticsearch 时,可以使用 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 问答系统,在保证回答准确性的同时兼顾部署灵活性和数据私有性,为企业知识库问答、智能搜索等应用场景提供有力支持。