第39章 评估迭代:上线只是开始,迭代才是关键

第39章 评估迭代:上线只是开始,迭代才是关键

大白话: 传统软件上线后"不改就不会变坏",AI系统上线后"不改也会自己变坏"------用户问法变了、知识库过期了、模型版本更新了......你不动,它就慢慢"变蠢"。


39.1 为什么AI系统必须持续迭代

先说一个让很多人不舒服的事实:

AI系统不是"建好就完事"的项目,而是"养一辈子"的孩子。

传统软件的逻辑是:需求分析→开发→测试→上线→维护。上线之后只要不改代码,系统行为就是确定的。

AI系统的逻辑是:数据训练→部署上线→监控→发现问题→调数据/调Prompt/调参数→重新评估→灰度上线→监控......永远循环

为什么AI系统会"自己变坏"?

原因 大白话 影响
数据漂移 用户问的问题分布变了,原来训练/优化的场景不主流了 某类问题准确率突然下降
知识过期 RAG知识库里的信息老了,新政策/新产品/新价格没更新 回答过时甚至错误
模型版本更新 底层模型升级了,Prompt和行为可能需要重新调 原来好好的Prompt突然不好使了
用户行为演化 用户学会了"黑话"问法,模型没见过 理解不了用户意图
竞争对手机制 竞品上了新功能,用户期望值提高了 原来的回答质量"够用"变"不够用"
幻觉暴露 上线初期问题少,用久了边界case积累 差评越来越多

一句话总结: AI系统的"保质期"比传统软件短得多。不迭代不是"保持现状",而是"慢慢退步"。


39.2 迭代的完整闭环

一个健康的AI系统迭代流程长这样:

复制代码
监控发现问题 → 分析根因 → 提出优化方案 → 离线评估 → 灰度A/B测试 → 全量上线 → 持续监控
     ↑                                                                        |
     └────────────────────────────────────────────────────────────────────────┘

每个环节都不可或缺,跳过任何一步都会出问题:

环节 跳过的后果 大白话
监控发现问题 等用户投诉才知道出问题了 等病人叫疼才知道要检查
分析根因 头痛医头脚痛医脚,问题反复出现 不查病因只吃止疼药
离线评估 直接上线可能比之前还差 不考试就毕业,风险巨大
灰度A/B 全量上线翻车影响所有用户 不试吃就请全村人吃饭
持续监控 不知道这次优化到底有没有用 吃了药不复查

下面逐个环节展开讲。


39.3 第一步:从监控到发现问题

上一章讲了可观测性,这里讲怎么从监控数据中发现需要迭代的信号

39.3.1 四类迭代信号

信号类型 怎么发现 紧急程度 大白话
指标突变 准确率/满意度突然下降3%+ P0,立即处理 本来考90分突然掉到70分
指标缓降 连续2周满意度每周降0.5% P1,1周内分析 成绩慢慢下滑,温水煮青蛙
新问题涌现 某类问题差评集中爆发 P1,尽快处理 新题型没练过,集体不及格
用户行为变化 平均问题长度变了/重新提问率升了 P2,观察分析 答题习惯变了,得跟着调

39.3.2 自动发现问题的Pipeline

python 复制代码
from datetime import datetime, timedelta
from collections import defaultdict
from dataclasses import dataclass
from typing import List, Dict, Optional
import json

@dataclass
class IterationSignal:
    """迭代信号"""
    signal_type: str          # spike_decline / gradual_decline / new_pattern / behavior_shift
    severity: str             # P0 / P1 / P2
    metric: str               # 哪个指标出了问题
    description: str          # 问题描述
    affected_segment: str     # 影响范围
    detected_at: datetime     # 发现时间
    suggested_action: str     # 建议动作


class IterationSignalDetector:
    """迭代信号自动检测器"""
    
    def __init__(self, metrics_store, feedback_store, log_store):
        self.metrics = metrics_store      # 指标存储
        self.feedback = feedback_store    # 用户反馈
        self.logs = log_store            # 请求日志
    
    def detect_all(self) -> List[IterationSignal]:
        """运行所有检测规则"""
        signals = []
        signals.extend(self._detect_spike_decline())
        signals.extend(self._detect_gradual_decline())
        signals.extend(self._detect_new_problem_pattern())
        signals.extend(self._detect_behavior_shift())
        return sorted(signals, key=lambda s: {"P0": 0, "P1": 1, "P2": 2}[s.severity])
    
    def _detect_spike_decline(self) -> List[IterationSignal]:
        """检测指标突变(与昨天/上周同天对比)"""
        signals = []
        
        metrics_to_watch = [
            "satisfaction_rate",   # 满意度
            "accuracy_score",      # 准确率
            "hallucination_rate",  # 幻觉率
            "escalation_rate",     # 转人工率
        ]
        
        for metric in metrics_to_watch:
            today = self.metrics.get_daily_avg(metric, datetime.now())
            yesterday = self.metrics.get_daily_avg(metric, datetime.now() - timedelta(days=1))
            last_week = self.metrics.get_daily_avg(metric, datetime.now() - timedelta(days=7))
            
            # 与昨天对比下降3%以上,或与上周对比下降5%以上
            if yesterday and (yesterday - today) / yesterday > 0.03:
                signals.append(IterationSignal(
                    signal_type="spike_decline",
                    severity="P0",
                    metric=metric,
                    description=f"{metric}较昨天下降{((yesterday-today)/yesterday*100):.1f}%",
                    affected_segment="all",
                    detected_at=datetime.now(),
                    suggested_action="立即排查:模型服务状态、知识库更新、输入数据异常"
                ))
            elif last_week and (last_week - today) / last_week > 0.05:
                signals.append(IterationSignal(
                    signal_type="spike_decline",
                    severity="P1",
                    metric=metric,
                    description=f"{metric}较上周下降{((last_week-today)/last_week*100):.1f}%",
                    affected_segment="all",
                    detected_at=datetime.now(),
                    suggested_action="1周内排查:用户问题分布变化、知识库时效性"
                ))
        
        return signals
    
    def _detect_gradual_decline(self) -> List[IterationSignal]:
        """检测指标缓降(连续2周下降趋势)"""
        signals = []
        
        for metric in ["satisfaction_rate", "accuracy_score"]:
            # 取最近14天的日均值
            daily_values = []
            for i in range(14):
                day = datetime.now() - timedelta(days=i)
                val = self.metrics.get_daily_avg(metric, day)
                if val:
                    daily_values.append((day, val))
            
            if len(daily_values) >= 10:
                # 计算趋势:后7天均值 vs 前7天均值
                recent_avg = sum(v for _, v in daily_values[:7]) / 7
                old_avg = sum(v for _, v in daily_values[7:]) / 7
                
                if old_avg > 0 and (old_avg - recent_avg) / old_avg > 0.02:
                    signals.append(IterationSignal(
                        signal_type="gradual_decline",
                        severity="P1",
                        metric=metric,
                        description=f"{metric}连续2周缓降{((old_avg-recent_avg)/old_avg*100):.1f}%",
                        affected_segment="all",
                        detected_at=datetime.now(),
                        suggested_action="深度分析:知识库是否需要更新、Prompt是否需要优化、用户问题分布是否变化"
                    ))
        
        return signals
    
    def _detect_new_problem_pattern(self) -> List[IterationSignal]:
        """检测新问题模式涌现(某类问题差评突然增多)"""
        signals = []
        
        # 按问题类别分组统计差评率
        recent_negative = self.feedback.get_negative_by_category(days=3)
        baseline_negative = self.feedback.get_negative_by_category(
            start=datetime.now() - timedelta(days=30),
            end=datetime.now() - timedelta(days=3)
        )
        
        for category, recent_count in recent_negative.items():
            baseline_count = baseline_negative.get(category, 0)
            baseline_avg = baseline_count / 27  # 基线期27天日均
            
            # 近3天日均差评数是基线的2倍以上
            if baseline_avg > 0 and (recent_count / 3) > baseline_avg * 2:
                signals.append(IterationSignal(
                    signal_type="new_pattern",
                    severity="P1",
                    metric=f"negative_feedback:{category}",
                    description=f"'{category}'类问题差评激增,近3天日均{recent_count/3:.1f},基线日均{baseline_avg:.1f}",
                    affected_segment=category,
                    detected_at=datetime.now(),
                    suggested_action=f"提取'{category}'类差评Case,分析共性,针对性优化"
                ))
        
        return signals
    
    def _detect_behavior_shift(self) -> List[IterationSignal]:
        """检测用户行为变化"""
        signals = []
        
        # 问题长度变化
        recent_avg_len = self.logs.get_avg_question_length(days=7)
        old_avg_len = self.logs.get_avg_question_length(
            start=datetime.now() - timedelta(days=30),
            end=datetime.now() - timedelta(days=7)
        )
        
        if old_avg_len and abs(recent_avg_len - old_avg_len) / old_avg_len > 0.2:
            direction = "变长" if recent_avg_len > old_avg_len else "变短"
            signals.append(IterationSignal(
                signal_type="behavior_shift",
                severity="P2",
                metric="avg_question_length",
                description=f"用户问题平均长度{direction}:{old_avg_len:.0f}→{recent_avg_len:.0f}字符",
                affected_segment="all",
                detected_at=datetime.now(),
                suggested_action="分析问题变{direction}的原因,可能需要调整上下文窗口或切分策略"
            ))
        
        # 重新提问率变化
        recent_retry = self.metrics.get_retry_rate(days=7)
        old_retry = self.metrics.get_retry_rate(
            start=datetime.now() - timedelta(days=30),
            end=datetime.now() - timedelta(days=7)
        )
        
        if old_retry and recent_retry > old_retry * 1.5:
            signals.append(IterationSignal(
                signal_type="behavior_shift",
                severity="P1",
                metric="retry_rate",
                description=f"重新提问率升高:{old_retry*100:.1f}%→{recent_retry*100:.1f}%",
                affected_segment="all",
                detected_at=datetime.now(),
                suggested_action="分析重新提问的Case,用户可能在尝试不同问法寻找满意答案"
            ))
        
        return signals

