Skip to main content
你在评估一个社交应用里的 AI 功能:用户发帖后,AI 自动生成配图、推荐回复。这里没有“发一条消息、收一条回复”的对话,有的是发帖、回复、生成图片这些业务操作。 硬把它们写成 t.send("帮我发帖") 很别扭。更直接的做法是用 defineAdapter 把应用自己的方法交给评估用例:返回对象里的每个操作,都会变成评估用例里带类型的 t.post(...)、t.generateImage(...)。 对话式 Agent 用的 defineAgent 是 Adapter 的一种特例,要实现 send、多轮会话和 Turn。自定义应用不需要实现这些。

定义应用连接

先告诉 NiceEval 怎么创建你的应用、评估要用到它的哪些操作。下面连接一个已有的社交应用。createSocialApp 属于你的应用,返回对象逐个列出评估需要的操作。
application.ts
运行器每个 Attempt 调用一次 create,Attempt 结束时执行登记过的 onCleanup 回调。
  • 每个 Attempt 要不要拿到独立的应用实例,由你的 create 决定。运行器不会替你复制实例。
  • 没登记 onCleanup 的资源,运行器不会释放。取得资源后立即登记。
  • 把 signal 传进应用的网络请求和长任务,超时或取消时它们才会停下。
TypeScript 会从返回对象推导出每个方法的参数和结果,不需要手写 t 的类型。返回对象里没有 send 时,在评估用例里写 t.send 会报类型错误。check 等 NiceEval 自带的名称不能被覆盖。已有的类实例,用上面的闭包写法逐个暴露方法。

调用应用动作并自定义检查

接下来在评估用例里调用这些操作,再检查结果。test(t) 里既有应用的操作,也有 NiceEval 的检查方法。应用返回的 Post、Reply 等对象,可以直接交给 defineValueMatch 定义的自定义 Match。
evals/publish.eval.ts
操作的参数和返回值由你的应用决定。defineValueMatch 只描述一个值怎样算匹配。t.check 登记一条断言,.gate() 表示这条不通过,整个评估用例就不通过。字符串、数值和 Schema 匹配器也都能直接用。 需要累计分数时,改用 social.defineScoreEval,再在检查上调用 .score(points)。语气、人物一致性、内容质量这类开放式标准可以交给 Judge。

在连接处定义断言方法

好几个评估用例都要做同一种检查时,把它写成 Adapter 上的断言方法,评估用例里一行就能调用。在 assertions 里定义这些方法,它们拿到当前 Attempt 的 app 和 check,返回 check 的结果。
application.ts
evals/publish.eval.ts
hasPublishedText() 和直接写 t.check 的效果一样,计分和报告里的展示也一样。defineEval 里的断言方法没有 .score(),defineScoreEval 里的有。post() 这样的操作方法仍然只调用应用。 写断言方法时注意几点:
  • 在方法被调用时才读取要检查的数据。assertions 只负责组装方法,不能在里面提前调用 check。
  • 返回对象里只放函数,不放 getter 或其它值,方法名也不能和应用操作或 NiceEval 自带的名称重名。
  • 方法同步返回 check 的结果,不返回 Boolean 或 Promise。异步操作和等待放在应用方法里,在检查前先完成。
  • 数据不完整时报一个明确的错误,不要当成“什么都没发生”来判断。
  • 参数写成具体类型。泛型或重载的断言方法无法调用,这类检查改用独立的 Match 加 t.check。应用操作本身可以是泛型方法。

选择实现并运行

写一个 Experiment 选定这个应用,然后运行。
experiments/local.ts
跑完后用 pnpm exec niceeval show 在终端看结果,或用 pnpm exec niceeval view 在浏览器里看。 Experiment 选的应用必须提供评估用例用到的操作。缺了操作时,NiceEval 在创建应用之前就报错,先核对 create 的返回对象。 操作的顺序、参数和重复次数都由评估代码决定。应用操作失败时不会像 Agent 消息那样自动重试。你也可以把某个操作命名为 send,它的参数和返回值仍由应用决定,不需要构造 Turn。

用相同评估比较两个实现

想知道换一套实现后效果是变好还是变差,就让两套实现跑同一批评估用例。先声明它们共同的接口,再分别提供实现:
评估用例用 social.defineEval 编写,两个 Experiment 分别选 baseline 和 candidate。评估用例里只能用共同接口里的方法。两次分别调用 defineAdapterContract 得到的接口即使同名,也不能互换。 两套实现要共用断言方法时,在创建实现前调用 withAssertions:
用 withAssertions 返回的接口创建实现和评估用例,各实现就会用同一套检查。withAssertions 返回的是一个新接口,在它之前创建的实现拿不到这些方法。

