跳到主要内容

Evidently 总览:这是什么 · 全景图 · 阅读地图

30 秒导读: Evidently 是一个开源 Python 框架,用来评估、测试、监控 ML 和 LLM 系统。 你把一个 pandas.DataFrame 装进它的 Dataset,挂上一批"评委"(Descriptor),再交给 Report 跑一遍,就能拿到一份带指标、图表和通过/失败判定的结果快照(Snapshot)。同一套代码,能用在 实验期的一次性评估、CI 里的回归测试、以及线上的持续监控。

本章是整个 Evidently 讲解的入口页:只讲"这是什么、大盘怎么转、每章讲什么",不下钻任何单个 机制的源码细节——那些留给 01–05 各章。


1. 这是什么(零基础也能懂)

一句话定义。 Evidently 是一个开源的 ML / LLM 评估与监控框架,把"给模型的输入输出打分、 判定质量好坏、并把结果画出来 / 存起来"这件事标准化成一条流水线。

给谁用、解决什么问题。 想象你在做一个 LLM 应用或 ML 模型,你会反复问三类问题——Evidently 把 这三个场景用同一套抽象覆盖了:

场景你想知道的Evidently 怎么帮你
实验期"这批回答质量如何?换个 prompt 好了没?"一次性跑 Report,在 Notebook 里看交互式结果
CI / 回归"这次改动有没有让指标掉下去?"给 Report 加通过/失败条件,变成 Test Suite,红了就 fail
线上监控"上周到这周,数据/质量漂移了吗?"把每次 Snapshot 按时间戳落库,在 Monitoring UI / Cloud 看时序

它能做什么(功能面)。 据 README(README.md:27-38)与"能评估什么"清单(README.md:208-224):

  • 文本描述符:长度、情感、毒性、语言、正则匹配等(行级评估)。
  • LLM 输出评估:语义相似度、检索相关性、摘要质量,含模型法与 LLM-as-a-judge
  • 数据质量与数据漂移:缺失值、重复、范围,外加 20+ 统计检验 / 距离度量比较分布偏移。
  • 经典 ML:分类(accuracy/precision/recall/ROC AUC…)、回归(MAE/RMSE…)、排序 / 推荐(NDCG/MAP/MRR…)。
  • 100+ 内置指标,且可自定义。

用起来什么样(一个最小真实示例)。 下面这段来自 README 的 "Hello World"(README.md:93-140): 先把问答对装进 Dataset 并挂三个描述符(情感、长度、是否含拒答词),再用 TextEvals 预设跑一份 Report:

import pandas as pd
from evidently import Report
from evidently import Dataset, DataDefinition
from evidently.descriptors import Sentiment, TextLength, Contains
from evidently.presets import TextEvals

eval_df = pd.DataFrame([
["What is the capital of Japan?", "The capital of Japan is Tokyo."],
["Who painted the Mona Lisa?", "Leonardo da Vinci."],
["Can you write an essay?", "I'm sorry, but I can't assist with homework."]],
columns=["question", "answer"])

# 把 DataFrame 装进 Dataset,并挂上三个行级"评委"(descriptor)
eval_dataset = Dataset.from_pandas(
eval_df,
data_definition=DataDefinition(),
descriptors=[
Sentiment("answer", alias="Sentiment"), # 每行答案的情感分
TextLength("answer", alias="Length"), # 每行答案的长度
Contains("answer", items=['sorry', 'apologize'], mode="any", alias="Denials"), # 是否拒答
])

# 用 TextEvals 预设汇总这些分数的分布,跑出一份 Report
report = Report([TextEvals()])
my_eval = report.run(eval_dataset)
my_eval # 在 Notebook 里渲染成交互式 HTML;也可 my_eval.json() / my_eval.dict()

一句话直觉 / 类比。 把 Evidently 当成给数据做体检的流水线:Dataset 是送检的样本, Descriptor 是一项项化验(每行一个分数),Metric 是把化验汇总成的指标,Test 是"指标是否 在正常范围"的红绿灯,Report → Snapshot 是最终那份体检报告——可以当场看,也可以归档进病历库 (监控 UI)对比历次。


2. 顶层全景(它大概怎么转)

2.1 一张图看清核心数据流

