> ## Documentation Index
> Fetch the complete documentation index at: https://niceeval.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 自定义应用的评估判据：业务事实、领域 Match 与普通计算

> 被测应用提供客服记录、游戏状态这类业务事实时，让 Adapter 交付事实，用领域 Match 组合小条件，在正文里写普通计算，并保留未知状态。

被测对象提供客服记录、游戏状态这类业务事实时，判据比 Agent 类评估多一层：Adapter 先把事实交给 NiceEval，评估正文再组合条件、计算分数。正文用的仍是同一套 `check`、`closeQA`、`.gate()` 和 `.score()`。

本页接着 [NiceEval Taste](/docs/zh/tutorials/niceeval-taste) 讲这一层。先读那一页的四条原则；交付前的共同检查也在那一页。

## 谁负责什么

| 谁 | 负责 |
| - | - |
| 评估正文 | 写场景、观察步骤、每项检查、业务判据和权重，并说明后续步骤依赖哪些前提 |
| Adapter | 从应用读取事实，给当前 Attempt 提供带类型的上下文；管理连接和清理，关联记录，上报用量 |
| 领域 Match | 提供身份、状态、操作类型这类可复用的小判据，并在失败时给出诊断。整条验收条件由正文组合 |
| NiceEval | 提供通用检查、组合求值、门槛、计分、停止控制、Judge、统计、取消和证据审计 |

Adapter 直接读取应用已有的协议、执行记录和状态，不另造一份数据。交给检查的事实是只读的，并且按 Attempt 隔离。日志格式转换、记录关联和计费都放在 Adapter 里，不要挤进评估正文。

## 先写场景和判据

先用一句话写下业务任务，再列出每一项可以单独失败的要求。每项要求写清四件事：看哪个对象、要求“一次”还是“每次”、在哪段时间内看、证据从哪来。最后分配权重。

正文按“做了什么、检查什么、门槛和权重是多少”的顺序写。注释写为什么检查这一项，不要复述方法名。用空行把观察前提、业务门槛和计分分开。宁可多遍历一次数据，也不要为了性能把判据压进难懂的通用包装。

证据要真的能证明结论。例如“收件人收到了邮件”，需要发送回执和收件记录能对上。生成了邮件正文，只说明有输出；发送请求被接受，只说明进入了处理流程。两者都不能单独证明邮件送到了。

抽象要让判据更好懂，而不是藏起来。把三十行过滤代码挪到另一个文件、改名叫 `journalMatches`，问题并没有解决。反过来，一个 `runEverything()` 把所有检查和权重都藏起来，读者就没法审阅这条评估。

## 读取事实和判断分开

上下文 Match 分两部分：`read(ctx)` 读取当前 Attempt 能访问的事实或完整材料，内层的 `match` 决定怎么判断。`ctx` 由 NiceEval 在执行检查时注入，你不需要把 `ctx` 或整份日志当参数传进去。签名见 [Match 参考](/docs/zh/reference/expect)。

应用的词汇要贴近事实本身，比如人物、发言、操作、请求和模型返回。不要让日志文件名或存储格式变成写评估时的主要语言。这些事实之间怎样关联、什么时候算完整、在哪个时间窗口里测量，由 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 接入。

```ts theme={null}
import { and, countWhere, equals, greaterThan } from "niceeval/expect";

// 指定人物中，生命值恰为 0 的有 3 个。
t.hp(npcMatch({ ids }), countWhere(equals(0), equals(3))).gate();

// 同一行动的身份、模型结果、决策采用与操作类型必须同时满足。
t.systemOne(and(
  npcMatch({ id }),
  modelMatch({ outcome: "succeeded" }),
  decisionMatch({ outcome: "adopted" }),
  operationMatch({ ids: operationIds }),
)).gate();

// 选择指定货箱，要求它仍有耐久且位于目标位置。
t.objects(
  objectMatch({ id: 剧本.carry.objectId }),
  and(
    durabilityMatch(greaterThan(0)),
    positionMatch({ map: 剧本.map.name, ...剧本.destination }),
  ),
).score(50);
```

