第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 本章总结
核心认知
- AI系统上线是起点不是终点 --- 不迭代不是"保持现状"而是"慢慢退步"
- 迭代必须有闭环 --- 监控→根因→方案→离线评估→灰度A/B→全量→监控,缺一不可
- 测试集是核心资产 --- 没有测试集的优化是盲人摸象,Bad Case是金矿
- 单变量原则 --- 每次只改一个东西,不然没法归因
- 数据驱动决策 --- 不靠"感觉变好了",靠统计检验说话
迭代节奏
- 热修复: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落地一遍。