21.12-模型质量监控
要点
- 模型质量监控不仅仅是性能监控,更重要的是输出质量
- 收集用户反馈(点赞/点踩)是最直接的质量指标
- 监控常见失败模式:拒绝回答、重复输出、幻觉
- 定期采样人工评审,建立质量基线
内容
1. 模型质量的维度
AI 模型的质量可以从多个维度评估:
| 维度 | 含义 | 如何监控 |
|---|---|---|
| 准确性 | 回答是否正确 | 用户反馈、人工评审 |
| 相关性 | 回答是否切题 | 用户反馈、相关性评分 |
| 完整性 | 回答是否完整 | finish_reason、长度统计 |
| 安全性 | 是否包含有害内容 | 内容过滤、安全审计 |
| 一致性 | 相同输入是否得到相似输出 | 重复测试、方差分析 |
2. 用户反馈收集
2.1 点赞/点踩接口
// src/routes/feedback.ts
app.post('/api/feedback', async (c) => {
const { requestId, rating, comment } = await c.req.json()
const userId = c.get('userId')
// 1. 记录反馈
await c.env.DB.prepare(`
INSERT INTO feedback (
request_id, user_id, rating, comment, created_at
) VALUES (?, ?, ?, ?, ?)
`).bind(
requestId,
userId,
rating, // 1 = 👍, -1 = 👎
comment,
Date.now()
).run()
// 2. 记录指标
await recordMetric(c.env, 'feedback.received', {
rating: String(rating),
userId,
}, [1])
return c.json({ success: true })
})2.2 前端集成
// frontend/src/components/ChatMessage.tsx
function ChatMessage({ message, requestId }) {
const [feedback, setFeedback] = useState(null)
const handleFeedback = async (rating: 1 | -1) => {
await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requestId, rating }),
})
setFeedback(rating)
}
return (
<div className="message">
<div className="content">{message.content}</div>
{message.role === 'assistant' && (
<div className="feedback">
<button
onClick={() => handleFeedback(1)}
disabled={feedback !== null}
className={feedback === 1 ? 'active' : ''}
>
👍
</button>
<button
onClick={() => handleFeedback(-1)}
disabled={feedback !== null}
className={feedback === -1 ? 'active' : ''}
>
👎
</button>
</div>
)}
</div>
)
}2.3 分析反馈数据
// src/routes/admin/quality.ts
app.get('/admin/quality/feedback', async (c) => {
const days = parseInt(c.req.query('days') || '7')
const startTime = Date.now() - days * 24 * 60 * 60 * 1000
// 1. 总体满意度
const overall = await c.env.DB.prepare(`
SELECT
COUNT(*) as total,
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) as positive,
SUM(CASE WHEN rating = -1 THEN 1 ELSE 0 END) as negative,
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) as satisfaction_rate
FROM feedback
WHERE created_at > ?
`).bind(startTime).first()
// 2. 按模型分组
const byModel = await c.env.DB.prepare(`
SELECT
cr.model,
COUNT(*) as total,
SUM(CASE WHEN f.rating = 1 THEN 1 ELSE 0 END) as positive,
SUM(CASE WHEN f.rating = -1 THEN 1 ELSE 0 END) as negative,
SUM(CASE WHEN f.rating = 1 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) as satisfaction_rate
FROM feedback f
JOIN cost_records cr ON f.request_id = cr.request_id
WHERE f.created_at > ?
GROUP BY cr.model
ORDER BY satisfaction_rate DESC
`).bind(startTime).all()
// 3. 负面反馈原因分析
const negativeComments = await c.env.DB.prepare(`
SELECT comment, COUNT(*) as count
FROM feedback
WHERE rating = -1
AND comment IS NOT NULL
AND comment != ''
AND created_at > ?
GROUP BY comment
ORDER BY count DESC
LIMIT 20
`).bind(startTime).all()
return c.json({
period: `last ${days} days`,
overall,
byModel: byModel.results,
commonIssues: negativeComments.results,
})
})3. 常见失败模式监控
3.1 拒绝回答
模型可能因为安全策略拒绝回答:
// src/lib/quality-monitor.ts
export async function checkRefusalRate(env: Env) {
const result = await env.DB.prepare(`
SELECT
model,
COUNT(*) as total_calls,
SUM(CASE WHEN
response LIKE '%I cannot%' OR
response LIKE '%I can't%' OR
response LIKE '%抱歉,我%' OR
response LIKE '%我无法%'
THEN 1 ELSE 0 END) as refusal_count,
SUM(CASE WHEN
response LIKE '%I cannot%' OR
response LIKE '%I can't%' OR
response LIKE '%抱歉,我%' OR
response LIKE '%我无法%'
THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) as refusal_rate
FROM cost_records
WHERE created_at > ?
GROUP BY model
`).bind(Date.now() - 24 * 60 * 60 * 1000).all()
for (const row of result.results) {
if (row.refusal_rate > 10) {
await sendAlert(env, {
level: 'P2',
type: 'high_refusal_rate',
message: `${row.model} 拒绝回答率 ${row.refusal_rate.toFixed(1)}% > 10%`,
})
}
}
return result.results
}3.2 重复输出
模型可能陷入循环重复:
export async function checkRepetitionRate(env: Env) {
const result = await env.DB.prepare(`
SELECT
request_id,
model,
response
FROM cost_records
WHERE created_at > ?
AND completion_tokens > 100
`).bind(Date.now() - 60 * 60 * 1000).all()
const repeated = result.results.filter(row => {
const response = row.response
// 检查是否有重复的段落
const sentences = response.split(/[.!?]/)
const uniqueSentences = new Set(sentences)
const repetitionRate = 1 - uniqueSentences.size / sentences.length
return repetitionRate > 0.3 // 30% 以上的句子重复
})
if (repeated.length > 0) {
await sendAlert(env, {
level: 'P2',
type: 'repetition_detected',
message: `检测到 ${repeated.length} 个请求存在重复输出`,
})
}
return repeated
}3.3 幻觉检测
模型可能编造不存在的信息(这个比较复杂,需要 LLM 辅助检测):
export async function checkHallucinationRate(env: Env) {
// 采样 10% 的负面反馈,用另一个 LLM 检测是否是幻觉
const samples = await env.DB.prepare(`
SELECT request_id, model, messages, response
FROM cost_records
WHERE request_id IN (
SELECT request_id FROM feedback WHERE rating = -1
)
AND created_at > ?
ORDER BY RANDOM()
LIMIT 100
`).bind(Date.now() - 7 * 24 * 60 * 60 * 1000).all()
let hallucinationCount = 0
for (const sample of samples.results) {
// 用另一个 LLM 检测是否是幻觉
const detection = await detectHallucination(
sample.messages,
sample.response,
env
)
if (detection.isHallucination) {
hallucinationCount++
}
}
const hallucinationRate = hallucinationCount / samples.results.length * 100
if (hallucinationRate > 20) {
await sendAlert(env, {
level: 'P1',
type: 'high_hallucination_rate',
message: `幻觉率 ${hallucinationRate.toFixed(1)}% > 20%`,
})
}
return { hallucinationRate, sampleCount: samples.results.length }
}
async function detectHallucination(
messages: string,
response: string,
env: Env
): Promise<{ isHallucination: boolean; reason: string }> {
const result = await callLLM({
model: 'gpt-4-turbo',
messages: [
{
role: 'system',
content: '你是一个事实核查专家。请判断以下回答是否包含幻觉(编造不存在的事实)。只回答 JSON:{"isHallucination": true/false, "reason": "..."}',
},
{
role: 'user',
content: `问题:${messages}\n\n回答:${response}`,
},
],
}, env)
return JSON.parse(result.choices[0].message.content)
}4. 质量指标监控
4.1 响应长度统计
export async function analyzeResponseLength(env: Env) {
const result = await env.DB.prepare(`
SELECT
model,
AVG(completion_tokens) as avg_tokens,
MIN(completion_tokens) as min_tokens,
MAX(completion_tokens) as max_tokens,
APPROX_QUANTILE(0.50, completion_tokens) as p50_tokens,
APPROX_QUANTILE(0.90, completion_tokens) as p90_tokens
FROM cost_records
WHERE created_at > ?
GROUP BY model
`).bind(Date.now() - 24 * 60 * 60 * 1000).all()
// 检查异常
for (const row of result.results) {
if (row.avg_tokens < 50) {
await sendAlert(env, {
level: 'P3',
type: 'short_responses',
message: `${row.model} 平均响应长度 ${row.avg_tokens} tokens,可能过于简短`,
})
}
}
return result.results
}4.2 finish_reason 分析
export async function analyzeFinishReasons(env: Env) {
const result = await env.DB.prepare(`
SELECT
model,
finish_reason,
COUNT(*) as count,
COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (PARTITION BY model) as percentage
FROM cost_records
WHERE created_at > ?
GROUP BY model, finish_reason
ORDER BY model, count DESC
`).bind(Date.now() - 24 * 60 * 60 * 1000).all()
// 检查 length(截断)比例
const lengthRatio = result.results
.filter(r => r.finish_reason === 'length')
.reduce((sum, r) => sum + r.percentage, 0) / result.results.length
if (lengthRatio > 20) {
await sendAlert(env, {
level: 'P2',
type: 'high_truncation_rate',
message: `${lengthRatio.toFixed(1)}% 的响应被截断(finish_reason=length)`,
})
}
return result.results
}5. 人工评审采样
定期采样请求进行人工评审:
// src/cron/quality-review.ts
export default {
async scheduled(event, env, ctx) {
// 每周一 00:00 执行
// 1. 随机采样 100 个请求
const samples = await env.DB.prepare(`
SELECT request_id, model, messages, response, user_id
FROM cost_records
WHERE created_at > ?
ORDER BY RANDOM()
LIMIT 100
`).bind(Date.now() - 7 * 24 * 60 * 60 * 1000).all()
// 2. 创建评审任务
for (const sample of samples.results) {
await env.DB.prepare(`
INSERT INTO quality_reviews (
request_id, model, messages, response, user_id,
status, created_at
) VALUES (?, ?, ?, ?, ?, 'pending', ?)
`).bind(
sample.request_id,
sample.model,
sample.messages,
sample.response,
sample.user_id,
Date.now()
).run()
}
// 3. 通知评审人员
await sendSlackAlert(env, {
level: 'INFO',
type: 'quality_review_ready',
message: `📋 本周有 ${samples.results.length} 个请求待评审`,
})
},
}评审人员可以在后台填写评审结果:
// src/routes/admin/quality-review.ts
app.post('/admin/quality-review/:id', async (c) => {
const id = c.req.param('id')
const { accuracy, relevance, completeness, safety, comment } = await c.req.json()
const reviewerId = c.get('userId')
await c.env.DB.prepare(`
UPDATE quality_reviews
SET
accuracy = ?,
relevance = ?,
completeness = ?,
safety = ?,
comment = ?,
reviewer_id = ?,
status = 'completed',
reviewed_at = ?
WHERE id = ?
`).bind(
accuracy, // 1-5 分
relevance, // 1-5 分
completeness, // 1-5 分
safety, // 1-5 分
comment,
reviewerId,
Date.now(),
id
).run()
return c.json({ success: true })
})6. 质量趋势分析
// src/routes/admin/quality-trends.ts
app.get('/admin/quality/trends', async (c) => {
const days = parseInt(c.req.query('days') || '30')
const startTime = Date.now() - days * 24 * 60 * 60 * 1000
// 1. 满意度趋势
const satisfactionTrend = await c.env.DB.prepare(`
SELECT
date(created_at / 1000, 'unixepoch') as date,
COUNT(*) as total,
SUM(CASE WHEN rating = 1 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) as satisfaction_rate
FROM feedback
WHERE created_at > ?
GROUP BY date
ORDER BY date
`).bind(startTime).all()
// 2. 各模型满意度对比
const modelComparison = await c.env.DB.prepare(`
SELECT
cr.model,
COUNT(*) as total,
SUM(CASE WHEN f.rating = 1 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0) as satisfaction_rate,
AVG(f.rating) as avg_rating
FROM feedback f
JOIN cost_records cr ON f.request_id = cr.request_id
WHERE f.created_at > ?
GROUP BY cr.model
ORDER BY satisfaction_rate DESC
`).bind(startTime).all()
return c.json({
period: `last ${days} days`,
satisfactionTrend: satisfactionTrend.results,
modelComparison: modelComparison.results,
})
})7. 实战:完整的质量监控系统
// src/cron/quality-monitor.ts
export default {
async scheduled(event, env, ctx) {
// 每 6 小时执行一次
const metrics = {
refusalRate: await checkRefusalRate(env),
repetitionRate: await checkRepetitionRate(env),
responseLength: await analyzeResponseLength(env),
finishReasons: await analyzeFinishReasons(env),
}
// 记录到指标系统
for (const [metric, values] of Object.entries(metrics)) {
for (const value of values) {
await recordMetric(env, `quality.${metric}`, {
model: value.model,
}, [value.value])
}
}
// 生成报告
const report = generateQualityReport(metrics)
await sendSlackAlert(env, {
level: 'INFO',
type: 'quality_report',
message: report,
})
},
}
function generateQualityReport(metrics: any): string {
return `📊 模型质量报告\n\n` +
`拒绝回答率: ${metrics.refusalRate.map(r => `${r.model}: ${r.refusal_rate.toFixed(1)}%`).join(', ')}\n` +
`平均响应长度: ${metrics.responseLength.map(r => `${r.model}: ${r.avg_tokens.toFixed(0)} tokens`).join(', ')}\n` +
`截断率: ${metrics.finishReasons.filter(r => r.finish_reason === 'length').map(r => `${r.model}: ${r.percentage.toFixed(1)}%`).join(', ')}`
}8. 小结
模型质量监控的关键点:
- 用户反馈:点赞/点踩是最直接的质量指标
- 失败模式:监控拒绝回答、重复输出、幻觉等常见问题
- 质量指标:响应长度、finish_reason、满意度趋势
- 人工评审:定期采样进行人工评审,建立质量基线
- 趋势分析:跟踪质量变化,及时发现问题
AI 模型的输出质量直接影响用户体验。通过自动化监控和人工评审相结合,可以持续提升模型质量。