第2391篇:负向检索过滤——如何确保RAG不检索到有害或错误的内容
大约 6 分钟
第2391篇:负向检索过滤——如何确保RAG不检索到有害或错误的内容
适读人群:关注RAG系统内容安全的AI工程师 | 阅读时长:约16分钟 | 核心价值:建立RAG系统的内容安全防护层,防止错误、有害或过期内容被检索和使用
有同事给我讲过一个他们项目里的事故:他们做的是食品行业的内部知识库,有人在里面上传了一份早年的培训材料,里面有一条已经被撤销的食品安全操作规范。知识库维护人员不知道这份材料的存在,没有删除它。
结果AI系统检索到这份材料后,按照已撤销的旧规范给员工提供了操作指导,差点出了食品安全事故。
这件事让我意识到:RAG系统的内容安全不只是"防止AI说坏话",更重要的是防止AI检索到和使用不该被使用的内容。
需要过滤的内容类型
/**
* RAG系统中需要过滤的内容类型
*
* 类型1:已废止/撤销的内容
* - 已被新版本替代的规定
* - 已撤回的产品说明
* - 已修正的错误信息
*
* 类型2:质量低下的内容
* - 明显的错别字、语法错误(影响理解)
* - 信息密度极低的内容(大量废话)
* - 截断不完整的内容
*
* 类型3:违规内容
* - 违反内容政策的文档
* - 含有个人敏感信息的文档
* - 竞争对手的专有信息(可能涉及法律问题)
*
* 类型4:有害信息
* - 煽动性或歧视性内容
* - 危险操作指导
* - 误导性或不实信息
*/文档入库时的前置过滤
@Service
public class DocumentPreIngestionFilter {
/**
* 文档入库前的多维度过滤检查
*/
public FilterResult filter(String content, DocumentMetadata metadata) {
List<FilterIssue> issues = new ArrayList<>();
// 检查1:内容有效性
issues.addAll(checkContentValidity(content));
// 检查2:敏感信息检测
issues.addAll(checkSensitiveInformation(content));
// 检查3:合规性检查
issues.addAll(checkComplianceRequirements(content, metadata));
// 判断是否允许入库
boolean hasBlockingIssue = issues.stream()
.anyMatch(i -> i.getSeverity() == IssueSeverity.BLOCK);
if (hasBlockingIssue) {
return FilterResult.blocked(issues);
}
List<FilterIssue> warnings = issues.stream()
.filter(i -> i.getSeverity() == IssueSeverity.WARN)
.collect(Collectors.toList());
return warnings.isEmpty()
? FilterResult.passed()
: FilterResult.passedWithWarnings(warnings);
}
/**
* 敏感信息检测:防止个人隐私数据进入知识库
*/
private List<FilterIssue> checkSensitiveInformation(String content) {
List<FilterIssue> issues = new ArrayList<>();
// 手机号码
Pattern phonePattern = Pattern.compile("1[3-9]\\d{9}");
if (phonePattern.matcher(content).find()) {
issues.add(FilterIssue.block(
IssueType.PII_DETECTED,
"文档包含手机号码,请脱敏后重新提交"
));
}
// 身份证号码
Pattern idPattern = Pattern.compile("\\d{17}[\\dX]");
if (idPattern.matcher(content).find()) {
issues.add(FilterIssue.block(
IssueType.PII_DETECTED,
"文档包含身份证号码,请脱敏后重新提交"
));
}
// 银行卡号
Pattern bankPattern = Pattern.compile("\\b\\d{16,19}\\b");
if (bankPattern.matcher(content).find()) {
issues.add(FilterIssue.warn(
IssueType.POSSIBLE_BANK_CARD,
"文档可能包含银行卡号,请人工确认"
));
}
return issues;
}
}检索时的实时过滤
@Service
public class RetrievalTimeFilter {
/**
* 在检索结果返回给LLM之前,过滤不合适的文档
*
* 这是最后一道防线,补充入库时的过滤
*/
public List<Document> filterRetrievedDocs(List<Document> docs, String question) {
return docs.stream()
.filter(doc -> isDocumentSafeToUse(doc, question))
.collect(Collectors.toList());
}
private boolean isDocumentSafeToUse(Document doc, String question) {
// 检查1:文档是否已被标记为失效
String lifecycle = (String) doc.getMetadata().getOrDefault("lifecycle_status", "active");
if ("retired".equals(lifecycle) || "deprecated".equals(lifecycle)) {
log.debug("Filtered out retired/deprecated document: {}", doc.getId());
return false;
}
// 检查2:文档是否已过期
String expiryDateStr = (String) doc.getMetadata().get("expiry_date");
if (expiryDateStr != null) {
LocalDate expiryDate = LocalDate.parse(expiryDateStr);
if (LocalDate.now().isAfter(expiryDate)) {
log.debug("Filtered out expired document: {}", doc.getId());
return false;
}
}
// 检查3:文档是否被标记为需要人工审核
String needsReview = (String) doc.getMetadata().getOrDefault("needs_review", "false");
if ("true".equals(needsReview)) {
log.debug("Filtered out document pending review: {}", doc.getId());
return false;
}
// 检查4:内容质量分数
String qualityScoreStr = (String) doc.getMetadata().getOrDefault("quality_score", "1.0");
double qualityScore = Double.parseDouble(qualityScoreStr);
if (qualityScore < 0.3) {
log.debug("Filtered out low quality document: {}, score: {}",
doc.getId(), qualityScore);
return false;
}
return true;
}
}用户输入的安全过滤
@Service
public class QuerySafetyFilter {
/**
* 用户查询的安全检查
*
* 场景:有用户可能尝试通过精心设计的查询
* 绕过系统的安全限制(Prompt Injection攻击)
*/
public QuerySafetyResult checkQuery(String query) {
// 检查1:Prompt Injection检测
if (containsPromptInjection(query)) {
return QuerySafetyResult.blocked(
"检测到异常的指令注入尝试",
SafetyViolationType.PROMPT_INJECTION
);
}
// 检查2:过于宽泛的查询(可能试图dump知识库)
if (isExcessivelyBroad(query)) {
return QuerySafetyResult.limited(
"查询范围过于宽泛,将返回有限结果",
SafetyViolationType.EXCESSIVE_SCOPE
);
}
// 检查3:含有有害意图的查询
if (containsHarmfulIntent(query)) {
return QuerySafetyResult.blocked(
"该查询不符合使用规范",
SafetyViolationType.HARMFUL_INTENT
);
}
return QuerySafetyResult.safe();
}
private boolean containsPromptInjection(String query) {
List<String> injectionPatterns = Arrays.asList(
"ignore previous instructions",
"忽略之前的指令",
"forget what you were told",
"你现在是", "扮演",
"system:", "assistant:",
"##JAILBREAK",
"<|im_start|>",
"\\n\\n---\\n\\n"
);
String lowerQuery = query.toLowerCase();
return injectionPatterns.stream()
.anyMatch(pattern -> lowerQuery.contains(pattern.toLowerCase()));
}
private boolean isExcessivelyBroad(String query) {
// 查询太短且过于宽泛
List<String> broadPatterns = Arrays.asList(
"列出所有", "显示全部", "导出所有文档",
"给我看所有", "把所有内容"
);
return broadPatterns.stream().anyMatch(query::contains);
}
}输出安全过滤
@Service
public class OutputSafetyFilter {
/**
* 对LLM生成的答案做安全检查
*
* 即使输入和检索都通过了,LLM可能从训练数据中
* 生成一些不应该出现在这个系统里的内容
*/
public OutputFilterResult filterOutput(String generatedAnswer, String question) {
// 检查1:是否意外包含了敏感信息(来自检索文档的泄露)
if (containsPII(generatedAnswer)) {
String sanitized = redactPII(generatedAnswer);
return OutputFilterResult.modified(sanitized, "已自动脱敏个人信息");
}
// 检查2:是否引用了过时的规定
if (referencesOutdatedPolicy(generatedAnswer)) {
return OutputFilterResult.modified(
generatedAnswer + "\n\n⚠️ 注意:部分内容可能引用了旧版规定,建议核实最新版本。",
"添加了时效性警告"
);
}
// 检查3:答案中是否有LLM的过度自信表述
// "一定"、"绝对"等过于确定的表述在知识库问答中应该谨慎
String moderated = moderateConfidenceLevel(generatedAnswer);
if (!moderated.equals(generatedAnswer)) {
return OutputFilterResult.modified(moderated, "调整了过度自信的表述");
}
return OutputFilterResult.safe(generatedAnswer);
}
private String redactPII(String text) {
// 手机号脱敏
text = text.replaceAll("1[3-9]\\d{9}", "1****$&".substring(5));
// 身份证脱敏
text = text.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2");
return text;
}
/**
* 将过于确定的表述改为更保守的表述
* "一定是X" -> "根据文档,通常是X"
*/
private String moderateConfidenceLevel(String text) {
// 简单的规则替换
text = text.replaceAll("一定(?!要)", "通常");
text = text.replaceAll("绝对(?!不)", "一般来说");
return text;
}
}违禁词和敏感主题的处理
@Service
public class SensitiveTopicHandler {
private final Set<String> blockedKeywords;
private final Map<String, String> sensitiveTopicGuidance;
/**
* 预定义的敏感主题处理
*
* 不同于完全拒绝,对于合理的敏感查询
* 可以给出安全的引导性回答
*/
public SensitiveTopicResult handle(String question, String regularAnswer) {
// 精确关键词匹配
for (String keyword : blockedKeywords) {
if (question.contains(keyword)) {
return SensitiveTopicResult.blocked(
"这类问题超出了本系统的服务范围,请联系相关专业机构。"
);
}
}
// 敏感主题检测
for (Map.Entry<String, String> entry : sensitiveTopicGuidance.entrySet()) {
if (question.contains(entry.getKey())) {
// 不直接回答,给出引导
return SensitiveTopicResult.guided(
entry.getValue(),
"敏感主题处理"
);
}
}
return SensitiveTopicResult.safe(regularAnswer);
}
@PostConstruct
public void initBlocklist() {
blockedKeywords = new HashSet<>(Arrays.asList(
// 根据具体业务场景配置
"泄露密码", "绕过审核", "伪造证明"
));
sensitiveTopicGuidance = new HashMap<>();
sensitiveTopicGuidance.put("竞争对手",
"关于竞争对手的信息,建议参考公开发布的市场分析报告。");
}
}内容安全是RAG系统的一个经常被低估的工程领域。很多团队在一次事故发生后才开始认真对待它。我建议把内容安全的各个过滤层从第一个版本就考虑进去,哪怕初期实现很简单,预留好扩展点,后续加强起来会容易很多。
