第2381篇:知识库质量监控——自动检测和修复RAG系统中的低质量内容
大约 6 分钟
第2381篇:知识库质量监控——自动检测和修复RAG系统中的低质量内容
适读人群:负责维护企业级RAG知识库的AI工程师 | 阅读时长:约18分钟 | 核心价值:建立知识库质量自动监控体系,主动发现和处理低质量内容而不是等用户投诉
上线运营半年后,产品经理和我说了一个奇怪的现象:用户对某类问题的满意度特别低,但我们查日志,那些问题确实被检索到了相关文档。
深挖下去才发现问题所在:知识库里有一批文档是半年前从旧系统迁移过来的,迁移时格式没处理好,里面有大量乱码、HTML标签、重复段落。AI检索到了这些文档,但内容质量太差,LLM无法从中提取有效信息,只能给出模糊的回答。
这让我意识到:知识库质量会随时间自然退化,不能只靠入库时的审核。需要持续的质量监控。
知识库质量的几个维度
/**
* 知识库质量评估框架
*
* 维度1:内容质量
* - 格式问题(乱码、HTML标签、特殊字符)
* - 信息密度(重复段落、无意义空白)
* - 完整性(截断的句子、不完整的内容)
*
* 维度2:向量质量
* - 孤立点(向量空间中离所有其他文档都很远,可能是异常内容)
* - 重复(向量几乎相同的文档,造成检索结果冗余)
* - 分布偏斜(某些主题过度密集,其他主题稀疏)
*
* 维度3:检索质量
* - 幽灵文档(在向量空间里存在但从来不被检索到)
* - 误导文档(经常被检索到但用户反馈没帮助)
* - 陈旧文档(内容已过时但还在被使用)
*
* 维度4:使用质量
* - 用户满意度(被用于生成答案后,用户是否满意)
* - 引用准确率(文档被引用后,AI的回答是否准确)
*/内容质量的自动检测
@Service
public class ContentQualityAnalyzer {
/**
* 对单个文档进行多维质量分析
*/
public QualityReport analyze(Document doc) {
QualityReport report = new QualityReport(doc.getId());
String content = doc.getContent();
// 检查1:格式问题
report.addIssues(detectFormatIssues(content));
// 检查2:内容完整性
report.addIssues(detectCompletenessIssues(content));
// 检查3:信息密度
report.addIssues(detectInformationDensityIssues(content));
// 检查4:语言质量
report.addIssues(detectLanguageQualityIssues(content));
// 综合评分
double score = calculateQualityScore(report.getIssues());
report.setQualityScore(score);
report.setLevel(mapScoreToLevel(score));
return report;
}
private List<QualityIssue> detectFormatIssues(String content) {
List<QualityIssue> issues = new ArrayList<>();
// HTML标签残留
if (content.matches(".*<[a-z][a-z0-9]*[^>]*>.*") ||
content.contains("</") || content.contains("/>")) {
issues.add(QualityIssue.warning(
IssueType.HTML_TAGS,
"内容包含HTML标签,可能影响检索质量"
));
}
// 乱码检测(中文文档中大量非中文ASCII字符)
long totalChars = content.length();
long chineseChars = content.chars()
.filter(c -> c >= 0x4E00 && c <= 0x9FFF)
.count();
long nonMeaningfulChars = content.chars()
.filter(c -> c > 127 && (c < 0x4E00 || c > 0x9FFF))
.count();
if (nonMeaningfulChars > totalChars * 0.1) {
issues.add(QualityIssue.error(
IssueType.GARBLED_TEXT,
String.format("疑似含有乱码(非常规字符占比%.1f%%)",
nonMeaningfulChars * 100.0 / totalChars)
));
}
// 重复段落检测
String[] paragraphs = content.split("\\n\\n+");
Set<String> seen = new HashSet<>();
int duplicateCount = 0;
for (String para : paragraphs) {
String normalized = para.trim().toLowerCase();
if (normalized.length() > 20 && !seen.add(normalized)) {
duplicateCount++;
}
}
if (duplicateCount > 0) {
issues.add(QualityIssue.warning(
IssueType.DUPLICATE_CONTENT,
String.format("发现%d个重复段落", duplicateCount)
));
}
return issues;
}
private List<QualityIssue> detectCompletenessIssues(String content) {
List<QualityIssue> issues = new ArrayList<>();
// 截断检测:内容突然结束,没有完整的句子结尾
String trimmed = content.trim();
if (trimmed.length() > 100) {
char lastChar = trimmed.charAt(trimmed.length() - 1);
if (Character.isLetterOrDigit(lastChar)) {
issues.add(QualityIssue.warning(
IssueType.TRUNCATED_CONTENT,
"内容可能被截断(末尾没有完整的句子结束符)"
));
}
}
// 内容过短
if (content.trim().length() < 50) {
issues.add(QualityIssue.warning(
IssueType.TOO_SHORT,
"内容过短,可能信息量不足"
));
}
return issues;
}
private List<QualityIssue> detectInformationDensityIssues(String content) {
List<QualityIssue> issues = new ArrayList<>();
// 空白字符比例
long whitespaceCount = content.chars()
.filter(Character::isWhitespace)
.count();
double whitespaceRatio = (double) whitespaceCount / content.length();
if (whitespaceRatio > 0.5) {
issues.add(QualityIssue.warning(
IssueType.LOW_INFORMATION_DENSITY,
String.format("空白字符比例过高(%.1f%%),信息密度低", whitespaceRatio * 100)
));
}
return issues;
}
}向量质量的自动检测
@Service
public class VectorQualityAnalyzer {
private final VectorStore vectorStore;
/**
* 检测向量库中的重复文档
*
* 重复文档会导致检索结果冗余,浪费上下文窗口
*/
public List<DuplicateDocumentGroup> findDuplicates(float similarityThreshold) {
List<Document> allDocs = getAllDocuments();
List<DuplicateDocumentGroup> groups = new ArrayList<>();
Set<String> processedIds = new HashSet<>();
for (Document doc : allDocs) {
if (processedIds.contains(doc.getId())) continue;
// 找与当前文档相似度高的其他文档
List<Document> similar = vectorStore.similaritySearch(
SearchRequest.query(doc.getContent())
.withTopK(10)
.withSimilarityThreshold(similarityThreshold)
);
// 排除自身
List<Document> duplicates = similar.stream()
.filter(s -> !s.getId().equals(doc.getId()))
.collect(Collectors.toList());
if (!duplicates.isEmpty()) {
DuplicateDocumentGroup group = new DuplicateDocumentGroup();
group.setPrimaryDoc(doc);
group.setDuplicates(duplicates);
groups.add(group);
// 标记为已处理
processedIds.add(doc.getId());
duplicates.forEach(d -> processedIds.add(d.getId()));
}
}
return groups;
}
/**
* 检测"幽灵文档"——从来不被检索到的文档
* 这些文档可能是:
* 1. 主题太小众,没有用户问
* 2. 向量化质量差,偏离了语义空间
* 3. 已经过时但没有删除
*/
public List<Document> findGhostDocuments(int daysThreshold) {
LocalDate cutoff = LocalDate.now().minusDays(daysThreshold);
// 获取指定时间段内从未被检索到的文档
List<String> retrievedDocIds = queryLogRepository
.findDistinctDocIdsSince(cutoff);
return getAllDocuments().stream()
.filter(doc -> !retrievedDocIds.contains(doc.getId()))
.collect(Collectors.toList());
}
}自动修复策略
@Service
public class AutoRepairService {
/**
* 自动修复常见的格式问题
* 注意:只做低风险的修复,高风险问题需要人工处理
*/
public RepairResult autoRepair(String docId, List<QualityIssue> issues) {
Document doc = findDocumentById(docId);
String content = doc.getContent();
boolean modified = false;
List<String> appliedFixes = new ArrayList<>();
for (QualityIssue issue : issues) {
if (issue.getSeverity() != Severity.WARNING) continue; // 只自动修复WARNING级别
switch (issue.getType()) {
case HTML_TAGS -> {
content = removeHtmlTags(content);
modified = true;
appliedFixes.add("移除HTML标签");
}
case DUPLICATE_CONTENT -> {
content = removeDuplicateParagraphs(content);
modified = true;
appliedFixes.add("删除重复段落");
}
case EXCESSIVE_WHITESPACE -> {
content = normalizeWhitespace(content);
modified = true;
appliedFixes.add("规范化空白字符");
}
default -> {
// 其他类型的问题,标记需要人工处理
}
}
}
if (modified) {
// 更新文档内容
updateDocument(docId, content, "系统自动修复:" + String.join("、", appliedFixes));
return RepairResult.success(docId, appliedFixes);
}
return RepairResult.noChange(docId);
}
private String removeHtmlTags(String content) {
// 移除HTML标签,但保留文本内容
return content.replaceAll("<[^>]+>", " ")
.replaceAll(" ", " ")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\\s{2,}", " ");
}
private String removeDuplicateParagraphs(String content) {
String[] paragraphs = content.split("\\n\\n+");
List<String> seen = new ArrayList<>();
StringBuilder result = new StringBuilder();
for (String para : paragraphs) {
String normalized = para.trim().toLowerCase();
if (!seen.contains(normalized)) {
seen.add(normalized);
result.append(para.trim()).append("\n\n");
}
}
return result.toString().trim();
}
}定时监控任务
@Service
public class KnowledgeQualityScheduler {
/**
* 每周定时扫描整个知识库
*/
@Scheduled(cron = "0 0 3 * * SUN") // 每周日凌晨3点
public void weeklyQualityScan() {
log.info("Starting weekly knowledge base quality scan");
List<Document> allDocs = getAllDocuments();
List<QualityReport> reports = new ArrayList<>();
List<String> autoFixedDocs = new ArrayList<>();
List<String> docsNeedReview = new ArrayList<>();
for (Document doc : allDocs) {
QualityReport report = qualityAnalyzer.analyze(doc);
reports.add(report);
if (report.getLevel() == QualityLevel.POOR) {
// 质量差:尝试自动修复
RepairResult repair = autoRepairService.autoRepair(
doc.getId(), report.getIssues()
);
if (repair.isFixed()) {
autoFixedDocs.add(doc.getId());
} else {
docsNeedReview.add(doc.getId());
}
} else if (report.getLevel() == QualityLevel.FAIR) {
docsNeedReview.add(doc.getId());
}
}
// 发送质量报告给内容团队
QualityScanSummary summary = QualityScanSummary.builder()
.totalDocs(allDocs.size())
.goodQuality((int) reports.stream().filter(r -> r.getLevel() == QualityLevel.GOOD).count())
.fairQuality((int) reports.stream().filter(r -> r.getLevel() == QualityLevel.FAIR).count())
.poorQuality((int) reports.stream().filter(r -> r.getLevel() == QualityLevel.POOR).count())
.autoFixedDocs(autoFixedDocs)
.docsNeedingReview(docsNeedReview)
.build();
notificationService.sendQualityReport(summary);
log.info("Quality scan complete: {} total, {} poor quality, {} auto-fixed",
allDocs.size(), summary.getPoorQuality(), autoFixedDocs.size());
}
}基于用户反馈的质量信号
@Service
public class UserFeedbackQualitySignal {
/**
* 利用用户反馈评估文档质量
*
* 如果一篇文档经常被检索到,但对应的回答
* 用户满意度很低,说明这篇文档质量有问题
*/
public void processNegativeFeedback(String queryId, String userId,
FeedbackType feedbackType) {
QueryLog queryLog = queryLogRepository.findById(queryId)
.orElseThrow();
// 为这次查询引用的每个文档记录负面反馈
for (String docId : queryLog.getRetrievedDocIds()) {
documentFeedbackRepository.save(DocumentFeedback.builder()
.docId(docId)
.queryId(queryId)
.feedbackType(feedbackType)
.recordedAt(LocalDateTime.now())
.build()
);
}
// 如果某文档积累了足够多的负面反馈,触发人工审查
long negativeCount = documentFeedbackRepository
.countNegativeFeedbackSince(queryLog.getRetrievedDocIds().get(0),
LocalDate.now().minusDays(7));
if (negativeCount >= 5) {
createReviewTask(queryLog.getRetrievedDocIds().get(0),
"累计" + negativeCount + "次负面用户反馈");
}
}
}知识库质量监控本质上是一个"持续集成"的概念——代码有CI来保证质量,知识库也需要类似的机制。建立了这套体系后,我们的知识库质量问题从"用户投诉后才发现"变成了"主动发现并修复",用户满意度提升了约15%。
