第一章:离线评估体系
1.1 测试集的构建
离线评估的基础是测试集。测试集的质量决定了评估的可信度。
**测试集的数据来源**:
| 来源 | 占比 | 说明 |
|------|------|------|
| 历史搜索日志 | 50% | 用户真实问过的问题,抽样 |
| 业务专家标注 | 30% | 各领域专家出的典型问题 |
| 边缘案例构造 | 20% | 我们人为构造的刁钻问题 |
**标注规范**:每个测试样本包含四个字段:
```json
{
"query": "CNC主轴驱动报警201怎么处理",
"ground_truth_docs": "doc_001234", "doc_005678",
"ground_truth_answer": "检查驱动器电源模块,确认输入电压正常后复位...",
"category": "故障处理",
"difficulty": 3
}
```
困难度分级:1-5分,1分最简单(直接映射),5分最难(需要多文档推理)。
测试集最终规模:1200条。其中600条用于日常回归测试,600条作为留验证集用于模型选型。
**避坑提醒**:测试集必须和训练数据隔离。我们第一次就犯了这个错------测试集里的问题和向量库里的文档有重叠,导致指标虚高。后来用文档ID做了严格隔离,确保测试query对应的ground_truth_docs不在任何人的训练数据里。
1.2 核心指标
定义了五个核心指标,每个指标有明确的阈值:
**指标一:Hit@K**
召回层面的核心指标。K取5和10两个值。
```
Hit@K = 正确文档是否出现在topK中
```
阈值:Hit@5 > 0.85,Hit@10 > 0.92
**指标二:MRR**
排序质量的衡量。
```
MRR = 1/N * Σ(1 / rank_of_first_correct)
```
阈值:> 0.80
**指标三:NDCG@10**
考虑排序位置和相关性等级(相关度分三级:0不相关、1部分相关、2完全相关)。
阈值:> 0.82
**指标四:AnswerCorrectness**
生成答案的质量,用GPT-4做裁判:
```python
def evaluate_answer_correctness(query, answer, ground_truth):
prompt = f"""
评估以下回答的质量。
用户问题:{query}
标准答案:{ground_truth}
待评估回答:{answer}
请从以下维度打分(1-5分):
-
事实准确性:回答中的事实是否与标准答案一致
-
完整性:是否覆盖了标准答案的核心要点
-
冗余度:是否有无关信息
输出JSON格式:{{"accuracy": x, "completeness": x, "conciseness": x, "overall": x}}
"""
用GPT-4打分
return gpt4_judge(prompt)
```
阈值:overall > 4.0(5分制)
初期用GPT-4做裁判,后来改用Claude 3.5 Sonnet,因为Claude在中文评估上更稳定。每个月人工抽查50条评估结果,校验AI裁判的准确性,发现偏差超过0.5分就调整prompt。
**指标五:Latency**
响应延迟。P50、P95、P99三个分位。
阈值:P95 < 3秒
1.3 日常自动化评估
每天凌晨,CI流水线自动跑一轮评估:
```yaml
.gitlab-ci.yml
evaluate:
script:
-
python run_evaluation.py --testset daily_regression.jsonl
-
python report_metrics.py --output metrics_report.html
artifacts:
reports:
metrics: metrics_report.html
only:
- main
```
评估结果自动发到钉钉群,任何指标下降超过5%触发告警。
第二章:在线监控体系
离线指标没问题不代表线上没问题。用户的query千奇百怪,模型在测试集上表现好,不代表能应对真实场景。
2.1 实时监控
线上部署后,每一条请求都记录完整的trace:
```python
def log_trace(query, retrieval_results, rerank_results, llm_answer, user_feedback):
trace = {
"timestamp": datetime.now().isoformat(),
"user_id": user_id,
"query": query,
"retrieval_top10": doc\['id' for doc in retrieval_results:10],
"rerank_top5": doc\['id' for doc inrerank_results:5],
"answer": llm_answer,
"answer_length": len(llm_answer),
"latency_ms": latency_ms,
"user_feedback": user_feedback, # 1有用 0无用 -1未反馈
"session_id": session_id
}
写入ES用于后续分析
es.index(index="rag_traces", body=trace)
```
**核心监控指标**:
| 指标 | 计算方式 | 告警阈值 |
|------|---------|---------|
| 空回答率 | 回答为"不知道"的占比 | >15% |
| 负反馈率 | 点"无用"的占比 | >15% |
| 平均响应时间 | P99延迟 | >3秒 |
| 检索空结果率 | top10无结果的占比 | >5% |
| 日均QPS | 请求量 | 同比波动>30% |
空回答率和负反馈率是我最关注的两个指标。它们之间的差值很有信息量:空回答率低、负反馈率高 → 幻觉严重(模型在瞎编);空回答率高、负反馈率低 → 检索召回差(模型很老实但找不到东西)。
2.2 Bad Case自动发现
负反馈数据是金矿,但人工看不过来。写了一套自动分析脚本:
```python
def analyze_bad_cases(date: str):
拉取当天的负反馈数据
bad_cases = es.search({
"query": {"term": {"user_feedback": 0}},
"size": 10000,
"sort": {"timestamp": "desc"}
})
按失败类型聚类
analysis = {
"retrieval_failure": \[\], # 检索没召回正确文档
"ranking_failure": \[\], # 召回了但排序太低
"generation_failure": \[\], # 召回了但LLM答错
"knowledge_gap": \[\] # 知识库缺失
}
for case in bad_cases:
检查检索结果是否包含ground_truth
ground_truth = get_ground_truth(case'query')
retrieved_ids = case'retrieval_top10'
if not any(doc in retrieved_ids for doc in ground_truth):
analysis'retrieval_failure'.append(case)
elif not any(doc in case'rerank_top5' for doc in ground_truth):
analysis'ranking_failure'.append(case)
else:
检索对了,生成错了
analysis'generation_failure'.append(case)
检查knowledge gap:相似query都回答不出
...聚类逻辑
return analysis
```
每天早上自动生成一份bad case分析报告,标注出当天的Top问题类型和具体案例。这个报告我雷打不动每天早上看一遍,比任何仪表盘都有用。
2.3 A/B实验体系
所有改动必须跑A/B实验。架构:
```
用户流量 →
├─ 实验组(5%流量):新版本
└─ 对照组(95%流量):旧版本
↓
分别记录指标
↓
统计显著性检验
```
配置中心控制实验开关:
```python
通过配置中心动态控制
experiments = {
"new_reranker": {
"enabled": True,
"traffic_percent": 5,
"target_users": "all" # 也可指定特定用户组
}
}
def route_request(user_id, query):
exp = experiments.get("new_reranker")
if exp'enabled' and hash(user_id) % 100 < exp'traffic_percent':
return handle_with_new_reranker(query)
else:
return handle_with_old_pipeline(query)
```
实验至少跑7天,收集足够样本后做t检验。p值<0.05且指标正向才全量发布。
第三章:用户反馈闭环
3.1 显式反馈
在回答下面加"有用/无用"按钮是最基础的。但仅此远远不够。
我加了一个追问机制:当用户点"无用"时,弹出一个轻量级选择框:
```
这个回答哪里有问题?
□ 答非所问
□ 信息不完整
□ 信息有误
□ 答案太啰嗦
□ 其他(请说明)
```
这个设计把负反馈从二元信号升级为多维信号,极大地方便了后续分析。上线后负反馈的分类准确率从靠猜变成了有数据支撑。
3.2 隐式反馈
用户不点按钮,但他们的行为泄露了大量信息:
```python
def collect_implicit_feedback(trace):
signals = {
"copy_action": user_copy_text, # 复制了回答 → 有用
"read_time": time_on_page, # 停留时间 >30秒 → 有用
"follow_up": follow_up_query, # 追问 → 可能没满意
"search_refine": refine_search, # 30秒内重新搜 → 没满意
"scroll_depth": scroll_position # 滚动到底部 → 可能看了完整内容
}
return signals
```
用一个轻量模型做加权打分,生成隐式反馈标签:
```python
def compute_implicit_score(signals):
score = 0
if signals'copy_action':
score += 0.8
if signals'read_time' > 30:
score += 0.3
if signals'search_refine':
score -= 0.6
if signals'follow_up':
score -= 0.4
return score # 范围-1, 1
```
隐式反馈覆盖了90%以上没点按钮的用户,让反馈数据量翻了5倍。
3.3 从反馈到迭代
反馈数据每周汇总一次,驱动三个动作:
**动作一:测试集补充。** 负反馈中的典型案例经过人工审核后,加入测试集。确保同一个问题下次回归测试能暴露。
```python
def update_testset(bad_cases):
for case in bad_cases:
if case'severity' == 'high':
严重bad case,立即加入blocking test
add_to_blocking_testset(case)
else:
普通bad case,加入weekly testset
add_to_daily_regression(case)
```
**动作二:检索调优。** 如果bad case集中在某些query模式上,针对性调整检索参数或分词词典。
**动作三:文档补全。** 如果判断为knowledge gap,触发文档补录流程,通知对应的业务部门负责人补充资料。
第四章:人工评估
机器指标再完善,最后还是要人来判断。
4.1 盲测
每月做一次盲测:把同一个问题分别问系统、问竞品、问纯LLM(无RAG),然后把三个答案混在一起,让专家打分。
```python
def blind_test(queries, systems):
results = \[\]
for query in queries:
answers = \[\]
for system in systems:
answers.append(system.answer(query))
打乱顺序
shuffled = shuffle(answers)
results.append({
"query": query,
"candidates": shuffled,
"ground_truth": get_ground_truth(query)
})
发给专家标注
return results
```
盲测一个月花两个工时的人工标注成本,换来的是对系统真实水平的清醒认知。不做盲测的团队很容易被离线指标欺骗。
4.2 专家标注SOP
给专家的标注规范:
| 维度 | 1分 | 3分 | 5分 |
|------|-----|-----|-----|
| 相关性 | 完全跑题 | 部分相关 | 精确命中 |
| 完整性 | 漏掉关键信息 | 覆盖大部分 | 完整无遗漏 |
| 可操作性 | 看了还是不会 | 有一定参考 | 照着做就能解决 |
| 可信度 | 有明显错误 | 无错误但有疑点 | 完全可信 |
标注结果作为"黄金标准",用来校验AI裁判的准确性。如果AI裁判和专家标注的分差超过1分,就调整裁判prompt。