正文摘要
大模型有知识截止日期、还会一本正经地胡说八道,企业落地 AI 问答绕不开 RAG(检索增强生成)。本文用纯 Java 实现一条完整 RAG 链路:文档加载与切分、文本向量化、余弦相似度检索、上下文组装与问答,代码可直接运行,不依赖重型框架。
正文
做企业知识库问答,最常被问的一句话是:“大模型不是什么都知道吗,为什么还要自己存文档?”
因为大模型不知道你们公司的制度、你们产品的参数。RAG 的思路很简单:先在你的文档里检索,把相关内容拼给大模型,再让它回答。
这篇用 Java 完整实现一遍,所有代码可直接跑。
一、RAG 全链路长这样
企业文档 → 切分 → 向量化 → 存向量库
↓
用户提问 → 向量化 → 相似度检索 → 命中片段
↓
拼进Prompt → 大模型 → 带来源的回答
二、文档切分:检索质量的地基
切分粒度决定检索质量,常用策略:按段落切 + 滑动窗口 + 重叠:
public List splitDocument(String content, int chunkSize, int overlap) {
List chunks = new ArrayList<>();
int start = 0;
while (start < content.length()) {
int end = Math.min(start + chunkSize, content.length());
chunks.add(content.substring(start, end));
if (end == content.length()) break;
start = end - overlap; // 重叠防止切断语义
}
return chunks;
}
// 使用:按150字切、重叠30字
List chunks = splitDocument(doc, 150, 30);
踩坑记录: 一开始按字符硬切,一句话被切成两半,检索全乱。后来改成 “先按换行分块,再合并到接近 150 字”,准确率明显提升。
三、向量化与相似度检索:不依赖向量库的最小实现
生产用向量数据库(如 Milvus、pgvector),但原理用一段代码就能讲透:
public record VectorDoc(String id, String text, double[] vector) {}
// 1. 向量化:调用Embedding接口把文本转成向量(示意)
public double[] embed(String text) {
// POST /embeddings 返回 float[],维度以服务方为准(如1024)
return llmClient.embed(text);
}
// 2. 余弦相似度:最常用的向量距离
public double cosineSimilarity(double[] a, double[] b) {
double dot = 0, normA = 0, normB = 0;
for (int i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] * a[i];
normB += b[i] * b[i];
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB) + 1e-9);
}
// 3. Top-K检索:全量扫描找最相似的K段(生产换向量索引)
public List retrieve(List docs, double[] queryVec, int topK) {
return docs.stream()
.sorted(Comparator.comparingDouble(
d -> -cosineSimilarity(d.vector(), queryVec)))
.limit(topK)
.toList();
}
生产环境一定要用向量数据库—— 全量扫描在几十万条文档时性能不可接受,原理不变,只是索引换了。
四、组装上下文:让大模型 “只答文档里有的”
public String buildRagAnswer(String question, List hits) {
// 1. 拼上下文:把命中的片段按顺序拼接
String context = hits.stream()
.map(VectorDoc::text)
.collect(Collectors.joining(“\n—\n”));
// 2. 拼系统提示词:约束回答边界 String systemPrompt = """ 你是一个企业知识库助手。请仅根据以下资料回答问题。 如果资料中没有相关内容,请明确回答"资料中未找到相关信息", 不要编造。回答时注明信息来自哪份资料。 """; // 3. 调大模型 return llmClient.chat(systemPrompt + "\n【资料】\n" + context + "\n【问题】\n" + question);}
关键设计: 提示词里明确 “没有就说不没有,不要编造”—— 这是 RAG 防幻觉的第一道闸。
五、完整调用示例
public class RagDemo {
public static void main(String[] args) {
// 1. 加载并切分企业文档
String doc = readFile(“公司考勤制度.txt”);
List chunks = splitDocument(doc, 150, 30);
// 2. 向量化入库 List<VectorDoc> docs = chunks.stream() .map(c -> new VectorDoc(uuid(), c, embed(c))) .toList(); // 3. 用户提问 → 检索 → 生成 String question = "请假流程是什么?"; List<VectorDoc> hits = retrieve(docs, embed(question), 3); String answer = buildRagAnswer(question, hits); System.out.println(answer); }}
六、落地时的四个工程问题
| 问题 | 解法 |
|---|---|
| 文档更新了怎么办 | 增量切分 + 重向量化,删除旧 chunk |
| 检索结果不准 | 调切分粒度、调 topK、加 rerank 重排 |
| 回答带幻觉 | 收紧系统提示词 + 强制引用来源 |
| 响应太慢 | 向量库索引 + 缓存高频问题 |
这套 RAG 我在北大青鸟长沙大计实训的 Java AI 项目里完整实现过,从文档切分到检索到问答一次跑通。RAG 不神秘,就是把 “找资料” 和 “问 AI” 两个环节串起来,但每个环节都值得较真。
正文相关问答
Q:RAG 和微调有什么区别?怎么选?
A:RAG 是 “外挂知识库”,不改模型、随时更新、可溯源;微调是 “改变模型行为”,成本高、更新慢。企业知识库场景首选 RAG,微调用于风格 / 领域强约束场景。
Q:Java 生态有没有现成的 RAG 框架?
A:有,如 LangChain4j、Spring AI,都支持 Java。但建议先自己实现一遍理解原理,再上框架 —— 否则排错都不知道从哪查。
Q:Embedding 模型选哪种?
A:中文场景选中文优化过的 Embedding 模型(如 bge 系列);维度越高越精细但检索越慢。先跑通再根据准确率和性能权衡。
Q:RAG 的检索准确率一般能达到多少?
A:视文档质量和切分策略而定,好的切分 + 重排在内部测试集上 Top-3 命中率可以到 85%-90%。没有绝对数字,务必建自己的评测集验证。