mcpField
E
eval-writer

eval-writer

github.com/langchain-ai测试与质量1.6k51 次安装2 天前更新
代码仓库

关于 eval-writer

eval-writer 是为 deepagentsjs monorepo 编写评测套件的技能,面向开发和测试 Agent 的工程师。它指导创建基于 vitest 的独立工作区包,使用 @deepagents/evals 运行并将结果记录到 LangSmith。

主要功能

  1. 01创建独立的 eval 工作区包
  2. 02设计内联、数据集或 LangSmith 测试用例
  3. 03使用轨迹匹配器和自定义评分
  4. 04为需要执行代码的评测接入沙箱
  5. 05运行测试并更新评测文档

适用场景

  1. 01评测 Agent 的工具调用和推理表现
  2. 02用数据集测试代码生成能力
  3. 03将评测结果记录到 LangSmith
  4. 04参照 Python deepagents 评测补齐 JS 套件

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/langchain-ai/deepagentsjs --skill eval-writer

安装后在 Claude Code 中输入/eval-writer确认已加载。

详细介绍

Eval Writer

为 deepagentsjs 单仓库创建新的评测套件。每个评测都是 evals/ 下的独立工作区包,使用 @deepagents/evals 测试框架,通过 vitest 运行,并将结果报告到 LangSmith。

开始之前

阅读现有评测基础设施,了解当前的模式:

internal/eval-harness/src/index.ts    # EvalRunner, RunAgentParams, matchers
internal/eval-harness/src/deepagent.ts # DeepAgentEvalRunner, extend()
internal/eval-harness/src/setup.ts     # Registered runners
evals/README.md                        # User-facing docs
internal/eval-harness/README.md        # Harness internals

扫描现有评测,了解其惯例:

evals/basic/index.test.ts     # Simple: system prompt, reasoning
evals/files/index.test.ts     # File ops: read, write, edit, glob, grep
evals/subagents/index.test.ts # Delegation: task tool, named subagents

工作流

1. 了解评测需求

向用户确认:

  • 要评估什么能力?(文件操作、工具使用、多轮推理、记忆、代码生成等)
  • 测试用例来自哪里? 可选项:
    • 内联 — 硬编码在测试文件中(简单,适合少于 20 个用例)
    • JSON/JSONL fixture — 纳入评测包(适合 20–200 个用例)
    • 外部数据集 — 在设置时下载(适合已发布的基准测试)
    • LangSmith 数据集 — 从 LangSmith API 拉取(适合协作整理)
  • 如何为结果评分? 可选项:
    • 轨迹匹配器 — 步数、工具调用、最终文本(内置)
    • 精确/模糊匹配 — 将输出与参考内容比较(简单)
    • LLM-as-judge — 使用模型为输出评分(复杂评测)
    • 代码执行 — 运行生成的代码并检查结果(SWE-bench 风格)
    • 自定义评估器 — 特定领域的评分函数
  • Agent 是否需要特殊配置?(自定义工具、子 Agent、系统提示词、初始文件)
2. 创建评测包

每个评测都是 evals/<name>/ 下的工作区包。

目录结构
evals/<name>/
├── package.json
├── vitest.config.ts
├── index.test.ts
├── README.md
└── (optional) fixtures/       # JSON/JSONL test data
└── (optional) vitest.setup.ts  # Dataset loading, custom setup
└── (optional) evaluators.ts   # Custom scoring functions
package.json
{
  "name": "@deepagents/eval-<name>",
  "private": true,
  "type": "module",
  "scripts": {
    "test:eval": "vitest run"
  },
  "dependencies": {
    "@deepagents/evals": "workspace:*",
    "deepagents": "workspace:*",
    "langsmith": "^0.5.4",
    "vitest": "^4.0.18"
  }
}

按需添加其他依赖(例如,用于工具 schema 的 zod、用于 tool() helper 的 langchain、特定数据集所需的包)。

vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    environment: "node",
    globals: false,
    testTimeout: 120_000,
    hookTimeout: 60_000,
    teardownTimeout: 60_000,
    include: ["**/*.test.ts"],
    setupFiles: ["@deepagents/evals/setup"],
    reporters: ["default", "langsmith/vitest/reporter"],
  },
});

对于运行时间较长的评测(多轮、代码执行),调整 testTimeout。 如果评测需要自定义设置(加载数据集等),将 "./vitest.setup.ts" 添加到 setupFiles。

README.md
# <name>

<One-line description of what this eval tests.>
验证工作区注册

检查 pnpm-workspace.yaml 是否包含 "evals/*"。它应该已经存在;如果没有,请添加。

