AI 应用评测与质量保障
AI 功能上线后怎么知道效果好不好?用户满不满意?换个模型会不会更好?这些问题不能靠感觉——需要系统化的评测体系。本章讲清 LLM 评测方法、自动化测试、A/B 测试、质量指标监控和持续改进闭环。
1. 为什么需要评测
AI 应用和传统软件不同——输出不确定。同样的输入,AI 可能给出不同的回答(受 temperature、上下文、模型版本影响)。传统的 expect(output).toBe(expected) 单元测试不适用——你不能要求 AI 每次返回完全相同的文本。需要的是质量度量而非精确匹配。
你需要回答的核心问题:
- AI 回答的准确率是多少?
- 换一个模型/Prompt,效果会更好还是更差?
- RAG 检索到的文档相关性如何?
- 用户对 AI 回复的满意度怎样?
- 生产环境中 AI 的延迟和成本趋势?
2. 评测数据集
2.1 构建测试集
评测的第一步——准备一组有标准答案的测试用例。好的测试集应该覆盖不同类型(知识问答、代码生成、创意写作)、不同难度、和常见的边界情况。测试集不需要很大,50-200 条就够用,关键是代表性:
// evaluation/datasets/chat-eval.ts
interface EvalCase {
id: string
input: string // 用户输入
expectedOutput?: string // 期望的回答(精确匹配)
expectedTopics?: string[] // 期望包含的关键信息点
category: string // 分类:knowledge | coding | math | creative
difficulty: 'easy' | 'medium' | 'hard'
}
export const chatEvalDataset: EvalCase[] = [
{
id: 'next-001',
input: 'Next.js 的 App Router 和 Pages Router 有什么区别?',
expectedTopics: ['Server Components', '文件系统路由', 'layout', '嵌套路由'],
category: 'knowledge',
difficulty: 'easy',
},
{
id: 'code-001',
input: '用 TypeScript 写一个防抖函数',
expectedTopics: ['setTimeout', 'clearTimeout', '泛型', '返回函数'],
category: 'coding',
difficulty: 'medium',
},
{
id: 'rag-001',
input: '我们公司的退款政策是什么?',
expectedTopics: ['7天', '无理由退款', '联系客服'],
category: 'knowledge',
difficulty: 'easy',
},
]2.2 从生产日志提取
最有价值的测试数据来自真实用户:
// scripts/extract-eval-cases.ts
async function extractEvalCases() {
// 提取用户评分过的对话
const ratedMessages = await db
.select()
.from(messages)
.innerJoin(messageRatings, eq(messages.id, messageRatings.messageId))
.where(eq(messageRatings.rating, 'positive'))
.orderBy(desc(messages.createdAt))
.limit(200)
// 转为评测数据集
const evalCases = ratedMessages.map((row) => ({
id: row.messages.id,
input: row.messages.content,
referenceOutput: row.messages.content, // 好评回复作为参考
category: 'production',
}))
await writeFile('evaluation/datasets/production-eval.json', JSON.stringify(evalCases, null, 2))
}3. 自动化评测
3.1 LLM-as-Judge
用另一个 LLM 作为“评委”来评判 AI 回复的质量。这是目前最实用的自动评测方法。
原理:利用强模型(如 GPT-4o)的语义理解能力,让它按定义好的维度和评分标准打分。用 generateObject 强制输出结构化评分,避免模型返回“我觉得还行”这样模糊的回复。
局限性:
- 偏见:评判模型可能偏爱自己风格的回复(用 GPT-4o 评判可能对 GPT-4o 的回复打分更高)
- 成本:每次评测本身也消耗 Token,评测 200 条用例大约 $1-2
- 不适合精确验证:数学计算、代码正确性这类有确定答案的场景,不如直接跑代码/算运算结果
// evaluation/judges/llm-judge.ts
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
const evaluationSchema = z.object({
relevance: z.number().min(1).max(5).describe('回答与问题的相关性'),
accuracy: z.number().min(1).max(5).describe('信息的准确性'),
completeness: z.number().min(1).max(5).describe('回答的完整性'),
clarity: z.number().min(1).max(5).describe('表达的清晰度'),
reasoning: z.string().describe('评分理由'),
})
export async function judgeResponse(
question: string,
response: string,
referenceAnswer?: string,
) {
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: evaluationSchema,
prompt: `你是一个 AI 回复质量评估专家。请评估以下 AI 回复的质量。
用户问题:${question}
AI 回复:${response}
${referenceAnswer ? `参考答案:${referenceAnswer}` : ''}
请从相关性、准确性、完整性、清晰度四个维度打分(1-5分),并给出评分理由。`,
})
return {
...object,
overallScore: (object.relevance + object.accuracy + object.completeness + object.clarity) / 4,
}
}3.2 关键词匹配评测
和 LLM-as-Judge 互补的轻量级方法——不调用 AI,零成本、毫秒级。原理很简单:检查回复是否包含期望的关键信息点。不能判断“质量”,但能判断“是否提到了该提的内容”:
function evaluateTopicCoverage(
response: string,
expectedTopics: string[],
): { score: number; matched: string[]; missed: string[] } {
const matched: string[] = []
const missed: string[] = []
for (const topic of expectedTopics) {
if (response.toLowerCase().includes(topic.toLowerCase())) {
matched.push(topic)
} else {
missed.push(topic)
}
}
return {
score: matched.length / expectedTopics.length,
matched,
missed,
}
}3.3 RAG 评测
RAG 系统需要评测两个环节:检索质量(找到的文档是否相关)和生成质量(回答是否忠实于检索内容)。下面的四个指标借鉴了 RAGAS 框架(RAG Assessment)的评测体系:
- retrievalPrecision:检索准确率——检索到的文档中有多少是真正相关的(过低说明混入了垃圾文档)
- retrievalRecall:检索召回率——回答问题需要的信息是否都被检索到了(过低说明漏检了关键文档)
- answerFaithfulness:回答忠实度——AI 是否严格基于检索到的内容回答,还是“编造”了信息(这是 RAG 最重要的指标)
- answerRelevance:回答相关性——AI 是否回答了用户实际问的问题
// evaluation/judges/rag-judge.ts
interface RAGEvalResult {
retrievalPrecision: number // 检索到的文档中有多少是相关的
retrievalRecall: number // 相关文档中有多少被检索到了
answerFaithfulness: number // 回答是否忠实于检索到的内容(不编造)
answerRelevance: number // 回答是否回答了用户的问题
}
export async function evaluateRAG(
question: string,
retrievedChunks: string[],
answer: string,
groundTruth: string,
): Promise<RAGEvalResult> {
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: z.object({
retrievalPrecision: z.number().min(0).max(1),
retrievalRecall: z.number().min(0).max(1),
answerFaithfulness: z.number().min(0).max(1),
answerRelevance: z.number().min(0).max(1),
reasoning: z.string(),
}),
prompt: `评估以下 RAG 系统的质量:
用户问题:${question}
检索到的文档片段:
${retrievedChunks.map((c, i) => `[${i + 1}] ${c}`).join('\n')}
AI 回答:${answer}
标准答案:${groundTruth}
请评估:
1. retrievalPrecision: 检索到的文档中有多少是与问题相关的(0-1)
2. retrievalRecall: 回答问题所需的信息是否都被检索到了(0-1)
3. answerFaithfulness: 回答是否忠实于检索到的文档内容,没有编造信息(0-1)
4. answerRelevance: 回答是否切中了用户的问题(0-1)`,
})
return object
}3.4 批量评测运行器
// evaluation/runner.ts
import { generateText } from 'ai'
interface EvalResult {
caseId: string
input: string
output: string
scores: {
relevance: number
accuracy: number
completeness: number
clarity: number
overall: number
}
latencyMs: number
tokenUsage: { input: number; output: number }
}
export async function runEvaluation(
dataset: EvalCase[],
config: {
model: string
systemPrompt: string
temperature?: number
},
): Promise<EvalResult[]> {
const results: EvalResult[] = []
for (const testCase of dataset) {
const startTime = Date.now()
// 调用被测 AI
const { text, usage } = await generateText({
model: getModel(config.model),
system: config.systemPrompt,
prompt: testCase.input,
temperature: config.temperature,
})
const latencyMs = Date.now() - startTime
// LLM-as-Judge 评分
const scores = await judgeResponse(testCase.input, text, testCase.expectedOutput)
results.push({
caseId: testCase.id,
input: testCase.input,
output: text,
scores: {
relevance: scores.relevance,
accuracy: scores.accuracy,
completeness: scores.completeness,
clarity: scores.clarity,
overall: scores.overallScore,
},
latencyMs,
tokenUsage: { input: usage.promptTokens, output: usage.completionTokens },
})
console.log(`[${testCase.id}] Score: ${scores.overallScore.toFixed(2)}, Latency: ${latencyMs}ms`)
}
return results
}4. A/B 测试
4.1 模型对比
A/B 测试的核心原则:控制变量。对比模型时,保持 Prompt、测试集、温度等参数完全相同,只改变模型。这样分数差异才能归因于模型本身。同时对比三个维度:质量分数、响应延迟、成本——最贵的模型不一定是最好的选择:
// evaluation/compare.ts
async function compareModels() {
const dataset = chatEvalDataset
const configs = [
{ model: 'gpt-4o', systemPrompt: DEFAULT_PROMPT, label: 'GPT-4o' },
{ model: 'gpt-4o-mini', systemPrompt: DEFAULT_PROMPT, label: 'GPT-4o-mini' },
{ model: 'claude-sonnet', systemPrompt: DEFAULT_PROMPT, label: 'Claude Sonnet' },
]
const allResults: Record<string, EvalResult[]> = {}
for (const config of configs) {
console.log(`\n--- 评测 ${config.label} ---`)
allResults[config.label] = await runEvaluation(dataset, config)
}
// 输出对比报告
for (const [label, results] of Object.entries(allResults)) {
const avgScore = results.reduce((sum, r) => sum + r.scores.overall, 0) / results.length
const avgLatency = results.reduce((sum, r) => sum + r.latencyMs, 0) / results.length
const totalCost = results.reduce((sum, r) => {
return sum + calculateCost(label, r.tokenUsage.input, r.tokenUsage.output)
}, 0)
console.log(`${label}: 平均分 ${avgScore.toFixed(2)} | 延迟 ${avgLatency.toFixed(0)}ms | 费用 $${totalCost.toFixed(4)}`)
}
}4.2 Prompt 对比
保持模型不变,对比不同 Prompt 的效果:
const promptVariants = [
{
label: 'v1-简洁',
systemPrompt: '你是一个 AI 助手。简洁回答问题。',
},
{
label: 'v2-详细',
systemPrompt: '你是一个 AI 助手。请给出详细、有条理的回答,包含代码示例。',
},
{
label: 'v3-角色',
systemPrompt: '你是一个有 10 年经验的高级全栈工程师。回答技术问题时,给出最佳实践和常见陷阱。',
},
]4.3 线上 A/B 测试
离线评测只能证明“在测试集上效果更好”,线上 A/B 测试才能证明“对真实用户效果更好”。关键设计:用用户 ID 哈希分组(而非随机),确保同一个用户始终看到同一个版本,避免体验不一致。先只给 10% 用户新版本,观察数据确认改善后再扩大:
// app/api/chat/route.ts
export async function POST(request: Request) {
const { messages, chatId } = await request.json()
const session = await auth()
// 根据用户 ID 哈希分组
const variant = getABVariant(session.user.id, 'prompt-v3-test')
const systemPrompt = variant === 'control'
? CURRENT_PROMPT
: NEW_PROMPT_V3
const result = streamText({
model: openai('gpt-4o'),
system: systemPrompt,
messages,
onFinish: async ({ text, usage }) => {
// 记录实验信息
await db.insert(abTestLogs).values({
experiment: 'prompt-v3-test',
variant,
chatId,
userId: session.user.id,
inputTokens: usage.promptTokens,
outputTokens: usage.completionTokens,
})
},
})
return result.toDataStreamResponse()
}
// 简单的 A/B 分组
function getABVariant(userId: string, experiment: string): 'control' | 'treatment' {
const hash = crypto.createHash('md5').update(userId + experiment).digest('hex')
return parseInt(hash.slice(0, 8), 16) % 2 === 0 ? 'control' : 'treatment'
}5. 用户反馈
5.1 点赞/点踩
所有自动化评测都不能替代用户反馈。👍👎 是最简单有效的反馈机制——用户不需要写任何文字,只需点一下。反馈率通常在 5-15%,但这些数据极其宝贵——它们是真实用户对真实场景的判断:
// components/message-feedback.tsx
'use client'
export function MessageFeedback({ messageId }: { messageId: string }) {
const [rating, setRating] = useState<'positive' | 'negative' | null>(null)
async function submitRating(value: 'positive' | 'negative') {
setRating(value)
await fetch('/api/feedback', {
method: 'POST',
body: JSON.stringify({ messageId, rating: value }),
})
}
return (
<div className="flex gap-1 mt-1">
<button
onClick={() => submitRating('positive')}
className={`text-sm ${rating === 'positive' ? 'text-green-500' : 'text-gray-300'}`}
>
👍
</button>
<button
onClick={() => submitRating('negative')}
className={`text-sm ${rating === 'negative' ? 'text-red-500' : 'text-gray-300'}`}
>
👎
</button>
</div>
)
}5.2 反馈数据分析
// 统计好评率
async function getFeedbackStats(timeRange: { start: Date; end: Date }) {
const stats = await db
.select({
model: messages.model,
total: count(),
positive: count(sql`CASE WHEN ${messageRatings.rating} = 'positive' THEN 1 END`),
negative: count(sql`CASE WHEN ${messageRatings.rating} = 'negative' THEN 1 END`),
})
.from(messageRatings)
.innerJoin(messages, eq(messageRatings.messageId, messages.id))
.where(between(messageRatings.createdAt, timeRange.start, timeRange.end))
.groupBy(messages.model)
return stats.map((s) => ({
model: s.model,
total: s.total,
positiveRate: s.positive / s.total,
negativeRate: s.negative / s.total,
}))
}6. 质量监控看板
6.1 关键指标
| 指标 | 计算方式 | 目标 |
|---|---|---|
| 好评率 | 👍 / (👍 + 👎) | > 85% |
| 平均延迟 | TTFT(首 Token 时间) | < 1s |
| 错误率 | 失败请求 / 总请求 | < 1% |
| 平均 Token 消耗 | 总 Token / 总请求 | 持续优化 |
| RAG 检索命中率 | 有检索结果 / 总 RAG 请求 | > 90% |
6.2 监控 API
// app/api/admin/ai-metrics/route.ts
export async function GET() {
const now = new Date()
const dayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const [feedbackStats, usageStats, errorStats] = await Promise.all([
// 好评率
db.select({
positive: count(sql`CASE WHEN rating = 'positive' THEN 1 END`),
total: count(),
})
.from(messageRatings)
.where(gte(messageRatings.createdAt, dayAgo)),
// 用量统计
db.select({
totalRequests: count(),
avgInputTokens: sql<number>`avg(input_tokens)`,
avgOutputTokens: sql<number>`avg(output_tokens)`,
avgLatency: sql<number>`avg(latency_ms)`,
totalCost: sql<number>`sum(cost)`,
})
.from(tokenUsageLogs)
.where(gte(tokenUsageLogs.createdAt, dayAgo)),
// 错误率
db.select({
errors: count(sql`CASE WHEN status = 'error' THEN 1 END`),
total: count(),
})
.from(aiRequestLogs)
.where(gte(aiRequestLogs.createdAt, dayAgo)),
])
return Response.json({
positiveRate: feedbackStats[0].total > 0
? feedbackStats[0].positive / feedbackStats[0].total
: null,
totalRequests: usageStats[0].totalRequests,
avgLatencyMs: usageStats[0].avgLatency,
dailyCost: usageStats[0].totalCost,
errorRate: errorStats[0].total > 0
? errorStats[0].errors / errorStats[0].total
: 0,
})
}7. 持续改进闭环
AI 应用的质量不是“上线就完了”,而是需要持续迭代。下面的闭环借鉴了 PDCA 循环(Plan-Do-Check-Act)的思想:监控发现问题 → 分析根因 → 修改并验证 → 上线并继续监控。
收集反馈 → 分析低分回复 → 改进 Prompt/模型 → 评测验证 → 上线
↑ ↓
←←←←←←←←←← 监控线上指标 ←←←←←←←←←←←←←←←←←←←←←←←←←
7.1 低分回复分析
定期提取差评回复,分析失败模式:
async function analyzeBadResponses() {
const badResponses = await db
.select()
.from(messages)
.innerJoin(messageRatings, eq(messages.id, messageRatings.messageId))
.where(eq(messageRatings.rating, 'negative'))
.orderBy(desc(messages.createdAt))
.limit(50)
// 用 AI 分析失败模式
const { object: analysis } = await generateObject({
model: openai('gpt-4o'),
schema: z.object({
patterns: z.array(z.object({
category: z.string().describe('失败类别,如"信息不准确"、"回答过于笼统"'),
count: z.number(),
examples: z.array(z.string()),
suggestedFix: z.string(),
})),
topIssue: z.string(),
}),
prompt: `分析以下被用户标记为差评的 AI 回复,找出共同的失败模式和改进建议:
${badResponses.map((r) => `问题:${r.messages.content}\n---`).join('\n')}`,
})
return analysis
}7.2 Prompt 迭代流程
- 发现问题:监控看板发现好评率下降 / 某类问题差评多
- 分析原因:提取差评回复,用 AI 分析失败模式
- 修改 Prompt:根据分析结果调整 System Prompt
- 离线评测:用测试数据集对比新旧 Prompt 的分数
- A/B 测试:新 Prompt 上线给 10% 的用户
- 全量发布:A/B 测试数据确认提升后全量发布
// 版本化管理 Prompt
const PROMPT_VERSIONS = {
'v1.0': {
content: '你是一个 AI 助手...',
date: '2024-01-15',
evalScore: 3.8,
},
'v1.1': {
content: '你是一个专业的 AI 助手...',
date: '2024-02-01',
evalScore: 4.1,
changelog: '增加了拒绝编造信息的指令',
},
'v1.2': {
content: '你是一个有 10 年经验的...',
date: '2024-03-01',
evalScore: 4.3,
changelog: '增加了角色设定和代码示例要求',
},
}本章小结
- 评测数据集:手动编写 + 从生产日志提取高质量用例
- 自动评测:LLM-as-Judge(最实用)+ 关键词覆盖 + RAG 四维评测
- A/B 测试:模型对比、Prompt 对比、线上用户分组实验
- 用户反馈:👍👎 最简单有效,定期分析差评找改进方向
- 质量监控:好评率、延迟、错误率、Token 消耗、RAG 命中率
- 持续改进:发现问题 → 分析原因 → 修改 Prompt → 离线评测 → A/B 测试 → 全量发布
至此,AI 能力集成完整 8 章全部完成。