关于 rag-observability-evals
面向 RAG 工程团队的可观测性与评估技能,提供检索、生成质量及可靠性指标的度量方法,并涵盖评估流水线、告警和故障排查。
主要功能
- 01检索质量指标评估
- 02生成依据与幻觉监测
- 03Prometheus 指标导出
- 04离线及在线评估流水线
- 05质量阈值与告警配置
适用场景
- 01监控生产 RAG 系统质量
- 02定位检索相关性或幻觉退化
- 03为 RAG 发布设置质量门槛
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ bagelhole/ devops-security-agent-skills --skill rag-observability-evals安装后在 Claude Code 中输入/rag-observability-evals确认已加载。
详细介绍
RAG 可观测性与评估
将检索增强生成作为可度量的生产系统运行,而不是黑盒。
何时使用此技能
- 将 RAG 系统部署到生产环境,并需要质量监控
- 设置检索和生成的自动化评估流水线
- 调试幻觉或相关性回退
- 为 RAG 特有的黄金信号构建仪表板
- 为 RAG 流水线变更建立质量门禁
前提条件
- 已对检索和生成阶段进行插桩的 RAG 流水线
- 已安装评估库(ragas、langchain、openai)的 Python 3.10+
- 用于导出自定义指标的 Prometheus 端点
- 包含标准答案、问题、答案和来源三元组的基准数据集
- 已集成到 RAG 服务中的 OpenTelemetry SDK
衡量内容
检索质量
- Top-k 分块的 Recall@k 和 MRR
- 引用覆盖率和来源时效性
- 嵌入漂移和索引过时情况
生成质量
- 有据性得分(答案是否得到检索上下文的支持)
- 按路由/用例统计的幻觉率
- 指令遵循情况和格式有效性
可靠性与成本
- 按检索和生成阶段拆分的 p50/p95 延迟
- 各阶段的令牌用量
- 缓存命中率和每个成功答案的成本
RAGAS 评估脚本
# rag_eval.py
"""Evaluate RAG pipeline quality using RAGAS metrics."""
from ragas import evaluate
from ragas.metrics import (
faithfulness,
answer_relevancy,
context_precision,
context_recall,
context_entity_recall,
answer_similarity,
)
from datasets import Dataset
import json
import sys
def load_eval_dataset(path: str) -> Dataset:
"""Load evaluation dataset with required columns."""
with open(path) as f:
data = json.load(f)
return Dataset.from_dict({
"question": [d["question"] for d in data],
"answer": [d["generated_answer"] for d in data],
"contexts": [d["retrieved_contexts"] for d in data],
"ground_truth": [d["reference_answer"] for d in data],
})
def run_evaluation(dataset_path: str, output_path: str):
"""Run full RAGAS evaluation suite."""
dataset = load_eval_dataset(dataset_path)
metrics = [
faithfulness,
answer_relevancy,
context_precision,
context_recall,
context_entity_recall,
answer_similarity,
]
results = evaluate(dataset, metrics=metrics)
# Print summary
print("=== RAG Evaluation Results ===")
for metric_name, score in results.items():
print(f" {metric_name}: {score:.4f}")
# Save detailed results
with open(output_path, "w") as f:
json.dump({
"summary": {k: float(v) for k, v in results.items()},
"dataset_size": len(dataset),
}, f, indent=2)
return results
if __name__ == "__main__":
run_evaluation(sys.argv[1], sys.argv[2])
有据性评分
# groundedness.py
"""Score whether generated answers are grounded in retrieved context."""
from openai import OpenAI
import json
from typing import List
client = OpenAI()
GROUNDEDNESS_PROMPT = """You are evaluating whether an AI answer is fully grounded
in the provided context documents. Score each claim in the answer.
Context documents:
{contexts}
Answer to evaluate:
{answer}
For each distinct claim in the answer, determine:
1. SUPPORTED - the claim is directly supported by the context
2. PARTIALLY_SUPPORTED - the claim is partially supported
3. NOT_SUPPORTED - the claim has no support in the context
Return JSON:
{{
"claims": [
{{"claim": "...", "verdict": "SUPPORTED|PARTIALLY_SUPPORTED|NOT_SUPPORTED", "evidence": "..."}}
],
"groundedness_score": <float 0-1>,
"unsupported_claims": ["..."]
}}
"""
def score_groundedness(answer: str, contexts: List[str]) -> dict:
"""Score groundedness of a single answer against its contexts."""
context_text = "\n---\n".join(
f"[Document {i+1}]: {c}" for i, c in enumerate(contexts)
)
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": GROUNDEDNESS_PROMPT.format(
contexts=context_text, answer=answer
),
}],
response_format={"type": "json_object"},
temperature=0,
)
return json.loads(response.choices[0].message.content)
def batch_groundedness(eval_data: list) -> dict:
"""Score groundedness for a batch of QA pairs."""
scores = []
unsupported_count = 0
total_claims = 0
for item in eval_data:
result = score_groundedness(
item["generated_answer"],
item["retrieved_contexts"],
)
scores.append(result["groundedness_score"])
unsupported_count += len(result["unsupported_claims"])
total_claims += len(result["claims"])
avg_score = sum(scores) / len(scores) if scores else 0
return {
"average_groundedness": avg_score,
"total_claims": total_claims,
"unsupported_claims": unsupported_count,
"unsupported_rate": unsupported_count / total_claims if total_claims else 0,
"sample_count": len(eval_data),
}
检索质量指标
# retrieval_metrics.py
"""Compute retrieval quality metrics for RAG evaluation."""
from typing import List, Set
import numpy as np
def recall_at_k(
retrieved_ids: List[str],
relevant_ids: Set[str],
k: int
) -> float:
"""Compute Recall@K for a single query."""
top_k = set(retrieved_ids[:k])
if not relevant_ids:
return 0.0
return len(top_k & relevant_ids) / len(relevant_ids)
def mrr(
retrieved_ids: List[str],
relevant_ids: Set[str]
) -> float:
"""Compute Mean Reciprocal Rank for a single query."""
for i, doc_id in enumerate(retrieved_ids):
if doc_id in relevant_ids:
return 1.0 / (i + 1)
return 0.0
def ndcg_at_k(
retrieved_ids: List[str],
relevant_ids: Set[str],
k: int
) -> float:
"""Compute NDCG@K for a single query."""
dcg = 0.0
for i, doc_id in enumerate(retrieved_ids[:k]):
if doc_id in relevant_ids:
dcg += 1.0 / np.log2(i + 2)
ideal_dcg = sum(1.0 / np.log2(i + 2) for i in range(min(len(relevant_ids), k)))
return dcg / ideal_dcg if ideal_dcg > 0 else 0.0
def compute_retrieval_metrics(
queries: list,
k_values: list = [1, 3, 5, 10]
) -> dict:
"""Compute aggregate retrieval metrics across all queries."""
results = {}
for k in k_values:
recalls = [
recall_at_k(q["retrieved_ids"], set(q["relevant_ids"]), k)
for q in queries
]
mrrs = [mrr(q["retrieved_ids"], set(q["relevant_ids"])) for q in queries]
ndcgs = [
ndcg_at_k(q["retrieved_ids"], set(q["relevant_ids"]), k)
for q in queries
]
results[f"recall@{k}"] = np.mean(recalls)
results[f"ndcg@{k}"] = np.mean(ndcgs)
results["mrr"] = np.mean(mrrs)
return results
Prometheus 指标导出
# rag_metrics_exporter.py
"""Export RAG quality metrics to Prometheus."""
from prometheus_client import Histogram, Counter, Gauge, start_http_server
import time
# Latency histograms by stage
RETRIEVAL_LATENCY = Histogram(
"rag_retrieval_duration_seconds",
"Time spent in retrieval stage",
["index_name", "retriever_type"],
buckets=[0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
)
GENERATION_LATENCY = Histogram(
"rag_generation_duration_seconds",
"Time spent in generation stage",
["model", "route"],
buckets=[0.5, 1.0, 2.0, 5.0, 10.0, 30.0],
)
RERANKING_LATENCY = Histogram(
"rag_reranking_duration_seconds",
"Time spent in reranking stage",
["reranker_model"],
buckets=[0.05, 0.1, 0.25, 0.5, 1.0],
)
# Quality gauges (updated from offline evals)
GROUNDEDNESS_SCORE = Gauge(
"rag_groundedness_score",
"Latest groundedness evaluation score",
["route", "model"],
)
FAITHFULNESS_SCORE = Gauge(
"rag_faithfulness_score",
"Latest faithfulness evaluation score",
["route", "model"],
)
CONTEXT_PRECISION = Gauge(
"rag_context_precision_score",
"Latest context precision score",
["route", "index_name"],
)
RECALL_AT_K = Gauge(
"rag_recall_at_k",
"Recall@K for retrieval",
["k", "index_name"],
)
# Operational counters
REQUESTS_TOTAL = Counter(
"rag_requests_total",
"Total RAG requests",
["route", "status"],
)
HALLUCINATION_DETECTED = Counter(
"rag_hallucination_detected_total",
"Detected hallucinations",
["route", "severity"],
)
FALLBACK_TRIGGERED = Counter(
"rag_fallback_triggered_total",
"Times RAG fell back to abstain/default",
["route", "reason"],
)
TOKENS_USED = Counter(
"rag_tokens_used_total",
"Tokens consumed by stage",
["stage", "model"],
)
CACHE_HITS = Counter(
"rag_cache_hits_total",
"Semantic cache hits",
["cache_type"],
)
# Index health
INDEX_STALENESS_SECONDS = Gauge(
"rag_index_staleness_seconds",
"Seconds since last index update",
["index_name"],
)
INDEX_DOCUMENT_COUNT = Gauge(
"rag_index_document_count",
"Number of documents in index",
["index_name"],
)
def start_metrics_server(port: int = 9090):
"""Start Prometheus metrics HTTP server."""
start_http_server(port)
print(f"RAG metrics server running on :{port}/metrics")
评估流水线
- 整理包含标准答案和来源文档的基准数据集。
- 对每种检索器/模型配置运行每夜离线评估。
- 对抽样的生产流量执行在线影子评估。
- 根据最低质量、安全性和延迟阈值为发布设置门禁。
# eval-pipeline-cron.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: rag-nightly-eval
namespace: ai-evals
spec:
schedule: "0 2 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: eval-runner
image: registry.internal/rag-eval:latest
command:
- python
- -m
- rag_eval
- --dataset=/data/benchmark_v3.json
- --output=/results/nightly-$(date +%Y%m%d).json
- --push-metrics
- --fail-on-regression
env:
- name: PROMETHEUS_PUSHGATEWAY
value: "http://pushgateway:9091"
- name: MLFLOW_TRACKING_URI
value: "http://mlflow:5000"
volumeMounts:
- name: eval-data
mountPath: /data
- name: results
mountPath: /results
volumes:
- name: eval-data
persistentVolumeClaim:
claimName: eval-benchmark-data
- name: results
persistentVolumeClaim:
claimName: eval-results
restartPolicy: OnFailure
告警策略
# rag-alerts.yaml
groups:
- name: rag-quality-alerts
rules:
- alert: GroundednessDropped
expr: rag_groundedness_score < 0.75
for: 10m
labels:
severity: sev2
annotations:
summary: "Groundedness score dropped below 0.75 for {{ $labels.route }}"
- alert: HallucinationSpike
expr: |
rate(rag_hallucination_detected_total[15m])
/ rate(rag_requests_total[15m]) > 0.10
for: 5m
labels:
severity: sev1
- alert: IndexStale
expr: rag_index_staleness_seconds > 86400
for: 5m
labels:
severity: sev3
annotations:
summary: "Index {{ $labels.index_name }} not updated in 24h"
- alert: HighFallbackRate
expr: |
rate(rag_fallback_triggered_total[10m])
/ rate(rag_requests_total[10m]) > 0.20
for: 10m
labels:
severity: sev2
- alert: RetrievalLatencyHigh
expr: |
histogram_quantile(0.95,
rate(rag_retrieval_duration_seconds_bucket[5m])
) > 2.0
for: 5m
labels:
severity: sev2
实用防护措施
- 对高风险领域强制要求引用。
- 置信度低于阈值时,返回弃答/回退结果。
- 在最终生成前对检索到的分块重新排序。
- 仅在严格的回归测试下使用查询改写。
事件分诊检查清单
| 症状 | 首先检查 | 其次检查 |
|---|---|---|
| 有据性下降 | 是否更改了嵌入模型? | 是否更改了分块/索引逻辑? |
| 检索结果包含不相关文档 | 索引时效性和文档数量 | 嵌入模型版本不匹配 |
| 检索延迟激增 | 向量数据库连接池和负载 | 索引大小是否超出阈值 |
| 每个答案的成本增加 | 各阶段令牌用量明细 | 缓存命中率下降 |
| 幻觉激增 | 模型版本或温度是否更改 | 上下文窗口溢出(文档被截断) |
故障排除
| 问题 | 诊断 | 解决方法 |
|---|---|---|
| RAGAS 评估的所有指标均返回 0 | 检查数据集格式是否符合预期架构 | 确保上下文是列表,而不是字符串 |
| 有据性得分不可靠 | LLM 评审结果不一致 | 增加评审样本量,将温度设为 0 |
| 索引过时告警触发 | 数据摄取流水线故障 | 检查数据源连通性和摄取日志 |
| 检索召回率下降 | 模型更新后发生嵌入漂移 | 使用当前嵌入模型重新为语料库建立索引 |
| 生成延迟过高 | 上下文对于模型来说过大 | 减少 top-k 或添加摘要步骤 |
相关技能
- rag-infrastructure - 部署健壮的 RAG 后端
- agent-observability - 为请求、追踪和成本添加插桩
- agent-evals - 构建可重复的评估套件
- ai-sre-incident-response - 应对质量回退的事件
- opentelemetry - 为 RAG 流水线进行分布式追踪
评论
支持 Markdown