Agent 内对话质量评分:自动化机评与人工抽样校验

Agent 内对话质量评分:自动化机评与人工抽样校验

一、你的 Agent 质量评估只靠用户反馈,等负反馈来了问题已经存在两周

Agent 的对话质量评估是一个被严重低估的问题。大多数团队的评估方式是"看用户投诉数量"——投诉少了就是质量好了,投诉多了就去查。这个指标太滞后了。用户从感到不满到实际投诉,中间可能经过了 5-10 次对话体验,其中大部分只是默默离开,不会给任何反馈。

需要建立一套"持续对话质量评分"机制:每次用户会话结束后自动打分,正常分通过,低分告警,抽样人工复核。这套机制不能太重——如果每次评分都需要人工介入,那还不如只看投诉。关键是让自动化评分承担 95% 的工作,人工只复核那 5% 的边界 case。

自动化评分的维度可以从五个方向切入:任务完成度(用户的问题是否被解决)、回复质量(是否冗余、是否离题、是否幻觉)、对话效率(用了多少轮解决问题)、安全性(是否出现违规内容)、用户情绪(从用户消息中推断满意度)。

二、底层机制与原理剖析

对话质量评分采用双轨制机制:自动评分负责初筛 95% 的对话,人工复核负责校准和模型迭代 5% 的对话。具体执行流程为:会话结束后触发自动质量评分,系统从任务完成度、回复质量、对话效率、安全性、用户情绪五个维度进行加权综合评分。根据评分结果进行判定,80 分及以上标记为优质对话,60 至 79 分标记为合格对话,60 分以下标记为低质对话并生成告警。在抽样复核环节,低质对话抽取 5% 进入人工复核池,合格与优质对话则各抽取 1% 随机抽样进入复核池。人工评分完成后,系统将自动评分与人工评分进行偏差对比:若偏差超过 20 分,则该样本加入评分模型训练集,用于定期更新评分模型;若偏差在 20 分以内,则仅做记录。

核心评估维度的详细定义:

任务完成度(40% 权重):最高权重。用户提出的核心问题是否被解决。判断依据是最后一轮用户的反馈——用户说"谢谢""好的""明白了"暗示问题解决;用户说"不对""不是""还不行"暗示问题未解决。也可以通过检查 Agent 是否在所有工具调用成功后才结束来判断。

回复质量(25% 权重):是否出现事实错误(幻觉检测)、是否冗余(回复过长但信息密度低)、是否与当前任务无关(离题)。

对话效率(15% 权重):优质对话应该高效解决用户问题。正常情况下 3-5 轮解决问题是高效的,超过 10 轮通常意味着兜圈。

安全性(15% 权重):内容安全审核——是否返回了违规内容、泄露了系统指令或内部信息。
用户情绪(5% 权重):从用户的措辞中推断满意度。正面词汇(谢谢、好的、明白了)+1,负面词汇(不对、不行、算了)-1,中性 0。

三、生产级代码实现

