eval-writer について
eval-writerは、deepagentsjs monorepoで評価スイートを作成するスキルで、エージェントをテストするエンジニア向けです。@deepagents/evalsを使い、vitestで実行してLangSmithに結果を記録する独立したワークスペースパッケージの作成を案内します。
主な機能
- 01独立した評価ワークスペースパッケージの作成
- 02インライン、データセット、LangSmithのテストケース設計
- 03軌跡マッチャーとカスタム評価器による採点
- 04コード実行が必要な評価へのサンドボックス追加
- 05テスト実行と評価ドキュメントの更新
活用シーン
- 01エージェントのツール利用と推論の評価
- 02データセットを使ったコード生成のテスト
- 03評価結果のLangSmithへの記録
- 04Python deepagentsとのJS評価スイートの対応
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ langchain-ai/ deepagentsjs --skill eval-writerインストール後、Claude Code で/eval-writerを入力して読み込まれたか確認します。
詳細
Eval Writer
deepagentsjs モノレポ用の新しい eval スイートを作成します。各 eval は evals/ 配下の独立したワークスペースパッケージで、@deepagents/evals ハーネスを使用し、vitest で実行して、結果を LangSmith に報告します。
始める前に
既存の eval インフラストラクチャを読み、現在のパターンを把握してください。
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
既存の eval を確認し、規約を把握してください。
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. eval の要件を理解する
ユーザーに確認してください。
- どの機能を評価しますか?(ファイル操作、ツールの使用、複数ターンの推論、メモリ、コード生成など)
- テストケースの出所はどこですか? 選択肢:
- インライン — テストファイルにハードコードします(シンプルで、20 件未満に適しています)
- JSON/JSONL フィクスチャ — eval パッケージにチェックインします(20~200 件に適しています)
- 外部データセット — セットアップ時にダウンロードします(公開ベンチマークに適しています)
- LangSmith データセット — LangSmith API から取得します(共同でのキュレーションに適しています)
- 結果をどのように採点しますか? 選択肢:
- 軌跡マッチャー — ステップ数、ツール呼び出し、最終テキスト(組み込み)
- 完全一致/あいまい一致 — 出力を参照値と比較します(シンプル)
- LLM-as-judge — モデルを使って出力を採点します(複雑な eval に適しています)
- コード実行 — 生成されたコードを実行して結果を確認します(SWE-bench 形式)
- カスタム評価関数 — ドメイン固有の採点関数
- エージェントに特別な設定が必要ですか?(カスタムツール、サブエージェント、システムプロンプト、初期ファイルなど)
2. eval パッケージを作成する
すべての eval は 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"
}
}
必要に応じて追加の依存関係を加えます(例:ツールスキーマ用の zod、tool() ヘルパー用の 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"],
},
});
実行時間の長い eval(複数ターン、コード実行など)では testTimeout を調整します。
eval にカスタムセットアップ(データセットの読み込みなど)が必要な場合は、setupFiles に "./vitest.setup.ts" を追加します。
README.md
# <name>
<One-line description of what this eval tests.>
ワークスペースへの登録を確認する
pnpm-workspace.yaml に "evals/*" が含まれていることを確認します。すでに含まれているはずですが、含まれていない場合は追加してください。
3. テストケースを設計する
パターン A:インラインテストケース(シンプルな eval)
小規模で手作業で作成したテストスイートに最適です。各テストは、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 を使ったデータ駆動方式(中規模の eval)
フィクスチャファイルから取得する 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...
},
);
フィクスチャの 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;
}
eval パッケージの .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. 採点ロジックを作成する
組み込みの軌跡マッチャー
ハーネスには、LangSmith のフィードバックスコアも記録する vitest マッチャーが用意されています。主な構成要素としてこれらを使用します。
// 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?, ... })— エージェント設定を上書きした新しいランナーを返します。カスタムのエージェント設定が必要なテストで使用します。ls.describe設定のprojectName— トレースの LangSmith プロジェクトを設定します。命名規則は"deepagents-js-<eval-name>"です。upsert: true— 実行のたびに新しいデータセット/プロジェクトを作成する代わりに、既存のものを再利用します。- 必ず
vitestからexpectをインポートしてください — ハーネスはインポート時にカスタムマッチャーを追加します。 ls.logOutputs()はランナー内で呼び出されます。テストコードでは呼び出さないでください。
カスタムのエージェント設定に extend() を使用する
// 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",
},
});
サンドボックスを使用する評価(コンテナ化された実行)
デフォルトの評価ランナーはインメモリの StateBackend を使用します。エージェントはファイルを読み書きできますが、シェルコマンドの実行、パッケージのインストール、実際の OS とのやり取りはできません。ツールの選択、推論、ファイル操作のテストにはこれで十分です。
実際の実行が必要な評価(SWE-bench、コード生成、エージェント型ベンチマーク)では、エージェントをサンドボックスバックエンドで実行する必要があります。利用可能なサンドボックスプロバイダーは次のとおりです。
| プロバイダー | パッケージ | 用途 |
|---|---|---|
| Modal | @deepagents/modal | リモートコンテナ、GPU サポート |
| Daytona | @deepagents/daytona | クラウド開発環境 |
| Deno | @deepagents/deno | 軽量なローカルサンドボックス |
| Node VFS | @deepagents/node-vfs | インプロセス仮想ファイルシステムとシェル |
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()(シェルコマンド)が必要な場合 - 評価でパッケージのインストールやシステム状態の変更を行う場合
- 評価で信頼できないコードや生成されたコードを実行する場合
- テストごとにファイルシステムを分離する必要がある場合
インメモリで十分な場合:
- ツールの選択、推論、応答品質をテストする場合
- ファイルの読み取り/書き込み/編集操作を行う場合(
StateBackendとinitialFilesで処理) - システムプロンプトへの準拠やサブエージェントのルーティングをテストする場合
サンドボックスプロバイダーを 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. ドキュメントを更新する
「Available eval suites」テーブルの 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 評価では、複数ステップの呼び出し(invoke → 中断の確認 → Command で再開)が必要です。評価ランナーの run() は1回だけ呼び出すため、HITL テストでは checkpointer と interruptOn 設定を指定して、createDeepAgent() からエージェントを直接構築します。実装パターンについては evals/hitl/index.test.ts を参照してください。
要約評価に関する注意事項
要約評価には、低いトークンしきい値を設定したSummarizationMiddleware、チェックポインター、実または仮想のファイルシステムバックエンド、および複数ターンの呼び出しが必要です。
これらのテストでは標準のEvalRunnerを経由せず、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 | LangSmithへのレポートを無効にするには"false"を設定します |
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)
- テストケース:質問+正解
- スコアリング:最終テキストの完全一致またはあいまい一致
- パターン:フィクスチャJSONを使用した
ls.test.each
複数ターン/会話型ベンチマーク(例:MT-Bench)
- テストケース:互いの内容を踏まえて展開する会話ターン
- スコアリング:各ターンをLLM-as-judgeで評価
- パターン:状態を共有する逐次的な
runner.run()呼び出し、または複数メッセージのクエリ
ツール使用ベンチマーク(例:ToolBench、API-Bank)
- テストケース:特定のツール呼び出しを必要とするタスク
- スコアリング:軌跡マッチャー(正しいツールの呼び出し、正しい引数)
- パターン:定型応答を返すカスタムツールを指定した
runner.extend({ tools: [...] })
コード生成ベンチマーク(例:HumanEval、SWE-bench)
- テストケース:問題の説明+テストスイート
- スコアリング:生成されたコードを実行し、テスト結果を確認
- サンドボックスが必要:はい — エージェントはコードを実行して出力を確認する必要があります
- パターン:
runner.extend({ backend: sandbox })を使い、コードをファイルに書き込み、サンドボックス経由でテストを実行します
メモリ/長文コンテキストベンチマーク(例:oolong、needle-in-haystack)
- テストケース:大きなコンテキスト+検索に関する質問
- スコアリング:エージェントが対象の情報を見つけられるかどうか
- サンドボックスが必要:いいえ —
initialFilesでシードファイルを設定し、最終テキストを確認します - パターン:インメモリの
StateBackendで十分です
エージェント型ベンチマーク(例:AgentBench、WebArena)
- テストケース:計画を必要とする複数ステップのタスク
- スコアリング:軌跡の分析と最終状態の確認の組み合わせ
- サンドボックスが必要:はい — エージェントにはシェルへのアクセス、パッケージのインストール、環境とのやり取りが必要です
- パターン:テストごとにサンドボックスを分離した
runner.extend({ backend: sandbox })
大規模なデータセットの扱い方
数千件のケースを含むベンチマークの場合:
- サブセットの選定 — 代表的なサブセットを選びます(例:カテゴリごとに100件)。選定基準を文書化します。
- 分割への対応 — テストケースの
splitフィールドを使って分類します(例:"easy"、"hard")。vitest のフィルタリングを使ってサブセットを実行します。 - キャッシュ — 一度ダウンロードし、
.cache/にキャッシュします(gitignore の対象)。 - コストの把握 — 実行前に API コストを見積もります。README に記録します。LLM を呼び出さずにフィクスチャを検証する
--dry-runの導入も検討します。