这段代码里，每个条件都能直接读到：

* `ids` 是这个场景要检查的人物，`id` 是被检查的那个人物，`operationIds` 是允许的操作类型。它们都在场景声明处定义。
* “全部败退”拆成了生命值条件和数量条件；“自主行动”拆成了模型调用成功和决策被采用。
* `剧本` 提供物体 ID、地图和目标位置。`t.objects` 的第一个参数选物体，第二个参数判断同一个物体的耐久和位置，50 分的权重直接写在后面。

`t.objects` 在 Adapter 内部用 `filterWhere`、`countWhere` 这些公开组合器拼好，再交给 `check`。它不调用任何内部求值函数，也不是 NiceEval 内置的物体 API。

Adapter 要提供已经关联好的事实，让组合条件判断的是同一个对象。不要在正文里临时构造数据结构或展开日志过滤，也不要让 Adapter 直接返回“这道题通过了”这种结论。

模型调用成功、决策被采用、操作执行成功、目标达成，是四个不同的事实。不能用甲的成功调用、乙被采用的决策和丙的操作拼出一次通过，也不能把一次模型调用成功解释成整个任务成功。决策被采用也不等于操作执行成功，任务需要执行结果时要另写检查。

### 集合组合器

| 组合器 | 作用 |
| - | - |
| `mapValue(label, project, match)` | 从单个事实里取出一个值，再交给内层判断。适合定义人物身份、模型结果这类小判据 |
| `filterWhere(selector, aggregate)` | 从完整集合里选出确定命中的项，按原顺序交给集合判断 |
| `mapEach(project, aggregate)` | 从每一项取出同一种值，保留材料身份和顺序，再交给集合判断 |
| `countWhere(item, count)` | 逐项判断，把命中的准确数量交给数量判断 |

集合不完整时，这些组合器返回不可判定，不会把已知部分的小计当成准确数量。

“人都到齐了”要单独检查。三个人的生命值是 0，不能证明所有指定人物都出现了。空集合的计数是 0，也不能证明“所有人都满足”。Adapter 要保留缺人物、缺测量的未知状态，不能删掉未知项再声称集合是完整的。

## 写清“一次”“每次”和“没有”

“有一次”“每一次”和“一次也没有”需要不同的证据。先限定在同一个 Attempt、同一个对象和同一段观察时间内，再决定缺失的记录会不会影响结论。

| 业务要求 | 什么证据足够 | 集合完整但为空时 |
| - | - | - |
| 至少有一次符合条件的行动 | 一个真实、可追溯、满足全部条件的实例 | 不成立 |
| 每次行动都符合条件 | 覆盖全部候选并逐项判断；如果要求确实参与过，另加“至少一次”检查 | 取决于业务是否要求实际参与 |
| 没有违规行动 | 完整范围内没有违规；一个反例就足以否定 | 成立 |
| 对整组材料提一个验收问题 | 完整、有序、非空的相关材料 | `closeQA` 得 0 分，不调用 Judge |

空集合的规则因检查而异。`closeQA` 遇到空集合得 0 分，`notCalledTool` 遇到空集合则成立。集合不完整（`partial`）或不可用（`unavailable`）时，不能证明某件事没发生，也不能当成空集合处理。

找到一个合格实例，可以证明“至少有一次”，但不能证明“全部合格”。同样，发现一个违规就能否定“没有违规”，即使其余材料还不完整。证据不足要如实标记，不要一律算成业务失败。

组合条件必须落在同一个对象上。“存在甲的行动”加上“存在一条送达乙的发言”，不能证明“甲对乙说了话并且送达了”。前两条可能分别由甲的移动和丙的发言满足。

## 完整示例：客服任务