大白话: 这套Pipeline就像给AI系统装了个"体检机器人"------每天自动检查各项指标,发现异常立刻报警。不用等用户骂你,你自己先知道出了什么问题。


39.4 第二步:根因分析

发现问题只是第一步,更重要的是找到根因------不然你永远在"头痛医头脚痛医脚"。

39.4.1 AI系统问题根因分类

AI系统出问题,根因通常逃不出这五类:

根因类别 典型症状 排查方法 大白话
数据问题 某类问题集体翻车 检查知识库覆盖率、时效性 教科书缺页了
Prompt问题 回答格式变乱、不听指令 对比Prompt版本变化 考题要求改了但没通知
模型问题 整体能力下降、延迟升高 检查模型版本、GPU状态 老师生病了发挥失常
用户问题 问法变了、问的东西没见过 分析问题分布变化 学生开始问超纲题
系统问题 偶发错误、超时 看链路追踪、日志 教室停电了

39.4.2 根因分析框架

python 复制代码
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum

class RootCauseCategory(Enum):
    DATA = "数据问题"
    PROMPT = "Prompt问题"
    MODEL = "模型问题"
    USER = "用户问题"
    SYSTEM = "系统问题"

@dataclass
class RootCauseAnalysis:
    """根因分析结果"""
    category: RootCauseCategory
    root_cause: str              # 根因描述
    evidence: List[str]          # 证据列表
    affected_queries: List[str]  # 受影响的典型问题
    confidence: float            # 置信度 0-1
    suggested_fix: str           # 建议修复方案
    estimated_impact: str        # 预估修复效果


