RAG系统搭建

第一章:离线评估体系

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分):

  1. 事实准确性:回答中的事实是否与标准答案一致

  2. 完整性:是否覆盖了标准答案的核心要点

  3. 冗余度:是否有无关信息

输出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。