下面三段代码按顺序组成一个 `support-review.ts` 模块。它导出项目里自己写的工厂函数 `createSupportReview`：传入应用已有的观测函数，返回一个 Adapter 和一条评估。这个工厂不是 NiceEval 的 API，只是让接入代码和评估正文分开、各自好读。

使用时，把返回的 `evaluation` 作为某个 `.eval.ts` 的默认导出，并在 Experiment 里使用返回的同一个 `adapter`。运行前要配置好 Judge，见[验证 Judge](/docs/zh/tutorials/verify-judge)。

### 第 1 段：应用交付事实，保留未知状态

`observe(ctx)` 执行或等待这次客服任务，返回当前 Attempt 的只读事实。它用 `ctx.signal` 响应取消，按需登记释放连接的回调，并通过 `ctx.recordUsage` 上报用量。记录关联、协议解码和时间校验都在这里完成，不放进评估正文。

完成状态要有证据：

* 任务完成时，要有完成事件，`businessSeconds` 是核验过的非负业务时间。
* 确定没完成时，要有观察完整结束的记录。
* 两者都确认不了时，返回 `unavailable`。

发言集合保留每条发言的证据 ID、原文、是否送达和模型来源。状态未知时要如实标记，不能伪装成完整记录。

```ts theme={null}
import { defineAdapter, type AdapterCreateContext } from "niceeval";
import {
  atLeast, atMost, countWhere, defineContextMatch, defineMaterialMatch,
  equals, filterWhere, mapEach, mapValue,
  type BooleanMatch, type CollectionValue, type MaterialCollection, type MatchFact,
  type NumericMaterial,
} from "niceeval/expect";

export type Speech = {
  readonly actorId: string;
  readonly text: string;
  readonly delivered: boolean;
  readonly modelOrigin: "verified" | "unknown";
};
export type Completion =
  | { readonly completed: true; readonly eventId: string; readonly businessSeconds: number }
  | { readonly completed: false; readonly observationEndId: string };
export type SupportFacts = {
  readonly speeches: MaterialCollection<Speech>;
  readonly completion: MatchFact<Completion>;
};
```

### 第 2 段：领域 Match 只选材料

发言只按人物选择。送达失败或模型来源未知的发言，也要参与表达质量的判断。`read` 从注入的 `ctx` 读取材料，不需要注册。注意读取预算按整个集合计算，没命中的发言也算在内。

完成状态和业务时间已经是现成的事实，直接读取、直接计算即可。不要为了取一个字段或包一段算术，再造一个“完成效率 Match”。公式、时间尺度和权重都写在正文里。

```ts theme={null}
export const 人物 = (actorId: string) =>
  mapValue("人物", (speech: Speech) => speech.actorId, equals(actorId));

export const 发言材料 = (match: BooleanMatch<Speech, Speech>) =>
  defineMaterialMatch<SupportFacts, Speech>({
    name: "actor-speeches",
    read: ctx => ctx.speeches,
    match,
    capture: { maxItems: 1024, maxBytes: 512 * 1024,
      maxNodes: 65536, maxDepth: 32 },
  });
```

### 第 3 段：正文写出每项判据和权重

这道题的计分规则是：参与交流 5 分，表达质量 45 分，完成效率 50 分。另外有三道门槛：至少一条客服发言送达，token 用量和运行耗时不超上限。

`said` 和 `speechDelivery` 是这个 Adapter 自己定义的检查方法。`said` 检查“至少说过一句”；`speechDelivery` 把完整的发言集合交给组合器筛选、取值和计数，集合不完整的状态会原样保留。两者都不替正文决定这道题怎样算通过。

