RAG的Java落地-从切分入库到检索问答

RAG 的 Java 落地:从切分、入库到检索问答

模型不知道你公司的文档,微调又贵又不划算,RAG 是那条正路:回答之前先检索相关片段塞进上下文。概念早就不神秘了,问题是怎么在 Java 里落地。这篇把五步流水线的骨架用零依赖代码跑通,先把形状看懂,再谈换向量库。

流水线五步

#mermaid-svg-x7YqAVBAUwmflCXR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-x7YqAVBAUwmflCXR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-x7YqAVBAUwmflCXR .error-icon{fill:#552222;}#mermaid-svg-x7YqAVBAUwmflCXR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-x7YqAVBAUwmflCXR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-x7YqAVBAUwmflCXR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-x7YqAVBAUwmflCXR .marker.cross{stroke:#333333;}#mermaid-svg-x7YqAVBAUwmflCXR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-x7YqAVBAUwmflCXR p{margin:0;}#mermaid-svg-x7YqAVBAUwmflCXR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-x7YqAVBAUwmflCXR .cluster-label text{fill:#333;}#mermaid-svg-x7YqAVBAUwmflCXR .cluster-label span{color:#333;}#mermaid-svg-x7YqAVBAUwmflCXR .cluster-label span p{background-color:transparent;}#mermaid-svg-x7YqAVBAUwmflCXR .label text,#mermaid-svg-x7YqAVBAUwmflCXR span{fill:#333;color:#333;}#mermaid-svg-x7YqAVBAUwmflCXR .node rect,#mermaid-svg-x7YqAVBAUwmflCXR .node circle,#mermaid-svg-x7YqAVBAUwmflCXR .node ellipse,#mermaid-svg-x7YqAVBAUwmflCXR .node polygon,#mermaid-svg-x7YqAVBAUwmflCXR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-x7YqAVBAUwmflCXR .rough-node .label text,#mermaid-svg-x7YqAVBAUwmflCXR .node .label text,#mermaid-svg-x7YqAVBAUwmflCXR .image-shape .label,#mermaid-svg-x7YqAVBAUwmflCXR .icon-shape .label{text-anchor:middle;}#mermaid-svg-x7YqAVBAUwmflCXR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-x7YqAVBAUwmflCXR .rough-node .label,#mermaid-svg-x7YqAVBAUwmflCXR .node .label,#mermaid-svg-x7YqAVBAUwmflCXR .image-shape .label,#mermaid-svg-x7YqAVBAUwmflCXR .icon-shape .label{text-align:center;}#mermaid-svg-x7YqAVBAUwmflCXR .node.clickable{cursor:pointer;}#mermaid-svg-x7YqAVBAUwmflCXR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-x7YqAVBAUwmflCXR .arrowheadPath{fill:#333333;}#mermaid-svg-x7YqAVBAUwmflCXR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-x7YqAVBAUwmflCXR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-x7YqAVBAUwmflCXR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x7YqAVBAUwmflCXR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-x7YqAVBAUwmflCXR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x7YqAVBAUwmflCXR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-x7YqAVBAUwmflCXR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-x7YqAVBAUwmflCXR .cluster text{fill:#333;}#mermaid-svg-x7YqAVBAUwmflCXR .cluster span{color:#333;}#mermaid-svg-x7YqAVBAUwmflCXR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-x7YqAVBAUwmflCXR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-x7YqAVBAUwmflCXR rect.text{fill:none;stroke-width:0;}#mermaid-svg-x7YqAVBAUwmflCXR .icon-shape,#mermaid-svg-x7YqAVBAUwmflCXR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-x7YqAVBAUwmflCXR .icon-shape p,#mermaid-svg-x7YqAVBAUwmflCXR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-x7YqAVBAUwmflCXR .icon-shape .label rect,#mermaid-svg-x7YqAVBAUwmflCXR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-x7YqAVBAUwmflCXR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-x7YqAVBAUwmflCXR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-x7YqAVBAUwmflCXR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始文档
切分

大文档变小块
表示

每块变成可检索的形态
入库

存进索引或向量库
用户问题
检索

按相似度取 top-k
拼上下文

塞进 prompt
模型回答

后端的直觉在这里全用得上:切分是分页,表示是编码,入库是索引,检索是查询,拼上下文是请求体组装。整条流水线就是一个「带检索的查询服务」。

切分的账

块太大会把无关内容塞进窗口,稀释相关性;块太小会把完整的语义切碎,检索到半句话。经验值是几百字一块,块与块之间留一点重叠,让被切在边界的知识有机会被完整检索。切分的粒度跟着文档类型走:规章条文按条切,代码按函数切,散文按段落切。

检索的两条腿

第一条腿,关键词打分。最朴素的做法是词频重合度,查询词在块里出现得越多越相关。优点是零依赖、可解释,缺点是同义词和语序全丢,「狗」检索不到「犬」。

第二条腿,向量相似。把块和查询都编码成向量,比余弦相似度。语义近的块距离近,同义词问题自然消解。代价是要 embedding 模型、要向量库、要算力。骨架阶段先用关键词腿跑通流程,流程对了再换向量腿;两条腿的接口是一样的,检索这个动作的输入输出没变。

