实现一个最小的RAG
这一小节,我们把之前的简单文本匹配检索升级为向量检索,并沿着代码看清楚:资料如何变成可检索的索引,问题如何找到相关片段,模型又如何根据这些片段组织答案。
本节 Demo 的代码位于 Gitee 仓库的 Page5 目录:https://gitee.com/toxplore/ai/tree/master/Page5
先看整体流程
RAG 可以拆成两个阶段:先为资料建立索引;收到问题后,再用索引找到相关资料并生成回答。
保存或复用本地索引
相似度排序并取 Top K
发送给聊天模型
一、资料怎样变成索引
程序从 data/ 读取 Markdown、纯文本和结构较简单的 HTML 文件。读取时会做基础清理;HTML 会移除标签、脚本和样式。随后按段落切分,再调用嵌入模型把片段转换为数字向量。
text-embedding-v3 把文本转换成向量;聊天模型负责根据问题和检索资料生成自然语言答案。向量相似度比较由 Java 程序完成。Chunk 保存什么?
每个 Chunk 代表一个可检索片段,包含来源文件、片段正文和嵌入向量。List<Chunk> 就是这些片段组成的列表:
Chunk {
source // 来源文件,例如 data/RAG入门.md
text // 片段正文
embedding // 片段的嵌入向量
}
切分规则:先保留段落,太长再拆
splitIntoChunks() 先按空行识别段落,再尽量把相邻段落组合到不超过 CHUNK_SIZE = 420 个 Java 字符的片段里。只有单独一段已经超过这个长度时,才由 splitLongParagraph() 继续拆分,并让相邻片段重叠约 CHUNK_OVERLAP = 60 个字符。
因此 420 是这个教学 Demo 的示例参数,不是所有资料都适用的最佳值。现实项目会考虑标题、章节、句子等文档结构,并通过真实问题评估切分效果。
二、buildOrLoadIndex():复用缓存或重建索引
- 调用
loadDocuments()读取资料,并计算资料内容与嵌入模型的指纹。 - 如果没有强制重建,而且本地索引存在、模型和指纹都匹配,就调用
readChunks()从 JSON 恢复片段和向量。 - 如果缓存不存在或不匹配,就重新切分资料,并分批调用嵌入 API 生成向量。
- 调用
writeIndex()把片段、来源和向量写入index/vector-index.json,然后返回List<Chunk>。
当前 Demo 每次启动都会读取资料并计算指纹;若索引有效,就加载缓存,不会重新切分或向量化。资料一旦变化,则当前实现会重新建立全部资料的索引,还没有做增量更新。
图:buildOrLoadIndex() 的缓存复用与索引重建分支

三、interact():一轮提问如何流动
- 进入交互循环,等待并读取用户问题;
/quit退出,/reindex强制重建索引。 - 普通问题交给
retrieve(question, chunks)。 retrieve()调用embed(question)将问题转成向量,并遍历片段列表,逐一计算余弦相似度。- 检索结果按分数降序排列,返回
TOP_K = 3个片段。 interact()把问题、命中片段和来源拼成上下文;askModel()再连同系统提示词一起发给聊天模型。- 程序打印答案,并回到循环等待下一问。
图:interact() 调度输入、检索和回答的过程

它衡量问题向量和片段向量方向的接近程度。分数用于排序,不代表答案正确概率。
TOP_K = 3 表示最终返回三个片段,不表示只比较三个片段。四、重要函数对照
| 函数 | 职责 |
|---|---|
loadDocuments() |
遍历资料目录并读取支持的文件。 |
htmlToText() |
移除 HTML 标签和脚本样式,整理文本。 |
splitIntoChunks() |
按段落组合文本片段。 |
buildOrLoadIndex() |
检查指纹,加载已有索引或重新构建。 |
embed() |
调用百炼嵌入 API,把文本转换为向量。 |
readChunks() / writeIndex() |
从本地 JSON 恢复索引,或保存新索引。 |
retrieve() |
嵌入问题、计算相似度、排序并选出 Top K。 |
cosineSimilarity() |
计算两个向量的余弦相似度。 |
interact() |
读取用户输入、调度检索与生成、打印回答。 |
askModel() |
把系统提示词、问题和检索上下文发送给聊天模型。 |
五、Demo 的边界与现实系统
现在的 Demo
本地 JSON 保存所有片段向量;每个问题会遍历全部片段算相似度。结构简单,便于看懂数据如何流动。
规模变大以后
通常用向量数据库或搜索引擎保存和索引向量,以近似最近邻等索引方法加速查找,并通过后台流程增量更新资料。
因此,目前的 retrieve() 是教学用的穷举比较:资料片段多时,计算和内存成本会增长。未来升级时,可以把本地列表扫描替换为向量数据库的 Top K 查询。
运行与观察
$env:DASHSCOPE_API_KEY = "你的百炼 API Key" cd "D:\data\ai学习\ai\Page5" mvn compile exec:java
- 首次建索引会调用嵌入 API;资料和模型未变化时复用本地索引。
- 每个问题会调用嵌入 API 生成问题向量,并调用聊天 API 生成答案。
- 输入
/reindex强制重建,输入/quit退出。
RAG Demo 不只是“把资料发给模型”:程序先准备可搜索的向量索引,再用问题从索引中挑出少数相关片段,最后把这些片段作为上下文交给聊天模型。嵌入模型负责表示文本,检索代码负责选片段,聊天模型负责组织回答。
《实现一个最小的RAG》有一个想法