摘要 本文给出一条可落地的企业RAG后端链路:文档入库时保留租户、知识域、来源和版本元数据,查询时先做身份到检索范围的转换,再执行向量检索,最后把命中文档与问题交给模型,并把引用一并返回。
示例基于Spring AI 2.0.0接口思路;官方文档显示Spring AI 2.0.x支持Spring Boot 4.0.x和4.1.x。向量库、模型和过滤语法应按实际实现调整。
- 运行环境和目标
示例环境:

图1 Spring Boot企业RAG链路(概念示意,依据正文结构整理)
ALT建议:Spring Boot企业RAG链路,主要包含:文档采集与解析、切分、权限元数据、Embedding与向量存储、检索与权限过滤、上下文组装与生成、引用、评测与审计。
text
Java 21
Spring Boot 4.0.x
Spring AI BOM 2.0.0
一个ChatModel实现
一个支持元数据过滤的VectorStore实现
pom.xml先锁定BOM,不要为每个Spring AI模块分别手写版本。
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>2.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-rag</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-vector-store</artifactId>
</dependency>
<!-- 再按模型与向量库选择对应starter -->
</dependencies>
官方Getting Started页面是版本兼容性的最终依据。若项目仍在Spring Boot 3.x,不要直接把Spring AI 2.0.0依赖塞入原工程,应选择兼容版本或单独部署AI服务。
- 先画清楚企业RAG链路
mermaid
flowchart LR
F"文件或业务知识" --> P"解析与切分"
P --> E"Embedding"
E --> V"VectorStore"
Q"用户问题" --> A"身份与数据范围"
A --> R"带元数据过滤的检索"
V --> R
R --> C"上下文与引用组装"
C --> L"ChatModel"
L --> O"答案、引用、Trace ID"
这条链路有两个不能省的旁路:

图2 从文档入库到带引用回答(概念示意,依据正文结构整理)
ALT建议:从文档入库到带引用回答,主要包含:解析文档、切分并继承ACL、写入向量库、按用户权限检索、生成并返回引用。
文档删除或权限变化后,要触发索引更新。
每次查询要记录用户范围、检索结果、模型版本和最终引用,敏感正文按策略脱敏。
- 文档如何切分并保留权限元数据?
切分的目标不是固定字符数,而是在语义完整、召回粒度和模型上下文成本之间平衡。制度文档可按标题层级加段落切分,表格需要保留表头,扫描PDF要把OCR置信度写入元数据。
定义入库对象:
java
public record KnowledgeChunk(
String chunkId,
String tenantId,
String knowledgeBaseId,
String aclScope,
String sourceUri,
String sourceTitle,
String sourceVersion,
int page,
String text
) {}
写入Document时保留可过滤字段和可溯源字段:
java
Document toDocument(KnowledgeChunk c) {
Map<String, Object> metadata = new HashMap<>();
metadata.put("tenantId", c.tenantId());
metadata.put("knowledgeBaseId", c.knowledgeBaseId());
metadata.put("aclScope", c.aclScope());
metadata.put("sourceUri", c.sourceUri());
metadata.put("sourceTitle", c.sourceTitle());
metadata.put("sourceVersion", c.sourceVersion());
metadata.put("page", c.page());
return new Document(c.chunkId(), c.text(), metadata);
}
aclScope是示例中的预计算检索范围,例如finance_read。真实企业常有用户、组织、角色、项目和密级的组合权限。不要直接把超长用户ID列表塞进每个向量元数据;可以先由权限服务把当前用户解析为有限知识域,再构造向量库支持的过滤表达式。
入库过程应幂等:
text
documentId + sourceVersion + chunkNo -> chunkId
同一版本重复导入不会生成重复向量;新版本成功后再切换活动索引,失败时保留旧版本。
- 如何在检索阶段做权限过滤?
权限过滤必须发生在检索阶段。先召回全部文档、再在生成前删除无权限片段,会让敏感内容进入日志、缓存或模型上下文。
下面用Spring AI的SearchRequest表达查询意图,字段与过滤表达式需要按所选VectorStore核对:
java
public record RetrievalScope(
String tenantId,
String knowledgeBaseId,
String aclScope
) {}
List<Document> retrieve(
VectorStore vectorStore,
String question,
RetrievalScope scope
) {
String filter = """
tenantId == '%s' &&
knowledgeBaseId == '%s' &&
aclScope == '%s'
""".formatted(
safe(scope.tenantId()),
safe(scope.knowledgeBaseId()),
safe(scope.aclScope())
);
SearchRequest request = SearchRequest.builder()
.query(question)
.topK(8)
.similarityThreshold(0.55)
.filterExpression(filter)
.build();
return vectorStore.similaritySearch(request);
}
safe()不能只是替换引号,最好不要拼字符串,而是使用实现提供的类型安全过滤API。示例阈值0.55也不是生产推荐值,必须通过自己的问答集调优。
对多租户向量库,隔离层级要根据风险选择。Milvus 2.6.x文档列出Database、Collection、Partition和Partition Key四类策略,它们在隔离、规模和灵活性上不同。高敏租户不应只因规模大就默认选择逻辑隔离最弱的方案。
- 如何生成答案并返回可点击引用?
为了完整记录命中文档,工程上可以显式检索,再将上下文交给ChatClient。这样引用不依赖模型"记得输出"。
java
public record Citation(
String title,
String uri,
String version,
Integer page
) {}
public record RagAnswer(
String answer,
List<Citation> citations,
String traceId
) {}
核心服务伪代码:
java
RagAnswer answer(String question, UserContext user) {
RetrievalScope scope = permissionService.resolve(user);
List<Document> docs = retrieve(vectorStore, question, scope);
if (docs.isEmpty()) {
return new RagAnswer(
"在当前权限和知识范围内没有找到足够证据。",
List.of(),
traceId()
);
}
String context = docs.stream()
.map(d -> "%s p.%s\n%s".formatted(
d.getMetadata().get("sourceTitle"),
d.getMetadata().get("page"),
d.getText()
))
.collect(Collectors.joining("\n\n"));
String result = chatClient.prompt()
.system("""
只能依据给定材料回答。
证据不足时明确说明不知道。
不得改变材料中的权限、金额和时间条件。
""")
.user("问题:%s\n\n材料:\n%s".formatted(question, context))
.call()
.content();
List<Citation> citations = docs.stream()
.map(this::toCitation)
.distinct()
.toList();
return new RagAnswer(result, citations, traceId());
}
生产实现还要限制上下文总长度、去重同源片段、避免把HTML或恶意指令直接拼进系统提示,并对引用做可访问性检查。答案引用应来自检索结果,而不是让模型凭空生成URL。
- 如何评测检索、答案和权限?
RAG不能只看最终答案。RAGAS论文与当前Ragas文档把评估拆为Context Precision、Context Recall、Faithfulness和Answer Relevance等维度;LangSmith教程也把正确性、相关性、groundedness和检索相关性分开。

图3 企业RAG评测指标框架,不包含虚构的实测数据。
ALT建议:企业RAG三层评测矩阵,主要包含:检索质量,Recall@K、上下文精度、回答质量,正确性、忠实性、拒答、权限安全,越权拦截、引用可见性、运行质量,时延、失败率、更新延迟。
最小评测集建议包含:
|------|-----------|----------------|
| 类别 | 样本 | 预期 |
| 标准问答 | 权威文档能直接回答 | 找到正确来源并给出准确答案 |
| 多版本 | 新旧制度都存在 | 只引用当前有效版本 |
| 无答案 | 知识库没有证据 | 拒答,不编造 |
| 越权 | 用户问无权文档 | 检索结果为空或仅返回有权内容 |
| 注入 | 文档中包含恶意指令 | 不改变系统规则、不调用工具 |
| 更新删除 | 原文撤回后再次查询 | 旧片段不再召回 |
建议同时记录Recall@K、上下文精度、答案正确性、引用正确率、越权召回数、P95时延和单次成本。LLM-as-judge可以辅助,但高风险问题要有人审和确定性断言。
- 常见故障与排查
答非所问:先检查切分和检索,再调整Prompt;不要直接换更大模型。
有答案但引用错:检查同源去重、版本字段和引用映射。
不同用户结果相同:检查身份到RetrievalScope的转换和过滤是否由向量库实际执行。
删除后仍能问到:检查异步索引任务、缓存和旧索引切换。
线上偶发错误:把模型、检索、权限和解析版本写入Trace,才能重放。
速众AI低代码开发平台可作为企业应用入口、流程和知识服务集成的一种承载方式,但本文代码是独立工程示例,不代表其产品源码。实际接入时仍要按平台当前接口、部署和权限模型核验。