Java 轉(zhuǎn) AI 應(yīng)用開發(fā)之RAG + Spring AI 簡(jiǎn)單實(shí)踐案例

本文是對(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ù)中的引用路徑為:

image.png

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 指什么?

HNSWHierarchical 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))

  1. 建庫(kù) / 灌庫(kù):文檔 chunk → EmbeddingModel.embed() → 向量 → VectorStore.add / addAll;存儲(chǔ)在落盤后 構(gòu)建或更新 HNSW(或 IVF 等),后續(xù)查詢 不再 對(duì)全量向量做樸素兩兩比較。
  2. 在線檢索:用戶問題 → 同維度 query 向量 → VectorStore.similaritySearch(SearchRequest)(或各實(shí)現(xiàn)類等價(jià) API)→ 返回 Top-K Document。
  3. 距離度量:與庫(kù)側(cè)索引 ops 一致(余弦 / L2 / 內(nèi)積),且與 Embedding 模型是否歸一化 的假設(shè)一致,否則召回會(huì)飄。

2. Spring AI 中的抽象

  • VectorStore:屏蔽 PgVectorStoreMilvusVectorStore、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 常選 HNSWparamsM、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)思路(與教程一致):

  1. 本地知識(shí)文件放在 resourcesClassPathResource 加載。
  2. 按分隔符 ---- 手工切分片段(教學(xué)向;生產(chǎn)慎用這種粗切法)。
  3. 啟動(dòng)時(shí)對(duì)每段 embed,向量放 內(nèi)存列表(教學(xué)向;重啟即失、量大必掛)。
  4. 用戶提問 embed 后,與所有片段向量算 余弦相似度,取 Top-K(示例代碼里 TOP_K = 3,可按題調(diào)整)拼上下文。
  5. 系統(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)系更大。

  1. 向量持久化
    使用 pgvector / Milvus / Qdrant / Redis Stack 等,配合 Spring AI 的 VectorStore 抽象;避免「重啟 embed 一遍」和「單機(jī) List 存全量」。

  2. Chunk 策略
    固定 token、段落/標(biāo)題、或 結(jié)構(gòu)化字段 切;必要時(shí) 重疊窗口(overlap) 保留上下文;長(zhǎng)文檔先 分段索引。

  3. 混合檢索與重排
    初召回:向量 Top 50 + 關(guān)鍵詞 Top 50;再用 Rerank 模型 壓成 Top 5 進(jìn) Prompt,命中率 往往上一臺(tái)階。

  4. 元數(shù)據(jù)與過濾
    索引里帶 tenantIddocVersion、permission,檢索前 先過濾 再相似度排序,企業(yè)里這是 合規(guī) 問題而不只是效果問題。

  5. 觀測(cè)與評(píng)測(cè)
    記錄 query、召回 id、得分、最終回答;用固定問集合做回歸,否則每次改分塊都像在黑盒煉丹。

  6. 異步與成本
    建索引(批量 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 AISpring AI 官方文檔(Embedding、VectorStore、ChatClient 等)。
  • 智譜 AI:以官方開放平臺(tái)說明為準(zhǔn),注意 Embedding 與 Chat 模型名、維度、計(jì)費(fèi) 分離配置。
  • 同倉(cāng)庫(kù)若已有 《Spring AI / Embedding》 小冊(cè)章節(jié),可與本文 交叉閱讀(余弦相似度、向量維度、批處理 embed 等)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容