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 迭代流程

  1. 发现问题:监控看板发现好评率下降 / 某类问题差评多
  2. 分析原因:提取差评回复,用 AI 分析失败模式
  3. 修改 Prompt:根据分析结果调整 System Prompt
  4. 离线评测:用测试数据集对比新旧 Prompt 的分数
  5. A/B 测试:新 Prompt 上线给 10% 的用户
  6. 全量发布: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 章全部完成。