# 第 4 步:构建数据集 **为什么需要这一步**:数据集将一切串联起来——可运行组件(第 2 步)、评估器(第 3 步)和使用场景(第 1c 步)——整合为具体的测试场景。在测试时,`pixie test` 使用 `input_data` 调用可运行组件,wrap 注册表中填充了 `eval_input`,评估器对捕获到的输出进行评分。 **在构建条目之前**,请回顾: - **`pixie_qa/00-project-analysis.md`**——能力清单与故障模式。数据集的条目应覆盖能力清单中的条目,并包含针对所列故障模式的条目。 - **`pixie_qa/02-eval-criteria.md`**——使用场景及其能力覆盖范围。确保每个列出的使用场景都有代表性的条目。 --- ## 理解 `input_data`、`eval_input` 和 `expectation` 在构建数据集之前,先理解这些术语的含义: - **`input_data`** = 作为 Pydantic 模型传递给 `Runnable.run()` 的 kwargs。这些是输入数据(用户消息、请求体、CLI 参数)。键名必须与为 `run(args: T)` 定义的 Pydantic 模型的字段匹配。 - **`eval_input`** = 一个 `{"name": ..., "value": ...}` 对象的列表,对应应用中 `wrap(purpose="input")` 的调用。在测试时,这些值由 wrap 注册表自动注入;应用中的 `wrap(purpose="input")` 调用会返回注册表中的值,而不是调用真实的外部依赖。 仅当应用中没有 `wrap(purpose="input")` 调用时,`eval_input` **才可以是空列表**。**如果应用存在 input wraps,则每个数据集条目都必须提供对应的 `eval_input` 值以及预捕获的内容**——否则应用会在评估期间进行实时外部调用,这会导致速度慢、不稳定且不可复现。请参见第 4b′ 节了解如何捕获此内容。 每个项都是一个 `NamedData` 对象,包含 `name`(字符串)和 `value`(任何可 JSON 序列化的值)。 - **`expectation`**(可选)= 针对特定场景的评估参考。正确输出在该场景下应呈现的样子。供需要与参考值进行比较的评估器使用(例如 `Factuality`、`ClosedQA`)。对于不需要参考值的输出质量评估器,则无需此字段。 - **评估输出** = 应用实际产生的内容,在运行时由 `wrap(purpose="output")` 和 `wrap(purpose="state")` 调用捕获。**不存储在数据集中**——它在 `pixie test` 运行应用时产生。 位于 `pixie_qa/reference-trace.jsonl` 的 **参考追踪** 是了解数据形状的主要来源: - 过滤它以查看 `eval_input` 值的精确序列化格式 - 读取 `kwargs` 记录以了解 `input_data` 结构 - 读取 `purpose="output"/"state"` 事件以了解应用产生的输出内容,从而编写有意义的 `expectation` 值 --- ## 4a. 推导评估器分配 评估标准工件(`pixie_qa/02-eval-criteria.md`)将每个标准映射到使用场景。评估器映射工件(`pixie_qa/03-evaluator-mapping.md`)将每个标准映射到具体的评估器名称。将两者结合: 1. **数据集级别的默认评估器**:标记为适用于"所有"使用场景的标准 → 其评估器名称放入顶层 `"evaluators"` 数组。 2. **条目级别的评估器**:仅适用于子集的标准 → 其评估器名称仅放在相关行的 `"evaluators"` 中,使用 `"..."` 来同时包含默认评估器。 ## 4b. 使用 `pixie format` 检查数据形状 在参考追踪上使用 `pixie format` 来查看精确的数据形状 **以及** 以数据集条目格式呈现的真实应用输出: ```bash uv run pixie format --input reference-trace.jsonl --output dataset-sample.json ``` 输出看起来像这样: ```json { "input_data": { "user_message": "你们营业时间是几点?" }, "eval_input": [ { "name": "customer_profile", "value": { "name": "Alice", "tier": "gold" } }, { "name": "conversation_history", "value": [{ "role": "user", "content": "你们几点开门?" }] } ], "expectation": null, "eval_output": { "response": "我们的营业时间是周一至周五,上午 9 点到下午 5 点..." } } ``` **重要**:此模板中的 `eval_output` 是运行中应用产生的 **完整真实输出**。请勿将 `eval_output` 复制到数据集条目中——这会让评估器拿到真实答案,导致测试轻易通过。应改为: - 使用 `input_data` 和 `eval_input` 作为数据键和格式的精确模板 - 查看 `eval_output` 以了解应用产生什么——然后为每个场景编写一个 **简洁的 `expectation` 描述**,捕捉关键质量标准 **示例**:如果 `eval_output.response` 是 `"我们的营业时间是周一至周五上午 9 点到下午 5 点,周六上午 10 点到下午 2 点。"`,则将 `expectation` 写为 `"应提及工作日时间(周一至周五 9am–5pm)和周六时间"`——一段简短描述,供人工或 LLM 评估器进行比较。 ## 4b′. 为 `eval_input` 捕获外部内容(必须执行) **关键**:如果应用有任何 `wrap(purpose="input")` 调用,则每个数据集条目都必须提供对应的 `eval_input` 值,且使用 **预先捕获的真实内容**。空的 `eval_input` 列表意味着应用将在每次评估运行时进行实时外部调用(HTTP 请求、数据库查询、API 调用)——这会使评估变慢、不稳定且不可复现。 ### 为什么这很重要 在 `pixie test` 期间,应用中的每个 `wrap(purpose="input", name="X")` 调用都会在 wrap 注册表中查找名为 `"X"` 的值: - **如果找到**:直接返回注册值(不进行外部调用) - **如果未找到**:执行真实的外部调用(非确定性、速度慢、可能失败) `eval_input: []` 的条目意味着注册表中没有任何内容,因此每个外部依赖都会实时运行。这违背了打桩的目的。 ### 如何捕获内容 对于应用中的每个 `wrap(purpose="input", name="X")`,您必须捕获一次真实数据并将其嵌入数据集中。选择以下方法之一: **方法 A——使用参考追踪**(推荐): 第 2c 步的参考追踪已包含每个 `purpose="input"` wrap 的捕获值。提取它们: ```bash # 查看参考追踪以找到 input wrap 的值 grep '"purpose": "input"' pixie_qa/reference-trace.jsonl ``` 或者使用 `pixie format` 以数据集条目格式查看数据——输出中的 `eval_input` 数组已包含捕获的值,名称和格式均正确。 **方法 B——直接获取内容**(适用于使用不同输入的新条目): 当创建使用不同输入源(例如不同的 URL、不同的查询)的数据集条目时,通过运行一次依赖代码来捕获内容: ```python # 示例:对于网页爬虫,运行一次应用自身的抓取逻辑 from myapp.fetcher import fetch_page page_content = fetch_page(target_url) # 使用应用的真实代码路径 ``` 然后将捕获的内容包含在条目的 `eval_input` 中: ```json { "eval_input": [ { "name": "fetch_result", "value": "<此处为捕获的页面内容>" } ] } ``` **方法 C——为每个输入运行 `pixie trace`**(最全面): 对于每组 `input_data`,运行 `pixie trace` 来使用真实依赖执行应用并捕获所有值: ```bash pixie trace --runnable pixie_qa/run_app.py:AppRunnable --input trace-input.json ``` 然后从生成的追踪中提取 `purpose="input"` 的值,并将其用作 `eval_input`。 ### 内容格式 `eval_input` 的值必须与 `wrap()` 调用返回的 **精确类型和格式** 匹配。查看参考追踪以了解应用产生的格式: - 如果 wrap 捕获的是字符串(例如 HTML 内容、markdown 文本),则值为字符串 - 如果 wrap 捕获的是字典(例如数据库记录),则值为 JSON 对象 - 如果 wrap 捕获的是列表,则值为 JSON 数组 **请勿跳过此步骤。** 应用中的每个 `wrap(purpose="input")` 都必须在每个数据集行中有对应的 `eval_input` 条目。如果您在应用存在 input wraps 的情况下使用空的 `eval_input` 继续,评估将不可靠。 ## 4c. 生成数据集项 根据参考追踪和使用场景创建多样化的条目: - **`input_data` 键** 必须与 `Runnable.run(args: T)` 中使用的 Pydantic 模型的字段匹配 - **`eval_input`** 必须是一个 `{"name": ..., "value": ...}` 对象的列表,与应用中 `wrap(purpose="input")` 调用的 `name` 值匹配 - **覆盖每个使用场景** 来自 `pixie_qa/02-eval-criteria.md`——每个使用场景至少一个条目,且各条目之间的输入要有有意义的多样性 **如果用户在提示中指定了数据集或数据源**(例如一个包含研究问题或对话场景的 JSON 文件),请读取该文件,将每个条目适配到 `input_data` / `eval_input` 的形状中,并将其纳入数据集。不要忽略指定的数据。 ### 条目质量检查清单 在最终确定数据集之前,根据以下标准验证每个条目: **输入的真实性**: - `eval_input` 是否包含尊重综合化边界的世界数据(参见第 2c 步)?用户编写的参数没问题;世界数据应来自真实来源,而不是从头编造。 - `eval_input` 中的世界数据的规模和复杂度是否与 `00-project-analysis.md` 中"真实输入特征"描述的一致?如果分析指出输入通常为 5KB–500KB,那么 200 字符的输入就不真实。 - 从输入中提取答案是否非平凡?如果答案位于一个清晰标记的 HTML 标签中或是第一句话,这样的测试并不能检验提取质量。 **场景多样性**: - 条目是否涵盖了有意义的难度差异——而不仅仅是不同主题但难度相同? - 是否至少有一个条目针对 `00-project-analysis.md` 中您预期可能实际导致分数下降(而非必然通过)的故障模式? - 条目是否使用了输入数据中不同的结构模式(而不仅仅是把不同内容填入同一模板)? **难度校准**: - 是否至少有一个条目您真的不确定应用是否能正确处理?如果您确信每个条目都能轻松通过,说明数据集太简单了。 - 考虑包含一个刻意具有挑战性的条目,用于探测已知限制——即"压力测试"条目。如果通过了,很好。如果失败了,则说明评估已证明它能够捕捉真实问题。 ### 数据集条目的反模式 - **编造世界数据**:手动编写应用通常从外部来源获取的内容(例如为网页爬虫编写 HTML,为 RAG 系统编写"检索到的文档")。这消除了现实世界的复杂性。 - **难度单一**:所有条目具有相同的复杂度水平。真实工作负载是有分布的——有些简单,有些困难,有些是边缘情况。 - **答案过于明显**:每个条目中目标信息都清晰标记且不含歧义。真实数据中,答案往往是分散的、部分存在的、有重复变体的,或者隐藏在噪声中。 - **自产自销**:您同时编写了输入和预期输出,因此您确切知道里面有什么。真正的评估器测试的是应用能否找到它之前未见过的信息。 - **只有快乐路径**:没有条目测试错误条件、边缘情况或已知的故障模式。 - **所有条目都来自同一个玩具追踪,仅做轻微改写**:如果所有条目的 `input_data` 和 `eval_input` 数据都相似,那么数据集无法测试任何有意义的内容。每个条目应代表一个有意义的不同的场景。 - **重复使用项目自身的测试夹具作为评估数据**:项目中的 `tests/`、`fixtures/`、`examples/` 和 `mock_server/` 目录包含为单元/集成测试设计的数据——小巧、干净、确定且过于简单。将它们用作 `eval_input` 数据可以保证 100% 通过率和零质量信号。即使这些夹具看起来很方便,它们绕过了应用工作所面临的所有真实世界困难。**应改为运行生产代码来捕获真实数据**,或生成与 `00-project-analysis.md` 中规模/复杂度相匹配的合成数据。 - **使用项目的 mock/伪造实现**:如果项目在其测试基础设施中包含 mock LLM、伪造 HTTP 服务器或桩服务,请勿在评估管道中使用它们。您的评估必须使用具有真实复杂度的数据来运行应用的真实代码路径——而不是使用项目自身的测试快捷方式。 ## 4c′. 对照项目分析验证覆盖范围 在编写最终的数据集 JSON 之前,打开 `pixie_qa/00-project-analysis.md` 并检查: 1. **真实输入特征**:对于列出的每个特征(大小、复杂度、噪声、多样性),确认至少有一个数据集条目反映了该特征。如果分析指出"包含导航和广告的混乱输入",则至少一个条目的 `eval_input` 应包含带有导航和广告的混乱数据。 2. **故障模式**:对于列出的每个故障模式,确认至少有一个数据集条目旨在测试该模式。该条目不需要保证失败——但它应创造条件,使该故障模式 _可能_ 出现。如果某个故障模式在当前打桩设置下无法被测试,请在 `02-eval-criteria.md` 中添加注释说明原因。 3. **能力覆盖范围**:确认数据集覆盖了评估标准(第 1c 步)中列出的能力。每个被覆盖的能力应至少有一个条目。 如果发现任何缺口,请在进入第 4d 步之前添加条目来弥补。 ## 4c″. 停止检查——数据集真实性审计(硬性关卡) **这是一个硬性关卡。** 在每项检查通过之前,请勿进入第 4d 步。如果任何检查失败,请修改数据集并重新审计。 在编写最终的数据集 JSON 之前,执行此自我审计: 1. **交叉参考 `00-project-analysis.md`**:打开"真实输入特征"部分。对于每个特征(大小、复杂度、噪声、结构),验证至少有一个数据集条目的 `eval_input` 反映了该特征。如果分析指出"5KB–500KB 的 HTML 页面,包含导航框架和广告",而您最大的 `eval_input` 只有 1KB 的干净 HTML,**则数据集不真实——请添加更难的条目。** 2. **统计不同来源数量**:数据集中有多少个唯一的 `eval_input` 数据源?如果超过 50% 的条目共享相同的 `eval_input` 内容(即使提示不同),则数据集缺乏多样性。同一输入上的不同提示测试的是 LLM 的解读能力,而非应用的数据处理能力。 3. **难度分布(强制性阈值)**:将每个条目标记为"常规"(确信能通过)、"中等"(可能通过但非平凡)或"有挑战性"(确实不确定或针对已知故障模式)。 - **最多 60% 的条目为"常规"。** 如果有 5 个条目,最多只能有 3 个是常规。 - **至少有一个"有挑战性"条目**,针对 `00-project-analysis.md` 中的一个故障模式,且您确实不确定其结果。如果每个条目都保证通过,则该数据集无法区分一个优秀的应用和一个有问题的应用。 4. **能力覆盖范围(强制性阈值)**:统计 `00-project-analysis.md` 中列出的能力中,有多少个被至少一个数据集条目所测试。 - **必须覆盖 ≥50% 的列出的能力。** 如果分析列出了 6 项能力,则数据集必须测试至少 3 项。 - 如果覆盖率低于阈值,请添加针对未覆盖能力的条目。 5. **项目夹具污染检查**:扫描每个 `eval_input` 值。是否有任何数据来自项目的 `tests/`、`fixtures/`、`examples/` 或 mock 服务器目录?如果是,**请用真实数据替换。** 这些夹具是为开发便利性设计的,而非评估的真实性。 6. **同义反复检查**:测试管道是否会产生有意义的分数,还是它是一个封闭循环?如果您同时编写了输入数据和评估器逻辑,使得通过是必然的(例如正则表达式提取器 + 在手动编写的 HTML 上进行精确匹配评估),**则该管道是同义反复的**,无法捕捉真实问题。应用的真实 LLM 应产生输出,而评估器应评估能够真正失败的质量维度。 7. **`eval_input` 完整性检查**:对于打桩应用代码中的每个 `wrap(purpose="input", name="X")` 调用,验证每个数据集条目都提供了对应的 `eval_input` 项,包含 `"name": "X"` 和一个非空的 `"value"`。如果任何条目的 `eval_input` 为 `[]` 而应用存在 input wraps,**则数据集不完整——缺少捕获的内容。** 请返回第 4b′ 步并捕获内容。 ## 4d. 构建数据集 JSON 文件 在 `pixie_qa/datasets/.json` 创建数据集: ```json { "name": "qa-golden-set", "runnable": "pixie_qa/run_app.py:AppRunnable", "evaluators": ["Factuality", "pixie_qa/evaluators.py:ConciseVoiceStyle"], "entries": [ { "input_data": { "user_message": "你们营业时间是几点?" }, "description": "金牌账户客户询问营业时间", "eval_input": [ { "name": "customer_profile", "value": { "name": "Alice Johnson", "tier": "gold" } } ], "expectation": "应提及周一至周五 9am–5pm 和周六 10am–2pm" }, { "input_data": { "user_message": "我想改点东西" }, "description": "基础账户客户的模糊更改请求", "eval_input": [ { "name": "customer_profile", "value": { "name": "Bob Smith", "tier": "basic" } } ], "expectation": "应要求澄清", "evaluators": ["...", "ClosedQA"] }, { "input_data": { "user_message": "我想结束这通电话" }, "description": "用户在验证失败后请求结束通话", "eval_input": [ { "name": "customer_profile", "value": { "name": "Charlie Brown", "tier": "basic" } } ], "expectation": "代理应调用 endCall 工具并结束对话", "eval_metadata": { "expected_tool": "endCall", "expected_call_ended": true }, "evaluators": ["...", "pixie_qa/evaluators.py:tool_call_check"] } ] } ``` ### 关键字段 **条目结构**——所有字段均为每个条目的顶层字段(扁平结构——无嵌套): ``` entry: ├── input_data (必需)——Runnable.run() 的参数 ├── eval_input (可选)——{"name": ..., "value": ...} 对象列表(默认:[]) ├── description (必需)——测试用例的可读标签 ├── expectation (可选)——供基于比较的评估器使用的参考 ├── eval_metadata (可选)——供自定义评估器使用的额外逐条目数据 └── evaluators (可选)——此条目的评估器名称 ``` **顶层字段:** - **`runnable`**(必需):`filepath:ClassName` 格式的引用,指向第 2 步中的 `Runnable` 类(例如 `"pixie_qa/run_app.py:AppRunnable"`)。路径相对于项目根目录。 - **`evaluators`**(数据集级别,可选):应用于每个条目的默认评估器名称——适用于所有使用场景的标准所对应的评估器。 **逐条目字段(每个条目的所有字段均为顶层):** - **`input_data`**(必需):键名与 `Runnable.run(args: T)` 的 Pydantic 模型字段匹配。这些是应用的输入数据。 - **`eval_input`**(可选,默认 `[]`):`{"name": ..., "value": ...}` 对象的列表。名称与应用中 `wrap(purpose="input")` 的名称匹配。运行器在构建 `Evaluable` 时会自动预置 `input_data`。 - **`description`**(必需):来自 `pixie_qa/02-eval-criteria.md` 的使用场景一句话描述。 - **`expectation`**(可选):针对特定场景的期望文本,供需要参考的评估器使用。 - **`eval_metadata`**(可选):供自定义评估器使用的额外逐条目数据——例如期望的工具名称、布尔标志、阈值。在评估器中可通过 `evaluable.eval_metadata` 访问。 - **`evaluators`**(可选):行级别的评估器覆盖。 ### 评估器分配规则 1. 适用于所有项的评估器放入顶层的 `"evaluators"` 数组。 2. 需要 **额外** 评估器的项使用 `"evaluators": ["...", "ExtraEval"]`——`"..."` 展开为默认评估器。 3. 需要 **完全不同** 评估器集合的项使用 `"evaluators": ["OnlyThis"]`,不带 `"..."`。 4. 仅使用默认评估器的项:省略 `"evaluators"` 字段。 --- ## 数据集创建参考 ### 使用 `eval_input` 值 `eval_input` 值是 `{"name": ..., "value": ...}` 对象。使用参考追踪作为模板——从相关的 `purpose="input"` 事件中复制 `"data"` 字段并调整值: **简单字典**: ```json { "name": "customer_profile", "value": { "name": "Alice", "tier": "gold" } } ``` **字典列表**(例如对话历史): ```json { "name": "conversation_history", "value": [ { "role": "user", "content": "你好" }, { "role": "assistant", "content": "你好!" } ] } ``` **重要**:确切格式取决于 `wrap(purpose="input")` 调用捕获的内容。始终从参考追踪中复制,而不是从头构建。 ### 构建多样化的评估场景 覆盖每个使用场景的不同方面。请参考 **`pixie_qa/00-project-analysis.md`** 了解能力清单和故障模式: - **覆盖每项能力**——能力清单中每项能力至少一个条目,而不仅仅是主要能力 - **针对故障模式**——包含测试项目分析中列出的难点/故障模式的条目(例如格式错误的输入、边缘情况、复杂场景) - 同一请求的不同用户表述方式 - 边缘情况(歧义输入、缺失信息、错误条件) - 对特定评估标准进行压力测试的条目 - 第 1c 步中每个使用场景至少一个条目 --- ## 输出 `pixie_qa/datasets/.json`——数据集文件。