For AI agents: the complete documentation index is available at https://scriptedalchemy.github.io/agent-bundle/zh/llms.txt, the full documentation bundle is available at https://scriptedalchemy.github.io/agent-bundle/zh/llms-full.txt, and this page is available as Markdown at https://scriptedalchemy.github.io/agent-bundle/zh/guide/development/evaluations.md.
跳至主要内容
  • 简体中文
  • 评测

    测试证明路由能渲染、生成式服务器遵守线上契约。Eval 追问的是另一个问题:给定一段提示词,智能体真的 抵达了你的插件吗?其结果是否就是你所声称的那个?这个答案是概率性的,因此 eval 运行器的设计目标是诚实 报告,而不是看起来一片绿。

    套件是带类型的模块

    Eval 套件按约定从 evals/**/*.eval.ts 中发现,每个套件默认导出 defineEvalSuite:

    import { 
    const defineEvalSuite: (value: EvalSuiteInput) => EvalSuite
    defineEvalSuite
    ,
    const expectOutcome: (options: ExpectOutcomeOptions) => EvalOutcomeAssertion
    expectOutcome
    ,
    const expectSkillActivation: (options: ExpectSkillActivationOptions) => EvalSkillActivationAssertion
    expectSkillActivation
    } from 'agent-bundle/eval';
    export default
    function defineEvalSuite(value: EvalSuiteInput): EvalSuite
    defineEvalSuite
    ({
    EvalSuiteInput.cases: readonly (EvalCase | EvalCaseInput)[]
    cases
    : [{
    assertions: (EvalOutcomeAssertion | EvalSkillActivationAssertion)[]
    assertions
    : [
    function expectSkillActivation(options: ExpectSkillActivationOptions): EvalSkillActivationAssertion
    expectSkillActivation
    ({
    ExpectSkillActivationOptions.skill: string
    skill
    : 'service-readiness' }),
    function expectOutcome(options: ExpectOutcomeOptions): EvalOutcomeAssertion
    expectOutcome
    ({
    ExpectOutcomeOptions.script: string
    script
    : './graders/status-result.ts' }),
    ],
    fixture: string
    fixture
    : './fixtures/status',
    hosts: {
        portable: {
            model: string;
        };
    }
    hosts
    : {
    portable: {
        model: string;
    }
    portable
    : {
    EvalHostBinding.model: string
    model
    : 'deterministic' } },
    id: string
    id
    : 'status-is-healthy',
    invocation: {
        mode: "explicit";
        skill: string;
    }
    invocation
    : {
    EvalInvocation.mode: EvalInvocationMode
    mode
    : 'explicit',
    EvalInvocation.skill?: string | undefined
    skill
    : 'service-readiness' },
    prompt: string
    prompt
    : 'Use service-readiness to verify the checked-in compiler service fixture.',
    trials: number
    trials
    : 1,
    }],
    EvalSuiteInput.name: string
    name
    : 'mcp-app-status',
    });

    一个 case 声明它的 id、prompt、fixture、hosts(逐宿主固定模型)、invocation 模式, automatic、explicit 或 none,它的 assertions,以及要运行多少次 trials。同一套件内的 case id 必须唯一。套件发现、运行记录目录与可选的语义 grader 都配置在顶层 evals 配置块中;见 配置。

    npx agent-bundle eval --root . --suite mcp-app-status --trials 3
    npx agent-bundle eval compare <baseline-run-id> <candidate-run-id>

    编写一个结果 grader

    expectOutcome({ script }) 指名一个默认导出一个 grader 函数的模块。路径相对套件文件解析,且不得越出套件 目录(越过它的 ../ 会被拒绝;这项检查针对的是路径本身,因此套件内的符号链接会被跟随到它所指向的任何位置)。agent-bundle/eval 导出了它的契约:EvalGraderFunction 接收一个 EvalGraderContext,并同步或以 promise 形式返回一个 EvalScriptOutcome。下面是 examples/skills-starter/evals/graders/release-result.ts 的原文;上面的套件指名的 ./graders/status-result.ts 在 examples/mcp-app 中具有同样的形状:

    // evals/graders/release-result.ts
    import { 
    function readFile<T extends NodeJS.ArrayBufferView>(path: PathLike | FileHandle, options: ReadFileOptionsWithBuffer<T>): Promise<BufferView<T>> (+3 overloads)

    Asynchronously reads the entire contents of a file.

    If no encoding is specified (using options.encoding), the data is returned as a Buffer object. Otherwise, the data will be a string.

    If options is a string, then it specifies the encoding.

    When the path is a directory, the behavior of fsPromises.readFile() is platform-specific. On macOS, Linux, and Windows, the promise will be rejected with an error. On FreeBSD, a representation of the directory's contents will be returned.

    An example of reading a package.json file located in the same directory of the running code:

    import { readFile } from 'node:fs/promises';
    try {
      const filePath = new URL('./package.json', import.meta.url);
      const contents = await readFile(filePath, { encoding: 'utf8' });
      console.log(contents);
    } catch (err) {
      console.error(err.message);
    }

    It is possible to abort an ongoing readFile using an AbortSignal. If a request is aborted the promise returned is rejected with an AbortError:

    import { readFile } from 'node:fs/promises';
    
    try {
      const controller = new AbortController();
      const { signal } = controller;
      const promise = readFile(fileName, { signal });
    
      // Abort the request before the promise settles.
      controller.abort();
    
      await promise;
    } catch (err) {
      // When a request is aborted - err is an AbortError
      console.error(err);
    }

    Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.readFile performs.

    Any specified FileHandle has to support reading.

    @sincev10 .0.0@parampath filename or FileHandle@returnFulfills with the contents of the file.
    readFile
    } from 'node:fs/promises';
    import {
    function join(...paths: string[]): string

    Join all arguments together and normalize the resulting path.

    @parampaths paths to join.@throws{TypeError} if any of the path segments is not a string.
    join
    } from 'node:path';
    import type {
    type EvalGraderFunction = (context: EvalGraderContext) => EvalScriptOutcome | Promise<EvalScriptOutcome>
    EvalGraderFunction
    } from 'agent-bundle/eval';
    interface ReleaseResult { readonly
    ReleaseResult.blockers?: unknown
    blockers
    ?: unknown;
    readonly
    ReleaseResult.verdict?: string | undefined
    verdict
    ?: string;
    } const
    const grade: EvalGraderFunction
    grade
    :
    type EvalGraderFunction = (context: EvalGraderContext) => EvalScriptOutcome | Promise<EvalScriptOutcome>
    EvalGraderFunction
    = async ({
    fixturePath: string
    fixturePath
    }) => {
    const
    const result: ReleaseResult
    result
    =
    var JSON: JSON

    An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.

    JSON
    .
    JSON.parse(text: string, reviver?: (this: any, key: string, value: any) => any): any

    Converts a JavaScript Object Notation (JSON) string into an object.

    @paramtext A valid JSON string.@paramreviver A function that transforms the results. This function is called for each member of the object. If a member contains nested objects, the nested objects are transformed before the parent object is.@throws{SyntaxError} If text is not valid JSON.
    parse
    (
    await
    function readFile(path: PathLike | FileHandle, options: ReadFileOptionsWithStringEncoding | BufferEncoding): Promise<string> (+3 overloads)

    Asynchronously reads the entire contents of a file.

    If no encoding is specified (using options.encoding), the data is returned as a Buffer object. Otherwise, the data will be a string.

    If options is a string, then it specifies the encoding.

    When the path is a directory, the behavior of fsPromises.readFile() is platform-specific. On macOS, Linux, and Windows, the promise will be rejected with an error. On FreeBSD, a representation of the directory's contents will be returned.

    An example of reading a package.json file located in the same directory of the running code:

    import { readFile } from 'node:fs/promises';
    try {
      const filePath = new URL('./package.json', import.meta.url);
      const contents = await readFile(filePath, { encoding: 'utf8' });
      console.log(contents);
    } catch (err) {
      console.error(err.message);
    }

    It is possible to abort an ongoing readFile using an AbortSignal. If a request is aborted the promise returned is rejected with an AbortError:

    import { readFile } from 'node:fs/promises';
    
    try {
      const controller = new AbortController();
      const { signal } = controller;
      const promise = readFile(fileName, { signal });
    
      // Abort the request before the promise settles.
      controller.abort();
    
      await promise;
    } catch (err) {
      // When a request is aborted - err is an AbortError
      console.error(err);
    }

    Aborting an ongoing request does not abort individual operating system requests but rather the internal buffering fs.readFile performs.

    Any specified FileHandle has to support reading.

    @sincev10 .0.0@parampath filename or FileHandle@returnFulfills with the contents of the file.
    readFile
    (
    function join(...paths: string[]): string

    Join all arguments together and normalize the resulting path.

    @parampaths paths to join.@throws{TypeError} if any of the path segments is not a string.
    join
    (
    fixturePath: string
    fixturePath
    , 'result.json'), 'utf8'),
    ) as ReleaseResult; return
    const result: ReleaseResult
    result
    .
    ReleaseResult.verdict?: string | undefined
    verdict
    === 'ready'
    &&
    var Array: ArrayConstructor
    Array
    .
    ArrayConstructor.isArray(arg: any): arg is any[]
    isArray
    (
    const result: ReleaseResult
    result
    .
    ReleaseResult.blockers?: unknown
    blockers
    )
    &&
    const result: ReleaseResult
    result
    .
    ReleaseResult.blockers?: any[]
    blockers
    .
    Array<any>.length: number

    Gets or sets the length of the array. This is a number one higher than the highest index in the array.

    length
    === 0
    ? {
    EvalScriptOutcome.detail: string
    detail
    : 'The release artifact is ready with no blockers.',
    EvalScriptOutcome.outcome: EvalAssertionOutcome
    outcome
    : 'pass' }
    : {
    EvalScriptOutcome.detail: string
    detail
    : 'The release artifact is not ready or has unresolved blockers.',
    EvalScriptOutcome.outcome: EvalAssertionOutcome
    outcome
    : 'fail',
    }; }; export default
    const grade: EvalGraderFunction
    grade
    ;

    上下文就是 grader 正在评判的那次试验:

    字段含义
    fixturePath该次试验自己的 case fixture 副本,agent 已经在其上运行过。从这里读取 agent 的输出;签入的 fixture 永远不会被改动。
    artifactRoot该次试验所针对的已准备产物,供检查"实际发布了什么"而非"agent 写了什么"的 grader 使用。
    promptagent 收到的 case 提示词。

    结果是 { outcome, detail }:outcome 是 pass、fail 或 inconclusive,与其他所有断言相同的三种裁定, detail 是在试验记录中显示于结果旁边的那句话。一个不默认导出函数、或返回了任何其他形状的模块,是 grader 的缺陷而不是插件的证据:该次试验会被报告为 EVAL_GRADER_FAILED,其结果保持 inconclusive。grader 在 eval 进程中以宿主的文件系统运行,因此请读取 fixture 与产物;不要触及 agent 的会话。

    三种结果,而不是两种

    每个断言都解析为 pass、fail 或 inconclusive,并声明它所接受的最低证据。当断言所需的证据强于 harness 实际产生的证据时,它是 inconclusive,绝不会被悄悄判为通过。

    证据含义
    observedharness 在宿主自己的输出中看到了这件事发生。
    inferredharness 由周边证据推导得出,而不是直接观察到。
    unavailable完全没有证据。它绝不是合法的声明最低值,因为那等于接受证据缺失。

    断言辅助函数会把 minimumEvidence 默认设为它们的证据所能支撑的最强门槛:expectExitCode、 expectMcpCall、expectNoMcpCall 与 expectOutcome 默认 observed,而 expectSkillActivation 与 expectNoSkillActivation 默认 inferred,因为并非每个宿主都会公布激活事件。当你希望较弱的证据读作 inconclusive 而不是通过时,用 minimumEvidence: 'observed' 抬高默认值。

    由于 inconclusive 的试验没有产生任何证据,它同样无法报告成功:只要一次运行中存在失败或 inconclusive 的试验,agent-bundle eval 就以非零退出。

    smoke 证据与可靠性数字

    少于三次试验会被报告为 smoke 证据,而不是一个可靠性数字。一次通过的试验说明这条路径存在,但它不是 一个比率。

    只有当以下每个 facet 都匹配时,对比行才算对齐:case、fixture、语义 grader 身份、harness、宿主 CLI 版本、invocation 与 model。产物 target 摘要刻意不是对齐 facet,它正是被对比的那个对象。运行中未 记录的 facet 会被标注为未核实(unverified),而不是被假定为对齐;某个 facet 不匹配会让该行变为 non-comparable,而不是产出一个没人该信的差值。

    可选的语义 grader

    语义 grader 用恰好一个被固定的 Claude 模型来配置:

    import { 
    const defineConfig: (config: AgentBundleConfig | ConfigFactory) => AgentBundleConfig | ConfigFactory
    defineConfig
    } from 'agent-bundle/config';
    export default
    function defineConfig(config: AgentBundleConfig | ConfigFactory): AgentBundleConfig | ConfigFactory
    defineConfig
    ({
    evals: {
        semanticGrader: {
            harness: string;
            model: string;
        };
    }
    evals
    : {
    semanticGrader: {
        harness: string;
        model: string;
    }
    semanticGrader
    : {
    harness: string
    harness
    : 'claude',
    model: string
    model
    : 'claude-sonnet-4-5' },
    },
    AgentBundleConfig.plugin: AgentBundlePluginConfig
    plugin
    : {
    AgentBundlePluginConfig.description?: string | undefined
    description
    : 'Evidence-backed project tools.',
    AgentBundlePluginConfig.name: string
    name
    : 'my-plugin' },
    AgentBundleConfig.targets?: string[] | undefined
    targets
    : ['portable', 'claude'],
    });

    它只在 agent-bundle eval --harness claude 下运行,且只针对固定到 Claude 的 case。在主轨迹可用且确定性 grader 完成之后,Agent Bundle 会发起一次服务端自有、不含插件的 Claude 评分调用。它的固定结果 id 是 claude-semantic,其请求、原始流、stderr 与规范 provenance 会与试验产物一并保留。格式错误或失败的语义 grader 会让该次试验保持 inconclusive,而不是变成插件证据,坏掉的 grader 不等于插件行为异常。

    语义评分需要原生 Claude harness 与一个已登录的 Claude Code 会话。配置了它之后,确定性与 Codex 选择会 被拒绝。

    Workbench Evals

    打开 Advanced → Evals → Runs 即可受理一次选定的运行、跟随实时进度,或通过运行生命周期取消它。 每次试验都会在存在时暴露其持久化的原始证据,以及记录下来的 CLI、invocation、grader 与用量 provenance。

    Advanced → Evals → Compare 会展示记录下来的 provenance 与用量,并且只纳入已对齐的 case、fixture、 harness、invocation、宿主与 model、CLI 以及 grader facet;未匹配的 facet 会被标注为 non-comparable 或 未核实。试验时长会被持久化;供应商 token 用量只在原生流上报了它时才展示。

    原生 harness

    确定性 harness 不需要安装任何东西。原生 Claude 与 Codex harness 运行真实的宿主 CLI,而那里的规则很严格:

    • 本包绝不接受、索取、注入或持久化任何模型供应商 API key。
    • 原生 harness 使用已安装且已登录的 CLI,并且是在移除供应商 key 环境变量之后运行。
    • 缺失、不兼容或未认证的 CLI 会被报告为 harness 失败,与插件失败明确区分。
    • 原生 smoke 是选择加入的,不属于普通测试运行的一部分。
    • Codex 不公布权威的 Skill 激活事件,因此 Codex 的激活证据是 inferred,绝不会被报告为 observed。

    若要做一次手动的、已认证的本地 smoke,请先完成每个 CLI 各自的常规交互式登录,然后核实 CLI 是受支持的 非预发布版本。Claude Code 至少需要 2.1.232;Codex 至少需要 0.147.0:

    claude --version
    npx agent-bundle eval --harness claude --trials 1
    
    codex --version
    npx agent-bundle eval --harness codex --trials 1

    每次 Codex 试验都会设置一个临时的 CODEX_HOME,并只把已安装 CLI 的不透明 auth.json 复制进去。你 常用的 Codex home、配置与已安装插件状态不会被用作试验状态,也不会被改动。完整的凭据契约见 安全。

    下一步

    • 分发:把一份已校验的产物变成可安装的捆绑包。
    • 已知限制:eval 与 harness 目前不能证明什么。