关于 eval-writer
eval-writer 是为 deepagentsjs monorepo 编写评测套件的技能,面向开发和测试 Agent 的工程师。它指导创建基于 vitest 的独立工作区包,使用 @deepagents/evals 运行并将结果记录到 LangSmith。
主要功能
- 01创建独立的 eval 工作区包
- 02设计内联、数据集或 LangSmith 测试用例
- 03使用轨迹匹配器和自定义评分
- 04为需要执行代码的评测接入沙箱
- 05运行测试并更新评测文档
适用场景
- 01评测 Agent 的工具调用和推理表现
- 02用数据集测试代码生成能力
- 03将评测结果记录到 LangSmith
- 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.py | evals/basic/ | ✅ 已覆盖 |
test_file_operations.py | evals/files/ | ✅ 已覆盖 |
test_subagents.py | evals/subagents/ | ✅ 已覆盖 |
test_memory.py | evals/memory/ | ✅ 已覆盖 |
test_hitl.py | evals/hitl/ | ✅ 已覆盖 |
test_skills.py | evals/skills/ | ✅ 已覆盖 |
test_summarization.py | evals/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_KEY | LangSmith 身份验证 |
LANGSMITH_PROJECT | 覆盖跟踪项目(通常通过 projectName 设置) |
LANGSMITH_TEST_TRACKING | 设置为 "false" 以禁用 LangSmith 报告 |
ANTHROPIC_API_KEY | 用于 Anthropic 模型运行器 |
OPENAI_API_KEY | 用于 OpenAI 模型运行器 |
可用运行器
定义于 internal/eval-harness/src/setup.ts:
| 运行器名称 | 模型 |
|---|---|
sonnet-4-5 | Claude Sonnet 4.5 |
sonnet-4-5-thinking | Claude Sonnet 4.5,启用扩展思考 |
opus-4-6 | Claude Opus 4.6 |
gpt-4.1 | GPT-4.1 |
gpt-4.1-mini | GPT-4.1 Mini |
o3-mini | o3-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 })
处理大型数据集
对于包含数千个用例的基准测试:
- 选择子集 — 选取有代表性的子集(例如,每个类别 100 个用例)。记录选择标准。
- 支持拆分 — 使用测试用例中的
split字段进行分类(例如,"easy"、"hard")。通过 vitest 筛选运行子集。 - 缓存 — 下载一次,并缓存到
.cache/(已添加到 gitignore)。 - 关注成本 — 运行前估算 API 成本,并记录在 README 中。考虑添加
--dry-run,在不调用 LLM 的情况下验证测试数据。