RAG消除AI幻觉:Spring Boot实战教程
之前聊了AI的起源和基础认知,有朋友留言说:
“这些我都懂,我现在的问题是——AI为什么老是胡说?”
这个问题问得很实在。如果你在做AI项目,多半已经被它坑过——明明文档里没有的内容,它能给你编一套完整方案;同一个问题,每次答案还不一样;有时候甚至“自信满满地错”。一开始以为是模型不行,后来才发现——问题根本不在模型,而在于你怎么用它。
这篇不讲概念,直接讲一个你必须搞懂的东西:
RAG(检索增强生成)
一、AI产生幻觉的根源
要理解RAG,得先摸清大模型的本质:它本质不是“查资料”,而是“生成文本”。你问它问题,它不是去数据库查答案,而是根据训练过的数据,“猜一个最像答案的话”。注意,这里说的是“猜”——可能不准确,但好理解。根据训练数据,预测“在当前上下文中最有可能出现的下一个词”(Next Token Prediction)。
这种预测带来一个很现实的问题:不管是公司还是个人,很多资料是不能在互联网上公开的——它不知道你公司的接口文档,不知道你的业务逻辑,更不知道你的私有数据。那它怎么给你答案?它只能“合理地编”。这就是我们常听到的:幻觉(Hallucination)。
而且越是表达能力强的模型,越会编,而且编得越像真的。例如你的代码这样写:
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
public class HallucinationDemo {
public static void main(String[] args) {
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey("YOUR_API_KEY")
.modelName("gpt-4o-mini")
.build();
String question = "我们公司内部接口 /api/internal/pay/v2 的调用流程是什么?";
String answer = model.generate(question);
System.out.println(answer);
}
}
AI回答得有板有眼,语气非常自信,结构非常完整,看起来“完全正确”。
接口 /api/internal/pay/v2 的调用流程如下:
1. 用户鉴权(Token校验)
2. 参数校验(金额、订单号等)
3. 调用支付服务
4. 返回支付结果
但很明显,这不是我们要的答案——模型本就不知道答案,但必须生成一个“像答案的东西”。它其实是在套模板:“接口调用流程通常是这样”,然后拼一个“合理答案”。
二、RAG到底在解决什么问题?
换个角度。如果是你自己回答一个问题,你会怎么做?比如有人问你:“我们系统A的接口调用流程是什么?”你的第一反应肯定不是“开始编”,而是先去翻文档。而RAG做的事情,和你的反应一模一样:让AI也“先查资料,再回答”。换句话说:RAG = 给AI装一个“可搜索的知识库”。
三、RAG其实很简单
RAG的架构图类似这样:
看起来很复杂,但其实你只要记住下面这个就够了:
- 第一步:把知识“存进去”
- 第二步:用户提问时,先去“找相关内容”
- 第三步:把“资料 + 问题”一起丢给AI
关于第一步,如何把知识存进去。你需要做三件事:
- 把文档切成一小段一小段(chunk)
- 把每一段转成向量(embedding)
- 存进数据库(向量库)
你可以把这个步骤理解为:把“文字”变成“可计算的坐标”。那么,为什么需要把文档切成一小段一小段?不切chunk,检索就不准;检索不准,AI一定胡说。
假设你有一份文档:
《系统设计文档》
- 用户登录流程
- 支付流程
- 订单系统
- 消息队列
- 接口A说明
- 接口B说明
你整篇直接丢进向量库。然后用户问:“接口A怎么调用?”向量检索会发生什么?它会拿“整篇文档”去做相似度计算。问题来了:文档里包含一堆无关内容(登录、支付、订单…),“接口A”只是其中一小部分。最终结果就是:相似度被“稀释”了——要么查不到(分数不够),要么查到一堆无关内容。
为什么切chunk就好了?把刚才那份文档拆开:
chunk1:用户登录流程
chunk2:支付流程
chunk3:接口A说明
chunk4:接口B说明
再问同样的问题:“接口A怎么调用?”这次会发生什么?检索系统会把问题转成向量,和每个chunk分别算相似度。结果:chunk3(接口A)会被精准命中。本质变化:从“一整本书参与匹配”变成“一小段一小段精确匹配”。
再说一个比较关键的点:大模型是有上下文长度限制的,比如4k/8k/128k token。如果不切chunk,你可能会把一整篇文档塞进去,结果要么超长直接截断,要么成本爆炸。chunk的作用之一就是:控制输入长度 + 提高信息密度。chunk本质是让向量搜索具备“段落级命中能力”。
第二步:用户提问时,先去“找相关内容”。用户问:“接口A怎么调用?”系统不会直接问AI,而是先做一件更重要的事:去向量数据库里找“最像这个问题的几段内容”。
EmbeddingStore store = PgVectorEmbeddingStore.builder()
.datasource(getDataSource())
.table("knowledge")
.dimension(1536)
.build();
EmbeddingModel embeddingModel = getEmbeddingModel();
ContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(store)
.embeddingModel(embeddingModel)
.maxResults(3)
.minScore(0.7)
.build();
String question = "接口A怎么调用?";
List contents = retriever.retrieve(question);
System.out.println("==== 检索结果 ====");
for (Content content : contents) {
System.out.println(content.textSegment().text());
System.out.println("------------------");
}
第三步:把“资料 + 问题”一起丢给AI。
String question = "接口A怎么调用?";
String context = contents.stream()
.map(Content::textSegment)
.map(segment -> segment.text())
.reduce("", (a, b) -> a + "n" + b);
String prompt = String.format("""
你是企业内部AI助手,请严格根据“资料”回答问题。
如果资料中没有相关信息,请回答:“无法确定”,不要编造。
===== 资料 =====
%s
===== 问题 =====
%s
===== 输出要求 =====
- 只基于资料回答
- 不允许编造
- 不确定就说无法确定
""", context, question);
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey("YOUR_API_KEY")
.modelName("gpt-4o-mini")
.build();
String answer = model.generate(prompt);
System.out.println("==== AI回答 ====");
System.out.println(answer);
}
}
这一步非常关键。最终给模型的,不是“请回答这个问题”,而是“基于以下资料回答,不要乱编”。你可以理解为:你在“喂答案范围”,而不是让它自由发挥。
五、演示
当我不上传任何文档的时候
我上传一个文档,里面描述了马明聪是谁
六、踩过的几个坑
这部分你一定会遇到。
❗chunk切分不合理。一开始直接按整段文档丢进去,结果查出来的内容完全不相关。后来改成200~500字一段,效果明显提升。
❗相似度阈值乱设。.minScore(0.7)这个值没有标准答案,但你要知道:太高→查不到内容,太低→垃圾内容混进来。最好的办法:自己打印日志调试。
❗以为用了RAG就不会胡说。这是一个大坑。现实是:RAG只能减少胡说,不会消灭。如果你检索不准、prompt没约束、数据本身有问题,那AI照样乱来。
❗忽略metadata过滤。比如你有多个系统、多个版本,但你不加过滤条件,AI会把A系统的答案用在B系统上。
七、你现在应该有一个新认知
很多人以为:做AI应用 = 调一个大模型API。但真实情况是:模型只占20%,剩下80%是工程问题,包括数据怎么处理、检索怎么做、prompt怎么设计。
你只记住一句话:RAG不是技术名词,而是一种“让AI不胡说的工程手段”。



