Appearance
课 3 · Bad Case 分析与回归测试
本课目标
系统性分析失败案例,找到根因,并建立回归测试防止已修复的问题再次出现。
关键理解:Bad Case 不是"系统出错了",而是"系统在哪类输入上不工作"——找到规律才能针对性改进。
Bad Case 分类
不是所有失败都一样,分类后才能对症下药:
| 类型 | 现象 | 根因 | 改进方向 |
|---|---|---|---|
| 召回失败 | 文档里有答案,但没被检索到 | 切块策略、Embedding 质量、索引问题 | 改切块、换 Embedding 模型、优化索引 |
| 幻觉 | 文档里没有,但答案编出来了 | prompt 约束不够、模型倾向 | 加引用约束、拒答策略 |
| 不完整 | 答案只涉及部分内容 | 多跳推理失败、上下文窗口截断 | 增加召回数量、改 prompt |
| 答非所问 | 找到了相关文档,但没回答问题 | 问题理解错误、prompt 设计 | Query Rewrite、改 prompt |
| 无答案未拒答 | 知识库没有,但给了个"答案" | 拒答策略缺失 | 加置信度判断、拒答 |
Bad Case 收集
typescript
// packages/eval-core/src/bad-case-collector.ts
import type { AnswerEvalResult } from './batch-eval'
export interface BadCase {
questionId: string
question: string
answer: string
contexts: string[]
metrics: {
faithfulness: number
answerRelevance: number
contextPrecision: number
}
type?: BadCaseType
rootCause?: string
}
export type BadCaseType =
| 'retrieval_failure'
| 'hallucination'
| 'incomplete_answer'
| 'off_topic'
| 'missing_rejection'
const THRESHOLDS = {
faithfulness: 0.7,
answerRelevance: 0.7,
contextPrecision: 0.5,
}
export function collectBadCases(
evalResults: AnswerEvalResult[],
allCases: Array<{ id: string; question: string; answer: string; contexts: string[] }>,
): BadCase[] {
return evalResults
.filter((r) =>
r.faithfulness < THRESHOLDS.faithfulness ||
r.answerRelevance < THRESHOLDS.answerRelevance ||
r.contextPrecision < THRESHOLDS.contextPrecision
)
.map((r) => {
const c = allCases.find((c) => c.id === r.questionId)!
return {
questionId: r.questionId,
question: c.question,
answer: c.answer,
contexts: c.contexts,
metrics: {
faithfulness: r.faithfulness,
answerRelevance: r.answerRelevance,
contextPrecision: r.contextPrecision,
},
}
})
}LLM 辅助根因分析
typescript
// packages/eval-core/src/root-cause-analyzer.ts
const rootCauseSchema = z.object({
type: z.enum(['retrieval_failure', 'hallucination', 'incomplete_answer', 'off_topic', 'missing_rejection']),
analysis: z.string(),
suggestion: z.string(),
})
export async function analyzeRootCause(badCase: BadCase) {
const { object } = await generateObject({
model: getModel(),
schema: rootCauseSchema,
prompt: `
分析以下 RAG 失败案例的根本原因。
问题:${badCase.question}
检索到的文档片段:
${badCase.contexts.map((c, i) => `[${i + 1}] ${c.slice(0, 200)}`).join('\n')}
系统答案:${badCase.answer}
指标:
- Faithfulness: ${badCase.metrics.faithfulness.toFixed(2)}(答案与文档的一致性)
- Answer Relevance: ${badCase.metrics.answerRelevance.toFixed(2)}(答案与问题的相关性)
- Context Precision: ${badCase.metrics.contextPrecision.toFixed(2)}(检索文档的精准度)
根据这些信息,判断失败类型和根因,提供改进建议。
`.trim(),
})
return object
}Bad Case 报告
typescript
// scripts/generate-bad-case-report.ts
const badCases = collectBadCases(evalResults, testCases)
// 并行分析根因
const analyzed = await Promise.all(
badCases.map(async (bc) => ({
...bc,
...(await analyzeRootCause(bc)),
}))
)
// 按类型统计
const byType = analyzed.reduce((acc, bc) => {
acc[bc.type] = (acc[bc.type] ?? 0) + 1
return acc
}, {} as Record<string, number>)
console.log('=== Bad Case 分布 ===')
Object.entries(byType).forEach(([type, count]) => {
console.log(`${type}: ${count} 条`)
})
// 输出报告
writeFileSync('datasets/bad-cases/report.json', JSON.stringify(analyzed, null, 2))回归测试
修复了某类 Bad Case 后,要确保下次改动不再引入:
typescript
// packages/eval-core/src/regression-test.ts
interface RegressionSuite {
name: string
cases: Array<{
question: string
expectedMinFaithfulness?: number
expectedMinRelevance?: number
shouldContain?: string[] // 答案应该包含的关键词
shouldNotContain?: string[] // 答案不应该包含的词(防幻觉)
}>
}
export async function runRegressionTest(suite: RegressionSuite) {
const failures: string[] = []
for (const c of suite.cases) {
const { answer, contexts } = await runFullPipeline(c.question)
if (c.expectedMinFaithfulness) {
const { score } = await evaluateFaithfulness(answer, contexts)
if (score < c.expectedMinFaithfulness) {
failures.push(`[${c.question}] Faithfulness ${score.toFixed(2)} < ${c.expectedMinFaithfulness}`)
}
}
if (c.shouldNotContain) {
for (const word of c.shouldNotContain) {
if (answer.includes(word)) {
failures.push(`[${c.question}] 答案包含禁用词: "${word}"`)
}
}
}
}
if (failures.length > 0) {
console.error('回归测试失败:')
failures.forEach((f) => console.error(' ✗', f))
process.exit(1)
} else {
console.log(`✓ 回归测试通过(${suite.cases.length} 个用例)`)
}
}在 CI 中每次改动后自动运行:
bash
# package.json
"scripts": {
"test:regression": "tsx scripts/run-regression.ts"
}本节产物
packages/eval-core/src/
bad-case-collector.ts # Bad Case 收集
root-cause-analyzer.ts # LLM 根因分析
regression-test.ts # 回归测试框架
datasets/bad-cases/
report.json # Bad Case 报告
scripts/
generate-bad-case-report.ts
run-regression.ts面试追问
线上效果变差后如何定位?
分三步:1)看指标趋势——Faithfulness、Answer Relevance 哪个先下降?缩小到答案质量问题还是检索问题;2)拉出近期的 Bad Case,看是否有明显规律(某类问题集中失败?某个文档的内容?);3)对比变更记录——最近改了什么(新文档导入、prompt 修改、模型切换)?回滚对比,定位具体改动。
如何解决幻觉问题?
幻觉主要来自"模型倾向于回答,而不是承认不知道"。改进方式:1)在 system prompt 里明确要求"如果答案不在文档里,直接说不知道";2)用结构化输出,要求模型先给出引用编号再生成答案,没有编号就不输出内容;3)答案后校验:检查答案中每个关键声明是否能在上下文中找到原文依据,低于阈值时拒绝返回或标记为不确定。