```ts theme={null}
export function createSupportReview(
  observe: (ctx: AdapterCreateContext) => Promise<SupportFacts>,
) {
  const adapter = defineAdapter({
    name: "support-review",
    create: observe,
    assertions({ check }) {
      return {
        said(match: BooleanMatch<Speech, Speech>) {
          return check(发言材料(match));
        },
        speechDelivery(
          selector: BooleanMatch<Speech, Speech>,
          aggregate: BooleanMatch<CollectionValue<boolean>, CollectionValue<boolean>>,
        ) {
          return check(defineContextMatch<SupportFacts, MaterialCollection<Speech>>({
            name: "speech-delivery",
            read: ctx => ({ state: "available", value: ctx.speeches }),
            match: filterWhere(selector, mapEach(
              (speech: Speech) => speech.delivered, aggregate,
            )),
          }));
        },
      };
    },
  });
  const evaluation = adapter.defineScoreEval({
    description: "客服正式发言质量与任务完成效率",
    test(t) {
      // 参与是加分；至少一次送达是这道题独立的业务门槛。
      t.said(人物("agent")).score(5).label("参与交流");
      t.speechDelivery(人物("agent"), countWhere(equals(true), atLeast(1)))
        .gate().label("至少一次送达");

      // 全部发言参与质量判断；不按送达或模型来源筛选。
      t.closeQA(发言材料(人物("agent")), "每句发言是否自然完整、意思清楚？", {
        maxMaterialBytes: 256 * 1024,
        maxAuditBytes: 1024 * 1024,
      }).score(45).label("表达质量");

      // 普通公式就在正文；未知仍占固定权重，不能当作未完成的 0。
      const completion = t.completion;
      const ratio: number | NumericMaterial = completion.state === "unavailable"
        ? { state: "unavailable", reason: completion.reason }
        : completion.value.completed
          ? Math.max(0, 1 - completion.value.businessSeconds / 180)
          : 0;
      t.score(ratio, { weight: 50 }).label("完成耗时");

      const usage = t.usage;
      t.check(usage.totalTokens, atMost(10_000)).gate().label("用量上限");
      t.check(t.elapsedMs, atMost(30_000)).gate().label("运行耗时上限");
    },
  });
  return { adapter, evaluation };
}
```

读这段代码时要注意几点：

* **两种预算不同。** `closeQA` 里的上限管的是交给 Judge 的材料和审计记录；`发言材料` 里的 `capture` 管的是读取整个集合。这里的数值只是示例，不代表任何长度的任务都装得下。
* **两种时间不同。** `t.elapsedMs` 是这次评估运行的墙钟时间；完成效率用的是应用里的业务时间。两者不能互换。
* **未上报的用量不会让检查通过。** `t.usage` 和 `t.elapsedMs` 是属性，直接读取。没有上报的 token 保留为未知，不会被当成 0。
* **每项得分只证明自己那一项。** 任务在第 90 个业务秒完成，效率这一项得 25 分；表达质量还要 Judge 另外打分。Judge 不可用或材料超限时，不能说拿到了完整的总分。送达检查也不能证明发言来自模型；业务要求模型自主表达时，要另写一项确定性检查。

## 案例一：问路后实际会面

任务是让甲问乙怎么去会面地点，然后甲自己走过去和乙会面。评估的是甲的选择、执行和结果，不是玩家有没有帮它完成。

这个任务要分开判断四件事：表达质量、信息是否有用、是否实际送达、是否来自模型。它们需要的材料不同：

* **评表达质量**，用甲在观察期内的全部发言，包括没人听见的、内容错误的和来源还没核验的。提问、回答、拒绝和澄清都要保留，再配上必要的双方对话上下文。不要先筛出送达或来源已核验的发言再评质量。
* **评信息是否有用**，用双方实际送达的完整对话，再加上正式地图。

下面是这条评估的设计草稿。分值只是这个任务的业务选择。它是伪代码，不能直接运行；应用动作和材料 API 按安装版本的参考页实现。

