Article
RAG质量评测与安全防护:从Rule-Based Evaluation到发布门禁
以个人知识库RAG问答为例,设计检索、引用、回答三层评估,六层安全防护,固定Evaluation Set和发布前审计门禁。
引言:RAG能回答,不等于可以公开
前两篇文章讨论了RAG系统的架构和检索实现。但公开/chat入口真正打开前,还要回答另一个问题:系统答错时,你能不能知道错在检索、引用还是生成?
一个典型失败场景是”如何联系”。如果系统召回了过时页面,回答可能很流畅,却把用户导向错误入口。这个问题不能只靠”回答看起来不错”发现。它需要检索、引用、回答三层都留下可诊断证据。
本文给出一套面向个人知识库RAG的质量门禁:三层质量评估、固定Evaluation Set、六层安全防护、Closeout Gate和Reverse Audit。它的目标不是证明系统已经完美,而是让每次发布都有可复核证据。
适用边界:本文适合公开内容问答、站内助手和文档搜索增强回答。不覆盖内部知识库权限隔离、PII合规处理、企业审计报表或多租户访问控制。
先看质量、安全和发布证据如何汇到同一个门禁:
图1:质量评估、安全门禁与发布闭环。它把“答得对不对”“该不该让请求继续往下游走”“这一版能不能发布”拆成三条并行但最终汇合的证据链。
三层质量评估体系
为什么需要分层评估?
传统RAG评测往往只有一个维度:“回答是否准确”。公开入口需要更细粒度的诊断能力。
示例故障:系统对”如何解决429错误”的回答流畅准确,但引用了旧版本文档。虽然回答内容本身可读,来源却已经过时。如果只测回答文本,这个问题会被掩盖。
分层评估的价值在于定位故障环节:是检索出了问题、引用出了问题,还是回答生成本身出了问题?
Layer 1: 检索质量(Retrieval Quality)
评估目标:系统是否找回了正确的外部知识
核心指标:
1. 预期来源召回率
interface RetrievalTest {
query: string;
expectedSources: string[]; // 预期应召回的来源URL
requiredFacts: string[]; // 回答需要覆盖的关键事实
minRecallRate: number; // 最低召回率阈值
}
// 测试示例
const retrievalTests: RetrievalTest[] = [
{
query: "AI-TDD如何约束AI输出?",
expectedSources: [
"https://hlluan.com/blog/ai-tdd-framework/"
],
requiredFacts: [
"Manifest",
"六个心智模型",
"准入门禁或交付门禁"
],
minRecallRate: 0.8 // 80%以上预期来源被召回
}
];
2. 低相关性过滤
interface RelevanceFilter {
// 向量检索分数阈值
minVectorScore: 0.35;
// 关键词检索强信号判断
hasStrongKeyword: boolean;
// 精排分数阈值
minRerankScore: 0.3;
}
// 过滤逻辑
if (!hasStrongKeyword && !hasStrongRerank && maxVectorScore < minVectorScore) {
return []; // 过滤掉低相关性结果
}
3. 当前页Scope验证
interface ScopeTest {
query: string;
intent: "current-page-summary";
currentUrl: string;
// 验证:返回的来源应限定在当前页
validate(sources: Source[]): boolean {
return sources.every(s => s.url === currentUrl);
}
}
Layer 2: 引用质量(Citation Quality)
评估目标:系统引用的来源是否可信、准确、无重复
核心指标:
1. 来源去重验证
interface DeduplicationRule {
// 同一页面的不同变体应被识别为同一来源
aliases: [
{ url: "https://hlluan.com/blog/post", title: "文章标题" },
{ url: "https://www.hlluan.com/blog/post/", title: "文章标题" },
{ url: "https://hlluan.com/blog/post?utm_source=test", title: "文章标题" },
];
// 验证:去重后只剩1个来源
validate(sources: Source[]): boolean {
return uniqueBy(sources, s => normalizeUrl(s.url)).length === 1;
}
}
2. 来源安全边界
// 禁止的来源URL模式
const forbiddenUrlPatterns = [
/^http:\/\//, // 非HTTPS
/localhost/, // 本地开发
/127\.0\.0\.1/, // 回环地址
/file:\/\//, // 本地文件
/workers\.dev/, // Workers开发域名
];
// 验证函数
function validateSourceUrl(url: string): boolean {
return !forbiddenUrlPatterns.some(pattern => pattern.test(url));
}
3. 来源新鲜度验证
// 优先使用D1中的新鲜metadata
async function enrichSourceFromD1(
source: RetrievedChunk,
env: Env
): Promise<Source> {
const row = await env.RAG_DB.prepare(`
SELECT title, url, updated_at
FROM rag_chunks
WHERE chunk_id = ?
`).bind(source.id).first<{
title: string;
url: string;
updated_at: string;
}>();
return {
...source,
title: row?.title ?? source.title, // 使用D1新鲜标题
url: row?.url ?? source.url, // 使用D1新鲜URL
};
}
Layer 3: 回答质量(Answer Quality)
评估目标:生成的回答是否完整、准确、符合预期语言
核心指标:
1. 必需事实覆盖
interface AnswerTest {
query: string;
answer: string;
requiredFacts: string[];
// 验证:所有必需事实都出现在回答中
validate(): boolean {
return requiredFacts.every(fact =>
answer.toLowerCase().includes(fact.toLowerCase())
);
}
}
2. 语言行为验证
// 文本语言推断
function inferTextLocale(text: string): "zh-CN" | "en" {
// 有英文字母且无中文字符 → 英文
const hasEnglish = /[a-z]/i.test(text);
const hasChinese = /\p{Script=Han}/u.test(text);
return (hasEnglish && !hasChinese) ? "en" : "zh-CN";
}
// 验证:回答语言与问题语言一致
test("Language matching", () => {
expect(inferTextLocale("How does AI-TDD govern AI output?")).toBe("en");
expect(inferTextLocale("AI-TDD如何约束AI输出?")).toBe("zh-CN");
});
3. Fallback策略验证
// 当证据不足时,系统应明确说明而非编造
interface FallbackTest {
query: string;
evidenceSufficient: false;
// 验证回答
validate(answer: string): boolean {
// 应包含"未找到充分依据"等明确说明
const hasExplicitLabel = /未找到|知识库.*不足|insufficient evidence/i.test(answer);
// 证据不足时不应继续给出来源编号
const hasCitationMarker = /\[source-\d+\]/i.test(answer);
return hasExplicitLabel && !hasCitationMarker;
}
}
Evaluation Set设计
什么是Evaluation Set?
Evaluation Set是一组预定义的测试用例,用于持续评估RAG系统的质量。不同于随机的用户查询,Evaluation Set覆盖高价值场景,降低关键入口被遗漏的概率。
九类高价值问题
基于我的个人知识库内容,我定义了九类高价值问题:
| 类别 | 代表问题 | 预期来源 | 验收重点 |
|---|---|---|---|
| 1. 关于作者 | Hualin是谁?有什么技术背景? | /about | 不编造履历,引用作者页 |
| 2. 合作联系 | 如何联系进行合作? | /contact、/about | 联系入口新鲜,来源去重 |
| 3. 网站主题 | 网站覆盖哪些技术主题? | /topics | 能列出站内topic,不扩展到站外 |
| 4. 量化交易 | Micang Trader系列讲什么? | /blog/series-quant-trading/*、series index | 能区分架构、测试、性能、AI工程化 |
| 5. AI工程 | AI-TDD如何约束AI输出? | AI-TDD文章或AI工程专题 | 回答Manifest、六心智模型、门禁时不夸大标准化程度 |
| 6. Java或Python | Java GC如何选型?Python GIL为什么重要? | Java/Python系列文章 | 能命中系列内容和关键概念 |
| 7. 云原生/微服务 | 微服务治理如何从CF演进到云原生? | microservices-governance系列 | 能按演进脉络回答 |
| 8. 当前页总结 | 总结当前页面 | 当前URL对应document | 来源限定在当前页 |
| 9. 低相关性Fallback | 站内不存在的主题 | 无强证据 | 明确说明证据不足,不编造引用 |
这九类不是越多越好,而是覆盖个人知识库最容易出错的入口:作者身份、联系入口、主题导航、核心专题、当前页总结和无答案处理。
下面是一条可执行样例。正式评估集应为每一类至少准备一条中文和一条英文问题。category最好直接复用稳定的意图或评估ID,而不是临时自然语言字符串。
case_id: "topics-001"
category: "site-topics-overview"
question: "网站覆盖哪些技术主题?"
expectedEvidence:
sourcePatterns: ["/topics"]
requiredSourceIds: ["topics"]
requiredFacts:
- "AI工程化"
- "量化交易"
- "云原生"
fallbackPolicy:
allowGeneralKnowledge: false
requireLabel: true
expectedLanguage: "zh-CN"
citationExpectations:
requireDeduplication: true
maxSources: 4
Evaluation Case Schema
每个测试用例包含以下字段:
interface EvaluationCase {
id: string; // 稳定标识
category: string; // 问题类别(9类之一)
question: string; // 测试问题
expectedEvidence: { // 预期证据
sourcePatterns: string[]; // 来源URL模式
requiredSourceIds: string[]; // 必需来源ID
};
requiredFacts: string[]; // 回答需要覆盖的关键事实
fallbackPolicy: { // Fallback策略
allowGeneralKnowledge: boolean;
requireLabel: boolean;
};
expectedLanguage: string; // 预期回答语言
citationExpectations: { // 引用预期
requireDeduplication: boolean;
maxSources: number;
};
}
Rule-Based Evaluation
为什么选择Rule-Based而非LLM-as-Judge?
在首阶段质量门禁中,我坚持确定性规则而非LLM评判:
- 可解释性:规则失败可以直接定位到具体规则ID
- 可复现性:相同输入在同一规则版本下会得到相同结果
- 成本可控:不需要为每次门禁调用LLM API
- 速度更可控:规则检查通常可以在本地或边缘侧快速完成
LLM-as-Judge仅用于:人工抽查、趋势分析、细粒度质量研究,不作为首阶段门禁。
Evaluation Debug Report
每个失败的测试用例生成Debug Report:
{
"query": "如何联系进行合作?",
"category": "contact-collaboration",
"retrievalCandidates": [
{ "id": "chunk-001", "score": 0.82, "source": "vector" },
{ "id": "chunk-015", "score": 0.75, "source": "keyword" }
],
"selectedCitations": [
{ "url": "https://hlluan.com/contact/", "title": "Contact | Hualin Luan" },
{ "url": "https://www.hlluan.com/contact", "title": "Contact | Hualin Luan" }
],
"deduplicationResult": { "before": 2, "after": 1 },
"fallbackDecision": "sufficient",
"languageDecision": "zh-CN",
"failedRuleIds": [
"CQ-003" // 来源去重:同一页面变体未被折叠
]
}
固定评估用例 Debug Report 安全边界:
- ✅ 允许:固定评估集的query、category、source identifiers、rule IDs
- ❌ 禁止:embedding values、private Markdown、API keys、raw user data
六层安全防护架构
Layer 1: Kill Switch(紧急制动)
作用:一键关闭系统,停止当前请求继续进入昂贵或高风险调用链
if (env.RAG_CHAT_ENABLED !== "true") {
return errorResponse("rag_chat_disabled", 503, request, env);
}
使用场景:
- 上游API异常(如OpenAI服务中断)
- 预算超限
- 安全事件
- 紧急维护
关键特性:在进入检索、模型或其他高成本分支之前先检查,尽量把拒绝成本压到最低
Layer 2: Origin校验(来源控制)
作用:只允许特定域名的请求
const allowedOrigins = env.ALLOWED_ORIGINS
.split(",")
.map(origin => origin.trim())
.filter(Boolean);
const origin = request.headers.get("Origin");
if (!origin || !allowedOrigins.includes(origin)) {
return errorResponse("origin_not_allowed", 403, request, env);
}
控制目标:降低API被未授权站点直接调用的风险。Origin校验不是身份认证,不能替代服务端鉴权、限流和预算门禁。
Layer 3: Rate Limit(限流)
作用:限制单用户或单IP的异常高频请求
interface RateLimitConfig {
perMinute: 3;
perHour: 20;
perDay: 50;
}
// 多维度限流
async function checkRateLimit(
env: Env,
sessionId: string,
ipHash: string
): Promise<RateLimitResult> {
const sessionCheck = await checkSessionLimit(env, sessionId);
const ipCheck = await checkIpLimit(env, ipHash);
return {
allowed: sessionCheck.allowed && ipCheck.allowed,
retryAfter: Math.max(sessionCheck.retryAfter, ipCheck.retryAfter)
};
}
Layer 4: Daily Budget(日预算)
作用:在调用高成本上游前限制每日请求预算
const dailyLimit = parseInt(env.RAG_GLOBAL_DAILY_LIMIT ?? "500");
const todayCount = await getTodayRequestCount(env);
if (todayCount >= dailyLimit) {
return errorResponse("budget_exhausted", 429, request, env);
}
关键决策:在调用LLM、rerank或其他高成本上游之前先检查预算,避免把预算控制放到调用之后
Layer 5: Circuit Breaker(熔断)
作用:降低上游连续失败引发级联故障的概率
class CircuitBreaker {
private failureCount = 0;
private lastFailureTime: number | null = null;
private readonly threshold = 10; // 10次失败
private readonly timeout = 5 * 60 * 1000; // 5分钟熔断窗口
recordFailure(): void {
this.failureCount++;
this.lastFailureTime = Date.now();
}
isOpen(): boolean {
if (this.failureCount < this.threshold) return false;
if (!this.lastFailureTime) return false;
const elapsed = Date.now() - this.lastFailureTime;
if (elapsed < this.timeout) return true;
this.failureCount = 0;
this.lastFailureTime = null;
return false;
}
}
在Workers这类边缘运行时中,类内存状态只能作为说明性伪代码,不能作为跨请求、跨实例的权威熔断状态。落地实现应把失败计数和窗口状态写入KV或D1,并接受KV最终一致性带来的短暂误差。
Layer 6: Input Validation(输入校验)
作用:在进入检索和模型调用前裁剪明显危险或过大的输入
interface ValidationRules {
message: {
minLength: 1;
maxLength: 1000;
forbiddenPatterns: [/<script>/i, /javascript:/i];
};
pageContext: {
maxLength: 3000;
allowedFields: ["title", "url", "content"];
};
history: {
maxLength: 12;
itemMaxLength: 1000;
};
}
隐私保护设计
Telemetry脱敏原则
允许记录的字段(脱敏后):
interface TelemetryLog {
requestId: string; // 匿名ID
sessionHash: string; // HMAC后的session,不可逆
ipHash: string; // HMAC后的IP前缀,不保存原始IP
status: "success" | "error"; // 状态码
latencyMs: number; // 响应时间
retrievedCount: number; // 检索数量
indexEpoch: string; // 索引版本
}
禁止记录的字段:
// ❌ 禁止记录
{
message: "用户原始问题", // 隐私
pageContext: "原始页面内容", // 隐私
answer: "模型回答全文", // 隐私
rawIP: "192.168.1.1", // 隐私
embedding: [0.1, 0.2, ...], // 敏感
}
固定评估集替代方案:
- 第一版不做真实用户问题聚合
- 质量分析依赖固定评估问题集,并覆盖中英文与核心入口场景
- 避免隐私合规复杂度
治理与审计
本节使用三个治理术语。它们不是读者必须采用的产品名,而是发布前质量控制的角色划分。
| 术语 | 含义 | 在RAG发布中的作用 |
|---|---|---|
| Traceability Matrix | 需求、任务、证据、命令和结果的追踪表 | 让需求完成状态有可检查证据,而不是只靠口头声明 |
| Closeout Gate | 交付前需要通过的命令和证据门禁 | 阻断评估集、构建、安全测试失败的版本 |
| Reverse Audit | 从交付证据反查需求覆盖 | 发现”做了很多事但没覆盖关键需求”的问题 |
Requirement Traceability Matrix
每个关键需求都应有追踪链:
Trace Row:
id: TRACE-001
covers: [MUST-001, MUST-002, NEG-001] # 覆盖的需求ID
taskRefs: [TASK-001] # 关联任务
evidenceRefs: [EVD-001, EVD-002] # 证据引用
contractValidationCommandRefs: [CMD-CONTRACT-001]
deliveryEvidenceCommandRefs: [CMD-DELIVERY-001]
sequenceViewRefs: [SEQ-001]
artifactRefs: [ART-001]
status: PASS # PENDING / PASS / FAIL
Closeout Gate
交付前建议通过的门禁:
requiredCommands:
- CMD-DELIVERY-001: audit Evaluation Set
- CMD-DELIVERY-002: run answer-quality evaluation
- CMD-DELIVERY-003: E2E gate
- CMD-DELIVERY-004: build + export + audit
- CMD-DELIVERY-005: debug safety audit
blockingConditions:
- Evaluation Set missing required categories
- answer-quality runner missing deterministic rule results
- current-page summary drifts
- low-relevance displays unrelated sources
- debug report exposes sensitive fields
- LLM-as-Judge required for first-stage pass/fail
Reverse Audit
交付前执行反向审计:
node scripts/reverse_audit_contract.js \
path/to/confirmed-rag-requirement-contract.md
审计内容:
- implementationConfirmation block完整性
- requirement ID交叉引用一致性
- trace rows覆盖完整性
- evidence gates可执行性
Launch Gate检查清单
在公开入口启用前,建议把以下检查作为发布模板。它不是”本文系统已经全部完成”的声明,而是上线前应收集的证据清单:
基础设施检查
- Worker部署成功,/health返回ok
- Vectorize索引创建成功,维度=1024
- D1数据库创建成功,migrations已执行
- KV namespace创建成功
数据检查
- baseline verify通过
- /admin/stats返回正确indexEpoch
- /admin/manifest 返回的 manifest 数量与 corpus 一致
- corpusHash匹配
安全检查
- RAG_CHAT_ENABLED=false验证返回503
- 非法Origin返回403 origin_not_allowed
- rate limit阈值测试通过
- daily budget计数正常
质量检查
- 固定评估集覆盖9类问题,并记录每类中英文用例是否通过
- smoke chat来源引用正确
- hybrid/rerank开关可独立控制
- 当前页总结scope验证通过
发布决策
- 所有TRACE状态=PASS
- 所有EVD证据已收集
- Closeout Gate通过,并保存命令输出或审计报告
- 用户确认上线
总结
RAG质量保障不是单一环节的工作,而是贯穿设计、开发、测试和发布的证据链。
关键实践回顾
质量评估:
- 三层评估体系(检索/引用/回答)定位故障环节
- 九类高价值问题覆盖核心场景
- Rule-Based Evaluation提供确定性门禁
安全防护:
- 六层防护架构(Kill Switch→Input Validation)
- 隐私保护通过Telemetry脱敏实现
- 固定评估集替代用户问题聚合
治理审计:
- Traceability Matrix追踪需求闭环
- Closeout Gate阻断质量债务进入发布
- Reverse Audit复核交付证据覆盖
持续改进
当前机制:
- Rule-Based Evaluation作为首阶段门禁
- 固定评估集降低核心入口回归风险
- Debug Report支持故障诊断
未来演进:
- LLM-as-Judge用于人工抽查和趋势分析
- 用户反馈闭环(需补充隐私设计)
- A/B测试框架支持算法迭代
读者下一步:
- 先写9类固定评估问题,每类至少覆盖中文和英文各一条。
- 给每条问题写
expectedEvidence、requiredFacts和fallback策略。 - 在
RAG_CHAT_ENABLED=false状态下验证安全闸门,不要边公开边补门禁。 - 发布前保存Evaluation Set、answer-quality报告、来源去重报告和debug安全审计结果。
参考资料:
系列文章:
Reading path
继续沿这条专题路径阅读
按推荐顺序继续阅读 AI 工程化实践 相关内容,而不是只看同专题的随机文章。
Next step
继续深入这个专题
如果这篇内容对你有帮助,下一步可以回到专题页继续系统阅读,或者订阅后续更新。
正在加载评论...
评论与讨论
使用 GitHub 账号登录参与讨论,评论将同步至 GitHub Discussions