本文基于私有化部署 场景,在「文本向量化入库」的基础上,完整实现 RAG 核心链路:用户提问 → 向量库语义检索 → Prompt 增强(拼接检索结果) → 本地大模型生成回答,核心技术栈为 LangChain4j + Spring Boot + Ollama(Qwen 2/DeepSeek 大模型) + Milvus/Chroma(向量数据库),兼顾企业级场景的准确性、稳定性和可扩展性。
一、前置条件(必做)
-
完成「文本向量化入库」基础环境搭建(参考之前教程):
-
Spring Boot 项目已集成 LangChain4j,且能正常向 Chroma/Milvus 入库向量;
-
Ollama 已部署嵌入模型 (nomic-embed-text)和大语言模型 (如 Qwen 2 7B,中文优化):
bash# 拉取 Qwen 2 7B 模型(私有化大模型核心) ollama pull qwen2:7b # 验证大模型可调用 curl http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{ "model": "qwen2:7b", "prompt": "测试回答" }' -
向量数据库(Chroma/Milvus)已有入库的文本向量(如企业知识库片段)。
-
-
补充 Maven 依赖(大模型调用核心):
xml<!-- LangChain4j Ollama 聊天模型(调用本地大模型) --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-ollama</artifactId> <version>0.35.0</version> </dependency> <!-- LangChain4j 记忆模块(多轮对话可选) --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-memory</artifactId> <version>0.35.0</version> </dependency> <!-- Redis 记忆持久化(可选,多轮对话) --> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-redis</artifactId> <version>0.35.0</version> </dependency>
二、核心配置(大模型 + 记忆模块)
在原有 RagConfig.java 基础上,新增大模型 Bean 和对话记忆 Bean(可选,支撑多轮对话)。
1. 补充 application.yml 配置
yaml
rag:
# 原有配置(嵌入模型、向量库)不变
ollama:
base-url: http://localhost:11434
embedding-model-name: nomic-embed-text
chat-model-name: qwen2:7b # 新增:本地大模型名称
temperature: 0.3 # 生成温度(0-1,越低越精准)
max-tokens: 2048 # 最大生成长度
# 记忆模块配置(可选)
memory:
redis:
host: localhost
port: 6379
ttl: 3600 # 对话记忆过期时间(秒)
2. 扩展配置类(RagConfig.java)
java
package com.rag.demo.config;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.memory.redis.RedisChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.ollama.OllamaChatModel;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class RagConfig {
// ========== 原有 Bean(嵌入模型、向量存储)不变 ==========
// ========== 新增:本地大模型 Bean(核心) ==========
@Value("${rag.ollama.base-url}")
private String ollamaBaseUrl;
@Value("${rag.ollama.chat-model-name}")
private String chatModelName;
@Value("${rag.ollama.temperature}")
private double temperature;
@Value("${rag.ollama.max-tokens}")
private int maxTokens;
@Bean
public ChatLanguageModel ollamaChatModel() {
return OllamaChatModel.builder()
.baseUrl(ollamaBaseUrl)
.modelName(chatModelName)
.temperature(temperature) // 降低随机性,提升回答准确性
.maxTokens(maxTokens) // 限制生成长度,避免冗余
.timeout(Duration.ofMinutes(3)) // 超时时间(大模型推理可能较慢)
.build();
}
// ========== 新增:对话记忆 Bean(可选,多轮对话) ==========
@Value("${rag.memory.redis.host}")
private String redisHost;
@Value("${rag.memory.redis.port}")
private int redisPort;
@Value("${rag.memory.redis.ttl}")
private long ttlSeconds;
// 轻量记忆(内存版,适合测试)
@Bean
public ChatMemory messageWindowChatMemory() {
// 保留最近5轮对话,避免上下文过长
return MessageWindowChatMemory.withMaxMessages(5);
}
// 持久化记忆(Redis版,适合生产,注释掉内存版启用)
/*
@Bean
public ChatMemory redisChatMemory() {
return RedisChatMemory.builder()
.redisHost(redisHost)
.redisPort(redisPort)
.ttl(Duration.ofSeconds(ttlSeconds))
.build();
}
*/
}
三、核心服务开发(RAG 全流程)
创建 RagAnswerService,整合「检索 → Prompt 增强 → 大模型生成」核心逻辑,支持单轮/多轮对话。
1. 服务接口(RagAnswerService.java)
java
package com.rag.demo.service;
import java.util.Map;
public interface RagAnswerService {
/**
* 单轮 RAG 问答(核心)
* @param userId 用户ID(用于记忆区分,多轮对话必填)
* @param question 用户问题
* @return 回答结果(包含回答文本、检索来源、相似度得分)
*/
Map<String, Object> ragAnswer(String userId, String question);
/**
* 清空用户对话记忆(可选)
* @param userId 用户ID
*/
void clearMemory(String userId);
}
2. 服务实现类(核心逻辑)
java
package com.rag.demo.service;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.embedding.Embedding;
import dev.langchain4j.data.segment.TextSegment;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.embedding.EmbeddingModel;
import dev.langchain4j.model.input.Prompt;
import dev.langchain4j.model.input.PromptTemplate;
import dev.langchain4j.store.embedding.EmbeddingMatch;
import dev.langchain4j.store.embedding.EmbeddingStore;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
@RequiredArgsConstructor
public class RagAnswerServiceImpl implements RagAnswerService {
// 注入核心 Bean
private final EmbeddingModel embeddingModel; // 向量化模型
private final EmbeddingStore<TextSegment> embeddingStore; // 向量库
private final ChatLanguageModel chatModel; // 本地大模型
private final ChatMemory chatMemory; // 对话记忆(可选)
// 企业级 Prompt 模板(核心:约束大模型仅基于检索结果回答)
private static final String PROMPT_TEMPLATE = """
角色:你是企业专属的智能问答助手,仅能基于提供的参考资料回答用户问题。
参考资料:
{reference_documents}
严格遵守以下规则:
1. 必须完全基于参考资料回答,禁止编造任何未提及的信息;
2. 若参考资料中无相关内容,仅回复「暂无相关信息」,不添加额外内容;
3. 回答语言简洁、专业,符合企业话术规范,使用中文;
4. 涉及敏感信息(如手机号、姓名)自动脱敏;
5. 回答长度控制在500字以内。
用户问题:{user_question}
""";
// 检索参数
private static final int TOP_K = 5; // 检索最相似的5个片段
private static final double SIMILARITY_THRESHOLD = 0.5; // 相似度阈值(低于则视为无相关内容)
@Override
public Map<String, Object> ragAnswer(String userId, String question) {
try {
// ========== 步骤1:语义检索(从向量库获取相关片段) ==========
// 1.1 问题向量化
Embedding questionEmbedding = embeddingModel.embed(question).content();
// 1.2 向量库检索相似片段
List<EmbeddingMatch<TextSegment>> matches = embeddingStore.findRelevant(questionEmbedding, TOP_K);
// 1.3 过滤低相似度片段,提取有效参考资料
List<EmbeddingMatch<TextSegment>> validMatches = matches.stream()
.filter(match -> match.score() >= SIMILARITY_THRESHOLD)
.collect(Collectors.toList());
log.info("检索到有效片段数:{},平均相似度:{}",
validMatches.size(),
validMatches.stream().mapToDouble(EmbeddingMatch::score).average().orElse(0));
// 1.4 拼接参考资料文本(带相似度和元数据,便于溯源)
String referenceDocuments = validMatches.stream()
.map(match -> String.format(
"【相似度:%.2f】%s(元数据:%s)",
match.score(),
match.embeddedItem().text(),
match.embeddedItem().metadata()
))
.collect(Collectors.joining("\n\n"));
// 无有效检索结果时的兜底
if (referenceDocuments.isEmpty()) {
return Map.of(
"code", 200,
"answer", "暂无相关信息",
"sources", "",
"similarityScore", 0
);
}
// ========== 步骤2:Prompt 增强(拼接问题+参考资料) ==========
PromptTemplate promptTemplate = PromptTemplate.from(PROMPT_TEMPLATE);
Prompt prompt = promptTemplate.apply(Map.of(
"reference_documents", referenceDocuments,
"user_question", question
));
// ========== 步骤3:调用大模型生成回答(支持多轮对话) ==========
AiMessage aiMessage;
if (chatMemory != null) {
// 多轮对话:从记忆中加载历史,拼接新问题
List<ChatMessage> historyMessages = chatMemory.getMessages(userId);
aiMessage = chatModel.generate(
ChatMessage.combine(historyMessages, UserMessage.from(prompt.text()))
).content();
// 保存本轮对话到记忆
chatMemory.add(userId, UserMessage.from(question), aiMessage);
} else {
// 单轮对话:直接调用
aiMessage = chatModel.generate(prompt.toUserMessage()).content();
}
// ========== 步骤4:组装返回结果(含溯源信息) ==========
return Map.of(
"code", 200,
"answer", aiMessage.text(), // 大模型回答
"sources", referenceDocuments, // 检索来源(便于企业溯源)
"similarityScore", validMatches.stream().mapToDouble(EmbeddingMatch::score).average().orElse(0) // 平均相似度
);
} catch (Exception e) {
log.error("RAG 问答失败", e);
return Map.of(
"code", 500,
"answer", "系统异常,请稍后重试",
"sources", "",
"similarityScore", 0
);
}
}
@Override
public void clearMemory(String userId) {
if (chatMemory != null) {
chatMemory.clear(userId);
log.info("清空用户 {} 对话记忆", userId);
}
}
}
四、测试接口开发(Controller)
创建 RagAnswerController,提供 HTTP 接口测试完整 RAG 流程。
java
package com.rag.demo.controller;
import com.rag.demo.service.RagAnswerService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/rag")
@RequiredArgsConstructor
public class RagAnswerController {
private final RagAnswerService ragAnswerService;
/**
* RAG 核心问答接口
* 请求示例:
* POST /api/rag/answer
* {
* "userId": "user001",
* "question": "LangChain4j 适合开发什么类型的应用?"
* }
*/
@PostMapping("/answer")
public Map<String, Object> ragAnswer(@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String question = request.get("question");
if (userId == null || question == null) {
return Map.of("code", 400, "msg", "userId和question不能为空");
}
return ragAnswerService.ragAnswer(userId, question);
}
/**
* 清空用户对话记忆
* 请求示例:DELETE /api/rag/memory?userId=user001
*/
@DeleteMapping("/memory")
public Map<String, Object> clearMemory(@RequestParam String userId) {
ragAnswerService.clearMemory(userId);
return Map.of("code", 200, "msg", "清空记忆成功");
}
}
五、启动与测试验证
1. 启动应用
确保以下服务均已启动:
- Ollama(嵌入模型 + Qwen 2 大模型);
- Chroma/Milvus(向量数据库,已有入库数据);
- Redis(可选,多轮对话记忆);
- Spring Boot 应用(指定 Profile:
spring.profiles.active=chroma或milvus)。
2. 测试步骤
(1)单轮问答测试
使用 Postman/Curl 发送 POST 请求:
bash
curl -X POST http://localhost:8080/api/rag/answer \
-H "Content-Type: application/json" \
-d '{
"userId": "user001",
"question": "LangChain4j 适合开发什么类型的应用?"
}'
响应示例(核心包含回答、检索来源、相似度):
json
{
"code": 200,
"answer": "LangChain4j 是JVM生态的LLM应用开发框架,适合开发私有化RAG系统、企业级AI应用,支持Java和Kotlin语言,可集成Ollama、Milvus等工具。",
"sources": "【相似度:0.98】LangChain4j 是JVM生态的LLM应用开发框架,支持Java和Kotlin语言,可快速构建私有化RAG系统。它集成了Ollama、Milvus等工具,适合企业级AI应用开发。(元数据:doc-001|langchain4j-knowledge)",
"similarityScore": 0.98
}
(2)多轮对话测试(可选)
继续发送追问请求(相同 userId):
bash
curl -X POST http://localhost:8080/api/rag/answer \
-H "Content-Type: application/json" \
-d '{
"userId": "user001",
"question": "它如何集成Ollama?"
}'
响应示例(大模型结合历史对话和检索结果回答):
json
{
"code": 200,
"answer": "LangChain4j 提供了专门的Ollama集成模块,通过配置Ollama的Base URL和模型名称,可快速初始化OllamaChatModel和OllamaEmbeddingModel,实现本地大模型调用和文本向量化,无需依赖云端服务。",
"sources": "【相似度:0.95】LangChain4j 集成了Ollama、Milvus等工具,通过OllamaEmbeddingModel调用本地嵌入模型,OllamaChatModel调用本地大模型,适配私有化部署。(元数据:doc-001|langchain4j-knowledge)",
"similarityScore": 0.95
}
六、企业级优化(核心)
1. 检索精度优化
-
混合检索 :结合向量检索(语义)+ BM25 检索(关键词),提升召回率:
java// 引入 BM25 检索器 import dev.langchain4j.retriever.bm25.Bm25Retriever; // 初始化混合检索器 Bm25Retriever bm25Retriever = Bm25Retriever.from(textSegments); HybridRetriever hybridRetriever = HybridRetriever.builder() .vectorRetriever(embeddingStore.asRetriever()) .bm25Retriever(bm25Retriever) .build(); -
重排序 :用 Cross-BERT 对检索结果二次排序,过滤低相关片段:
xml<dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-reranker-cross-encoder</artifactId> <version>0.35.0</version> </dependency>javaCrossEncoderReranker reranker = CrossEncoderReranker.builder().build(); List<TextSegment> rerankedSegments = reranker.rerank(question, retrievedSegments);
2. 性能优化
-
缓存策略 :对高频问题的检索结果+回答缓存(Redis),避免重复推理:
java// 伪代码:缓存逻辑 String cacheKey = "rag:" + userId + ":" + MD5(question); String cachedAnswer = redisTemplate.opsForValue().get(cacheKey); if (cachedAnswer != null) { return Map.of("code", 200, "answer", cachedAnswer); } // 无缓存时执行RAG流程,然后存入缓存 redisTemplate.opsForValue().set(cacheKey, aiMessage.text(), Duration.ofMinutes(30)); -
异步处理 :用线程池异步执行检索+生成,提升接口响应速度:
java@Async("ragExecutor") public CompletableFuture<Map<String, Object>> ragAnswerAsync(String userId, String question) { return CompletableFuture.supplyAsync(() -> ragAnswer(userId, question)); }
3. 安全与合规
-
权限过滤 :检索时根据用户角色过滤向量库数据(如售后人员仅能检索售后知识库):
java// 检索结果过滤:仅保留元数据中role匹配当前用户角色的片段 List<EmbeddingMatch<TextSegment>> filteredMatches = validMatches.stream() .filter(match -> match.embeddedItem().metadata().get("role").equals(currentUserRole)) .collect(Collectors.toList()); -
敏感词过滤 :对大模型回答进行敏感词检测,避免违规内容:
java// 伪代码:敏感词过滤 String filteredAnswer = sensitiveWordFilter.filter(aiMessage.text());
4. 监控与可观测性
- 记录关键指标:检索耗时、大模型推理耗时、相似度得分,接入 Prometheus/Grafana;
- 日志记录:记录用户问题、检索来源、回答内容(脱敏后),便于故障排查和效果分析。
七、常见问题排查
1. 大模型回答"暂无相关信息"但向量库有数据
- 原因:相似度阈值设置过高、嵌入模型维度不匹配、检索参数
topK过小; - 解决:降低相似度阈值(如 0.3)、核对嵌入模型维度(如 nomic-embed-text 为 768)、调大
topK至 10。
2. 大模型回答包含幻觉(编造信息)
- 原因:Prompt 约束不足、检索结果为空但模型仍生成内容;
- 解决:强化 Prompt 规则(如"无参考资料时仅回复暂无相关信息")、检索结果为空时直接返回兜底回答,不调用大模型。
3. 多轮对话上下文丢失
- 原因:ChatMemory 未绑定 userId、记忆窗口过小;
- 解决:确保每个用户的对话记忆通过 userId 区分,调大
MessageWindowChatMemory的最大消息数(如 10)。
总结
LangChain4j + Spring Boot 实现大模型与向量数据库协同的核心逻辑是:
- 检索:将用户问题向量化,从向量库获取语义相似的知识片段;
- 增强:用结构化 Prompt 约束大模型仅基于检索结果回答;
- 生成:调用本地大模型生成回答,结合对话记忆实现多轮交互。
该方案完全私有化部署,数据不出内网,适配企业级 RAG 场景的准确性、安全性和性能要求,可直接作为智能客服、内部知识库问答等场景的核心架构。