```text theme={null}
场景：甲不知道路线；乙知道路线；两者处于可交谈范围。
目标：甲问路后自主前往指定地点，与乙会面。

观察：运行应用直到任务结束或业务观察期限到达。

前提：确认当前 Attempt 的对话、行动和结果记录完整可关联。
      无法确认则报告证据不可用，停止依赖它们的检查。

检查点一：存在甲向乙送达的问路发言。
          通过门槛；同一发言同时满足人物、接收者与送达事实。

检查点二：分别选择与两个验收问题对应的完整材料。
          表达质量：甲全部正式发言是否自然、完整，可结合上下文理解？权重 2。
          不排除无人听见、内容错误或模型来源尚未核验的发言。
          信息有效：实际送达对话是否给出足以找到目标地点的信息？权重 3。
          同时提供该场景的正式地图与目标信息作为判断依据。
          模型来源另行检查；来源不明不能当作模型已自主表达。

检查点三：甲的模型选择了移动；应用实际执行；最终到达并会面。
          三项分别记录；最终会面必须关联到甲的自主行动。
          自主会面设为通过门槛，并计 5 分；玩家代走不得满足。

结果：逐项显示判断与理由。完整评分范围为 0 至 10 分。
      未通过门槛仍保留已取得分数；证据不可用不伪造完整零分。
```

“先收到路线，后发生移动”只证明了先后顺序。如果要求“甲是按乙的指路走的”，还需要应用提供“这次选择用到了哪条输入”的关联证据。拿不到这种证据时，就收窄判据并写明限制，不能用时间上接近来代替因果。

Judge 的问题不能暗示答案，也要有明确标准。“甲是否表现优秀并成功理解乙”就没有验收标准。拆成表达和信息两个问题后，读者能分别说清每一项为什么得分。

## 案例二：协商搬运与实际交货

任务是让甲和乙合作搬运一件物品。先想清楚这次的业务条件：双方是否已经知道目标、是否必须协商、谁有权拒绝。判据来自这些条件，不能照搬问路任务的要求。

| 场景 | 交流怎么判 | 完成怎么判 |
| - | - | - |
| 目标还没对齐，乙解释自己搬不了，并提出合理的替代方案 | 可以算有效交流，材料里保留拒绝和后续回应 | 没有实际交货，不算完成 |
| 双方已经知道目标，按约定默契搬运 | 业务没要求说话，就不登记依赖发言的质量项 | 用搬运和交货记录检查结果 |
| 操作已被接受，但物品还在原地 | 接受回执证明不了沟通质量 | 还没交货 |
| 玩家完成了交货，甲没有自己搬 | 不能据此推断甲理解了任务 | 不满足“甲自主完成” |

评交流质量时，保留双方完整对话，提出能根据材料回答的问题。评交货时，检查物品、目标位置、执行者和完成回执能否对上。交流和交货分开计分，权重都写在正文里。

同样的方法也适用于操作请求和模型返回：操作记录保留失败和调整，请求记录保留实际发出的内容，模型返回保留没被采用的输出。选择、执行和结果分开观察，不能拿一层代替另一层。

`closeQA(材料Match, 问题)` 可以评价一个人物的全部发言、操作、请求或模型返回。NiceEval 按原顺序把全部命中的材料交给 Judge，你不需要在正文里逐条调用模型。只传第一条、一份摘要或几条成功样本，都代表不了整体表现。

只在这条评估里用的问题，直接写在 `closeQA` 调用处，让读者同时看到材料和验收标准。几处共用的问题文本可以提取成常量复用，但不要把材料选择、问题和权重一起藏进某个场景专用的函数。

保存下来的最终位置，只能证明结束时在哪里。即使应用把最终位置附到每条操作记录上，它也不是每次操作当时的位置。要评价路线或每一步的轨迹，就要读取对应时刻的记录。

## 常见的误导写法

下面都是业务伪代码，变量代表应用里的事实，不是 NiceEval 提供的字段。

| 原写法 | 问题在哪 | 怎么改 |
| - | - | - |
| `journal.filter(isSuccess).slice(0, 12)` 交给 Judge | 只留成功的、只取前几条，掩盖了拒绝、错误和调整 | 按业务对象和观察期选全部材料，保留原顺序、原文和证据 ID |
| `speeches.filter(deliveredAndModelVerified)` 评表达 | 没人听见或来源待核验的差表现被排除了 | 表达质量用人物全部发言；送达和来源另外检查 |
| `journal.map(...).reduce(...)` 后命名为 `cooperation` | 统计数字没有说明什么算协作 | 先写清协作的判据，再用领域 Match 检查主体、目标和行动的关联 |
| `t.observation(...)`、`t.efficiency(...)`、`runEverything()` | 名字看不出证据和判据，或者把全部评分藏起来 | 在正文里分别写出观察前提、业务检查、Judge 问题和分值 |
| `胜者有自主战斗行动()` | 函数名遮住了判断过程 | 在正文组合人物身份、模型调用成功、决策被采用和允许的操作类型 |
| `三名敌人全部败退()` | “败退”可能混合了生命值、离场和任务结果，数量也藏在实现里 | 写明人物范围，分别写出生命值条件和所需数量 |
| `货箱完好送达()` | 身份、耐久和位置条件都被函数名遮住了 | 在同一个物体上组合身份、耐久门槛和目标位置 |
| `首次决胜游戏秒数()`、`决胜前调用效率()`、`反比得分Match()` | 为取一个数或几行公式造了新概念，读者还要去查定义 | 从 `ctx` 读取同一时间窗口的耗时和调用数，在正文里算出 0 到 1 的比例再加权 |
| `has(actorA) && has(deliveredToB)` | 两条不同的记录也能分别满足 | 在同一条记录上组合人物、动作和送达条件 |
| `output.includes("移动")` | 说要移动不等于真的移动了 | 分别检查模型的选择、执行回执和位置变化 |
| `accepted === true` 就算任务完成 | 请求被受理不等于最终完成 | 检查对应请求的完成回执和目标状态 |
| `tokens ?? 0`、`cost ?? 0` | 缺数据变成了免费或低用量 | 保留未知或已知下限，按覆盖情况判断预算 |
| `Date.now() - started` 当作业务完成时间 | 墙钟时间包含了等待和评估本身的开销 | 业务时限读应用里的时间，运行耗时单独看 |
| `judge("是否调用了工具？")` | 让模型猜一个能直接读到的事实 | 用工具调用记录做确定性检查 |
| `reply.includes("抱歉")` 判断拒绝是否合理 | 关键词解释不了对话的意思 | 给 Judge 完整上下文，问拒绝理由是否符合业务限制 |

## 用量和业务时间分开看

调用次数、token 和费用是三个不同的指标。Adapter 在每次真实调用时上报用量，失败和重试也要上报；NiceEval 负责汇总。评估正文不要再维护一份模型价格表。费用未知时不要补成 0，已知的下限也不能冒充完整的总量。

`t.usage` 和 `t.elapsedMs` 是通用的用量和墙钟时间。业务上的“完成前调用了几次”“到完成用了多少游戏时间”，要由应用明确声明观察窗口再提供。不要把某一时刻读到的累计用量改名叫“完成前用量”，也不要拿游戏时间代替运行耗时。

## 保留完整证据

材料按原顺序保存，并能追溯到同一个 Attempt 的记录。“完整”是相对于你声明的业务范围而言的，不是把所有无关日志都发给 Judge。选材料的范围按对象、任务和观察期决定，不能按表现好坏决定。

需要看整段对话时，不要截断原文，也不要删掉拒绝和澄清。超出容量就报告不可判定，不要为了拿到分数而缩短观察期、抽样或只留成功的部分。如果业务问题确实变了，就另写一条评估，不要把它的结果当成原任务的结论。

### 读取预算和 Judge 预算分开算

两种预算管的东西不同：

* **读取预算**管整个候选集合，没命中的记录也算在内。
* **Judge 材料预算**管验收问题、全部命中的材料和它们的身份信息。
* **审计预算**还要装下实际发给 Judge 的请求、返回和引用信息。