class RootCauseAnalyzer:
    """根因分析器"""
    
    def __init__(self, metrics_store, log_store, feedback_store, 
                 knowledge_base, prompt_versions):
        self.metrics = metrics_store
        self.logs = log_store
        self.feedback = feedback_store
        self.kb = knowledge_base
        self.prompts = prompt_versions
    
    def analyze(self, signal: IterationSignal) -> RootCauseAnalysis:
        """根据迭代信号进行根因分析"""
        
        # 按优先级逐项排查
        checks = [
            self._check_system_issue,
            self._check_model_issue,
            self._check_data_issue,
            self._check_prompt_issue,
            self._check_user_issue,
        ]
        
        for check in checks:
            result = check(signal)
            if result and result.confidence > 0.6:
                return result
        
        # 没有高置信度结论,返回最可能的
        return RootCauseAnalysis(
            category=RootCauseCategory.SYSTEM,
            root_cause="无法确定单一根因,可能多因素叠加",
            evidence=["多维度排查未发现明确单点问题"],
            affected_queries=[],
            confidence=0.3,
            suggested_fix="启动全面排查:提取Bad Case库中最近异常样本,人工逐条分析",
            estimated_impact="需进一步分析才能评估"
        )
    
    def _check_system_issue(self, signal) -> Optional[RootCauseAnalysis]:
        """排查系统问题"""
        # 检查错误率、超时率
        error_rate = self.metrics.get_error_rate(hours=24)
        timeout_rate = self.metrics.get_timeout_rate(hours=24)
        
        if error_rate > 0.05 or timeout_rate > 0.1:
            evidence = []
            if error_rate > 0.05:
                evidence.append(f"24小时错误率{error_rate*100:.1f}%,超阈值5%")
            if timeout_rate > 0.1:
                evidence.append(f"24小时超时率{timeout_rate*100:.1f}%,超阈值10%")
            
            return RootCauseAnalysis(
                category=RootCauseCategory.SYSTEM,
                root_cause="系统基础设施异常:错误率或超时率过高",
                evidence=evidence,
                affected_queries=self.logs.get_error_samples(10),
                confidence=0.85,
                suggested_fix="检查模型服务状态、GPU负载、网络延迟,必要时重启或扩容",
                estimated_impact="修复后预计指标恢复到正常水平"
            )
        return None
    
    def _check_model_issue(self, signal) -> Optional[RootCauseAnalysis]:
        """排查模型问题"""
        # 检查模型版本是否变化
        current_version = self.metrics.get_current_model_version()
        version_change_time = self.metrics.get_model_version_change_time()
        
        # 如果模型版本最近变过,且问题在变版本后出现
        if (version_change_time and 
            (datetime.now() - version_change_time).days < 3 and
            signal.detected_at > version_change_time):
            
            return RootCauseAnalysis(
                category=RootCauseCategory.MODEL,
                root_cause=f"模型版本更新:{self.metrics.get_previous_model_version()} → {current_version}",
                evidence=[
                    f"模型版本在{version_change_time}更新",
                    f"问题在版本更新后{signal.detected_at - version_change_time}内出现",
                    f"更新前后准确率对比:{self.metrics.get_accuracy_before(version_change_time):.1%} → {self.metrics.get_accuracy_after(version_change_time):.1%}"
                ],
                affected_queries=self.logs.get_regression_samples(
                    before_time=version_change_time, limit=10
                ),
                confidence=0.8,
                suggested_fix="回滚到旧版本,或针对新版本重新优化Prompt和参数",
                estimated_impact="回滚后预计恢复,新版本需要单独适配"
            )
        return None
    
    def _check_data_issue(self, signal) -> Optional[RootCauseAnalysis]:
        """排查数据/知识库问题"""
        # 检查信号涉及的问题类别,看知识库覆盖情况
        affected_category = signal.affected_segment
        
        # 提取该类别最近的差评问题
        bad_cases = self.feedback.get_negative_cases(
            category=affected_category, 
            days=7, 
            limit=20
        )
        
        if not bad_cases:
            return None
        
        # 检查这些问题在知识库中能否找到相关文档
        no_coverage_count = 0
        low_coverage_count = 0
        
        for case in bad_cases:
            # 模拟检索
            retrieval_result = self.kb.retrieve(case.question, top_k=3)
            if not retrieval_result:
                no_coverage_count += 1
            elif retrieval_result[0].score < 0.5:
                low_coverage_count += 1
        
        coverage_rate = 1 - (no_coverage_count + low_coverage_count) / len(bad_cases)
        
        if coverage_rate < 0.6:
            evidence = [
                f"类别'{affected_category}'的{len(bad_cases)}个差评Case中:",
                f"  知识库完全无相关文档:{no_coverage_count}个",
                f"  知识库相关度低(score<0.5):{low_coverage_count}个",
                f"  知识库覆盖率:{coverage_rate*100:.0f}%(低于60%阈值)"
            ]
            
            if no_coverage_count > len(bad_cases) * 0.4:
                fix = f"补充'{affected_category}'类知识库文档,覆盖用户高频问题"
            else:
                fix = f"优化'{affected_category}'类文档切分策略和检索参数,可能需要调整chunk_size或增加同义词"
            
            return RootCauseAnalysis(
                category=RootCauseCategory.DATA,
                root_cause=f"知识库覆盖不足:'{affected_category}'类问题找不到相关资料",
                evidence=evidence,
                affected_queries=[c.question for c in bad_cases[:5]],
                confidence=0.75,
                suggested_fix=fix,
                estimated_impact="补充/优化后预计该类别准确率提升15-30%"
            )
        return None
    
    def _check_prompt_issue(self, signal) -> Optional[RootCauseAnalysis]:
        """排查Prompt问题"""
        # 检查Prompt是否最近改过
        current_prompt = self.prompts.get_current_version()
        prompt_history = self.prompts.get_history(limit=5)
        
        if len(prompt_history) > 1:
            latest_change = prompt_history[0]
            change_time = latest_change.changed_at
            
            if (datetime.now() - change_time).days < 3:
                # Prompt最近改过,检查是否影响了质量
                before_quality = self.metrics.get_quality_score(
                    end=change_time, days=7
                )
                after_quality = self.metrics.get_quality_score(
                    start=change_time, days=7
                )
                
                if before_quality and after_quality and after_quality < before_quality - 0.03:
                    return RootCauseAnalysis(
                        category=RootCauseCategory.PROMPT,
                        root_cause=f"Prompt变更导致质量下降",
                        evidence=[
                            f"Prompt在{change_time}更新",
                            f"更新前质量分:{before_quality:.1%}",
                            f"更新后质量分:{after_quality:.1%}",
                            f"变化内容:{latest_change.diff_summary}"
                        ],
                        affected_queries=self.logs.get_quality_regression_samples(
                            before=change_time, limit=10
                        ),
                        confidence=0.8,
                        suggested_fix="回滚Prompt到上一版本,或针对新Prompt的问题点微调",
                        estimated_impact="回滚后预计恢复到变更前水平"
                    )
        return None
    
    def _check_user_issue(self, signal) -> Optional[RootCauseAnalysis]:
        """排查用户行为变化"""
        # 分析最近问题分布是否发生变化
        recent_distribution = self.logs.get_question_distribution(days=7)
        baseline_distribution = self.logs.get_question_distribution(
            start=datetime.now() - timedelta(days=30),
            end=datetime.now() - timedelta(days=7)
        )
        
        # 计算分布差异(KL散度或简单比例变化)
        shifted_categories = []
        for cat, recent_ratio in recent_distribution.items():
            baseline_ratio = baseline_distribution.get(cat, 0)
            if baseline_ratio > 0 and abs(recent_ratio - baseline_ratio) / baseline_ratio > 0.3:
                shifted_categories.append({
                    "category": cat,
                    "baseline": f"{baseline_ratio*100:.1f}%",
                    "recent": f"{recent_ratio*100:.1f}%",
                    "change": f"{(recent_ratio - baseline_ratio)*100:+.1f}%"
                })
        
        if shifted_categories:
            return RootCauseAnalysis(
                category=RootCauseCategory.USER,
                root_cause="用户问题分布发生变化,系统未适配新分布",
                evidence=[f"类别'{s['category']}'占比从{s['baseline']}变为{s['recent']}({s['change']})" 
                         for s in shifted_categories],
                affected_queries=self.logs.get_samples_by_category(
                    [s["category"] for s in shifted_categories], limit=5
                ),
                confidence=0.6,
                suggested_fix="针对占比增长的新类别补充知识库和优化Prompt",
                estimated_impact="针对新分布优化后预计满意度提升5-10%"
            )
        return None

大白话: 根因分析就像"破案"------系统出问题了,你得收集证据(监控数据)、排除嫌疑(逐项排查)、锁定真凶(找到根因)。上面这套代码就是你的"AI侦探"。


39.5 第三步:离线评估------改了到底好不好

找到根因、提出方案后,不能直接上线。先在离线环境跑一遍评估,确认"改了确实比没改好"。

39.5.1 评估测试集的构建与维护

测试集是离线评估的核心资产。没有好的测试集,评估就是"自欺欺人"。

python 复制代码
from dataclasses import dataclass, field
from typing import List, Dict, Optional
from datetime import datetime
import json
import random

@dataclass
class TestCase:
    """单条测试用例"""
    test_id: str
    question: str
    expected_answer: str           # 标准答案(可选)
    expected_keywords: List[str]   # 答案应包含的关键信息
    category: str                  # 问题类别
    difficulty: str                # easy / medium / hard
    source: str                    # 来源:manual / user_log / bad_case / synthetic
    tags: List[str]               # 标签
    created_at: datetime
    updated_at: datetime
    notes: str = ""                # 备注


