跳转到主要内容
NiceEval 里,Adapter 是连接运行器和被测系统的适配层。Adapter 需要实现 send 函数:把 t.send() 等评估侧动作发给你的应用,再把应用返回翻译成 NiceEval 的标准 Turn 后续阅读写 send

适配器

如果被测系统使用标准的 OpenAI Chat Completions 或 Responses 协议,可以直接用官方适配器。自己实现的前后端协议(HTTP、gRPC、WebSocket 都行)则写一个 Adapter:它知道怎么鉴权、怎么调用你的应用、怎么把返回翻译成标准事件流。 一次 t.send() 的完整往返,穿过这条边界两次: 一次 t.send 的完整往返:评估用例调用 t.send,运行器组装 TurnInput 与 ctx,adapter 调用你的应用并返回标准事件流 Turn。 去程:评估用例里的驱动动词(t.send / t.sendFile / t.respond)组装 TurnInput 和本会话线的 ctx,调一次 Adapter 的 send 回程:Adapter 把应用的原始返回翻译成 Turn——其中 events 是一个按发生顺序排列的事件对象数组(下文有实际值) 被测系统的 URL、鉴权、协议细节通过 Adapter 工厂的配置传入。defineExperiment 里放的是已经配置好的 agent 实例;Adapter 在 send 闭包里消费这些配置,再把每轮动态值(如 ctx.modelctx.flagsctx.telemetry)随请求转发给应用:

接入等级

按「Adapter 接到哪里、额外拿到什么观测数据」,接入分三级:Tier 1 只接 send(应用代码一行不改,全套断言在这一级就齐了)、Tier 2 send + OTel(应用把 span 发给 NiceEval,换 niceeval view 的调用瀑布图)、Tier 3 侵入改造 + experiment flags(feature A/B)。每档投入什么、买到什么、什么时候升级,见 Tier 评估侧的驱动 API——t.send()t.sendFile()t.newSession()、HITL 的 t.respond() / t.respondAll() 统一都是调用 Adapter 的 send 怎么收敛、send 里怎么接,见写 send

send 函数

不管 Adapter 连的是 HTTP 服务还是 Sandbox 里的 CLI,暴露给运行器的接口完全一致:
progress 是运行中的短期状态,不落盘;diagnostic 是需要运行后回顾的有界 warning/error,会随 Attempt 保存。它们都不能指定 lifecycle phase,也不会自动改变 Turn.status。连接失败或响应无法解析时抛出异常;runner 会把它记录为 errored,并在终端给出可下钻的 locator。 send 是唯一要实现的函数。它的签名里只有三个类型,分开看。
这里的 setup / teardown 是 Agent 自己”怎么连自己”的私事(装 CLI、写鉴权配置)。按实验变化的环境准备(装某个实验专属的二进制、预热、跨 attempt 保存状态)不写在 Agent 上,挂在 sandbox 字段那个 Sandbox spec 自己的 .setup() / .teardown() 链式方法上,见 Sandbox provider · 生命周期

传入:TurnInput

评估侧不管调的是 t.send()t.sendFile() 还是 t.respond(),到 Adapter 时都是一次普通的 send,差别全在字段上: 如果你的应用接口不收文件就忽略 files files 接口不收文件就忽略; outputSchema 应用接口收 schema 就转发(Chat Completions 的 response_format、Responses 的 text.format),不收也没关系——校验反正发生在运行器(见下面 Turn.data 的规则)。

不同回答的入参

HITL 回答轮里,人的裁决以结构化形式随 input.responses 到达——Adapter 不需要解析 text 去猜哪句回答对应哪个请求、算不算批准。每条回答必带 requestIdoptionIdtext 二选一:回答命中请求 options 里的某个 id 就是 optionId(评估侧已校验过存在,不会有打错的字静默传进来),否则整句作为自由文本落在 text。四种典型形态:
Adapter 侧的对应义务:按 requestId 把裁决交回应用(不要按顺序猜);被人拒绝的调用把 action.resultstatus"rejected" 而不是 "failed"——拒绝是人的决定、不是工具故障,这样 noFailedActions() 不误伤,calledTool(..., { status: "rejected" }) 可精确断言。