3. 设计测试用例
模式 A:内联测试用例(简单评测)

适用于小型、手工编写的测试套件。每个测试都是包含 inputs 和可选 referenceOutputs 的 ls.test() 调用。

ls.test(
  "descriptive test name",
  {
    inputs: { query: "What is 2+2?" },
    referenceOutputs: { expectedAnswer: "4" },
  },
  async ({ inputs, referenceOutputs }) => {
    const result = await runner.run({ query: inputs.query });
    // assertions...
  },
);
模式 B:使用 ls.test.each 的数据驱动评测(中型评测)

适用于从 fixture 文件加载的 10–200 个用例。加载数据并遍历:

import testCases from "./fixtures/cases.json";

// testCases = [{ inputs: { query: "..." }, referenceOutputs: { answer: "..." } }, ...]

ls.test.each(testCases)(
  "case: ${inputs.query}",
  async ({ inputs, referenceOutputs }) => {
    const result = await runner.run({ query: inputs.query });
    // assertions using referenceOutputs...
  },
);

fixture JSON 必须是对象数组,且至少包含 { inputs: {...} }。可选字段:referenceOutputs、id、metadata、split。

模式 C:外部数据集(已发布的基准测试)

对于已发布的基准测试(oolong、AgentBench、SWE-bench 等),在设置文件中下载并缓存数据集。

创建 vitest.setup.ts:

import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
import { join } from "path";

const CACHE_DIR = join(import.meta.dirname, ".cache");
const DATA_PATH = join(CACHE_DIR, "dataset.json");

export async function loadDataset(): Promise<TestCase[]> {
  if (existsSync(DATA_PATH)) {
    return JSON.parse(readFileSync(DATA_PATH, "utf-8"));
  }

  mkdirSync(CACHE_DIR, { recursive: true });

  // Download from source — adapt to the specific benchmark
  const response = await fetch("https://example.com/dataset.json");
  const data = await response.json();

  // Transform into eval format
  const cases = data.map((item: any) => ({
    inputs: { query: item.question },
    referenceOutputs: { answer: item.gold_answer },
    metadata: { source: item.id, category: item.category },
  }));

  writeFileSync(DATA_PATH, JSON.stringify(cases, null, 2));
  return cases;
}

在评测包的 .gitignore 中添加 .cache/。

然后在 vitest.config.ts 中将其注册为 vitest 设置文件:

setupFiles: ["@deepagents/evals/setup", "./vitest.setup.ts"],

并在测试文件中:

import { loadDataset } from "./vitest.setup.js";

const dataset = await loadDataset();

ls.describe(runner.name, () => {
  ls.test.each(dataset)(
    "${metadata.source}: ${inputs.query}",
    async ({ inputs, referenceOutputs }) => {
      // ...
    },
  );
}, { projectName: "deepagents-js-<name>", upsert: true });
模式 D:LangSmith 数据集

从 LangSmith 数据集拉取测试用例。适用于协作整理的场景,非工程人员可以通过 LangSmith UI 添加示例。

import { Client } from "langsmith";

const client = new Client();

export async function loadDataset(): Promise<TestCase[]> {
  const examples = [];
  for await (const example of client.listExamples({
    datasetName: "my-dataset-name",
  })) {
    examples.push({
      id: example.id,
      inputs: example.inputs,
      referenceOutputs: example.outputs ?? {},
    });
  }
  return examples;
}
4. 编写评分逻辑
内置轨迹匹配器

测试框架提供的 vitest 匹配器也会记录 LangSmith 反馈分数。请将其作为主要构建模块:

// Exact step count
expect(result).toHaveAgentSteps(3);

// Exact tool-call count across all steps
expect(result).toHaveToolCallRequests(2);

// Check a specific tool call in step N (1-indexed)
expect(result).toHaveToolCallInStep(1, {
  name: "write_file",
  argsContains: { file_path: "/out.txt" },  // partial match
  argsEquals: { file_path: "/out.txt" },     // exact match
});

// Final response text
expect(result).toHaveFinalTextContaining("hello", true /* caseInsensitive */);

// Extract final text for custom assertions
import { getFinalText } from "@deepagents/evals";
const text = getFinalText(result);
expect(text.trim()).toBe("4");

// File system assertions
expect(result.files["/output.md"]).toContain("expected content");
expect(Object.keys(result.files)).toHaveLength(3);
自定义反馈记录

记录匹配器未提供的其他 LangSmith 反馈分数:

import * as ls from "langsmith/vitest";

// Numeric score
ls.logFeedback({ key: "accuracy", score: 0.95 });

// Boolean score
ls.logFeedback({ key: "correct", score: 1 });

// With comment
ls.logFeedback({ key: "quality", score: 0.8, comment: "Minor formatting issue" });
LLM-as-judge 评估器

对于主观质量评估,使用 ls.wrapEvaluator() 创建带追踪的评估器,以自动记录反馈:

import * as ls from "langsmith/vitest";
import { ChatAnthropic } from "@langchain/anthropic";

const judge = new ChatAnthropic({ model: "claude-sonnet-4-5-20250929" });

const evaluateHelpfulness = ls.wrapEvaluator(
  async ({ inputs, outputs, referenceOutputs }) => {
    const response = await judge.invoke([
      {
        role: "system",
        content: `Rate the helpfulness of the assistant's response on a scale of 0-1.
          Respond with JSON: { "score": <number>, "reasoning": "<explanation>" }`,
      },
      {
        role: "user",
        content: `Question: ${inputs.query}\nExpected: ${referenceOutputs.answer}\nActual: ${outputs.response}`,
      },
    ]);

    const parsed = JSON.parse(response.content as string);
    return {
      key: "helpfulness",
      score: parsed.score,
      comment: parsed.reasoning,
    };
  },
);

// In a test:
const result = await runner.run({ query: inputs.query });
const text = getFinalText(result);
await evaluateHelpfulness({
  inputs: { query: inputs.query },
  outputs: { response: text },
  referenceOutputs: referenceOutputs ?? {},
});
5. 编写测试文件
最简模板
import * as ls from "langsmith/vitest";
import { expect } from "vitest";
import { getDefaultRunner, getFinalText } from "@deepagents/evals";

const runner = getDefaultRunner();

ls.describe(
  runner.name,
  () => {
    ls.test(
      "test name",
      {
        inputs: { query: "..." },
        referenceOutputs: { answer: "..." },
      },
      async ({ inputs, referenceOutputs }) => {
        const result = await runner.run({ query: inputs.query });

        expect(result).toHaveAgentSteps(1);
        expect(result).toHaveFinalTextContaining(referenceOutputs.answer);
      },
    );
  },
  { projectName: "deepagents-js-<name>", upsert: true },
);
关键约定
  • getDefaultRunner() — 读取 EVAL_RUNNER 环境变量。未设置时会抛出错误。
  • runner.name — 用作 ls.describe 的名称 → 成为 LangSmith 数据集名称。
  • runner.run({ query, initialFiles? }) — 纯调用。返回 AgentTrajectory。
  • runner.extend({ systemPrompt?, tools?, subagents?, ... }) — 返回一个新的 runner,并覆盖 Agent 配置。用于需要自定义 Agent 设置的测试。
  • ls.describe 配置中的 projectName — 设置用于追踪的 LangSmith 项目。约定:"deepagents-js-<eval-name>"。
  • upsert: true — 复用现有数据集/项目,而不是每次运行都新建。
  • 始终从 vitest 导入 expect — harness 会在导入时为其扩展自定义匹配器。
  • ls.logOutputs() 在 runner 内部调用 — 不要在测试代码中调用。
使用 extend() 自定义 Agent 配置
// Custom system prompt
const result = await runner
  .extend({ systemPrompt: "You are a code reviewer." })
  .run({ query: inputs.query });

// Custom tools
const result = await runner
  .extend({ tools: [myCustomTool] })
  .run({ query: inputs.query });

// Custom subagents
const result = await runner
  .extend({
    subagents: [{
      name: "researcher",
      description: "Research assistant",
      systemPrompt: "You help with research.",
      tools: [searchTool],
    }],
  })
  .run({ query: inputs.query });
使用 initialFiles 为状态预置内容
const result = await runner.run({
  query: "Read /data.csv and count the rows.",
  initialFiles: {
    "/data.csv": "name,age\nAlice,30\nBob,25\n",
  },
});
基于沙箱的评估(容器化执行)

默认评估 runner 使用内存中的 StateBackend — Agent 可以读写文件,但无法执行 shell 命令、安装软件包或与真实操作系统交互。这对于测试工具选择、推理和文件操作来说已经足够。

对于需要真实执行的评估(SWE-bench、代码生成、Agent 式基准测试),Agent 必须运行在沙箱后端上。可用的沙箱提供方:

提供方软件包使用场景
Modal@deepagents/modal远程容器,支持 GPU
Daytona@deepagents/daytona云端开发环境
Deno@deepagents/deno轻量级本地沙箱
Node VFS@deepagents/node-vfs进程内虚拟文件系统 + shell

通过 extend({ backend }) 传入沙箱。使用 beforeAll / afterAll(套件级)或 beforeEach / afterEach(每个测试隔离)管理其生命周期:

import * as ls from "langsmith/vitest";
import { expect, beforeAll, afterAll } from "vitest";
import { getDefaultRunner, getFinalText } from "@deepagents/evals";
import { ModalSandbox } from "@deepagents/modal";

const runner = getDefaultRunner();
let sandbox: ModalSandbox;

beforeAll(async () => {
  sandbox = await ModalSandbox.create({
    image: "python:3.12-slim",
    timeout: 600,
  });
});

afterAll(async () => {
  await sandbox?.terminate();
});

ls.describe(
  runner.name,
  () => {
    ls.test(
      "agent can run python",
      { inputs: { query: "Write a Python script that prints 'hello' and run it." } },
      async ({ inputs }) => {
        const result = await runner
          .extend({ backend: sandbox })
          .run({ query: inputs.query });

        expect(result).toHaveFinalTextContaining("hello");
      },
    );
  },
  { projectName: "deepagents-js-sandbox-eval", upsert: true },
);

若要实现每个测试的隔离(每个测试都使用全新的沙箱):

import { beforeEach, afterEach } from "vitest";

let sandbox: ModalSandbox;

beforeEach(async () => {
  sandbox = await ModalSandbox.create({ image: "python:3.12-slim" });
});

afterEach(async () => {
  await sandbox?.terminate();
});

适合使用容器的情况:

  • 评估需要 execute()(shell 命令)
  • 评估涉及安装软件包或修改系统状态
  • 评估运行不受信任或生成的代码
  • 测试之间需要文件系统隔离

适合使用内存后端的情况:

  • 测试工具选择、推理或响应质量
  • 文件读取/写入/编辑操作(由 StateBackend + initialFiles 处理)
  • 系统提示词遵循情况、子 Agent 路由

将沙箱提供方添加到 package.json 依赖项中:

{
  "dependencies": {
    "@deepagents/modal": "workspace:*"
  }
}

并在 vitest.config.ts 中增加 testTimeout — 创建沙箱会增加额外开销:

testTimeout: 300_000,  // 5 minutes for sandbox evals
hookTimeout: 120_000,  // sandbox setup/teardown
6. 安装并验证
# From repo root
pnpm install

# Build the harness (if you changed it)
cd internal/eval-harness && pnpm build && cd ../..

# Run the new eval
EVAL_RUNNER=sonnet-4-5 pnpm --filter @deepagents/eval-<name> test:eval
7. 更新文档

在 evals/README.md 的“可用评估套件”表格中添加新的评估:

| [`<name>/`](./<name>/) | <one-line description> |

与 Python deepagents 评估保持一致

Python deepagents 软件包在 libs/deepagents/tests/evals/ 中包含评估套件。JS 评估应保持一致。创建新评估时,请查看 Python 源码中的参考实现: https://github.com/langchain-ai/deepagents/blob/v0.5/libs/deepagents/tests/evals/

当前一致性状态
Python 评估JS 评估状态
test_system_prompt.pyevals/basic/✅ 已覆盖
test_file_operations.pyevals/files/✅ 已覆盖
test_subagents.pyevals/subagents/✅ 已覆盖
test_memory.pyevals/memory/✅ 已覆盖
test_hitl.pyevals/hitl/✅ 已覆盖
test_skills.pyevals/skills/✅ 已覆盖
test_summarization.pyevals/summarization/❌ 缺失
关于 HITL 评估的说明

HITL 评估需要多步调用(调用 → 检查中断 → 使用 Command 恢复)。评估 runner 的 run() 只执行一次调用,因此 HITL 测试会通过带有 checkpointer 和 interruptOn 配置的 createDeepAgent() 直接构造 Agent。请参阅 evals/hitl/index.test.ts 中的模式。

关于摘要评测的说明

摘要评测需要使用较低 token 阈值的 SummarizationMiddleware、检查点管理器、真实或虚拟文件系统后端,以及多轮调用。 这些测试会绕过标准的 EvalRunner,直接构建 Agent,类似于 HITL。

参考:ls.test.each API

ls.test.each 是数据驱动评测中功能最强大的模式。数据表必须是一个对象数组,且至少包含 { inputs }:

ls.test.each([
  { inputs: { query: "Q1" }, referenceOutputs: { answer: "A1" } },
  { inputs: { query: "Q2" }, referenceOutputs: { answer: "A2" } },
  // Optional additional fields: id, metadata, split
  { id: "custom-id", inputs: { query: "Q3" }, referenceOutputs: { answer: "A3" }, split: "hard" },
])(
  "case: ${inputs.query}",  // Name template — interpolates from row
  async ({ inputs, referenceOutputs, testMetadata }) => {
    // testMetadata.exampleId, testMetadata.datasetId, etc.
    const result = await runner.run({ query: inputs.query });
    // ...
  },
);

参考:LangSmith 集成

数据集映射方式
概念LangSmith 实体
ls.describe(name, ...)数据集(name = 数据集名称)
ls.test(name, { inputs, referenceOutputs }, fn)数据集中的示例
运行测试套件对数据集运行实验
ls.logFeedback(...)实验运行的反馈
ls.logOutputs(...)实验输出(由运行器调用)
环境变量
变量用途
EVAL_RUNNER要使用的模型运行器(例如 sonnet-4-5)
LANGSMITH_API_KEYLangSmith 身份验证
LANGSMITH_PROJECT覆盖跟踪项目(通常通过 projectName 设置)
LANGSMITH_TEST_TRACKING设置为 "false" 以禁用 LangSmith 报告
ANTHROPIC_API_KEY用于 Anthropic 模型运行器
OPENAI_API_KEY用于 OpenAI 模型运行器
可用运行器

定义于 internal/eval-harness/src/setup.ts:

运行器名称模型
sonnet-4-5Claude Sonnet 4.5
sonnet-4-5-thinkingClaude Sonnet 4.5,启用扩展思考
opus-4-6Claude Opus 4.6
gpt-4.1GPT-4.1
gpt-4.1-miniGPT-4.1 Mini
o3-minio3-mini

参考:实现已发布的基准测试

实现现有基准测试时,请遵循以下署名和方法指南。

署名

务必注明原始基准测试的作者。在评测的 README.md 中:

# <benchmark-name>

Implementation of [<Benchmark Name>](<paper-or-repo-url>) by <authors> (<year>).

> <brief description from the paper abstract>

## Citation

\`\`\`bibtex
@article{...}
\`\`\`

## Adaptations

<Describe any differences from the original benchmark methodology:>
- <e.g. "Subset of N cases selected for cost efficiency">
- <e.g. "Adapted for agentic tool-use evaluation rather than direct QA">
- <e.g. "Uses LLM-as-judge instead of human annotation">
常见基准测试模式
QA / 事实类基准测试(例如 MMLU、TrivialQA)
  • 测试用例:问题 + 标准答案
  • 评分:对最终文本进行精确匹配或模糊匹配
  • 模式:使用 fixture JSON 的 ls.test.each
多轮 / 对话类基准测试(例如 MT-Bench)
  • 测试用例:相互衔接的对话轮次
  • 评分:对每一轮使用大模型作为裁判
  • 模式:共享状态的顺序 runner.run() 调用,或多消息查询
工具使用类基准测试(例如 ToolBench、API-Bank)
  • 测试用例:需要调用特定工具的任务
  • 评分:轨迹匹配器(调用了正确的工具,参数正确)
  • 模式:使用返回预设响应的自定义工具执行 runner.extend({ tools: [...] })
代码生成(例如 HumanEval、SWE-bench)
  • 测试用例:问题描述 + 测试套件
  • 评分:执行生成的代码并检查测试结果
  • 需要沙箱:是 — Agent 必须运行代码并观察输出
  • 模式:执行 runner.extend({ backend: sandbox }),将代码写入文件,并通过沙箱运行测试
记忆 / 长上下文(例如 oolong、needle-in-haystack)
  • 测试用例:大段上下文 + 检索问题
  • 评分:Agent 是否找到目标信息
  • 需要沙箱:否 — 通过 initialFiles 预置文件,并检查最终文本
  • 模式:使用内存中的 StateBackend 即可
Agent 类基准测试(例如 AgentBench、WebArena)
  • 测试用例:需要规划的多步骤任务
  • 评分:结合轨迹分析与最终状态检查
  • 需要沙箱:是 — Agent 需要访问 shell、安装软件包并与环境交互
  • 模式:使用每个测试用例独立的沙箱隔离来执行 runner.extend({ backend: sandbox })
处理大型数据集

对于包含数千个用例的基准测试:

  1. 选择子集 — 选取有代表性的子集(例如,每个类别 100 个用例)。记录选择标准。
  2. 支持拆分 — 使用测试用例中的 split 字段进行分类(例如,"easy"、"hard")。通过 vitest 筛选运行子集。
  3. 缓存 — 下载一次,并缓存到 .cache/(已添加到 gitignore)。
  4. 关注成本 — 运行前估算 API 成本,并记录在 README 中。考虑添加 --dry-run,在不调用 LLM 的情况下验证测试数据。

评论

支持 Markdown