class TestSetManager:
    """测试集管理器"""
    
    def __init__(self, test_set_path: str):
        self.path = test_set_path
        self.test_cases: Dict[str, TestCase] = self._load()
    
    def _load(self) -> Dict[str, TestCase]:
        """加载测试集"""
        try:
            with open(self.path, 'r') as f:
                data = json.load(f)
            return {t['test_id']: TestCase(**t) for t in data['cases']}
        except FileNotFoundError:
            return {}
    
    def save(self):
        """保存测试集"""
        data = {
            'version': datetime.now().isoformat(),
            'total_cases': len(self.test_cases),
            'cases': [self._to_dict(t) for t in self.test_cases.values()]
        }
        with open(self.path, 'w') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    
    def add_case(self, question: str, expected_keywords: List[str], 
                 category: str, difficulty: str = "medium",
                 source: str = "manual", expected_answer: str = "",
                 tags: List[str] = None, notes: str = ""):
        """添加测试用例"""
        test_id = f"tc_{len(self.test_cases)+1:04d}"
        now = datetime.now()
        case = TestCase(
            test_id=test_id,
            question=question,
            expected_answer=expected_answer,
            expected_keywords=expected_keywords,
            category=category,
            difficulty=difficulty,
            source=source,
            tags=tags or [],
            created_at=now,
            updated_at=now,
            notes=notes
        )
        self.test_cases[test_id] = case
        return case
    
    def add_from_bad_cases(self, bad_cases: List[dict]):
        """从Bad Case库自动生成测试用例"""
        """
        Bad Case是测试集的金矿------用户真实遇到的问题,比人工编造的真实得多
        """
        added = 0
        for bad in bad_cases:
            # 检查是否已有类似问题
            if self._has_similar(bad['question']):
                continue
            
            self.add_case(
                question=bad['question'],
                expected_keywords=bad.get('expected_keywords', []),
                category=bad.get('category', 'uncategorized'),
                difficulty=bad.get('difficulty', 'medium'),
                source='bad_case',
                expected_answer=bad.get('correct_answer', ''),
                tags=['from_bad_case', bad.get('issue_type', 'unknown')],
                notes=f"来源:用户差评。原始问题类型:{bad.get('issue_type', 'unknown')}"
            )
            added += 1
        
        return added
    
    def add_from_user_logs(self, logs: List[dict], min_frequency: int = 5):
        """从高频用户问题自动提取测试用例"""
        """
        统计高频问题,自动加入测试集
        """
        # 按问题聚类
        question_clusters = self._cluster_questions(logs)
        
        added = 0
        for cluster_id, cluster in question_clusters.items():
            if len(cluster['questions']) < min_frequency:
                continue
            
            # 取代表性问题
            rep_question = cluster['representative']
            
            if self._has_similar(rep_question):
                continue
            
            self.add_case(
                question=rep_question,
                expected_keywords=[],  # 需要人工补充
                category=cluster.get('category', 'uncategorized'),
                difficulty='medium',
                source='user_log',
                tags=[f'frequency_{len(cluster["questions"])}'],
                notes=f"高频问题,近30天出现{len(cluster['questions'])}次"
            )
            added += 1
        
        return added
    
    def get_evaluation_set(self, strategy: str = "balanced") -> List[TestCase]:
        """获取评估用测试集"""
        all_cases = list(self.test_cases.values())
        
        if strategy == "all":
            return all_cases
        
        elif strategy == "balanced":
            # 按类别和难度均衡采样
            by_category = {}
            for case in all_cases:
                if case.category not in by_category:
                    by_category[case.category] = []
                by_category[case.category].append(case)
            
            selected = []
            for cat, cases in by_category.items():
                # 每个类别按难度分配:easy 30%, medium 50%, hard 20%
                easy = [c for c in cases if c.difficulty == 'easy']
                medium = [c for c in cases if c.difficulty == 'medium']
                hard = [c for c in cases if c.difficulty == 'hard']
                
                n = min(20, len(cases))  # 每类最多20条
                selected.extend(random.sample(easy, min(int(n*0.3), len(easy))))
                selected.extend(random.sample(medium, min(int(n*0.5), len(medium))))
                selected.extend(random.sample(hard, min(int(n*0.2), len(hard))))
            
            return selected
        
        elif strategy == "bad_case_focus":
            # 重点关注Bad Case来源的测试用例
            return [c for c in all_cases if 'from_bad_case' in c.tags]
        
        elif strategy == "regression":
            # 回归测试:包含所有历史Bug的测试用例
            return [c for c in all_cases if c.source in ('bad_case', 'regression')]
    
    def _has_similar(self, question: str, threshold: float = 0.85) -> bool:
        """检查是否已有类似问题(简单版:用编辑距离,生产用向量相似度)"""
        for existing in self.test_cases.values():
            if self._similarity(question, existing.question) > threshold:
                return True
        return False
    
    def _similarity(self, a: str, b: str) -> float:
        """简单相似度(生产环境用Embedding)"""
        # 这里用Jaccard作为示例
        set_a = set(a)
        set_b = set(b)
        intersection = set_a & set_b
        union = set_a | set_b
        return len(intersection) / len(union) if union else 0
    
    def _cluster_questions(self, logs: List[dict]) -> Dict:
        """问题聚类(生产环境用Embedding + K-Means)"""
        # 简化版:按类别直接分组
        clusters = {}
        for log in logs:
            cat = log.get('category', 'uncategorized')
            if cat not in clusters:
                clusters[cat] = {'questions': [], 'representative': None}
            clusters[cat]['questions'].append(log['question'])
        
        # 取最长的作为代表
        for cluster in clusters.values():
            cluster['representative'] = max(cluster['questions'], key=len)
        
        return clusters
    
    def _to_dict(self, case: TestCase) -> dict:
        return {
            'test_id': case.test_id,
            'question': case.question,
            'expected_answer': case.expected_answer,
            'expected_keywords': case.expected_keywords,
            'category': case.category,
            'difficulty': case.difficulty,
            'source': case.source,
            'tags': case.tags,
            'created_at': case.created_at.isoformat(),
            'updated_at': case.updated_at.isoformat(),
            'notes': case.notes
        }
    
    def get_stats(self) -> dict:
        """测试集统计"""
        total = len(self.test_cases)
        by_category = {}
        by_difficulty = {'easy': 0, 'medium': 0, 'hard': 0}
        by_source = {}
        
        for case in self.test_cases.values():
            by_category[case.category] = by_category.get(case.category, 0) + 1
            by_difficulty[case.difficulty] = by_difficulty.get(case.difficulty, 0) + 1
            by_source[case.source] = by_source.get(case.source, 0) + 1
        
        return {
            'total': total,
            'by_category': by_category,
            'by_difficulty': by_difficulty,
            'by_source': by_source,
            'coverage': f"{len(by_category)}个类别"
        }

39.5.2 离线评估Pipeline

python 复制代码
from dataclasses import dataclass
from typing import List, Dict, Optional
from datetime import datetime
import asyncio
import time

@dataclass
class EvaluationResult:
    """单条评估结果"""
    test_id: str
    question: str
    generated_answer: str
    expected_keywords: List[str]
    keyword_coverage: float        # 关键词覆盖率
    llm_judge_score: float         # LLM-as-Judge评分 0-1
    llm_judge_feedback: str        # 评审反馈
    latency_ms: float              # 响应延迟
    token_count: int               # Token消耗
    passed: bool                   # 是否通过
    error: Optional[str] = None    # 如果出错

@dataclass
class EvaluationReport:
    """评估报告"""
    total_cases: int
    passed_cases: int
    pass_rate: float
    avg_keyword_coverage: float
    avg_llm_score: float
    avg_latency_ms: float
    avg_tokens: int
    by_category: Dict[str, dict]   # 按类别分组统计
    by_difficulty: Dict[str, dict] # 按难度分组统计
    failed_cases: List[dict]       # 失败的Case
    comparison: Optional[dict]     # 与上一版本对比


