平台宿主集成(评测中心类)
什么时候需要这篇? 你在构建企业「评测中心」类平台:平台统一下发结构化评测用例、评分标准与执行器标识并派发任务;执行侧 Node 脚本用 eval-runtime 串联准备→执行→打分→报告;过程与结果回写平台,各类评测复用同一套标准流程。这篇指南定义宿主侧的集成契约:下发什么、执行侧如何解析实现、如何持久回写过程、如何跨进程回读重评,以及如何治理可比性。
只在单个服务里调用 evaluate() 时,先读在 Node.js 服务中使用;本篇假设你已经理解 executor(执行器)、evaluator(评分器)、variant 与 control/treatment 的基本概念。
定位与边界
- 宿主持有 effect,Core 持有测量语义。 凭证、网络、存储、队列、租户隔离全部由平台宿主实现;OMK Core 只表达可版本化的测量契约与确定性变换,不触碰任何 effect。
- 派发/队列/重试/续跑/租户是 OMK 非目标。 OMK 不提供任务派发、跨进程队列、平台级重试、断点续跑或多租户隔离,这些能力由平台自建。
policy里的 retry 只是单次 sealed 测量契约内的 attempt 级重试,不是任务重调度。 - canonical Report 不可插拔。 测量产生的 Report 字段语义是跨版本可比性的锚点,不能替换成宿主自定义结构。自定义报告把宿主元数据放进 Report 已有的宿主槽位——
summaries与annotations由 run options 传入并原样落进 Report,extensions是 Report 自带的扩展字段。 - 派生视图不覆盖原始证据。 平台可以缓存派生视图加速看板,但原始结果与证据链必须完整保留,且派生视图可以随时从原始证据重新生成。
下发映射契约
EvaluateInput 含代码注入点——variant.execution.executor 的 execute()/openSession()、custom evaluator 的 implementation、评委的 invoke() 都是回调。因此 EvaluateInput 不可能、也不应该整体序列化为一个 wire schema。推荐把下发契约拆成三部分:
| 下发内容 | 载体 | 执行侧映射 |
|---|---|---|
| 评测用例 | 已发布 schema omk.eval-sample-set/v3(schema 文件经 oh-my-knowledge/eval-samples/schemas/v3/eval-sample-set.schema.json 解析) | 编译为 EvaluateInput.dataset.samples |
| analyses/decision/policy/experiment/comparisons 等可序列化测量声明 | 已发布 Core JSON Schema(oh-my-knowledge/eval-core/schemas/v1..v5/*),运行时按文件名用 resolveEvaluationCoreJsonSchema 解析;每个文件名只对应一个版本目录,如 evaluation-definition.schema.json 在 v5、measurement-policy.schema.json 在 v1 | 映射为 EvaluateInput.analyses、decision、policy、experiment、comparisons |
| executor/evaluator/评委/报告逻辑 | 只下发「注册表 id+配置+版本 digest」,不下发代码 | 执行侧按 id 从自有注册表解析实现(见下一节),注入 variants/evaluators |
版本化规则:
- 下发契约自带
schemaVersion;字段增删必须升版本,不靠隐式约定兼容。 - 改评分标准=改测量语义。 修改 rubric、metric 定义或 decision 阈值必须发布新的契约版本,并标记
BREAKING-COMPARABILITY语义:新版本的结果与旧版本不可直接比较,看板上分开展示。 - OMK 的解析在形状上 fail-closed:不做 parser coercion、不静默删字段;已发布 schema 全部是 strict 的,下发内容多出字段或缺少必填字段会在准备阶段报错,而不是在运行时被「修复」。唯一会被补全的是可选策略字段:
prepareEvaluation会把每个省略的可选项物化为文档化的 canonical 默认值——未声明policy.eventDelivery时得到writerMode: 'disabled',writerFailureMode跟随writerMode,未声明retry时得到「一次尝试、无可重试 code」——并把结果封存进 Plan。需要生效值时回读 sealed 的prepared.policy,不要假设实际运行的就是下发的那份 payload。 - 用例集的
requires(工具、文件、环境变量、preflight 命令)是宿主侧依赖预检输入,不进入 Core 测量 digest。执行侧应在调用 Target 前完成预检并失败关闭,不静默降级成「能跑多少跑多少」。
id→实现注册表
执行侧维护一个 versioned registry:按下发的 id 解析实现,并把版本与配置 digest 写进 identity 指纹。
import type { JsonValue } from 'oh-my-knowledge/eval-core';
import type {
CustomEvaluator,
EvaluationExecutor,
Judge,
} from 'oh-my-knowledge';
/** 已发布的 invoke/session Executor 联合类型,不必自己拼。 */
type DispatchedExecutor = EvaluationExecutor<JsonValue, JsonValue | undefined, JsonValue>;
/** 下发契约中的实现引用:只有 id、版本、配置与配置 digest,没有代码。 */
interface DispatchedRef {
readonly registryId: string;
readonly version: string;
readonly configDigest: string; // config canonical JSON 的 sha256
readonly config: Readonly<Record<string, JsonValue>>;
}
const executors = new Map<string, (ref: DispatchedRef) => DispatchedExecutor>();
const evaluators = new Map<string, (ref: DispatchedRef) => CustomEvaluator>();
const judges = new Map<string, (ref: DispatchedRef) => Judge>();
function resolveExecutor(ref: DispatchedRef): DispatchedExecutor {
const build = executors.get(`${ref.registryId}@${ref.version}`);
if (build === undefined) {
// Fail closed:绝不降级,也绝不换一个「差不多」的实现顶上。
throw new Error(`unregistered executor: ${ref.registryId}@${ref.version}`);
}
return build(ref);
}
executors.set('executor:http-qa@2026.09.1', (ref) => ({
executorId: ref.registryId,
version: ref.version,
// parse(value: unknown) 只做校验与收窄,不改写 canonical JSON。
schemas: { input: parseQaInput, output: parseQaOutput },
// 把下发配置 digest 与部署 facet 写进 identity 指纹,
// 可比性评估才能区分「同 id 不同配置」的两次运行。
fingerprintFacets: { configDigest: ref.configDigest, endpoint: ref.config.endpoint },
async execute(invocation) {
return callQaService(invocation.input, invocation.signal, ref.config);
},
}));注册表要求:
- 注册键=
registryId@version;任何影响行为的变更(配置结构、依赖升级、prompt 改动)都必须升版本号。 - identity facet 完整落位:executor 声明
executorId、version、fingerprintFacets;custom evaluator 声明instrumentId与implementation.implementationId/implementation.version/implementation.schemas.fingerprintFacets;评委声明judgeId、version、fingerprintFacets。这些 facet 是可比性评估的 identity 输入。 - capability 自我声明是诚实义务。 determinism、并发安全、取消支持、trace 与 usage 上报能力全部由执行器自我声明(可用
oh-my-knowledge的createInvokeExecutorIdentity/createSessionExecutorIdentity生成完整 capability 清单)。声明了不具备的能力会污染测量结论,而且比崩溃更难被发现。
注册表里的实现不在执行侧进程里时,不必自己拼子进程协议:oh-my-knowledge 的 createSubprocessCommandExecutor() 把「一条命令」包成 Executor。交换协议是版本化的 SUBPROCESS_COMMAND_EXCHANGE_SCHEMA_VERSION(omk.subprocess-command-exchange/v1):请求以一行 canonical JSON 写进子进程 stdin,而子进程的整个 stdout 必须恰好是一份响应文档——结尾换行可以接受,但任何额外输出(调试打印、进度行)都会以 OMK_SUBPROCESS_COMMAND_RESPONSE_INVALID 失败关闭。诊断信息应写到 stderr:stderr 永不被解析,只计入字节上限;单条流超过 DEFAULT_SUBPROCESS_COMMAND_MAX_OUTPUT_BYTES(10 MiB)或超过 timeoutMs 即终止子进程并失败关闭。可执行文件路径、参数与环境变量来自你自己的注册表,摘要后进 command identity facet;子进程只继承 PATH 与你显式声明的环境变量。继承来的 PATH 不在 facet 内,因此裸命令名可能在另一台宿主上解析到不同的二进制,而 fingerprint 保持不变——需要让 fingerprint 钉住二进制时,声明绝对路径的 executablePath。
它只承载 ExecutorInvocation 合法持有的字段,不伪造 run/trial/attempt 坐标或执行计划 digest。调用里出现 leased 受控资源(workspace overlay、原生 MCP 配置、工具调用拦截)时失败关闭,声明这些 capability 也会在构造阶段被拒——需要受控资源时用同进程 Executor。它也不是插件加载器:OMK 不做实现发现、下载或动态装载,注册与版本治理始终是宿主的责任。
持久过程回写
EvaluationRunOptions.onEvent 是有序、best-effort 的进度投影:缓冲有界(eventBufferCapacity,默认 256),消费落后时会丢弃最旧的进度事件、保留最新事件,因此事件序号可能出现缺口;observer 失败也不影响测量终态。它适合做进度条,不适合做审计。
评分阶段的细粒度进度读 evaluation.run.progress 事件,payload 为 { completed, total, failed, maxConcurrency, retry, timeoutMs? }:total 是本次 Run 计划的全部 coordinate(含未评定的),completed 与 failed 随 coordinate 落定单调递增,maxConcurrency/retry/timeoutMs 是评分阶段的执行控制元数据,timeoutMs 未配置时不出现。发射频率由 EvaluateInput.policy.eventDelivery.progressGranularity 控制:per-batch(缺省,按并发宽度每批一条)、per-coordinate(每条 coordinate 一条)、start-end-only(不发中间进度,只保留 evaluation.run.started 与终态事件)。省略该字段即维持 per-batch 且不写入封存 Plan,已有 Plan 的 digest 不变;进度事件属于过程记录,不改变任何测量终态。进度写入在 fail-run 下失败会让本次 Run 停止调度后续批次,但 Evaluator 已经产出的 record 仍保留在 Bundle 中——已被真实测得的 coordinate 不会被改写成未评定证据。
审计级的过程回写用两个字段配合:
- 准备或执行时在
EvaluateInput.policy.eventDelivery声明投递模式(形状即MeasurementEventDeliveryInput,可从oh-my-knowledge具名导入;在 façade 上直接写字面量即可):writerMode取disabled(默认)、optional或required,writerFailureMode取ignore或fail-run。两者按严格校验配对:disabled只接受ignore,required只接受fail-run且缺省即fail-run,optional缺省为ignore。 - 在
EvaluationRunOptions.eventWriter传入写入器,按顺序逐条持久化事件;事件形状见已发布 schemaevaluation-event.schema.json(resolveEvaluationCoreJsonSchema可解析)。完整性只由required保证:写入失败即整次运行失败,过程记录里的空洞不可能被误认为完整。optional+ignore是 best-effort——该阶段首次写入失败后会静默停止持久投递,run 仍然完成,结果里也没有任何字段报告这次截断,因此记录本身就是交付物时不要用它。
const prepared = await prepareEvaluation({
...input,
policy: {
...input.policy,
// 审计运行:写入器失败=run 失败,宁可失败也不留空洞的过程记录。
eventDelivery: { writerMode: 'required' },
},
});
const result = await prepared.run({
eventWriter: {
async write(event) {
await auditLog.append(event); // append-only,逐条持久化,不批量丢弃
},
},
});边界:
- 事件不改变测量终态。 回写成功与否,结果的分数与证据都不受影响,声明了 Decision 时的 verdict 同样不受影响;
required+fail-run改变的是 run 的成败判定,不是测量语义。 - 声明
required却未注入写入器,会在调用任何 Target 之前失败关闭。 每个 Core 阶段都会检查这组配对:evaluate()与prepared.run()会 resolve 出status: 'failed'、error.code为EXECUTION_RUNTIME_EVENT_WRITER_REQUIRED、stage为configuration的结果;rescore/reanalyze/redecide以复用边界自身的 code 拒绝,并把底层 Core code 放进error.cause:依次为EVALUATION_RUNTIME_EVENT_WRITER_REQUIRED、ANALYSIS_RUNTIME_EVENT_WRITER_REQUIRED、DECISION_EVENT_WRITER_REQUIRED。镜像情形在标准入口与复用入口共用同一道守卫并失败关闭:delivery 为disabled却传入写入器,会以EVAL_RUNTIME_INPUT_INVALID被拒。 - 复用失败会说明来源,但不泄露内容。
EVAL_RUNTIME_REUSE_INVALID仍是后缀边界唯一的公开 code,但每次拒绝都有各自的一句话,并带一个脱敏cause,由cause.failureKind区分三类来源:configuration(Run 配置错误,cause.code给出稳定的 Core code)、invariant(OMK 自身断言被破坏)、unknown(阶段抛出的其他失败)。复用前提本身不成立——历史结果与新声明不一致——继续保留独立的一句话,且完全不带cause,因此「这份结果不能复用」不需要从共用文案里猜。cause只装这些稳定 token,不放被包装异常的任何文本;它是非枚举属性,读取error.cause可用,但不会进入序列化 payload。 - 消费侧失败收敛为
EvaluationEventConsumptionError(code 为EVAL_RUNTIME_EVENT_OBSERVER_FAILED或EVAL_RUNTIME_EVENT_STREAM_FAILED),错误对象带runResult承载 Core 终态,宿主可以先落盘再决定告警。
跨进程回读与重评
在另一个进程(复评方、审计方)回读结果的标准链路:
- 保存。 执行侧调用
saveEvaluationResult({ result, store }):结果以版本化 envelopeomk.eval-runtime.stored-result/v1写入宿主注入的ContentStore,classification 恒为gold,mediaType 为EVALUATION_RESULT_MEDIA_TYPE,返回ContentDescriptor引用。 - 重新准备。 另一进程用同一份下发契约走
prepareEvaluation,得到PreparedEvaluation;其 sealed plan digest 就是回读的准入凭证——契约不同,plan digest 不同,回读直接被拒。 - 加载。
loadEvaluationResult({ prepared, reference, resolver, verifier })。除了解析内容与校验 digest,还必须注入独立的EvaluationResultVerifier:verifier 要认证 envelope digest(verifiedResultDigest必须等于 reference 的 digest),并独立认证 provenance bundle、cache record、policy execution 三组 digest 集合。只做 checksum 不够——存储完整性不等于来源可信。验证失败以EvaluationResultStoreError关闭;无法structuredClone的 JSON 同样关闭,不做部分恢复。 - 重评。 按需
rescore(复用 Execution,重新打分)、reanalyze(复用打分,重新分析)、redecide(复用分析,重新决策),或用assessComparability比较两份 canonical 结果。
可运行的参考样板见 examples/eval-runtime/result-store.mjs(临时目录上的文件后端 ContentStore/ContentResolver,在存储边界校验 digest,另配一个独立的审计回执 verifier);单进程视角的同一流程见在新进程里读回历史结果再复用。
先执行、后评分
当 Target 调用必须在评分标准确定之前跑完——标注队列还在运转、评委 rubric 仍在评审,或一次执行要服务多个候选版本——就把两个阶段拆开,而不是回读一次已评分的 Run:
- 执行。
executeEvaluation(input, options)只运行 Execution stage,返回ExecutedEvaluation句柄(runId、executionPlanDigest、executionInputDigest、ExecutionBundle本体,bundleOrigin: 'runtime')。不会尝试任何 Evaluator、评委、Analysis、Decision 或 Report。 - 封存。
saveExecutedEvaluation({ executed, store })以EXECUTED_EVALUATION_MEDIA_TYPE写入 envelopeomk.eval-runtime.stored-executed/v1,分类固定为gold。它只承载 Target evidence,绝不能作为 context 回流给 Target。 - 重新接纳。
loadExecutedEvaluation({ reference, resolver, verifier })校验存储完整性与宿主认证,返回bundleOrigin: 'store'的句柄。与已存 result 不同,执行 envelope 与声明无关:这一步会比较的内容里没有 plan digest,因为一个 envelope 本就服务于后续多份评分声明。 - 评分。
scoreExecutedEvaluation(newInput, executed, options)先封存新声明,再按rescore使用的同一条 Core 规则准入该 envelope,并以零次 Target 调用执行 Evaluation 及其后续阶段。每个评分版本重复一次。
有两条边界记录在宿主侧,而不是由 OMK 代劳:
- 每份评分 Run 都要绑定它复用的 envelope。 把
executed.bundle.bundleDigest与每个评分runId一起持久化。看板展示三份评分结果却不说明它们共享同一次执行,读者就会把三个评分版本当成三次独立测量。 - 重新接纳的 envelope 无法靠存储恢复信任。 verifier 未认证 provenance bundle 时该状态保持 indeterminate:评分 Report 照常完成,但只能声称
unknownprovenance,声明了 Decision 也会被门槛拦住。只靠校验和的 verifier 重新接纳属于诊断路径,不是发布证据。
可运行的样板 examples/eval-runtime/staged-execute-score.mjs 离线走完整个拆分流程;调用侧的叙述见拆分为先执行、后评分。
可比性治理工作流
- identity facet 从第一天进下发协议。 executor 的
executorId/version/fingerprintFacets、evaluator 的instrumentId与 implementation 版本、评委的judgeId/version、所用 schema 的 digest——全部属于测量身份,应当是下发契约的固定字段,而不是事后补充。 - 评测用例集同样要版本化。
omk.eval-sample-set/v3文档自带schemaVersion,但EvaluateInput.dataset只有datasetId、样本内容与可选的analysisCohorts/annotations,没有版本字段:样本内容变化会改变 sealed plan digest(跨进程回读必须用同一份声明),而datasetId本身不会告诉你标注是否被改过。平台应把用例集版本与内容 digest 作为下发契约的固定字段,并写进dataset.annotations,让看板与assessComparability的 reason 能区分「同 id、不同标注」的两次测量。修正标注后用rescore()重评属于新的测量版本,必须与修正前的结果分区展示。 - 评分标准变更走版本化流程:发布新契约版本→标记
BREAKING-COMPARABILITY→新旧结果在看板分区展示。仓库内的 rubric 评委 prompt 由test/measurement-governance的 prompt registry 冻结治理,任何字节漂移都会被测试拦截;平台自有的评委 prompt 也应有同样的冻结与版本管理。 assessComparability的三个状态独立解读:designStatus(compatible/incompatible)看测量设计是否可比;evidenceQualificationStatus(verified/conditional/rejected)看证据认证是否完整;comparabilityStatus(compatible/conditional/incompatible)是派生的总体结论。设计可比不代表证据过关,反之亦然。- 看板分区展示。 不可比(incompatible)的结果单独分区并展示 reason code,不与可比结果混在同一条趋势线里;conditional 的结果标注限制条件后再展示。
Gold 与访问控制
stored result 恒为 gold 分类——包含原始输出、trace 与完整证据链。宿主的 ContentStore 必须实施与 gold 匹配的访问控制:最小授权、租户隔离、访问审计。
回写评测中心的副本应定位为下发配置+派生状态/视图(进度、verdict 摘要、报告投影),用于展示与检索;gold 原件与其证据链保留在受控 store 中,派生副本永远不替代原件。