Pydantic Evals 数据集序列化完全指南:YAML/JSON 持久化、Schema 生成与自定义 Evaluator
【免费下载链接】pydantic-aiHow Python does AI. Agents, realtime voice, image generation, embeddings. Every model, every interface, typed end to end.项目地址: https://gitcode.com/GitHub_Trending/py/pydantic-ai
导读
本文聚焦 Pydantic Evals 的Dataset序列化能力,讲解如何将测试用例数据集保存为 YAML 与 JSON 文件、如何在保存时自动生成 JSON Schema 以驱动 IDE 的自动补全与校验,以及如何处理自定义 Evaluator 的序列化与反序列化。读完本文,你将掌握to_file()/from_file()的完整参数体系、三种 Evaluator 简写形式、Schema 路径模板,并能独立排查"自定义 Evaluator 无法加载""格式推断失败"等常见问题。
概述:两种格式,一套机制
Pydantic Evals 支持将数据集序列化到文件,共两种格式:
- YAML(
.yaml、.yml)—— 人类可读、注释友好、git diff 干净,适合绝大多数场景; - JSON(
.json)—— 结构化、机器可读,适合程序化生成与严格结构约束。
两种格式共同具备以下能力:
- 自动生成 JSON Schema,供 IDE 自动补全与校验(
yaml-language-server与$schema机制); - 自定义 Evaluator 的序列化 / 反序列化(通过
custom_evaluator_types注册); - 带泛型参数的类型安全加载(
Dataset[InputsT, OutputT, MetadataT].from_file(...))。
从源码看,序列化的核心实现在 pydantic_evals/pydantic_evals/dataset.py:
DEFAULT_SCHEMA_PATH_TEMPLATE = './{stem}_schema.json'定义了默认 Schema 文件命名模板({stem}会被替换为数据集文件名主干);_YAML_SCHEMA_LINE_PREFIX = '# yaml-language-server: $schema='是 YAML 文件首行注释的前缀;- 内部通过
_CaseModel/_DatasetModel两个extra='forbid'的 Pydantic 模型完成严格的序列化校验,且_DatasetModel显式声明了$schema字段别名(alias='$schema'),避免加载时因多余的$schema键触发校验失败。
YAML 格式:从保存到加载
基础示例
from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import EqualsExpected, IsInstance # Create a dataset with typed parameters dataset = Datasetstr, str, Any, ], evaluators=[ IsInstance(type_name='str'), EqualsExpected(), ], ) # Save to YAML dataset.to_file('my_tests.yaml')执行后会产生两个文件:
my_tests.yaml—— 数据集本体;my_tests_schema.json—— 供 IDE 使用的 JSON Schema(由默认模板./{stem}_schema.json推导,即{stem}=my_tests)。
YAML 输出内容
# yaml-language-server: $schema=my_tests_schema.json name: my_tests cases: - name: test_1 inputs: hello expected_output: HELLO evaluators: - IsInstance: str - EqualsExpected注意几个细节(均有源码依据):
- 首行的
# yaml-language-server: $schema=...注释由to_file()依据_YAML_SCHEMA_LINE_PREFIX自动写入; - Evaluator 默认被写成短形式:
to_file()在model_dump时传入context={'use_short_form': True}(见 dataset.py),NamedSpec序列化器据此输出IsInstance: str/EqualsExpected这种紧凑写法(见 pydantic_ai_slim/pydantic_ai/_spec.py); - YAML 写出使用
yaml.dump(dumped_data, sort_keys=False, allow_unicode=True),因此不会打乱字段顺序,且保留 Unicode 原样——仓库测试 tests/evals/test_dataset.py 专门验证了Привет这类非 ASCII 文本在往返后不丢失。
供 IDE 使用的 JSON Schema
YAML 首行的yaml-language-server指令让编辑器将my_tests_schema.json作为该文件的 Schema,从而获得:
- ✅自动补全(VS Code、PyCharm 等编辑器)
- ✅编辑过程中的内联校验
- ✅字段文档提示(tooltips)
- ✅非法数据的高亮报错
支持该指令的编辑器包括:VS Code(需安装 YAML 扩展)、JetBrains 系列 IDE(PyCharm、IntelliJ 等),以及大多数支持 YAML Language Server 的编辑器。若希望获得完整补全体验,需确保编辑器已启用 YAML Language Server 的内联 Schema 支持。
从 YAML 加载
from pathlib import Path from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import EqualsExpected, IsInstance # First create and save the dataset Path('my_tests.yaml').parent.mkdir(exist_ok=True) dataset = Datasetstr, str, Any], evaluators=[IsInstance(type_name='str'), EqualsExpected()], ) dataset.to_file('my_tests.yaml') # Load the dataset with type parameters dataset = Dataset[str, str, Any].from_file('my_tests.yaml') def my_task(text: str) -> str: return text.upper() # Run evaluation report = dataset.evaluate_sync(my_task)加载链路为from_file()→_infer_fmt()推断格式 → 读取文本 →from_text()/from_dict()→_DatasetModel.model_validate()校验 → 从注册表还原 Evaluator 实例(见 dataset.py)。
值得注意的加载行为:
- 若 YAML 中没有
name字段,from_file()会自动使用**文件名主干(stem)**作为数据集名称(通过default_name=path.stem传入),这一点由 test_deserializing_without_name 验证; - 泛型参数
Dataset[str, str, Any]决定了inputs、expected_output等字段的校验类型,加载是类型安全的。
JSON 格式:程序化生成的首选
JSON 格式适合程序化生成数据集或需要严格结构的场景。
基础示例
from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import EqualsExpected dataset = Datasetstr, str, Any, ], evaluators=[EqualsExpected()], ) # Save to JSON dataset.to_file('my_tests.json')JSON 输出内容
{ "$schema": "my_tests_schema.json", "name": "my_tests", "cases": [ { "name": "test_1", "inputs": "hello", "expected_output": "HELLO" } ], "evaluators": [ "EqualsExpected" ] }顶层的$schema键由Dataset的@model_serializer(mode='wrap')钩子注入:序列化上下文携带$schema时,输出结果会被前置合并(见 dataset.py)。它和 YAML 首行注释一样,为 IDE 提供 Schema 关联;同时_DatasetModel.json_schema_path字段别名保证了加载时该键能被正确解析而不报错。
从 JSON 加载
from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import EqualsExpected # First create and save the dataset dataset = Datasetstr, str, Any], evaluators=[EqualsExpected()], ) dataset.to_file('my_tests.json') # Load from JSON dataset = Dataset[str, str, Any].from_file('my_tests.json')JSON 加载走model_validate_json()路径,同样受泛型参数的类型约束。仓库测试 test_serialization_to_json 验证了 JSON 往返后$schema指向的 Schema 文件确实存在于同一目录。
Schema 生成:自动、定制与手动
自动创建
默认情况下,to_file()会在数据集文件旁生成 JSON Schema 文件:
from typing import Any from pydantic_evals import Case, Dataset dataset = Datasetstr, str, Any]) # Creates both my_tests.yaml AND my_tests_schema.json dataset.to_file('my_tests.yaml')默认模板常量DEFAULT_SCHEMA_PATH_TEMPLATE = './{stem}_schema.json'决定了文件名为<数据集文件名主干>_schema.json。若 Schema 文件内容与上次一致,_save_schema()会跳过重复写入(见 dataset.py)。
自定义 Schema 位置
from pathlib import Path from typing import Any from pydantic_evals import Case, Dataset dataset = Datasetstr, str, Any]) # Create directories Path('data').mkdir(exist_ok=True) # Custom schema filename (relative to dataset file location) dataset.to_file( 'data/my_tests.yaml', schema_path='my_schema.json', ) # No schema file dataset.to_file('my_tests.yaml', schema_path=None)关键行为(源码依据见 dataset.py):
schema_path相对路径是相对于数据集文件所在目录解析的:data/my_tests.yaml+schema_path='my_schema.json'会把 Schema 写到data/my_schema.json;- 若
schema_path传绝对路径且位于数据集文件目录内,写入时会用_get_relative_path_reference()递归计算相对引用,保证 YAML 首行/JSON$schema中记录的是可移植的相对路径(见 dataset.py); - 传入
schema_path=None则完全不生成 Schema 文件,首行注释也不会写入。
Schema 路径模板
{stem}占位符会被替换为数据集文件名主干:
from typing import Any from pydantic_evals import Case, Dataset dataset = Datasetstr, str, Any]) # Creates: my_tests.yaml and my_tests.schema.json dataset.to_file( 'my_tests.yaml', schema_path='{stem}.schema.json', )模板替换实现于to_file()内部:Path(schema_path.format(stem=path.stem))。
手动生成 Schema
不保存数据集、直接拿到 Schema 字典:
import json from typing import Any from pydantic_evals import Dataset # Get schema as dictionary for a specific dataset type schema = Dataset[str, str, Any].model_json_schema_with_evaluators() # Save manually with open('custom_schema.json', 'w', encoding='utf-8') as f: json.dump(schema, f, indent=2)model_json_schema_with_evaluators()(见 dataset.py)的工作方式:
- 依据
_params()解析Dataset[...]的三个泛型参数,据此构造临时的Case/DatasetPydantic 模型; - 将默认 Evaluator(
DEFAULT_EVALUATORS)与传入的自定义 Evaluator 类合并为 Union 类型写入evaluators字段,使 Schema 能精确描述每一种 Evaluator 的短形式/长形式; - 额外注册
$schema: {type: string}属性,与序列化输出的$schema键对齐。
默认注册的 Evaluator 清单见 pydantic_evals/pydantic_evals/evaluators/common.py:Equals、EqualsExpected、Contains、IsInstance、MaxDuration、LLMJudge、HasMatchingSpan、ToolCorrectness、TrajectoryMatch、ArgumentCorrectness、MaxToolCalls、MaxModelRequests、GEval。
自定义 Evaluator:序列化与反序列化
自定义 Evaluator 需要在序列化 / 反序列化时做特殊处理。
三项硬性要求
- 必须用
@dataclass装饰; - 必须继承
Evaluator(或其报告级变体ReportEvaluator); - 必须同时传给
to_file()和from_file()。
源码侧,注册表构建函数_get_evaluator_registry()会逐条校验(见 dataset.py):
- 不是
Evaluator子类会报ValueError; __dict__中缺少__dataclass_fields__(即未用@dataclass)会报ValueError: All custom evaluator classes must be decorated with @dataclass, but ... is not。
完整示例
from dataclasses import dataclass from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import Evaluator, EvaluatorContext @dataclass class CustomThreshold(Evaluator): """Check if output length exceeds a threshold.""" min_length: int max_length: int = 100 def evaluate(self, ctx: EvaluatorContext) -> bool: length = len(str(ctx.output)) return self.min_length <= length <= self.max_length # Create dataset with custom evaluator dataset = Datasetstr, str, Any, ], ), ], ) # Save with custom evaluator types dataset.to_file( 'dataset.yaml', custom_evaluator_types=[CustomThreshold], )保存后的 YAML
# yaml-language-server: $schema=dataset_schema.json cases: - name: test_length inputs: example expected_output: long result evaluators: - CustomThreshold: min_length: 5 max_length: 20序列化细节(依据 pydantic_evals/pydantic_evals/evaluators/_base.py):
BaseEvaluator.as_spec()基于build_serialization_arguments()收集 dataclass 字段值,默认值字段会被剔除(如max_length: int = 100若等于默认值就不会写入),因此CustomThreshold(min_length=5, max_length=20)的两个字段都会保留,而只填min_length时输出会更紧凑;- 若唯一非默认字段恰好是第一个 dataclass 字段,则走单参数紧凑形式;否则退化为键值对形式;
get_serialization_name()默认返回类名,CustomThreshold因而成为 YAML 中的键名。
加载自定义 Evaluator
from dataclasses import dataclass from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import Evaluator, EvaluatorContext @dataclass class CustomThreshold(Evaluator): """Check if output length exceeds a threshold.""" min_length: int max_length: int = 100 def evaluate(self, ctx: EvaluatorContext) -> bool: length = len(str(ctx.output)) return self.min_length <= length <= self.max_length # First create and save the dataset dataset = Datasetstr, str, Any], ), ], ) dataset.to_file('dataset.yaml', custom_evaluator_types=[CustomThreshold]) # Load with custom evaluator registry dataset = Dataset[str, str, Any].from_file( 'dataset.yaml', custom_evaluator_types=[CustomThreshold], )!!! warning "重要"必须将custom_evaluator_types同时传给to_file()和from_file():
- `to_file()`:把该 Evaluator 类型纳入生成的 JSON Schema(否则 IDE 无法补全其参数); - `from_file()`:把该类型注册进反序列化注册表(否则加载时抛 "Unknown evaluator name" 错误)。底层还原逻辑:_from_dataset_model()用build_registry()将"自定义类型 + 默认类型"合并为名称→类的注册表,再对每个EvaluatorSpec调用load_from_registry(),等价于cls(*args, **kwargs)实例化(见 pydantic_ai_slim/pydantic_ai/_spec.py)。若某个 Evaluator 解析失败,错误会收集为ExceptionGroup抛出(最多展示前 3 条),异常信息中包含当前注册表全部合法名称,便于快速定位拼写问题。
Evaluator 序列化的三种形式
EvaluatorSpec(即共享模块pydantic_ai._spec.NamedSpec,见 pydantic_evals/pydantic_evals/evaluators/spec.py)支持三种书写形式,分别对应无参、单参、多参:
1. 仅名称(无参数)
evaluators: - EqualsExpected - IsInstance: str # Using default parameter2. 单参数(短形式)
evaluators: - IsInstance: str - Contains: "required text" - MaxDuration: 2.03. 多参数(字典形式)
evaluators: - CustomThreshold: min_length: 5 max_length: 20 - LLMJudge: rubric: "Response is accurate" model: "openai:gpt-5" include_input: true这三种形式的解析逻辑由_SerializedNamedSpec实现(见 pydantic_ai_slim/pydantic_ai/_spec.py):字符串 → 无参数;{Name: value}且 value 为字典 → 关键字参数;{Name: value}且 value 非字典 → 单个位置参数。需要说明的是:LLMJudge的model字段在序列化时会通过_serialize_model_as_string()将Model实例改写为model_id字符串,从而保证规范可往返(见 pydantic_evals/pydantic_evals/evaluators/common.py);序列化端还会做反向保护——若单个位置参数本身是"全字符串键字典",会自动退化为长形式,避免反序列化时被误判为 kwargs(见 pydantic_ai_slim/pydantic_ai/_spec.py)。
格式对比与选型建议
| Feature | YAML | JSON |
|---|---|---|
| 人类可读 | ✅ 优秀 | ⚠️ 良好 |
| 注释 | ✅ 支持 | ❌ 不支持 |
| 紧凑 | ✅ 紧凑 | ⚠️ 冗长 |
| 机器解析 | ✅ 良好 | ✅ 优秀 |
| IDE 支持 | ✅ 支持 | ✅ 支持 |
| 版本控制 | ✅ 干净 diff | ⚠️ 嘈杂 diff |
建议:绝大多数场景使用 YAML(可读、可注释、diff 友好);程序化生成数据集或对结构有严格约束时使用 JSON。
仓库中的真实案例可佐证这一选型:examples/pydantic_ai_examples/evals/example_01_generate_dataset.py用 LLM 生成数据集后以fmt='yaml'写入datasets/time_range_v1.yaml,其产物(见 examples/pydantic_ai_examples/evals/datasets/time_range_v1.yaml)展示了数据集级LLMJudge与用例级IsInstance: TimeRangeBuilderSuccess短形式混合书写的实际效果,并配套生成time_range_v1_schema.json。
进阶:自定义序列化名称
当 Evaluator 类名很长时,可通过get_serialization_name()控制其在序列化文件中的名称:
from dataclasses import dataclass from pydantic_evals.evaluators import Evaluator, EvaluatorContext @dataclass class VeryLongDescriptiveEvaluatorName(Evaluator): @classmethod def get_serialization_name(cls) -> str: return 'ShortName' def evaluate(self, ctx: EvaluatorContext) -> bool: return True在 YAML 中:
evaluators: - ShortName # Instead of VeryLongDescriptiveEvaluatorName源码层面,BaseEvaluator.get_serialization<|begin▁of▁sentence|>#默认返回cls.__name__,子类覆写后即可改变注册表键名与文件中的写法;同时build_registry()支持类通过返回None主动退出序列化(此时会抛错提示)。
故障排查
1. IDE 中 Schema 未生效(无自动补全)
现象:YAML 文件没有自动补全。
排查步骤:
- 检查 YAML 首行 Schema 路径是否正确:
# yaml-language-server: $schema=correct_schema_name.json - 确认同名 Schema 文件存在于同一目录(相对路径是相对于数据集文件目录解析的);
- 重启 IDE 的 YAML Language Server;
- 安装 YAML 扩展(VS Code 使用 Red Hat 出品的 "YAML" 扩展)。
2. 自定义 Evaluator 无法加载
现象:ValueError: Unknown evaluator name: 'CustomEvaluator'
原因:反序列化注册表里没有这个名称。实际的报错信息是Evaluator 'xxx' is not in the provided custom_evaluator_types. Valid choices: [...](可在 tests/evals/test_dataset.py 的test_from_text_failure中看到完整错误样例)。
解决:加载时传入custom_evaluator_types:
from dataclasses import dataclass from typing import Any from pydantic_evals import Case, Dataset from pydantic_evals.evaluators import Evaluator, EvaluatorContext @dataclass class CustomEvaluator(Evaluator): def evaluate(self, ctx: EvaluatorContext) -> bool: return True # First create and save with custom evaluator dataset = Datasetstr, str, Any])], ) dataset.to_file('tests.yaml', custom_evaluator_types=[CustomEvaluator]) # Load with custom evaluator types dataset = Dataset[str, str, Any].from_file( 'tests.yaml', custom_evaluator_types=[CustomEvaluator], # Required! )3. 格式推断失败
现象:ValueError: Cannot infer format from extension
原因:_infer_fmt()只认.yaml/.yml/.json后缀(见 dataset.py),其它扩展名无法自动判断,且扩展名判断不区分大小写。
解决:显式指定fmt:
from typing import Any from pydantic_evals import Case, Dataset dataset = Datasetstr, str, Any]) # Explicit format for unusual extensions dataset.to_file('data.txt', fmt='yaml') dataset_loaded = Dataset[str, str, Any].from_file('data.txt', fmt='yaml')4. Schema 生成失败
现象:自定义 Evaluator 导致model_json_schema_with_evaluators()报错。
原因:Schema 生成依赖build_schema_types()通过inspect.signature读取构造参数(见 pydantic_ai_slim/pydantic_ai/_spec.py),普通类没有 dataclass 元信息。
解决:确保 Evaluator 是规范的 dataclass:
from dataclasses import dataclass from pydantic_evals.evaluators import Evaluator, EvaluatorContext # ✅ Correct @dataclass class MyEvaluator(Evaluator): value: int def evaluate(self, ctx: EvaluatorContext) -> bool: return True # ❌ Wrong: Missing @dataclass class BadEvaluator(Evaluator): def __init__(self, value: int): self.value = value def evaluate(self, ctx: EvaluatorContext) -> bool: return True下一步
- Dataset Management —— 数据集的创建与组织
- Custom Evaluators —— 编写自定义评估逻辑
- Core Concepts —— 理解底层数据模型
【免费下载链接】pydantic-aiHow Python does AI. Agents, realtime voice, image generation, embeddings. Every model, every interface, typed end to end.项目地址: https://gitcode.com/GitHub_Trending/py/pydantic-ai
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考