class OfflineEvaluator:
    """离线评估器"""
    
    def __init__(self, llm_client, test_set_manager: TestSetManager,
                 judge_llm_client=None):
        self.llm = llm_client           # 被评估的LLM
        self.judge_llm = judge_llm_client or llm_client  # 评审LLM(可以用更强的模型)
        self.test_set = test_set_manager
    
    async def evaluate(self, strategy: str = "balanced",
                       compare_with: Optional[List[EvaluationResult]] = None) -> EvaluationReport:
        """执行离线评估"""
        
        # 1. 获取测试集
        test_cases = self.test_set.get_evaluation_set(strategy)
        print(f"开始评估:{len(test_cases)}条测试用例")
        
        # 2. 并行执行评估
        results = await asyncio.gather(*[
            self._evaluate_single(case) for case in test_cases
        ])
        
        # 3. 生成报告
        report = self._generate_report(results, test_cases)
        
        # 4. 如果有对比数据,加入对比
        if compare_with:
            report.comparison = self._compare(results, compare_with)
        
        return report
    
    async def _evaluate_single(self, case: TestCase) -> EvaluationResult:
        """评估单条用例"""
        start_time = time.time()
        
        try:
            # 调用被评估的LLM
            response = await self.llm.chat(case.question)
            latency = (time.time() - start_time) * 1000
            
            # 关键词覆盖率检查
            keyword_coverage = self._check_keywords(response, case.expected_keywords)
            
            # LLM-as-Judge 评审
            judge_score, judge_feedback = await self._llm_judge(
                case.question, response, case.expected_answer, 
                case.expected_keywords
            )
            
            # 判断是否通过
            passed = keyword_coverage >= 0.8 and judge_score >= 0.7
            
            return EvaluationResult(
                test_id=case.test_id,
                question=case.question,
                generated_answer=response,
                expected_keywords=case.expected_keywords,
                keyword_coverage=keyword_coverage,
                llm_judge_score=judge_score,
                llm_judge_feedback=judge_feedback,
                latency_ms=latency,
                token_count=len(response) // 2,  # 粗估
                passed=passed
            )
            
        except Exception as e:
            return EvaluationResult(
                test_id=case.test_id,
                question=case.question,
                generated_answer="",
                expected_keywords=case.expected_keywords,
                keyword_coverage=0,
                llm_judge_score=0,
                llm_judge_feedback="",
                latency_ms=(time.time() - start_time) * 1000,
                token_count=0,
                passed=False,
                error=str(e)
            )
    
    def _check_keywords(self, answer: str, keywords: List[str]) -> float:
        """检查关键词覆盖率"""
        if not keywords:
            return 1.0  # 没有关键词要求则默认通过
        
        answer_lower = answer.lower()
        covered = sum(1 for kw in keywords if kw.lower() in answer_lower)
        return covered / len(keywords)
    
    async def _llm_judge(self, question: str, answer: str,
                         expected: str, keywords: List[str]) -> tuple:
        """LLM-as-Judge评审"""
        judge_prompt = f"""你是一个严格的评审专家,请对以下AI回答进行评分。

用户问题:{question}
AI回答:{answer}
参考答案:{expected or '无'}
应包含关键信息:{', '.join(keywords) if keywords else '无特殊要求'}

请从以下5个维度评分(每项0-2分,总分0-10分):
1. 准确性:回答是否正确,有无事实错误
2. 完整性:是否覆盖了问题的所有要点
3. 相关性:回答是否紧扣问题,有无跑题
4. 语言质量:表述是否清晰、通顺、专业
5. 安全性:有无不当内容、有害信息

请输出JSON格式:
{{"accuracy": 0-2, "completeness": 0-2, "relevance": 0-2, "language": 0-2, "safety": 0-2, "feedback": "简要说明扣分原因"}}"""
        
        response = await self.judge_llm.chat(judge_prompt, temperature=0)
        
        try:
            import json
            result = json.loads(response)
            total_score = sum(result[k] for k in ['accuracy', 'completeness', 'relevance', 'language', 'safety'])
            normalized_score = total_score / 10  # 归一化到0-1
            feedback = result.get('feedback', '')
            return normalized_score, feedback
        except:
            return 0.5, "评审解析失败"
    
    def _generate_report(self, results: List[EvaluationResult], 
                         test_cases: List[TestCase]) -> EvaluationReport:
        """生成评估报告"""
        total = len(results)
        passed = sum(1 for r in results if r.passed)
        
        # 按类别统计
        case_map = {c.test_id: c for c in test_cases}
        by_category = {}
        by_difficulty = {}
        
        for r in results:
            case = case_map.get(r.test_id)
            if case:
                # 按类别
                if case.category not in by_category:
                    by_category[case.category] = {'total': 0, 'passed': 0, 'avg_score': []}
                by_category[case.category]['total'] += 1
                if r.passed:
                    by_category[case.category]['passed'] += 1
                by_category[case.category]['avg_score'].append(r.llm_judge_score)
                
                # 按难度
                if case.difficulty not in by_difficulty:
                    by_difficulty[case.difficulty] = {'total': 0, 'passed': 0, 'avg_score': []}
                by_difficulty[case.difficulty]['total'] += 1
                if r.passed:
                    by_difficulty[case.difficulty]['passed'] += 1
                by_difficulty[case.difficulty]['avg_score'].append(r.llm_judge_score)
        
        # 计算平均值
        for stats in list(by_category.values()) + list(by_difficulty.values()):
            scores = stats.pop('avg_score')
            stats['pass_rate'] = stats['passed'] / stats['total'] if stats['total'] else 0
            stats['avg_score'] = sum(scores) / len(scores) if scores else 0
        
        # 失败Case
        failed_cases = [
            {
                'test_id': r.test_id,
                'question': r.question,
                'answer': r.generated_answer[:200],
                'keyword_coverage': r.keyword_coverage,
                'llm_score': r.llm_judge_score,
                'feedback': r.llm_judge_feedback,
                'error': r.error
            }
            for r in results if not r.passed
        ]
        
        return EvaluationReport(
            total_cases=total,
            passed_cases=passed,
            pass_rate=passed / total if total else 0,
            avg_keyword_coverage=sum(r.keyword_coverage for r in results) / total,
            avg_llm_score=sum(r.llm_judge_score for r in results) / total,
            avg_latency_ms=sum(r.latency_ms for r in results) / total,
            avg_tokens=sum(r.token_count for r in results) / total,
            by_category=by_category,
            by_difficulty=by_difficulty,
            failed_cases=failed_cases
        )
    
    def _compare(self, current: List[EvaluationResult], 
                 previous: List[EvaluationResult]) -> dict:
        """与上一版本对比"""
        prev_map = {r.test_id: r for r in previous}
        
        improved = 0
        regressed = 0
        unchanged = 0
        
        for curr in current:
            prev = prev_map.get(curr.test_id)
            if prev:
                if curr.llm_judge_score > prev.llm_judge_score + 0.05:
                    improved += 1
                elif curr.llm_judge_score < prev.llm_judge_score - 0.05:
                    regressed += 1
                else:
                    unchanged += 1
        
        return {
            'improved': improved,
            'regressed': regressed,
            'unchanged': unchanged,
            'current_pass_rate': sum(1 for r in current if r.passed) / len(current),
            'previous_pass_rate': sum(1 for r in previous if r.passed) / len(previous),
            'verdict': '✅ 可以上线' if regressed < improved else '❌ 不建议上线,存在退化'
        }

大白话: 离线评估就是"模拟考试"------用测试集跑一遍,看看改了之后是变好了还是变差了。关键原则:没有通过离线评估的改动,绝对不能上线。


39.6 第四步:灰度A/B测试------线上验证

离线评估通过了,不代表线上就能好。离线环境和线上环境有本质差异:

差异维度 离线环境 线上环境
问题分布 测试集是采样 真实用户问题分布更广
问题质量 标准化 真实用户问题可能很模糊
并发 串行 高并发可能影响性能
用户期望 没有 真实用户的耐心和期望

所以需要灰度A/B测试------让一小部分真实用户先用新版本,对比效果。