从左到右就是一次评估的生命周期;每一步都在给数据"加东西",最后落成一份可看、可导出、可入库的 Snapshot

挂"评委"富化 装 Metric/Preset/Test
pandas.DataFrame ─────────────────► Dataset ─────────────────────► Report
│ add_descriptors() (+DataDefinition: (一份评估的配置)
│ 每个 Descriptor 列语义/类型映射) │
│ 往表上加一列分数 │ │ .run(current, reference?)
└────────────────────────────────────┘ ▼
Context(执行引擎)
· 遍历 Report 里的每个 item
· 算 Metric、按 id 缓存、跑依赖
· 对每个结果跑绑定的 Test(红绿灯)


Snapshot (= Run)
MetricResult + TestResult + widgets

┌────────────────────┬───────────────────┬─────────────┴──────────┐
▼ ▼ ▼ ▼
HTML JSON / dict 时序落库 Workspace / Project
(Notebook/文件) (给程序/CI 用) (按 timestamp) → Monitoring UI / Cloud

怎么读这张图: 主干是"数据 → 富化 → 配置 → 引擎 → 快照"五步,下方四个分叉是同一份 Snapshot四种出口(看、导出、入库、上监控)。reference 数据集是可选的第二份输入——传了它,漂移/对比类 指标才会做"当前 vs 参照"的两列对照。

2.2 部件一句话职责

部件干什么在哪个文件
Dataset / DataDefinition承载数据 + 声明每列语义(target/prediction/文本…)与类型core/datasets.py:1197 / :367
Descriptor行级"评委":给每行算一个分数,作为新列长到数据上core/datasets.py:737
Report一次评估的配置:装一组 Metric / Preset / Test,.run() 是入口core/report.py:821
Metric / MetricResult数据集级度量与它的计算结果(单值/按标签/均值方差…)core/metric_types.py:1114 / :165
MetricContainer / Preset一次展开成"一组指标"的容器;Preset 是它的成品(如 TextEvals)core/container.py:25
Test / condition挂在指标上的通过/失败判定(gt/lt…),让 Report 变 Test Suitecore/metric_types.py:1008 (MetricTest)
Context运行引擎:遍历 item、算指标、按 id 缓存、跑依赖与 Testcore/report.py:123
Snapshot (= Run)一次运行的结果快照:结果 + widgets + 元数据,可导 HTML/JSONcore/report.py:487
Workspace / Project把 Snapshot 按时间归档成项目,喂给监控 UI / Cloudui/workspace.py:731 / :146
LLM judge用大模型当评委的 Descriptor(打分/分类)descriptors/llm_judges.py:119 (LLMEval)

2.3 主线走一遍(高层,不进代码)

一次 report.run(data) 到底发生了什么?对照 core/report.py 的三个关键落点:

  1. 入口:Report.run(core/report.py:904)——把传入的 DataFrame/Dataset 统一成 Dataset (Dataset.from_any),盖上时间戳,新建一个 Snapshot 并调用它的 run()

  2. 驱动:Snapshot._run_items(core/report.py:548)——遍历 Report 里的每个 item。若是 MetricContainer(Preset),先把它展开成一批子指标再递归;若是普通 Metric,调 item.to_calculation() 得到计算对象,交给引擎。

  3. 计算 + 缓存 + 判定:Context.calculate_metric(core/report.py:191)——这是引擎心脏:

    • 若该指标 id 还没算过,调 calc.call(self) 真正计算,得到 (current_result, reference_result);
    • 把结果按 calc.id 缓存self._metrics(依赖同一指标的其它计算直接命中,不重复算);
    • 对结果跑所有绑定的 Test(get_bound_testsrun_test),把红绿灯挂回结果上。

    算完所有 item 后,Snapshot.run(core/report.py:569)收集顶层指标、把测试结果汇成两个 统计 widget。至此 Snapshot 就绪,可 _repr_html_ 直接在 Notebook 渲染,或 json()/dict() 导出,或入库上监控。


3. 一个大主题:core/(v2 新引擎)与 legacy/(v1 老实现)并存

这是理解整个代码库最关键的一张地图。Evidently 经历过一次大版本重构,新旧两套并存,而且不是 简单的"老代码等着删"——新引擎在运行时仍然调用老实现