""" Agent 对话质量评分引擎 五个维度评分 + 加权汇总 + 低分告警 + 抽样人工复核 """ from dataclasses import dataclass, field from typing import List, Dict, Optional, Tuple from enum import Enum import re import json import hashlib import random class ScoreLevel(Enum): EXCELLENT = "excellent" # >= 80 ACCEPTABLE = "acceptable" # 60-79 POOR = "poor" # < 60 @dataclass class ConversationTurn: """对话的一轮""" role: str # "user" | "assistant" | "system" content: str timestamp: str @dataclass class QualityScore: """对话质量评分""" conversation_id: str overall: int # 综合评分 0-100 dimensions: Dict[str, int] # 各维度评分 level: ScoreLevel reasons: List[str] # 扣分原因 needs_review: bool # 是否需要人工复核 review_priority: int # 复核优先级 0-100 @dataclass class ReviewTask: """人工复核任务""" conversation_id: str conversations: List[ConversationTurn] auto_score: QualityScore human_score: Optional[QualityScore] = None def score_deviation(self) -> int: """自动评分与人工评分的偏差""" if not self.human_score: return 0 return abs(self.auto_score.overall - self.human_score.overall) # ===== 用户情绪词典 ===== POSITIVE_SIGNALS = [ "谢谢", "好的", "明白了", "清楚了", "知道了", "可以", "行", "thanks", "ok", "got it", "perfect", "great", ] NEGATIVE_SIGNALS = [ "不对", "不是", "错了", "不行", "算了", "没用", "浪费", "不明白", "不懂", "没听懂", "重新", "再试", "wrong", "incorrect", "nope", "useless", ] class ConversationQualityScorer: """对话质量自动评分器""" def __init__(self): # 权重配置 self.weights = { "task_completion": 0.40, # 任务完成度 "reply_quality": 0.25, # 回复质量 "conversation_efficiency": 0.15, # 对话效率 "safety": 0.15, # 安全性 "user_emotion": 0.05, # 用户情绪 } # 复核抽样配置 self.review_sample_rate = { ScoreLevel.EXCELLENT: 0.01, # 1% 随机抽样 ScoreLevel.ACCEPTABLE: 0.02, # 2% 随机抽样 ScoreLevel.POOR: 0.30, # 30% 进入复核池 } def score(self, conversation_id: str, turns: List[ConversationTurn]) -> QualityScore: """对一段对话做综合评分""" if not turns: return QualityScore( conversation_id=conversation_id, overall=0, dimensions={}, level=ScoreLevel.POOR, reasons=["空对话"], needs_review=False, review_priority=0, ) dimensions = {} reasons = [] # 维度 1:任务完成度 task_score, task_reasons = self._score_task_completion(turns) dimensions["task_completion"] = task_score reasons.extend(task_reasons) # 维度 2:回复质量 quality_score, quality_reasons = self._score_reply_quality(turns) dimensions["reply_quality"] = quality_score reasons.extend(quality_reasons) # 维度 3:对话效率 efficiency_score, efficiency_reasons = self._score_efficiency(turns) dimensions["conversation_efficiency"] = efficiency_score reasons.extend(efficiency_reasons) # 维度 4:安全性 safety_score, safety_reasons = self._score_safety(turns) dimensions["safety"] = safety_score reasons.extend(safety_reasons) # 维度 5:用户情绪 emotion_score, emotion_reasons = self._score_user_emotion(turns) dimensions["user_emotion"] = emotion_score reasons.extend(emotion_reasons) # 综合评分 overall = sum( dimensions.get(dim, 0) * weight for dim, weight in self.weights.items() ) overall = int(round(overall)) overall = max(0, min(100, overall)) # 判定级别 if overall >= 80: level = ScoreLevel.EXCELLENT elif overall >= 60: level = ScoreLevel.ACCEPTABLE else: level = ScoreLevel.POOR # 是否需要人工复核 needs_review = self._should_review(level, overall) review_priority = 100 - overall if level == ScoreLevel.POOR else 50 return QualityScore( conversation_id=conversation_id, overall=overall, dimensions=dimensions, level=level, reasons=reasons, needs_review=needs_review, review_priority=review_priority, ) def _score_task_completion(self, turns: List[ConversationTurn]) -> Tuple[int, List[str]]: """评估任务完成度""" reasons = [] user_turns = [t for t in turns if t.role == "user"] if not user_turns: return 0, ["无用户消息"] # 检查最后一轮用户消息是否包含正面信号 last_user_msg = user_turns[-1].content has_positive = any(s in last_user_msg for s in POSITIVE_SIGNALS) has_negative = any(s in last_user_msg for s in NEGATIVE_SIGNALS) if has_negative: reasons.append("用户最后回合表示不满意") return 30, reasons if has_positive: return 95, reasons # 如果没有明确的信号,根据对话长度推断 assistant_turns = [t for t in turns if t.role == "assistant"] if len(assistant_turns) <= 2: return 80, reasons # 少数轮解决,推测完成 elif len(assistant_turns) <= 5: return 65, reasons else: reasons.append("对话轮数过多,可能未解决") return 45, reasons def _score_reply_quality(self, turns: List[ConversationTurn]) -> Tuple[int, List[str]]: """评估回复质量""" score = 100 reasons = [] assistant_turns = [t for t in turns if t.role == "assistant"] if not assistant_turns: return score, reasons for i, turn in enumerate(assistant_turns): # 检查是否过短(< 10 字,可能是不够详细的敷衍) if len(turn.content) < 10: score -= 10 reasons.append(f"第 {i+1} 轮回复过短") # 检查是否过长(> 1000 字,可能冗余) if len(turn.content) > 1000: score -= 5 reasons.append(f"第 {i+1} 轮回复过长") # 检查是否有幻觉标记(如编造不存在的信息) if "作为AI" in turn.content or "我没有相关信息" in turn.content: score -= 15 reasons.append(f"第 {i+1} 轮可能包含不确定信息") return max(0, score), reasons def _score_efficiency(self, turns: List[ConversationTurn]) -> Tuple[int, List[str]]: """评估对话效率""" reasons = [] user_turns = [t for t in turns if t.role == "user"] if not user_turns: return 0, reasons rounds = len(user_turns) # 理想轮数:1-3 轮解决 if rounds <= 3: return 100, reasons elif rounds <= 5: return 85, reasons elif rounds <= 8: reasons.append(f"对话共 {rounds} 轮,效率偏低") return 65, reasons else: reasons.append(f"对话共 {rounds} 轮,效率很低,可能兜圈") return 35, reasons def _score_safety(self, turns: List[ConversationTurn]) -> Tuple[int, List[str]]: """评估安全性(简化版)""" reasons = [] assistant_turns = [t for t in turns if t.role == "assistant"] # 简化的安全检查——生产环境应接入内容安全审核 API forbidden_patterns = [ r"(?i)password", r"(?i)token.*=.*[a-zA-Z0-9]{20,}", r"System Prompt", ] for i, turn in enumerate(assistant_turns): for pattern in forbidden_patterns: if re.search(pattern, turn.content): reasons.append(f"第 {i+1} 轮可能泄露敏感信息") return 0, reasons return 100, reasons def _score_user_emotion(self, turns: List[ConversationTurn]) -> Tuple[int, List[str]]: """评估用户情绪""" reasons = [] user_turns = [t for t in turns if t.role == "user"] if not user_turns: return 50, reasons emotion_score = 0 for turn in user_turns: positive_count = sum(1 for s in POSITIVE_SIGNALS if s in turn.content) negative_count = sum(1 for s in NEGATIVE_SIGNALS if s in turn.content) emotion_score += positive_count - negative_count # 归一化到 0-100 if emotion_score > 0: score = 50 + min(50, emotion_score * 10) elif emotion_score < 0: score = 50 - min(50, abs(emotion_score) * 10) else: score = 50 if score < 30: reasons.append("用户情绪偏负面") return score, reasons def _should_review(self, level: ScoreLevel, overall: int) -> bool: """判断是否应进入人工复核池""" sample_rate = self.review_sample_rate.get(level, 0.01) if level == ScoreLevel.POOR: # 低分:30% 概率进入复核 return random.random() < sample_rate # 高分和合格:小概率随机抽样 return random.random() < sample_rate def batch_score(self, conversations: List[Tuple[str, List[ConversationTurn]]]) -> List[QualityScore]: """批量评分""" results = [] for conv_id, turns in conversations: try: score = self.score(conv_id, turns) results.append(score) except Exception as e: results.append(QualityScore( conversation_id=conv_id, overall=0, dimensions={}, level=ScoreLevel.POOR, reasons=[f"评分异常: {e}"], needs_review=True, review_priority=90, )) return results def generate_review_batch( self, scores: List[QualityScore], conversations: Dict[str, List[ConversationTurn]] ) -> List[ReviewTask]: """生成人工复核任务批次""" tasks = [] for score in scores: if score.needs_review: turns = conversations.get(score.conversation_id, []) tasks.append(ReviewTask( conversation_id=score.conversation_id, conversations=turns, auto_score=score, )) # 按优先级排序 tasks.sort(key=lambda t: t.auto_score.review_priority, reverse=True) return tasks

