作者:前端转 AI 深度实践者
【省流助手/核心观点】:RAG 优化不能靠“我问了几句,感觉还不错”。只要你改了切块、Embedding、Rerank、Prompt 或引用逻辑,就可能出现“这个问题变好了,另一个问题又坏了”。前端开发者做知识库问答,至少要准备一套 20 条左右的 RAG 评测集,用固定问题验证命中率、拒答准确率、引用命中率和回归风险。
很多 RAG 项目不是死在“做不出来”,而是死在“上线后才发现答错了”。
前面几篇我们已经聊过:
- 文档切块会影响检索质量
- 检索范围不能无脑全量搜索
- Rerank 能把更关键的资料排到前面
- 答案要带引用来源,不能只会说
这些优化都很有价值。
但你还需要回答一个更现实的问题:
这次改动真的让系统变好了吗?
很多团队调 RAG 的流程是这样的:
- 改一下 Prompt。
- 手动问 3 个熟悉的问题。
- 觉得答案更顺了。
- 上线。
- 用户用另一个问法把系统问崩。
问题不在于手动验证没用,而在于它太不稳定。你问的几个问题只能证明“这几个问题暂时没问题”,不能证明整个知识库问答系统没有回归。
这篇文章讲一个特别适合前端开发者落地的动作:给 RAG 做一套最小评测集。
1. 痛点:RAG 最怕“局部变好,全局变差”
RAG 不是一个简单函数,而是一条链路:
用户问题 -> 查询处理 -> 检索 -> Rerank -> 上下文拼装 -> 生成 -> 引用校验 -> 前端展示
你改其中任何一环,都可能影响最终答案。
常见情况是:
- 切块变大:上下文更完整,但检索噪音也变多。
- Rerank 加严:错误引用减少,但本来能回答的问题被拒答。
- Prompt 更保守:幻觉少了,但用户经常看到“无法确定”。
- 引用校验更严格:证据链更可信,但老问题大量降级。
这就是 RAG 优化的麻烦之处:单个 case 变好,不代表整体质量变好。
前端同学应该很熟悉这种风险。你修了一个移动端样式问题,桌面端可能被影响;你改了一个状态判断,另一个交互可能回归。
所以 RAG 也需要测试用例,而且要能反复跑。
2. 错误做法:只靠 console 和肉眼判断
很多早期项目会这样验收:
const questions = [
"怎么申请退款?",
"会员可以开发票吗?",
"接口超时怎么办?"
];
for (const question of questions) {
const answer = await askRag(question);
console.log(question, answer);
}
这段代码能帮你快速看效果,但它有 4 个明显缺口:
- 不知道标准答案应该是什么。
- 不知道应该命中哪些文档片段。
- 不知道这个问题是否应该拒答。
- 不知道和上一个版本相比是变好还是变差。
最后就会变成凭感觉判断:
- “这句好像还行。”
- “这个引用看起来也对。”
- “这版回答比上版自然。”
但 RAG 最危险的答案,往往就是“看起来很自然”的错误答案。
3. 正确做法:把问题、答案和证据写成评测集
一套最小可用的 RAG 评测集,不需要一开始就做到几百条。
先准备 20 条高频问题,就能拦住很多明显回归。
type RagEvalCase = {
id: string;
question: string;
expectedAnswer: string;
expectedChunkIds: string[];
shouldRefuse: boolean;
tags: Array<"policy" | "api" | "pricing" | "edge-case">;
};
const evalCases: RagEvalCase[] = [
{
id: "refund-001",
question: "订单超过 7 天还能退款吗?",
expectedAnswer: "超过 7 天通常不支持无理由退款,特殊情况需要人工审核。",
expectedChunkIds: ["refund_policy_03"],
shouldRefuse: false,
tags: ["policy"]
},
{
id: "invoice-001",
question: "个人用户可以开专票吗?",
expectedAnswer: "当前资料没有说明个人用户可以开专票。",
expectedChunkIds: [],
shouldRefuse: true,
tags: ["edge-case"]
}
];
这份数据里最重要的不是 expectedAnswer 写得多漂亮,而是 3 个判断边界:
- 这个问题应该回答,还是应该拒答?
- 正确证据应该来自哪些 chunk?
- 这个问题属于高频问题、边界问题,还是历史事故?
有了这些边界,你再做 RAG 优化,就不是“问两句看看”,而是可以稳定对比。
4. 评测不要只看答案文本,要看 4 个指标
很多人做 AI 评测时,只盯着答案文本像不像。
但知识库问答更关心的是:答案有没有依据、该拒答时有没有拒答、证据有没有命中。
先假设你的 RAG 接口返回这样的结构:
type RagAnswer = {
answer: string;
citations: Array<{
chunkId: string;
quote: string;
}>;
confidence: "high" | "medium" | "low";
};
指标 1:可回答问题是否答出来
资料里有答案的问题,系统不能过度保守。
function isAnswerHit(result: RagAnswer, testCase: RagEvalCase) {
if (testCase.shouldRefuse) return true;
return result.confidence !== "low" && result.answer.trim().length > 0;
}
这个指标可以快速发现一种问题:你为了减少幻觉,把系统调得什么都不敢答。
指标 2:应该拒答的问题有没有拒答
资料里没有答案的问题,系统不能强行编。
function isRefusalCorrect(result: RagAnswer, testCase: RagEvalCase) {
if (!testCase.shouldRefuse) return true;
return (
result.confidence === "low" ||
result.answer.includes("根据当前资料无法确定")
);
}
对企业知识库、客服知识库、内部制度问答来说,这个指标比“回答是否流畅”更重要。
指标 3:引用来源是否命中
答案说得像,不代表证据对。
function isCitationHit(result: RagAnswer, testCase: RagEvalCase) {
if (testCase.shouldRefuse) {
return result.citations.length === 0;
}
const citedChunkIds = new Set(
result.citations.map((citation) => citation.chunkId)
);
return testCase.expectedChunkIds.some((chunkId) =>
citedChunkIds.has(chunkId)
);
}
这个指标能帮你判断问题出在哪一层:
- 没命中正确 chunk:优先看切块、Embedding、检索和 Rerank。
- 命中了正确 chunk 但答案错:优先看 Prompt、上下文拼装和生成约束。
指标 4:是否出现回归
每次改动都要问一句:
这次修好了几个问题,又弄坏了几个问题?
type EvalResult = {
caseId: string;
answerHit: boolean;
refusalCorrect: boolean;
citationHit: boolean;
};
type EvalSummary = {
total: number;
answerHitRate: number;
refusalAccuracy: number;
citationHitRate: number;
};
function summarize(results: EvalResult[]): EvalSummary {
const total = results.length;
return {
total,
answerHitRate:
results.filter((item) => item.answerHit).length / total,
refusalAccuracy:
results.filter((item) => item.refusalCorrect).length / total,
citationHitRate:
results.filter((item) => item.citationHit).length / total
};
}
这几个数字不一定能代表全部体验,但它们能帮你从“感觉变好”进入“有数据可比”。
5. 一个可以直接改造的 RAG 评测脚本
下面这个脚本适合作为本地命令、CI 任务或发布前检查的雏形。
async function runRagEval(cases: RagEvalCase[]) {
const results: EvalResult[] = [];
for (const testCase of cases) {
const result = await askRag(testCase.question);
results.push({
caseId: testCase.id,
answerHit: isAnswerHit(result, testCase),
refusalCorrect: isRefusalCorrect(result, testCase),
citationHit: isCitationHit(result, testCase)
});
}
const summary = summarize(results);
console.table(results);
console.table([summary]);
if (summary.refusalAccuracy < 0.9) {
throw new Error("拒答准确率过低,存在无依据回答风险");
}
if (summary.citationHitRate < 0.8) {
throw new Error("引用命中率过低,证据链不可靠");
}
return { results, summary };
}
这套脚本的价值不是“自动判断所有答案对不对”,而是让你每次改动后都能快速发现明显退化。
对前端开发者来说,它就像给 RAG 加了一层端到端回归测试。
6. 再进一步:把新旧版本差异打出来
如果你想让评测结果更适合团队协作,可以把上一次结果保存成 JSON,再和本次结果做 diff。
function diffEvalResults(previous: EvalResult[], current: EvalResult[]) {
const previousMap = new Map(
previous.map((item) => [item.caseId, item])
);
return current
.map((item) => {
const old = previousMap.get(item.caseId);
if (!old) return null;
const wasPassed =
old.answerHit && old.refusalCorrect && old.citationHit;
const isPassed =
item.answerHit && item.refusalCorrect && item.citationHit;
if (wasPassed === isPassed) return null;
return {
caseId: item.caseId,
type: isPassed ? "fixed" : "regression",
previous: old,
current: item
};
})
.filter(Boolean);
}
这样评审时就不用争论“感觉如何”,而是直接看:
- 哪些 case 被修复了
- 哪些 case 回归了
- 回归发生在回答、拒答,还是引用命中上
这比只给一个总分更有用。
7. 生产环境避坑指南
1. 评测集不要只放标准问题
如果评测集全是 FAQ 里的标准问法,系统会显得很优秀。
真实用户不会这么客气。
建议至少加入这些问题:
- 资料里完全没有答案的问题
- 多份文档结论冲突的问题
- 文档过期但仍会被检索命中的问题
- 需要精确字段、金额、日期、比例的问题
- 用户表达口语化、错别字、缩写的问题
RAG 的事故经常发生在边界问题上,而不是标准问题上。
2. 线上错答要沉淀成 regression case
用户反馈“答错了”的问题,不要只修一次。
最好把它加入评测集,让以后每次上线前都会跑到它。
const regressionCase: RagEvalCase = {
id: "regression-20260427-001",
question: "免费版能导出团队数据吗?",
expectedAnswer: "免费版不支持导出团队数据。",
expectedChunkIds: ["pricing_limit_02"],
shouldRefuse: false,
tags: ["pricing", "edge-case"]
};
每一个线上事故,都应该变成下一次上线的护栏。
3. 不要把所有评判都交给大模型
LLM-as-a-judge 很有用,但不要把所有判断都交给模型。
优先用确定性规则判断:
- 是否触发拒答
- citation 的 chunkId 是否存在
- quote 是否出现在原文里
- 是否命中 expectedChunkIds
- 是否出现禁用承诺、敏感话术或越权建议
模型裁判可以用来判断语义相似度,但证据链校验尽量用代码做。
4. 小评测集要稳定,大评测集要分层
建议准备两套:
- smoke eval:20 条左右,每次改动都跑。
- full eval:100 到 300 条,上线前或每天定时跑。
smoke eval 负责挡住明显回归。
full eval 负责观察长期趋势。
5. 评测报告必须能定位问题
只输出一个 82 分,实际帮助很有限。
更有用的是每条 case 都记录这些信息:
- 用户问题
- 期望 chunkId
- 实际命中 chunkId
- 检索分数和 Rerank 分数
- 是否拒答
- 最终 answer
- citation 是否有效
这样你才能判断下一步该改切块、检索、排序、Prompt,还是引用校验。
8. 常见误区
误区 1:评测集要很大才有意义
不需要。刚开始 20 条就有价值。重点是覆盖高频问题、边界问题和历史事故。
误区 2:答案文本不完全一致就是错
不一定。自然语言回答可以有多种表达。RAG 评测更应该关注事实是否正确、引用是否命中、资料不足时是否拒答。
误区 3:线上有监控,就不需要离线评测
监控能告诉你事故已经发生。
评测集能帮你在上线前发现事故。
它们不是替代关系,而是互补关系。
误区 4:评测集写完就不用维护
错。评测集要跟业务一起演进。文档更新、产品策略变化、用户高频问题变化,都应该同步更新。
9. 给前端开发者的落地清单
如果你正在做知识库问答,可以按这个顺序补评测:
- 从客服记录、用户搜索词、产品 FAQ 里挑 20 个高频问题。
- 给每个问题标注是否应该拒答。
- 给可回答问题标注 expectedChunkIds。
- 每次改 Prompt、切块、Rerank 前后都跑一遍。
- 把线上错答问题加入 regression cases。
- 在 CI 或发布脚本里加入 smoke eval。
做到这一步,你的 RAG 优化就不再是调参碰运气,而是进入可验证、可回归、可持续迭代的工程状态。
结语
RAG 从 demo 到生产,最关键的变化不是代码变复杂,而是你开始用工程方式管理不确定性。
没有评测集时,每次优化都像凭感觉开车。
有了评测集后,你至少能回答三个问题:
- 这次改动真的变好吗?
- 哪些问题被修复了?
- 哪些问题被弄坏了?
前端开发者做 AI 应用,不要只满足于“页面能展示答案”。真正可交付的 RAG 系统,应该能被评测、能被复盘,也能在一次次改动后持续变稳。
能被评测的 RAG,才有资格持续优化。