候选少、命中少和字节少是三回事。例如读了一千条操作，其中二十条属于甲，读取预算就要按一千条算。甲的二十条操作如果正文很长，条数少也不代表 Judge 材料或审计预算够用。预算不够时保留未知，不要把前十二条当成全部。

“至少一次”只需要一个确认过的实例；对整组材料提问，则必须确认集合完整，并处理全部命中项。发现一个合格的行动，证明不了其余行动也好，也不能让 Judge 忽略还没读到的记录。因预算停止读取后，“至少一次”的结论同样要依据正式的证据记录，不能从被截断的数组里自己推断。

## 用普通计算算分

完成耗时是应用里的一个事实，不需要把整份日志传进评估正文。应用提供可追溯的完成状态和业务时间，正文直接算分，不用为一次取数或一段算术定义新的函数、Match 或计分方式。

例如“180 个业务秒内越早完成分越高”，可以写成 `max(0, 1 - 完成秒数 / 180)`，权重 50：

* 完整观察证明没有完成，得 0 分。
* 缺少完成记录或观察结束记录，不可判定。
* 不要把缺少完成事件当成耗时 0，也不要把运行的墙钟时间代入业务时间。

第 90 秒完成，只说明这一项得 25 分。送达、模型自主选择和表达质量，仍然要在正文里分别检查。一个样例或类型检查通过，也不等于整条评估已经验证过。

战斗场景可以写明等待条件：“指定人物中只剩 1 人存活，或观察期限到了”。到期不等于分出胜负，最终观察要区分只剩一人、还有多人和无人存活三种情况。停止后，读取同一时间窗口的游戏耗时和实际调用数，按业务规则直接计算，不要从日志里再推算一个“首次决胜”的概念。

这里只说明观察意图，不代表 NiceEval 提供实时状态或截止条件的 API。应用要先提供只读的状态接口和停止机制，才能实现这类实时条件。不要私下调用内部求值函数、读取还没保存完的内部存储，也不要以为取消了就等于所有进行中的操作都已经结束。状态接口和清理回调的写法，以应用自己的约定和 [Adapter 参考](/docs/zh/reference/define-agent)为准。

## Judge 的问题要能从材料里回答

外部地图、规则或参考答案确实是验收依据时，明确提供给 Judge 并标明来源，不要把期望的结论写进问题里当前提。材料里如果有指令，Judge 把它当作被评估的内容，不会去执行。

复用领域 Match 时，同时检查它的名字、它认为什么证据足够、失败时给出什么解释。保留各个业务检查点和独立的权重，不要为同一个能力加几套别名或注册步骤。接口的具体形状以安装版本的参考页为准，不要把设计草稿当成能运行的 API。

## 交付前再多检查几项

先完成 [NiceEval Taste · 交付前的共同检查](/docs/zh/tutorials/niceeval-taste#交付前的共同检查)，再补上业务事实特有的验证：

* **集合的各种情况**：完整非空、完整为空、缺项和 partial 都要测到。组合条件要确认不同人物的条件不会拼在一起成立。判断胜负时，只剩一人、尚未分出胜负和同归于尽要分别作为业务情况处理，不要把所有非胜利的情况都当成技术错误。
* **真实应用的生命周期**：有外部 HTTP 或持续运行的应用时，验证取消、等待进行中的操作结束、保存证据这三步的顺序。检查取消后迟到的事件是否被正确处理、保存后是否还有写入、不完整的观察是否如实标记。局部 Fixture 通过，不能证明真实应用的整个生命周期都没问题。

提交前再回答这几个问题：

* 表达质量用的是人物的全部发言吗？送达和模型来源有没有另外检查？
* 读取集合、Judge 材料和审计分别在预算内吗？超出时有没有如实报告？
* 不打开函数实现，能读出人物、状态、数量、门槛和组合关系吗？
* 合理的拒绝、不需要说话的协作、失败后的调整，都按这个场景的规则处理了吗？
* 有没有把最终状态误当成每一步的轨迹？业务测量窗口和通用用量有没有混在一起？


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.