上下文:AgentContext

Runner 为 Agent 的 setup、每次 sendteardown 分别绑定 lifecycle scope。Adapter 只报告当前回调内部的 progress/diagnostic,不能传 phase、颜色或输出流。progress 不落盘;diagnostic 会进入 Attempt 的 result.json。无法继续时抛错,由 runner 保存结构化 error 并把 Attempt 标为 errored ctx 里没有要你检查的标志位。三个档位字段都是”透传”语义:modelflags 由 experiment 声明、运行器原样递过来,Adapter 只负责随请求转发给应用,不解释它们的含义;telemetry 仅在配置了 OTel 接入时出现,send 里只需要把 headers spread 进请求头——接收端点在 defineConfig 里固定、由应用启动时指向,不从这里传,见OTel 接入experimentId 是路径推导出的稳定标识,典型用途是在 Sandbox 的生命周期里按实验隔离跨 attempt 的状态(缓存目录名、Sandbox 快照 tag 按它分区),见 Sandbox provider · 生命周期 session 是本条会话线的自有状态,NiceEval 对它只承诺一件事:同一条会话线的每次 send 拿到同一个 ctx.session,新会话线(评估用例的第一轮,或 t.newSession() 之后)拿到一个全新的。 会话续接(id/capturehistory)和 HITL 停轮现场(hold/take)的存取器都在它上面,“第一轮”就是新会话线的自然形态——idundefinedhistory.get() 是空数组,没有要判断的分支;state 是这些存取器之外的逃生舱,框架从不往里写数据。

返回:Turn

events 就是一个普通的 JS 数组:这一轮里发生的每件事一个对象,按真实发生顺序排列。比如”北京今天多少度?”这一轮,agent 先查天气再回答,events 长这样:
对象统共十种类型(messageaction.*input.requested……完整清单见事件流参考)。断言读的就是这个数组:t.calledTool("get_weather")action.calledt.reply 取最后一条 assistant message。注意 send 每次只返回本轮的数组——跨轮拼成整条会话线是运行器的事,见下一节。多数情况下你也不手写这些对象:官方转换器的返回值就是一个填好 eventsusagestatus 的完整 Turn,怎么选转换器见写 send data 不是”随便放点什么”的口袋,它只有一条规则:要什么,由评估用例在 send 上用 schema 声明;声明了才有 data,拿到手就是声明的类型。
声明降为 JSON Schema 后经 input.outputSchema 到达 Adapter;Adapter 拿到应用的返回后把结构化产物放进 data(比如 JSON.parse(reply.content))。校验是运行器强制的data 不匹配声明,本轮直接 failed 并报出差异——不存在”塞了个错的对象、断言静默通过”这条路。反过来,没声明 output 的轮次没有 data:只回文本的应用永远不碰这个字段,不要把原始响应 body 复制进去凑数。

send 返回之后:Turn 的四个字段各去哪

send 一 return,Adapter 的工作就结束了——剩下全是运行器的事。四个字段各有明确去向: await t.send() 拿到的句柄就是对这一轮 Turn 的视角:turn 级断言(draft.parked() 这类)只看这一轮的 eventst 级断言看整条会话线累计的。所以 Adapter 不需要”配合”任何断言——把这四个字段填对,断言、评分、报告全部在下游自动发生。

能力从哪来:构造证明,不是问卷

负断言那行是唯一分档次的:notCalledTool 断的是”没发生”,前提是”事件全量”——这只有来源本身有完整性契约时才成立(SDK 原生事件流透传、AI SDK 的 result.steps、Responses 的 output)。手写映射没有证明,负断言会在运行时提示不可信,而不是静默假通过。所以也不存在”声明了做不到”的失真:可信度跟着事件的来源走,不跟作者的自觉走。逐能力的精确义务见能力参考

相关阅读

  • 写 send — 实操教程:从发一条消息到完整接入,七步递进,每步解锁一组断言。
  • 接入你的 Agent — 接入全景:最小接入、参数通道与增量地图。
  • Drive — 评估侧视角:t.send()t.newSession() 与 HITL 怎么用。
  • Assert — 标准事件流驱动的完整断言词汇。
  • 架构概览 — 四层架构和边界。