一、RAG 介紹
RAG(檢索增強(qiáng)生成,Retrieval-Augmented Generation)是一種通過在生成答案前先從外部知識庫檢索相關(guān)信息,來優(yōu)化大語言模型(LLM)輸出的技術(shù)。它解決了模型知識滯后、產(chǎn)生幻覺(一本正經(jīng)地胡說八道)以及缺乏私有領(lǐng)域數(shù)據(jù)的問題,能以低成本實(shí)現(xiàn)更準(zhǔn)確、實(shí)時(shí)、可信的內(nèi)容生成。
RAG 并不是重新訓(xùn)練模型,而是在模型回答問題時(shí),給它外掛一個(gè)“圖書館”。流程如下:
- 檢索(Retrieval): 當(dāng)用戶輸入查詢時(shí),系統(tǒng)會搜索向量數(shù)據(jù)庫或其他知識庫,查找相關(guān)的文檔片段。
- 增強(qiáng)(Augmented): 系統(tǒng)將檢索到的相關(guān)知識與用戶的問題組合成一個(gè)富含上下文的提示(Prompt)。
- 生成(Generation): 大模型根據(jù)這個(gè)提示生成最終答案,確保內(nèi)容有事實(shí)依據(jù)。
1.1 為什么需要 RAG?
- 緩解模型幻覺: 通過引用外部權(quán)威知識源,大幅減少AI虛構(gòu)事實(shí)的概率。
- 實(shí)時(shí)與專有知識: 模型不需要重新訓(xùn)練就能獲取最新信息,或使用企業(yè)內(nèi)部私有數(shù)據(jù)。
- 高性價(jià)比: 相比于對大模型進(jìn)行微調(diào)(Fine-tuning),RAG 更快、更經(jīng)濟(jì),適合快速調(diào)整模型以適應(yīng)特定業(yè)務(wù)場景。
- 可解釋性: 生成的回答通??梢愿綆畔碓?,便于用戶核查。
1.2 典型應(yīng)用場景
- 企業(yè)內(nèi)部知識庫問答: 基于公司內(nèi)部文檔、規(guī)章制度回答員工或客戶問題。
- 專業(yè)客服機(jī)器人: 提供準(zhǔn)確的產(chǎn)品指南、售后咨詢。
- 專業(yè)研究與檢索: 如醫(yī)療、法律領(lǐng)域,輔助查找案例或論文證據(jù)。
總之,RAG 是將通用大模型連接到特定、實(shí)時(shí)和私有數(shù)據(jù)的橋梁,使其在專業(yè)領(lǐng)域更可靠、更實(shí)用。
二、向量數(shù)據(jù)庫
向量數(shù)據(jù)庫(Vector Database)是一種專為存儲、索引和查詢高維向量嵌入(Embedding)而設(shè)計(jì)的數(shù)據(jù)庫,主要用于處理文本、圖像、音視頻等非結(jié)構(gòu)化數(shù)據(jù)的語義相似性搜索。它基于“近似最近鄰搜索”(ANN)技術(shù),幫助大型語言模型(LLM)理解上下文,實(shí)現(xiàn)快速檢索、推薦系統(tǒng)及知識庫擴(kuò)充(RAG)。
2.1 核心概念
- 向量嵌入 (Embedding):機(jī)器學(xué)習(xí)模型將圖像、文本等非結(jié)構(gòu)化數(shù)據(jù)轉(zhuǎn)化為數(shù)字向量,其中語義相似的數(shù)據(jù)在向量空間中距離更近。
- 相似性搜索 (Similarity Search):不同于傳統(tǒng)數(shù)據(jù)庫的精確匹配,向量數(shù)據(jù)庫根據(jù)計(jì)算后的相似度(如余弦相似度、歐氏距離)找出最接近的結(jié)果。
- 近似最近鄰 (ANN) 算法:在高維數(shù)據(jù)中快速查找,能在毫秒級從百萬級數(shù)據(jù)中找到相似項(xiàng)。
2.2 向量數(shù)據(jù)庫與傳統(tǒng)數(shù)據(jù)庫的區(qū)別
| 特性 | 傳統(tǒng)數(shù)據(jù)庫 (Relational DB) | 向量數(shù)據(jù)庫 (Vector DB) |
|---|---|---|
| 數(shù)據(jù)類型 | 結(jié)構(gòu)化數(shù)據(jù) (行/列) | 非結(jié)構(gòu)化數(shù)據(jù)轉(zhuǎn)化為高維向量 |
| 查詢方式 | 精確查找 (SQL, 關(guān)鍵詞) | 近似搜索 (基于相似度/語義) |
| 應(yīng)用場景 | 事務(wù)處理、業(yè)務(wù)記錄 | AI 語義搜索、推薦、知識檢索 |
| 查找準(zhǔn)確性 | 100% 匹配 | 前 K 個(gè)最相似結(jié)果 (Top-K) |
2.3 主要應(yīng)用場景
- 檢索增強(qiáng)生成 (RAG):將企業(yè)自有知識庫轉(zhuǎn)化為向量存儲,LLM 根據(jù)查詢檢索相關(guān)文檔以生成更準(zhǔn)確的回答,減少“幻覺”。
- 語義搜索引擎:基于含義而非關(guān)鍵字匹配(例如:搜索“手機(jī)”能檢索到“移動設(shè)備”)。
- 推薦系統(tǒng):根據(jù)用戶畫像向量和商品向量的距離,推薦相似內(nèi)容。
- 多模態(tài)搜索:通過文字查找圖片,或查找相似的視頻片段。
2.4 主流向量數(shù)據(jù)庫
- 開源/專用:Milvus (螞蟻/Zilliz), Pinecone, Qdrant, Weaviate, Faiss (Facebook)。
- 集成式:Elasticsearch, Redis, Tencent Cloud VectorDB, PostgreSQL (pgvector)。
向量數(shù)據(jù)庫在生成式 AI 時(shí)代是連接向量模型與實(shí)際應(yīng)用數(shù)據(jù)的關(guān)鍵橋梁,它使得機(jī)器不僅能存儲數(shù)據(jù),還能理解數(shù)據(jù)。
三、獲取API Key
要調(diào)用阿里千問大模型API,首先需要完成阿里云賬號的注冊、百煉服務(wù)開通及API Key獲取。
3.1 注冊阿里云賬號
若你尚未擁有阿里云賬號,需先前往阿里云注冊頁面完成注冊,建議使用企業(yè)或個(gè)人常用手機(jī)號/郵箱注冊,方便后續(xù)賬號管理與服務(wù)開通。
3.2 開通阿里云百煉服務(wù)
注冊并登錄阿里云賬號后,前往阿里云百煉大模型服務(wù)平臺,按照頁面指引開通百煉服務(wù)。開通過程中需確認(rèn)服務(wù)協(xié)議,無需額外付費(fèi)即可享受新人專屬免費(fèi)額度。
3.3 獲取API Key
API Key是調(diào)用千問大模型的身份憑證,獲取步驟如下:
- 登錄阿里云百煉控制臺,前往密鑰管理頁面;
- 點(diǎn)擊「創(chuàng)建API Key」按鈕,系統(tǒng)會自動生成API Key;
- 保存好生成的API Key,后續(xù)后端代碼中需使用該密鑰進(jìn)行身份驗(yàn)證,建議妥善保管,避免泄露。
3.4 模型與計(jì)費(fèi)說明
阿里云百煉不僅支持阿里千問系列大模型(如qwen-plus、qwen-max等),還兼容DeepSeek、Kimi、GLM、MiniMax等第三方知名大模型,可根據(jù)業(yè)務(wù)需求[靈活選擇]
3.5 計(jì)費(fèi)規(guī)則
首次開通百煉服務(wù)時(shí),平臺會自動發(fā)放各模型的新人專屬免費(fèi)額度,有效期通常為30~90天;免費(fèi)額度耗盡或過期后,繼續(xù)使用模型推理服務(wù)將按實(shí)際調(diào)用量計(jì)費(fèi),具體計(jì)費(fèi)標(biāo)準(zhǔn)可參考阿里云百煉官方定價(jià)文檔。
四、安裝 Milvus 向量數(shù)據(jù)庫
Milvus 提供三種部署模式:
- Milvus Lite 是一個(gè) Python 庫,可以輕松集成到您的應(yīng)用程序中。作為 Milvus 的輕量級版本,它非常適合快速原型設(shè)計(jì)或在資源有限的邊緣設(shè)備上運(yùn)行。
- Milvus Standalone 是單機(jī)服務(wù)器部署,所有組件都捆綁到一個(gè) Docker 鏡像中,部署方便。
- Milvus Distributed 可以部署在 Kubernetes 集群上,采用云原生架構(gòu),專為數(shù)十億甚至更大的場景而設(shè)計(jì)。此體系結(jié)構(gòu)可確保關(guān)鍵組件的冗余。
本文安裝的是 Milvus Lite,參考文章本地運(yùn)行 Milvus Lite
五、工程搭建:基于 Spring Boot 的后端開發(fā)
5.1 創(chuàng)建Maven工程
使用IntelliJ IDEA創(chuàng)建一個(gè)Maven工程,完成工程初始化。
5.2 引入核心 pom 依賴
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.13</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.devpotato</groupId>
<artifactId>chat-service</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
</configuration>
</plugin>
</plugins>
</build>
<packaging>jar</packaging>
<name>ChatService</name>
<url>http://maven.apache.org</url>
<properties>
<java.version>17</java.version>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.15.0</jackson.version>
<spring-ai.version>1.0.0-M1</spring-ai.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- spring-boot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI -->
<!-- Source: https://mvnrepository.com/artifact/org.springframework.ai/spring-ai-ollama -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama</artifactId>
<version>${spring-ai.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
</dependency>
<!-- PDF 解析 -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>2.0.30</version>
</dependency>
<!-- Milvus Java SDK -->
<dependency>
<groupId>io.milvus</groupId>
<artifactId>milvus-sdk-java</artifactId>
<version>2.4.0</version>
</dependency>
<!-- jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- openai -->
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>4.28.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java-client-okhttp</artifactId>
<version>4.28.0</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>2.3.20-RC3</version>
<scope>compile</scope>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>2.3.20-RC3</version>
<scope>compile</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
</project>
5.3 工程配置文件
在src/main/resources目錄下創(chuàng)建application.yml文件
server:
port: 8080
# Milvus 配置
milvus:
uri: http://localhost:19530
token: root:Milvus
collection-name: pdf_rag_collection
vector-dim: 1024 # Embedding向量維度
5.4 解決跨域問題
由于前端頁面與后端服務(wù)可能存在跨域(CORS)問題,導(dǎo)致前端無法正常調(diào)用后端API,因此需要添加跨域過濾器。創(chuàng)建MyFilter類,實(shí)現(xiàn)Filter接口,具體代碼如下:
import org.springframework.stereotype.Component;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class MyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 解決Chrome等瀏覽器訪問本地服務(wù)的跨域問題
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 允許所有域名跨域訪問(生產(chǎn)環(huán)境建議指定具體域名,提升安全性)
httpResponse.setHeader("Access-Control-Allow-Origin", "*");
// 允許的請求方式
httpResponse.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
// 允許的請求頭
httpResponse.setHeader("Access-Control-Allow-Headers", "Content-Type");
// 繼續(xù)執(zhí)行過濾鏈
chain.doFilter(request, response);
}
}
5.5 PDF 讀取工具類
提取 PDF 純文本內(nèi)容,自動去除空白行、格式冗余內(nèi)容
package org.devpotato.rag;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import java.io.File;
import java.io.IOException;
public class PdfReaderUtil {
/**
* 讀取PDF文件為純文本
*/
public static String readPdfToString(String pdfPath) throws IOException {
try (PDDocument document = PDDocument.load(new File(pdfPath))) {
PDFTextStripper stripper = new PDFTextStripper();
// 按順序讀取文本
stripper.setSortByPosition(true);
return stripper.getText(document).trim();
}
}
}
5.6 文本分塊工具類
按固定長度分塊(512 字符 / 塊),保留語義完整性
package org.devpotato.rag;
import java.util.ArrayList;
import java.util.List;
public class TextSplitterUtil {
/**
* 文本分塊(固定長度,無重疊)
*
* @param text 原始文本
* @param chunkSize 每塊大小
* @return 分塊后的文本列表
*/
public static List<String> splitText(String text, int chunkSize) {
List<String> chunks = new ArrayList<>();
int length = text.length();
int start = 0;
while (start < length) {
int end = Math.min(start + chunkSize, length);
chunks.add(text.substring(start, end).trim());
start = end;
}
return chunks;
}
// 默認(rèn)分塊大小:512字符
public static List<String> splitText(String text) {
return splitText(text, 512);
}
}
5.7 千問 API 工具類
實(shí)現(xiàn)文本向量化和大模型問答兩個(gè)核心功能
package org.devpotato.rag;
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.chat.completions.ChatCompletion;
import com.openai.models.chat.completions.ChatCompletionCreateParams;
import com.openai.models.chat.completions.ChatCompletionMessage;
import com.openai.models.embeddings.CreateEmbeddingResponse;
import com.openai.models.embeddings.Embedding;
import com.openai.models.embeddings.EmbeddingCreateParams;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
@Component
public class QianWenUtil {
private static final String API_KEY = "xxx";
private static final String BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
/**
* 使用的模型名稱
*/
private static final String LLM_MODEL = "qwen-plus";
/**
* 系統(tǒng)提示詞
*/
private static final String SYSTEM_PROMPT = "作為一個(gè)專業(yè)的電商售后支持客服,你具備深厚的問題解決能力。請針對用戶提出的問題,提供詳細(xì)的解答步驟或有效的解決方案,并且考慮到用戶的水平可能有所不同,請盡可能地簡化語言。";
@Value("${milvus.vector-dim}")
private int vectorDim;
OpenAIClient client = OpenAIOkHttpClient.builder()
.apiKey(API_KEY)
.baseUrl(BASE_URL)
.build();
public List<Float> textToEmbedding(String text) {
// 創(chuàng)建向量化請求參數(shù)
EmbeddingCreateParams params = EmbeddingCreateParams.builder()
.model("text-embedding-v4")
.input(EmbeddingCreateParams.Input.ofString(text))
// 指定向量維度(僅 text-embedding-v3及 text-embedding-v4支持該參數(shù))
.dimensions(vectorDim)
.build();
try {
// 發(fā)送請求并獲取響應(yīng)
CreateEmbeddingResponse response = client.embeddings().create(params);
System.out.println(response);
List<Embedding> embeddingList = response.data();
List<Float> vector = new ArrayList<>();
for (Embedding embedding : embeddingList) {
vector.addAll(embedding.embedding());
}
return vector;
} catch (Exception e) {
System.err.println("請求出錯(cuò),請查看錯(cuò)誤碼對照網(wǎng)頁:");
System.err.println("https://help.aliyun.com/zh/model-studio/faq-about-alibaba-cloud-model-studio?spm=a2c4g.11186623.help-menu-2400256.d_0_17_0.18733a66lTrcHv#1c38f58abfcml");
System.err.println("錯(cuò)誤詳情:" + e.getMessage());
e.printStackTrace();
}
return new ArrayList<>();
}
public String llmChat(String prompt) {
ChatCompletionCreateParams params = ChatCompletionCreateParams.builder()
.addSystemMessage(SYSTEM_PROMPT)
.addUserMessage(prompt)
.model(LLM_MODEL)
.build();
ChatCompletion chatCompletion = client.chat().completions().create(params);
ChatCompletionMessage completionMessage = chatCompletion.choices().get(0).message();
return completionMessage.content().orElse("");
}
}
5.8 Milvus 向量數(shù)據(jù)庫操作類
實(shí)現(xiàn)創(chuàng)建集合、插入向量、相似度檢索核心功能
package org.devpotato.rag;
import com.alibaba.fastjson.JSONObject;
import io.milvus.v2.client.ConnectConfig;
import io.milvus.v2.client.MilvusClientV2;
import io.milvus.v2.common.DataType;
import io.milvus.v2.common.IndexParam;
import io.milvus.v2.service.collection.request.AddFieldReq;
import io.milvus.v2.service.collection.request.CreateCollectionReq;
import io.milvus.v2.service.collection.request.GetLoadStateReq;
import io.milvus.v2.service.vector.request.InsertReq;
import io.milvus.v2.service.vector.request.SearchReq;
import io.milvus.v2.service.vector.response.InsertResp;
import io.milvus.v2.service.vector.response.SearchResp;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Component
public class MilvusUtil {
@Value("${milvus.uri}")
private String uri;
@Value("${milvus.token}")
private String token;
@Value("${milvus.collection-name}")
private String collectionName;
@Value("${milvus.vector-dim}")
private int vectorDim;
private MilvusClientV2 milvusClient;
/**
* 初始化 Milvus 客戶端
*/
@PostConstruct
public void init() {
// 1. Connect to Milvus server
ConnectConfig connectConfig = ConnectConfig.builder()
.uri(uri)
.token(token)
.build();
milvusClient = new MilvusClientV2(connectConfig);
createCollection();
}
/**
* 創(chuàng)建向量集合(表)
*/
private void createCollection() {
// 3. Create a collection in customized setup mode
// 3.1 Create schema
CreateCollectionReq.CollectionSchema schema = milvusClient.createSchema();
// 3.2 Add fields to schema
// 字段1:ID(主鍵)
schema.addField(AddFieldReq.builder()
.fieldName("id")
.dataType(DataType.Int64)
.isPrimaryKey(true)
.autoID(true)
.build());
// 字段2:文本內(nèi)容
schema.addField(AddFieldReq.builder()
.fieldName("text")
.dataType(DataType.VarChar)
.maxLength(2000)
.build());
// 字段3:向量
schema.addField(AddFieldReq.builder()
.fieldName("vector")
.dataType(DataType.FloatVector)
.dimension(vectorDim)
.build());
// 3.3 Prepare index parameters(主鍵字段無需手動創(chuàng)建索引,Milvus會自動處理)
IndexParam indexParamForVectorField = IndexParam.builder()
.fieldName("vector")
.indexType(IndexParam.IndexType.AUTOINDEX)
.metricType(IndexParam.MetricType.COSINE)
.build();
List<IndexParam> indexParams = new ArrayList<>();
indexParams.add(indexParamForVectorField);
// 3.4 Create a collection with schema and index parameters
CreateCollectionReq customizedSetupReq = CreateCollectionReq.builder()
.collectionName(collectionName)
.collectionSchema(schema)
.indexParams(indexParams)
.build();
milvusClient.createCollection(customizedSetupReq);
// 3.5 Get load state of the collection
GetLoadStateReq customSetupLoadStateReq1 = GetLoadStateReq.builder()
.collectionName(collectionName)
.build();
Boolean loaded = milvusClient.getLoadState(customSetupLoadStateReq1);
System.out.println(loaded);
}
/**
* 插入文本+向量到Milvus
*/
public void insertData(String text, List<Float> vector) {
List<JSONObject> data = new ArrayList<>();
JSONObject jsonObject = new JSONObject();
jsonObject.put("text", text);
jsonObject.put("vector", vector);
data.add(jsonObject);
InsertReq insertReq = InsertReq.builder()
.collectionName(collectionName)
.data(data)
.build();
InsertResp insertResp = milvusClient.insert(insertReq);
System.out.println(insertResp);
}
/**
* 相似度檢索(返回最相似的3條文本)
*/
public List<String> searchSimilarText(List<Float> queryVector) {
SearchReq searchReq = SearchReq.builder()
.collectionName(collectionName)
.data(Collections.singletonList(queryVector))
.annsField("vector")
.topK(3)
.outputFields(Collections.singletonList("text")) // 使用輸出字段
.build();
SearchResp searchResp = milvusClient.search(searchReq);
List<List<SearchResp.SearchResult>> searchResults = searchResp.getSearchResults();
List<String> resultTextList = new ArrayList<>();
for (List<SearchResp.SearchResult> results : searchResults) {
System.out.println("TopK results:");
for (SearchResp.SearchResult result : results) {
System.out.println(result);
resultTextList.add(result.getEntity().get("text").toString());
}
}
return resultTextList;
}
}
5.9 RAG 核心服務(wù)類
整合PDF 讀取→分塊→向量化→入庫→檢索→問答全流程
package org.devpotato.rag;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
@RequiredArgsConstructor
public class RagService {
private final QianWenUtil qianWenUtil;
private final MilvusUtil milvusUtil;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 上傳PDF并向量化入庫(初始化數(shù)據(jù))
*/
public void uploadPdfAndInitData(String pdfPath) throws Exception {
// 1. 讀取PDF
String pdfText = PdfReaderUtil.readPdfToString(pdfPath);
// 2. 文本分塊
List<String> chunks = TextSplitterUtil.splitText(pdfText);
// 3. 逐塊向量化+存入Milvus
for (String chunk : chunks) {
List<Float> vector = qianWenUtil.textToEmbedding(chunk);
milvusUtil.insertData(chunk, vector);
}
}
/**
* RAG 問答核心邏輯
*/
public String ragChat(String question) throws Exception {
// 1. 用戶問題向量化
List<Float> queryVector = qianWenUtil.textToEmbedding(question);
// 2. 向量庫檢索相似文本
List<String> similarTexts = milvusUtil.searchSimilarText(queryVector);
// 3. 拼接提示詞(RAG核心)
String prompt = "基于以下文檔內(nèi)容回答問題,不要編造答案:\n" +
"文檔內(nèi)容:" + String.join("\n", similarTexts) +
"\n用戶問題:" + question;
// 4. 調(diào)用大模型生成答案
return qianWenUtil.llmChat(prompt);
}
}
5.10 核心對話控制器
對外提供PDF 上傳初始化和RAG 問答接口
package org.devpotato.rag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/chat")
@RequiredArgsConstructor
public class RagController {
private final RagService ragService;
/**
* 初始化PDF數(shù)據(jù)(調(diào)用一次即可)
* 參數(shù):PDF文件本地路徑,例如 D:/docs/spring-boot-doc.pdf
*/
@GetMapping("/init")
public String initPdfData(@RequestParam("path") String pdfPath) {
try {
ragService.uploadPdfAndInitData(pdfPath);
return "PDF初始化完成,文本已向量化存入Milvus!";
} catch (Exception e) {
return "初始化失?。? + e.getMessage();
}
}
/**
* RAG 問答接口
*/
@RequestMapping(path = "", method = RequestMethod.POST)
public String chat(@RequestBody Map<String, Object> map) throws Exception {
String message = map.get("message").toString();
return ragService.ragChat(message);
}
}
5.11 啟動類開發(fā)
package org.devpotato;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration;
@SpringBootApplication(exclude = {
ContextFunctionCatalogAutoConfiguration.class
})
public class StartServer {
public static void main(String[] args) {
SpringApplication.run(StartServer.class, args);
System.out.println(">>> start");
}
}
六、前端頁面搭建(簡易版)
前端采用簡單的HTML+JavaScript搭建聊天界面,實(shí)現(xiàn)用戶輸入提問、展示客服回復(fù)的功能。創(chuàng)建index.html文件,代碼如下(可直接在瀏覽器中打開使用):
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>電商客服中心</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
padding: 20px;
}
.chat-container {
width: 100%;
max-width: 800px;
height: 80vh;
background-color: #fff;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.chat-header {
background-color: #409eff;
color: #fff;
padding: 15px 20px;
display: flex;
align-items: center;
gap: 10px;
}
.chat-header img {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #fff;
}
.chat-header h2 {
font-size: 18px;
font-weight: 600;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f9f9f9;
}
.message {
margin-bottom: 15px;
max-width: 70%;
display: flex;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
margin-left: auto;
flex-direction: row-reverse;
}
.message-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
margin: 0 8px;
flex-shrink: 0;
}
.user-message .message-content {
background-color: #409eff;
color: #fff;
border-radius: 10px 10px 0 10px;
}
.bot-message .message-content {
background-color: #fff;
color: #333;
border-radius: 10px 10px 10px 0;
border: 1px solid #eee;
}
.message-content {
padding: 10px 15px;
word-wrap: break-word;
line-height: 1.4;
}
/* 快捷按鈕區(qū)域樣式 */
.quick-buttons {
padding: 10px 15px;
border-top: 1px solid #eee;
background-color: #fafafa;
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.quick-button {
padding: 6px 15px;
background-color: #e8f4ff;
color: #409eff;
border: 1px solid #d1e9ff;
border-radius: 20px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.quick-button:hover {
background-color: #409eff;
color: #fff;
border-color: #409eff;
}
.chat-input {
display: flex;
padding: 15px;
border-top: 1px solid #eee;
background-color: #fff;
}
#message-input {
flex: 1;
padding: 12px 15px;
border: 1px solid #ddd;
border-radius: 25px;
outline: none;
font-size: 14px;
resize: none;
height: 45px;
max-height: 120px;
}
#message-input:focus {
border-color: #409eff;
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}
#send-button {
margin-left: 10px;
padding: 0 20px;
background-color: #409eff;
color: #fff;
border: none;
border-radius: 25px;
cursor: pointer;
font-size: 14px;
transition: background-color 0.2s;
}
#send-button:hover {
background-color: #337ecc;
}
#send-button:disabled {
background-color: #b3d8ff;
cursor: not-allowed;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid rgba(255,255,255,.3);
border-radius: 50%;
border-top-color: white;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-hint {
text-align: center;
color: #999;
padding: 50px 0;
font-size: 14px;
}
</style>
</head>
<body>
<div class="chat-container">
<div class="chat-header">
<img src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjAiIGN5PSIyMCIgcj0iMjAiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTE1IDI1QzE1IDI3LjcxIDE2Ljk5IDI5IDE5IDI5QzIxLjAxIDI5IDIzIDI3LjcxIDIzIDI1QzIzIDIyLjc5IDIxLjAxIDIxIDE5IDIxQzE2Ljk5IDIxIDE1IDIyLjc5IDE1IDI1WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==" alt="客服圖標(biāo)">
<h2>在線客服中心</h2>
</div>
<div class="chat-messages" id="chat-messages">
<div class="empty-hint" id="empty-hint">歡迎咨詢,我是您的專屬客服??</div>
</div>
<!-- 新增快捷按鈕區(qū)域 -->
<div class="quick-buttons" id="quick-buttons">
<div class="quick-button" onclick="sendQuickMessage('查看訂單')">查看訂單</div>
<div class="quick-button" onclick="sendQuickMessage('查看物流')">查看物流</div>
<div class="quick-button" onclick="sendQuickMessage('申請退款')">申請退款</div>
<div class="quick-button" onclick="sendQuickMessage('修改收貨地址')">修改收貨地址</div>
<div class="quick-button" onclick="sendQuickMessage('商品質(zhì)量問題')">商品質(zhì)量問題</div>
</div>
<div class="chat-input">
<textarea id="message-input" placeholder="請輸入您想咨詢的問題..." onkeydown="if(event.keyCode===13&&!event.shiftKey){event.preventDefault();sendMessage();}"></textarea>
<button id="send-button" onclick="sendMessage()">發(fā)送</button>
</div>
</div>
<script>
// 獲取DOM元素
const messageInput = document.getElementById('message-input');
const sendButton = document.getElementById('send-button');
const chatMessages = document.getElementById('chat-messages');
const emptyHint = document.getElementById('empty-hint');
// 頭像URL(使用base64編碼的SVG,也可以替換為實(shí)際圖片URL)
const BOT_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiM0MDllZmYiLz4KPHBhdGggZD0iTTEzIDIyQzEzIDI0LjIxIDE0Ljk5IDI2IDE3IDI2QzE5LjAxIDI2IDIxIDI0LjIxIDIxIDIyQzIxIDE5Ljc5IDE5LjAxIDE4IDE3IDE4QzE0Ljk5IDE4IDEzIDE5Ljc5IDEzIDIyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';
const USER_AVATAR = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzYiIGhlaWdodD0iMzYiIHZpZXdCb3g9IjAgMCAzNiAzNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMTgiIGN5PSIxOCIgcj0iMTgiIGZpbGw9IiNmZmYwMDAiLz4KPHBhdGggZD0iTTEyIDIwQzEyIDIyLjcxIDEzLjk5IDI1IDE2IDI1QzE4LjAxIDI1IDIwIDIyLjcxIDIwIDIwQzIwIDE3Ljc5IDE4LjAxIDE1IDE2IDE1QzEzLjk5IDE1IDEyIDE3Ljc5IDEyIDIwWiIgZmlsbD0id2hpdGUiLz4KPHBhdGggZD0iTTIwIDI5QzIwIDI5IDE3IDMwIDE3IDMwQzE0IDMwIDEyIDI5IDEyIDI5QzEyIDI5IDEyIDI3IDEyIDI3QzEyIDI3IDE0IDI2IDE2IDI2QzE4IDI2IDIwIDI3IDIwIDI3QzIwIDI3IDIwIDI5IDIwIDI5WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==';
// 自動調(diào)整輸入框高度
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
});
// 快捷消息發(fā)送函數(shù)
function sendQuickMessage(message) {
// 將快捷消息填入輸入框
messageInput.value = message;
messageInput.style.height = 'auto';
messageInput.style.height = (messageInput.scrollHeight > 45 ? messageInput.scrollHeight : 45) + 'px';
// 自動發(fā)送該消息
sendMessage();
}
// 發(fā)送消息函數(shù)
async function sendMessage() {
const message = messageInput.value.trim();
// 驗(yàn)證輸入內(nèi)容
if (!message) {
alert('請輸入咨詢內(nèi)容!');
return;
}
// 禁用發(fā)送按鈕和輸入框
sendButton.disabled = true;
messageInput.disabled = true;
try {
// 隱藏空提示
emptyHint.style.display = 'none';
// 添加用戶消息到聊天窗口
addMessageToChat(message, 'user');
// 清空輸入框并恢復(fù)高度
messageInput.value = '';
messageInput.style.height = '45px';
// 添加加載狀態(tài)
const loadingId = addLoadingMessage();
// 調(diào)用后端接口
const response = await fetch('http://127.0.0.1:8080/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ message: message })
});
// 移除加載狀態(tài)
removeLoadingMessage(loadingId);
// 處理響應(yīng)
if (response.ok) {
const data = await response.text();
// 添加客服回復(fù)到聊天窗口
addMessageToChat(data, 'bot');
} else {
addMessageToChat('抱歉,服務(wù)器暫時(shí)無法響應(yīng),請稍后再試!', 'bot');
console.error('接口請求失敗:', response.status);
}
} catch (error) {
// 移除加載狀態(tài)
const loadingElements = document.querySelectorAll('.loading-message');
loadingElements.forEach(el => el.remove());
addMessageToChat('網(wǎng)絡(luò)錯(cuò)誤,請檢查您的網(wǎng)絡(luò)連接!', 'bot');
console.error('請求出錯(cuò):', error);
} finally {
// 恢復(fù)發(fā)送按鈕和輸入框
sendButton.disabled = false;
messageInput.disabled = false;
messageInput.focus();
// 滾動到最新消息
scrollToBottom();
}
}
// 添加消息到聊天窗口
function addMessageToChat(content, sender) {
const messageDiv = document.createElement('div');
messageDiv.className = `message ${sender}-message`;
// 創(chuàng)建頭像元素
const avatarImg = document.createElement('img');
avatarImg.className = 'message-avatar';
avatarImg.src = sender === 'user' ? USER_AVATAR : BOT_AVATAR;
avatarImg.alt = sender === 'user' ? '用戶頭像' : '客服頭像';
// 創(chuàng)建消息內(nèi)容元素
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.textContent = content;
// 組裝消息元素
messageDiv.appendChild(avatarImg);
messageDiv.appendChild(contentDiv);
chatMessages.appendChild(messageDiv);
// 滾動到最新消息
scrollToBottom();
}
// 添加加載中的消息
function addLoadingMessage() {
const loadingId = 'loading-' + Date.now();
const loadingDiv = document.createElement('div');
loadingDiv.id = loadingId;
loadingDiv.className = 'message bot-message loading-message';
// 創(chuàng)建客服頭像
const avatarImg = document.createElement('img');
avatarImg.className = 'message-avatar';
avatarImg.src = BOT_AVATAR;
avatarImg.alt = '客服頭像';
// 創(chuàng)建加載內(nèi)容
const contentDiv = document.createElement('div');
contentDiv.className = 'message-content';
contentDiv.innerHTML = '<div class="loading"></div>';
// 組裝加載消息
loadingDiv.appendChild(avatarImg);
loadingDiv.appendChild(contentDiv);
chatMessages.appendChild(loadingDiv);
scrollToBottom();
return loadingId;
}
// 移除加載中的消息
function removeLoadingMessage(loadingId) {
const loadingDiv = document.getElementById(loadingId);
if (loadingDiv) {
loadingDiv.remove();
}
}
// 滾動到聊天底部
function scrollToBottom() {
chatMessages.scrollTop = chatMessages.scrollHeight;
}
// 監(jiān)聽輸入框回車事件(兼容)
messageInput.addEventListener('input', function() {
this.style.height = 'auto';
this.style.height = (this.scrollHeight > 45 ? this.scrollHeight : 45) + 'px';
});
messageInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
});
</script>
</body>
</html>
七、運(yùn)行測試
7.1 沒有 RAG 前的問答(推薦的是其他品牌的手機(jī))

7.2 初始化
調(diào)用初始化接口,初始化知識庫 pdf 文檔。
http://127.0.0.1:8080/chat/init?path=/Users/xxx/Downloads/阿里云百煉系列手機(jī)產(chǎn)品介紹.pdf
7.3 RAG 后的問答
推薦的是我們 pdf 里的內(nèi)容

八、RAG 流程說明(核心)
離線構(gòu)建階段
PDF 讀取 → 文本清洗 → 文本分塊 → Embedding 向量化 → 存入向量數(shù)據(jù)庫
在線問答階段
用戶問題 → 問題向量化 → 向量庫相似度檢索 → 拼接上下文 → 大模型生成答案
九、優(yōu)化方向
分塊優(yōu)化:使用語義分塊替代固定長度分塊,保留段落完整性
檢索優(yōu)化:增加關(guān)鍵詞檢索 + 向量檢索混合模式