分层直觉:

  • core/ + descriptors/ + llm/ = v2 新 API 与新引擎。你 from evidently import ... 拿到的 ReportDatasetDescriptor 都在这里(见 src/evidently/__init__.py:12-22)。
  • legacy/ = v1 的实现,以及被 v2 复用的底层零件。三类东西仍被 core 直接调用:
    • 统计检验:20+ stattest 都在 legacy/calculations/stattests/(04 章讲)。
    • HTML widget 与渲染器:Snapshot 出 HTML 用的是 legacy/renderers/(core/report.py:36 就 从这里 import DEFAULT_RENDERERS;get_legacy_metriccore/report.py:301 找老渲染器)。
    • LLMJudge feature:LLM 评委的底层特征计算落在 legacy/features/llm_judge.py(03 章讲)。

一句话记住: 新 API 是门面,老代码是仍在服役的地基。core/report.py 里那一堆 from evidently.legacy... 的 import,就是这层"新调老"关系的直接证据。各章遇到具体机制时会指出它 究竟落在 core 还是 legacy。


4. 关键概念一句话表(带跳转)

想快速对上号,先看这张表;每个概念的"为什么这么设计"在对应章展开。

概念一句话定义位置详见
Dataset数据的容器,from_pandas/from_any 入口core/datasets.py:119701
DataDefinition声明列语义与类型(target/prediction/文本…)core/datasets.py:36701
Descriptor行级评委,add_descriptors 把分数长成新列core/datasets.py:737,:137801
Metric / MetricResult数据集级度量及其结果(单值/按标签/均值方差…)core/metric_types.py:1114,:16502
MetricCalculationMetric 对应的实际计算逻辑core/metric_types.py:122902
MetricContainer / Preset一展开成一组指标的容器;成品如 TextEvalscore/container.py:25;presets/dataset_stats.py:49402
Test / condition指标上的通过/失败判定,让 Report 变 Test Suitecore/metric_types.py:100802
Report / Snapshot(Run)评估配置 / 运行结果快照core/report.py:821,:48702
Context运行引擎:遍历、缓存、跑依赖与 Testcore/report.py:12302
LLM judge(LLMEval)用大模型当评委的 Descriptordescriptors/llm_judges.py:11903
Prompt 模板LLM 判官的判定模板(二分类/多分类)llm/templates.py:9503
stattest20+ 分布检验 / 距离度量legacy/calculations/stattests/04
Workspace / ProjectSnapshot 归档与监控项目ui/workspace.py:731,:14605
Guardrails把评委当运行时护栏用guardrails/05

5. 阅读地图(建议顺序)

各章由浅入深,建议按顺序读;每章自带代码地图,可直接跳源码。

讲什么什么时候读
01-data-model.md数据模型与 Descriptor:Dataset/DataDefinition 怎么声明列语义,Descriptor 如何把行级分数长到数据上想弄懂"输入端"怎么组织、怎么写自定义评委
02-metrics-engine.md度量与运行引擎:Metric 的几种结果类型、Context 的依赖缓存、Report→Snapshot 全流程、Preset 展开与 Test 条件想弄懂 core 的心脏——一次 run 到底怎么算
03-llm-as-judge.mdLLM 即评委:判官 Descriptor、Prompt 模板、异步批量 LLM 引擎做 LLM 应用评估、想用大模型打分
04-drift-and-stats.md数据漂移与统计检验:分布怎么比、20+ stattest 怎么组织与选择做数据漂移检测、想懂统计层
05-observability-and-ops.md从评估到监控:Snapshot 时序化、Workspace/Project、Cloud SDK、Guardrails 与 Prompt 优化把一次性评估升级成线上监控/护栏

最短路径: 只想跑起来 → 读 §1 的示例即可;想懂原理 → 01 → 02;做 LLM 评估 → 加 03; 上生产监控 → 加 05;深究漂移统计 → 04。


6. 巧妙之处(可借鉴的技术)

这几点是读源码值得带走的跨章设计,先说妙在哪,再给锚点;细节各章展开。

