Runtime 配置与接入示例
先完成在代码里比较两个版本的入门示例。本页按需求查阅评分、数据、策略、持久化与 Agent 接入,代码中的客户端、存储和凭证由你的应用提供。
评分方法
读懂代码中的几个名字
可以把一次评测理解为:准备题目 → 分别运行待比较版本 → 按规则评分 → 汇总差异。代码中的字段对应这些工作:
| 字段/术语 | 你要提供或得到的内容 |
|---|---|
artifact(知识载体) | 被修改的 prompt、skill、agent、workflow 或空白基线。 |
variant(待测版本) | 知识载体加上运行它所需的配置,例如「旧 prompt」和「新 prompt」。 |
dataset/sample(数据集/用例) | 一组测试输入;expected 保存标准答案,供评分使用。 |
executor(执行器) | 你编写的调用代码,接收输入并返回实际输出。 |
evaluator(评分器) | 判断输出的规则,例如完全匹配、检索指标或 LLM 评委。 |
metric(指标) | 一个读数的名字和含义,例如 correct 表示是否完全匹配。 |
comparison(比较关系) | 哪个是对照组(control),哪个是实验组(treatment),比较哪些指标。 |
experiment(实验设计) | 用例如何分配、重复运行几次、使用什么测量种子。 |
analysis(统计分析) | 如何将逐条读数汇总为均值、差值或置信区间。 |
decision(可选结论规则) | 根据指定分析给出结论;声明评分器本身不会自动产生发布结论。 |
policy(运行限制) | 并发、超时、重试、预算和证据保留方式。 |
result(结果) | 运行状态、逐条证据、统计分析、可选结论与报告。 |
执行器负责「运行」,评分器负责「评分」,分析负责「汇总」。后文的宿主指你的 Node.js 应用;Core 指 OMK 的测量引擎;封存指在执行前固定配置,以免运行中改变评分口径。trial 是一次计划执行,attempt 是其中的一次尝试,失败重试会增加尝试次数。
完全匹配与模型接入代码
当任务要求返回固定答案、分类标签或结构化数据时,可以使用「完全匹配」评分器(exact-match)。你为每条样本提供标准答案 expected,OMK 将执行器返回的 output 与它比较:一致记为 true,不一致记为 false,默认指标名为 correct。这项比较不需要调用 LLM 评委。
例如,标准答案是字符串 "巴黎" 时:
| 实际输出 | 是否匹配 | 原因 |
|---|---|---|
"巴黎" | 是 | 与标准答案完全一致。 |
"法国的首都是巴黎" | 否 | 意思正确,但输出内容不同。 |
"巴黎。" | 否 | 多了句号。 |
" 巴黎 " | 否 | 多了前后空格,评分器不会自动去除。 |
它适合要求精确输出的任务,例如返回 "退款" 或 "咨询" 的分类任务。允许多种正确表述的开放问答,应考虑 Rubric 评委,按明确的评分标准判断答案;需要自行去除空格、忽略大小写或提取字段后再比较时,可使用 自定义评分器。
对于 JSON 输出,OMK 比较规范化后的 JSON 值:对象字段顺序不影响结果,数组元素顺序、值的类型和字符串内容仍须一致。例如,{"a":1,"b":2} 与 {"b":2,"a":1} 匹配,数字 4 与字符串 "4" 不匹配。字符串形式的 JSON 不会自动解析成对象。
下面演示如何接入模型服务、提供标准答案,并比较两个 prompt 版本的完全匹配率。modelGateway 和 reportStore 代表你自己的模型调用与报告存储代码,需要替换为实际实现。
安装 OMK 和一个运行时 schema 库。Schema 只需提供 parse(unknown) 方法;下面使用 Zod:
npm install oh-my-knowledge@next zod1.接入自己的服务
执行器声明输入、配置和输出的格式,并在 execute() 中调用你的服务。capabilities 必须描述服务的真实能力;例如随机模型不能声明为确定性服务,声明支持取消时必须将 signal 传给实际调用。版本和 fingerprintFacets 用来识别实际实现,示例占位值需要替换。
import { z } from 'zod';
import { evaluate, type EvaluateInput, type Executor, type Variant } from 'oh-my-knowledge';
type Input = { prompt: string };
type Config = { deployment: string };
const executor: Executor<Input, Config, string> = {
executorId: 'acme.answer-service/v1',
version: '1.4.0',
schemas: {
input: z.object({ prompt: z.string() }).strict(),
config: z.object({ deployment: z.string() }).strict(),
output: z.string(),
},
outputClassification: 'sensitive',
capabilities: {
determinism: 'stochastic',
cancellation: 'cooperative',
concurrency: { safety: 'parallel-safe', maxInFlight: 16 },
seedControl: 'unsupported',
telemetry: { trace: 'unsupported', usage: 'required' },
},
fingerprintFacets: { deploymentRevision: 'sha256:...' },
async execute({ input, artifact, config, runtimeContext, signal }) {
const response = await modelGateway.generate({
deployment: config.deployment,
prompt: `${artifact.content ?? ''}\n${input.prompt}`,
context: runtimeContext?.values,
signal,
});
return { output: response.text, usage: response.usage };
},
};2.声明待比较的版本
下面只改变 prompt,两个版本使用同一模型部署和运行配置。这样才能将差异归因于 prompt;如果模型、知识库或工具也变了,需要把它们作为有意改变的条件说明。
const variants: Variant<Input, Config, string>[] = [{
variantId: 'prompt-v1',
artifact: {
name: 'answer-prompt-v1',
kind: 'prompt',
source: 'inline',
content: '简洁回答。',
},
execution: {
executor,
config: { deployment: 'deployment-a' },
runtimeContext: { values: { tenant: 'evaluation' } },
},
}, {
variantId: 'prompt-v2',
artifact: {
name: 'answer-prompt-v2',
kind: 'prompt',
source: 'inline',
content: '简洁、准确地回答。',
},
execution: {
executor,
config: { deployment: 'deployment-a' },
runtimeContext: { values: { tenant: 'evaluation' } },
},
}];3.准备标准答案并运行
input 是发送给服务的内容,expected 是供评分使用的标准答案;不要将标准答案拼进待测 prompt。下面让两个版本回答相同的两道题(paired),再比较完全匹配率。两条用例只展示接线方式,真实结论需要有代表性、数量充分的评测数据。
示例服务不支持控制模型种子,因此显式设置 seedCoupling: 'uncontrolled':同一条用例仍在两个版本上配对运行,但模型随机性不受控。experiment.seed 固定 OMK 的测量设计,不会让模型自动变成确定性服务;默认的共享种子配对不能用于这种执行器。
const input: EvaluateInput = {
dataset: {
datasetId: 'answer-regression',
samples: [
{ sampleId: 'one', input: { prompt: '法国的首都是哪里?' }, expected: '巴黎' },
{ sampleId: 'two', input: { prompt: '2 + 2 等于几?' }, expected: '4' },
],
},
variants,
evaluators: [{ evaluatorKind: 'exact-match' }],
comparisons: [{
comparisonId: 'prompt-v1-vs-v2',
controlVariantId: 'prompt-v1',
treatmentVariantIds: ['prompt-v2'],
metricIds: ['correct'],
}],
analyses: [{
analysisId: 'prompt-v1-vs-v2-correct',
analysisKind: 'comparison-interval',
statistic: 'mean-difference',
comparisonId: 'prompt-v1-vs-v2',
treatmentVariantId: 'prompt-v2',
metricId: 'correct',
confidence: { method: 'percentile-bootstrap', level: 0.95, resamples: 1_000 },
}],
decision: {
decisionKind: 'analysis',
analysisId: 'prompt-v1-vs-v2-correct',
},
experiment: {
seed: 'release-2026-09-04',
trials: 1,
sampling: { samplingKind: 'paired', seedCoupling: 'uncontrolled' },
},
policy: {
execution: { maxConcurrency: 4 },
evaluation: { maxConcurrency: 4 },
},
};
const result = await evaluate(input);
if (result.status === 'failed') throw new Error(result.error.code);
if (result.status !== 'completed') throw new Error(`评测未完成:${result.status}`);
await reportStore.put(result.report);判断检索结果是否相关、排序是否合理
检索评测回答的是「找到了多少相关文档、相关文档排得够不够靠前」,不会判断最终生成的回答质量。准备每道查询的已知相关文档 ID,再让检索执行器返回实际的有序 ID 列表。
下面的 retrieverVariant 需要由你按前面的接入方式定义:接收 { query: string },成功时返回 { output: { documents: ['refund-policy', 'other-doc'] } },并声明匹配的输出 schema。/documents 表示读取输出对象的 documents 字段;/relevantDocumentIds 表示读取 expected 中的同名字段。cutoff: 10 只检查前 10 个结果。使用 solo 时,执行器须为确定性执行或真实支持 OMK 传入的测量种子;能力要求详见 API 参考。
import { evaluate, type RetrievalEvaluator } from 'oh-my-knowledge';
const retrieval: RetrievalEvaluator = {
evaluatorKind: 'retrieval',
evaluatorId: 'retrieval-quality',
cutoff: 10,
ranking: { source: 'output', pointer: '/documents' },
relevantDocumentIdsPointer: '/relevantDocumentIds',
metricIds: {
recallAtK: 'recall-at-10',
precisionAtK: 'precision-at-10',
reciprocalRankAtK: 'reciprocal-rank-at-10',
ndcgAtK: 'ndcg-at-10',
},
};
const result = await evaluate({
dataset: {
datasetId: 'search-regression',
samples: [{
sampleId: 'refund-policy',
input: { query: '退款规则是什么?' },
expected: { relevantDocumentIds: ['refund-policy', 'billing-faq'] },
}],
},
variants: [retrieverVariant],
evaluators: [retrieval],
comparisons: [],
analyses: [{
analysisId: 'mean-reciprocal-rank-at-10',
analysisKind: 'summary',
statistic: 'mean',
variantId: retrieverVariant.variantId,
metricId: 'reciprocal-rank-at-10',
}],
experiment: { seed: 'search-v1', sampling: { samplingKind: 'solo' } },
policy: {},
});| 指标 | 回答的问题 | 数值解读 |
|---|---|---|
| Recall@K(召回率) | 已知相关文档找全了多少? | 前 K 条中命中的相关 ID 数除以已知相关 ID 总数。 |
| Precision@K(查准率) | 前 K 个位置有多少命中? | 命中数除以 K;只返回 1 条正确结果,K 为 10 时仍是 0.1。 |
| Reciprocal Rank@K(倒数排名) | 第一条相关文档出现得够早吗? | 第 1 名为 1,第 2 名为 0.5,前 K 条未命中为 0。多条用例的均值称为 MRR。 |
| nDCG@K(归一化折损累计增益) | 相关文档整体是否排在前面? | 与理想排序比较,取值为 0~1,越高越好。 |
上例的汇总位于 result.analysisResults['mean-reciprocal-rank-at-10'],只请求了 MRR;需要其它指标的均值时,为其分别声明 summary 分析。没有任何相关文档的用例,应按下一节的弃答场景评测,不要用空标注冒充零分。
Ranking 必须是由不重复、非空字符串 ID 组成的有序数组,可来自 output 或 trace;relevant ID 始终来自 expected,不会传给 Executor。该预设先按 cutoff 截断,再以 hits / known relevant 计算 Recall、以 hits / cutoff 计算 Precision、以首个 relevant 文档的名次计算 Reciprocal Rank,并使用 binary gain 与 log2 discount 计算 nDCG。空返回 ranking 是合法的零分;重复或非法 ID、空 relevant 集合会产出 invalid evidence。Reciprocal Rank 的 mean summary 才是 MRR,不要把单个 sample 的观测称为 MRR。
召回与空返回混合评测
要同时回答「有方案时找得对吗」「没方案时能正确空返回吗」「是否推荐了明确不可用的方案」,从单文件 examples/eval-runtime/retrieval-abstention.mjs 开始。OMK 内置召回与弃答评分;文件中的数据准备和禁用 ID 检查是可修改的业务示例,不需要自己重写弃答评分器。
1.先跑通示例
使用 Node.js 22 或更高版本,在包含该示例的源码检出目录运行:
yarn install --immutable
yarn build
node examples/eval-runtime/retrieval-abstention.mjs示例使用合成数据,不需要 API Key 或业务服务。独立项目只需复制这一个 .mjs 文件,安装包含 AbstentionEvaluator 的 OMK 版本及示例直接使用的 Zod:
npm install oh-my-knowledge@next zod
node retrieval-abstention.mjs如果功能尚未随 npm 版本发布,先使用主指南的源码运行方式;复制新示例配合旧版安装包不会获得新增能力。
2.替换 source 数据
保持每个 sampleId 唯一。下面是一条已确认没有适用方案的样本:
{
sampleId: 'no-solution-001',
input: { query: '这个问题没有适用的现有方案' },
expected: {
shouldAbstain: true,
acceptableSolutionIds: [],
forbiddenSolutionIds: ['solution-wrong'],
},
quality: { reviewStatus: 'reviewed' },
}| 样本情况 | 填写方式 |
|---|---|
| 有适用方案 | shouldAbstain: false,acceptableSolutionIds 非空。 |
| 没有适用方案 | shouldAbstain: true,acceptableSolutionIds: []。 |
| 尚未确认答案 | shouldAbstain: null,或 reviewStatus: 'pending_human_annotation';即使已有 AI 初始标签,也按待标注处理。 |
| 已知不可用方案 | 写入 forbiddenSolutionIds;为空时不参与禁用命中统计。 |
prepareRecommendationDataset() 默认遇到待标注就报错;演示代码显式使用 pendingPolicy: 'exclude',运行结果的 audit.excluded 会列出排除对象和原因。正式评测可删除这个选项以恢复默认阻止行为,同时把 sourceRevision 改为真实数据版本。
query 是示例字段,不是 OMK 的固定要求。若业务数据使用 input.prompt,可以先映射成 query;也可同步修改示例的 Row、Executor input schema 和调用代码。期望答案、禁用标签和审核状态留在评测侧,不放入发给被测系统的 input。
3.替换 executor.execute()
在该函数中调用自己的检索服务,并把经过应用过滤后的最终有序方案 ID 列表映射为以下返回值:
| 执行结果 | 返回形式 |
|---|---|
| 成功推荐方案 | return { output: { solutionIds: ['solution-a', 'solution-b'] } }; |
| 成功执行,但没有推荐方案 | return { output: { solutionIds: [] } }; |
| 调用失败 | 抛出异常,或 return { errorCode: 'recommendation-request-failed' };;不能伪装成成功空返回。 |
向业务调用传递收到的 signal,并如实更新 Executor 的 version、fingerprintFacets 和 capabilities。示例的 deterministic 只描述合成检索器;当前 solo 配置要求被测系统确定性执行,或实际支持并消费 OMK 传入的 seed。无 seed 支持的随机服务不能通过照抄 deterministic 声明接入;需要选择支持无控随机性的测量设计,参见公开采样契约。
按示例输出 solutionIds 时,evaluators 和 analyses 可直接使用。需要改为其它输出结构时,同步修改 output schema 和各评分器的绑定;JSON Pointer /solutionIds 表示读取输出对象的 solutionIds 字段。默认召回和禁用检查都取 top-3;调整范围时分别修改 retrieval 的 cutoff、forbiddenIdEvaluator(3) 的参数及相应指标名称。保留 analyses 中的 cohort 过滤,使各项失败覆盖数仍对应自己的适用样本群。
4.解读输出
原样运行示例,应排除 1 条待标注样本,并成功执行剩余 2 条。metrics 中的预期结果如下:
| 指标 | 含义 | 示例值/有效分母 |
|---|---|---|
recall-at-3 | 正向样本的已知正确方案召回比例,越高越好。 | 1/1 |
precision-at-3 | 前三项中正确方案数除以 3,越高越好。只返回 1 个正确方案仍为三分之一。 | 0.333…/1 |
rr-at-3、ndcg-at-3 | 首个正确方案的位置、排序质量,越高越好。rr-at-3 的均值即 MRR。 | 均为 1/1 |
correct-abstention | 应空返回样本的成功、合法响应中,空列表的比例,越高越好。 | 1/1 |
false-abstention | 应返回方案样本的成功、合法响应中,空列表的比例,越低越好。 | 0/1 |
forbidden-hit | 有禁用标注且响应成功、合法的样本中,前三项命中禁用 ID 的比例,越低越好。 | 0/2 |
每项先看 status 和 coverage:planned 是对应样本群的计划数,included 是实际参与计算的分母;sourceUnavailable 可能来自执行失败或缺失输出,invalid 表示非法证据。没有有效观测时 value 为 null,不是零分。再看 executionCoverage 的整体执行情况;有效响应上的百分之百不代表全部请求都成功。完整协议与限制见内置弃答参考。
检查 Agent 是否按要求调用工具
例如,你希望 Agent 先搜索、再读取文档,可以检查它的工具调用记录(轨迹)。下面要求 Search 出现在 Read 之前,允许中间有其它调用。把 trajectory 加入 evaluate() 的 evaluators,把 sample 放入 dataset.samples。
执行器需要返回符合 omk.source-neutral-trace/v2 的 trace,不能直接传入某个模型厂商的原始日志;先由接入代码将日志转换为这个统一格式。只检查调用过程并不能证明工具成功或最终答案正确:
import { evaluate, type ToolTrajectoryEvaluator } from 'oh-my-knowledge';
const trajectory: ToolTrajectoryEvaluator = {
evaluatorKind: 'tool-trajectory',
evaluatorId: 'tool-trajectory',
metricId: 'tool-trajectory-match',
tracePointer: '',
expectedToolNamesPointer: '/expectedToolNames',
match: 'contains-in-order',
};
const sample = {
sampleId: 'research-policy',
input: { request: '检索并总结这项规则。' },
expected: { expectedToolNames: ['Search', 'Read'] },
};模式名直接描述 actual trajectory 与 expected trajectory 的关系:exact-order 要求序列完全相同;same-tools 忽略顺序;contains-in-order 允许额外调用,但 expected 必须保持为 subsequence;contains-any-order 同时允许额外调用与任意顺序。所有模式都保留重复调用 multiplicity,并区分 source-neutral 工具名的大小写。Success、failure、cancelled 与 unknown 调用全部参与;工具执行结果属于另一项 construct。空 actual 轨迹合法;空 expected 只允许 exact 模式,用于断言“不应调用工具”。如果路径和最终结果都重要,可将这个 boolean Metric 与 final-output 或 Rubric 评委 evaluator 组合。
编写自己的评分规则(Custom Evaluator)
业务规则无法由内置评分器表达时,使用 CustomEvaluator,每个评分器声明一个或多个指标,在每个 Sample × Variant × Trial 的一次评分尝试中共同计算。例如检查禁用 ID、字段格式,或统计输出长度。
下面统计去除前后空格后的 JavaScript 字符串长度(UTF-16 码元数,部分 emoji 会占两个或更多码元),同时记录输出是否非空,并汇总实验组的平均长度。示例声明越短越好,只用于展示规则接入,不能把长度当成整体回答质量。替换 evaluate() 回调、指标定义与输入 schema 即可实现自己的规则:
import { z } from 'zod';
import { createCustomEvaluator, debugEvaluator, evaluate } from 'oh-my-knowledge';
const outputLength = createCustomEvaluator({
evaluatorId: 'output-length',
instrumentId: 'output-length-v1',
metrics: {
'output-length-chars': {
valueType: 'numeric', unit: 'characters', direction: 'lower-is-better',
schema: z.number().int().nonnegative(),
},
'output-nonempty': {
valueType: 'boolean', direction: 'higher-is-better', schema: z.boolean(),
},
},
bindings: [{ bindingId: 'actual', sourceKind: 'output', pointer: '' }],
parameters: { trim: true },
implementation: {
implementationId: 'acme.output-length/v1',
version: '1.0.0',
schemas: {
bindings: z.object({ actual: z.string() }).strict(),
fingerprintFacets: { bindings: 'actual-string/v1', values: 'length-and-nonempty/v1' },
},
fingerprintFacets: { sourceRevision: 'sha256:...' },
evaluate({ bindings, parameters, signal }) {
signal.throwIfAborted();
const actual = parameters?.trim ? bindings.actual.trim() : bindings.actual;
return {
resultKind: 'completed',
results: {
'output-length-chars': { resultKind: 'score', value: actual.length },
'output-nonempty': { resultKind: 'score', value: actual.length > 0 },
},
};
},
},
});
const result = await evaluate({
dataset: input.dataset,
variants,
evaluators: [outputLength],
comparisons: [{
comparisonId: 'prompt-v1-vs-v2',
controlVariantId: 'prompt-v1',
treatmentVariantIds: ['prompt-v2'],
metricIds: ['output-length-chars'],
}],
analyses: [{
analysisId: 'candidate-output-length',
analysisKind: 'summary',
statistic: 'mean',
variantId: 'prompt-v2',
metricId: 'output-length-chars',
}],
experiment: { seed: 'length-release-42', sampling: { samplingKind: 'paired', seedCoupling: 'uncontrolled' } },
policy: { evaluation: { timeoutMs: 5_000 } },
});先用同一个评分器调试一条样本;这会真实调用所选 Variant 的执行器和评分回调:
const debug = await debugEvaluator({
evaluator: outputLength,
sample: input.dataset.samples[0],
variant: variants[0],
policy: { evaluation: { timeoutMs: 5_000 } },
});
// 包含校验前的实际输入,可能含 gold/secret 数据,只在可信本地环境查看。
console.dir(debug.bindingInputs, { depth: null });
console.dir(debug.run.artifacts?.evaluation?.records, { depth: null });createCustomEvaluator() 从 schema 推断 bindings 和各指标分数类型,检查结果是否覆盖所有指标,并默认使用 missingPolicyId: 'exclude/v1'。它返回标准 v2 CustomEvaluator;现有数组形式仍可直接使用。debugEvaluator() 只运行一个 Sample × Variant × Trial,保留正式执行的预算、重试、取消与用量记录。它不生成比较、汇总分析或发布决定。bindingInputs 每次 parser 调用记录一项,包括重试与 schema 拒绝;源值缺失或命中评分缓存时可能为空。调试数据只在返回值内存中保留,不自动写日志或发送到事件观察器;取消后的返回值也不继续收集后台输入。
配置失败时,捕获 EvaluationConfigurationError 并查看 issues:例如 path: ['evaluators', 0, 'bindings', 0, 'pointer']、reasonCode: 'invalid-value'。这里不包含被拒绝的值或用户 parser 的异常文本。运行期的 schema 拒绝、漏指标、超时等仍在 debug.run 的逐指标 observation 或整次 record 中以稳定原因码报告。
查看 result.analysisResults['candidate-output-length'] 的状态、有效观测数和均值。这里只汇总 prompt-v2;要对比两个版本,应声明引用同一指标的比较分析。
Bindings 是最小权限 allowlist。只有 evaluator 确实需要 gold data 时才声明 expected 或 evaluation-context;callback 无法读取未声明的 sample 字段。JSON Pointer 会在投递前进一步收窄 source。execution-facts 是例外:它的 pointer 必须为空,让 callback 消费完整、已经脱敏的 canonical facts projection,避免产生第二套 projection identity。Binding 与 value schema 只能校验和收窄,不能 coercion、补默认值或删除字段。
构造器 callback 使用按指标 ID 映射的结果,自动展开为底层 v2 数组并生成 schemas.values。底层 CustomEvaluator callback 返回 { resultKind: 'completed', results, usage? }。每项结果带 metricId,状态为 score、missing 或 invalid;所有声明的指标必须恰好返回一次,顺序不限。schemas.values 必须按指标 ID 为每个指标提供一个 value parser,不能遗漏或多配。分数未通过校验时只将对应指标记为无效;证据不足时显式返回带原因的 missing。未知、重复或遗漏的指标 ID 会使整次调用失败。稳定失败使用 { resultKind: 'failed', errorCode, usage? },作用于整次调用。usage 放在外层,token 和费用只计一次。重试会重跑整个 callback;进度、并发槽、超时和调用预算按 evaluator 调用计算,覆盖率与汇总仍逐指标保留。Score 会作为 measurement data 直接持久化,不是带 classification 的 source content;text、category 与 ranking schema 必须把它约束在安全的测量词表内,绝不能回显 answer、trace、secret 或评委解释。这类支撑材料应放入显式声明 classification 的 CustomEvaluatorContent evidence。Invalid value 同样使用 CustomEvaluatorContent;普通异常会被脱敏。不要在 callback 内自行重试或实现超时:Core 会执行已封存的并发、超时、预算、取消、计量与失败策略。Callback 必须无状态、可安全并行且协作响应 signal;需要有状态资源时使用 advanced 生命周期 SPI。
scale 与 unit 的分工必须分清。跨指标合成前,OMK 先按每个 component 声明的 scale 线性归一化到 [0, 1],再按 direction 定向,因此可比性由 valueType、scale、direction 承载,量纲在这一步已经被消掉;这也是 composite 要求 numeric component 必须有界的原因。unit 只是给人看的量纲标注,用于展示与人工核对,不参与任何数值判定,也不会被用来自动换算或纠正越界值——换算必须由配置显式表达,不省略。因为它不承载计算语义,只允许 numeric Metric 声明 unit,与 scale 使用同一条收紧规则。
OMK 不会根据 Function#toString() 推导 provenance,因此 identity 必须显式声明。当代码、依赖、schema 或 provider 配置改变测量行为时,必须更新 version、schema fingerprintFacets 或 implementation fingerprintFacets。单个 custom evaluator 可产出多个 Metric,但不代表 ensemble member。把独立的模型调用合为一次会改变测量工具,不能因为指标 ID 相同就认为分数或耗时等价;迁移方式见 v2 迁移说明。Numeric 与 boolean Metric 必须声明单调 direction;只有调用方声明兼容的具名 summary 或 interval 后,它们才会成为 analysis result。Categorical、text 与 ranking Metric 在通过 advanced API 明确选择兼容 estimator 前只保留为 evaluation evidence。比较估计值保持原始 treatment-minus-control 差值。单 analysis progress Decision 只接受 higher-is-better;需要分别约束不同原始有符号 effect 时,应使用显式 comparison-family criterion。
让 LLM 按明确的评分标准评价答案
Rubric 就是明确写出的评分标准。开放问答允许多种正确表述时,可以让 LLM 评委按标准给出 1~5 分,而不是逐字比较答案。你提供评分标准和模型调用,OMK 负责组织评分提示、解析结果和聚合读数。
下面使用一个评委模型,对每条实际输出评分两次,再取平均,并汇总实验组的平均分。internalGateway 和 judge-model 需要替换为真实模型接入;评分会产生额外的模型调用。正式评测前应明确各分档的含义,并用人工标注样例校准标准。
import { createRubricEvaluator, evaluate } from 'oh-my-knowledge';
const rubricEvaluator = createRubricEvaluator({
evaluatorId: 'correctness-judge',
rubrics: {
'correctness-score': {
criterionId: 'correctness',
prompt: '判断答案的事实正确性。', rubric: '完全正确为 5 分,完全错误为 1 分。',
},
'completeness-score': {
criterionId: 'completeness',
prompt: '判断答案是否覆盖全部要求。', rubric: '全部覆盖为 5 分,全部遗漏为 1 分。',
},
},
judges: [{
memberId: 'primary',
model: 'judge-model',
effort: 'low',
replicateCount: 2,
judge: {
judgeId: 'acme.model-gateway/v1',
version: '2026.09.04',
providerCost: { reporting: 'optional' },
fingerprintFacets: { deploymentRevision: 'sha256:...' },
async invoke(request) {
const response = await internalGateway.generate({
model: request.model,
system: request.system,
prompt: request.prompt,
signal: request.signal,
});
return { invocationStatus: 'completed', output: response.text, usage: response.usage };
},
},
}],
aggregation: { method: 'mean', missing: 'require-complete' },
});
const result = await evaluate({
dataset: input.dataset,
variants,
evaluators: [rubricEvaluator],
comparisons: [{
comparisonId: 'prompt-v1-vs-v2',
controlVariantId: 'prompt-v1',
treatmentVariantIds: ['prompt-v2'],
metricIds: ['correctness-score'],
}],
analyses: [{
analysisId: 'candidate-correctness',
analysisKind: 'summary',
statistic: 'mean',
variantId: 'prompt-v2',
metricId: 'correctness-score',
}],
experiment: { seed: 'rubric-release-42', sampling: { samplingKind: 'paired', seedCoupling: 'uncontrolled' } },
policy: {},
});createRubricEvaluator() 的 rubrics 以指标 ID 为键,每项保留显式的 criterionId、prompt 和 rubric;不要再写 metricId。构造时即校验配置,不调用模型,返回的标准声明可直接用于下方单样本调试或正式评测。judges 和 aggregation 仍须显式填写。错误位于 EvaluationConfigurationError.issues,例如 ['rubrics', 'correctness-score', 'rubric'];非法指标键只定位到 rubrics,不回显拒绝值。
查看 result.analysisResults['candidate-correctness'] 的状态、有效观测数和均值。这里只汇总 prompt-v2;要对比两个版本,应声明引用同一指标的比较分析。
一次调用同时返回正确性和完整性分数及各自理由;因此 replicateCount: 2 对每个样本/版本调用两次。每个维度仍是独立的 1~5 分指标,保留各自证据、覆盖率与按需声明的分析。需要汇总完整性时,增加引用 completeness-score 的 analysis。单个读数不合法只影响该指标;遗漏、重复或未知指标 ID 会使整组响应无效。API、identity 与可比性变化见 v2 迁移说明。产品 CLI 的 rubric 使用同一联合协议,样本文件格式不变。
评委 callback 只执行一次 provider 调用,不得自行重试。replicateCount 只重复评测,不重复 Target 执行,也不增加 Bootstrap 样本量。存在多个成员时,mean 会在各成员的 replicate 先求均值后赋予成员等权;weighted-mean 要求为每个 memberId 显式提供正权重,且总和为 1。require-complete 会在某指标的任一计划坐标不可用时排除该指标对应的 Target × Sample × Trial panel 读数。Provider failure 会保留合法的计量事实,并移除 provider 私有原因与 usage details。只有当所有 Executor 都返回 oh-my-knowledge 中的版本化 trace 契约时,才使用 tracePolicy: 'source-neutral'。
先调试一条 Rubric 样本
把已配置的 Rubric Evaluator、样本和版本传给同一调试入口:
import { debugEvaluator } from 'oh-my-knowledge';
const debug = await debugEvaluator({
evaluator: rubricEvaluator,
sample: dataset.samples[0],
variant: variants[0],
});
const call = debug.judgeInvocations[0];
if (call?.response.responseStatus === 'completed') {
const { prompt } = call.request;
const { output, readings, usage } = call.response;
// 在本地调试器中查看实际 prompt、原始响应、逐维读数和用量。
}调试会真实运行一个 Sample × Variant × Trial;多评委、replicate 和 retry 仍可能产生多次调用。judgeInvocations 按调用开始顺序排列,run 保留正式运行的评分、稳定失败码与总用量。原始响应仅在返回值内存中保留,不自动写入日志或事件。维度遗漏/重复会显示 judge-response-metric-set-invalid;非法分数只使对应读数无效。配置错误可读取 EvaluationConfigurationError.issues,例如 evaluators[0].rubrics[1].metricId。完整状态与隐私边界见 API 参考。
重复评测与结果复用
先检查计划,再决定是否运行
需要 dry-run 检查、预算复核或人工审批时,先完成准备:
const prepared = await prepareEvaluation(input);
console.log(prepared.definition, prepared.policy);
console.log(prepared.planDigest, prepared.resolvedRuntimes);
console.log(prepared.estimatedWork);
const result = await prepared.run({ runId: 'approved-release-42', signal });准备阶段会解析 capability 并封存完整 Core Plan,不会调用 Target 或 Evaluator。prepared.run() 精确执行这份不可变 Plan;准备后修改原始 input,不会改变 Definition、Policy、digest 或执行行为。estimatedWork 给出 retry 或提前终止前计划的 execution/evaluation coordinate,并明确标出只有运行时才能确定的 duration 与 provider cost。直接调用 evaluate(input, options) 与 prepareEvaluation(input).run(options) 保持 canonical equivalence。
检查两次历史测量是否可比
需要判断两次独立 Run 是否支持精确比较时,把两份原始 result 交给 assessComparability(),并将每个有意变化的 Variant 显式映射成 subject:
import { assessComparability } from 'oh-my-knowledge';
const assessment = assessComparability({
comparisonScope: 'decision',
subjects: [{
subjectId: 'candidate-under-test',
leftVariantId: 'candidate',
rightVariantId: 'candidate',
}],
left: previousResult,
right: candidateResult,
});
if (assessment.comparabilityStatus !== 'compatible') {
console.error(assessment.designStatus, assessment.evidenceQualificationStatus);
}该 Assessment 不会比较分数,也不会判断候选是否进步;它只检查声明 subject 变化后,测量设计是否保持不变,以及两条 source chain 是否具备足够的认证证据。必须保留原始 result object:clone 或反序列化 artifact 无法保留进程内 Core source authority,因此会失败关闭。跨进程读回历史结果时,先用 在新进程里读回历史结果再复用 的 loadEvaluationResult() 重新取得 source authority,再交给 assessComparability()。
重复整轮评测,检查结果是否稳定
如果一次测量看起来有提升,但你担心换一轮运行就得到不同结论,可以让 OMK 重复执行整轮评测。Evaluation Series 固定数据、评分方式和测量种子,记录每轮指定统计量的均值与波动。下面的 repeatableInput 需要另行准备:沿用接入示例中比较的分析 ID,但使用确定性服务,或真实支持种子控制的执行器,并声明受控的种子配对设计。该接入示例的 uncontrolled 配置不满足跨轮精确可比性要求,直接用于 Series 会得到 inconclusive,不能靠反复运行得到稳定性数值。
满足这些前提后,使用下面的代码重复运行并读取结果:
import { prepareEvaluationSeries } from 'oh-my-knowledge';
const preparedSeries = await prepareEvaluationSeries({
evaluation: repeatableInput,
seriesInstanceId: 'release-42-repeatability',
repeatCount: 10,
stability: {
sourceAnalysisId: 'prompt-v1-vs-v2-correct',
projection: 'interval-estimate',
},
});
// 此时尚未调用 Target 或 Evaluator。
console.log(preparedSeries.memberPlans, preparedSeries.estimatedWork);
const series = await preparedSeries.run({ signal });
if (series.status === 'failed') throw new Error(series.error.code);
if (series.status === 'cancelled') throw new Error('Series 已取消。');
if (series.stability?.analysisStatus === 'completed') {
console.log(series.stability.value.mean);
console.log(series.stability.value.sampleStandardDeviation);
} else {
console.error(series.stability);
}必须在执行前声明完整 repeatCount。OMK 只捕获一次 Evaluation 声明,预注册全部 membership,并验证每个 member 的各阶段 plan digest 保持一致,同时为其分配唯一 Run contract。Member 按顺序执行,Execution/Evaluation cache 必须禁用。失败或取消的 member 会保留真实的 partial、failed、cancelled 或 missing coverage 状态,绝不会被替换;API 也不会根据已观察值提前停止。每个 member 独立使用自己的 Run budget。
Series 的实验单位是一轮完整 Run。Trial、retry、sample 与评委 replicate 仍嵌套在 Run 内,不会增加 runCount。测量 seed 与其它设计条件一起保持固定,因此支持 seed 的 Executor 会在各 member 收到相同 trial seed;有意改变 Run-level seed 需要另一种实验契约。稳定性表只提供描述性统计:mean、分母为 n - 1 的贝塞尔校正样本方差、标准差、最小值、最大值与极差;它不会发布 release verdict、估计 iid 置信区间,也不能证明跨环境复现性。全部预注册 slot 都必须符合 evidence 门槛并可比较,否则 stability 为 inconclusive,不会静默删除失败或缺失 Run。使用 projection: 'scalar' 选择 scalar Analysis result;需要提取区间点估计时,必须显式使用 interval-estimate projection。默认只接纳完整 evidence;只有对应 missingness policy 对目标结论合理时,才显式允许 partial evidence。
PreparedEvaluationSeries 只能使用一次,seriesInstanceId 标识本次有意执行。真正开始一轮新 Series 时应使用新的值。直接调用 evaluateSeries(input, options) 等价于准备后运行一次。
改了标注或统计方法后,复用已有输出
修正了标准答案、调整了评分规则或统计方式时,不一定需要再次调用模型。按变化发生的位置选择下面的函数,并传入上一次运行的原始 result:
| 改了什么 | 调用什么 | 哪些工作会重做 |
|---|---|---|
| 标准答案或评分规则 | rescore() | 评分及其后的分析、结论。 |
| 统计分析方式 | reanalyze() | 分析及结论。 |
| 结论规则 | redecide() | 只重做结论。 |
如果 prompt、实际输入或执行配置变了,应重新运行 evaluate()。如果希望 Target 调用先跑完并沉淀,之后再决定评分口径,用 executeEvaluation() 停在 Execution,再用 scoreExecutedEvaluation() 评分,见拆分为先执行、后评分。下面的 correctedGoldDataset 等变量代表你更新后的完整声明:
import { reanalyze, redecide, rescore } from 'oh-my-knowledge';
const rescored = await rescore(
{ ...input, dataset: correctedGoldDataset },
originalResult,
{ runId: 'corrected-gold' },
);
const reanalyzed = await reanalyze(
{ ...input, analyses: revisedAnalyses },
rescored,
{ runId: 'revised-analysis' },
);
const redecided = await redecide(
{ ...input, analyses: revisedAnalyses, decision: revisedDecision },
reanalyzed,
{ runId: 'revised-decision' },
);rescore() 复用 Execution,reanalyze() 复用 Execution 与 Evaluation,redecide() 复用 Execution、Evaluation 与 Analysis。每次调用都接收一份完整的新声明,确保默认值与 identity 在后缀运行前封存。Core 会拒绝任何属于已跳过阶段的变化;只有当前进程中的原始 canonical result object 携带所需 source authority。Run options、进度事件与预算消耗只作用于新执行的后缀;复用 bundle 保留原始 identity 与历史 evidence,不会再次计费。跨进程复用持久化结果时,先用 loadEvaluationResult() 通过独立 verifier 重新取得 source authority,见在新进程里读回历史结果再复用;report 或 JSON clone 绝不是充分证据。
拆分为先执行、后评分
Target 调用最贵时,可以让它只跑一次,之后再决定评分口径。executeEvaluation() 只执行 Execution stage 并返回 ExecutedEvaluation 句柄;scoreExecutedEvaluation() 接收一份完整的新声明加上该句柄,在不再次调用 Target 的前提下执行 Evaluation、Analysis、Decision 与 Report:
import {
executeEvaluation,
saveExecutedEvaluation,
loadExecutedEvaluation,
scoreExecutedEvaluation,
} from 'oh-my-knowledge';
const executed = await executeEvaluation(input, {
runId: 'release-42-execution',
signal,
});
console.log(executed.runId, executed.bundle.records.length);
const first = await scoreExecutedEvaluation(
{ ...input, dataset: goldV1 },
executed,
{ runId: 'release-42-gold-v1' },
);
const second = await scoreExecutedEvaluation(
{ ...input, dataset: goldV2, evaluators: revisedEvaluators },
executed,
{ runId: 'release-42-gold-v2' },
);句柄就是 Execution stage 的已认证 evidence:它的 runId、executionPlanDigest 与 executionInputDigest,以及 ExecutionBundle 本体。bundleOrigin 表明这批工作是在当前进程完成的(runtime),还是从存储重新接纳回来的(store)。要在后续进程里评分,先把 envelope 持久化,再由宿主 verifier 独立认证后重新接纳:
const reference = await saveExecutedEvaluation({ executed, store: hostContentStore });
const reloaded = await loadExecutedEvaluation({
reference,
resolver: hostContentResolver,
verifier: {
verifierId: 'acme.execution-authority/v1',
async verify({ reference: candidate }) {
const attestation = await executionRegistry.authenticate(candidate.digest);
return {
verifiedExecutedDigest: candidate.digest,
attestationDigest: attestation.digest,
verifiedProvenanceBundleDigests: attestation.provenanceBundleDigests,
verifiedCacheRecordDigests: attestation.cacheRecordDigests,
};
},
},
});envelope 使用 media type application/vnd.omk.executed-evaluation+json;version=1(EXECUTED_EVALUATION_MEDIA_TYPE),并始终以 classification: 'gold' 写入,因为它保存的是绝不能作为 context 回流给 Target 的 Target evidence。存储由宿主负责,OMK 只固定 envelope 结构与 digest。
准入沿用与 rescore() 完全相同的 Core 规则,而且刻意收窄:新声明必须逐字节复现封存的 Execution stage,Gold、Evaluator、评委 rubric、Analysis 与 Decision 内容都可以变化。因此 prompt、执行输入或执行配置一旦变化,会在任何评分工作之前被拒绝;来自 structuredClone()、JSON 反序列化或手工拼装的手柄同样被拒绝。这类拒绝统一是一个 EvaluationConfigurationError,code 为 EVAL_RUNTIME_REUSE_INVALID,其 cause 只携带脱敏来源,不含宿主错误文本。每次评分都是一个普通 Run:拥有自己的 runId,只产生后缀阶段的进度事件,只消耗后缀预算;被复用的 Execution bundle 保留原始 identity,不会二次计费。
复用本身不构成发布结论的依据:
- 由同一 envelope 评分出的两份结果共享 Execution stage identity,因此
assessComparability()会把执行视为不变,只读你声明的 subject 变化。这是关于封存 bytes 的 identity 陈述,不是“两次评分时模型、供应商或环境表现一致”的证据。 - verifier 未认证该 envelope 的 provenance bundle 时,其 provenance status 保持 indeterminate:Report 只能声称
unknownprovenance,声明了 Decision 时也会被门槛拦住,哪怕被评分的事实毫无变化。经过一次成功的存储往返并不会自动恢复信任。 - 只执行 Run 没有任何分数。请把
executeEvaluation()视为未完成的工作:持久化句柄、完成评分并归档评分后的EvaluationResult,或让句柄留在进程内。要持久化一次完整的已评分 Run,仍然使用saveEvaluationResult()。
可直接运行的单文件样板 examples/eval-runtime/staged-execute-score.mjs 离线走完整个流程:一次合成 Target 调用,然后在同一个已持久化 envelope 上套用两套评分口径。
在新进程里读回历史结果再复用
分阶段复用要求传入当前进程里的原始 result。历史 Run 落在另一个进程或另一台机器上时,用下面三步把它读回来,再交给 rescore()、reanalyze()、redecide() 或 assessComparability():
- 保存:
saveEvaluationResult({ result, store })把 canonical result 写成一份 Gold 级 ContentDescriptor(media typeapplication/vnd.omk.evaluation-result+json;version=1)。存储仍归宿主,OMK 只规定信封与摘要。 - 读回:
prepareEvaluation(input)用产生这份 result 的同一声明重新封存 Plan,loadEvaluationResult({ prepared, reference, resolver, verifier })依次校验 reference、内容摘要与 sealed plan digest,并由verifier独立完成信任认证。 - 复用:返回的 result 与原始 result 等价,携带进程内 Core source authority,可直接进入分阶段复用;被复用的阶段不会再次调用 Target,也不会再次计费。
import {
loadEvaluationResult,
prepareEvaluation,
rescore,
saveEvaluationResult,
} from 'oh-my-knowledge';
const reference = await saveEvaluationResult({ result, store: hostResultStore });
const restored = await loadEvaluationResult({
prepared: await prepareEvaluation(input),
reference,
resolver: hostContentResolver,
verifier: {
verifierId: 'acme.result-authority/v1',
async verify({ reference: candidate, planDigest }) {
const attestation = await resultRegistry.authenticate(candidate.digest, planDigest);
return {
verifiedResultDigest: candidate.digest,
attestationDigest: attestation.digest,
verifiedProvenanceBundleDigests: attestation.provenanceBundleDigests,
verifiedCacheRecordDigests: attestation.cacheRecordDigests,
verifiedPolicyExecutionDigests: attestation.policyExecutionDigests,
};
},
},
});
const rescored = await rescore({ ...input, dataset: correctedGoldDataset }, restored, {
runId: 'restored-corrected-gold',
});verifier 是宿主的独立信任边界,不能只复算摘要:它必须认证产生这份 result 的 Runtime、provenance bundle、cache receipt 与 policy execution,并把 attestation 绑定到确切的 stored envelope digest;五个字段缺一不可,authority 不完整同样失败关闭。下列情况都在调用任何 Target 前失败关闭:reference 或内容元数据无效、内容摘要与 descriptor 不匹配、stored plan 与 prepared 的 sealed plan 不一致、resolver 或 verifier 调用失败、result 引用的 evidence content 无法解析,以及 Core admission 拒绝。structuredClone() 或 JSON 反序列化得到的对象始终失败关闭——只有 loadEvaluationResult() 返回的 result 才具备复用资格。
**缓存验证边界:**每条 Evaluation transparent-hit 的 verifiedCacheRecordDigests 都须独立认证原始缓存记录摘要,以及绑定当前 Execution source 后的确切 replay 记录摘要。仅有原始摘要不能认证被改动的观测值。生产 Core Runtime 会捕获两者;宿主 verifier 必须先认证缓存及 replay 投影,不能复制不可信 envelope 的自述摘要。历史 native-only attestation 现在会得到 indeterminate 的缓存收据及预算证明。Artifact Schema、已有存储字节与评分公式不变。
让不同用例分配给不同版本
paired 设计让每条用例都运行两个版本。若实验要求每条用例只运行其中一个版本,改用 independent 并声明各版本的分配权重。下面按 locale 分层分配,要求数据中提供 executionContext.locale,且用例数量满足每组及每层的最低要求;接入示例中的两条教学用例无法满足这个配置:
comparisons: [{
comparisonId: 'prompt-v1-vs-v2',
controlVariantId: 'prompt-v1',
treatmentVariantIds: ['prompt-v2'],
metricIds: ['correct'],
}],
experiment: {
seed: 'release-2026-09-04',
sampling: {
samplingKind: 'independent',
allocations: [
{ variantId: 'prompt-v1', weight: 1 },
{ variantId: 'prompt-v2', weight: 1 },
],
minimumSamplesPerVariant: 20,
minimumSamplesPerVariantPerStratum: 5,
stratumKey: '/executionContext/locale',
},
},OMK 会在执行前确定性地为每个 sample 封存唯一 Variant。重复 trial 沿用该分组;改变 seed、weight、stratum 或 minimum 都会产生新的 randomization identity。
同时约束多个发布指标
如果发布必须同时满足「正确性不能下降太多」和「安全性不能下降」,应在运行前一起声明这组比较和阈值。比较越多,单独看每个 95% 区间越容易作出错误的联合判断;comparison-family 会对这组区间进行多重比较校正。下面假设已定义 correctness 与 safety 评分指标:
analyses: [{
analysisId: 'release-family',
analysisKind: 'comparison-family',
statistic: 'mean-difference',
members: [
{
analysisId: 'v2-correctness',
comparisonId: 'prompt-v1-vs-v2',
treatmentVariantId: 'prompt-v2',
metricId: 'correctness',
},
{
analysisId: 'v2-safety',
comparisonId: 'prompt-v1-vs-v2',
treatmentVariantId: 'prompt-v2',
metricId: 'safety',
},
],
confidence: {
method: 'bonferroni-percentile-bootstrap',
level: 0.95,
resamples: 10_000,
},
}],
decision: {
decisionKind: 'comparison-family',
analysisId: 'release-family',
rule: 'all',
criteria: [
{ analysisId: 'v2-correctness', minimumEffect: -0.01 },
{ analysisId: 'v2-safety', minimumEffect: 0 },
],
},上例两个 member 使用 97.5% 边际区间;当边际区间过程达到其标称覆盖率时,目标是让已声明 family 的同时覆盖率至少为 95%。Percentile Bootstrap 仍是近似方法,因此这项校正不构成无条件的有限样本覆盖保证。Family record 位于 result.analysisResults['release-family'],每个 member 仍可通过自己的 analysisId 定位。成员会在执行前固定,preset 绝不会从 Bootstrap 区间伪造 p-value。
可选的 family decision 会指向该外层 family,并为每个 member 声明一项有界 criterion。Boundary 使用原始 treatment-minus-control effect 单位,相等视为 acceptable。使用 rule: 'all' 时,只有每个完整同时区间都落入各自声明的 boundary,OMK 才返回 RELEASE;任一区间完全落在某条 boundary 外即返回 BLOCK;任一区间仍跨越 boundary 则返回 not-decided。Criterion 不能缺失、重复、在看到结果后补充、加权,或折叠为 composite score。
将多个指标合成为一个分数
如果业务明确规定「总体质量由正确性占 70%、简洁性占 30% 构成」,可以声明加权综合分。先确认这种权衡有业务依据,再固定权重;不能用综合分掩盖必须分别达标的安全或质量要求。下面假设已经定义了这两个指标:
analyses: [{
analysisId: 'v2-overall-quality',
analysisKind: 'composite-comparison-interval',
compositeMetricId: 'overall-quality',
comparisonId: 'prompt-v1-vs-v2',
treatmentVariantId: 'prompt-v2',
components: [
{ metricId: 'correctness', weight: 0.7 },
{ metricId: 'conciseness', weight: 0.3 },
],
aggregation: { method: 'weighted-mean', missing: 'require-complete' },
confidence: { method: 'percentile-bootstrap', level: 0.95, resamples: 10_000 },
}],每个 component 必须是 boolean Metric,或具有单调 direction 的有界 numeric Metric。OMK 会依据 sealed source Metric 将其转换到 [0, 1],在实验单位内合成完整读数,最后才对 derived Metric 执行 Bootstrap。权重必须为正、按 metricId 唯一且严格求和为一;系统不会提供默认权重、覆盖 scale、clamp 越界值,也不会在证据缺失后重新归一化。单 Variant 质量使用带 variantId 的 composite-quality-interval;treatment-minus-control 变化使用 composite-comparison-interval,并由 paired 或 independent Sampling Design 决定重采样语义。Decision 通过 analysisId 选择其中任一结果。
运行与存储
接入服务时的输入、错误与凭证约定
除接入示例展示的值外,executor.execute() 还会收到 variantId。比较角色属于 comparisons,不会注入 Executor invocation。可预期的宿主失败应返回 { errorCode },其中 error code 必须稳定且不包含敏感信息;普通异常会统一成为脱敏的 EVAL_RUNTIME_EXECUTOR_FAILED。
Schema 只能校验并收窄。若 parser coercion、补默认值或删除 JSON 字段,OMK 会拒绝执行,因为这些行为会在同一 identity 下静默改变实际测量。需要有意变换时,应在 execute() 内完成,并提升 version 或测量相关的 fingerprintFacets。
Variant config 与 runtimeContext 会序列化进入 sealed Definition,因此只应放入可重放、非敏感的测量输入。凭证、client 与进程内资源应保留在 Executor closure 中,绝不能进入 Definition。
把较大或敏感的输出保存在自己的存储中
默认会将实际输出、调用记录和评分依据直接保存在运行结果中(full)。如果内容太大,或应由自己的存储服务控制访问,可选择 reference:你负责保存和读取内容,OMK 在结果中保留可校验的引用。评分时需要读取引用内容,因此同时提供 contentStore 和 contentResolver。下面的 objectStore 需要替换为你的存储实现:
import { checkContentStore, type ContentResolver, type ContentStore } from 'oh-my-knowledge';
const contentStore: ContentStore = {
async put(request) {
// 验证 request.digest,持久化 canonical JSON 值,再返回 descriptor。
return objectStore.putVerified(request);
},
};
const contentResolver: ContentResolver = {
async resolve(descriptor) {
return objectStore.resolveVerified(descriptor);
},
};
const storageCheck = await checkContentStore({ contentStore, contentResolver });
if (!storageCheck.conformant) throw new Error('内容存储未通过一致性检查。');
const result = await evaluate({
...input,
policy: {
...input.policy,
evidence: {
output: 'reference',
trace: 'digest',
evaluatorEvidence: 'reference',
maximumClassification: 'sensitive',
},
},
infrastructure: { contentStore, contentResolver },
});checkContentStore() 会写入两次相同的固定 public probe,再回读一次;稳定 reason code 不会保留 payload 或宿主异常文本。full 内联 canonical JSON 值,reference 持久化该值并记录经过验证的 descriptor,digest 只保留 canonical value digest,none 则省略该项捕获;output、trace 与 evaluatorEvidence 可以分别选择。内容超过 maximumClassification 时会失败关闭。Evaluator 把 output 或 trace 声明为输入后,对应 capture 必须保留为 full 或 reference;reference 输入还必须提供 resolver。OMK 会在 prepare 阶段、任何 Target 调用之前校验这些依赖。Store 实现与 credential 绝不进入 Definition;返回的 descriptor 会进入 run artifact,因此可选 uri 必须是稳定、opaque 且不含 credential 的 locator,不能是物理路径或 signed URL。授权与大小限制仍由宿主负责。
检查默认最多等待每个操作 5 秒;若存储服务使用不同的本地 SLO,可显式设置 timeoutMs。Content port 不暴露取消能力,因此 timeout 后停止底层操作仍由宿主负责。
使用缓存,减少重复调用
多次运行相同任务时,可以分别复用系统输出(执行缓存)或评分结果(评价缓存)。它们默认不开启,需要你提供缓存存储。执行缓存只适用于可验证身份的确定性执行器,不能直接拿来复用随机模型输出;如果只改了评分或分析,先看分阶段复用。
下面的 cacheableInput 是你为确定性执行器准备的评测声明,不能直接使用随机模型配置。缓存和部署认证服务也由你的应用提供;OMK 负责生成缓存键、校验记录并保留命中来源:
import type {
EvaluationCache,
ExecutionCache,
ExecutorIdentityVerifier,
} from 'oh-my-knowledge';
const executionCache: ExecutionCache = durableExecutionCache;
const evaluationCache: EvaluationCache = durableEvaluationCache;
const executorIdentityVerifier: ExecutorIdentityVerifier = {
verifierId: 'acme.signed-deployment-registry/v1',
async verify({ executor, declaredIdentity }) {
const attestation = await deploymentRegistry.verifyCallable({
implementation: executor,
declaredIdentity,
});
return { attestationDigest: attestation.digest };
},
};
const cached = await evaluate({
...cacheableInput,
policy: {
...cacheableInput.policy,
cache: { execution: 'reuse', evaluation: 'reuse' },
},
infrastructure: {
executionCache,
evaluationCache,
executorIdentityVerifier,
},
});execution: 'reuse' 表示命中时复用,miss 时执行并写入;它只适用于声明为 deterministic 的 Executor,并且必须由独立认证器把捕获的实际 callable、依赖和部署配置绑定到稳定 attestation。checkExecutor() 只检查行为一致性,不会把自报身份升级为 verified;认证器也不能只复述 declaredIdentity。execution: 'replay-only' 不写入,任一 miss 都会在调用 Target 前失败,适合显式离线重放。evaluation: 'reuse' 独立复用已完成的评价记录。
缺少所需 cache port 或透明 Execution 复用所需的认证器时,prepareEvaluation() 会失败关闭。缓存实现和 credential 不进入 Definition;缓存 entry 类型是公开的 ExecutionCacheEntry 与 EvaluationCacheEntry,但调用方不应自行放宽或重写 Core 的验证规则。不同实现、workspace、工具策略、评测输入或测量策略会通过 sealed identity 失效相应缓存。
接收进度与取消运行
在 evaluate() 的第二个参数中接收进度事件,并传入用于取消的信号。下面的 controller 应由你的取消按钮或请求生命周期持有;需要取消时调用 controller.abort()。
const controller = new AbortController();
const running = evaluate(input, {
signal: controller.signal,
onEvent(event) { console.log(event); },
});
const result = await running;进度事件用于观察,可能丢失;最终结论以返回的 result 为准。它不适合作为必须逐条保留的审计日志。
runId、signal、onEvent、eventWriter、clock、报告 annotation/summary 与 eventBufferCapacity 都属于可选的第二个 EvaluationRunOptions 参数,不属于测量声明。onEvent 是 best-effort 进度观察器。已投递事件保持顺序,但慢观察器不会反向阻塞测量:有界 Core stream 会丢弃最旧的待处理进度并保留较新的事件,因此序号允许出现缺口。eventBufferCapacity 控制这项内存上界,默认值为 256。观察器失败时,OMK 完成清理后抛出 EvaluationEventConsumptionError,其中保留终态 runResult,并由 canonical façade 隐去宿主回调的原始异常。持久事件投递同样留在 canonical façade 上:在测量声明里写明 policy.eventDelivery,再传入 eventWriter,写入器就会按顺序逐条收到事件。完整性只由 writerMode: 'required' 保证:写入失败即整次运行失败。optional+ignore 下,该阶段首次写入失败会静默停止持久投递,run 仍然完成,结果里也没有任何字段报告这次截断,因此不是审计级配置。两种错配都会在调用任何 Target 之前失败关闭:delivery 为 disabled 却传入写入器,以及声明 required 却未注入写入器。取消只由调用方传入的 AbortSignal 控制。
设置并发、超时、重试和预算
接入真实服务后,按服务容量和费用设置 policy。execution 限制待测系统的调用,evaluation 限制评分器或评委的调用,两者分别设置。下面的数值仅展示配置方式,应根据你的服务调整;maxAttempts: 3 包含第一次调用和最多两次重试。预算根据已上报用量检查,并发调用可能使最终成本超过设定值。
policy: {
execution: {
maxConcurrency: 8,
timeoutMs: 30_000,
retry: {
maxAttempts: 3,
retryableErrorCodes: ['rate-limit', 'timeout'],
backoff: {
backoffKind: 'exponential',
initialDelayMs: 250,
maxDelayMs: 5_000,
},
},
},
evaluation: {
maxConcurrency: 4,
timeoutMs: 10_000,
retry: {
maxAttempts: 2,
retryableErrorCodes: ['judge-rate-limit'],
backoff: { backoffKind: 'fixed', initialDelayMs: 200 },
},
},
failure: { failureMode: 'failure-threshold', maxFailures: 2 },
budget: {
run: {
maxInvocations: 1_000,
maxActiveDurationMs: 300_000,
maxWallClockMs: 600_000,
maxProviderCost: { amount: 20, currency: 'USD' },
},
execution: { maxInvocations: 800, maxProviderCost: { amount: 12, currency: 'USD' } },
evaluation: { maxInvocations: 200, maxProviderCost: { amount: 8, currency: 'USD' } },
coordinate: { maxInvocations: 4 },
attempt: { maxProviderCost: { amount: 0.25, currency: 'USD' } },
onUnreportedProviderCost: 'fail-run',
},
evidence: { maximumClassification: 'sensitive' },
},maxAttempts 包含第一次尝试。只有显式列出的宿主稳定错误码可以重试;普通抛错仍会脱敏,绝不被静默归类为可重试。none 立即重试,fixed 使用固定 delay,exponential 从 initialDelayMs 增长到可选的 maxDelayMs。continue 与 fail-fast 不接受 maxFailures;failure-threshold 必须声明它,并在已完成的失败数超过 threshold 后停止接纳后续 scheduling block。
预算采用分层且可审计的模型。run 同时覆盖 execution 与 evaluation;execution 和 evaluation 分别限制对应 stage;coordinate 作用于每个 Target/Sample/Trial 坐标;attempt 限制一次 attempt 上报的 provider cost。Invocation 数量包含 retry。maxActiveDurationMs 累加已完成 attempt 的执行时长;仅属于 run 的 maxWallClockMs 使用单调时钟计量完整经过时间,包括排队与 backoff。同一 run 中配置的所有 provider-cost limit 必须使用相同的三位大写货币代码。
Canonical façade 固定采用 Core 的 bounded-overshoot admission。它会在接纳新工作前检查已累计上报成本,但这不是调用前的硬性金额上限:已经接纳的并发调用仍可能让最终金额超过 limit,签名预算摘要会如实记录 overshoot。Provider cost 缺失时需要失败关闭,可设置 onUnreportedProviderCost: 'fail-run';默认 mark-unverifiable 会保留 run,同时把成本验证标为 indeterminate。Attempt cost 同样根据调用结束后的上报 usage 判断;attempt 时长由 stage timeoutMs 控制,不属于 attempt budget。
默认值为 execution/evaluation 并发 4、无 timeout、不重试、failure continue、run.maxInvocations 10,000、无其它 budget limit、onUnreportedProviderCost: 'mark-unverifiable',以及 maximum classification gold。
检查接入代码是否符合 OMK 要求
写好执行器或评分器后,用 checkRuntime() 检查成功、失败、取消和资源清理等行为是否符合 OMK 的要求。它会实际调用被检查的组件,应使用专门的测试输入和可丢弃资源。下面的三类输入需要由你准备:一条成功、一条返回预期错误码、一条可验证取消行为。
import { checkRuntime } from 'oh-my-knowledge';
const runtimeCheck = await checkRuntime({
runtimeKind: 'executor',
variant: variants[1],
success: { input: successInput, expected: expectedOutput },
failure: { input: failureInput, expectedErrorCode: 'model-unavailable' },
cancellation: { input: longRunningInput },
});
if (!runtimeCheck.conformant) console.error(runtimeCheck.checks);runtimeKind 判别字段还可以选择 evaluator、judge、cache、content-store 或 workspace-provider。checkExecutor() 与 checkContentStore() 继续作为复用既有探针的聚焦入口。无效声明会以 EVAL_RUNTIME_INPUT_INVALID 拒绝;宿主行为不符合契约时返回带稳定 reason code 的 conformant: false。检查通过不会把自报 Runtime identity 升级为已认证,也不证明模型 provider 的质量;随后仍应通过真实 evaluate() 验证预期组件组合。
若实现忽略取消信号,cancellation case 仍必须自行保证有界;进程内检查无法 containment 恶意代码。Evaluation cache、Custom Evaluator 与 Judge 检查会经 Core 运行重叠调用;execution cache 只按 Core 当前的串行读取路径检查,不声称更多保证。Cache 与 ContentStore 检查会写入数据,因此应使用可丢弃资源,并为 cache 提供唯一 probeNamespace。Workspace 检查会观察 lease 隔离、retry 复用与清理,但不能证明物理删除或 sandbox;timeoutMs 会限制检查等待清理的时间,但无法停止 provider 底层 promise。Judge 检查最多执行四次 provider 调用并可能产生费用,因此必须显式设置 allowExternalCalls: true;每个 publicProbeText 都会发送给 provider,只能包含无害的 public data。结果会返回实际 invocation 数与 provider-cost 汇总。稳定结果不会保留 probe payload、provider exception 文本、prompt、模型 output、cache entry、workspace root、locator 或 credential。
何时需要高级 API
多数业务接入使用包根的 evaluate() 和评分方法中的评分器即可。包根暴露所有用户能力:canonical façade(evaluate、prepareEvaluation 等)、构造 Definition、Policy 与 Evaluator 的命令式 builder(createExactMatchDefinition、createMeasurementPolicy、createRubricJudgeKit 等)、Runtime 装配(createEvaluationRuntime)、自定义 port(包括 subprocess command Executor adapter)、分阶段运行(runEvaluation)与版本化 wire schema:
import {
evaluate,
createExactMatchDefinition,
createMeasurementPolicy,
createEvaluationRuntime,
createJsonExecutorAdapter,
runEvaluation,
} from 'oh-my-knowledge';多指标图、自定义 Analysis Runtime、artifact 重放、跨进程 transported comparability 或自定义 comparability policy 使用 oh-my-knowledge/eval-core。MCP 集成使用 oh-my-knowledge/mcp,DSH 集成使用 oh-my-knowledge/dsh-plugin。跨进程读回历史 result 再做分阶段复用属于 canonical façade,见在新进程里读回历史结果再复用。eval-workflows 只依赖 runtime foundation 叶子模块,不依赖任一用户 façade。package.json#exports 之外的深路径均为私有实现。
可运行的最小示例与 packed-package fixture 会在 clean host 中验证 canonical API。
Agent 接入
为 Agent 提供相互隔离的文件工作区
如果 Agent 要读取代码仓库或修改文件,需要让不同用例使用各自的工作区,避免前一次修改影响后一次评分。用 WorkspaceDescriptor 记录文件快照的内容摘要,用 WorkspaceProvider 创建工作目录并在结束后清理。下面的 cas 代表你自己的快照存储和工作目录管理实现;示例摘要需要替换成真实内容摘要。
本地路径只交给执行器,不放进 runtimeContext,以便同一份文件内容在不同机器上仍有相同身份:
import type {
Executor,
WorkspaceDescriptor,
WorkspaceProvider,
} from 'oh-my-knowledge';
const workspace: WorkspaceDescriptor = {
resourceId: 'support-repository',
digest: `sha256:${'a'.repeat(64)}`,
mediaType: 'application/vnd.acme.source-tree',
classification: 'sensitive',
size: 184_320,
};
const workspaceProvider: WorkspaceProvider = {
providerId: 'acme.cas-overlay/v1',
version: '2.1.0',
fingerprintFacets: { materializer: 'overlayfs-v2' },
async open({ descriptor, runId, trialId }) {
// 返回可写、trial 私有 overlay 前,必须验证 descriptor.digest。
const root = await cas.createOverlay(descriptor, { runId, trialId });
return { root, close: () => cas.removeOverlay(root) };
},
};
const executor: Executor<{ task: string }, undefined, string> = {
executorId: 'acme.workspace-agent/v1',
version: '1.0.0',
schemas: { input: z.object({ task: z.string() }), output: z.string() },
workspaceProvider,
async execute({ input, workspace, signal }) {
if (workspace === undefined) return { errorCode: 'workspace-required' };
return { output: await agent.run(input.task, { cwd: workspace.root, signal }) };
},
};
const variant = {
variantId: 'workspace-agent',
artifact: { name: 'agent', kind: 'agent', source: 'inline', content: '...' },
execution: { executor, workspace },
};不同 sample 需要不同 snapshot 时,使用 { default, bySampleId },其中 null override 表示该 sample 显式不使用 workspace。OMK 会在执行前封存 descriptor 与 provider identity,为每个 Target × Sample × Trial 打开一份新 lease,只在该 trial 的 retry 间复用,并在所有终态路径关闭。物理 root 不会成为测量 identity 或自动 evidence。Provider 必须完成有界的本地资源获取并自行验证内容;OMK 不发现文件、locator 或 credential。可写 lease 用于隔离测量,不是不可信代码的 sandbox。
按用例约束工具访问
如果有些用例只允许搜索和读取、有些用例不允许调用任何工具,可以按用例配置允许使用的工具。先确认 Agent 后端能够严格执行这份列表,再声明支持并转发 allowedTools。OMK 不会代替后端拦截调用:
const executor: Executor<{ task: string }, undefined, string> = {
executorId: 'acme.tool-restricted-agent/v1',
version: '1.0.0',
schemas: { input: z.object({ task: z.string() }), output: z.string() },
capabilities: {
toolPolicy: 'allow-list',
cancellation: 'cooperative',
},
async execute({ input, allowedTools, signal }) {
return {
output: await agent.run(input.task, {
tools: allowedTools,
signal,
}),
};
},
};
const variant = {
variantId: 'restricted-agent',
artifact: { name: 'agent', kind: 'agent', source: 'inline', content: '...' },
execution: {
executor,
allowedTools: {
default: ['Read', 'Search'],
bySampleId: {
offline: [],
unrestricted: null,
},
},
},
};直接传入数组时,该列表适用于所有 sample。在 plan 中,[] 表示禁用全部工具,null 表示为该 sample 有意恢复 Executor runtime 默认值。OMK 会为 canonical identity 排序,始终隔离不同 Sample 的列表,并在一个 Trial 的 retry 间传递同一份不可变列表;OMK 本身既不发现工具,也不执行 provider 调用限制。Executor 必须把 allowedTools 转换为后端的准确约束;如果后端只能近似执行、忽略或扩大列表,就绝不能声明 toolPolicy: 'allow-list'。Variant 请求列表而 Executor 缺少 capability 时,prepareEvaluation() 会失败关闭。
按用例选择原生 MCP 配置
MCP 是 Agent 连接外部工具服务的协议。如果不同用例需要连接不同的 MCP 服务,用描述符标识配置版本,由你提供的 mcpConfigProvider 读取真正的配置与凭证。OMK 不会自动查找本机 MCP 配置。下面的 secretStore 是你的凭证存储;摘要和大小应按实际配置计算:
const executor: Executor<{ task: string }, undefined, string> = {
executorId: 'acme.mcp-agent/v1',
version: '1.0.0',
schemas: { input: z.object({ task: z.string() }), output: z.string() },
capabilities: { mcp: 'native-config' },
mcpConfigProvider: {
providerId: 'acme.secret-store/v1',
version: '1.0.0',
async open({ descriptor }) {
const config = await secretStore.readJson(descriptor.resourceId);
return { config, close: () => secretStore.release(descriptor.resourceId) };
},
},
async execute({ input, mcpConfig, signal }) {
return { output: await agent.run(input.task, { mcp: mcpConfig?.config, signal }) };
},
};
const variant = {
variantId: 'mcp-agent',
artifact: { name: 'agent', kind: 'agent', source: 'inline', content: '...' },
execution: {
executor,
mcpConfig: {
default: {
resourceId: 'mcp-config-a',
digest: 'sha256:<canonical-json-digest>',
size: 123,
mediaType: 'application/json',
classification: 'secret',
},
bySampleId: { offline: null },
},
},
};OMK 会验证 provider 返回值的 canonical JSON digest 与 byte size,为每个 Trial 打开一份新 lease,只在该 Trial 的 retry 间复用,并在所有终态路径关闭。原生配置只对选中用例的 Executor invocation 或 session 可见,OMK 不会把它写入 result 或 error;Executor 自己也不能通过 output 或 trace 返回 secret。逐 sample descriptor 变化只会失效选择该 descriptor 的 coordinate;provider identity 变化则会保守失效使用该 Executor 的全部 coordinate。Runtime 有意不发现 MCP 文件,也不选择 provider 默认值;产品层 discovery 与 Workflow 到 Runtime 的装配属于 eval-workflows。
用模拟结果替换指定工具调用
例如,评测退款 Agent 时希望返回固定的查询结果、又不访问真实业务接口,可以让支持拦截的后端使用 execution.mockInterception。Mock 就是为指定工具调用提供预先准备的模拟响应。它可以直接接收一个 secret MockInterceptionDescriptor,也可以使用 { default, bySampleId },其中 null 表示为该 sample 禁用 interception。Executor 必须成对声明 capabilities.mockInterception: 'pre-tool-call' 与 mockInterceptionProvider:
const executor: Executor<string, undefined, string> = {
executorId: 'acme.mockable-agent/v1',
version: '1.0.0',
schemas: { input: z.string(), output: z.string() },
capabilities: { mockInterception: 'pre-tool-call' },
mockInterceptionProvider: {
providerId: 'acme.mock-provider/v1',
version: '1.0.0',
async open({ descriptor, signal }) {
const plan = await mockStore.readAndVerify(descriptor, signal);
const matcher = createMatcher(plan);
return {
intercept: ({ callId, toolName, input, signal: callSignal }) =>
matcher.intercept({ callId, toolName, input, signal: callSignal }),
close: () => matcher.close(),
};
},
},
async execute({ input, signal, mockInterception }) {
return { output: await agent.run(input, { signal, mockInterception }) };
},
};Descriptor media type 固定为 application/vnd.omk.mock-interception-plan+json;其 digest-bound plan 必须覆盖 strictness、first-match 规则顺序和有序返回 payload descriptor。Provider 负责加载 plan,并在返回 lease 前校验 digest、byte size、media type 与 classification。Runtime 为每个 attempt 打开一份新 lease,retry 也不复用,因此返回序列和命中状态会重置。Runtime 校验 mocked、pass-through 与 denied decision,等待 Target 调用 settle 后再清理,并对 provider failure 脱敏。Interception 生效时产生的 output 与 trace 会保守标记为 secret。Strict miss 必须返回 denied,绝不能静默调用真实工具。checkExecutor() 暂不认证 interception,应通过真实 Evaluation 验证。
为多步 Agent 保留一次评测内的会话
Agent 需要在一次任务内保留多步状态或复用会话句柄时,使用 SessionExecutor。不同用例和计划执行各自创建会话;失败重试沿用本次会话。无状态的一次请求继续使用Executor。Executor 继续表示简洁的无状态 omk.invoke/v1 接口;EvaluationExecutor 是 Variant 接受的联合类型,InvokeExecutor 是无状态形态的显式名称:
import type { SessionExecutor } from 'oh-my-knowledge';
const agentExecutor: SessionExecutor<{ task: string }, undefined, string> = {
protocol: 'session',
executorId: 'acme.research-agent/v1',
version: '1.0.0',
schemas: {
input: z.object({ task: z.string() }).strict(),
output: z.string(),
},
capabilities: {
cancellation: 'cooperative',
concurrency: { safety: 'parallel-safe' },
telemetry: { trace: 'unsupported', usage: 'optional' },
},
async openSession({ runId, trialId, input }) {
const handle = agentClient.createLocalHandle({ runId, trialId, task: input.task });
return {
async execute({ attemptId, signal }) {
const response = await handle.run({ idempotencyKey: `${runId}:${attemptId}`, signal });
return { output: response.text, usage: response.usage };
},
close: () => handle.close(),
};
},
};OMK 会为每个 Target × Sample × Trial 打开一个新的 ExecutorSession object,并拒绝跨 trial 或 Run 复用同一个 object。Retry 会以新的 ExecutorSessionAttempt 调用同一 session。attemptId 在对应测量坐标内稳定,但可能在另一个 Run 中重复,因此 provider 幂等键必须用 runId(或等价的 provider session scope)限定命名空间;远端提交状态不明确时要失败关闭。ExecutorSessionContext 包含 runId、trialId、Variant 最小投影与 execution context,不包含 Gold、evaluation context 或 analysis membership。成功、失败、timeout 或取消后,close() 都只运行一次。openSession() 与 close() 必须是有界的本地生命周期工作;打开 session 是未计量的资源获取,不得执行模型推理或其它计费 attempt 工作。这个生命周期是临时测量边界,不是产品用户的持久 conversation store。
Runtime 接入流程
eval-runtime 把应用提供的输入与实现装配为 Core 契约,再驱动 Core 运行。它不维护第二套评分、调度或判定流程。
flowchart TD
INPUT["应用提供 EvaluateInput<br/>用例、版本与执行实现、评分配置<br/>比较与分析、可选判定、运行策略"]
DIRECT["evaluate(input, options)<br/>准备后直接运行"]
PREVIEW["prepareEvaluation(input)<br/>先准备,再决定是否运行"]
CAPTURE["捕获输入与实现绑定<br/>编译 EvaluationDefinition 和 MeasurementPolicy<br/>装配运行能力"]
PREPARE["调用 Core prepare<br/>校验并封存 SealedRunPlan"]
PREPARED["PreparedEvaluation<br/>提供 definition、policy、plan 与 run()"]
INSPECT["应用检查计划"]
RUN["prepared.run(options)"]
CORE["Evaluation Core<br/>执行 → 评分 → 分析 → 可选判定 → 报告"]
RESULT["EvaluationResult<br/>运行状态、证据产物与报告"]
HOST["应用读取结果<br/>自行保存、展示或作业务决定"]
INPUT --> DIRECT --> CAPTURE
INPUT --> PREVIEW --> CAPTURE
CAPTURE --> PREPARE --> PREPARED
PREPARED -->|evaluate 自动继续| RUN
PREPARED -->|显式 prepare 返回应用| INSPECT
INSPECT -->|决定执行| RUN
RUN --> CORE --> RESULT --> HOST
evaluate(input, options) 等价于准备后调用 prepared.run(options)。runId、signal、onEvent 等属于运行选项;prepareEvaluation() 固定评测契约,不执行被测任务。Core 内部各阶段见单次评测流程。