chore: import zh skill eval-driven-dev
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- Skill 名称:`eval-driven-dev`
|
||||
- 中文类目:搭建 agent 评测框架与持续门禁
|
||||
- 上游仓库:`github__awesome-copilot`
|
||||
- 上游路径:`skills/eval-driven-dev/SKILL.md`
|
||||
- 上游链接:https://github.com/github/awesome-copilot/blob/HEAD/skills/eval-driven-dev/SKILL.md
|
||||
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
|
||||
- 原作者、版权和许可证信息以上游仓库为准
|
||||
@@ -0,0 +1,210 @@
|
||||
```yaml
|
||||
---
|
||||
name: eval-driven-dev
|
||||
description: >
|
||||
使用评估驱动开发改进 AI 应用。定义评估标准、对应用进行插桩、构建黄金数据集、观察和评估应用运行情况、分析结果,并生成具体的改进行动计划。
|
||||
当用户要求为任何调用 LLM 模型的 Python 项目搭建 QA、添加测试、添加评估、做评测、做基准测试、修复错误行为、改进质量或做质量保障时,始终使用此技能。
|
||||
license: MIT
|
||||
compatibility: Python 3.10+
|
||||
metadata:
|
||||
version: 0.8.4
|
||||
pixie-qa-version: ">=0.8.4,<0.9.0"
|
||||
pixie-qa-source: https://github.com/yiouli/pixie-qa/
|
||||
---
|
||||
|
||||
# Python LLM 应用的评估驱动开发
|
||||
|
||||
你正在构建一个**自动化评估流水线**,用于端到端测试基于 Python 的 AI 应用——就像真实用户那样运行它,使用真实输入——然后通过评估器对输出进行评分,并通过 `pixie test` 产生通过/失败结果。
|
||||
|
||||
**你测试的是应用本身**——它的请求处理、上下文组装(如何收集数据、构建提示词、管理对话状态)、路由和响应格式化。应用使用了 LLM,这使得输出具有非确定性——这就是为什么你使用评估器(LLM 作为评判者、相似度评分)而不是 `assertEqual`——但被测对象是应用的代码,而不是 LLM。
|
||||
|
||||
在评估过程中,应用自身的代码会真实运行——路由、提示词组装、LLM 调用、响应格式化——没有任何内容被 mock 或 stub。但应用从外部来源(数据库、缓存、第三方 API、语音流)读取的数据,会通过插桩(instrumentations)替换为测试指定的值。这意味着每个测试用例精确控制应用看到的数据,同时仍然执行完整的应用代码路径。
|
||||
|
||||
**规则:应用的 LLM 调用必须使用真实的 LLM。** 不得用虚假实现替换、mock、stub 或拦截 LLM。LLM 是核心价值生成组件——替换它会使得评估变成同义反复(你同时控制输入和输出,因此评分毫无意义)。如果项目的测试套件中包含 LLM mock 模式,那是项目自身单元测试使用的——切勿将其用于评估 Runnable。
|
||||
|
||||
**交付物是一个能运行的 `pixie test` 并产生真实评分**——不是计划,不光是插桩,不光是数据集。
|
||||
|
||||
此技能是关于执行工作,而非描述工作。阅读代码、编辑文件、运行命令,产出可运行的流水线。
|
||||
|
||||
---
|
||||
|
||||
## 开始之前
|
||||
|
||||
**首先,激活虚拟环境**。找到项目正确的虚拟环境并激活它。虚拟环境激活后,运行技能资源中包含的 setup.sh 脚本。
|
||||
该脚本会将 `eval-driven-dev` 技能和 `pixie-qa` Python 包更新到最新版本,初始化 pixie 工作目录(如果尚未初始化),并在后台启动一个 Web 服务器以向用户展示更新。
|
||||
|
||||
**安装错误处理——哪些可以跳过,哪些必须成功:**
|
||||
|
||||
- **技能更新失败** → 可以继续。现有技能版本足以使用。
|
||||
- **pixie-qa 升级失败但已安装** → 可以继续使用现有版本。
|
||||
- **pixie-qa 未安装且安装失败** → **停止。** 请用户帮助。工作流无法在没有 `pixie` 包的情况下进行。
|
||||
- **`pixie init` 失败** → **停止。** 请用户帮助。
|
||||
- **`pixie start`(Web 服务器)失败** → **停止。** 请用户帮助。检查 pixie 根目录下的 `server.log` 以获取诊断信息。常见原因:端口冲突、缺少依赖、环境运行缓慢。**没有 Web 服务器就不能继续**——用户需要它来查看评估结果。
|
||||
|
||||
---
|
||||
|
||||
## 工作流程
|
||||
|
||||
按顺序执行步骤 1-6,中间不要停顿。不要在中间步骤询问用户确认——自行验证每个步骤并继续。
|
||||
|
||||
**如何工作——在执行任何操作之前先阅读以下内容:**
|
||||
|
||||
- **一次只做一个步骤。** 只阅读当前步骤的说明。在处理步骤 1 时,不要阅读步骤 2-6。
|
||||
- **仅在步骤要求时阅读参考资料。** 每个步骤会指定一个特定的参考文件。到达该步骤时再阅读——不要提前阅读。
|
||||
- **立即创建产物。** 为某个子步骤阅读代码后,立即写入该子步骤的输出文件,再继续下一步。不要在多个子步骤间积累理解之后再写任何东西。
|
||||
- **验证,然后继续。** 每个步骤都有一个检查点。验证通过后,再进入下一步。不要在验证当前步骤的同时规划后续步骤。
|
||||
|
||||
**何时停止并请求帮助:**
|
||||
|
||||
有些阻碍是无法也不应该绕过的。当你遇到以下任何情况时,**立即停止并请用户帮助**——不要尝试变通方案:
|
||||
|
||||
- **应用因缺少环境变量或配置而无法运行**:应用需要未设置且无法推断的环境变量或配置。**不要**通过 mock、伪造或替换应用组件来绕过——评估必须执行真实的生产代码。请用户修复环境设置。
|
||||
- **应用导入失败表明项目已损坏**:如果应用的核心模块因缺少系统依赖或不兼容的 Python 版本而无法导入(而不仅仅是你可以安装的 pip 包),请用户修复项目设置。
|
||||
- **入口点不明确**:如果应用有多个同样合理的入口点,且项目分析无法明确哪个最重要,请询问用户应该以哪个为目标。
|
||||
|
||||
你应该自行解决的阻碍(不要询问):缺少 Python 包(安装它们)、缺少 `pixie` 包(安装它)、端口冲突(选择不同端口)、文件权限问题(修复它们)。
|
||||
|
||||
**按顺序执行步骤 1-6。** 如果用户的提示明确表明前面的步骤已经完成(例如"运行现有测试"、"重新运行评估"),则跳到相应步骤。如有疑问,从步骤 1 开始。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 1:理解应用并定义评估标准
|
||||
|
||||
**首先,检查用户提示中的具体要求。** 在阅读应用代码之前,先审视用户的要求:
|
||||
|
||||
- **引用的文档或规格说明**:提示中是否提到要遵循的文件(例如"按照 EVAL_SPEC.md 中的规格说明"、"使用 REQUIREMENTS.md 中的方法")?如果是,**先阅读该文件**——它可能指定了数据集、评估维度、通过标准或方法论,这些会覆盖你的默认值。
|
||||
- **指定的数据集或数据来源**:提示是否引用了特定的数据文件(例如"使用 eval_inputs/research_questions.json 中的问题"、"使用 call_scenarios.json 中的场景")?如果是,**阅读这些文件**——你必须以它们为基础构建评估数据集,而不是编造泛化的替代品。
|
||||
- **指定的评估维度**:提示是否命名了要评估的具体质量方面(例如"评估事实准确性、完整性和偏见"、"测试身份验证和工具调用正确性")?如果是,**每个命名的维度都必须在你的测试文件中有一个对应的评估器**。
|
||||
|
||||
如果提示指定了上述任何内容,则它们具有优先权。在继续之前先阅读并整合它们。
|
||||
|
||||
步骤 1 有三个子步骤。每个子步骤阅读自己的参考文件并生成自己的输出文件。**在开始下一个子步骤之前,完整完成当前子步骤。**
|
||||
|
||||
#### 子步骤 1a:项目分析
|
||||
|
||||
> **参考文件**:立即阅读 `references/1-a-project-analysis.md`。
|
||||
|
||||
在查看代码结构或入口点之前,先理解这个软件在真实世界中的功能——它的目的、它的用户、真实输入的复杂性,以及它在哪里失败。这种理解驱动所有下游决策:哪些入口点最重要、定义哪些评估标准、使用哪些追踪输入、以及创建哪些数据集条目。在继续之前,先写详细的上下文文件。**注意**:项目可能包含 `tests/`、`fixtures/`、`examples/`、mock 服务器和文档——这些是项目自身的开发基础设施,**不是**你评估流水线的数据来源。在获取追踪输入和数据集内容时忽略它们。
|
||||
|
||||
> **检查点**:`pixie_qa/00-project-analysis.md` 已写入——涵盖该软件的功能、目标用户、能力清单(如果项目有,至少 3 个能力)、真实输入特征,以及困难问题/故障模式(至少 2 个)。
|
||||
|
||||
#### 子步骤 1b:入口点与执行流程
|
||||
|
||||
> **参考文件**:立即阅读 `references/1-b-entry-point.md`。
|
||||
|
||||
阅读源代码以了解应用如何启动以及真实用户如何调用它。使用 `pixie_qa/00-project-analysis.md` 中的**能力清单**来确定入口点的优先级——专注于能运行最有价值能力的入口点,而不是只找第一个找到的。在继续之前,先写详细的上下文文件。
|
||||
|
||||
> **检查点**:`pixie_qa/01-entry-point.md` 已写入——涵盖入口点、执行流程、面向用户的接口和环境要求。
|
||||
|
||||
#### 子步骤 1c:评估标准
|
||||
|
||||
> **参考文件**:立即阅读 `references/1-c-eval-criteria.md`。
|
||||
|
||||
定义应用的用例和评估标准。从 `pixie_qa/00-project-analysis.md` 中的**能力清单**推导出用例。从**困难问题/故障模式**推导出评估标准——而不是泛化的质量维度。用例驱动数据集创建(步骤 4);评估标准驱动评估器选择(步骤 3)。在继续之前,先写详细的上下文文件。
|
||||
|
||||
> **检查点**:`pixie_qa/02-eval-criteria.md` 已写入——涵盖用例、评估标准及其适用范围。现在还不要阅读步骤 2 的说明。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 2:插桩、运行应用并捕获参考追踪
|
||||
|
||||
步骤 2 有三个子步骤。每个子步骤阅读自己的参考文件。**在开始下一个子步骤之前,完整完成当前子步骤。**
|
||||
|
||||
#### 子步骤 2a:使用 `wrap` 进行插桩
|
||||
|
||||
> **参考文件**:立即阅读 `references/2a-instrumentation.md`。
|
||||
|
||||
在应用的数据边界处添加 `wrap()` 调用,以便评估框架可以注入受控输入并捕获输出。这使得应用无需改变其逻辑即可被测试。
|
||||
|
||||
> **检查点**:所有数据边界均已添加 `wrap()` 调用。`pixie_qa/02-eval-criteria.md` 中的每个评估标准都有对应的数据点。
|
||||
|
||||
#### 子步骤 2b:实现 Runnable
|
||||
|
||||
> **参考文件**:立即阅读 `references/2b-implement-runnable.md`。
|
||||
|
||||
编写一个 Runnable 类,让评估框架能够像真实用户一样调用应用。Runnable 应该很简单——它只是将应用的真正入口点连接到评估框架接口。如果它变得复杂,说明有问题。
|
||||
|
||||
> **检查点**:`pixie_qa/run_app.py` 已写入。Runnable 使用真实的 LLM 配置调用应用的真正入口点——没有 mock,没有伪造,没有组件替换。
|
||||
|
||||
#### 子步骤 2c:捕获并验证参考追踪
|
||||
|
||||
> **参考文件**:立即阅读 `references/2c-capture-and-verify-trace.md`。
|
||||
|
||||
通过 Runnable 运行应用并捕获追踪。该追踪证明插桩和 Runnable 工作正常,并为步骤 4 的数据集创建提供所需的数据结构。
|
||||
|
||||
> **检查点**:`pixie_qa/reference-trace.jsonl` 存在。所有预期的 `wrap` 条目和 `llm_span` 条目均已出现。`pixie format` 显示所有评估所需的数据点。现在还不要阅读步骤 3 的说明。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 3:定义评估器
|
||||
|
||||
> **参考文件**:立即阅读 `references/3-define-evaluators.md` 以获取详细的子步骤。
|
||||
|
||||
**目标**:将步骤 1c 中的定性评估标准转化为具体的、可运行的评分函数。每个标准映射到一个内置评估器、一个**智能体评估器**(任何语义或定性标准的默认选择),或一个手动自定义函数(仅用于机械性/确定性检查,如正则表达式或字段存在性)。评估器映射产物在标准和数据集之间架起桥梁,确保每个质量维度都有一个评分器。选择能够衡量 `pixie_qa/00-project-analysis.md` 中识别的**困难问题**的评估器——而不仅仅是泛化的质量维度。
|
||||
|
||||
> **检查点**:所有评估器已实现。`pixie_qa/03-evaluator-mapping.md` 已写入,包含标准到评估器的映射及决策理由。现在还不要阅读步骤 4 的说明。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 4:构建数据集
|
||||
|
||||
> **参考文件**:立即阅读 `references/4-build-dataset.md` 以获取详细的子步骤。
|
||||
|
||||
**目标**:创建将所有内容联系在一起的测试场景——Runnable(步骤 2)、评估器(步骤 3)和用例(步骤 1c)。每个数据集条目定义要向应用发送什么、应用应从外部服务看到什么数据,以及如何对结果进行评分。使用步骤 2 中的参考追踪作为数据结构和字段名的真相来源。覆盖 `pixie_qa/00-project-analysis.md` 中**能力清单**的条目,并包括针对其中识别的**故障模式**的条目。**不要使用项目自身的测试夹具、mock 服务器或示例数据作为数据集的 `eval_input` 内容**——应使用真实世界的数据。**应用中每个 `wrap(purpose="input")` 必须在每个条目的 `eval_input` 中有预先捕获的内容**——当应用有输入 wrap 时,不要将 `eval_input` 留空。
|
||||
|
||||
> **检查点**:数据集 JSON 已创建于 `pixie_qa/datasets/<name>.json`,包含覆盖所有用例的多样化条目。**数据集真实性审核已通过**——条目使用真实世界的数据且规模具有代表性,没有项目测试夹具污染,至少有一个条目针对故障模式且结果不确定,并且每个 `eval_input` 对所有输入 wrap 都有捕获的内容。现在还不要阅读步骤 5 的说明。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 5:运行 `pixie test` 并修复机械性问题
|
||||
|
||||
> **参考文件**:立即阅读 `references/5-run-tests.md` 以获取详细的子步骤。
|
||||
|
||||
**目标**:端到端执行完整流水线,使其在没有机械错误的情况下运行。此步骤严格限于修复 pixie QA 组件(数据集、Runnable、自定义评估器)中的设置和数据问题——**不是**修复应用本身或评估结果质量。一旦 `pixie test` 无错误完成,并为每个条目产生真实的评估器评分,此步骤即完成。
|
||||
|
||||
> **检查点**:`pixie test` 运行完成。每个数据集条目都有评估器评分(真实的 `EvaluationResult` 或 `PendingEvaluation`)。没有设置错误,没有导入失败,没有数据验证错误。
|
||||
>
|
||||
> 如果测试报错,那是你的 QA 组件中的机械性 bug——修复并重新运行。但一旦测试产生了评分,就继续前进。不要在这里评估结果质量——那是步骤 6 的事。
|
||||
|
||||
**在测试产生评分后,始终进行到步骤 6。** 分析是必不可少的关键步骤——没有它,待定评估永远无法完成,用户只能得到未经解读的原始评分,没有任何可操作的见解。不要停在这里问用户是否要继续。
|
||||
|
||||
**迭代运行的循环规则**:每次成功的 `pixie test` 调用都会创建一个具体的 `pixie_qa/results/<test_id>` 目录并启动一个新的分析周期。在你编辑应用代码、提示词、数据集、评估器或重新运行 `pixie test` 之前,先对该确切的结果目录完成步骤 6。不要跳过早期周期而只分析最后一次运行。
|
||||
|
||||
---
|
||||
|
||||
### 步骤 6:分析结果
|
||||
|
||||
> **参考文件**:立即阅读 `references/6-analyze-outcomes.md`——其中包含完整的三阶段分析流程、编写指南和输出格式要求。
|
||||
|
||||
**目标**:通过结构化的数据驱动流程分析 `pixie test` 结果,生成关于测试用例质量、评估器质量和应用质量的可操作见解。此步骤完成待定评估,写入每个条目和每个数据集的分析,并生成一个按优先级排序的行动计划。每项陈述必须有评估运行中的具体数据支持——不得推测,不得含糊。
|
||||
|
||||
**持久化的分析产物**:在此简化的工作流中,仅在数据集级别和测试运行级别持久化分析。这些产物仍然包含**详细版本**(供智能体消费:数据点、证据线索、推理链)和**摘要版本**(供人类审查:可在 2 分钟内阅读的简洁 TLDR)。不要创建每个条目的分析文件。
|
||||
|
||||
**硬性完成门禁**:步骤 6 **未完成**,直到以下所有条件均满足:
|
||||
|
||||
- 每个 `pixie_qa/results/<test_id>/dataset-*/entry-*/evaluations.jsonl` 中 `"status": "pending"` 的条目,都已替换为包含 `score` 和 `reasoning` 的评分结果。
|
||||
- 每个数据集目录都有 `analysis.md` 和 `analysis-summary.md`。
|
||||
- 测试运行根目录下有 `action-plan.md` 和 `action-plan-summary.md`。
|
||||
- 你已经针对 `pixie_qa/results/<test_id>` 运行了此技能 `resources/` 目录中的步骤 6 验证脚本,并且它报告成功。
|
||||
|
||||
**明确不足够的:**
|
||||
|
||||
- 只写一个顶层文件如 `pixie_qa/06-analysis.md`
|
||||
- 说待定评估留给用户在 Web UI 中查看
|
||||
- 说某个条目"可能通过"而没有更新 `evaluations.jsonl`
|
||||
|
||||
---
|
||||
|
||||
## Web 服务器管理
|
||||
|
||||
pixie-qa 在后台运行一个 Web 服务器,用于向用户展示上下文、追踪和评估结果。它由设置脚本自动启动(通过 `pixie start` 命令,该命令启动一个分离的后台进程并立即返回)。
|
||||
|
||||
当用户完成 eval-driven-dev 工作流后,告知他们 Web 服务器仍在运行,你可以通过以下命令清理它:
|
||||
|
||||
```bash
|
||||
pixie stop
|
||||
```
|
||||
|
||||
重要提示:Web 服务器停止后,Web UI 将无法访问。因此,只有在用户确认他们已完成所有 Web UI 功能后才停止服务器。如果他们想继续使用 Web UI,**不要**停止服务器。
|
||||
|
||||
每当你重新启动工作流时,始终再次运行 resources 中的 setup.sh 脚本,以确保 Web 服务器正在运行:
|
||||
@@ -0,0 +1,102 @@
|
||||
# 步骤 1a:项目分析
|
||||
|
||||
在查看代码结构、入口点或编写任何检测工具之前,先理解该软件在真实世界中是做什么的。这份分析是后续每个步骤的基础——它决定了哪些入口点应优先处理、定义哪些评估标准、使用什么追踪输入,以及构建哪些数据集条目。
|
||||
|
||||
---
|
||||
|
||||
## 需要调查的内容
|
||||
|
||||
阅读项目的 README、文档以及顶层源文件。你需要找到以下五个问题的答案:
|
||||
|
||||
### 1. 该软件是做什么的?
|
||||
|
||||
用一段简明易懂的语言来概括。它解决了什么问题?一次成功的运行是什么样的?
|
||||
|
||||
### 2. 谁在使用它以及为什么?
|
||||
|
||||
目标用户是谁?主要用例是什么?它解决了哪些替代方案无法解决的问题?这有助于你理解对该应用而言"质量"意味着什么——一个与客户对话的聊天机器人,与一个综合多源报告的研究型代理,有着截然不同的质量要求。
|
||||
|
||||
### 3. 能力清单
|
||||
|
||||
列出该应用提供的各项独立能力、模式或功能。要具体。例如:
|
||||
|
||||
- 对于一个爬取库:单页爬取、多页爬取、基于搜索的爬取、语音输出、脚本生成
|
||||
- 对于一个语音代理:问候语、FAQ 处理、账户查询、转接人工、通话摘要
|
||||
- 对于一个研究代理:主题研究、多源综合、引文生成、报告格式化
|
||||
|
||||
每项能力可能需要自己的入口点、自己的追踪记录以及自己的数据集条目。该清单直接为步骤 1c(用例)和步骤 4(数据集多样性)提供输入。
|
||||
|
||||
### 4. 真实的输入是什么样的?
|
||||
|
||||
描述该应用在真实世界中处理的输入——而非玩具示例:
|
||||
|
||||
- 对于一个网页爬取器:"包含导航、广告、动态内容、表格、嵌套结构的杂乱 HTML 页面——通常为 5KB–500KB 的 HTML"
|
||||
- 对于一个研究代理:"需要多源综合的开放式研究问题,包含 3–10 个子问题"
|
||||
- 对于一个语音代理:"带有背景噪音、打断和模糊请求的多轮对话"
|
||||
|
||||
要具体说明**规模**(多大)、**复杂度**(有多杂乱/多样)以及**多样性**(有哪些种类)。这直接为追踪输入选择(步骤 2)提供依据——如果你不在这里描述真实的输入特征,最终就会使用绕过应用真实逻辑的玩具输入。
|
||||
|
||||
**本节是一项操作约束,而不仅仅是文档。** 步骤 2c(追踪输入)和步骤 4c(数据集条目)将交叉引用这些特征,以验证追踪输入和数据集条目是否匹配真实世界的规模和复杂度。要具体且量化——写"5KB–500KB 的 HTML 页面",而不是"各种 HTML 页面"。
|
||||
|
||||
### 5. 难点/故障模式有哪些?
|
||||
|
||||
是什么让该应用的工作变得困难?它在实际使用中会在哪些地方失败?这些将成为最有价值的评估场景:
|
||||
|
||||
- 对于一个爬取器:"格式错误的 HTML、动态 JS 渲染内容、复杂的嵌套模式、超出上下文窗口的超大页面"
|
||||
- 对于一个研究代理:"相互矛盾的来源、需要多步推理的问题、编造引用"
|
||||
- 对于一个语音代理:"模糊的来电者意图、账户查询失败、同时进行的工具调用"
|
||||
|
||||
每个故障模式应至少映射到一个评估标准(步骤 1c)和至少一个数据集条目(步骤 4)。
|
||||
|
||||
---
|
||||
|
||||
## 输出:`pixie_qa/00-project-analysis.md`
|
||||
|
||||
将你的发现写入该文件。**在进入子步骤 1b 之前,完成全部五个部分。** 该文档被后续每个步骤引用。
|
||||
|
||||
### 模板
|
||||
|
||||
```markdown
|
||||
# 项目分析
|
||||
|
||||
## 该软件的功能
|
||||
|
||||
<一段话:它做什么,用通俗易懂的语言。不是类名或文件路径——它为使用者解决了什么问题?>
|
||||
|
||||
## 目标用户与价值主张
|
||||
|
||||
<谁使用它,为什么,它解决了哪些替代方案无法解决的问题>
|
||||
|
||||
## 能力清单
|
||||
|
||||
1. <能力名称>:<一行描述>
|
||||
2. <能力名称>:<一行描述>
|
||||
3. ...
|
||||
|
||||
## 真实输入特征
|
||||
|
||||
<真实世界输入的样子——规模、复杂度、杂乱程度、多样性。要具体说明规模和结构。>
|
||||
|
||||
## 难点与故障模式
|
||||
|
||||
1. <故障模式>:<为什么难,会出什么问题>
|
||||
2. <故障模式>:<为什么难,会出什么问题>
|
||||
3. ...
|
||||
```
|
||||
|
||||
### 质量检查
|
||||
|
||||
在继续之前,请验证:
|
||||
|
||||
- "该软件的功能"部分应以非技术用户能理解的方式描述应用的用途——而不仅仅是"它运行一个图"或"它调用 OpenAI"
|
||||
- 能力清单至少列出 3 项能力(如果项目具备的话)——如果你只找到了 1 项,可能只看了代码库的一部分
|
||||
- 真实输入特征应描述真实世界的规模和复杂度,而非最简单可能的输入
|
||||
- 故障模式应针对该应用的特定领域,而非泛泛而谈("输入错误"不是一个故障模式;"格式错误的 HTML 带有未闭合标签导致解析器崩溃"才是)
|
||||
|
||||
### 项目中应忽略的内容
|
||||
|
||||
项目中可能包含属于其自身开发/测试基础设施的目录和文件——例如 `tests/`、`fixtures/`、`examples/`、`mock_server/`、`docs/`、演示脚本等。这些是为项目开发者准备的,而不是为你的评估管道准备的。
|
||||
|
||||
**关键**:请勿将项目的测试夹具、模拟服务器、示例数据或单元测试基础设施用作评估追踪或数据集条目的输入。它们是为开发速度和隔离性而设计的——数据规模小、干净、确定性强,绕过了所有真实世界的难点。使用它们会产生过于简单的评估,无法捕捉到真正的质量问题。
|
||||
|
||||
当你在分析过程中遇到这些目录时,注意它们的存在,但将其视为项目的实现细节——而不是你的 QA 管道的数据来源。你的 QA 管道必须在真实世界条件下测试应用,而不是使用项目自身的测试捷径。
|
||||
@@ -0,0 +1,68 @@
|
||||
# 步骤 1b:入口点与执行流程
|
||||
|
||||
确定应用程序的启动方式以及真实用户如何调用它。利用 `pixie_qa/00-project-analysis.md` 中的**能力清单**来确定优先级——专注于那些发挥最有价值、最常用能力的入口点,而不是仅仅找到第一个就结束。
|
||||
|
||||
---
|
||||
|
||||
## 调查内容
|
||||
|
||||
### 1. 软件的运行方式
|
||||
|
||||
入口点是什么?如何启动它?是 CLI、服务器还是库函数?需要哪些参数、配置文件或环境变量?
|
||||
|
||||
查找以下内容:
|
||||
|
||||
- `if __name__ == "__main__"` 代码块
|
||||
- 框架入口点(FastAPI `app`、Flask `app`、Django `manage.py`)
|
||||
- `pyproject.toml` 中的 CLI 入口点(`[project.scripts]`)
|
||||
- 可揭示启动命令的 Docker/compose 配置
|
||||
|
||||
### 2. 真实用户入口点
|
||||
|
||||
真实用户或客户端如何调用该应用?评估必须测试的是这个——而不是绕过请求管道的内部函数。
|
||||
|
||||
- **Web 服务器**:哪些 HTTP 端点接受用户输入?使用什么方法(GET/POST)?请求体的结构是怎样的?
|
||||
- **CLI**:用户提供哪些命令行参数?
|
||||
- **库/函数**:调用方导入并调用哪个函数?有哪些参数?
|
||||
|
||||
### 3. 环境与配置
|
||||
|
||||
- 应用需要哪些环境变量?(服务端点、数据库 URL、功能开关)
|
||||
- 它读取哪些配置文件?
|
||||
- 哪些有合理的默认值,哪些必须显式设置?
|
||||
|
||||
---
|
||||
|
||||
## 输出:`pixie_qa/01-entry-point.md`
|
||||
|
||||
将调查结果写入此文件。保持聚焦——仅包含入口点和执行流程。
|
||||
|
||||
### 模板
|
||||
|
||||
```markdown
|
||||
# 入口点与执行流程
|
||||
|
||||
## 运行方式
|
||||
|
||||
<启动应用的命令、必需的环境变量、配置文件>
|
||||
|
||||
## 入口点
|
||||
|
||||
- **文件**:<例如 app.py、main.py>
|
||||
- **类型**:<FastAPI 服务器 / CLI / 独立函数 / 其他>
|
||||
- **框架**:<FastAPI、Flask、Django、无>
|
||||
|
||||
## 面向用户的端点 / 接口
|
||||
|
||||
<针对用户与应用程序交互的每种方式:>
|
||||
|
||||
- **端点 / 命令**:<例如 POST /chat、python main.py --query "...">
|
||||
- **输入格式**:<请求体结构、CLI 参数、函数参数>
|
||||
- **输出格式**:<响应结构、stdout 格式、返回类型>
|
||||
|
||||
## 环境要求
|
||||
|
||||
| 变量 | 用途 | 是否必需 | 默认值 |
|
||||
| -------- | ------- | --------- | ------- |
|
||||
| ... | ... | ... | ... |
|
||||
```
|
||||
@@ -0,0 +1,6 @@
|
||||
明白了。你发来的就是这份 `README.md` 文件,你需要我把它的内容作为原文,按照你设定的翻译规范(逐句精准、保留 Markdown/YAML 结构、不译代码块/标识符/专有名词、中文标点等)翻译成简体中文,对吗?
|
||||
|
||||
如果是,我现在就开始翻译。在继续之前,请确认:
|
||||
|
||||
1. 原文就是你上面消息中从 `# Step 1c: Eval Criteria` 开始到模板结束的那部分内容(不含 `> README.md` 这行)?
|
||||
2. 翻译后的文件需要保存到哪里,还是只输出到对话中?
|
||||
@@ -0,0 +1,134 @@
|
||||
# 第 2a 步:使用 `wrap` 进行仪表化
|
||||
|
||||
> 完整的 `wrap()` API 参考见 `wrap-api.md`。
|
||||
|
||||
**目标**:在数据边界添加 `wrap()` 调用,以便评估工具能够(1)用受控输入替代真实的外部依赖进行注入,以及(2)捕获输出用于评分。
|
||||
|
||||
---
|
||||
|
||||
## 数据流分析
|
||||
|
||||
从 LLM 调用点出发,沿着代码向前和向后追溯,找到:
|
||||
|
||||
- **依赖输入**:来自外部系统(数据库、API、缓存、文件系统、网络请求)的数据
|
||||
- **应用输出**:输出给用户或外部系统的数据
|
||||
- **中间状态**:与评估相关的内部决策(路由、工具调用)
|
||||
|
||||
你**不需要**包装 LLM 调用的参数或响应——这些已被 OpenInference 自动仪表化捕获。
|
||||
|
||||
## 添加 `wrap()` 调用
|
||||
|
||||
对于找到的每个数据点,在应用代码中添加 `wrap()` 调用:
|
||||
|
||||
```python
|
||||
import pixie
|
||||
|
||||
# 外部依赖数据——函数形式(在评估模式下阻止真实调用)
|
||||
profile = pixie.wrap(db.get_profile, purpose="input", name="customer_profile",
|
||||
description="从数据库获取的客户资料")(user_id)
|
||||
|
||||
# 外部依赖数据——函数形式(在评估模式下阻止真实调用)
|
||||
history = pixie.wrap(redis.get_history, purpose="input", name="conversation_history",
|
||||
description="从 Redis 获取的对话历史")(session_id)
|
||||
|
||||
# 应用输出——用户收到的内容
|
||||
response = pixie.wrap(response_text, purpose="output", name="response",
|
||||
description="助手对用户的响应")
|
||||
|
||||
# 中间状态——与评估相关的内部决策
|
||||
selected_agent = pixie.wrap(selected_agent, purpose="state", name="routing_decision",
|
||||
description="被选中处理此请求的智能体")
|
||||
```
|
||||
|
||||
### 值与函数包装的对比
|
||||
|
||||
```python
|
||||
# 值形式:包装一个数据值(结果已计算完毕)
|
||||
profile = pixie.wrap(db.get_profile(user_id), purpose="input", name="customer_profile")
|
||||
|
||||
# 函数形式:包装可调用对象——在评估模式下不会调用原函数;
|
||||
# 而是直接返回注册表中的值。
|
||||
profile = pixie.wrap(db.get_profile, purpose="input", name="customer_profile")(user_id)
|
||||
```
|
||||
|
||||
**关键:对于 `purpose="input"` 的外部调用包装,始终使用函数形式**——HTTP 请求、数据库查询、API 调用、文件读取、缓存查找。函数形式可以阻止真实调用在评估模式下执行,从而直接返回数据集中的值,而无需发起实时网络请求或数据库查询。值形式仍然会先执行真实调用,之后再替换结果——这会浪费时间、产生不稳定的测试,并使评估依赖于外部服务的可用性。
|
||||
|
||||
`purpose="input"` 下唯一可接受值形式的情况是:被包装的值是一个本地计算(无 I/O、无副作用)且重新计算代价很低。
|
||||
|
||||
### 放置规则
|
||||
|
||||
1. **在数据边界处包装**——在数据进入或离开应用的位置,而不是在工具函数内部深处。
|
||||
2. **名称必须在整个应用中唯一**(用作注册表键名和数据集字段名)。
|
||||
3. **名称使用 `lower_snake_case`**(小写蛇形命名法)。
|
||||
4. **不要改变函数的接口**——`wrap()` 是纯附加的,返回相同类型。
|
||||
|
||||
### 按用途放置
|
||||
|
||||
#### `purpose="input"`——外部数据进入的位置
|
||||
|
||||
在**外部数据进入应用的边界**处放置输入包装,而不是在中间处理阶段。在管道架构(获取→处理→提取→格式化)中:
|
||||
|
||||
- **正确**:在 HTTP 请求边界处使用**函数形式** `wrap(fetch_page, purpose="input", name="fetched_page")(url)`——在评估模式下,请求被完全跳过并返回数据集中的值;在跟踪模式下,执行真实请求并捕获结果。
|
||||
- **错误**:使用值形式 `wrap(html_content, purpose="input", name="fetched_page")`——评估模式下 HTTP 请求仍然会执行(浪费时间且产生不稳定测试),之后才替换结果。
|
||||
- **错误**:在解析之后包装 `wrap(processed_chunks, purpose="input", name="chunks")`——评估模式会绕过解析和分块逻辑。
|
||||
|
||||
**原则**:`wrap(purpose="input")` 替换的是**最小外部依赖**,同时**最大限度地执行内部逻辑**。尽可能将边界向上游推进。对于外部调用的输入包装,**始终使用函数形式**——这样能阻止真实调用在评估模式下执行。
|
||||
|
||||
#### `purpose="output"`——处理后的数据退出的位置
|
||||
|
||||
从 LLM 响应开始**向下游**追踪,找到数据离开应用的位置——发送给用户、写入存储、在 UI 中渲染,或传递给外部系统。在该出口边界处进行包装。
|
||||
|
||||
- 不要包装原始 LLM 响应——这些已被 OpenInference 自动仪表化作为 `llm_span` 条目捕获。
|
||||
- 包装应用的**最终处理结果**——即应用对 LLM 输出进行后处理、格式化或转换后的最终结果。
|
||||
- 如果应用有多个输出通道(例如,给用户的响应 AND 写入数据库的副作用),分别包装每个通道。
|
||||
|
||||
```python
|
||||
# 经过应用格式化管道后的最终响应
|
||||
response = pixie.wrap(formatted_response, purpose="output", name="response",
|
||||
description="发送给用户的最终响应")
|
||||
|
||||
# 副作用输出——写入外部存储的数据
|
||||
pixie.wrap(saved_record, purpose="output", name="saved_summary",
|
||||
description="保存到数据库的摘要记录")
|
||||
```
|
||||
|
||||
**原则**:输出包装仅用于观察——它们捕获应用产生的内容,供评估者评分。在评估运行期间永远不会被模拟或注入。
|
||||
|
||||
#### `purpose="state"`——与评估相关的内部决策
|
||||
|
||||
某些评估标准需要评判应用的内部推理——不仅仅是输入或输出了什么,还包括应用**如何**做出决策。当某个评估标准需要此类数据,且该数据在输入或输出中不可见时,包装内部状态。
|
||||
|
||||
常见示例:
|
||||
|
||||
- **智能体路由**:选择了哪个子智能体或工具来处理请求
|
||||
- **计划/步骤决策**:智能体选择执行哪些步骤
|
||||
- **记忆更新**:智能体在工作记忆中添加或移除了什么
|
||||
- **检索结果**:在输入 LLM 之前检索了哪些文档/分块
|
||||
|
||||
```python
|
||||
# 智能体路由决策
|
||||
selected_agent = pixie.wrap(selected_agent, purpose="state", name="routing_decision",
|
||||
description="被选中处理此请求的智能体")
|
||||
|
||||
# 输入 LLM 的检索上下文
|
||||
pixie.wrap(retrieved_chunks, purpose="state", name="retrieved_context",
|
||||
description="RAG 在 LLM 调用前检索到的文档分块")
|
||||
```
|
||||
|
||||
**原则**:只包装评估标准实际需要的状态。不要包装每个变量——状态包装仅用于评估者必须看到但不在应用输入或输出中出现的内部数据。
|
||||
|
||||
### 覆盖检查
|
||||
|
||||
添加完所有 `wrap()` 调用后,逐一检查 `pixie_qa/02-eval-criteria.md` 中的每条评估标准,确认:
|
||||
|
||||
1. 每条评判**输入了什么**的标准,都有对应的 `input` 或 `entry` 包装。
|
||||
2. 每条评判**输出了什么**的标准,都有对应的 `output` 包装。
|
||||
3. 每条评判**应用如何决策**的标准,都有对应的 `state` 包装。
|
||||
|
||||
如果某条标准需要的数据尚未被捕获,立即添加该包装——不要推迟。
|
||||
|
||||
---
|
||||
|
||||
## 输出
|
||||
|
||||
修改后的应用源代码文件,在数据边界处添加了 `wrap()` 调用。
|
||||
@@ -0,0 +1,145 @@
|
||||
# Step 2b:实现 Runnable
|
||||
|
||||
> 关于完整的 `Runnable` 协议和 `wrap()` API,请参阅 `wrap-api.md`。
|
||||
|
||||
**目标**:编写一个 Runnable 类,让评估框架能够像真实用户一样调用应用程序。
|
||||
|
||||
---
|
||||
|
||||
## 核心思路
|
||||
|
||||
Runnable 是 `pixie test` 和 `pixie trace` 运行应用程序的方式。可以把它想象成真实用户的程序化替身:它启动应用、发送请求,然后让应用自行处理。评估框架会为每个测试用例调用 `run()` 方法,传入用户的输入参数。应用程序通过其真实代码——真实的路由、真实的提示词组装、真实的 LLM 调用、真实的响应格式化——来处理这些参数,而评估框架则通过第 2a 步中的 `wrap()` 插桩来观察发生的一切。
|
||||
|
||||
**这意味着 Runnable 应该很简单。** 它只是将应用程序的真实入口点连接到评估框架的接口。如果你的 Runnable 变得越来越复杂——比如你在构建自定义逻辑、重新实现应用行为、或替换组件——那一定是有问题的。
|
||||
|
||||
## 四项要求
|
||||
|
||||
### 1. 运行真实的生产代码
|
||||
|
||||
Runnable 调用应用程序的实际入口点——也就是真实用户会触发的同一个函数、类或端点。它不会重新实现、偷工减料或替代应用程序的任何部分。
|
||||
|
||||
这包括 LLM。应用程序的 LLM 调用必须经过真实的代码路径——不要模拟、伪造或替换应用组件。基于评估的测试的全部意义就在于 LLM 输出是非确定性的,因此你要使用评估器(而不是断言)来打分。如果你用伪造品替换了任何组件,你就消除了真实行为,评估也就毫无意义。
|
||||
|
||||
**如果应用因无法解决的环境变量或配置缺失而无法运行,请停止并向用户反馈,让其修复环境配置。** 不要通过模拟组件来绕过问题。
|
||||
|
||||
### 2. 用 Pydantic BaseModel 表示启动参数
|
||||
|
||||
`run()` 方法接收一个 Pydantic `BaseModel`,其字段从数据集的 `input_data` 中填充。定义包含应用所需字段的子类:
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
user_message: str
|
||||
# 根据应用入口点的需要添加更多字段。
|
||||
# 这些字段与数据集的 input_data 键一一对应。
|
||||
```
|
||||
|
||||
**字段必须反映真实用户实际提供的内容。** 请阅读 `pixie_qa/00-project-analysis.md`——其中的「真实输入特征」部分描述了真实输入的复杂度、规模与多样性。设计模型时应使其能够接受该级别的输入,而非简化的玩具版本。
|
||||
|
||||
理解用户提供的参数与世界数据之间的边界:
|
||||
|
||||
- **用户提供的参数**(BaseModel 上的字段):真实用户输入或配置的内容——提示词、查询、配置标志、URL、模式定义。
|
||||
- **世界数据**(由第 2a 步中的 `wrap(purpose="input")` 处理):应用在执行过程中从外部来源获取的内容——网页、数据库记录、API 响应。这不属于 BaseModel。
|
||||
|
||||
| 应用类型 | BaseModel 字段(用户提供) | 世界数据(wrap 提供) |
|
||||
| ------------------ | ---------------------------------- | ----------------------------------------- |
|
||||
| 网页爬虫 | URL + 提示词 + 模式定义 | HTML 页面内容 |
|
||||
| 研究代理 | 研究问题 + 范围约束 | 源文档、搜索结果 |
|
||||
| 客户支持机器人 | 客户的口头消息 | CRM 中的客户资料、会话存储中的对话历史 |
|
||||
| 代码审查工具 | PR URL + 审查标准 | 实际的 diff、文件内容、CI 结果 |
|
||||
|
||||
如果某个字段最终保存的是应用本来会自行获取的数据,那它可能应该放在 `wrap(purpose="input")` 调用中,而不是 BaseModel 上。
|
||||
|
||||
### 3. 线程安全
|
||||
|
||||
`run()` 会被并发调用以处理多条数据集条目(最多并行 4 条)。如果应用使用了共享可变状态——SQLite、基于文件的数据库、全局缓存——请使用 `asyncio.Semaphore` 保护访问:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
_sem: asyncio.Semaphore
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
inst = cls()
|
||||
inst._sem = asyncio.Semaphore(1)
|
||||
return inst
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
async with self._sem:
|
||||
await call_app(args.message)
|
||||
```
|
||||
|
||||
仅在应用确实存在共享可变状态时才添加信号量。如果应用使用基于请求的状态(以唯一 ID 为键)或本质上是无状态的,那么并发调用自然就是隔离的。
|
||||
|
||||
### 4. 遵循 Runnable 接口
|
||||
|
||||
```python
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable": ... # 构造实例
|
||||
async def setup(self) -> None: ... # 一次,在第一次 run() 之前
|
||||
async def run(self, args: AppArgs) -> None: ... # 每条数据集条目,并发执行
|
||||
async def teardown(self) -> None: ... # 一次,在最后一次 run() 之后
|
||||
```
|
||||
|
||||
- `create()` —— 类方法,返回一个新实例。使用带引号的返回类型(`-> "AppRunnable"`)以避免前向引用错误。
|
||||
- `setup()` —— 可选的异步方法;初始化共享资源(HTTP 客户端、数据库连接、服务器)。
|
||||
- `run(args)` —— 异步方法;每条数据集条目调用一次。在此处调用应用的真正入口点。
|
||||
- `teardown()` —— 可选的异步方法;清理 `setup()` 中的资源。
|
||||
|
||||
## 最小示例
|
||||
|
||||
```python
|
||||
# pixie_qa/run_app.py
|
||||
from pydantic import BaseModel
|
||||
import pixie
|
||||
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
user_message: str
|
||||
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
"""驱动应用程序以进行追踪和评估。"""
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
from myapp import handle_request
|
||||
await handle_request(args.user_message)
|
||||
```
|
||||
|
||||
仅此而已。Runnable 导入应用的真正入口点并调用它。没有自定义逻辑、没有组件替换、没有巧妙变通。
|
||||
|
||||
## 架构特定示例
|
||||
|
||||
根据应用程序的运行方式,阅读对应的示例文件:
|
||||
|
||||
| 应用类型 | 入口点 | 示例文件 |
|
||||
| ----------------------------------- | ---------------------- | ------------------------------------------------------------ |
|
||||
| **独立函数**(无服务器) | Python 函数 | 阅读 `references/runnable-examples/standalone-function.md` |
|
||||
| **Web 服务器**(FastAPI、Flask) | HTTP/WebSocket 端点 | 阅读 `references/runnable-examples/fastapi-web-server.md` |
|
||||
| **CLI 应用程序** | 命令行调用 | 阅读 `references/runnable-examples/cli-app.md` |
|
||||
|
||||
只阅读与你应用类型匹配的那个示例文件。
|
||||
|
||||
## 文件放置
|
||||
|
||||
- 将文件放在 `pixie_qa/run_app.py`。
|
||||
- 数据集中的 `"runnable"` 字段引用:`"pixie_qa/run_app.py:AppRunnable"`。
|
||||
- 项目根目录会自动加入 `sys.path`,因此可以使用正常的导入方式(`from app import service`)。
|
||||
|
||||
## 技术说明
|
||||
|
||||
不要在 runnable 文件中使用 `from __future__ import annotations`——它会破坏 Pydantic 对嵌套模型的模型解析。在需要的地方改用带引号的返回类型。
|
||||
|
||||
---
|
||||
|
||||
## 输出
|
||||
|
||||
`pixie_qa/run_app.py` —— Runnable 类。
|
||||
@@ -0,0 +1,118 @@
|
||||
# Step 2c:捕获并验证参考 Trace
|
||||
|
||||
**目标**:通过 Runnable 运行应用程序,捕获一个 Trace,并验证插桩和 Runnable 正常工作。Trace 证明一切已正确连接,并为步骤 4 的数据集创建提供精确的数据结构。
|
||||
|
||||
---
|
||||
|
||||
## 选择 Trace 输入
|
||||
|
||||
Trace 输入决定了捕获哪些代码路径。一个简单的输入会产生一个简单的 Trace,从而错过应用程序的真实行为。
|
||||
|
||||
输入必须符合你在步骤 2b 中阅读的 `pixie_qa/00-project-analysis.md` 中「真实输入特征」部分的要求。
|
||||
|
||||
输入包含两部分——理解它们之间的边界:
|
||||
|
||||
- **用户提供的参数**(你编写):真实用户键入或配置的内容——提示词、查询、配置标志、URL、模式定义。编写这些内容时应使其具有代表性,反映真实使用场景。
|
||||
- **世界数据**(从生产代码中捕获,而非虚构):应用程序在执行过程中从外部来源获取的内容——数据库记录、API 响应、文件等。运行一次生产代码以将此数据捕获到 Trace 中。仅在以下情况下才使用合成数据生成:
|
||||
- 用户明确指示你使用合成数据,或者
|
||||
- 从真实来源获取数据不可行(获取次数过多、产生实际金钱成本、或耗时过长——超过约 30 分钟)
|
||||
|
||||
**编写输入前的快速检查**:「真实用户会创建这些数据,还是应用程序会从其他地方获取它?」如果数据由应用程序获取,就让生产代码运行并捕获它。
|
||||
|
||||
| 应用类型 | 用户提供(你编写) | 世界提供(你获取) |
|
||||
| ---------------- | ----------------------------------------- | ----------------------------------------- |
|
||||
| 网页爬虫 | URL + 提示词 + 模式定义 | HTML 页面内容 |
|
||||
| 研究代理 | 研究问题 + 范围约束 | 源文档、搜索结果 |
|
||||
| 客服机器人 | 客户的语音消息 | CRM 中的客户资料、会话存储中的对话历史 |
|
||||
| 代码审查工具 | PR URL + 审查标准 | 实际差异、文件内容、CI 结果 |
|
||||
|
||||
### 捕获多个 Trace
|
||||
|
||||
在构建数据集之前,**至少捕获 2 个**具有不同输入特征的 Trace:
|
||||
|
||||
- 不同的复杂度(简单情况 vs. 复杂情况)
|
||||
- 不同的能力(参见 `00-project-analysis.md` 的能力清单)
|
||||
- 不同的边缘条件(缺少可选数据、异常大的输入)
|
||||
|
||||
这种校准可以防止数据集的同质性——你能看到应用程序在处理各种输入时的实际表现。
|
||||
|
||||
---
|
||||
|
||||
## 运行 `pixie trace`
|
||||
|
||||
**首先**,验证应用程序可以被导入:`python -c "from <module> import <class>"`。在进入 trace-安装-重试循环之前先捕获缺失的包。
|
||||
|
||||
```bash
|
||||
# 创建一个包含输入数据的 JSON 文件
|
||||
echo '{"user_message": "一个真实的示例输入"}' > pixie_qa/sample-input.json
|
||||
|
||||
uv run pixie trace --runnable pixie_qa/run_app.py:AppRunnable \
|
||||
--input pixie_qa/sample-input.json \
|
||||
--output pixie_qa/reference-trace.jsonl
|
||||
```
|
||||
|
||||
`--input` 标志接收一个 JSON **文件路径**(而非内联 JSON)。JSON 的键将成为 Pydantic 模型的 kwargs。
|
||||
|
||||
对于额外的 Trace:
|
||||
|
||||
```bash
|
||||
uv run pixie trace --runnable pixie_qa/run_app.py:AppRunnable \
|
||||
--input pixie_qa/sample-input-complex.json \
|
||||
--output pixie_qa/trace-complex.jsonl
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验证 Trace
|
||||
|
||||
### 快速检查
|
||||
|
||||
Trace JSONL 包含每个 `wrap()` 事件一行和每个 LLM span 一行:
|
||||
|
||||
```jsonl
|
||||
{"type": "kwargs", "value": {"user_message": "你们的营业时间是什么?"}}
|
||||
{"type": "wrap", "name": "customer_profile", "purpose": "input", "data": {...}, ...}
|
||||
{"type": "llm_span", "request_model": "gpt-4o", "input_messages": [...], ...}
|
||||
{"type": "wrap", "name": "response", "purpose": "output", "data": "我们的营业时间是..."}
|
||||
```
|
||||
|
||||
检查以下内容:
|
||||
|
||||
- 预期的 `wrap` 条目出现(代码中每个 `wrap()` 调用对应一个)
|
||||
- 至少出现一个 `llm_span` 条目(确认进行了真实的 LLM 调用)
|
||||
- 缺失的条目表明执行路径与预期不同——在继续之前修复
|
||||
|
||||
### 格式化并验证覆盖率
|
||||
|
||||
运行 `pixie format` 以数据集条目格式查看数据:
|
||||
|
||||
```bash
|
||||
pixie format --input trace.jsonl --output dataset_entry.json
|
||||
```
|
||||
|
||||
输出显示:
|
||||
|
||||
- `input_data`:Runnable 参数的精确键/值
|
||||
- `eval_input`:来自 `wrap(purpose="input")` 调用的数据
|
||||
- `eval_output`:实际的应用输出(来自 `wrap(purpose="output")`)
|
||||
|
||||
对于 `pixie_qa/02-eval-criteria.md` 中的每个评估标准,验证格式化输出包含所需的数据。如果某个数据点缺失,返回步骤 2a 并添加 `wrap()` 调用。
|
||||
|
||||
### Trace 审计
|
||||
|
||||
在进入步骤 3 之前,审计每个 Trace:
|
||||
|
||||
1. **世界数据检查**:对于每个 `wrap(purpose="input")` 字段,数据是否具有真实的复杂度?与 `00-project-analysis.md` 的「真实输入特征」进行比较。如果分析说明输入为 5KB–500KB,而你的输入小于 5KB,则说明不具代表性。
|
||||
|
||||
2. **LLM span 检查**:`llm_span` 条目是否出现?如果没有出现,说明应用程序的 LLM 调用未触发——Runnable 可能配置错误,或者 LLM 被 mock/伪造。在继续之前修复此问题。
|
||||
|
||||
3. **复杂度检查**:Trace 是否涉及 `00-project-analysis.md` 中的困难问题?如果它只覆盖了快乐路径,请使用更难的输入捕获额外的 Trace。
|
||||
|
||||
如果任何检查失败,请返回修复输入或 Runnable,然后重新捕获。
|
||||
|
||||
---
|
||||
|
||||
## 输出
|
||||
|
||||
- `pixie_qa/reference-trace.jsonl`——包含所有预期 wrap 事件和 LLM span 的参考 Trace
|
||||
- 针对不同输入的其他 Trace 文件
|
||||
@@ -0,0 +1,158 @@
|
||||
# 步骤 3:定义评估器
|
||||
|
||||
**为什么需要这一步**:在完成应用的仪器化(步骤 2)之后,现在你需要将每个评估标准映射到具体的评估器——在需要的地方实现自定义评估器——以便数据集(步骤 4)可以通过名称引用它们。
|
||||
|
||||
---
|
||||
|
||||
## 3a. 将标准映射到评估器
|
||||
|
||||
**步骤 1c 中的每个评估标准——包括用户在提示中指定的任何维度——都必须有对应的评估器。** 如果用户要求了"事实准确性、完整性和偏见性",你就需要三个评估器(或者一个覆盖全部三者的多标准评估器)。不要静默地丢弃任何被请求的维度。优先选择那些能够衡量 `pixie_qa/00-project-analysis.md` 中确定的**困难问题/失败模式**的评估器——这些比通用的质量评估器更有价值。
|
||||
|
||||
对于每个评估标准,按照以下决策顺序选择评估器:
|
||||
|
||||
1. **内置评估器**——如果标准评估器符合该标准(事实正确性 → `Factuality`,精确匹配 → `ExactMatch`,RAG 忠实度 → `Faithfulness`)。完整目录请参见 `evaluators.md`。
|
||||
2. **Agent 评估器**(`create_agent_evaluator`)——**所有语义性、定性和应用特定标准的默认选择**。Agent 评估器将由你(编码 agent)在步骤 6 中进行评分,你将在该步骤中全面审查每个条目的追踪记录和输出。对于像"提取结果是否准确捕获了源内容?""是否存在幻觉值?"或"应用是否优雅地处理了嘈杂输入?"这样的标准,这种方法远比自动化评分有效。
|
||||
3. **手动自定义评估器**——仅用于**机械性、确定性的检查**,即程序化函数可以明确给出正确答案的场景:字段存在性、正则表达式模式匹配、JSON Schema 验证、数值阈值、类型检查。**切勿将手动自定义评估器用于语义质量评估**——如果检查需要对内容是否正确、相关或完整做出**判断**,则应改用 Agent 评估器。
|
||||
|
||||
**区分结构性标准与语义性标准**:对于每个标准,问自己:"能否通过一个简单的程序化规则来检查,并且该规则总能给出正确答案?"如果能 → 使用手动自定义评估器。如果不能 → 使用 Agent 评估器。大多数应用特定的质量标准是语义性的,而非结构性的。
|
||||
|
||||
对于开放式 LLM 文本,**切勿**使用 `ExactMatch`——LLM 输出是非确定性的。
|
||||
|
||||
`AnswerRelevancy` 是**仅限 RAG** 的——它需要在追踪记录中存在 `context` 值。缺少该值时返回 0.0。对于一般的相关性,请使用带有明确标准的 Agent 评估器。
|
||||
|
||||
## 3b. 实现自定义评估器
|
||||
|
||||
如果任何标准需要自定义评估器,请立即实现。将自定义评估器放置在 `pixie_qa/evaluators.py` 中(如果数量较多,可放在子模块中)。
|
||||
|
||||
### Agent 评估器(`create_agent_evaluator`)——默认选择
|
||||
|
||||
对于**所有语义性、定性和基于判断的标准**,请使用 Agent 评估器。这些评估器将由你(编码 agent)在步骤 5d 中进行评分,你将在该步骤中结合完整上下文审查每个条目的追踪记录和输出——对于准确性、完整性、幻觉检测或错误处理等质量维度,这远比任何自动化方法有效。
|
||||
|
||||
```python
|
||||
from pixie import create_agent_evaluator
|
||||
|
||||
extraction_accuracy = create_agent_evaluator(
|
||||
name="ExtractionAccuracy",
|
||||
criteria="提取的数据准确反映了源内容。所有字段都包含源中的正确值——没有幻觉值、"
|
||||
"编造值或占位符值。将 final_answer 与 fetched_content "
|
||||
"和 parsed_content 进行比较,以验证每个声称的事实。",
|
||||
)
|
||||
|
||||
noise_handling = create_agent_evaluator(
|
||||
name="NoiseHandling",
|
||||
criteria="应用正确忽略了源中的导航装饰、样板文本、广告和其他非内容元素。"
|
||||
"提取的数据仅包含与用户提示相关的信息,而不包含页面结构中的噪音。",
|
||||
)
|
||||
|
||||
schema_compliance = create_agent_evaluator(
|
||||
name="SchemaCompliance",
|
||||
criteria="输出包含提示中请求的所有字段,且具有适当的类型和非空值。"
|
||||
"缺失字段、必需数据的空值或包含通用占位符文本的字段表示失败。",
|
||||
)
|
||||
```
|
||||
|
||||
在数据集中通过 `filepath:callable_name`(例如 `"pixie_qa/evaluators.py:extraction_accuracy"`)引用 Agent 评估器。
|
||||
|
||||
在 `pixie test` 运行期间,Agent 评估器在控制台中显示为 `⏳`。它们将在步骤 5d 中进行评分。
|
||||
|
||||
**编写有效的标准**:`criteria` 字符串是你在步骤 5d 中将要遵循的评分细则。请使其具体且可操作:
|
||||
|
||||
- **不好**:"检查输出是否良好"——过于模糊,难以一致评分
|
||||
- **不好**:"响应应该准确"——没有说明要与什么进行比较
|
||||
- **好**:"将提取的字段与源 HTML/文档进行比较。每个字段必须在源中有对应的段落。标记任何无法追溯到源内容的字段值。"
|
||||
- **好**:"应用应保留源文档的结构层次。如果源有章节/子章节,提取结果应反映该嵌套结构,而不是将所有内容扁平化为单一层级。"
|
||||
|
||||
### 手动自定义评估器——仅用于机械性检查
|
||||
|
||||
仅在确定性的程序化检查中使用手动自定义评估器**,即简单函数能够明确给出正确答案的场景。示例包括:字段存在性、正则匹配、JSON Schema 验证、数值范围检查、类型验证。
|
||||
|
||||
**不要将手动自定义评估器用于语义质量评估。** 如果检查需要对内容是否正确、相关、完整或文笔优美做出**判断**,请改用 Agent 评估器。判定标准:"正则表达式、字符串匹配或比较运算符能否完美实现这个检查?"如果不能,则属于语义性检查——请使用 Agent 评估器。
|
||||
|
||||
自定义评估器可以是**同步或异步函数**。将它们赋值给 `pixie_qa/evaluators.py` 中的模块级变量:
|
||||
|
||||
```python
|
||||
from pixie import Evaluation, Evaluable
|
||||
|
||||
def my_evaluator(evaluable: Evaluable, *, trace=None) -> Evaluation:
|
||||
score = 1.0 if "expected pattern" in str(evaluable.eval_output) else 0.0
|
||||
return Evaluation(score=score, reasoning="...")
|
||||
```
|
||||
|
||||
在数据集中通过 `filepath:callable_name` 引用:`"pixie_qa/evaluators.py:my_evaluator"`。
|
||||
|
||||
**访问 `eval_metadata` 和捕获的数据**:自定义评估器通过 `Evaluable` 字段访问每个条目的元数据和 `wrap()` 输出:
|
||||
|
||||
- `evaluable.eval_metadata` —— 来自条目 `eval_metadata` 字段的字典(例如 `{"expected_tool": "endCall"}`)
|
||||
- `evaluable.eval_output` —— 包含所有 `wrap(purpose="output")` 和 `wrap(purpose="state")` 值的 `list[NamedData]`。每个项包含 `.name`(str)和 `.value`(JsonValue)。使用下面的辅助函数按名称查找。
|
||||
|
||||
```python
|
||||
def _get_output(evaluable: Evaluable, name: str) -> Any:
|
||||
"""按名称从 eval_output 中查找 wrap 值。"""
|
||||
for item in evaluable.eval_output:
|
||||
if item.name == name:
|
||||
return item.value
|
||||
return None
|
||||
|
||||
def call_ended_check(evaluable: Evaluable, *, trace=None) -> Evaluation:
|
||||
expected = evaluable.eval_metadata.get("expected_call_ended") if evaluable.eval_metadata else None
|
||||
actual = _get_output(evaluable, "call_ended")
|
||||
if expected is None:
|
||||
return Evaluation(score=1.0, reasoning="eval_metadata 中无 expected_call_ended")
|
||||
match = bool(actual) == bool(expected)
|
||||
return Evaluation(
|
||||
score=1.0 if match else 0.0,
|
||||
reasoning=f"期望 call_ended={expected},实际得到 {actual}",
|
||||
)
|
||||
```
|
||||
|
||||
### ValidJSON 与字符串期望值的冲突
|
||||
|
||||
当数据集的 `expectation` 字段中存在值时,`ValidJSON` 会将其视为 JSON Schema。如果你的条目使用**字符串**期望值(例如用于 `Factuality`),将 `ValidJSON` 添加为数据集级别的默认评估器会导致失败——它无法将纯字符串验证为 JSON Schema。要么仅将 `ValidJSON` 应用于具有对象/布尔值期望值的条目,要么在数据集依赖字符串期望值时省略它。
|
||||
|
||||
## 3c. 生成评估器映射文档
|
||||
|
||||
将标准到评估器的映射写入 `pixie_qa/03-evaluator-mapping.md`。该文档连接了评估标准(步骤 1c)和数据集(步骤 4)。
|
||||
|
||||
**关键**:使用与 `evaluators.md` 参考文档中完全一致的评估器名称——内置评估器使用其短名称(例如 `Factuality`、`ClosedQA`),自定义评估器使用 `filepath:callable_name` 格式(例如 `pixie_qa/evaluators.py:ConciseVoiceStyle`)。
|
||||
|
||||
### 模板
|
||||
|
||||
```markdown
|
||||
# 评估器映射
|
||||
|
||||
## 使用的内置评估器
|
||||
|
||||
| 评估器名称 | 覆盖的标准 | 适用范围 |
|
||||
| ------------ | ---------------- | -------------------------- |
|
||||
| Factuality | 事实准确性 | 所有条目 |
|
||||
| ClosedQA | 答案正确性 | 含有 expected_output 的条目 |
|
||||
|
||||
## Agent 评估器
|
||||
|
||||
| 评估器名称 | 覆盖的标准 | 适用范围 | 源文件 |
|
||||
| -------------------------------------------- | ------------------------ | --------- | ----------------------- |
|
||||
| pixie_qa/evaluators.py:extraction_accuracy | 内容与源的准确性对照 | 所有条目 | pixie_qa/evaluators.py |
|
||||
| pixie_qa/evaluators.py:noise_handling | 导航/样板文本噪音 | 所有条目 | pixie_qa/evaluators.py |
|
||||
|
||||
## 手动自定义评估器(仅限机械性检查)
|
||||
|
||||
| 评估器名称 | 覆盖的标准 | 适用范围 | 源文件 |
|
||||
| -------------------------------------------- | ---------------- | --------- | ----------------------- |
|
||||
| pixie_qa/evaluators.py:required_fields_present | 必填字段检查 | 所有条目 | pixie_qa/evaluators.py |
|
||||
|
||||
## 适用范围总结
|
||||
|
||||
- **数据集级别默认**(适用于所有条目):Factuality、pixie_qa/evaluators.py:extraction_accuracy
|
||||
- **条目特定**(适用于子集):ClosedQA(仅适用于含有 expected_output 的条目)
|
||||
```
|
||||
|
||||
## 输出
|
||||
|
||||
- `pixie_qa/evaluators.py` 中的自定义评估器实现(如果需要任何自定义评估器)
|
||||
- `pixie_qa/03-evaluator-mapping.md`——标准到评估器的映射
|
||||
|
||||
---
|
||||
|
||||
> **评估器选择指南**:参见 `evaluators.md` 获取完整的内置评估器目录和 `create_agent_evaluator` 参考。
|
||||
>
|
||||
> **如果在实现评估器时遇到意外错误**(导入失败、API 不匹配),请先阅读 `evaluators.md` 获取权威的评估器参考,以及 `wrap-api.md` 获取 API 详细信息,然后再猜测修复方法。
|
||||
@@ -0,0 +1,368 @@
|
||||
# 第 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/<name>.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/<name>.json`——数据集文件。
|
||||
@@ -0,0 +1,85 @@
|
||||
# 步骤 5:运行 `pixie test` 并修复机械问题
|
||||
|
||||
**为什么需要这一步**:运行 `pixie test` 并修复 QA 组件中的机械问题——数据集格式问题、可运行实现中的 bug 以及自定义评估器错误——直到每一条记录都产生真实评分。这一步**不**涉及评估结果质量或修复应用程序本身。
|
||||
|
||||
---
|
||||
|
||||
## 5a. 运行测试
|
||||
|
||||
```bash
|
||||
uv run pixie test
|
||||
```
|
||||
|
||||
如需包含每条用例评分和评估器推理的详细输出:
|
||||
|
||||
```bash
|
||||
uv run pixie test -v
|
||||
```
|
||||
|
||||
`pixie test` 在运行测试前会自动加载 `.env` 文件。
|
||||
|
||||
评估框架的执行流程如下:
|
||||
|
||||
1. 从数据集的 `runnable` 字段解析出 `Runnable` 类
|
||||
2. 调用 `Runnable.create()` 构造实例,然后调用一次 `setup()`
|
||||
3. 以**并发**方式运行所有数据集条目(最多 4 个并行):
|
||||
a. 从条目中读取 `input_data` 和 `eval_input`
|
||||
b. 用 `eval_input` 数据填充 wrap 输入注册表
|
||||
c. 初始化捕获注册表
|
||||
d. 将 `input_data` 校验为 Pydantic 模型,然后调用 `Runnable.run(args)`
|
||||
e. 应用中的 `wrap(purpose="input")` 调用返回注册表中的值,而不是调用外部服务
|
||||
f. `wrap(purpose="output"/"state")` 调用捕获评估所需的数据
|
||||
g. 从捕获的数据构建 `Evaluable`
|
||||
h. 运行评估器
|
||||
4. 调用一次 `Runnable.teardown()`
|
||||
|
||||
由于条目是并发运行的,Runnable 的 `run()` 方法必须是并发安全的。如果看到 `sqlite3.OperationalError`、`"database is locked"` 或类似错误,请为 Runnable 添加一个 `Semaphore(1)`(参见步骤 2 参考文档中的并发章节)。
|
||||
|
||||
## 5b. 仅修复机械问题
|
||||
|
||||
这一步严格限于修复你在前面步骤中构建的内容——数据集、runnable 以及任何自定义评估器。你修复的是阻止流水线运行的机械问题,而非评估或改进应用程序的输出质量。
|
||||
|
||||
**哪些属于机械问题**(需要修复):
|
||||
|
||||
| 错误 | 原因 | 修复方法 |
|
||||
| ------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
||||
| `WrapRegistryMissError: name='<key>'` | 数据集条目缺少 `eval_input` 项,其中 `name` 与应用中 `wrap(purpose="input", name="<key>")` 所期望的名称不匹配 | 在每个受影响的条目的 `eval_input` 中添加缺失的 `{"name": "<key>", "value": ...}` |
|
||||
| `WrapTypeMismatchError` | 反序列化的类型与应用的期望不匹配 | 修复数据集中的值 |
|
||||
| Runnable 解析失败 | `runnable` 路径或类名错误,或该类未实现 `Runnable` 协议 | 修复数据集中的 `filepath:ClassName`;确保该类包含 `create()` 和 `run()` 方法 |
|
||||
| 导入错误 | 模块路径错误或 runnable/evaluator 中存在语法错误 | 修复引用的文件 |
|
||||
| `ModuleNotFoundError: pixie_qa` | `pixie_qa/` 目录缺少 `__init__.py` | 运行 `pixie init` 重新创建 |
|
||||
| `TypeError: ... is not callable` | 评估器名称指向一个不可调用的属性 | 评估器必须是函数、类或可调用实例 |
|
||||
| `sqlite3.OperationalError` | 并发 `run()` 调用共享同一个 SQLite 连接 | 为 Runnable 添加 `asyncio.Semaphore(1)`(参见步骤 2 的并发章节) |
|
||||
| 自定义评估器崩溃 | 自定义评估器实现中存在 bug | 修复评估器代码 |
|
||||
|
||||
**哪些不属于机械问题**(此处不要修复):
|
||||
|
||||
- 应用程序输出错误或质量低下 → 属于应用程序行为,在步骤 6 中分析
|
||||
- 评估器评分低 → 属于质量信号,在步骤 6 中分析
|
||||
- 应用内部的 LLM 调用失败 → 在步骤 6 中报告,不要模拟或绕过
|
||||
- 评估器评分在不同运行之间波动 → 正常的 LLM 非确定性,不是 bug
|
||||
|
||||
反复迭代——修复错误,重新运行,修复下一个错误——直到 `pixie test` 成功运行完毕,所有条目都产生真实的评估器评分。
|
||||
|
||||
## 输出
|
||||
|
||||
`pixie test` 成功完成后,结果会存储在按条目组织的目录结构中:
|
||||
|
||||
```
|
||||
{PIXIE_ROOT}/results/<test_id>/
|
||||
meta.json # 测试运行元数据
|
||||
dataset-{idx}/
|
||||
metadata.json # 数据集名称、路径、runnable
|
||||
entry-{idx}/
|
||||
config.json # 评估器、描述、预期结果
|
||||
eval-input.jsonl # 提供给评估器的输入数据
|
||||
eval-output.jsonl # 从应用捕获的输出数据
|
||||
evaluations.jsonl # 评估结果(已评分 + 待评分)
|
||||
trace.jsonl # LLM 调用追踪记录(如果已捕获)
|
||||
```
|
||||
|
||||
`<test_id>` 会打印在控制台输出中。你将在步骤 6 中引用该目录。
|
||||
|
||||
---
|
||||
|
||||
> **如果在运行测试时遇到意外错误**(参数名称错误、导入失败、API 不匹配),请在猜测修复方法之前先阅读 `wrap-api.md`、`evaluators.md` 或 `testing-api.md` 获取权威 API 参考。
|
||||
@@ -0,0 +1,341 @@
|
||||
# 步骤 6:分析结果
|
||||
|
||||
**为什么需要这一步**:`pixie test` 生成了原始分数。现在你要分析这些结果,理解它们意味着什么——完成待定的评估、识别模式、验证假设、并生成可执行的改进方案。分析按三个相互递进的阶段进行:条目级 → 数据集级 → 行动计划。
|
||||
|
||||
---
|
||||
|
||||
## 结果目录结构
|
||||
|
||||
执行 `pixie test` 后,结果目录结构如下:
|
||||
|
||||
```text
|
||||
{PIXIE_ROOT}/results/<test_id>/
|
||||
meta.json
|
||||
dataset-{idx}/
|
||||
metadata.json
|
||||
entry-{idx}/
|
||||
config.json # 评估器、描述、期望
|
||||
eval-input.jsonl # 输入给评估器的数据
|
||||
eval-output.jsonl # 从应用中捕获的输出数据
|
||||
evaluations.jsonl # 已评分 + 待定的评估
|
||||
trace.jsonl # LLM 调用轨迹
|
||||
```
|
||||
|
||||
读取 `meta.json` 来找到 `<test_id>`。你分析所需的所有数据都在这个目录中。
|
||||
|
||||
---
|
||||
|
||||
## 硬性完成门槛
|
||||
|
||||
你是步骤 6 的评分者。**待定评估不能移交给用户处理,Web UI 也不能替代评分。** 你可以使用 Web UI 浏览轨迹和输出,但完成的标准是在磁盘上写入文件。
|
||||
|
||||
步骤 6 只有在以下条件全部满足时才视为完成:
|
||||
|
||||
- 每个 `evaluations.jsonl` 文件中所有 `"status": "pending"` 的条目都已替换为包含 `score` 和 `reasoning` 的已评分条目。
|
||||
- 每个数据集目录都包含 `analysis.md` 和 `analysis-summary.md`。
|
||||
- 测试运行根目录包含 `action-plan.md` 和 `action-plan-summary.md`。
|
||||
- 本技能 `resources/` 目录下的验证脚本对目标结果目录通过验证。
|
||||
|
||||
**禁止的捷径**:
|
||||
|
||||
- 保留任何 `"status": "pending"` 的条目
|
||||
- 让用户在 Web UI 中审查待定的评估
|
||||
- 在顶层写一个替代文件如 `pixie_qa/06-analysis.md`
|
||||
- 写"可能通过"或"可能失败"这类表述,而不对评估进行评分并更新 `evaluations.jsonl`
|
||||
|
||||
如果你做了上述任何一条,步骤 6 都未完成。
|
||||
|
||||
## 迭代规则
|
||||
|
||||
如果你在多个修复/测试周期中迭代,每次成功的 `pixie test` 运行都会创建一个新的 `pixie_qa/results/<test_id>` 目录和新的步骤 6 义务。一旦该目录存在,它就成为当前周期的分析目标。
|
||||
|
||||
在修改应用代码、提示词、数据集、评估器或重新运行 `pixie test` 之前,先完成该结果目录的步骤 6。不要跳过前面的周期,只分析最后一次运行。
|
||||
|
||||
**额外的禁止捷径**:
|
||||
|
||||
- 不要创建一个更新的 `pixie_qa/results/<test_id>` 而将同一任务中较旧的目录留下,不生成步骤 6 产物。
|
||||
|
||||
---
|
||||
|
||||
## 编写原则
|
||||
|
||||
你生成的每个分析**详细**产物必须遵循以下原则:
|
||||
|
||||
- **数据驱动**:每一个观点或陈述必须有评估运行中的具体数据作为支撑。引用分数、引用条目索引、引用特定的评估输入/输出内容。不要空泛地描述。写没有依据的东西还不如不写。
|
||||
- **证据优先**:在得出结论之前先呈现原始数据和证据。读者(另一个编码 agent)应能根据你引用的证据独立验证你的结论。
|
||||
- **可追溯**:对于每个结论,提供完整的链条:数据来源 → 观察 → 推理 → 结论。另一个 agent 应能沿这条链条反向追溯,以验证或质疑任何主张。
|
||||
- **不推销**:不要鼓吹、美化或使用价值判断词汇("优秀的"、"健壮的"、"令人印象深刻的"、"设计良好的")。陈述数据展示了什么以及它暗示了哪些行动。让读者自己判断质量。
|
||||
- **面向行动**:每一项分析都应服务于最终目标——对评估管线或应用做出具体的改进。不要写那些不导向任何行动的观察。
|
||||
|
||||
每个持久化的分析**摘要**产物必须遵循以下原则:
|
||||
|
||||
- **简洁**:人类读者应能在不到 2 分钟内理解任一产物的关键发现和行动项。
|
||||
- **结论先行**:先告诉读者需要知道的内容(结果、发现、行动),而不是方法论或背景。
|
||||
- **通俗语言**:避免行话。非技术利益相关者应能读懂摘要。
|
||||
- **一致**:摘要的结论必须与详细版本的证据一致。绝不在摘要中添加详细版本中没有支撑的主张。
|
||||
|
||||
### 双变体模式
|
||||
|
||||
本步骤中的每个持久化分析产物都有两个文件:
|
||||
|
||||
| 产物 | 详细文件(供 agent 使用) | 摘要文件(供人类阅读) |
|
||||
| -------------- | ---------------------------- | --------------------------------- |
|
||||
| 数据集分析 | `dataset-{idx}/analysis.md` | `dataset-{idx}/analysis-summary.md` |
|
||||
| 行动计划 | `action-plan.md` | `action-plan-summary.md` |
|
||||
|
||||
**始终先写详细版本**,然后从中推导出摘要。摘要是详细版本内容的严格子集——它不应包含详细版本中没有的主张或结论。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 1:条目级评分
|
||||
|
||||
逐个处理每个数据集条目。对于每个 `dataset-{idx}/entry-{idx}/`:
|
||||
|
||||
### 1a. 读取条目数据
|
||||
|
||||
读取该条目的以下文件:
|
||||
|
||||
- `config.json` — 配置了哪些评估器、描述、期望
|
||||
- `eval-input.jsonl` — 输入给应用/评估器的数据
|
||||
- `eval-output.jsonl` — 应用产生的输出
|
||||
- `evaluations.jsonl` — 当前的评估结果(已评分和待定)
|
||||
- `trace.jsonl` — 应用进行的 LLM 调用(如果有)
|
||||
|
||||
### 1b. 完成待定评估
|
||||
|
||||
如果 `evaluations.jsonl` 包含 `"status": "pending"` 的条目,你必须对它们进行评分:
|
||||
|
||||
1. 读取待定评估的 `criteria` 字段
|
||||
2. 将评分标准应用于该条目的评估输入、评估输出和轨迹数据
|
||||
3. 给出 **score**,范围在 0.0 到 1.0 之间:
|
||||
- `1.0` — 完全满足标准
|
||||
- `0.5`–`0.9` — 部分满足标准(说明缺失了什么)
|
||||
- `0.0`–`0.4` — 不满足标准
|
||||
4. 写出 **reasoning** 字符串(1–3 句话,引用输出或轨迹中的具体证据)
|
||||
5. 将 `evaluations.jsonl` 中的待定条目替换为评分结果。**不要追加第二行而保留待定行。直接覆盖待定行本身。**
|
||||
|
||||
**修改前**(待定):
|
||||
|
||||
```json
|
||||
{
|
||||
"evaluator": "ResponseQuality",
|
||||
"status": "pending",
|
||||
"criteria": "回复应该……"
|
||||
}
|
||||
```
|
||||
|
||||
**修改后**(已评分):
|
||||
|
||||
```json
|
||||
{
|
||||
"evaluator": "ResponseQuality",
|
||||
"score": 0.85,
|
||||
"reasoning": "回复解决了主要问题,但省略了……"
|
||||
}
|
||||
```
|
||||
|
||||
**评分准则**:
|
||||
|
||||
- 基于证据——每个分数必须引用具体的输出或轨迹内容
|
||||
- 严格按照评分标准字面含义执行——不要扩展或重新解读超出已写范围的内容
|
||||
- 考虑轨迹——区分应用逻辑问题和 LLM 质量问题
|
||||
- 校准分数——保留 1.0 给那些真正完全满足标准的输出
|
||||
- 不惩罚 LLM 的非确定性——正确答案的不同措辞不是失败
|
||||
- 不推给用户——如果证据足以写出"可能通过",那就足以给出分数并更新 `evaluations.jsonl`
|
||||
|
||||
### 1c. 不持久化条目级分析文件
|
||||
|
||||
在这个精简工作流中,**不要写 `entry-{idx}/analysis.md` 或 `entry-{idx}/analysis-summary.md`**。阶段 1 仅用于读取证据并将每个待定评估转换为 `evaluations.jsonl` 中的评分行。
|
||||
|
||||
你可以在推理过程中做临时的草稿笔记,但它们不是交付物。仅持久化以下内容:
|
||||
|
||||
- 每个条目目录中更新后的 `evaluations.jsonl`
|
||||
- 阶段 2 中的数据集级分析文件
|
||||
- 阶段 3 中的运行级行动计划文件
|
||||
|
||||
---
|
||||
|
||||
## 阶段 2:数据集级分析
|
||||
|
||||
在一个数据集的所有条目分析完成后,生成数据集级分析。将 `analysis.md` 写入数据集目录(`dataset-{idx}/analysis.md`)。
|
||||
|
||||
### 2a. 聚合数据
|
||||
|
||||
汇总数据集中所有条目的情况:
|
||||
|
||||
- 通过/失败计数和总体通过率
|
||||
- 按评估器统计(通过率、最低/最高/平均分数)
|
||||
- 哪些条目在哪些评估器上失败(失败聚类)
|
||||
|
||||
### 2b. 形成并验证假设
|
||||
|
||||
从以下三个维度提出**恰好 3 个高置信度假设**:
|
||||
|
||||
1. **测试用例质量**——测试用例集是否充分且高效地验证了应用的能力?是否覆盖了重要的失败模式?是否存在盲点?
|
||||
|
||||
2. **评估标准/评估器质量**——评估器是否有足够的粒度来捕获真正的问题?是否存在橡皮图章式评估器(全部 1.0)?是否存在不稳定的评估器(没有代码变更但方差很大)?标准是否过于模糊或过于严格?
|
||||
|
||||
3. **应用质量**——基于评估结果,应用的优势和劣势是什么?它在哪些方面产生了高质量的输出?它在哪些方面失败了?
|
||||
|
||||
对于每个假设:
|
||||
|
||||
- **清晰陈述假设**,一句话说明
|
||||
- **引用证据**——条目索引、评估器名称、分数、推理引用、轨迹数据
|
||||
- **验证或否定**——查看实际的评估输入/输出数据和代码来确认或反驳
|
||||
- **结论**——这个假设暗示了哪些行动?
|
||||
|
||||
即使数据有限,也总能产生 3 个假设。如果评估数据不能对应用质量给出确定性的结论,这本身就是一个关于测试用例或评估器缺口的信号。
|
||||
|
||||
### 2c. 编写数据集分析(两个文件)
|
||||
|
||||
为数据集分析生成**两个文件**。先写详细版本,然后推导出摘要。
|
||||
|
||||
#### 详细版本:`dataset-{idx}/analysis.md`
|
||||
|
||||
此文件供 **agent 使用**——提供完整的数据聚合、带有证据链的假设形成、以及经过验证的结论,编码 agent 可以直接据此采取行动。
|
||||
|
||||
**编写原则:**
|
||||
|
||||
- **在解释数据之前先展示所有数据。** 先呈现原始聚合数据(通过/失败、按评估器统计、失败聚类),然后再提出任何假设。数据应能独立地说明问题。
|
||||
- **对于每个假设,按以下顺序呈现:数据 → 推理 → 结论。** 读者应能逐步跟随你的逻辑,并独立得出相同结论。
|
||||
- **直接交叉引用原始条目证据。** 引用证据时,引用具体的条目索引及其底层文件/数据点(例如:`entry-3/evaluations.jsonl`、`entry-3/eval-output.jsonl` 或 `entry-3/trace.jsonl`)。
|
||||
- **区分相关性与因果性。** 如果两个条目在同一个评估器上失败,这是一个模式。但根本原因可能不同——通过检查实际的输出来验证,不要做假设。
|
||||
- **如果不确定,不要不标明。** 如果结论不确定,请说"假设(未验证):……"并解释需要哪些额外数据来确认或反驳。
|
||||
|
||||
**内容:**
|
||||
|
||||
1. **概述**——数据集名称、条目数量、总体通过率
|
||||
2. **原始聚合数据**
|
||||
- 按评估器统计表(通过率、分数范围、均值、标准差)
|
||||
- 失败矩阵:条目 × 评估器,显示分数,突出显示失败
|
||||
- 失败聚类:按共同失败的评估器分组的条目
|
||||
3. **假设 1:测试用例**——假设陈述、带有条目/评估器引用的证据、采取的验证步骤、带有具体行动的结论
|
||||
4. **假设 2:评估器**——相同结构
|
||||
5. **假设 3:应用**——相同结构
|
||||
6. **未解决的问题**——数据无法给出确定性结论的问题,附上哪些额外数据会有帮助的建议
|
||||
|
||||
#### 摘要版本:`dataset-{idx}/analysis-summary.md`
|
||||
|
||||
此文件供**人工审查**——数据集结果、关键发现和推荐行动的可扫描概览。
|
||||
|
||||
**模板:**
|
||||
|
||||
```markdown
|
||||
# 数据集分析 — 摘要
|
||||
|
||||
**数据集**:<名称> | **条目数**:<N> | **通过率**:<X/N (Y%)>
|
||||
|
||||
## 结果概览
|
||||
|
||||
| 评估器 | 通过率 | 平均分 | 备注 |
|
||||
| ------ | ------ | ------ | --------------------- |
|
||||
| ... | ... | ... | <一行,如值得注意> |
|
||||
|
||||
## 关键发现
|
||||
|
||||
1. <发现>:<1–2 句话说明结论及其影响>
|
||||
2. ...
|
||||
3. ...
|
||||
|
||||
## 推荐行动(按优先级排序)
|
||||
|
||||
1. <行动>:<做什么以及预期影响,1–2 句话>
|
||||
2. ...
|
||||
3. ...
|
||||
```
|
||||
|
||||
摘要最多约 40 行。
|
||||
|
||||
---
|
||||
|
||||
## 阶段 3:行动计划(两个文件)
|
||||
|
||||
在所有数据集分析完成后,生成行动计划。在测试运行根目录写入**两个文件**。先写详细版本,然后推导出摘要。
|
||||
|
||||
### 详细版本:`{PIXIE_ROOT}/results/<test_id>/action-plan.md`
|
||||
|
||||
此文件供 **agent 使用**——提供具体的、可实施的改进项,附带完整的证据链条,使编码 agent 可以拿起任何一个改进项直接执行,无需额外收集上下文。
|
||||
|
||||
**编写原则:**
|
||||
|
||||
- **每个改进项必须自包含。** 只读一个优先级项的编码 agent 应拥有足够的上下文(证据引用、文件路径、预期变更)来实施它。
|
||||
- **每项都要追溯到证据。** 每个优先级必须引用:来自哪个数据集分析的哪个假设、哪些条目/评估器提供了证据、以及具体数据展示的内容。
|
||||
- **关于"如何做"要具体。** 不要只说"改进提示词"——要说"在 `scrapegraphai/prompts/generate_answer.py` 第 45 行,添加指令:'……'"。越具体,越可操作。
|
||||
- **不包含推测性改进项。** 每个改进项必须有经过验证的证据。如果一个改进项基于未验证的假设,要么先验证它,要么排除它。
|
||||
|
||||
**结构:**
|
||||
|
||||
```markdown
|
||||
# 行动计划(详细)
|
||||
|
||||
## 摘要
|
||||
|
||||
- X 个数据集已分析,Y 个总条目,Z% 总体通过率
|
||||
- [1–2 句话的高层评估]
|
||||
|
||||
## 优先级 1:[最 impactful 的改进]
|
||||
|
||||
- **内容**:[具体要做的变更]
|
||||
- **原因**:[来自哪个数据集分析的哪个假设,附上条目/评估器引用]
|
||||
- **证据**:[支持此改进的具体分数、输出摘录、轨迹数据]
|
||||
- **预期影响**:[哪些条目/评估器将得到改进,以及预测的分数变化]
|
||||
- **如何实现**:[具体的实施步骤,附上文件路径和行号]
|
||||
- **验证方式**:[如何验证修复是否生效——重新运行哪些条目,预期获得哪些分数]
|
||||
|
||||
## 优先级 2:...
|
||||
|
||||
...
|
||||
```
|
||||
|
||||
### 摘要版本:`{PIXIE_ROOT}/results/<test_id>/action-plan-summary.md`
|
||||
|
||||
此文件供**人工审查**——一份按优先级排序的改进清单,人类能在 2 分钟内理解并批准。
|
||||
|
||||
**模板:**
|
||||
|
||||
```markdown
|
||||
# 行动计划 — 摘要
|
||||
|
||||
**总体情况**:<X 个条目,Y% 通过率。1 句话评估。>
|
||||
|
||||
## 行动项(按优先级排序)
|
||||
|
||||
1. **<行动标题>**:<要更改什么以及原因,2–3 句话。预期影响。>
|
||||
2. **<行动标题>**:<要更改什么以及原因,2–3 句话。预期影响。>
|
||||
3. ...
|
||||
```
|
||||
|
||||
摘要最多约 30 行。
|
||||
|
||||
**优先级排序标准**:
|
||||
|
||||
- 系统性问题(影响多个条目/数据集)优先于孤立问题
|
||||
- 有清晰、经过验证的证据的问题优先于推测性问题
|
||||
- 应用质量差距优先于评估器优化,评估器优化优先于测试用例补充
|
||||
- 快速修复优先于大规模重构
|
||||
|
||||
行动计划应有 3–5 个改进项。每项必须追溯到阶段 2 中经过验证的假设。不要包含推测性或缺乏证据的改进项。
|
||||
|
||||
---
|
||||
|
||||
## 流程总结
|
||||
|
||||
1. **阶段 1**(每个条目):读取数据 → 对待定评估进行评分 → 更新 `evaluations.jsonl`
|
||||
2. **阶段 2**(每个数据集):聚合数据 → 形成 3 个假设 → 验证 → 写入 `dataset-{idx}/analysis.md` + `dataset-{idx}/analysis-summary.md`
|
||||
3. **阶段 3**(每个测试运行):综合 → 排序优先级 → 写入 `action-plan.md` + `action-plan-summary.md`
|
||||
|
||||
数据集内的条目可以并发处理(如果可用则使用子 agent)。阶段按顺序处理——阶段 2 依赖于阶段 1 的输出,阶段 3 依赖于阶段 2 的输出。
|
||||
|
||||
---
|
||||
|
||||
## 最终验证
|
||||
|
||||
在结束你的回合之前,针对你分析的具体测试运行目录,运行本技能 `resources/` 目录中与 `setup.sh` 一同提供的步骤 6 验证脚本。
|
||||
|
||||
示例形式:
|
||||
|
||||
```bash
|
||||
python /path/to/eval-driven-dev/resources/verify_step6_completion.py pixie_qa/results/<test_id>
|
||||
```
|
||||
|
||||
如果验证器报告任何错误,继续工作。在验证器通过之前,步骤 6 不算完成。
|
||||
@@ -0,0 +1,504 @@
|
||||
# 内置评估器
|
||||
|
||||
> 从 pixie 源代码文档字符串自动生成。
|
||||
> 请勿手动编辑——运行 `uv run python scripts/generate_skill_docs.py`。
|
||||
|
||||
Autoevals 适配器——封装 `autoevals` 评分器的预制评估器。
|
||||
|
||||
本模块提供 :class:`AutoevalsAdapter`,它将 autoevals 的 `Scorer` 接口桥接到 pixie 的 `Evaluator` 协议,以及一组用于常见评估任务的工厂函数。
|
||||
|
||||
公共 API(均从 `pixie.evals` 重新导出):
|
||||
|
||||
**核心适配器:** - :class:`AutoevalsAdapter` —— 任意 autoevals `Scorer` 的通用包装器。
|
||||
|
||||
**启发式评分器(无需 LLM):** - :func:`LevenshteinMatch` —— 编辑距离字符串相似度。 - :func:`ExactMatch` —— 精确值比较。 - :func:`NumericDiff` —— 归一化数值差异。 - :func:`JSONDiff` —— 结构化 JSON 比较。 - :func:`ValidJSON` —— JSON 语法/模式验证。 - :func:`ListContains` —— 两个字符串列表之间的重叠。
|
||||
|
||||
**嵌入评分器:** - :func:`EmbeddingSimilarity` —— 基于嵌入的余弦相似度。
|
||||
|
||||
**LLM 作为评判者的评分器:** - :func:`Factuality`、:func:`ClosedQA`、:func:`Battle`、
|
||||
:func:`Humor`、:func:`Security`、:func:`Sql`、
|
||||
:func:`Summary`、:func:`Translation`、:func:`Possible`。
|
||||
|
||||
**内容审核:** - :func:`Moderation` —— OpenAI 内容审核检查。
|
||||
|
||||
**RAGAS 指标:** - :func:`ContextRelevancy`、:func:`Faithfulness`、
|
||||
:func:`AnswerRelevancy`、:func:`AnswerCorrectness`。
|
||||
|
||||
## 评估器选择指南
|
||||
|
||||
根据**输出类型**和评估标准选择评估器:
|
||||
|
||||
| 输出类型 | 评估器类别 | 示例 |
|
||||
| -------------------------------------------- | ----------------------------------------------------------- | -------------------------------------- |
|
||||
| 确定性输出(标签、是/否、固定格式) | 启发式:`ExactMatch`、`JSONDiff`、`ValidJSON` | 标签分类、JSON 提取 |
|
||||
| 带有参考答案的开放式文本 | LLM 作为评判者:`Factuality`、`ClosedQA`、`AnswerCorrectness` | 聊天机器人回复、问答、摘要 |
|
||||
| 带有预期上下文/依据的文本 | RAG:`Faithfulness`、`ContextRelevancy` | RAG 流水线 |
|
||||
| 带有风格/格式要求的文本 | 通过 `create_llm_evaluator` 自定义 | 语音友好型回复、语气检查 |
|
||||
| 多维度质量 | 组合多个评估器 | 事实性 + 相关性 + 语气 |
|
||||
| 依赖追踪的质量(工具使用、路由) | 通过 `create_agent_evaluator` 创建代理评估器 | 工具正确性、多步推理质量 |
|
||||
|
||||
关键规则:
|
||||
|
||||
- 对于开放式 LLM 文本,**切勿**使用 `ExactMatch`——LLM 输出是非确定性的。
|
||||
- `AnswerRelevancy` 是**仅限 RAG** 的——需要追踪中的 `context`。没有该值则返回 0.0。对于通用相关性,请使用 `create_llm_evaluator`。
|
||||
- 不要在缺少 `expected_output` 的条目上使用比较评估器(`Factuality`、`ClosedQA`、
|
||||
`ExactMatch`)——它们会产生无意义的分数。
|
||||
|
||||
---
|
||||
|
||||
## 评估器参考
|
||||
|
||||
### `AnswerCorrectness`
|
||||
|
||||
```python
|
||||
AnswerCorrectness(*, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
答案正确性评估器(RAGAS)。
|
||||
|
||||
判断 `eval_output` 与 `expected_output` 相比是否正确,综合了事实相似度和语义相似度。
|
||||
|
||||
**使用场景**:RAG 流水线中的问答场景,当你拥有参考答案并希望获得全面的正确性分数时。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
**需要 `eval_metadata["context"]`**:可选(可提高准确性)。
|
||||
|
||||
参数:
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `AnswerRelevancy`
|
||||
|
||||
```python
|
||||
AnswerRelevancy(*, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
答案相关性评估器(RAGAS)。
|
||||
|
||||
判断 `eval_output` 是否直接回应了 `eval_input` 中的问题。
|
||||
|
||||
**使用场景**:仅限 RAG 流水线——需要追踪中的 `context`。没有该值则返回 0.0。对于通用(非 RAG)响应相关性,请改用 `create_llm_evaluator` 并编写自定义提示词。
|
||||
|
||||
**需要 `expected_output`**:否。
|
||||
**需要 `eval_metadata["context"]`**:是——**仅限 RAG 流水线**。
|
||||
|
||||
参数:
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Battle`
|
||||
|
||||
```python
|
||||
Battle(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
头对头比较评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 将 `eval_output` 与 `expected_output` 进行比较,并根据 `eval_input` 中的指令判断哪个更优。
|
||||
|
||||
**使用场景**:A/B 测试场景、比较模型输出、或对备选回复进行排序。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `ClosedQA`
|
||||
|
||||
```python
|
||||
ClosedQA(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
闭卷问答评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` 是否与 `expected_output` 相比正确回答了 `eval_input` 中的问题。可选地转发 `eval_metadata["criteria"]` 以用于自定义评分标准。
|
||||
|
||||
**使用场景**:答案应与参考答案匹配的问答场景——例如客户支持回答、知识库查询。
|
||||
|
||||
**需要 `expected_output`**:是——不要在缺少 `expected_output` 的条目上使用;会产生无意义的分数。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `ContextRelevancy`
|
||||
|
||||
```python
|
||||
ContextRelevancy(*, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
上下文相关性评估器(RAGAS)。
|
||||
|
||||
判断检索到的上下文是否与查询相关。将 `eval_metadata["context"]` 转发到底层评分器。
|
||||
|
||||
**使用场景**:RAG 流水线——评估检索质量。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
**需要 `eval_metadata["context"]`**:是(仅限 RAG 流水线)。
|
||||
|
||||
参数:
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `EmbeddingSimilarity`
|
||||
|
||||
```python
|
||||
EmbeddingSimilarity(*, prefix: 'str | None' = None, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
基于嵌入的语义相似度评估器。
|
||||
|
||||
计算 `eval_output` 和 `expected_output` 嵌入向量之间的余弦相似度。
|
||||
|
||||
**使用场景**:比较两段文本的语义含义,而不关心具体措辞。对于改写内容比 Levenshtein 更稳健,但不如 LLM 作为评判者的评估器细致。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
prefix:可选的前缀文本,用于提供领域上下文。
|
||||
model:嵌入模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `ExactMatch`
|
||||
|
||||
```python
|
||||
ExactMatch() -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
精确值比较评估器。
|
||||
|
||||
如果 `eval_output` 与 `expected_output` 完全相等,则返回 1.0,否则返回 0.0。
|
||||
|
||||
**使用场景**:确定性、结构化的输出(分类标签、是/否回答、固定格式字符串)。**切勿**用于开放式 LLM 文本——LLM 输出是非确定性的,因此精确匹配几乎总是失败。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
### `Factuality`
|
||||
|
||||
```python
|
||||
Factuality(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
事实准确性评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` 在 `eval_input` 的上下文中是否与 `expected_output` 事实一致。
|
||||
|
||||
**使用场景**:需要事实正确性的开放式文本(聊天机器人回复、问答回答、摘要)。对于 LLM 生成的文本,优先于 `ExactMatch`。
|
||||
|
||||
**需要 `expected_output`**:是——不要在缺少 `expected_output` 的条目上使用;会产生无意义的分数。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Faithfulness`
|
||||
|
||||
```python
|
||||
Faithfulness(*, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
忠实度评估器(RAGAS)。
|
||||
|
||||
判断 `eval_output` 是否忠实于(即是否由)所提供的上下文所支持。转发 `eval_metadata["context"]`。
|
||||
|
||||
**使用场景**:RAG 流水线——确保回答不会超出检索到的上下文而产生幻觉。
|
||||
|
||||
**需要 `expected_output`**:否。
|
||||
**需要 `eval_metadata["context"]`**:是(仅限 RAG 流水线)。
|
||||
|
||||
参数:
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Humor`
|
||||
|
||||
```python
|
||||
Humor(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
幽默质量评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` 相对于 `expected_output` 的幽默质量。
|
||||
|
||||
**使用场景**:评估创意写作、聊天机器人个性或娱乐应用中的幽默效果。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `JSONDiff`
|
||||
|
||||
```python
|
||||
JSONDiff(*, string_scorer: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
结构化 JSON 比较评估器。
|
||||
|
||||
递归比较两个 JSON 结构并生成相似度分数。支持嵌套对象、数组和混合类型。
|
||||
|
||||
**使用场景**:需要字段级比较的结构化 JSON 输出(例如提取的数据、API 响应模式、工具调用参数)。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
string_scorer:用于字符串字段的可选成对评分器。
|
||||
|
||||
### `LevenshteinMatch`
|
||||
|
||||
```python
|
||||
LevenshteinMatch() -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
编辑距离字符串相似度评估器。
|
||||
|
||||
计算 `eval_output` 与 `expected_output` 之间的归一化 Levenshtein 距离。完全相同返回 1.0,编辑距离越大分数越低。
|
||||
|
||||
**使用场景**:确定性或近似确定性的输出,允许较小的文本变化(例如格式差异、轻微拼写错误)。不适用于开放式 LLM 文本——请改用 LLM 作为评判者的评估器。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
### `ListContains`
|
||||
|
||||
```python
|
||||
ListContains(*, pairwise_scorer: 'Any' = None, allow_extra_entities: 'bool' = False) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
列表重叠评估器。
|
||||
|
||||
检查 `eval_output` 是否包含 `expected_output` 中的所有条目。基于重叠比例评分。
|
||||
|
||||
**使用场景**:输出为条目列表且完整性至关重要的场景(例如提取的实体、搜索结果、推荐)。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
pairwise_scorer:用于逐对元素比较的可选评分器。
|
||||
allow_extra_entities:如果为 True,输出中的多余条目不扣分。
|
||||
|
||||
### `Moderation`
|
||||
|
||||
```python
|
||||
Moderation(*, threshold: 'float | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
内容审核评估器。
|
||||
|
||||
使用 OpenAI 审核 API 检查 `eval_output` 中是否包含不安全内容(仇恨言论、暴力、自残等)。
|
||||
|
||||
**使用场景**:任何需要关注输出安全性的应用——聊天机器人、内容生成、面向用户的 AI。
|
||||
|
||||
**需要 `expected_output`**:否。
|
||||
|
||||
参数:
|
||||
threshold:自定义标记阈值。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `NumericDiff`
|
||||
|
||||
```python
|
||||
NumericDiff() -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
归一化数值差异评估器。
|
||||
|
||||
计算 `eval_output` 与 `expected_output` 之间的归一化数值距离。完全相同返回 1.0,差异越大分数越低。
|
||||
|
||||
**使用场景**:允许近似相等的数值输出(例如价格计算、分数、测量值)。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
### `Possible`
|
||||
|
||||
```python
|
||||
Possible(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
可行性/合理性评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` 是否是一个合理或可行的回答。
|
||||
|
||||
**使用场景**:通用质量检查,当你希望验证输出是否合理而不需要特定参考答案时。
|
||||
|
||||
**需要 `expected_output`**:否。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Security`
|
||||
|
||||
```python
|
||||
Security(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
安全漏洞评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 检查 `eval_output` 是否基于 `eval_input` 中的指令存在安全漏洞。
|
||||
|
||||
**使用场景**:代码生成、SQL 输出,或任何需要检查输出是否存在注入或漏洞风险的场景。
|
||||
|
||||
**需要 `expected_output`**:否。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Sql`
|
||||
|
||||
```python
|
||||
Sql(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
SQL 等价性评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` SQL 是否与 `expected_output` SQL 语义等价。
|
||||
|
||||
**使用场景**:Text-to-SQL 应用,要求生成的 SQL 在功能上与参考查询等价。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Summary`
|
||||
|
||||
```python
|
||||
Summary(*, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
摘要质量评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` 作为摘要的质量,并与 `expected_output` 中的参考摘要进行比较。
|
||||
|
||||
**使用场景**:摘要任务,要求输出必须从源材料中捕获关键信息。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `Translation`
|
||||
|
||||
```python
|
||||
Translation(*, language: 'str | None' = None, model: 'str | None' = None, client: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
翻译质量评估器(LLM 作为评判者)。
|
||||
|
||||
使用 LLM 判断 `eval_output` 与目标语言中 `expected_output` 相比的翻译质量。
|
||||
|
||||
**使用场景**:机器翻译或多语言输出场景。
|
||||
|
||||
**需要 `expected_output`**:是。
|
||||
|
||||
参数:
|
||||
language:目标语言(例如 `"Spanish"`)。
|
||||
model:LLM 模型名称。
|
||||
client:OpenAI 客户端实例。
|
||||
|
||||
### `ValidJSON`
|
||||
|
||||
```python
|
||||
ValidJSON(*, schema: 'Any' = None) -> 'AutoevalsAdapter'
|
||||
```
|
||||
|
||||
JSON 语法和模式验证评估器。
|
||||
|
||||
如果 `eval_output` 是有效 JSON(并且可选地匹配提供的模式),则返回 1.0,否则返回 0.0。
|
||||
|
||||
**使用场景**:输出必须是有效 JSON 的场景——可选择性地要求符合特定模式(例如工具调用响应、结构化提取)。
|
||||
|
||||
**需要 `expected_output`**:否。
|
||||
|
||||
参数:
|
||||
schema:用于验证的可选 JSON Schema。
|
||||
|
||||
---
|
||||
|
||||
## 自定义评估器:`create_llm_evaluator`
|
||||
|
||||
从提示词模板创建自定义 LLM 作为评判者的评估器的工厂函数。
|
||||
|
||||
用法::
|
||||
|
||||
from pixie import create_llm_evaluator
|
||||
|
||||
concise_voice_style = create_llm_evaluator(
|
||||
name="ConciseVoiceStyle",
|
||||
prompt_template="""
|
||||
你正在评估一个语音代理的回复是否简洁且适合电话沟通。
|
||||
|
||||
用户说:{eval_input}
|
||||
代理回复:{eval_output}
|
||||
预期行为:{expectation}
|
||||
|
||||
如果回复简洁(少于 3 句)、直接回答问题、并使用适合电话对话的口语化语言,则评分 1.0。
|
||||
如果回复冗长、离题、或使用了书面风格格式,则评分 0.0。
|
||||
""",
|
||||
)
|
||||
|
||||
### `create_llm_evaluator`
|
||||
|
||||
```python
|
||||
create_llm_evaluator(name: 'str', prompt_template: 'str', *, model: 'str' = 'gpt-4o-mini', client: 'Any | None' = None) -> '_LLMEvaluator'
|
||||
```
|
||||
|
||||
从提示词模板创建一个自定义的 LLM 作为评判者的评估器。
|
||||
|
||||
模板可以引用以下变量(这些变量从 :class:`~pixie.storage.evaluable.Evaluable` 字段填充):
|
||||
|
||||
- `{eval_input}` —— evaluable 的输入数据。单项列表展开为该条目的值;多项列表展开为 `name → value` 对的 JSON 字典。
|
||||
- `{eval_output}` —— evaluable 的输出数据(规则同 `eval_input`)。
|
||||
- `{expectation}` —— evaluable 的预期输出
|
||||
|
||||
参数:
|
||||
name:评估器的显示名称(显示在计分卡中)。
|
||||
prompt_template:包含 `{eval_input}`、`{eval_output}` 和/或 `{expectation}` 占位符的字符串模板。
|
||||
model:OpenAI 模型名称(默认:`gpt-4o-mini`)。
|
||||
client:可选预配置的 OpenAI 客户端实例。
|
||||
|
||||
返回:
|
||||
一个满足 `Evaluator` 协议的可调用评估器。
|
||||
|
||||
抛出:
|
||||
ValueError:如果模板使用类似 `{eval_input[key]}` 的嵌套字段访问(仅支持顶层占位符)。
|
||||
|
||||
### `create_agent_evaluator`
|
||||
|
||||
```python
|
||||
create_agent_evaluator(name: 'str', criteria: 'str') -> '_AgentEvaluator'
|
||||
```
|
||||
|
||||
创建一个将评分委托给编码代理的评估器。
|
||||
|
||||
在 `pixie test` 期间,代理评估器不会自动评分。相反,它们会抛出 `AgentEvaluationPending` 异常,并使用评估标准记录一个 `PendingEvaluation`。编码代理(由步骤 6 指导)审查每个条目的追踪和输出,然后对待处理评估进行评分。
|
||||
|
||||
**使用场景**:需要对 LLM 追踪进行全面审查的质量维度——工具调用正确性、多步推理质量、路由决策——这些场景下自动化的 LLM 作为评判者的提示词无法捕捉到细微差异。
|
||||
|
||||
**何时不该使用**:简单的文本质量检查(改用 `create_llm_evaluator`)、确定性检查(使用启发式评估器),或任何仅凭输入+输出即可评分而无需追踪上下文的评估标准。
|
||||
|
||||
参数:
|
||||
name:评估器的显示名称(计分卡中显示为 ⏳ 待处理)。
|
||||
criteria:评估标准——编码代理在审查结果时将遵循的评分说明。请具体且可操作。
|
||||
|
||||
返回:
|
||||
一个满足 `Evaluator` 协议的可调用评估器。其 `__call__` 方法抛出 `AgentEvaluationPending` 异常,而不是返回 `Evaluation`。
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
from pixie import create_agent_evaluator
|
||||
|
||||
ResponseQuality = create_agent_evaluator(
|
||||
name="ResponseQuality",
|
||||
criteria="回复直接回应用户的问题,信息准确、结构清晰。"
|
||||
"没有幻觉或离题内容。",
|
||||
)
|
||||
|
||||
ToolUsageCorrectness = create_agent_evaluator(
|
||||
name="ToolUsageCorrectness",
|
||||
criteria="应用根据用户的意图以正确顺序调用了正确的工具。"
|
||||
"没有不必要的或遗漏的工具调用。",
|
||||
)
|
||||
```
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
|
||||
# 可运行示例:CLI 应用程序
|
||||
|
||||
**当应用程序从命令行被调用时**(例如 `python -m myapp`,使用 argparse/click 的 CLI 工具)。
|
||||
|
||||
**做法**:使用 `asyncio.create_subprocess_exec` 调用 CLI 并捕获输出。
|
||||
|
||||
```python
|
||||
# pixie_qa/run_app.py
|
||||
import asyncio
|
||||
import sys
|
||||
|
||||
from pydantic import BaseModel
|
||||
import pixie
|
||||
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
query: str
|
||||
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
"""通过子进程驱动 CLI 应用程序。"""
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
sys.executable, "-m", "myapp", "--query", args.query,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(f"App 失败(退出码 {proc.returncode}):{stderr.decode()}")
|
||||
```
|
||||
|
||||
## 当 CLI 需要修补的依赖项时
|
||||
|
||||
如果 CLI 读取外部服务,请创建一个包装入口点,在运行真实 CLI 之前修补依赖项:
|
||||
|
||||
```python
|
||||
# pixie_qa/patched_app.py
|
||||
"""在运行真实 CLI 之前修补外部依赖项的入口点。"""
|
||||
import myapp.config as config
|
||||
config.redis_url = "mock://localhost"
|
||||
|
||||
from myapp.main import main
|
||||
main()
|
||||
```
|
||||
|
||||
然后将 Runnable 指向该包装器:
|
||||
|
||||
```python
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
sys.executable, "-m", "pixie_qa.patched_app", "--query", args.query,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=120)
|
||||
```
|
||||
|
||||
**注意**:对于 CLI 应用程序,`wrap(purpose="input")` 注入仅当应用程序在同一进程中运行时才有效。如果使用子进程,则可能需要通过环境变量或配置文件传递测试数据。
|
||||
@@ -0,0 +1,125 @@
|
||||
# Runnable Example: FastAPI / Web Server
|
||||
|
||||
**当应用是 Web 服务器时**(FastAPI、Flask、Starlette),需要测试完整的 HTTP 请求管道。
|
||||
|
||||
**方法**:使用 `httpx.AsyncClient` 配合 `ASGITransport`,在进程内运行 ASGI 应用。这是最快且最可靠的方式——无需子进程,无需管理端口。
|
||||
|
||||
```python
|
||||
# pixie_qa/run_app.py
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
import pixie
|
||||
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
user_message: str
|
||||
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
"""通过进程内 ASGI 传输驱动 FastAPI 应用。"""
|
||||
|
||||
_client: httpx.AsyncClient
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def setup(self) -> None:
|
||||
from myapp.main import app # 你的 FastAPI/Starlette 应用实例
|
||||
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
self._client = httpx.AsyncClient(transport=transport, base_url="http://test")
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
await self._client.post("/chat", json={"message": args.user_message})
|
||||
|
||||
async def teardown(self) -> None:
|
||||
await self._client.aclose()
|
||||
```
|
||||
|
||||
## ASGITransport 会跳过生命周期事件
|
||||
|
||||
`httpx.ASGITransport` **不会**触发 ASGI 生命周期事件(`startup` / `shutdown`)。如果应用在其生命周期中初始化了资源(数据库连接、缓存、服务客户端),则必须在 `setup()` 中手动复制该初始化逻辑:
|
||||
|
||||
```python
|
||||
async def setup(self) -> None:
|
||||
# 手动复制应用生命周期中的操作
|
||||
from myapp.db import get_connection, init_db, seed_data
|
||||
import myapp.main as app_module
|
||||
|
||||
conn = get_connection()
|
||||
init_db(conn)
|
||||
seed_data(conn)
|
||||
app_module.db_conn = conn # 设置应用期望的模块级全局变量
|
||||
|
||||
transport = httpx.ASGITransport(app=app_module.app)
|
||||
self._client = httpx.AsyncClient(transport=transport, base_url="http://test")
|
||||
|
||||
async def teardown(self) -> None:
|
||||
await self._client.aclose()
|
||||
# 清理手动初始化的资源
|
||||
import myapp.main as app_module
|
||||
if hasattr(app_module, "db_conn") and app_module.db_conn:
|
||||
app_module.db_conn.close()
|
||||
```
|
||||
|
||||
## 共享可变状态下的并发
|
||||
|
||||
如果应用使用了共享可变状态(内存 SQLite、基于文件的数据库、全局缓存),请添加信号量来串行化访问:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
_client: httpx.AsyncClient
|
||||
_sem: asyncio.Semaphore
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
inst = cls()
|
||||
inst._sem = asyncio.Semaphore(1)
|
||||
return inst
|
||||
|
||||
async def setup(self) -> None:
|
||||
from myapp.main import app
|
||||
transport = httpx.ASGITransport(app=app)
|
||||
self._client = httpx.AsyncClient(transport=transport, base_url="http://test")
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
async with self._sem:
|
||||
await self._client.post("/chat", json={"message": args.user_message})
|
||||
|
||||
async def teardown(self) -> None:
|
||||
await self._client.aclose()
|
||||
```
|
||||
|
||||
仅在需要时才使用信号量——如果应用使用以唯一 ID(call_sid、session_id)为键的按会话状态,则并发调用天然隔离,无需加锁。
|
||||
|
||||
## 备选方案:外部服务器配合 httpx
|
||||
|
||||
当应用无法直接导入时(复杂的启动流程、`__main__` 中的 `uvicorn.run()`),以子进程方式启动并通过 HTTP 访问:
|
||||
|
||||
```python
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
_client: httpx.AsyncClient
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def setup(self) -> None:
|
||||
# 假定服务器已在运行(通过 run-with-timeout.sh 启动)
|
||||
self._client = httpx.AsyncClient(base_url="http://localhost:8000")
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
await self._client.post("/chat", json={"message": args.user_message})
|
||||
|
||||
async def teardown(self) -> None:
|
||||
await self._client.aclose()
|
||||
```
|
||||
|
||||
在运行 `pixie trace` 或 `pixie test` 之前启动服务器:
|
||||
|
||||
```bash
|
||||
bash resources/run-with-timeout.sh 120 uv run python -m myapp.server
|
||||
sleep 3 # 等待就绪
|
||||
@@ -0,0 +1,59 @@
|
||||
# 可运行示例:独立函数(无服务器)
|
||||
|
||||
**当应用是一个纯 Python 函数或模块时**——没有 Web 框架,没有服务器,没有基础设施。
|
||||
|
||||
**方法**:直接从 `run()` 中导入并调用该函数。这是最简单的情况。
|
||||
|
||||
```python
|
||||
# pixie_qa/run_app.py
|
||||
from pydantic import BaseModel
|
||||
import pixie
|
||||
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
question: str
|
||||
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
"""驱动一个独立函数,用于追踪和评估。"""
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
from myapp.agent import answer_question
|
||||
await answer_question(args.question)
|
||||
```
|
||||
|
||||
如果函数是同步的,用 `asyncio.to_thread` 包裹它:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
from myapp.agent import answer_question
|
||||
await asyncio.to_thread(answer_question, args.question)
|
||||
```
|
||||
|
||||
如果函数依赖外部服务(例如向量存储),你在步骤 2a 中添加的 `wrap(purpose="input")` 调用会自动处理——注册中心会在评估模式下注入测试数据。
|
||||
|
||||
### 何时使用 `setup()` / `teardown()`
|
||||
|
||||
大多数独立函数不需要生命周期方法。仅当函数需要共享资源(例如预加载的嵌入模型、数据库连接)时才使用它们:
|
||||
|
||||
```python
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
_model: SomeModel
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def setup(self) -> None:
|
||||
from myapp.models import load_model
|
||||
self._model = load_model()
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
from myapp.agent import answer_question
|
||||
await answer_question(args.question, model=self._model)
|
||||
@@ -0,0 +1,339 @@
|
||||
# 测试 API 参考
|
||||
|
||||
> 自动从 pixie 源代码文档字符串生成。
|
||||
> 请勿手动编辑——请运行 `uv run python scripts/generate_skill_docs.py`。
|
||||
|
||||
pixie.evals —— LLM 应用程序的评估框架。
|
||||
|
||||
公开 API: - `Evaluation` —— 单次评估器运行的结果数据类 - `Evaluator` —— 评估可调用对象的协议 - `evaluate` —— 针对一个可评估对象运行一个评估器 - `run_and_evaluate` —— 评估来自 MemoryTraceHandler 的跨度 - `assert_pass` —— 带通过/失败条件的批量评估 - `assert_dataset_pass` —— 加载数据集并运行 assert_pass - `EvalAssertionError` —— 当 assert_pass 失败时抛出 - `capture_traces` —— 用于内存中追踪捕获的上下文管理器 - `MemoryTraceHandler` —— 收集跨度的 InstrumentationHandler - `ScoreThreshold` —— 可配置的通过标准 - `last_llm_call` / `root` —— 追踪到可评估对象的辅助函数 - `DatasetEntryResult` —— 单个数据集条目的评估结果 - `DatasetScorecard` —— 带非统一评估器的按数据集评分卡 - `generate_dataset_scorecard_html` —— 将评分卡渲染为 HTML - `save_dataset_scorecard` —— 将评分卡 HTML 写入磁盘
|
||||
|
||||
预置评估器(autoevals 适配器): - `AutoevalsAdapter` —— 任意 autoevals `Scorer` 的通用包装器 - `LevenshteinMatch` —— 编辑距离字符串相似度 - `ExactMatch` —— 精确值比较 - `NumericDiff` —— 归一化数值差异 - `JSONDiff` —— 结构化 JSON 比较 - `ValidJSON` —— JSON 语法 / Schema 验证 - `ListContains` —— 列表重叠 - `EmbeddingSimilarity` —— 嵌入余弦相似度 - `Factuality` —— LLM 事实准确性检查 - `ClosedQA` —— 闭卷问答评估 - `Battle` —— 两两对比 - `Humor` —— 幽默检测 - `Security` —— 安全漏洞检查 - `Sql` —— SQL 等价性 - `Summary` —— 摘要质量 - `Translation` —— 翻译质量 - `Possible` —— 可行性检查 - `Moderation` —— 内容审核 - `ContextRelevancy` —— RAGAS 上下文相关性 - `Faithfulness` —— RAGAS 忠实度 - `AnswerRelevancy` —— RAGAS 答案相关性 - `AnswerCorrectness` —— RAGAS 答案正确性
|
||||
|
||||
## 数据集 JSON 格式
|
||||
|
||||
数据集是一个 JSON 对象,包含以下顶层字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "customer-faq",
|
||||
"runnable": "pixie_qa/run_app.py:AppRunnable",
|
||||
"evaluators": ["Factuality"],
|
||||
"entries": [
|
||||
{
|
||||
"input_data": { "question": "Hello" },
|
||||
"description": "Basic greeting",
|
||||
"eval_input": [{ "name": "input", "value": "Hello" }],
|
||||
"expectation": "A friendly greeting that offers to help",
|
||||
"evaluators": ["...", "ClosedQA"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 条目结构
|
||||
|
||||
所有字段均在每个条目上作为顶层字段(扁平结构——无嵌套):
|
||||
|
||||
```
|
||||
entry:
|
||||
├── input_data (必需)—— Runnable.run() 的参数
|
||||
├── eval_input (可选)—— {"name": ..., "value": ...} 对象的列表(默认:[])
|
||||
├── description (必需)——测试用例的人类可读标签
|
||||
├── expectation (可选)——用于基于比较的评估器的参考值
|
||||
├── eval_metadata (可选)——用于自定义评估器的额外逐条目数据
|
||||
└── evaluators (可选)——此条目的评估器名称
|
||||
```
|
||||
|
||||
### 字段参考
|
||||
|
||||
- `runnable`(必需):驱动评估期间应用的 `Runnable` 子类的 `filepath:ClassName` 引用。
|
||||
- `evaluators`(数据集级别,可选):默认评估器名称——应用于每个未声明自己的 `evaluators` 的条目。
|
||||
- `entries[].input_data`(必需):作为 Pydantic 模型传递给 `Runnable.run()` 的关键字参数。键必须与 `run(args: T)` 中使用的 Pydantic 模型的字段匹配。
|
||||
- `entries[].description`(必需):测试用例的人类可读标签。
|
||||
- `entries[].eval_input`(可选,默认 `[]`):`{"name": ..., "value": ...}` 对象的列表。用于填充包装输入注册表——应用中的 `wrap(purpose="input")` 调用返回按 `name` 键控的注册表值。运行器在构建 `Evaluable` 时会自动前置 `input_data`。
|
||||
- `entries[].expectation`(可选):针对基于比较的评估器的简洁预期描述。应描述正确输出的样子,**而不是**逐字复制输出。对追踪使用 `pixie format` 以查看实际输出形状,然后编写更短的描述。
|
||||
- `entries[].eval_metadata`(可选):用于自定义评估器的额外逐条目数据——例如,预期的工具名称、布尔标志、阈值。在评估器中通过 `evaluable.eval_metadata` 访问。
|
||||
- `entries[].evaluators`(可选):行级别的评估器覆盖。规则:
|
||||
- 省略 → 条目继承数据集级别的 `evaluators`。
|
||||
- `["...", "ClosedQA"]` → 数据集默认值 **加上** ClosedQA。
|
||||
- `["OnlyThis"]`(无 `"..."`)→ **仅** OnlyThis,不包含默认值。
|
||||
|
||||
## 评估器名称解析
|
||||
|
||||
在数据集 JSON 中,评估器名称按如下方式解析:
|
||||
|
||||
- **内置名称**(裸名称,如 `"Factuality"`、`"ExactMatch"`)会自动解析为 `pixie.{Name}`。
|
||||
- **自定义评估器**使用 `filepath:callable_name` 格式(例如 `"pixie_qa/evaluators.py:my_evaluator"`)。
|
||||
- 自定义评估器引用指向模块级别的可调用对象——类(自动实例化)、工厂函数(如果零参数则调用)、评估器函数(按原样使用)或预实例化的可调用对象(例如 `create_llm_evaluator` 的结果——按原样使用)。
|
||||
|
||||
## CLI 命令
|
||||
|
||||
| 命令 | 描述 |
|
||||
| ------------------------------------------- | ----------------------------- |
|
||||
| `pixie test [path] [-v] [--no-open]` | 对数据集文件运行评估测试 |
|
||||
| `pixie dataset create <name>` | 创建一个新的空数据集 |
|
||||
| `pixie dataset list` | 列出所有数据集 |
|
||||
| `pixie dataset save <name> [--select MODE]` | 将跨度保存到数据集中 |
|
||||
| `pixie dataset validate [path]` | 验证数据集 JSON 文件 |
|
||||
| `pixie analyze <test_run_id>` | 生成分析和建议 |
|
||||
|
||||
---
|
||||
|
||||
## 类型
|
||||
|
||||
### `Evaluable`
|
||||
|
||||
```python
|
||||
class Evaluable(TestCase):
|
||||
eval_output: list[NamedData] # wrap(purpose="output") + wrap(purpose="state") 值
|
||||
# 继承自 TestCase:
|
||||
# eval_input: list[NamedData] # 来自数据集条目中的 eval_input
|
||||
# expectation: JsonValue | _Unset # 来自数据集条目中的 expectation
|
||||
# eval_metadata: dict[str, JsonValue] | None # 来自数据集条目中的 eval_metadata
|
||||
# description: str | None
|
||||
```
|
||||
|
||||
评估器的数据载体。使用实际输出扩展 `TestCase`。
|
||||
|
||||
- `eval_input` —— 从条目的 `eval_input` 字段加上 `input_data`(由运行器前置)填充的 `list[NamedData]`。始终至少包含一个条目。
|
||||
- `eval_output` —— 包含运行期间捕获的所有 `wrap(purpose="output")` 和 `wrap(purpose="state")` 值的 `list[NamedData]`。每个条目都有 `.name`(str)和 `.value`(JsonValue)。使用 `_get_output(evaluable, "name")` 按名称查找。
|
||||
- `eval_metadata` —— 来自条目 `eval_metadata` 字段的 `dict[str, JsonValue] | None`
|
||||
- `expected_output` —— 来自数据集的预期文本(如果未提供则为 `UNSET`)
|
||||
|
||||
属性:
|
||||
eval_input:命名输入数据项(来自数据集 + 运行器前置的 input_data)。始终非空。
|
||||
eval_output:命名输出数据项(来自运行期间的 wrap 调用)。
|
||||
每个条目都有 `.name`(str)和 `.value`(JsonValue)。
|
||||
包含所有 `wrap(purpose="output")` 和 `wrap(purpose="state")` 值。
|
||||
eval_metadata:补充元数据(不存在时为 `None`)。
|
||||
expected_output:用于评估的预期/参考输出。
|
||||
默认为 `UNSET`(未提供)。可被显式
|
||||
设置为 `None`,表示"没有预期输出"。
|
||||
|
||||
### 测试时 `wrap()` 如何映射到 `Evaluable` 字段
|
||||
|
||||
当 `pixie test` 运行数据集条目时,应用中的 `wrap()` 调用会填充评估器接收的 `Evaluable`:
|
||||
|
||||
| 应用代码中的 `wrap()` 调用 | Evaluable 字段 | 类型 | 在评估器中的访问方式 |
|
||||
| --------------------------------------------- | --------------- | ----------------- | --------------------------------------------------------- |
|
||||
| `wrap(data, purpose="input", name="X")` | `eval_input` | `list[NamedData]` | 从数据集条目的 `eval_input` 预填充 |
|
||||
| `wrap(data, purpose="output", name="X")` | `eval_output` | `list[NamedData]` | `_get_output(evaluable, "X")` —— 参见下面的辅助函数 |
|
||||
| `wrap(data, purpose="state", name="X")` | `eval_output` | `list[NamedData]` | `_get_output(evaluable, "X")` —— 与输出相同的列表 |
|
||||
| (来自数据集条目 `expectation`) | `expected_output` | `str \| None` | `evaluable.expected_output` |
|
||||
| (来自数据集条目 `eval_metadata`) | `eval_metadata` | `dict \| None` | `evaluable.eval_metadata` |
|
||||
|
||||
**关键理解**:`purpose="output"` 和 `purpose="state"` 的 wrap 值最终都作为 `NamedData` 条目进入 `eval_output`。没有单独的 `captured_output` 或 `captured_state` 字典。使用下面的辅助函数按 wrap 名称查找值:
|
||||
|
||||
```python
|
||||
def _get_output(evaluable: Evaluable, name: str) -> Any:
|
||||
"""Look up a wrap value by name from eval_output."""
|
||||
for item in evaluable.eval_output:
|
||||
if item.name == name:
|
||||
return item.value
|
||||
return None
|
||||
```
|
||||
|
||||
**`eval_metadata`** 用于向评估器传递额外的逐条目数据,这些数据既不是输入数据也不是输出——例如,预期的工具名称、布尔标志、阈值。定义为条目的顶层字段,通过 `evaluable.eval_metadata` 访问。
|
||||
|
||||
**完整的自定义评估器示例**(工具调用检查 + 数据集条目):
|
||||
|
||||
```python
|
||||
from pixie import Evaluation, Evaluable
|
||||
|
||||
def _get_output(evaluable: Evaluable, name: str) -> Any:
|
||||
"""Look up a wrap value by name from eval_output."""
|
||||
for item in evaluable.eval_output:
|
||||
if item.name == name:
|
||||
return item.value
|
||||
return None
|
||||
|
||||
def tool_call_check(evaluable: Evaluable, *, trace=None) -> Evaluation:
|
||||
expected = evaluable.eval_metadata.get("expected_tool") if evaluable.eval_metadata else None
|
||||
actual = _get_output(evaluable, "function_called")
|
||||
if expected is None:
|
||||
return Evaluation(score=1.0, reasoning="No expected_tool specified")
|
||||
match = str(actual) == str(expected)
|
||||
return Evaluation(
|
||||
score=1.0 if match else 0.0,
|
||||
reasoning=f"Expected {expected}, got {actual}",
|
||||
)
|
||||
```
|
||||
|
||||
对应的数据集条目:
|
||||
|
||||
```json
|
||||
{
|
||||
"input_data": { "user_message": "I want to end this call" },
|
||||
"description": "User requests call end after failed verification",
|
||||
"eval_input": [{ "name": "user_input", "value": "I want to end this call" }],
|
||||
"expectation": "Agent should call endCall tool",
|
||||
"eval_metadata": {
|
||||
"expected_tool": "endCall",
|
||||
"expected_call_ended": true
|
||||
},
|
||||
"evaluators": ["...", "pixie_qa/evaluators.py:tool_call_check"]
|
||||
}
|
||||
```
|
||||
|
||||
### `Evaluation`
|
||||
|
||||
```python
|
||||
Evaluation(score: 'float', reasoning: 'str', details: 'dict[str, Any]' = <factory>) -> None
|
||||
```
|
||||
|
||||
单个评估器应用于单个测试用例的结果。
|
||||
|
||||
属性:
|
||||
score:介于 0.0 和 1.0 之间的评估分数。
|
||||
reasoning:人类可读的解释(必需)。
|
||||
details:任意的 JSON 可序列化元数据。
|
||||
|
||||
### `ScoreThreshold`
|
||||
|
||||
```python
|
||||
ScoreThreshold(threshold: 'float' = 0.5, pct: 'float' = 1.0) -> None
|
||||
```
|
||||
|
||||
通过标准:在所有评估器上,_pct_ 比例的输入必须得分 >= _threshold_。
|
||||
|
||||
属性:
|
||||
threshold:单次评估必须达到的最低分数。
|
||||
pct:必须通过的测试用例输入比例(0.0–1.0)。
|
||||
|
||||
## 评估函数
|
||||
|
||||
### `pixie.run_and_evaluate`
|
||||
|
||||
```python
|
||||
pixie.run_and_evaluate(evaluator: 'Callable[..., Any]', runnable: 'Callable[..., Any]', eval_input: 'Any', *, expected_output: 'Any' = <object object at 0x7788c2ad5c80>, from_trace: 'Callable[[list[ObservationNode]], Evaluable] | None' = None) -> 'Evaluation'
|
||||
```
|
||||
|
||||
在捕获追踪的同时运行 _runnable(eval_input)_,然后进行评估。
|
||||
|
||||
结合了 `_run_and_capture` 和 `evaluate` 的便捷包装器。
|
||||
runnable 仅被调用一次。
|
||||
|
||||
参数:
|
||||
evaluator:评估器可调用对象(同步或异步)。
|
||||
runnable:要测试的应用函数。
|
||||
eval_input:传递给 _runnable_ 的单个输入。
|
||||
expected_output:合并到 evaluable 中的可选预期值。
|
||||
from_trace:用于从追踪树中选择特定跨度进行评估的可选可调用对象。
|
||||
|
||||
返回:
|
||||
`Evaluation` 结果。
|
||||
|
||||
抛出:
|
||||
ValueError:如果执行期间未捕获到任何跨度。
|
||||
|
||||
### `pixie.assert_pass`
|
||||
|
||||
```python
|
||||
pixie.assert_pass(runnable: 'Callable[..., Any]', eval_inputs: 'list[Any]', evaluators: 'list[Callable[..., Any]]', *, evaluables: 'list[Evaluable] | None' = None, pass_criteria: 'Callable[[list[list[Evaluation]]], tuple[bool, str]] | None' = None, from_trace: 'Callable[[list[ObservationNode]], Evaluable] | None' = None) -> 'None'
|
||||
```
|
||||
|
||||
在多个输入上对 runnable 运行评估器。
|
||||
|
||||
对于每个输入,通过 `_run_and_capture` 运行一次 runnable,
|
||||
然后通过 `asyncio.gather` 并发地使用每个评估器进行评估。
|
||||
|
||||
结果矩阵的形状为 `[eval_inputs][evaluators]`。
|
||||
如果未满足通过标准,则抛出携带该矩阵的 :class:`EvalAssertionError`。
|
||||
|
||||
当提供了 `evaluables` 时,行为取决于每个条目是否已填充了 `eval_output`:
|
||||
|
||||
- **eval_output 为 None** —— 通过 `run_and_evaluate` 调用 `runnable` 以从追踪生成输出,并且来自 evaluable 的 `expected_output` 会合并到结果中。
|
||||
- **eval_output 不为 None** —— 直接使用该 evaluable(该条目不调用 runnable)。
|
||||
|
||||
参数:
|
||||
runnable:要测试的应用函数。
|
||||
eval_inputs:输入列表,每个都传递给 _runnable_。
|
||||
evaluators:评估器可调用对象列表。
|
||||
evaluables:可选的 `Evaluable` 项列表,每个输入对应一个。
|
||||
当提供时,它们的 `expected_output` 会被转发到
|
||||
`run_and_evaluate`。必须与 _eval_inputs_ 具有相同的长度。
|
||||
pass_criteria:接收结果矩阵,返回
|
||||
`(passed, message)`。默认为 `ScoreThreshold()`。
|
||||
from_trace:可选的跨度选择器,转发到
|
||||
`run_and_evaluate`。
|
||||
|
||||
抛出:
|
||||
EvalAssertionError:当未满足通过标准时。
|
||||
ValueError:当 _evaluables_ 长度与 _eval_inputs_ 不匹配时。
|
||||
|
||||
### `pixie.assert_dataset_pass`
|
||||
|
||||
```python
|
||||
pixie.assert_dataset_pass(runnable: 'Callable[..., Any]', dataset_name: 'str', evaluators: 'list[Callable[..., Any]]', *, dataset_dir: 'str | None' = None, pass_criteria: 'Callable[[list[list[Evaluation]]], tuple[bool, str]] | None' = None, from_trace: 'Callable[[list[ObservationNode]], Evaluable] | None' = None) -> 'None'
|
||||
```
|
||||
|
||||
按名称加载数据集,然后对其条目运行 `assert_pass`。
|
||||
|
||||
这是一个便捷包装器,它:
|
||||
|
||||
1. 从 `DatasetStore` 加载数据集。
|
||||
2. 从每个条目中提取 `eval_input` 作为 runnable 输入。
|
||||
3. 使用完整的 `Evaluable` 项(它们携带 `expected_output`)
|
||||
作为评估对象。
|
||||
4. 委托给 `assert_pass`。
|
||||
|
||||
参数:
|
||||
runnable:要测试的应用函数。
|
||||
dataset_name:要加载的数据集的名称。
|
||||
evaluators:评估器可调用对象列表。
|
||||
dataset_dir:数据集存储的覆盖目录。
|
||||
当为 `None` 时,从 `PixieConfig.dataset_dir` 读取。
|
||||
pass_criteria:接收结果矩阵,返回
|
||||
`(passed, message)`。
|
||||
from_trace:可选的跨度选择器,转发到
|
||||
`assert_pass`。
|
||||
|
||||
抛出:
|
||||
FileNotFoundError:如果不存在名为 _dataset_name_ 的数据集。
|
||||
EvalAssertionError:当未满足通过标准时。
|
||||
|
||||
## 追踪辅助函数
|
||||
|
||||
### `pixie.last_llm_call`
|
||||
|
||||
```python
|
||||
pixie.last_llm_call(trace: 'list[ObservationNode]') -> 'Evaluable'
|
||||
```
|
||||
|
||||
在追踪树中找到具有最新 `ended_at` 的 `LLMSpan`。
|
||||
|
||||
参数:
|
||||
trace:追踪树(根 `ObservationNode` 实例的列表)。
|
||||
|
||||
返回:
|
||||
包装最近结束的 `LLMSpan` 的 `Evaluable`。
|
||||
|
||||
抛出:
|
||||
ValueError:如果追踪中不存在 `LLMSpan`。
|
||||
|
||||
### `pixie.root`
|
||||
|
||||
```python
|
||||
pixie.root(trace: 'list[ObservationNode]') -> 'Evaluable'
|
||||
```
|
||||
|
||||
将第一个根节点的跨度作为 `Evaluable` 返回。
|
||||
|
||||
参数:
|
||||
trace:追踪树(根 `ObservationNode` 实例的列表)。
|
||||
|
||||
返回:
|
||||
包装第一个根节点跨度的 `Evaluable`。
|
||||
|
||||
抛出:
|
||||
ValueError:如果追踪为空。
|
||||
|
||||
### `pixie.capture_traces`
|
||||
|
||||
```python
|
||||
pixie.capture_traces() -> 'Generator[MemoryTraceHandler, None, None]'
|
||||
```
|
||||
|
||||
安装 `MemoryTraceHandler` 并返回它的上下文管理器。
|
||||
|
||||
调用 `init()`(如果已初始化则为空操作),然后通过 `add_handler()` 注册该处理器。退出时,处理器被移除,交付队列被刷新,以便所有跨度在 `handler.spans` 上可用。
|
||||
@@ -0,0 +1,221 @@
|
||||
# Wrap API 参考文档
|
||||
|
||||
> 自动从 pixie 源代码文档字符串生成。
|
||||
> 请勿手动编辑——运行 `uv run python scripts/generate_skill_docs.py`。
|
||||
|
||||
`pixie.wrap` —— 面向数据的观测 API。
|
||||
|
||||
`wrap()` 在处理管道的命名点观测一个数据值或可调用对象。其行为取决于当前模式:
|
||||
|
||||
- **无操作模式**(追踪已禁用,无 eval 注册表):直接返回 `data`,不做任何改变。
|
||||
- **追踪模式**(在 `pixie trace` 期间):写入追踪文件并发出一个 OTel 事件(如果 span 处于活动状态则通过 span event,否则通过 OTel logger),然后返回 `data` 不做改变(或者包装一个可调用对象,使得事件在调用时触发)。
|
||||
- **评估模式**(eval 注册表已激活):为 `purpose="input"` 注入依赖数据,为 `purpose="output"` / `purpose="state"` 捕获输出/状态。
|
||||
|
||||
---
|
||||
|
||||
## CLI 命令
|
||||
|
||||
| 命令 | 描述 |
|
||||
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `pixie trace --runnable <filepath:ClassName> --input <kwargs.json> --output <file.jsonl>` | 使用 JSON 文件中的 kwargs 运行一次 Runnable 并写入追踪文件。`--input` 是一个**文件路径**(非内联 JSON)。 |
|
||||
| `pixie format --input <trace.jsonl> --output <dataset_entry.json>` | 将追踪文件转换为格式化数据集条目模板。展示 `input_data`、`eval_input` 和 `eval_output`(实际捕获的输出)。 |
|
||||
| `pixie trace filter <file.jsonl> --purpose input` | 仅打印匹配指定 purpose 的 wrap 事件。每个匹配事件输出一行 JSON。 |
|
||||
|
||||
---
|
||||
|
||||
## 类
|
||||
|
||||
### `pixie.Runnable`
|
||||
|
||||
```python
|
||||
class pixie.Runnable(Protocol[T]):
|
||||
@classmethod
|
||||
def create(cls) -> Runnable[Any]: ...
|
||||
async def setup(self) -> None: ...
|
||||
async def run(self, args: T) -> None: ...
|
||||
async def teardown(self) -> None: ...
|
||||
```
|
||||
|
||||
供评估框架使用的结构化 Runnable 协议。`T` 是 `pydantic.BaseModel` 的子类,其字段与数据集 JSON 中的 `input_data` 键对应。
|
||||
|
||||
生命周期:
|
||||
|
||||
1. `create()` —— 类方法,用于构造并返回一个 runnable 实例。
|
||||
2. `setup()` —— **异步**,在首次 `run()` 调用之前**仅调用一次**。在此初始化共享资源(例如 `TestClient`、数据库连接)。可选——具有默认的空操作实现。
|
||||
3. `run(args)` —— **异步**,对**每个数据集条目并发调用**(最多并行 4 个条目)。`args` 是根据 `input_data` 构建的经过验证的 Pydantic 模型。调用应用程序的真实入口点。
|
||||
4. `teardown()` —— **异步**,在最后一次 `run()` 调用之后**仅调用一次**。释放在 `setup()` 中获取的任何资源。可选——具有默认的空操作实现。
|
||||
|
||||
`setup()` 和 `teardown()` 具有默认的空操作实现;仅在需要共享资源时才需重写它们。
|
||||
|
||||
**并发性**:`run()` 通过 `asyncio.gather` 并发调用。你的实现**必须是并发安全的**。如果它使用了共享的可变状态(例如 SQLite 连接、内存缓存、文件句柄),请使用 `asyncio.Semaphore` 或 `asyncio.Lock` 进行保护:
|
||||
|
||||
```python
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
_sem: asyncio.Semaphore
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
inst = cls()
|
||||
inst._sem = asyncio.Semaphore(1) # serialise DB access
|
||||
return inst
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
async with self._sem:
|
||||
await call_app(args.message)
|
||||
```
|
||||
|
||||
常见的并发陷阱:
|
||||
|
||||
- **SQLite**:对并发写入不安全——使用 `Semaphore(1)` 或启用 WAL 模式的 `aiosqlite`。
|
||||
- **全局可变状态**:在 `run()` 中修改的模块级 dict/list 需要保护。
|
||||
- **速率受限的 API**:添加信号量以避免 429 错误。
|
||||
|
||||
**导入解析**:项目根目录(即调用 `pixie test` / `pixie trace` 的目录)会在加载 runnable 和 evaluator 之前自动添加到 `sys.path` 中。这意味着你的 runnable 可以使用常规的 `import` 语句来引用项目模块(例如 `from app import service`)。
|
||||
|
||||
**示例**:
|
||||
|
||||
```python
|
||||
# pixie_qa/run_app.py
|
||||
from pydantic import BaseModel
|
||||
import pixie
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
user_message: str
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
from myapp import handle_request
|
||||
await handle_request(args.user_message)
|
||||
```
|
||||
|
||||
**Web 服务器示例**(使用异步 HTTP 客户端):
|
||||
|
||||
```python
|
||||
import httpx
|
||||
from pydantic import BaseModel
|
||||
import pixie
|
||||
|
||||
class AppArgs(BaseModel):
|
||||
user_message: str
|
||||
|
||||
class AppRunnable(pixie.Runnable[AppArgs]):
|
||||
_client: httpx.AsyncClient
|
||||
|
||||
@classmethod
|
||||
def create(cls) -> "AppRunnable":
|
||||
return cls()
|
||||
|
||||
async def setup(self) -> None:
|
||||
self._client = httpx.AsyncClient(base_url="http://localhost:8000")
|
||||
|
||||
async def run(self, args: AppArgs) -> None:
|
||||
await self._client.post("/chat", json={"message": args.user_message})
|
||||
|
||||
async def teardown(self) -> None:
|
||||
await self._client.aclose()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 函数
|
||||
|
||||
### `pixie.wrap`
|
||||
|
||||
```python
|
||||
pixie.wrap(data: 'T', *, purpose: "Literal['input', 'output', 'state']", name: 'str', description: 'str | None' = None) -> 'T'
|
||||
```
|
||||
|
||||
在处理管道的某个点观测一个数据值或数据提供者可调用对象。
|
||||
|
||||
`data` 可以是一个纯值,也可以是一个产生值的可调用对象。两种情况下返回类型均为 `T` —— 在无操作模式或追踪模式下,调用者取回与传入时完全相同的类型。
|
||||
|
||||
在评估模式下且 `purpose="input"` 时,返回值(或可调用对象)会被替换为反序列化后的注册表值。当 `data` 为可调用对象时,返回的包装器会忽略原始函数,每次调用都返回注入的值;在所有其他模式下,返回的可调用对象会包装原始函数并添加追踪或捕获行为。
|
||||
|
||||
参数:
|
||||
data:一个数据值或数据提供者可调用对象。
|
||||
purpose:数据点的分类:- "input":来自外部依赖的数据(数据库记录、API 响应)- "output":发往外部系统或用户的数据 - "state":用于评估的中间状态(路由决策等)
|
||||
name:此数据点的唯一标识符。用作 eval 注册表中的键以及追踪日志中的键。
|
||||
description:可选的人类可读描述,说明此数据是什么。
|
||||
|
||||
返回:
|
||||
原始数据不做改变(追踪/无操作模式),或注册表值(评估模式下且 purpose="input")。当 `data` 为可调用对象时,返回值也是可调用的。
|
||||
|
||||
---
|
||||
|
||||
## 错误类型
|
||||
|
||||
### `WrapRegistryMissError`
|
||||
|
||||
```python
|
||||
WrapRegistryMissError(name: 'str') -> 'None'
|
||||
```
|
||||
|
||||
当在 eval 注册表中未找到 wrap(purpose="input") 的名称时抛出。
|
||||
|
||||
### `WrapTypeMismatchError`
|
||||
|
||||
```python
|
||||
WrapTypeMismatchError(name: 'str', expected_type: 'type', actual_type: 'type') -> 'None'
|
||||
```
|
||||
|
||||
当反序列化后的注册表值与期望类型不匹配时抛出。
|
||||
|
||||
---
|
||||
|
||||
## 追踪文件工具函数
|
||||
|
||||
用于 wrap 日志条目的 Pydantic 模型及 JSONL 加载工具函数。
|
||||
|
||||
`WrapLogEntry` 是记录在 JSONL 追踪文件中的单个 `wrap()` 事件的类型化表示。代码库中有多个地方会加载这些对象——`pixie trace filter` CLI、数据集加载器和验证脚本——因此它们共享同一个模型。
|
||||
|
||||
### `pixie.WrapLogEntry`
|
||||
|
||||
```python
|
||||
pixie.WrapLogEntry(*, type: str = 'wrap', name: str, purpose: str, data: Any, description: str | None = None, trace_id: str | None = None, span_id: str | None = None) -> None
|
||||
```
|
||||
|
||||
记录到 JSONL 追踪文件中的单个 wrap() 事件。
|
||||
|
||||
属性:
|
||||
type:对于 wrap 事件,始终为 `"wrap"`。
|
||||
name:wrap 点名称(与 `wrap(name=...)` 匹配)。
|
||||
purpose:`"input"`、`"output"` 或 `"state"` 之一。
|
||||
data:序列化后的数据(jsonpickle 字符串)。
|
||||
description:可选的人类可读描述。
|
||||
trace_id:OTel 追踪 ID(如有)。
|
||||
span_id:OTel span ID(如有)。
|
||||
|
||||
### `pixie.load_wrap_log_entries`
|
||||
|
||||
```python
|
||||
pixie.load_wrap_log_entries(jsonl_path: 'str | Path') -> 'list[WrapLogEntry]'
|
||||
```
|
||||
|
||||
从 JSONL 文件加载所有 wrap 日志条目。
|
||||
|
||||
跳过非 wrap 行(例如 `type=llm_span`)及格式错误的行。
|
||||
|
||||
参数:
|
||||
jsonl_path:JSONL 追踪文件的路径。
|
||||
|
||||
返回:
|
||||
:class:`WrapLogEntry` 对象的列表。
|
||||
|
||||
### `pixie.filter_by_purpose`
|
||||
|
||||
```python
|
||||
pixie.filter_by_purpose(entries: 'list[WrapLogEntry]', purposes: 'set[str]') -> 'list[WrapLogEntry]'
|
||||
```
|
||||
|
||||
按 purpose 过滤 wrap 日志条目。
|
||||
|
||||
参数:
|
||||
entries:wrap 日志条目列表。
|
||||
purposes:要包含的 purpose 值集合。
|
||||
|
||||
返回:
|
||||
过滤后的列表。
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env bash
|
||||
# Setup script for eval-driven-dev skill.
|
||||
# Updates the skill, installs/upgrades pixie-qa[all], initializes the
|
||||
# pixie working directory, and starts the web UI server in the background.
|
||||
#
|
||||
# Error handling:
|
||||
# - Skill update failure → non-fatal (continue with existing version)
|
||||
# - pixie-qa upgrade failure when already installed → non-fatal
|
||||
# - pixie-qa NOT installed and install fails → FATAL (exit 1)
|
||||
# - pixie init failure → FATAL (exit 1)
|
||||
# - pixie start failure → FATAL (exit 1)
|
||||
set -u
|
||||
|
||||
echo "=== Updating skill ==="
|
||||
npx skills update github/awesome-copilot --skill eval-driven-dev -g -y && npx skills update github/awesome-copilot --skill eval-driven-dev -p -y || {
|
||||
echo "(skill update failed — proceeding with existing version)"
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "=== Installing / upgrading pixie-qa[all] ==="
|
||||
|
||||
# Helper: check if pixie CLI is importable
|
||||
_pixie_available() {
|
||||
if [ -f uv.lock ]; then
|
||||
uv run python -c "import pixie" 2>/dev/null
|
||||
elif [ -f poetry.lock ]; then
|
||||
poetry run python -c "import pixie" 2>/dev/null
|
||||
else
|
||||
python -c "import pixie" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Check if pixie is already installed before attempting upgrade
|
||||
PIXIE_WAS_INSTALLED=false
|
||||
if _pixie_available; then
|
||||
PIXIE_WAS_INSTALLED=true
|
||||
fi
|
||||
|
||||
INSTALL_OK=false
|
||||
if [ -f uv.lock ]; then
|
||||
# uv add does universal resolution across all Python versions in
|
||||
# requires-python. If the host project supports a Python version
|
||||
# where pixie-qa is unavailable (e.g. <3.10), uv add fails.
|
||||
# Fall back to uv pip install which only targets the active interpreter.
|
||||
if uv add "pixie-qa[all]>=0.8.4,<0.9.0" --upgrade 2>&1; then
|
||||
INSTALL_OK=true
|
||||
else
|
||||
echo "(uv add failed — falling back to uv pip install)"
|
||||
if uv pip install "pixie-qa[all]>=0.8.4,<0.9.0" 2>&1; then
|
||||
INSTALL_OK=true
|
||||
fi
|
||||
fi
|
||||
elif [ -f poetry.lock ]; then
|
||||
if poetry add "pixie-qa[all]>=0.8.4,<0.9.0"; then
|
||||
INSTALL_OK=true
|
||||
fi
|
||||
else
|
||||
if pip install --upgrade "pixie-qa[all]>=0.8.4,<0.9.0"; then
|
||||
INSTALL_OK=true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$INSTALL_OK" = false ]; then
|
||||
if [ "$PIXIE_WAS_INSTALLED" = true ]; then
|
||||
echo "(pixie-qa upgrade failed — proceeding with existing version)"
|
||||
else
|
||||
echo ""
|
||||
echo "ERROR: pixie-qa is not installed and installation failed."
|
||||
echo "The eval-driven-dev workflow requires the pixie-qa package."
|
||||
echo "Please install it manually and re-run this script."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Initializing pixie working directory ==="
|
||||
if [ -f uv.lock ]; then
|
||||
uv run pixie init
|
||||
elif [ -f poetry.lock ]; then
|
||||
poetry run pixie init
|
||||
else
|
||||
pixie init
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "ERROR: Failed to initialize pixie working directory."
|
||||
echo "Please check the error above and fix it before continuing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Starting web UI server (background) ==="
|
||||
if [ -f uv.lock ]; then
|
||||
uv run pixie start
|
||||
elif [ -f poetry.lock ]; then
|
||||
poetry run pixie start
|
||||
else
|
||||
pixie start
|
||||
fi
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "ERROR: Failed to start the web UI server."
|
||||
echo "Please check the error above and fix it before continuing."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Setup complete ==="
|
||||
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Validate that eval-driven-dev Step 6 artifacts are complete.
|
||||
|
||||
Usage:
|
||||
python verify_step6_completion.py /path/to/pixie_qa/results/<test_id>
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ENTRY_REQUIRED_FILES = ("evaluations.jsonl",)
|
||||
DATASET_ANALYSIS_FILES = ("analysis.md", "analysis-summary.md")
|
||||
ROOT_ANALYSIS_FILES = ("action-plan.md", "action-plan-summary.md", "meta.json")
|
||||
|
||||
|
||||
def _dataset_dirs(results_dir: Path) -> list[Path]:
|
||||
return sorted(
|
||||
path
|
||||
for path in results_dir.iterdir()
|
||||
if path.is_dir() and path.name.startswith("dataset-")
|
||||
)
|
||||
|
||||
|
||||
def _entry_dirs(dataset_dir: Path) -> list[Path]:
|
||||
return sorted(
|
||||
path
|
||||
for path in dataset_dir.iterdir()
|
||||
if path.is_dir() and path.name.startswith("entry-")
|
||||
)
|
||||
|
||||
|
||||
def _read_jsonl(path: Path, errors: list[str]) -> list[dict[str, object]]:
|
||||
rows: list[dict[str, object]] = []
|
||||
try:
|
||||
for index, line in enumerate(
|
||||
path.read_text(encoding="utf-8").splitlines(), start=1
|
||||
):
|
||||
if not line.strip():
|
||||
continue
|
||||
obj = json.loads(line)
|
||||
if not isinstance(obj, dict):
|
||||
errors.append(f"{path}: line {index} is not a JSON object")
|
||||
continue
|
||||
rows.append(obj)
|
||||
except OSError as exc:
|
||||
errors.append(f"{path}: could not read file ({exc})")
|
||||
except json.JSONDecodeError as exc:
|
||||
errors.append(f"{path}: invalid JSONL ({exc})")
|
||||
return rows
|
||||
|
||||
|
||||
def validate_results_dir(results_dir: Path) -> list[str]:
|
||||
"""Return a list of validation errors for a pixie results directory."""
|
||||
errors: list[str] = []
|
||||
|
||||
if not results_dir.is_dir():
|
||||
return [f"{results_dir}: results directory not found"]
|
||||
|
||||
for file_name in ROOT_ANALYSIS_FILES:
|
||||
if not (results_dir / file_name).is_file():
|
||||
errors.append(f"Missing root artifact: {results_dir / file_name}")
|
||||
|
||||
datasets = _dataset_dirs(results_dir)
|
||||
if not datasets:
|
||||
errors.append(f"{results_dir}: no dataset-* directories found")
|
||||
return errors
|
||||
|
||||
for dataset_dir in datasets:
|
||||
for file_name in DATASET_ANALYSIS_FILES:
|
||||
if not (dataset_dir / file_name).is_file():
|
||||
errors.append(f"Missing dataset artifact: {dataset_dir / file_name}")
|
||||
|
||||
entry_dirs = _entry_dirs(dataset_dir)
|
||||
if not entry_dirs:
|
||||
errors.append(f"{dataset_dir}: no entry-* directories found")
|
||||
continue
|
||||
|
||||
for entry_dir in entry_dirs:
|
||||
for file_name in ENTRY_REQUIRED_FILES:
|
||||
if not (entry_dir / file_name).is_file():
|
||||
errors.append(f"Missing entry artifact: {entry_dir / file_name}")
|
||||
|
||||
evaluations_path = entry_dir / "evaluations.jsonl"
|
||||
if not evaluations_path.is_file():
|
||||
continue
|
||||
|
||||
evaluations = _read_jsonl(evaluations_path, errors)
|
||||
for row in evaluations:
|
||||
status = row.get("status")
|
||||
if status == "pending":
|
||||
errors.append(
|
||||
"Pending evaluation remains: "
|
||||
f"{evaluations_path} ({row.get('evaluator', 'unknown evaluator')})"
|
||||
)
|
||||
continue
|
||||
|
||||
if "score" not in row:
|
||||
errors.append(
|
||||
"Missing score in scored evaluation: "
|
||||
f"{evaluations_path} ({row.get('evaluator', 'unknown evaluator')})"
|
||||
)
|
||||
if "reasoning" not in row:
|
||||
errors.append(
|
||||
"Missing reasoning in scored evaluation: "
|
||||
f"{evaluations_path} ({row.get('evaluator', 'unknown evaluator')})"
|
||||
)
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
"""CLI entry point."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate Step 6 completion for a pixie results directory"
|
||||
)
|
||||
parser.add_argument(
|
||||
"results_dir",
|
||||
type=Path,
|
||||
help="Path to pixie_qa/results/<test_id>",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
errors = validate_results_dir(args.results_dir)
|
||||
if errors:
|
||||
print("Step 6 completion check failed:")
|
||||
for error in errors:
|
||||
print(f"- {error}")
|
||||
return 1
|
||||
|
||||
print("Step 6 completion check passed.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user