① 一套抽象吃两种场景:实验 = 监控。 「评估」和「监控」在别的工具里常是两套代码,Evidently 让它们共用 Report/Snapshot。一次 report.run 产出的 Snapshot 既能当场看,也能被 Workspace 按时间串起来画曲线——监控只是「重复跑同一个报告并存快照」。见 Snapshot(别名 Run,core/report.py:487)与 Report.run(core/report.py:904)。

② 指标结果不是裸浮点,而是带形状的类型。 一个指标的结果可能是单值、按标签一组值、计数+占比、均值+标准差、整张 DataFrame。Evidently 把这些统一成 MetricResult 的子类(SingleValueByLabelValueByLabelCountValueCountValueMeanStdValueDataframeValue),既能渲染又能被 Test 精确取值。见 core/metric_types.py:403 起。

③ Context 是「带缓存和依赖图的计算器」。 报告里不同指标常需要同一个中间量(比如都要先算某列的分布)。Context.calculate_metricmetric_id 做缓存键:没算过才真算,否则直接返回;同时用嵌套的依赖图记录谁依赖谁,并在算完顺手跑该指标绑定的 Test。这让「一堆指标共享子计算」既不重复算、又能画出依赖结构。见 core/report.py:191(calculate_metric)。

④ metric_id = 指纹,天然去重。 指标的唯一 id 直接取其配置的 fingerprint(Metric.get_metric_idget_fingerprint)。参数相同的两个指标指纹相同、id 相同,于是自动共享缓存、自动去重——不需要手写「这两个是不是同一个指标」的判断。见 core/metric_types.py:1114(Metric)。

⑤ Preset 是「运行时才展开」的指标生成器。 Preset/MetricContainer 不预先固定指标,而是在拿到数据后才 generate_metrics(context)——所以 DataDriftPreset看到实际有几列、每列什么类型,再给每列挑合适的检验、生成 ValueDrift + DriftedColumnsCount。见 core/container.py:25presets/drift.py:24

⑥ LLM 判官只是「一种描述子」。 用大模型打分没有被做成特殊分支,而是复用行级抽象:LLMEval 继承 Descriptor,generate_data 里去调大模型。底层 LLMWrapper 负责异步并发 + 信号量限流 + 批量(_batchasyncio.Semaphore(batch_size) 控制并发,complete_batch 打包整批请求),让「给上千行调判官」既快又不打爆速率限制。见 descriptors/llm_judges.py:119llm/utils/wrapper.py:252(_batch)、:285(complete_batch)。

⑦ 20+ 统计检验被收进一个 StatTest 壳。 不同分布检验(PSI、KS、卡方、Jensen-Shannon、Wasserstein、能量距离、MMD…)签名各异,StatTest 把它们统一成「输入两列 → 输出(漂移分数, 是否漂移, 实际阈值)」,于是漂移逻辑不关心底下用的是哪种检验,可按列类型自由替换。见 legacy/calculations/stattests/registry.py:34 及同目录各 *_stattest.py


7. 代码地图(导航索引)

index 只给"进门指路";每章末尾另有更细的代码地图。

主题文件路径符号名
对外导出总清单src/evidently/__init__.py__all__
Report 配置与入口src/evidently/core/report.py:821,:904Report,Report.run
运行引擎(缓存/依赖/判定)src/evidently/core/report.py:123,:191Context,Context.calculate_metric
item 驱动与结果收集src/evidently/core/report.py:548,:569Snapshot._run_items,Snapshot.run
结果快照与导出src/evidently/core/report.py:487,:640Snapshot(Run),Snapshot.json
数据容器与列语义src/evidently/core/datasets.py:1197,:367Dataset,DataDefinition
行级评委src/evidently/core/datasets.py:737,:1378Descriptor,Dataset.add_descriptors
度量类型与结果src/evidently/core/metric_types.py:1114,:165Metric,MetricResult
指标容器 / Presetsrc/evidently/core/container.py:25;src/evidently/presets/dataset_stats.py:494MetricContainer,TextEvals
LLM 评委src/evidently/descriptors/llm_judges.py:119;src/evidently/llm/templates.py:95LLMEval,BinaryClassificationPromptTemplate
统计检验(legacy 复用)src/evidently/legacy/calculations/stattests/psi,jensenshannon,chisquare_stattest
监控归档src/evidently/ui/workspace.py:731,:146Workspace,Project