Spring AI 实战 RAG:文档解析、混合检索与权限控制
为什么大模型不知道公司的报销制度
很多团队在引入大模型后的第一周都会遇到同一个尴尬场景:业务方兴致勃勃地问“出差住宿标准是多少”,模型却顾左右而言他,或者编造一个看似合理但完全错误的数字。
这不是模型不够聪明,而是它的知识边界止步于训练数据。你公司的内部制度、最新的产品参数、历史工单记录,从来不在它的训练集里,也不应该在里面。
要让模型掌握“你的知识”,工程上通常有三条路:微调(Fine-tuning)、长上下文全塞、以及检索增强生成(RAG)。
微调常被误解为注入知识的手段。实际上,微调改变的是模型的“行为倾向”和“语气风格”,让它更像你的员工说话,但它很难准确记住并复述一条具体的、随时可能变化的事实。一旦制度更新,你需要重新训练,成本高昂且滞后。
长上下文全塞看起来简单直接,把所有文档扔进提示词。但这面临三个硬伤:每次请求都按全部 Token 计费,成本极高;上下文越长,模型对中间内容的注意力越稀释,容易遗漏关键信息;输入越长,首字生成延迟越高。
RAG 则是目前性价比最高的选择。它先把文档切块存入向量库,提问时只检索最相关的几段内容塞给模型。这种方式实时生效,修改文档即可同步更新;每次只支付少量 Token 费用;且每一段回答都能追溯到原文出处。在企业场景里,“能告诉用户答案在第几页”的价值,往往比单纯提高准确率更重要,因为这意味着业务人员可以自行核验。
RAG 的核心逻辑与常见误区
RAG 的原理听起来很简单:先检索,再生成。但在工程落地中,它包含四个独立环节,每个环节都有各自的失败模式。
首先是查询处理。如果用户用口语问“怎么报销”,而文档里写的是“费用核销流程”,语义匹配可能失效。
其次是检索阶段。这是决定 RAG 效果上限的关键。如果检索没召回正确文档,或者召回了用户无权查看的文档,后续步骤再完美也无济于事。
接着是重排与筛选。原始检索结果可能包含噪声,需要通过重排序模型(Rerank)或阈值截断来提纯。
最后是生成阶段。模型需要严格依据提供的片段回答,避免幻觉。
很多开发者调试 RAG 时喜欢直接改提示词,比如加强语气要求“请严格根据资料回答”。但如果检索回来的片段本身就是错的,改提示词毫无意义。正确的排查思路是“分段验证”:先把检索出来的片段打印出来看。如果片段里没有答案,问题出在检索侧(解析、分块、嵌入模型);如果片段里有答案但模型没用好,问题才出在生成侧。
一个重要的判断准则是:RAG 的效果八成由检索决定,模型只负责表达。不要试图通过更换更强的生成模型来解决“检索不到”的问题。
核心组件选型与策略
Embedding:语义的坐标化
Embedding 将文本映射为多维空间中的向量坐标。语义相近的文本,其坐标距离更近。选型时需注意三点:
第一,不同模型的向量空间不兼容。更换嵌入模型意味着全量重新嵌入,这是一次性的高成本操作,选型需谨慎。
第二,相似度阈值比 Top-K 更重要。如果用户的问题在知识库中无答案,检索仍会返回“最接近”的几条无关内容。设置合理的相似度阈值,让系统在无法确认时坦诚回答“不知道”,比强行编造答案更安全。
第三,中文语料需实测。不同模型对中文专业术语、缩写的理解差异较大。建议用 30-50 条真实问句构建小测试集,对比候选模型的召回表现。
分块(Chunking):被低估的关键环节
文档太长无法整体向量化,必须切分。分块策略直接影响检索精度。
推荐从“按结构切分 + 定长兜底”开始。利用文档原有的标题层级进行切分,能保证块内语义完整。对于过长的章节,再进行固定长度切分并设置重叠(Overlap),防止答案被切断。
表格处理是难点。按行切分会导致表头与数据分离,模型无法理解数值含义。正确做法是将整张表作为一个块,并在块首附加一句由模型生成的“表意摘要”,说明这张表的内容。
每块数据必须携带元数据:来源文件、页码、章节路径、文档版本以及权限标签。这些元数据是后续过滤和引用回传的基础。
向量库选型:复用现有设施
选型原则是“先看已有的是什么”。
如果团队已使用 PostgreSQL,首选 pgvector 扩展。它能在一个 SQL 中同时完成元数据过滤和向量检索,事务与备份体系成熟,适合百万级以下数据量。
若已使用 Redis 且对延迟敏感,可选用 Redis 向量搜索功能,但需评估内存成本。
如果已在用 Elasticsearch 且需要强大的混合检索能力,ES 是自然选择,尽管其向量性能略逊于专用库。
只有当数据量达到千万级或对向量性能有极致要求时,才考虑引入 Milvus 等专用向量数据库。选型时需重点验证其对“带过滤条件的向量检索”的支持效率,这直接关系到权限控制的实现难度。
混合检索:中文场景的必需品
纯向量检索对精确字符串不敏感。例如用户搜索文档编号“XX-2026-003”或具体产品型号,向量检索往往失效,因为这些字符串缺乏语义特征。
混合检索结合向量检索(管语义)和关键词检索(如 BM25,管精确匹配)。两路召回后,由于分数体系不同(余弦相似度 vs BM25 分数),不能直接相加。最佳实践是将两路候选合并去重后,交给重排序模型(Rerank)统一打分排序。
Rerank 虽然增加了一次模型调用延迟,但能显著提升“召回多但排序差”场景下的准确率。它只在少量候选集中精挑细选,性价比高。
工程落地:从入库到问答
文档解析与入库
“垃圾进,垃圾出”在 RAG 中体现得淋漓尽致。PDF 双栏排版错乱、扫描件无文本层、Excel 表格拍平,都会导致后续环节失效。
解析环节需针对不同类型处理:文字版 PDF 需纠正行序并剔除页眉页脚;扫描件必须经过 OCR 并抽查错误率;Word 文档需保留标题层级以辅助分块。
入库代码示例如下:
@Service
public class KbIngestService {
private final VectorStore vectorStore;
private final TokenTextSplitter splitter;
public void ingest(KbDoc doc, List<KbSection> sections) {
List<Document> docs = new ArrayList<>();
for (KbSection s : sections) {
// 结构切分后,过长部分再定长切分
List<String> pieces = s.text().length() > 800
? splitter.split(s.text())
: List.of(s.text());
for (int i = 0; i < pieces.size(); i++) {
docs.add(new Document(pieces.get(i), Map.of(
"doc_id", doc.id(),
"chunk_no", i,
"source_name", doc.fileName(),
"page_from", s.pageFrom(),
"section_path", s.path(),
"doc_version", doc.version(),
"acl_tags", String.join(",", doc.aclTags()) // 权限标签
)));
}
}
vectorStore.add(docs); // 批量写入
}
}注意元数据在入库时即打全,尤其是 acl_tags 权限标签,这是后续安全过滤的物理基础。
检索与权限控制
检索代码需显式包含权限过滤表达式:
public List<Document> search(String question, UserCtx user) {
SearchRequest req = SearchRequest.builder()
.query(question)
.topK(20) // 多取候选,供后续重排
.similarityThreshold(0.45) // 阈值标定
.filterExpression("acl_tags in [" + user.aclTagExpr() + "]") // 检索层过滤
.build();
return vectorStore.similaritySearch(req);
}权限过滤必须在检索层完成,绝不能依赖提示词。 如果在提示词中要求模型“不要回答无权查看的内容”,敏感数据已经进入了提示词上下文,可能出现在日志、调试信息或链路追踪中,造成数据泄露。这就好比先把机密文件发给外人,再嘱咐他保密,逻辑上是荒谬的。
此外,过滤表达式应同时应用于向量检索和关键词检索两路,避免通过关键词搜索绕过权限控制。
提示词约束与引用回传
系统提示词需包含三条硬约束:
- 只依据提供的资料片段回答,无信息则拒答。
- 每条结论标注来源编号,如 [1]。
- 若片段冲突,明确指出而非自行取舍。
引用回传时,需确保前端展示的引用编号与注入模型的片段顺序严格一致。代码如下:
private List<Citation> toCitations(List<Document> docs) {
List<Citation> list = new ArrayList<>();
for (int i = 0; i < docs.size(); i++) {
Document d = docs.get(i);
list.add(new Citation(
"[" + (i + 1) + "]",
(String) d.getMetadata().get("source_name"),
(Integer) d.getMetadata().get("page_from"),
(String) d.getMetadata().get("section_path"),
abbreviate(d.getText(), 120)
));
}
return list;
}评估与运维
分离式评估指标
RAG 测试需将“检索命中率”与“答案正确率”分开评估。
- 检索命中率:正确文档是否出现在 Top-K 中。反映分块、嵌入模型和检索策略的效果。
- 答案正确率:生成结果是否准确且引用正确。反映提示词和模型能力。
- 越权率:必须为 0。通过构造低权限用户查询高权限文档进行测试。
- 拒答正确率:对无答案问题是否正确拒答。
若检索命中率高但答案正确率低,优化提示词;若检索命中率低,优化分块和检索策略。
文档更新与版本管理
文档更新时,应按 doc_id 删除旧版本所有块,再入库新版本,避免新旧版本共存导致冲突。定期比对文档系统清单与向量库数据,清理“孤儿块”,防止已删除文档仍可被检索。
成本控制
RAG 的主要成本在于生成阶段的输入 Token。优化方向包括:控制注入片段数量(如 Top-3 而非 Top-10),限制每段长度,以及对简单问题分流至低价模型。检索和嵌入成本相对固定,非主要优化点。
总结
构建企业级 RAG 系统,核心不在于堆砌最先进的模型,而在于扎实的工程细节。文档解析的质量、分块的策略、混合检索的实现、以及严格的检索层权限控制,共同决定了系统的可用性与安全性。
记住五个核心判断:
- 检索决定上限,生成只决定表达。
- 相似度阈值比 Top-K 更重要,敢于拒答。
- 中文场景必需混合检索,解决专有名词匹配问题。
- 权限过滤必须在检索层实现,提示词不可靠。
- 文档解析是隐性天花板,上线前务必抽样验证。
通过分离式评估和线上反馈闭环,持续迭代检索策略,才能让 RAG 系统真正服务于业务,成为可靠的知识助手。