跳到主要内容

Phoenix experiments:系统性对比变体

本章讲什么: evals(ch.03)是"给一条输出打分";experiments 是把它规模化、系统化——在一整个数据集上跑你应用的某个变体,逐条打分,再和别的变体对比。本章讲数据模型和服务端 ExperimentRunner 的调度结构。

诚实声明: experiment_runner.py 有约 2849 行,本章讲到它的结构和设计意图(以代码内注释和类形状为据),不逐行追踪每个调度分支。

3.1 概念:数据集 + 任务 + 评估器

问题: 你改了 prompt(或换了模型、改了检索策略),想知道"在我关心的 50 个代表性问题上,新版本比旧版本好还是坏"。手动一个个试不现实。

思路: 三样东西组合:

Dataset (一组带版本的样例,每个样例有 input + 可选 expected output)

│ 对每个 example 跑一次 ──► Task (= 你的应用变体,如"新 prompt 调 GPT-4")
│ │ 产出
│ ▼
│ ExperimentRun (这个样例在这个变体下的输出 + 耗时 + 错误)
│ │ 对每个 run 跑
│ ▼
└──────────────────────► Evaluator (ch.03 的评估器,打分)
│ 产出

ExperimentRunAnnotation (这次 run 的得分)

模型见 src/phoenix/db/models.py:Dataset:1293DatasetVersion:1401DatasetExample:1414DatasetExampleRevision:1448Experiment:1531ExperimentRun:1630ExperimentRunAnnotation:1674

数据集为什么要版本化: DatasetExampleRevision(models.py:1448)记录每次对样例的增删改。实验跑在某个 DatasetVersion 上,所以"我上周跑的实验"和"这周跑的"即使数据集后来改了,也能精确对应到当时的样例快照——结果可复现、可比较。

3.2 服务端调度:ExperimentRunner 守护进程

实验不是同步阻塞跑的,而是交给一个后台守护进程 ExperimentRunner(src/phoenix/server/daemons/experiment_runner.py),它和 ch.01 的 BulkInserter、成本计算器一样,在 app.py_lifespan 里作为长驻 context 启动(app.py:639)。

两类工作项,优先级不同。 runner 把活拆成两种:

  • 跑任务:对一个 dataset example 执行用户的应用变体,产出 ExperimentRun
  • 跑评估:对一个已完成的 ExperimentRun 跑一个评估器,产出得分。

文件顶部的注释点明了一个调度取舍(experiment_runner.py:29 附近):

"Why: evaluator output is user-visible feedback, so evals are prioritized..." —— 评估输出是用户直接看到的反馈,所以评估被优先调度(让"分数"尽快出现在 UI 上),而不是等所有任务都跑完。

评估工作项的类带注释 "Eval work item: run one evaluator on one task result. No streaming - evaluators run silently."(experiment_runner.py:727-729),可见任务运行可能流式回传进度,而评估是静默的。

外部调用的隔离。 注释里有一段调度纪律(experiment_runner.py:314 附近):"Run the external call (LLM / evaluator). Nothing else." —— 把对外的 LLM/评估调用单独成一步,其余编排逻辑不和它混在一起。这是为了让慢的、可能失败的外部 I/O 边界清晰、好重试、好限流。

3.3 评估器从哪来:DB 里存配置,运行时再水合

实验用的评估器配置存在数据库(DatasetEvaluators),运行时再"水合"(hydrate)成可执行的评估器对象。runner 里 _resolve... 这类逻辑优先用 DB 里存的 output_configs,只有当行里没有时才回退到水合出的评估器自带配置:

# src/phoenix/server/daemons/experiment_runner.py:253-258 (节选)
# Prefer dataset_evaluators.output_configs from the database; fall back to the
# hydrated evaluator only when the row has no stored configs.
if dataset_evaluator.output_configs:
return list(dataset_evaluator.output_configs)
return list(evaluator.output_configs)

服务端评估器的获取/构造在 src/phoenix/server/api/evaluators.py(runner 从这里 import get_evaluators 等,experiment_runner.py:121-125)。这里的评估器和 ch.03 的 phoenix-evals 抽象相连——experiments 是 evals 的规模化消费者。

3.4 沙箱:跑用户代码的隔离

实验任务可能要执行用户提供的代码(task 函数)。runner 依赖一个 SandboxSessionManager,且 _lifespan 里特意安排它先于 runner 进入 context,以保证 AsyncExitStack 逆序拆解时 runner 先停——避免 runner 在 manager 关停后还去 acquire 而泄漏 provider session。这段时序考量在 app.py:633-639 的注释里写得很细。沙箱实现细节见 src/phoenix/server/sandbox/(本章未深入)。

3.5 边界与坑

  • 本章对调度细节是"结构级"理解。 runner 文件极大,具体的并发度、重试、限流分支以代码为准。
  • 实验结果绑定到 dataset version。 改了数据集不影响已有实验的可比性,但新旧实验若跑在不同 version 上,直接比分要留意基线是否一致。
  • 评估优先于任务的调度 意味着资源紧张时任务推进可能被评估"插队",这是为 UI 反馈体验做的主动取舍。

3.6 代码地图

主题文件符号
数据集模型src/phoenix/db/models.pyDatasetDatasetVersionDatasetExampleDatasetExampleRevision
实验模型src/phoenix/db/models.pyExperimentExperimentRunExperimentRunAnnotation
调度守护进程src/phoenix/server/daemons/experiment_runner.pyExperimentRunner
服务端评估器src/phoenix/server/api/evaluators.pyget_evaluators
沙箱src/phoenix/server/sandbox/SandboxSessionManager
装配与时序src/phoenix/server/app.py_lifespan(:633-639)
评估器抽象packages/phoenix-evals/03-evals.md