四、边界分析与架构权衡

自动评分的准确度

基于规则的评分在边界 case 上误差很大。用户说"好的"可能只是礼貌,不代表问题真的解决了。用户说"算了"可能真的是问题没法解决、用户放弃了。自动评分应该被视为"筛子"而非"裁判"——筛出可能有问题的对话,最终的判断交给人工复核。

幻觉检测的难度

回复质量中的"幻觉检测"是一个独立的复杂问题。基于规则只能做最粗浅的检查(如匹配已知的幻觉模式),真正的检测需要单独训练或使用 LLM 做 Self-Consistency 验证。

适用边界

最适合对话量较大的 Agent(日均 > 1000 次会话),人工完全覆盖不可行。也适合需要 SLA 监控的多租户 Agent 平台——按租户统计平均分可以快速发现哪个租户的 Agent 体验下降。

禁用场景

不适合低对话量(日均 < 50 次)的 Agent——全部人工评审一遍不难,自动化系统反而过度工程化。也不适合对评分精度要求极高的场景(如医学咨询),自动评分的误判可能带来严重后果。

五、结语

对话质量评分的目标不是替代人工判断,而是让 95% 的对话自动打分、5% 的边界 case 进入人工复核。五维度评分覆盖了任务完成度、回复质量、效率、安全和用户情绪的完整面板。低分自动告警 + 抽样人工复核的组合,实现了质量监控的闭环。核心是让评分系统成为一个"发现问题的探测器",而非"完美评分的裁判员"。