什么时候沿用上次结果

再次运行时,NiceEval 会判断能否沿用上次的结果。参与判断的有 Adapter 的 name、contract、behaviorRevision,以及 Experiment 的 flags。评估用例文件和它在项目内用相对路径静态 import 的文件一改,这道题就会重跑。 Experiment 单独选择的实现、动态 import、外部包和远端服务,NiceEval 看不到它们的变化。改了这些行为时,提高 behaviorRevision,或者用显式配置体现变化。没有声明 behaviorRevision 的自定义 Adapter 每次都重跑,不沿用上次结果。用 pnpm exec niceeval exp local --dry 可以先看哪些会沿用、哪些会重跑,见修改后只重跑受影响的评估。

保存应用执行轨迹

评估失败时,你想知道应用内部到底做了什么。把应用的执行日志交给 NiceEval,之后就能用 niceeval show 逐条查看,不必再去应用那边翻日志。 应用结束、拿到完整日志后,在 Adapter 里调用 ctx.recordTrace。每个事件保留自己的类型、执行者、摘要和已知的关联,不需要包装成聊天消息。下面的事件只表示发帖请求已被接受,真正发布完成要另记一条事件。
  • key 用来在这次提交的事件之间互相引用。source.eventId 只填应用真实提供的 ID,没有就省略。
  • schema.id 标明应用的事件格式,只填 id,不填 revision。
  • 返回的 receipt.events 给出每个事件的 eventId,查看时用它展开。同一个 traceId 再次提交相同内容,返回同样的 eventId;内容不同会被拒绝。
  • 摘要有长度上限。大段输入放进附件,用 evidence 的 pointer 指向附件里的具体位置,查看时直接定位,不用扫完整份日志。
跑完后查看这个 Attempt 的执行过程,再展开某个事件:
collection.state 描述日志是否完整,和运行有没有被取消无关。取消后仍拿到了完整日志,就写 complete。缺日志时写 partial,并在 limitations 里说明缺了什么,不要用空列表表示“不知道”。超出 scopes 采集范围的事件也保留下来,标成 excluded,判断不了的标成 unknown。提交执行日志本身不影响判分。 onCleanup 回调有自己的时限,在里面也能提交执行日志。保存后的执行日志和附件随结果文件 .niceeval/record.sqlite 一起保存,查看时不会再调用应用或模型。

归档大附件

应用的原始日志、网络响应这类大文件,可以作为附件和结果一起保存,排查时再取出来看。用 ctx.attach 把文件、网络响应或生成器作为字节流交给 NiceEval,NiceEval 不解析其中的格式:
先关掉本地日志的写入,再提交附件,读取期间不要改动源文件。 返回的回执包含 artifactId、原始字节的 sha256 和 byteLength。拿到回执后,NiceEval 已经有了自己的副本,之后删除或改写源文件不影响附件。附件在这个 Attempt 的结果保存时一起写进结果文件,不会上传到任何远端服务。 大小限制:
  • 字节流每块最多 1 MiB,单个附件最多 1 GiB,每个 Attempt 的附件合计最多 2 GiB,同时最多提交 4 个流。
  • 字符串和数组形式的附件单个最多 64 MiB,合计最多 256 MiB。
  • 作为执行日志里 pointer 证据的附件,单个不能超过 64 MiB。
NiceEval 写完一块才读下一块,不会把整个文件读进内存。自定义的流必须响应传入的 signal,并释放自己的文件或连接。 在 ctx.onCleanup 里提交附件时,用 cleanup 回调提供的 signal,不要用已经取消的 ctx.signal。提交失败时,即使你捕获了错误,这个 Attempt 也会显示采集失败,之前成功提交的附件仍然保留。cleanup 总共只有 30 秒,大文件应在运行过程中提交。

阅读结果

用 pnpm exec niceeval show 查看这次运行。每条 t.check 保存了被检查的值(有长度上限)和判定。 NiceEval 没有采集到的会话、工具调用和费用会显示为缺失,不会填成零。没有 Turn 不代表用了零 token,Judge 的费用也不等于整个应用的费用。 因为拿不到应用的完整费用,自定义应用不能设置 budget,也不能声明只有 Agent 才有的 Sandbox 或会话能力。生命周期 Plugin 需要 Agent 的执行准备步骤,在自定义应用上配置它们会在运行前报错。 完整可运行的模拟 X 示例位于 LLM X 源码。 它由 LLM 生成人物、个人主页、推文、配图和后续社交动态,并使用应用原生方法进行评估。