39.6.1 A/B测试设计

python 复制代码
from dataclasses import dataclass, field
from typing import List, Dict, Optional, Tuple
from datetime import datetime, timedelta
from enum import Enum
import hashlib
import math

class ABTestStatus(Enum):
    DRAFT = "draft"
    RUNNING = "running"
    COMPLETED = "completed"
    STOPPED = "stopped"

@dataclass
class ABTestConfig:
    """A/B测试配置"""
    test_id: str
    name: str
    description: str
    control_version: str        # 当前线上版本
    treatment_version: str      # 新版本
    traffic_percentage: float   # 实验组流量比例 (0-1)
    target_metrics: List[str]   # 关注的指标
    min_sample_size: int        # 最小样本量
    significance_level: float   # 显著性水平 (通常0.05)
    start_time: datetime
    end_time: Optional[datetime] = None
    status: ABTestStatus = ABTestStatus.DRAFT
    # 分流规则
    segment_rules: Dict = field(default_factory=lambda: {
        'user_type': None,      # all / new / returning
        'region': None,         # 地区过滤
        'platform': None,       # 平台过滤
    })


class ABTestManager:
    """A/B测试管理器"""
    
    def __init__(self, config_store, metrics_store, feedback_store):
        self.configs: Dict[str, ABTestConfig] = config_store
        self.metrics = metrics_store
        self.feedback = feedback_store
    
    def assign_group(self, user_id: str, test_id: str) -> str:
        """为用户分配实验组(确定性分流,同一用户永远在同一组)"""
        config = self.configs.get(test_id)
        if not config or config.status != ABTestStatus.RUNNING:
            return 'control'  # 没在跑的实验,默认对照组
        
        # 检查用户是否符合分段规则
        if not self._user_matches_segment(user_id, config.segment_rules):
            return 'control'
        
        # 基于用户ID的确定性哈希分流
        hash_value = int(hashlib.md5(f"{test_id}:{user_id}".encode()).hexdigest(), 16)
        bucket = hash_value % 100  # 0-99
        
        if bucket < config.traffic_percentage * 100:
            return 'treatment'
        return 'control'
    
    def _user_matches_segment(self, user_id: str, rules: Dict) -> bool:
        """检查用户是否符合分段规则"""
        # 实际实现需要查用户画像
        return True  # 简化
    
    def get_results(self, test_id: str) -> dict:
        """获取A/B测试结果"""
        config = self.configs.get(test_id)
        if not config:
            return {'error': 'Test not found'}
        
        # 收集对照组和实验组数据
        control_data = self._collect_group_data(test_id, 'control', config)
        treatment_data = self._collect_group_data(test_id, 'treatment', config)
        
        # 统计检验
        results = {}
        for metric in config.target_metrics:
            control_values = control_data.get(metric, [])
            treatment_values = treatment_data.get(metric, [])
            
            if len(control_values) < config.min_sample_size or \
               len(treatment_values) < config.min_sample_size:
                results[metric] = {
                    'status': 'insufficient_data',
                    'control_n': len(control_values),
                    'treatment_n': len(treatment_values),
                    'min_required': config.min_sample_size
                }
            else:
                stat_result = self._statistical_test(
                    control_values, treatment_values, config.significance_level
                )
                results[metric] = stat_result
        
        # 综合判断
        overall = self._make_decision(results, config)
        
        return {
            'test_id': test_id,
            'name': config.name,
            'status': config.status.value,
            'duration_days': (datetime.now() - config.start_time).days,
            'control_sample': sum(len(v) for v in control_data.values()) // len(control_data) if control_data else 0,
            'treatment_sample': sum(len(v) for v in treatment_data.values()) // len(treatment_data) if treatment_data else 0,
            'metric_results': results,
            'recommendation': overall
        }
    
    def _collect_group_data(self, test_id: str, group: str, 
                            config: ABTestConfig) -> Dict[str, List[float]]:
        """收集某一组的指标数据"""
        data = {}
        for metric in config.target_metrics:
            data[metric] = self.metrics.get_metric_by_group(
                test_id=test_id,
                group=group,
                metric=metric,
                start=config.start_time,
                end=config.end_time or datetime.now()
            )
        return data
    
    def _statistical_test(self, control: List[float], 
                          treatment: List[float],
                          alpha: float) -> dict:
        """统计检验(简化版Z-test,生产环境建议用scipy)"""
        n1, n2 = len(control), len(treatment)
        mean1 = sum(control) / n1
        mean2 = sum(treatment) / n2
        
        var1 = sum((x - mean1) ** 2 for x in control) / n1
        var2 = sum((x - mean2) ** 2 for x in treatment) / n2
        
        # 标准误差
        se = math.sqrt(var1 / n1 + var2 / n2)
        
        if se == 0:
            return {'status': 'error', 'message': 'Zero standard error'}
        
        # Z统计量
        z_score = (mean2 - mean1) / se
        
        # 双尾p值(简化,生产用scipy.stats.norm.sf)
        p_value = 2 * (1 - self._normal_cdf(abs(z_score)))
        
        # 效应量
        effect_size = (mean2 - mean1) / (math.sqrt((var1 + var2) / 2) or 1)
        
        # 置信区间(95%)
        margin = 1.96 * se
        ci_lower = (mean2 - mean1) - margin
        ci_upper = (mean2 - mean1) + margin
        
        return {
            'control_mean': round(mean1, 4),
            'treatment_mean': round(mean2, 4),
            'difference': round(mean2 - mean1, 4),
            'difference_pct': round((mean2 - mean1) / mean1 * 100, 2) if mean1 else 0,
            'z_score': round(z_score, 4),
            'p_value': round(p_value, 4),
            'significant': p_value < alpha,
            'effect_size': round(effect_size, 4),
            'ci_95': [round(ci_lower, 4), round(ci_upper, 4)],
            'interpretation': self._interpret_result(mean1, mean2, p_value, alpha)
        }
    
    def _normal_cdf(self, x: float) -> float:
        """标准正态分布CDF(近似)"""
        return 0.5 * (1 + math.erf(x / math.sqrt(2)))
    
    def _interpret_result(self, control_mean: float, treatment_mean: float,
                          p_value: float, alpha: float) -> str:
        """解读结果"""
        if p_value >= alpha:
            return "无显著差异,实验组与对照组表现相当"
        
        if treatment_mean > control_mean:
            return f"✅ 实验组显著优于对照组(p={p_value:.4f}<{alpha}),建议全量上线"
        else:
            return f"❌ 实验组显著差于对照组(p={p_value:.4f}<{alpha}),建议停止实验"
    
    def _make_decision(self, results: dict, config: ABTestConfig) -> str:
        """综合所有指标做决策"""
        significant_metrics = []
        improved_metrics = []
        regressed_metrics = []
        
        for metric, result in results.items():
            if result.get('status') == 'insufficient_data':
                continue
            if result.get('significant'):
                significant_metrics.append(metric)
                if result['difference'] > 0:
                    improved_metrics.append(metric)
                else:
                    regressed_metrics.append(metric)
        
        if regressed_metrics:
            return f"❌ 不建议全量上线:{', '.join(regressed_metrics)}出现显著退化"
        elif len(improved_metrics) >= len(significant_metrics) * 0.5:
            return f"✅ 建议全量上线:{', '.join(improved_metrics)}显著提升"
        else:
            return "⚠️ 结果不明确,建议延长实验时间或增加样本量"

39.6.2 A/B测试的注意事项

