跳转到主要内容
在断言或 judge 之前,先要让 agent 动起来。Drivetest(t) 里发送输入、拿到结果的那部分——t.send()t.sendFile()t.newSession(),以及 HITL 的 t.respond() / t.respondAll()。驱动产出的都是一个 Turn,NiceEval 里所有断言和 judge 都从 Turn 的数据上读,具体看 AssertJudge

t.send() 和它返回的 Turn

t.send(input) 是唯一的动词。它底层调用 agent adapter 的 send(input, ctx),把返回值归一成一个 Turn
const turn = await t.send("布鲁克林今天天气怎么样?");

turn.message;   // 最后一条 assistant 消息
turn.data;      // 结构化输出(如果 agent 返回了)
turn.status;    // "completed" | "failed" | "waiting"
turn.events;    // 这一轮的 StreamEvent[]
turn.usage;     // { inputTokens, outputTokens, ... },adapter 报了才有
turn.expectOk(); // status === "failed" 时抛出;否则原样返回 turn
t.reply 是主 session 最后一条 assistant 消息的简写,等价于最近一次 t.send() 返回的 turn.messaget.events 是主 session 目前累积的完整事件流。
turn.expectOk() 是前置条件检查,不是评分断言:它立即抛出(中止 test(t) 剩余部分),而不是记一条失败断言。后面几轮依赖前一轮成功时用它——在一个已经失败的轮次上继续挂断言,通常只会产出一堆混乱的连带失败。

带文件的一轮 —— t.sendFile()

t.sendFile(path, text?) 读一个本地文件(相对 eval 所在目录),按扩展名推断 MIME 类型,把它作为 data URL 附加到这一轮的输入里:
const turn = await t.sendFile("fixtures/invoice.png", "这张发票的总额是多少?");
t.check(turn.message, includes("$"));

多轮对话

每次 await t.send(...) 都是同一个对话上的新一轮。把每轮的返回赋给局部变量,除了 run 级的 t 断言外还能单独断言这一轮:
const draft = await t.send("帮我拟一封跟进邮件。");
draft.expectOk();
t.check(draft.message, includes("此致"));
draft.judge.autoevals.closedQA("语气是否专业").atLeast(0.6);

await t.send("好,发出去。");
t.calledTool("send_email");
多轮 t.send() 依赖 agent 声明 conversation 能力——没声明时,adapter 没有义务记住上一轮说了什么,第二次 t.send() 可能接进一个全新、无关的上下文。完整能力列表见 Agents & Adapters

独立会话 —— t.newSession()

t.newSession() 开一条与主会话并行、互不干扰的第二条对话线。它返回的 session handle 带同一套 drive API(send / sendFile / respond / respondAll)和同一套作用域断言词汇,但只看这条 session 自己的事件:
await t.send("我叫小明。");
await t.send("我叫什么?");
t.check(t.reply, includes("小明"));         // 主 session 记住了

const fresh = t.newSession();
await fresh.send("我叫什么?");
t.check(fresh.reply, satisfies((r) => !r.includes("小明"), "没有记忆泄漏"));
常见错误是想当然认为 t.newSession() 天然保证隔离。隔离与否完全取决于 adapter 是否读了 ctx.session.isNew 并照做——一个忽略 isNew、永远续接同一个底层上下文的 adapter,会让 t.newSession() 悄悄共享状态,且不会报错。写 adapter 时,先用上面这条 eval 验一下隔离,再信任它。

人工介入(HITL)

有些 agent 会在一轮执行中间停下来,等审批或缺失信息,而不是直接跑完。这时这一轮以 status: "waiting" 结束,并带一条或多条 input.requested 事件说明在等什么。
const draft = await t.send("拟一封跟进邮件,但先别发,等我确认。");
draft.parked();                              // t.parked() 断言 status === "waiting"

const request = t.requireInputRequest({
  prompt: /是否发送/,
  optionIds: ["approve", "reject"],
});

await t.respond({ request, optionId: "approve" });
t.calledTool("send_email");
t.requireInputRequest(filter) 把一个待处理的 HITL 请求变成可检查、可回应的具体值——如果匹配到 0 个或超过 1 个待处理请求就会抛出,所以尽量把能填的 filter 字段都填上(id / prompt / display / action / optionIds / input)来消歧。t.respond(...) 回答它并发出下一轮;在底层它只是又一次带着你的回答的普通 send 如果当前轮有多个同类待处理请求、都该给同一个答案(比如逐个批准一批文件改动),用 t.respondAll(optionId) 一次性处理,不用挨个解:
await t.send("把这批改动逐项提交审批。");
t.requireInputRequest({ display: /审批/ });

await t.respondAll("approve");
t.succeeded();

相关阅读

  • Assert — 从 Turn.eventsTurn.data 上读的断言词汇。
  • Judget.judge / session.judge / turn.judge 各自默认评什么材料。
  • Agents & Adapters — 哪些能力位解锁 t.newSession()、HITL 和工具相关断言。