Phoenix evals:给 LLM 输出打分
本章讲什么:
phoenix-evals是一个可独立 pip 安装的子包(packages/phoenix-evals/),不依赖 Phoenix 服务也能用。它提供"评估器(Evaluator)"的统一抽象,以及把任意数据形状对接到评估器的input_mapping机制。本章讲它的三块积木:Score、Evaluator、ClassificationEvaluator。
3.1 这是什么:打分的统一形状
问题: "这次回答好不好"有无数种判法——有没有幻觉、相不相关、有没有毒性、引用对不对。每种判法输入不同(有的要 question+answer,有的要 documents),输出也想统一(都能存进 Phoenix 当标注)。
思路: 用两个抽象统一一切。
Score= 一次评估的结果,统一形状(evaluators.py:159):name、score(数值)、label(类别)、explanation(解释)、metadata、kind(human/llm/heuristic/code)、direction(maximize/minimize)。任何评估器都吐Score,所以下游统一处理。Evaluator= 一个评估器(evaluators.py:303),抽象基类,核心方法是evaluate(eval_input)→List[Score]。
最小用法(来自 README):
# 示意,基于 packages/phoenix-evals README + evaluators.py
from phoenix.evals import create_classifier
from phoenix.evals.llm import LLM
llm = LLM(provider="openai", model="gpt-4o")
sentiment = create_classifier(
name="sentiment",
prompt_template="Classify the sentiment: {text}",
llm=llm,
choices=["positive", "negative", "neutral"],
)
result = sentiment.evaluate({"text": "I love this product!"})
print(result[0].label) # "positive"
print(result[0].explanation) # LLM 的理由
3.2 核心机制:input_mapping —— 把任意数据形状"绑"到评估器
要解决的小问题: 一个相关性评估器内部要的是字段 question 和 answer。但你的数据里可能叫 query 和 response,或者嵌在 record["input"]["q"] 里。怎么对接而不强迫你改数据?
思路: 评估器声明它需要哪些字段(从 prompt 模板变量或 input_schema 推断);你提供一个 input_mapping 说"我的数据里这个字段对应你要的那个字段",值可以是键名、JSONPath 风格路径,或一个取值函数。
Evaluator.evaluate 的骨架(evaluators.py:419-437):
# src/.../evaluators.py:419-437 (节选)
def evaluate(self, eval_input, input_mapping=None):
input_mapping = input_mapping or self._input_mapping # 1. 用本次或已绑定的映射
required_fields = self._get_required_fields(input_mapping) # 2. 我需要哪些字段
remapped = _remap_and_validate_input( # 3. 按映射抽取并校验
eval_input, required_fields, input_mapping=input_mapping, ...)
# 4. 用 remapped 跑实际评估,产出 List[Score]
remap 与校验的实现转交 utils.remap_eval_input(evaluators.py:39 导入),并用 Pydantic 校验抽出来的字段。
bind:固定映射复用。 如果你要在一批同形状数据上反复用同一映射,可以 bind 一次,之后 evaluate 不必再传:
# src/.../evaluators.py:459-465
def bind(self, input_mapping):
self._input_mapping = input_mapping
def unbind(self):
self._input_mapping = None
直觉: 评估器是个"插座"(规定了几个孔的形状),
input_mapping是"转接头"——你的数据无论原来什么插头,接上转接头就能插进去。bind就是把转接头焊死。
3.3 核心机制:ClassificationEvaluator —— 一次结构化打分
这是最常用的评估器(evaluators.py:601)。给它一个 prompt 模板和一组 choices,它让 LLM 做选择题并要求结构化输出。
choices 有三种写法,表达力递增:
| choices 形式 | 例子 | 产出的 Score |
|---|---|---|
List[str] | ["positive","negative"] | 只有 label,score 为 None |
Dict[str, number] | {"good": 1, "bad": 0} | label + 对应 score |
Dict[str, (number, desc)] | {"good": (1, "...")} | label+score,desc 进 prompt 帮 LLM 理解(文档注明不太可靠) |
(三种形式的语义见 evaluators.py:615-623 的 docstring。)
为什么要结构化输出: 评估器要的是"label + 可选 explanation",而不是一段自由文本。ClassificationEvaluator 依赖 LLM 的 tool calling / structured output 能力(docstring 在 evaluators.py:606 明确要求),把 LLM 回答约束成一个 schema,这样 label 一定是 choices 之一、explanation 单独成字段——可靠可解析。
便捷工厂 create_classifier(evaluators.py:1133)是它的薄包装,就是把上面几个参数转构造函数,让常见用法一行搞定。
3.4 巧妙之处:评估器自带 OTel 追踪
README 强调"Evaluators are natively instrumented via OpenTelemetry"。代码里,真正被 @trace 装饰的是内部包装方法 _traced_evaluate(同步,evaluators.py:383)和 _async_traced_evaluate(异步,evaluators.py:401);公开的 evaluate(evaluators.py:419)在校验/重映射之后调用 self._traced_evaluate,由它来真正产 span(@trace 来自 from .tracing import trace,evaluators.py:34)。配套一组帮助函数把评估器元数据、remapped 输入、输出 Score 序列化成 span 属性(evaluators.py:91-120)。
这是个漂亮的闭环: 用 Phoenix-evals 打分这件事本身,也会产生 span 发回 Phoenix——于是"打分过程"也变成可观测、可curation的数据。评估器既是 Phoenix 的消费者也是生产者。