事项 为什么重要 大白话
确定性分流 同一用户每次访问必须在同一组 不然用户体验前后不一致
最小样本量 样本太少统计检验没意义 3个人投票不代表全班意见
运行时间 至少覆盖一个完整周期(7天) 周末和工作日行为不同
** novelty效应** 新功能初期效果好可能是新鲜感 新玩具谁都觉得好玩,得看长期
多维指标 不能只看一个指标 准确率上去了但延迟也上去了,不一定值
提前停止 显著就停可能引入偏差 考试没考完就交卷,成绩不靠谱

39.7 第五步:用户反馈闭环

A/B测试告诉你"整体好不好",但用户反馈告诉你"具体哪里不好"

39.7.1 反馈采集设计

python 复制代码
from dataclasses import dataclass
from typing import List, Dict, Optional
from datetime import datetime
from enum import Enum

class FeedbackType(Enum):
    THUMBS_UP = "thumbs_up"       # 点赞
    THUMBS_DOWN = "thumbs_down"   # 点踩
    REGENERATE = "regenerate"     # 重新生成
    REPORT = "report"             # 举报
    RATING = "rating"             # 评分
    TEXT = "text"                 # 文字反馈
    COPY = "copy"                 # 复制(隐式正反馈)
    SHARE = "share"               # 分享(隐式正反馈)

class FeedbackSeverity(Enum):
    POSITIVE = "positive"
    NEUTRAL = "neutral"
    NEGATIVE = "negative"
    CRITICAL = "critical"

@dataclass
class UserFeedback:
    user_id: str
    session_id: str
    message_id: str
    question: str
    answer: str
    feedback_type: FeedbackType
    severity: FeedbackSeverity
    rating: Optional[int] = None          # 1-5星
    text_feedback: Optional[str] = None   # 文字反馈
    issue_category: Optional[str] = None  # 问题类别
    timestamp: datetime = None
    context: Dict = field(default_factory=dict)  # 附加上下文


class FeedbackCollector:
    """用户反馈采集器"""
    
    def __init__(self, feedback_store):
        self.store = feedback_store
    
    async def collect(self, feedback: UserFeedback):
        """采集用户反馈"""
        # 保存原始反馈
        self.store.save(feedback)
        
        # 如果是负面反馈,触发深度分析
        if feedback.severity in [FeedbackSeverity.NEGATIVE, FeedbackSeverity.CRITICAL]:
            await self._analyze_negative_feedback(feedback)
    
    async def _analyze_negative_feedback(self, feedback: UserFeedback):
        """对负面反馈进行深度分析"""
        from dataclasses import dataclass
        
        analysis_prompt = f"""分析以下用户负面反馈,输出结构化结果。

用户问题:{feedback.question}
AI回答:feedback.answer}
反馈类型:{feedback.feedback_type.value}
文字反馈:{feedback.text_feedback or '无'}

请分析:
1. 问题类型(accuracy/hallucination/incomplete/irrelevant/format/safety/other)
2. 根因分析
3. 严重程度(1-5)
4. 建议修复方案

输出JSON格式。"""
        
        # 用LLM分析
        analysis = await self.llm.analyze(analysis_prompt)
        
        # 自动加入Bad Case库
        self.bad_case_store.add({
            'question': feedback.question,
            'answer': feedback.answer,
            'issue_type': analysis['issue_type'],
            'root_cause': analysis['root_cause'],
            'severity': analysis['severity'],
            'suggested_fix': analysis['suggested_fix'],
            'user_feedback': feedback.text_feedback,
            'timestamp': feedback.timestamp
        })
        
        # 严重问题立即告警
        if analysis['severity'] >= 4:
            self.alerting.send_alert(
                level='P1',
                title=f'严重负面反馈:{analysis["issue_type"]}',
                message=f'问题:{feedback.question[:100]}\n根因:{analysis["root_cause"]}'
            )


class ImplicitFeedbackTracker:
    """隐式反馈追踪器"""
    
    """用户不会主动告诉你好不好,但行为会说话"""
    
    def __init__(self, event_store):
        self.events = event_store
    
    def extract_signals(self, session_id: str) -> dict:
        """从用户行为中提取隐式反馈信号"""
        events = self.events.get_session_events(session_id)
        
        signals = {
            'satisfied': False,
            'frustrated': False,
            'neutral': True,
            'signals': []
        }
        
        # 信号1:重新提问(改问法)
        regenerate_count = sum(1 for e in events if e.type == 'regenerate')
        if regenerate_count >= 2:
            signals['frustrated'] = True
            signals['neutral'] = False
            signals['signals'].append(f"重新生成{regenerate_count}次")
        
        # 信号2:复制回答
        copy_events = [e for e in events if e.type == 'copy']
        if copy_events:
            signals['satisfied'] = True
            signals['neutral'] = False
            signals['signals'].append("复制了回答内容")
        
        # 信号3:后续追问相关问题(说明回答不够)
        followup_count = sum(1 for e in events if e.type == 'followup')
        if followup_count >= 2:
            signals['frustrated'] = True
            signals['neutral'] = False
            signals['signals'].append(f"追问{followup_count}次")
        
        # 信号4:长时间停留后离开(可能不满意但懒得反馈)
        last_interaction = events[-1] if events else None
        if last_interaction and last_interaction.type == 'exit':
            time_on_page = last_interaction.timestamp - events[0].timestamp
            if time_on_page.total_seconds() < 5 and not copy_events:
                signals['frustrated'] = True
                signals['neutral'] = False
                signals['signals'].append("快速离开,可能不满意")
        
        # 信号5:转人工
        if any(e.type == 'escalate_to_human' for e in events):
            signals['frustrated'] = True
            signals['neutral'] = False
            signals['signals'].append("转人工客服")
        
        return signals

39.7.2 反馈数据怎么用

反馈类型 怎么用 大白话
点赞 统计正面率,优秀回答存入"Good Case"库 好学生作业留着当范本
点踩+文字 提取问题模式,加入Bad Case库 错题本
重新生成 对比两次回答差异,找改进点 同一道题做了两遍,对比哪次好
转人工 分析为什么AI回答不了 补考的学生,看看哪里没学会
隐式信号 补充显式反馈的不足 学生嘴上说"懂了"但表情告诉你他没懂

大白话: 用户反馈是AI系统最宝贵的"免费标注数据"。每次差评都是一个改进机会------把它加入测试集,下次就不会再犯同样的错。


39.8 迭代节奏管理

39.8.1 不同频率的迭代

不是所有迭代都需要同样的频率和深度。按紧急程度分三层:

层级 频率 内容 参与角色 大白话
热修复 随时(P0触发) 紧急Bug、安全漏洞、模型服务故障 值班工程师 火烧眉毛,先灭火
常规迭代 每周/双周 Prompt优化、知识库更新、Bad Case修复 AI工程师+业务 周考改错,稳步提升
版本迭代 每月/季度 换模型、架构调整、新功能开发 全团队 期末大考,全面升级

39.8.2 迭代SOP(标准操作流程)

复制代码
【常规迭代SOP - 每周执行】

周一:数据回顾
├── 拉取上周监控数据
├── 检查迭代信号检测器的输出
├── 整理用户反馈Top10差评
└── 确定本周优化目标

周二-周三:方案开发
├── 根因分析
├── 制定优化方案
├── 修改Prompt/知识库/参数
└── 准备离线评估

周四:离线评估
├── 在测试集上跑评估
├── 对比上一版本
├── 分析退化Case
└── 判断是否通过

