短剧农村小萌妻嫁到剧情介绍
2026-07-12 3394215
2026-07-12 0
企业RAG项目常因检索范围过宽导致回答混乱,Metadata Filter才是确保精准召回的关键。核心内容:1. 企业RAG的核心痛点:检索范围过宽2. Metadata Filter的作用与常见过滤维度3. 在Spring AI等框架中的落地实践
很多 Java 开发者第一次做 RAG,最容易把重点放在向量数据库选型上:到底用 Milvus、pgvector、Elasticsearch 还是 Redis?Embedding 模型怎么选?topK 设多少?这些都重要,但在企业知识库项目里,另一个问题往往更早暴露:用户问的是同一个问题,系统却把别的部门、别的租户、过期版本、未发布文档一起召回了。这不是模型能力问题,也不完全是向量数据库问题,而是 RAG 检索链路缺少了一个工程上非常关键的约束:Metadata Filter。
RAG 里的 Metadata,通常是文档切块时附带的一组结构化字段。比如:Embedding 负责表达文本语义,Metadata 负责表达业务边界。两者的分工很像 Java 后端里的“全文搜索 + SQL 条件”。相似度检索解决“内容像不像”,Metadata Filter 解决“这条数据有没有资格参与搜索”。常见过滤维度包括:{"tenantId": "t_001","department": "finance","docType": "policy","version": "2025","status": "published","visibility": "internal"}
| 过滤维度 | 作用 |
|---|---|
| tenantId | 多租户隔离,避免串数据 |
| department | 部门级知识隔离 |
| docType | 只检索制度、FAQ、接口文档等特定类型 |
| status | 排除草稿、废弃、待审核内容 |
| version | 限定最新版本或指定版本 |
| permission | 根据用户权限控制可见范围 |
| effectiveDate | 控制制度生效时间 |
这个例子里有三个关键点。第一,tenantId 不应该由前端传入,而应该来自后端登录态或鉴权上下文。第二,status == 'published' 这种条件要尽量固化在服务端,不能让用户通过 Prompt 改写。第三,topK 和 similarityThreshold 不是越大越好。过滤之后的候选集更干净,通常可以用更小的上下文换来更稳定的回答。如果你使用 Spring AI 的 Advisor 机制,也可以把过滤条件作为检索增强的一部分挂到 ChatClient 调用链里。但我更建议第一版先把“检索服务”单独封装出来,方便记录日志、调试召回结果、做评估集。@Servicepublic class KnowledgeSearchService {private final VectorStore vectorStore;public KnowledgeSearchService(VectorStore vectorStore) {this.vectorStore = vectorStore;}public Listsearch(String question, LoginUser user) { String filter = """tenantId == '%s' && status == 'published' && department in ['%s', 'public']""".formatted(user.tenantId(), user.department());SearchRequest request = SearchRequest.builder().query(question).topK(6).similarityThreshold(0.72).filterExpression(filter).build();return vectorStore.similaritySearch(request);}}
这里的 Metadata 不要设计得太随意。它不是给人看的备注,而是后续检索、权限、评估、审计都会依赖的索引字段。尤其是多租户系统,tenantId 必须是强约束。不要指望大模型理解“不要回答其他公司的内容”,这种边界应该由后端检索层保证。Mapmetadata = Map.of( "tenantId", tenantId,"department", department,"docType", "policy","status", "published","version", version,"source", fileName);Document document = new Document(chunkText, metadata);vectorStore.add(List.of(document));
你只能根据当前用户有权限的文档回答。
这句话可以作为补充约束,但不能替代 Metadata Filter。原因很简单:如果没有权限的文档已经被召回并塞进上下文,大模型就已经看到了它。此时再要求模型“不要使用”,本质上是在把权限控制交给概率模型。在 Java 后端视角里,这就像接口已经查出了全量订单,再让前端“不要展示别人的订单”。这不是权限控制,而是事故预备。正确顺序应该是: