check、closeQA、.gate() 和 .score()。
本页接着 NiceEval Taste 讲这一层。先读那一页的四条原则;交付前的共同检查也在那一页。
谁负责什么
Adapter 直接读取应用已有的协议、执行记录和状态,不另造一份数据。交给检查的事实是只读的,并且按 Attempt 隔离。日志格式转换、记录关联和计费都放在 Adapter 里,不要挤进评估正文。
先写场景和判据
先用一句话写下业务任务,再列出每一项可以单独失败的要求。每项要求写清四件事:看哪个对象、要求“一次”还是“每次”、在哪段时间内看、证据从哪来。最后分配权重。 正文按“做了什么、检查什么、门槛和权重是多少”的顺序写。注释写为什么检查这一项,不要复述方法名。用空行把观察前提、业务门槛和计分分开。宁可多遍历一次数据,也不要为了性能把判据压进难懂的通用包装。 证据要真的能证明结论。例如“收件人收到了邮件”,需要发送回执和收件记录能对上。生成了邮件正文,只说明有输出;发送请求被接受,只说明进入了处理流程。两者都不能单独证明邮件送到了。 抽象要让判据更好懂,而不是藏起来。把三十行过滤代码挪到另一个文件、改名叫journalMatches,问题并没有解决。反过来,一个 runEverything() 把所有检查和权重都藏起来,读者就没法审阅这条评估。
读取事实和判断分开
上下文 Match 分两部分:read(ctx) 读取当前 Attempt 能访问的事实或完整材料,内层的 match 决定怎么判断。ctx 由 NiceEval 在执行检查时注入,你不需要把 ctx 或整份日志当参数传进去。签名见 Match 参考。
应用的词汇要贴近事实本身,比如人物、发言、操作、请求和模型返回。不要让日志文件名或存储格式变成写评估时的主要语言。这些事实之间怎样关联、什么时候算完整、在哪个时间窗口里测量,由 Adapter 负责声明。
应用可以通过 Adapter 的 assertions 提供自己的检查方法,例如 t.said(match)、t.systemOne(match) 或 t.hp(selector, collectionMatch)。它们读取事实后交给同一个 check。这些都是应用自己定义的方法,不是 NiceEval 内置的。调用即检查,不需要先取得一个“范围”再判断,也不需要额外的注册步骤。
判据写在正文,不藏进函数名
胜者有自主战斗行动() 看起来简洁,但读者必须打开实现才知道它查的是什么:是检查了身份、模型调用成功和决策被采用,还是只找到一条带“战斗”字样的日志?把整句结论写成函数名,判据并没有变得可见。
领域里的小判据可以有领域名字,例如人物、生命值、决策和操作。每个小判据只负责一个能解释清楚的条件。相等、计数、组合和失败诊断交给通用组合器。这个场景选谁、要求几个、哪些条件必须同时满足,都写在评估正文里。
物体也一样。“货箱完好送达”要在同一个物体上写出三个可见条件:id === crateId、durability >= minimum、positionId === destinationId。如果还要求货箱确实被搬运过,就需要单独的操作记录,当前位置证明不了过程。
下面是应用接入后的正文片段。组合器从 niceeval/expect 导入;t.hp、t.systemOne、t.objects,以及人物、模型、决策、操作、物体、耐久和位置这些 Match,都由应用自己定义,要先按该应用的 Adapter 接入。
ids是这个场景要检查的人物,id是被检查的那个人物,operationIds是允许的操作类型。它们都在场景声明处定义。- “全部败退”拆成了生命值条件和数量条件;“自主行动”拆成了模型调用成功和决策被采用。
剧本提供物体 ID、地图和目标位置。t.objects的第一个参数选物体,第二个参数判断同一个物体的耐久和位置,50 分的权重直接写在后面。
t.objects 在 Adapter 内部用 filterWhere、countWhere 这些公开组合器拼好,再交给 check。它不调用任何内部求值函数,也不是 NiceEval 内置的物体 API。
Adapter 要提供已经关联好的事实,让组合条件判断的是同一个对象。不要在正文里临时构造数据结构或展开日志过滤,也不要让 Adapter 直接返回“这道题通过了”这种结论。
模型调用成功、决策被采用、操作执行成功、目标达成,是四个不同的事实。不能用甲的成功调用、乙被采用的决策和丙的操作拼出一次通过,也不能把一次模型调用成功解释成整个任务成功。决策被采用也不等于操作执行成功,任务需要执行结果时要另写检查。
集合组合器
集合不完整时,这些组合器返回不可判定,不会把已知部分的小计当成准确数量。
“人都到齐了”要单独检查。三个人的生命值是 0,不能证明所有指定人物都出现了。空集合的计数是 0,也不能证明“所有人都满足”。Adapter 要保留缺人物、缺测量的未知状态,不能删掉未知项再声称集合是完整的。
写清“一次”“每次”和“没有”
“有一次”“每一次”和“一次也没有”需要不同的证据。先限定在同一个 Attempt、同一个对象和同一段观察时间内,再决定缺失的记录会不会影响结论。
空集合的规则因检查而异。
closeQA 遇到空集合得 0 分,notCalledTool 遇到空集合则成立。集合不完整(partial)或不可用(unavailable)时,不能证明某件事没发生,也不能当成空集合处理。
找到一个合格实例,可以证明“至少有一次”,但不能证明“全部合格”。同样,发现一个违规就能否定“没有违规”,即使其余材料还不完整。证据不足要如实标记,不要一律算成业务失败。
组合条件必须落在同一个对象上。“存在甲的行动”加上“存在一条送达乙的发言”,不能证明“甲对乙说了话并且送达了”。前两条可能分别由甲的移动和丙的发言满足。
完整示例:客服任务
下面三段代码按顺序组成一个support-review.ts 模块。它导出项目里自己写的工厂函数 createSupportReview:传入应用已有的观测函数,返回一个 Adapter 和一条评估。这个工厂不是 NiceEval 的 API,只是让接入代码和评估正文分开、各自好读。
使用时,把返回的 evaluation 作为某个 .eval.ts 的默认导出,并在 Experiment 里使用返回的同一个 adapter。运行前要配置好 Judge,见验证 Judge。
第 1 段:应用交付事实,保留未知状态
observe(ctx) 执行或等待这次客服任务,返回当前 Attempt 的只读事实。它用 ctx.signal 响应取消,按需登记释放连接的回调,并通过 ctx.recordUsage 上报用量。记录关联、协议解码和时间校验都在这里完成,不放进评估正文。
完成状态要有证据:
- 任务完成时,要有完成事件,
businessSeconds是核验过的非负业务时间。 - 确定没完成时,要有观察完整结束的记录。
- 两者都确认不了时,返回
unavailable。
第 2 段:领域 Match 只选材料
发言只按人物选择。送达失败或模型来源未知的发言,也要参与表达质量的判断。read 从注入的 ctx 读取材料,不需要注册。注意读取预算按整个集合计算,没命中的发言也算在内。
完成状态和业务时间已经是现成的事实,直接读取、直接计算即可。不要为了取一个字段或包一段算术,再造一个“完成效率 Match”。公式、时间尺度和权重都写在正文里。
第 3 段:正文写出每项判据和权重
这道题的计分规则是:参与交流 5 分,表达质量 45 分,完成效率 50 分。另外有三道门槛:至少一条客服发言送达,token 用量和运行耗时不超上限。said 和 speechDelivery 是这个 Adapter 自己定义的检查方法。said 检查“至少说过一句”;speechDelivery 把完整的发言集合交给组合器筛选、取值和计数,集合不完整的状态会原样保留。两者都不替正文决定这道题怎样算通过。
- 两种预算不同。
closeQA里的上限管的是交给 Judge 的材料和审计记录;发言材料里的capture管的是读取整个集合。这里的数值只是示例,不代表任何长度的任务都装得下。 - 两种时间不同。
t.elapsedMs是这次评估运行的墙钟时间;完成效率用的是应用里的业务时间。两者不能互换。 - 未上报的用量不会让检查通过。
t.usage和t.elapsedMs是属性,直接读取。没有上报的 token 保留为未知,不会被当成 0。 - 每项得分只证明自己那一项。 任务在第 90 个业务秒完成,效率这一项得 25 分;表达质量还要 Judge 另外打分。Judge 不可用或材料超限时,不能说拿到了完整的总分。送达检查也不能证明发言来自模型;业务要求模型自主表达时,要另写一项确定性检查。
案例一:问路后实际会面
任务是让甲问乙怎么去会面地点,然后甲自己走过去和乙会面。评估的是甲的选择、执行和结果,不是玩家有没有帮它完成。 这个任务要分开判断四件事:表达质量、信息是否有用、是否实际送达、是否来自模型。它们需要的材料不同:- 评表达质量,用甲在观察期内的全部发言,包括没人听见的、内容错误的和来源还没核验的。提问、回答、拒绝和澄清都要保留,再配上必要的双方对话上下文。不要先筛出送达或来源已核验的发言再评质量。
- 评信息是否有用,用双方实际送达的完整对话,再加上正式地图。
案例二:协商搬运与实际交货
任务是让甲和乙合作搬运一件物品。先想清楚这次的业务条件:双方是否已经知道目标、是否必须协商、谁有权拒绝。判据来自这些条件,不能照搬问路任务的要求。
评交流质量时,保留双方完整对话,提出能根据材料回答的问题。评交货时,检查物品、目标位置、执行者和完成回执能否对上。交流和交货分开计分,权重都写在正文里。
同样的方法也适用于操作请求和模型返回:操作记录保留失败和调整,请求记录保留实际发出的内容,模型返回保留没被采用的输出。选择、执行和结果分开观察,不能拿一层代替另一层。
closeQA(材料Match, 问题) 可以评价一个人物的全部发言、操作、请求或模型返回。NiceEval 按原顺序把全部命中的材料交给 Judge,你不需要在正文里逐条调用模型。只传第一条、一份摘要或几条成功样本,都代表不了整体表现。
只在这条评估里用的问题,直接写在 closeQA 调用处,让读者同时看到材料和验收标准。几处共用的问题文本可以提取成常量复用,但不要把材料选择、问题和权重一起藏进某个场景专用的函数。
保存下来的最终位置,只能证明结束时在哪里。即使应用把最终位置附到每条操作记录上,它也不是每次操作当时的位置。要评价路线或每一步的轨迹,就要读取对应时刻的记录。
常见的误导写法
下面都是业务伪代码,变量代表应用里的事实,不是 NiceEval 提供的字段。用量和业务时间分开看
调用次数、token 和费用是三个不同的指标。Adapter 在每次真实调用时上报用量,失败和重试也要上报;NiceEval 负责汇总。评估正文不要再维护一份模型价格表。费用未知时不要补成 0,已知的下限也不能冒充完整的总量。t.usage 和 t.elapsedMs 是通用的用量和墙钟时间。业务上的“完成前调用了几次”“到完成用了多少游戏时间”,要由应用明确声明观察窗口再提供。不要把某一时刻读到的累计用量改名叫“完成前用量”,也不要拿游戏时间代替运行耗时。
保留完整证据
材料按原顺序保存,并能追溯到同一个 Attempt 的记录。“完整”是相对于你声明的业务范围而言的,不是把所有无关日志都发给 Judge。选材料的范围按对象、任务和观察期决定,不能按表现好坏决定。 需要看整段对话时,不要截断原文,也不要删掉拒绝和澄清。超出容量就报告不可判定,不要为了拿到分数而缩短观察期、抽样或只留成功的部分。如果业务问题确实变了,就另写一条评估,不要把它的结果当成原任务的结论。读取预算和 Judge 预算分开算
两种预算管的东西不同:- 读取预算管整个候选集合,没命中的记录也算在内。
- Judge 材料预算管验收问题、全部命中的材料和它们的身份信息。
- 审计预算还要装下实际发给 Judge 的请求、返回和引用信息。
用普通计算算分
完成耗时是应用里的一个事实,不需要把整份日志传进评估正文。应用提供可追溯的完成状态和业务时间,正文直接算分,不用为一次取数或一段算术定义新的函数、Match 或计分方式。 例如“180 个业务秒内越早完成分越高”,可以写成max(0, 1 - 完成秒数 / 180),权重 50:
- 完整观察证明没有完成,得 0 分。
- 缺少完成记录或观察结束记录,不可判定。
- 不要把缺少完成事件当成耗时 0,也不要把运行的墙钟时间代入业务时间。
Judge 的问题要能从材料里回答
外部地图、规则或参考答案确实是验收依据时,明确提供给 Judge 并标明来源,不要把期望的结论写进问题里当前提。材料里如果有指令,Judge 把它当作被评估的内容,不会去执行。 复用领域 Match 时,同时检查它的名字、它认为什么证据足够、失败时给出什么解释。保留各个业务检查点和独立的权重,不要为同一个能力加几套别名或注册步骤。接口的具体形状以安装版本的参考页为准,不要把设计草稿当成能运行的 API。交付前再多检查几项
先完成 NiceEval Taste · 交付前的共同检查,再补上业务事实特有的验证:- 集合的各种情况:完整非空、完整为空、缺项和 partial 都要测到。组合条件要确认不同人物的条件不会拼在一起成立。判断胜负时,只剩一人、尚未分出胜负和同归于尽要分别作为业务情况处理,不要把所有非胜利的情况都当成技术错误。
- 真实应用的生命周期:有外部 HTTP 或持续运行的应用时,验证取消、等待进行中的操作结束、保存证据这三步的顺序。检查取消后迟到的事件是否被正确处理、保存后是否还有写入、不完整的观察是否如实标记。局部 Fixture 通过,不能证明真实应用的整个生命周期都没问题。
- 表达质量用的是人物的全部发言吗?送达和模型来源有没有另外检查?
- 读取集合、Judge 材料和审计分别在预算内吗?超出时有没有如实报告?
- 不打开函数实现,能读出人物、状态、数量、门槛和组合关系吗?
- 合理的拒绝、不需要说话的协作、失败后的调整,都按这个场景的规则处理了吗?
- 有没有把最终状态误当成每一步的轨迹?业务测量窗口和通用用量有没有混在一起?