SDK:TypeScript、Python 和 preworld eval
开发中
SDK 这种接法正在开发,暂时不能在平台上选用(见 SDK 快速开始)。已经用它接好的项目照常可用,这里的说明照旧适用。
SDK 在你们自己的进程里跑评测,结果作为实验传上来,并用 OpenTelemetry 记录模型调用。TypeScript:@preworld/sdk(Node 20 及以上,ESM 和 CommonJS)。Python:preworld(Python 3.10 及以上)。两者都不在 npm 和 PyPI 上:平台自己在 /downloads/ 提供,每次发版一个版本(sdk.json 写着当前的文件名),目标 →「SDK」卡片给出安装命令,见从平台装 SDK。一步一步的做法见 SDK 快速开始。
配置
| 变量 | 意思 |
|---|---|
PREWORLD_API_KEY | 有上传权限(「允许上传日志与实验」)的项目令牌 pj_… |
PREWORLD_PROJECT | 项目编号 prj_… |
PREWORLD_BASE_URL | 平台地址(默认 https://review.nice2.io) |
PREWORLD_ENVIRONMENT | dev、staging、prod 或 ci;设了 CI 时默认 ci。别的值会提醒一次并忽略。 |
PREWORLD_LANG | zh 让摘要用中文(没设时看 LC_ALL、LANG) |
PREWORLD_GATE_WAIT | Eval 等平台评分器打完分、再读发布门槛,最多等几分钟(默认 10;0 只读一次) |
OTEL_SERVICE_NAME | 追踪的 service.name |
也可以在代码里设:init({ project, apiKey?, baseUrl?, environment? })(TypeScript)或 preworld.init(project=..., api_key=None, base_url=None, environment=None)(Python),优先于环境变量。SDK 从不读 .env 文件。git 信息来自 GITHUB_SHA、GITHUB_HEAD_REF、GITHUB_REF_NAME 和 GITHUB_REF(PR 编号从 refs/pull/N/… 里取),没有就问 git。
Eval
Eval<I, O, E>(name: string, options: {
data: Row[] | Iterable<Row> | AsyncIterable<Row> | Dataset | (() => …), // Row = { input, expected?, metadata?, tags?, id? }
task: (input: I, ctx: { expected?, metadata?, tags, trial, rowKey, experimentId?, span? }) => O | Promise<O>,
scores?: Array<ScorerFn | string>,
metadata?: Record<string, unknown>,
trialCount?: number, // 默认 1
maxConcurrency?: number, // 默认 10
experimentName?: string, // 默认 "<name> · <提交号前 7 位>",没有 git 时 "<name> · YYYY-MM-DD HH:MM"
baseline?: string, // exp_…;默认:项目的基线,没有就是这个 Eval(按名字)上一次跑完的实验
}): Promise<EvalResult>Eval(name, *, data, task, scores=(), metadata=None, trial_count=1, max_concurrency=10,
experiment_name=None, baseline=None) -> EvalResult
await EvalAsync(...) # 一样的参数,在已经运行的事件循环里用- data:一组行、任意(同步或异步)可迭代对象、返回它们的函数,或用
dataset("<名字或 ds_…>", { version })/dataset(name, version=None)读平台上的数据集(当前的行,或那一版)。平台数据集的行只把input、expected、metadata和tags交给任务。 - task:你们的代码。Python 的函数有第二个参数时传入上下文;普通函数在工作线程上跑,async 的在事件循环上跑。
- scores:本地评分函数,用
{input, output, expected, metadata}调用(Python 按参数名传),返回 0 到 1 的数、布尔值、null/None,或{name?, score, metadata?};以及按编号写的平台评分器,builtin:<slug>或sc_…(可以带@版本),上传后在平台上打分。评分器的列名是函数名(没名字的是score_N),存作local:<名字>,门槛规则也这样写它;metadata.rationale和metadata.label会保留。 - 未判定:任务抛错的行记
task_error;评分函数抛错记scorer_error,什么都没返回记skipped,不在 0–1 之间记bad_score。一行的分数是判定了的那几次的平均。 - 行的键:平台数据集的行是
<数据集行编号>#normal,否则是行的id,再没有就是输入的哈希;重复的加#2、#3。同样的输入在两种 SDK 里得到同样的键,所以多次运行之间能和基线逐行对上。
Eval 做的事:建实验(POST /v2/projects/:p/experiments),按最多 100 行或 4 MB 一批追加行(POST /v2/experiments/:id/rows,每批有自己的幂等键),结束实验,等发布门槛定下来——平台评分器(builtin:…、sc_…)在结束之后才给这些行打分,门槛用它的长轮询读(GET /v2/experiments/:id/gate?wait=60,没定就再问,最多 PREWORLD_GATE_WAIT 分钟)——读基线的行来比较,打印摘要并返回结果。摘要里平台评分器的数字就是平台的,和实验页一样;到时没打完,就仍显示「待平台评分」,不读门槛(退出码 2),不会判没通过。遇到网络错误和 408、425、429、5xx 会重试(最多 5 次,Retry-After 最多等 60 秒;每次请求 60 秒超时)。平台拒收某一批时,剩下的行不再跑。
摘要:
Preworld · support-bot
实验:support-bot · 4f2c9e1 (exp_…)
20 行 · 20 次 · 任务出错 0 · 14.2 秒
评分器 平均 基线 变化 改善 回归
exact 85% 80% +5.0 个百分点 2 1
builtin:factuality 90% 85% +5.0 个百分点 1 0
发布门槛:通过 ✓
✓ local:exact mean >= 0.8 (实得 0.85)
https://<平台地址>/p/prj_…/experiments/exp_…EvalResult 有 name、project、experiment({id, name, url})、baselineId、rows、trials、taskErrors、durationMs、scores(每个评分器:mean、decided、undecided、pending、direction、baseline_mean、delta、improved、regressed)、gate、gateError、error、exitCode 和 results(Python 用下划线命名)。Eval 从不退出进程:exitCode(results) / exit_code(results) 给出 preworld eval 会用的退出码。
追踪
| TypeScript | Python | 记录什么 |
|---|---|---|
traced(fn, { name?, kind? }) | @traced 或 @traced(name=None, kind="chain") | 一个函数:参数作为输入,返回值作为输出,抛错记为出错 |
wrapOpenAI(client) | wrap_openai(client) | chat.completions.create、responses.create、embeddings.create,包括流式(TypeScript 返回一个包装;Python 直接改传进去的客户端并返回它) |
wrapAnthropic(client) | wrap_anthropic(client) | messages.create,包括 stream: true |
aiSdkTelemetry({ functionId?, recordInputs?, recordOutputs? }) | — | Vercel AI SDK 5、6、7:experimental_telemetry: aiSdkTelemetry() |
currentSpan()、flush()、shutdownTracing() | current_span()、flush()、shutdown() | 短命进程退出前把没发的发出去(Eval 和命令行会自己做) |
种类:agent、llm、tool、retrieval、embedding、chain(默认)、other。跨度带 OpenTelemetry GenAI 属性(gen_ai.request.model、gen_ai.usage.*、gen_ai.input.messages、gen_ai.output.messages……),以 OTLP/HTTP protobuf 发到 <平台地址>/otel/v1/traces,带上令牌和 x-preworld-environment。SDK 用自己的 tracer,从不替换全局的 OpenTelemetry provider。在 Eval 里,每个跨度都带 preworld.experiment_id、preworld.row_key 和 preworld.trial,每一行都能连到它的追踪。没有 API key 或用了 --no-send 时不记录追踪。
命令行:preworld eval
npx preworld eval [文件或目录…] [--no-send] [--json] [--first N] # TypeScript
preworld eval [文件或目录…] [--no-send] [--json] [--first N] # Python(或 python -m preworld eval)| 文件 | TypeScript:*.eval.ts、.mts、.cts、.js、.mjs、.cjs(TypeScript 要装 tsx)。Python:eval_*.py 和 *_eval.py。默认在当前目录下找;跳过 node_modules、__pycache__、虚拟环境和点开头的目录。 |
--no-send | 只在本地跑和打分;什么都不上传,也不需要令牌。 |
--json | JSON 打到标准输出,摘要打到标准错误:{version: 1, exit_code, evals: [{file, name, project, experiment, baseline_id, rows, trials, task_errors, duration_ms, scores, gate, gate_error, error, exit_code}]}。 |
--first N | 每个 Eval 只跑前 N 行。 |
退出码:0 发布门槛通过或没设规则 · 1 发布门槛没通过 · 2 读不到门槛(平台评分器在 PREWORLD_GATE_WAIT 内没打完,或平台没回答) · 3 出错(没找到文件、某个文件出错、参数不对)。有几个 Eval 时:有一个 3 就是 3,否则有 1 就是 1,否则有 2 就是 2。
pytest
Python 包同时是一个 pytest 插件。标了 @pytest.mark.preworld(可以带 tags=[…])的测试成为同一个实验的行,按节点编号做键;输入是测试的参数(没有就是测试名)。
import pytest
from preworld.pytest_plugin import check, log_outputs
@pytest.mark.preworld(tags=["math"])
@pytest.mark.parametrize("q,expected", [("2+2", "4")])
def test_answer(q, expected):
out = agent(q)
log_outputs(out)
check("short", len(out) < 100) # 一条断言,同时记成它自己的分数
assert out == expected- 每个测试的
pass分是 1 或 0,没过的断言写进理由。check(name, condition, message=None)、log_score(name, value)、log_inputs、log_outputs和log_expected往这一行里补充内容。跳过的和 xfail 的测试不算行。 - 设了
PREWORLD_API_KEY和PREWORLD_PROJECT才上传。--preworld-no-send只在本地;--preworld-experiment 名字给实验起名(默认<目录名> · <提交号前 7 位>)。 - 结束时实验被结束,读门槛(定下来之后)和基线,打印一段「preworld」。pytest 自己的退出码从不改变。
下一步
- CI 与 GitHub Action:每个 PR 都跑,并评论在 PR 上。
- v2 接口:底下用的 API。