拼上下文的预算

检索回来的块不是全塞,按相似度从高到低,塞到预算满为止,这就是之前装箱篇的场景再现。还有一件小事收益巨大:把每块的来源编号带上,回答里让模型引用编号,用户能核对,幻觉当场收敛。

亲手跑

下面用一段 Java 跑完整骨架,切块、词频打分、取 top-k、拼上下文、模拟回答,零依赖。直接运行 java RagSkeleton.java(JDK 11+)。

java 复制代码
// 直接运行:java RagSkeleton.java
// RAG 骨架:切块、打分、检索、拼上下文,零依赖。

import java.util.ArrayList;
import java.util.List;

public class RagSkeleton {

    static class Chunk {
        final int id;
        final String text;
        Chunk(int id, String text) {
            this.id = id;
            this.text = text;
        }
    }

    // 第一步:切分,演示按句号切
    static List<Chunk> split(String doc) {
        List<Chunk> chunks = new ArrayList<>();
        int id = 1;
        for (String part : doc.split("。")) {
            if (!part.isBlank()) {
                chunks.add(new Chunk(id++, part.trim() + "。"));
            }
        }
        return chunks;
    }

    // 第二步:检索,二元组打分的最简版(中文零依赖的粗切词)
    static List<String> tokenize(String text) {
        List<String> grams = new ArrayList<>();
        for (int i = 0; i < text.length() - 1; i++) {
            grams.add(text.substring(i, i + 2));
        }
        return grams;
    }

    static List<Chunk> retrieve(String query, List<Chunk> chunks, int topK) {
        List<String> grams = tokenize(query);
        List<Chunk> ranked = new ArrayList<>(chunks);
        ranked.sort((a, b) -> Integer.compare(score(grams, b), score(grams, a)));
        return ranked.subList(0, Math.min(topK, ranked.size()));
    }

    static int score(List<String> grams, Chunk c) {
        int s = 0;
        for (String g : grams) {
            if (c.text.contains(g)) {
                s++;
            }
        }
        return s;
    }

    // 第三步:拼上下文,带来源编号
    static String buildContext(List<Chunk> hits) {
        StringBuilder sb = new StringBuilder();
        for (Chunk c : hits) {
            sb.append("[").append(c.id).append("] ").append(c.text).append("\n");
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        String doc = "退款规则:七天内无理由退款。超期按比例折算。"
                + "运费规则:满 99 包邮。偏远地区加收 20 元。"
                + "发票规则:订单完成后 30 天内可开电子发票。";
        List<Chunk> chunks = split(doc);
        String query = "键盘退款怎么算";

        List<Chunk> hits = retrieve(query, chunks, 2);
        String context = buildContext(hits);

        System.out.println("== 检索到的块 ==");
        System.out.println(context);
        System.out.println("== 拼进 prompt 的形状 ==");
        System.out.println("请根据以下资料回答,引用编号:\n" + context + "问题:" + query);
        System.out.println("模型的回答会带着 [编号],幻觉有据可查。");
    }
}

记住一句话:RAG 是带检索的查询服务,先跑通形状,再谈换昂贵的腿。

适用边界

这一篇管骨架与形状。Embedding 的选型、向量库(Milvus、pgvector)的接入、检索质量的评估(命中率、忠实度),以及重排序(Rerank)这层加速器,是另外的题。词频打分是最简腿,只配当骨架,别在生产里裸奔。跑完骨架,把你的文档喂进来,先看检索命中率;命中率不够再考虑向量腿,顺序别反。

相关推荐
像风一样自由20204 小时前
4.DockerCompose是什么为什么算法服务需要多个容器
人工智能·rag·智能体
呆某人7 小时前
RAG-FastGPT安装
fastgpt·rag·glm-4-flash·embedding-3
我喜欢就喜欢10 小时前
RAG 重排序 Rerank 实战:用 bge-reranker 把 Top-K 命中率拉回来
python·大模型·向量检索·rag·rerank
ss27312 小时前
不用向量库,我用 MySQL 跑通了蔬菜 RAG--基于 RuoYi-AI 的蔬菜种植知识库 + 智能咨询系统,普通开发机就能跑
数据库·人工智能·智慧农业·ruoyi·知识库·rag
骑着蜗牛撵大象3271 天前
Spring Boot 4.1 + Spring AI 2.0:RAG 供文、图谱供关系、MCP 供工具的 Java Agent 平台
java·spring boot·spring·微服务·知识图谱·rag·mcp
救救孩子把2 天前
生产 RAG 检索故障怎么查?沿着正确证据逐站定位
rag·rag检索·准确度
仙逆GPT2 天前
ChatGPT、Codex工程实战:Agent接入企业知识库,文档明明更新了,为什么回答还是旧版本?
chatgpt·codex·rag·企业知识库·ai agent
第404块砖头3 天前
Spring Boot 4.1 + Spring AI 2.0:我如何用 Java 微服务把 RAG、知识图谱和 MCP 协议串成一个 Agent 平台
知识图谱·知识库·graph·rag·mcp·spring boot 4.1·spring ai 2.0
code2cat4 天前
【随笔】RAG混合检索:让关键词与向量一起找证据
人工智能·rag·混合检索