周五:灰度上线
├── 配置A/B测试(10%流量)
├── 观察实时指标
├── 收集用户反馈
└── 如果没问题,下周扩大流量

次周一:灰度回顾
├── 分析A/B测试结果
├── 统计显著性检验
├── 决策:全量上线/继续观察/回滚
└── 更新测试集(加入新Bad Case)

39.8.3 迭代日志管理

每次迭代都要记录"改了什么、为什么改、效果如何"------不然三个月后没人记得改过什么。

python 复制代码
@dataclass
class IterationLog:
    """迭代日志"""
    iteration_id: str
    date: datetime
    version: str                # 版本号
    
    # 改了什么
    change_type: str            # prompt / knowledge_base / model / parameter / architecture
    change_description: str     # 详细描述
    change_diff: str            # 具体差异
    
    # 为什么改
    trigger: str                # 触发原因:monitoring / feedback / requirement / model_update
    root_cause: str             # 根因分析
    related_signal_id: str      # 关联的信号ID
    
    # 效果如何
    offline_evaluation: dict    # 离线评估结果
    ab_test_result: dict        # A/B测试结果
    metrics_before: dict        # 改前指标
    metrics_after: dict         # 改后指标
    user_feedback_summary: str  # 用户反馈摘要
    
    # 决策
    decision: str               # full_release / rollback / continue_observation
    decision_reason: str        # 决策理由
    
    # 经验教训
    lessons_learned: str        # 经验总结
    new_test_cases: int         # 新增测试用例数

39.9 迭代常见陷阱

陷阱1:过度优化局部指标

复制代码
❌ 错误做法:
看到"准确率"指标低,拼命优化准确率
结果:准确率上去了,但延迟翻倍、成本翻3倍、用户满意度反而降了

✅ 正确做法:
同时监控多个指标,找到"帕累托最优"------不让一个指标的优化以另一个指标的恶化为代价

陷阱2:测试集污染

复制代码
❌ 错误做法:
用户问了一个问题AI答错了 → 直接把这个问题+正确答案加入测试集
结果:测试集和真实分布脱节,离线评估"虚高"

✅ 正确做法:
Bad Case要经过"泛化处理"------不直接加原题,而是提取问题模式,生成多个变体

陷阱3:频繁改动无法归因

复制代码
❌ 错误做法:
周一改了Prompt,周二改了知识库,周三换了模型,周四改了参数
结果:效果变好了不知道是谁的功劳,变差了不知道是谁的锅

✅ 正确做法:
每次只改一个变量,评估后再改下一个。如果必须同时改,至少在A/B测试中做好标记

陷阱4:只加不减

复制代码
❌ 错误做法:
Prompt越来越长(每次迭代加一段约束),知识库文档越来越多(从不删旧文档)
结果:Token消耗暴涨、检索噪声增加、维护成本飙升

✅ 正确做法:
定期"断舍离"------Prompt定期精简合并,知识库定期清理过期/低质文档

陷阱5:忽视回归测试

复制代码
❌ 错误做法:
优化了A类问题,上线后发现B类问题变差了
原因:没做回归测试,改A的时候影响到了B

✅ 正确做法:
每次迭代后,除了评估新优化方向,还要用回归测试集验证"原来好的地方没变差"

39.10 评估迭代最佳实践清单

P0 --- 必须做(上线前就必须建立)

# 实践 说明
1 建立测试集 至少200条,覆盖主要类别和难度,含Bad Case
2 建立离线评估Pipeline 自动化跑测试集,生成评估报告
3 建立A/B测试框架 确定性分流 + 统计检验 + 多指标对比
4 建立反馈采集 显式反馈(赞/踩/评分)+ 隐式反馈(行为信号)
5 建立迭代日志 每次迭代记录改了什么、为什么、效果如何
6 设立回滚机制 任何迭代都能快速回滚到上一版本

P1 --- 应该做(上线后1个月内建立)

# 实践 说明
7 迭代信号自动检测 每日自动扫描指标异常
8 根因分析框架 五类根因系统化排查
9 Bad Case管理流程 差评→分析→测试用例→修复→验证闭环
10 定期知识库更新 每周检查过期内容、每月补充新内容
11 Prompt版本管理 Git管理Prompt,每次修改有diff和原因
12 回归测试集 标记"曾经出过问题"的Case,每次迭代必跑

P2 --- 建议做(持续优化)

# 实践 说明
13 用户画像驱动的个性化评估 不同用户群体分别评估
14 竞品对比评估 定期用同样问题测竞品,了解差距
15 自动化优化建议 LLM自动分析Bad Case并生成优化建议
16 多目标优化 准确率+延迟+成本联合优化
17 知识库质量评分 每篇文档打分,低质文档自动标记清理

39.11 本章总结

核心认知

  1. AI系统上线是起点不是终点 --- 不迭代不是"保持现状"而是"慢慢退步"
  2. 迭代必须有闭环 --- 监控→根因→方案→离线评估→灰度A/B→全量→监控,缺一不可
  3. 测试集是核心资产 --- 没有测试集的优化是盲人摸象,Bad Case是金矿
  4. 单变量原则 --- 每次只改一个东西,不然没法归因
  5. 数据驱动决策 --- 不靠"感觉变好了",靠统计检验说话

迭代节奏

  • 热修复:P0问题随时修
  • 常规迭代:每周/双周一次,Prompt+知识库+Bad Case
  • 版本迭代:每月/季度一次,模型+架构+新功能

关键指标

层级 指标 目标
L0 基础 响应率、延迟 响应率>99.5%,P95<3s
L1 质量 准确率、满意度 准确率>85%,满意度>80%
L2 业务 解决率、转人工率 解决率>70%,转人工率<15%
L3 成本 单次Token、月费用 月环比不增长

一句话

上线只是开始,迭代才是关键。建立"监控→分析→优化→评估→上线"的闭环,让AI系统像活的有机体一样持续进化。不迭代的AI系统不是在"保持现状",而是在"慢慢变蠢"。


下一章预告: 第40章「Spring AI实战」------Java生态如何集成LLM,企业级AI应用架构怎么搭。这是整个系列的收官章,把第34-39章讲的架构设计、成本控制、性能优化、安全防护、可观测性、评估迭代全部用Spring AI落地一遍。

相关推荐
听你说3239 分钟前
鹏辉LIC+EDLC高功率电容解决方案,为工业自动化、智能IoT、UPS备电等高负载设备而生
人工智能·物联网·自动化·创业创新
墨染天姬43 分钟前
【AI】从MOE混合专家到 MOA混合智能体
人工智能
甲维斯1 小时前
国产版“Claude Code”也支持电脑控制了,kimi也是越用越爽了!
人工智能
oradh1 小时前
Oracle参数文件(PFILE与SPFILE)维护操作总结
数据库·oracle·oracle参数·spfile·pfile
KKKlucifer1 小时前
AI 驱动告警研判:运营商智能化安全运维支撑服务实践案例
人工智能·安全
仙女修炼史1 小时前
word2Vec理解(下):本质理解
人工智能·自然语言处理·word2vec
惊鸿一博1 小时前
# 生成模型(Generative Models)基础介绍_四大分类_扩散模型介绍
人工智能
l1t1 小时前
kryonix提交的DuckDB 统一并优化标量执行器基础设施 - #24564 PR
开发语言·数据库·数据仓库·sql
星恒讯工业路由器1 小时前
4G(LTE)基础技术解析:从频段划分到网络架构的核心知识点
大数据·网络·信息与通信·工业物联网·4g lte·lte网络架构·无线通信基础