在断言或 judge 之前,先要让 agent 动起来。Drive 是 test(t) 里发送输入、拿到结果的那部分——t.send()、t.sendFile()、t.newSession(),以及 HITL 的 t.respond() / t.respondAll()。驱动产出的都是一个 Turn,NiceEval 里所有断言和 judge 都从 Turn 的数据上读,具体看 Assert 和 Judge。
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.message;t.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.events 和 Turn.data 上读的断言词汇。
- Judge —
t.judge / session.judge / turn.judge 各自默认评什么材料。
- Agents & Adapters — 哪些能力位解锁
t.newSession()、HITL 和工具相关断言。