Spring AI 实战 RAG:公司制度问答为何答不上报销标准?
企业知识库问答常面临模型不懂内部制度、幻觉严重及数据泄露风险。本文基于 Spring AI 2.0 详解 RAG 全链路落地方案,涵盖文档结构化解析、分块策略优化、pgvector/Redis 向量库选型、中文场景必需的混合检索及重排序机制。重点阐述为何权限过滤必须在检索层而非提示词层实现,并提供可追溯引用回传与分离式
为什么大模型答不上来公司的报销标准
不少团队把大模型接进来,头一周就碰上同一件尴尬事:业务同事满怀期待地问“出差住宿标准是多少”,模型要么绕开话题,要么给出一个看着像样、实则完全错误的数字。
问题不在模型笨。它的知识只到训练数据为止。公司内部制度、最新产品参数、历史工单,这些从来不在训练集里,本来也不该进去。
想让模型掌握“你自己的知识”,工程上有三条常见路径:微调(Fine-tuning)、把长上下文全塞进去、以及检索增强生成(RAG)。
微调经常被当成灌知识的手段,其实不是。它调的是模型的“行为倾向”和“语气风格”,让它说话更像你的员工;但要让模型准确记住并复述一条具体事实——还是随时会变的那种——很难。制度一改,就得重新训练,花钱多,还慢半拍。
长上下文全塞听着省事,把所有文档一股脑丢进提示词就行。可它有三个硬伤:每次请求都按全部 Token 计费,成本高得离谱;上下文一长,模型对中间内容的注意力被摊薄,关键信息容易漏;输入越长,首字生成延迟越大。
RAG 是眼下性价比最高的做法。文档先切块存进向量库,提问时只把最相关的几段捞出来交给模型。改文档就能同步生效,实时;每次只付少量 Token 的钱;每段回答还能追到原文出处。企业场景里,“能告诉用户答案在第几页”往往比单纯把准确率往上提更值钱——业务人员可以自己核验。
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)管精确匹配。两路召回之后不能直接相加,因为分数体系不一样,一边是余弦相似度,一边是 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 系统才能真正服务业务,成为可靠的知识助手。