本文是對(duì)「RAG 基礎(chǔ)概念 + 本地知識(shí)庫(kù) Demo」的整理稿:上半部分講清楚 什么是 RAG 和 流程圖在說什么;下半部分保留可運(yùn)行的 Spring AI + 智譜 示例代碼,并補(bǔ)一段 為何不精準(zhǔn) 和 往生產(chǎn)走時(shí)多考慮啥。
適合已經(jīng)會(huì)寫 Spring Boot、正準(zhǔn)備接大模型做企業(yè)內(nèi)部問答的 Java 同學(xué)。
1. 一句話定義
RAG(Retrieval-Augmented Generation,檢索增強(qiáng)生成):在讓大模型 生成答案之前,先從你自己的知識(shí)源里 檢索 一段相關(guān)內(nèi)容,塞進(jìn) Prompt,再交給 LLM 生成。這樣模型既用得上 最新、私有、領(lǐng)域 的材料,又能在一定程度上抑制 空口編造(幻覺)。
它不是替代微調(diào),而是 外掛圖書館:考試允許翻書,翻到的頁碼就是檢索到的 chunk。
2. RAG 在干什么:對(duì)照流程圖理解
下面這張示意圖把 RAG 拆成兩條時(shí)間線:離線建索引 和 在線問答案。原圖常見于各類 RAG 教程,這里在倉(cāng)庫(kù)中的引用路徑為:

2.1 離線索引(Indexing,圖左側(cè)虛線框)
| 步驟 | 在干什么 |
|---|---|
| docs | 原始材料:URL、PDF、TXT、數(shù)據(jù)庫(kù)導(dǎo)出等,本質(zhì)是你的 本地 / 企業(yè)知識(shí)庫(kù)。 |
| Parsing + preprocessing | 解析格式、清洗噪聲,變成可切的純文本。 |
| Chunking | 切成 小塊(chunks):模型上下文有限,且小塊更利于 精準(zhǔn)命中 某一段說法。 |
| Embedding Model | 把每個(gè) chunk 變成 向量(高維數(shù)值),語義相近的文本在向量空間里通常 離得近。 |
| Vector store + Indexing | 向量(常附帶原文、元數(shù)據(jù))寫入 向量數(shù)據(jù)庫(kù)或索引,供后續(xù)檢索。 |
個(gè)人理解(Java 老鳥的直覺):這一步像給海量日志做 倒排索引,只不過索引鍵從「詞」換成了 語義向量;建索引進(jìn)度慢、占存儲(chǔ),但問的時(shí)候是在 近鄰搜索,不是在全庫(kù)掃字符串。
2.2 在線檢索與生成(Retrieval & Generation,圖右側(cè))
| 步驟 | 在干什么 | 白話解釋(中文) |
|---|---|---|
| query | 用戶問題。 | 用戶用自然語言提問,是整個(gè) 在線階段 的入口;后面所有步驟都圍繞「這句話要找什么、答什么」。 |
| Embedding Model(Vectorize) | 問題也編成 同維度向量,和庫(kù)里的向量 可比。 | 把「一句話」變成 一串?dāng)?shù)字(向量),和建庫(kù)時(shí) chunk 用的 同一套模型、同一維度,才能在同一空間里談「近不近」。 |
| Retrieve | 在向量庫(kù)里做 相似度檢索(常見:余弦相似度、點(diǎn)積;庫(kù)內(nèi)往往用 HNSW 等索引加速)。 | 在知識(shí)庫(kù)里 按語義遠(yuǎn)近 撈出與問題最相關(guān)的若干段材料,相當(dāng)于 翻書翻到最相關(guān)那幾頁(不必整本書掃字)。 |
| Relevant docs + prompt | 把 Top-K 片段拼進(jìn)提示詞模板,約束模型 只結(jié)合給定上下文作答。 | 把撈出來的片段當(dāng) 參考資料,寫進(jìn) Prompt,告訴模型:先信這些,再組織語言;減少空口編造。 |
| LLM → Generate → response | 大模型推理,輸出連貫回答。 | 大模型在 給定上下文 + 用戶問題 下做生成,輸出用戶看得懂的 最終答復(fù)(仍可能需后處理與審核)。 |
名詞:HNSW 指什么?
HNSW(Hierarchical Navigable Small World)是向量檢索里常用的一種 近似最近鄰(ANN) 算法:在高維向量空間里建 分層圖索引,查詢時(shí)從上層「粗跳」快速接近目標(biāo)區(qū)域,再在下層細(xì)查,從而在 不全表暴力掃描 的前提下,盡快找到與查詢向量 最接近 的若干條記錄。
- 為何需要它:全庫(kù)兩兩算相似度是 O(數(shù)據(jù)量),數(shù)據(jù)一大就不可用;向量庫(kù)(Milvus、Qdrant、pgvector 等)內(nèi)部用 HNSW、IVF 等索引做 加速召回。
- 近似:結(jié)果是 近似 最近鄰,用少量精度換 延遲與吞吐;重要場(chǎng)景可再疊 Rerank 精排。
-
和 Demo 的關(guān)系:教程里
for循環(huán)算余弦相似度是 樸素全表;生產(chǎn)應(yīng)交給 帶 ANN 索引的 Vector Store,而不是在應(yīng)用里自己掃List。
落地實(shí)現(xiàn):從「手寫循環(huán)」到「帶索引的 Vector Store」
1. 典型數(shù)據(jù)流(索引何時(shí)出現(xiàn))
-
建庫(kù) / 灌庫(kù):文檔 chunk →
EmbeddingModel.embed()→ 向量 →VectorStore.add/addAll;存儲(chǔ)在落盤后 構(gòu)建或更新 HNSW(或 IVF 等),后續(xù)查詢 不再 對(duì)全量向量做樸素兩兩比較。 -
在線檢索:用戶問題 → 同維度 query 向量 →
VectorStore.similaritySearch(SearchRequest)(或各實(shí)現(xiàn)類等價(jià) API)→ 返回 Top-KDocument。 - 距離度量:與庫(kù)側(cè)索引 ops 一致(余弦 / L2 / 內(nèi)積),且與 Embedding 模型是否歸一化 的假設(shè)一致,否則召回會(huì)飄。
2. Spring AI 中的抽象
-
VectorStore:屏蔽PgVectorStore、MilvusVectorStore、SimpleVectorStore等;業(yè)務(wù)只面對(duì)Document(text + metadata) 與SearchRequest(query / topK / filter)。 -
SimpleVectorStore:多作小數(shù)據(jù)、單測(cè)或本地 demo;不等于 生產(chǎn)級(jí)持久化 + HNSW,但可快速驗(yàn)證 RAG 鏈路。 - PgVector / Milvus / Qdrant:在庫(kù)里 建表 + 建向量索引(部分由 starter 自動(dòng) DDL,部分需你按廠商文檔先建 HNSW)。
3. 各后端「HNSW」長(zhǎng)什么樣(概念級(jí))
| 后端 | 實(shí)現(xiàn)要點(diǎn) |
|---|---|
| pgvector | 列類型如 vector(1536);索引示例:CREATE INDEX … ON tbl USING hnsw (embedding vector_cosine_ops);(ops 與距離一致)??烧{(diào) m、ef_construction 等(以 pgvector 當(dāng)前文檔為準(zhǔn))。 |
| Milvus | Collection 上 INDEX,index_type 常選 HNSW,params 含 M、efConstruction;查詢側(cè) ef 影響精度與延遲。 |
| Qdrant | 向量參數(shù) + HNSW(如 m、ef_construct),查詢可設(shè) hnsw_ef。 |
4. 調(diào)參直覺
- M:鄰居多 → 召回更好,建索引 更慢、占內(nèi)存更多。
- efConstruction / ef(查詢):搜索范圍大 → 更準(zhǔn)更慢。
- Top-K:可先 略大(如 20)再 Rerank 壓到 5,比單次 K=2 穩(wěn)。
5. 代碼形態(tài)(示意)
// 灌庫(kù):chunk → Document(metadata 帶 source、chunkId) → add
// vectorStore.add(List.of(new Document(text, Map.of("source", "poetry.txt"))));
// 檢索:由 VectorStore 內(nèi)部調(diào) Embedding 或傳入 query 文本(視實(shí)現(xiàn))
// List<Document> hits = vectorStore.similaritySearch(
// SearchRequest.builder().query("古代詩歌常用意象有哪些?").topK(8).build());
6. 與本文 Demo 的對(duì)應(yīng)
| Demo | 生產(chǎn) |
|---|---|
List<float[]> + for + cosineSimilarity
|
VectorStore + 庫(kù)內(nèi) ANN 索引
|
內(nèi)存 docs
|
持久化表 + metadata,向量可 重建 |
探究:RAG 解決的是 「知識(shí)從哪來」;「話怎么說得體」 仍靠 Prompt、模型能力和后處理。上下文給錯(cuò)了,模型照樣能一本正經(jīng)胡說——所以 檢索質(zhì)量 往往是瓶頸,而不是 Chat API 調(diào)得花不花。
3. Demo 案例目標(biāo)(Spring AI + 智譜)
案例中用到:
-
Spring AI:統(tǒng)一抽象
EmbeddingModel、ChatClient等,類似當(dāng)年用JdbcTemplate統(tǒng)一數(shù)據(jù)源。 - 智譜 AI Embedding:把文本變成向量。
- 智譜 Chat(如 GLM-4-Flash):結(jié)合檢索到的片段回答問題。
實(shí)現(xiàn)思路(與教程一致):
- 本地知識(shí)文件放在
resources,ClassPathResource加載。 - 按分隔符
----手工切分片段(教學(xué)向;生產(chǎn)慎用這種粗切法)。 - 啟動(dòng)時(shí)對(duì)每段 embed,向量放 內(nèi)存列表(教學(xué)向;重啟即失、量大必掛)。
- 用戶提問 embed 后,與所有片段向量算 余弦相似度,取 Top-K(示例代碼里
TOP_K = 3,可按題調(diào)整)拼上下文。 -
系統(tǒng)提示 + 用戶提示 交給
ChatClient同步調(diào)用返回文案。
4. 配置與資源
4.1 Chat 模型(application.properties)
# 使用智譜 AI Chat 模型(pom.xml 需引入對(duì)應(yīng) spring-ai-starter 與智譜 BOM/依賴)
spring.ai.zhipuai.chat.options.model=GLM-4-Flash
4.2 本地知識(shí)文件
在 resources 下放置 古代詩歌常用意象.txt(注意原文小標(biāo)題里有時(shí)寫作「意向」,文件名建議統(tǒng)一為 意象,與詩文術(shù)語一致)。
內(nèi)容結(jié)構(gòu)上,文檔用 ---- 分成多塊,涵蓋 植物類 / 動(dòng)物類 / 景象類 / 人文類 等意象說明——Demo 里 每一塊會(huì)被整體 embed 一次,塊太大或切法不對(duì)會(huì)直接影響檢索顆粒度。
5. 核心代碼:RagService
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@Service
public class RagService {
/** 檢索拼進(jìn) Prompt 的片段條數(shù);枚舉類問題可適當(dāng)調(diào)大 */
private static final int TOP_K = 3;
private final EmbeddingModel em; // 嵌入模型,用于生成文本的向量
private final ChatClient chatClient; // 聊天客戶端,用于與 AI 進(jìn)行交互
private final List<String> docs = new ArrayList<>();// 存儲(chǔ)本地文檔內(nèi)容
private final List<float[]> vectors = new ArrayList<>();// 存儲(chǔ)本地文檔向量
public RagService(EmbeddingModel embeddingModel, ChatClient.Builder chatBuilder) throws IOException {
this.em = embeddingModel;
this.chatClient = chatBuilder.build(); // 創(chuàng)建智譜 AI Chat 客戶端
Resource res = new ClassPathResource("古代詩歌常用意象.txt");
String content = new String(res.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
for (String part : content.split("----")) {
System.out.println("part: " + part);
if (part.isBlank()) continue;
docs.add(part);
vectors.add(em.embed(part)); // 將文檔生成 embedding 并存儲(chǔ)
}
}
public String answer(String q) {
if (vectors.isEmpty()) {
return "知識(shí)庫(kù)為空。";
}
float[] qv = em.embed(q);
int k = Math.min(TOP_K, vectors.size());
List<Integer> topIndices = topKSimilarIndices(qv, k);
StringBuilder ctx = new StringBuilder();
for (int i = 0; i < topIndices.size(); i++) {
if (i > 0) {
ctx.append("\n---\n");
}
ctx.append(docs.get(topIndices.get(i)));
}
String prompt = "以下是知識(shí)內(nèi)容:\n" + ctx + "\n請(qǐng)基于上述知識(shí)回答用戶問題:「" + q + "」";
var response = chatClient
.prompt()
.system("你是知識(shí)助手,結(jié)合上下文回答問題")
.user(prompt)
.call();
return response.content();
}
/**
* Top-K:先對(duì)每個(gè) chunk 算與問題的余弦相似度,再按分?jǐn)?shù)降序取前 k 個(gè)下標(biāo)。
* 數(shù)據(jù)量極大時(shí)可改為「最小堆維護(hù)前 K」避免全量排序,此處教學(xué)向保持可讀。
*/
private List<Integer> topKSimilarIndices(float[] queryVector, int k) {
int n = vectors.size();
double[] score = new double[n];
for (int i = 0; i < n; i++) {
score[i] = cosineSimilarity(queryVector, vectors.get(i));
}
Integer[] order = new Integer[n];
for (int i = 0; i < n; i++) {
order[i] = i;
}
Arrays.sort(order, (i, j) -> Double.compare(score[j], score[i]));
List<Integer> top = new ArrayList<>(k);
for (int i = 0; i < k; i++) {
top.add(order[i]);
}
return top;
}
// 余弦相似度:值域通常在 [-1, 1],越接近 1 越相似(向量若已 L2 歸一化則點(diǎn)積即余弦)
private double cosineSimilarity(float[] a, float[] b) {
double dot = 0, na = 0, nb = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
na += a[i] * a[i];
nb += b[i] * b[i];
}
return dot / (Math.sqrt(na) * Math.sqrt(nb));
}
}
防呆:實(shí)現(xiàn)手寫相似度時(shí),dot 必須是 (\sum a_i b_i);若誤寫成 dot += a[i] * a[i],Top-K 排序會(huì)完全失真——排錯(cuò)片段比「模型笨」更致命。
6. RagController
import com.example.springaiembedding.service.RagService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/rag")
public class RagController {
@Autowired
private RagService ragService;
@GetMapping("/ask")
public Map<String, String> ask(@RequestParam("question") String question) {
String answer = ragService.answer(question);
return Map.of("question", question, "answer", answer);
}
}
自測(cè) URL
http://localhost:8080/rag/ask?question=古代詩歌常用意象有哪些?
若答案 只覆蓋部分內(nèi)容 或 偏題,多半不是「模型不行」四個(gè)字能糊弄過去的,下面分條說原因。
7. 為何 Demo 容易「不夠準(zhǔn)」?
| 原因 | 說明 |
|---|---|
| 切分策略過粗 | 按 ---- 切,單塊可能仍很長(zhǎng),語義混雜;檢索命中的是「整坨」里最接近問題的平均語義,顆粒度不對(duì)。 |
| Top-K = 2 太小 | 「常用意象有哪些」是 枚舉型 問題,兩條 chunk 可能蓋不全植物/動(dòng)物/景象/人文。RAG 不是只配 Top2,可以先大 K 再壓縮。 |
| 線性掃全庫(kù) |
for 循環(huán)算相似度是 O(n),教學(xué)夠用;數(shù)據(jù)一大就要 向量索引(HNSW 等),否則延遲和內(nèi)存都扛不?。?strong>HNSW 含義見上文 §2.2)。 |
| 實(shí)現(xiàn)細(xì)節(jié) bug | 手寫相似度時(shí)點(diǎn)積維度搞錯(cuò)、向量維度不一致等,會(huì)直接導(dǎo)致 召回錯(cuò)亂。 |
| 僅有向量、沒有詞面 | 專有名詞、生僻書名,向量檢索偶爾會(huì)飄;生產(chǎn)常用 混合檢索(BM25 + Vector) 補(bǔ)短板。 |
| 缺少「不知道」約束 | Prompt 里應(yīng)明確:上下文沒有的信息 不要編;可顯著減少「像那么回事」的幻覺。 |
8. 往生產(chǎn)走:RAG 相關(guān)實(shí)現(xiàn)補(bǔ)全( checklist )
下面這些和「會(huì)不會(huì)調(diào) Chat Completion」關(guān)系不大,和 數(shù)據(jù)工程 關(guān)系更大。
向量持久化
使用 pgvector / Milvus / Qdrant / Redis Stack 等,配合 Spring AI 的VectorStore抽象;避免「重啟 embed 一遍」和「單機(jī) List 存全量」。Chunk 策略
按 固定 token、段落/標(biāo)題、或 結(jié)構(gòu)化字段 切;必要時(shí) 重疊窗口(overlap) 保留上下文;長(zhǎng)文檔先 分段索引。混合檢索與重排
初召回:向量 Top 50 + 關(guān)鍵詞 Top 50;再用 Rerank 模型 壓成 Top 5 進(jìn) Prompt,命中率 往往上一臺(tái)階。元數(shù)據(jù)與過濾
索引里帶tenantId、docVersion、permission,檢索前 先過濾 再相似度排序,企業(yè)里這是 合規(guī) 問題而不只是效果問題。觀測(cè)與評(píng)測(cè)
記錄 query、召回 id、得分、最終回答;用固定問集合做回歸,否則每次改分塊都像在黑盒煉丹。異步與成本
建索引(批量 embed)可走 異步任務(wù);查詢鏈路上區(qū)分 輕量模型 embed 與 大模型 generate 的配額與超時(shí)。
9. 小結(jié)
- RAG = 索引(Embedding + 向量存儲(chǔ))+ 檢索(相似度/混合)+ 增強(qiáng) Prompt + LLM 生成。
- 流程圖把 離線 與 在線 分清楚,你在架構(gòu)評(píng)審時(shí)也可以同樣畫:左邊數(shù)據(jù)管道,右邊查詢 SLAs。
- Demo 的價(jià)值是 跑通鏈路;要上生產(chǎn),重點(diǎn)會(huì)挪到 chunk、向量庫(kù)、混合檢索、權(quán)限與評(píng)測(cè)。
引用與延伸閱讀
- 文中 RAG 工作流程示意:見本文配圖
images/rag-workflow-architecture.png(經(jīng)典 Indexing / Retrieval & Generation 二分結(jié)構(gòu))。 -
Spring AI:Spring AI 官方文檔(Embedding、
VectorStore、ChatClient等)。 - 智譜 AI:以官方開放平臺(tái)說明為準(zhǔn),注意 Embedding 與 Chat 模型名、維度、計(jì)費(fèi) 分離配置。
- 同倉(cāng)庫(kù)若已有 《Spring AI / Embedding》 小冊(cè)章節(jié),可與本文 交叉閱讀(余弦相似度、向量維度、批處理 embed 等)。