AIエージェントに「自律的に動いてほしい」と思ったとき、最初に直面するのは「どう設計すれば安全に長時間動かせるか」という問題だ。
ハーネスエンジニアリングの基礎概念——CLAUDE.md・スキル・フック・エージェントの4要素——については【初心者向け】ハーネスエンジニアリング入門で解説している。この記事はその先、実際にコードでどう実装するかに特化した設計パターン集だ。
Anthropicのエンジニアリングブログで公開されているハーネス設計の実例、Claude Code SDKのマルチエージェントAPI、そしてHooksを使った安全ループの具体的な実装方法を体系的に解説する。
なぜ実装パターンが重要か——設計の失敗がエージェントを暴走させる
ハーネスエンジニアリングの概念を理解した後、多くの開発者が直面するのは「実際どう書けばいいのか」という実装の壁だ。
よくある失敗パターンがある。「とりあえずClaude Codeに全部任せる」——これは小タスクでは動くが、複雑な長期タスクでは必ず破綻する。理由は3つだ。
- コンテキスト枯渇: 長い会話ほどトークンが膨らみ、モデルが早期完了を選ぶ
- 自己評価バイアス: モデルは自分の出力を「良い」と評価しやすい
- 安全ガードの欠如: 危険なコマンドを実行してもフィードバックがない
「設計なきエージェントは暴走か停止の二択になる」——これがAnthropicのエンジニアが繰り返し強調するポイントだ。適切なハーネスがあれば、エージェントは自律的かつ安全に動く。
Claude Code完全ガイド2026でも触れたように、Claude Codeはエージェントとしての自律実行機能を持っているが、それを最大限に活かすためには設計が必要だ。
以降のセクションでは、AnthropicのSWE-bench事例・フロントエンド開発事例をベースに、再現性の高い実装パターンを紹介する。
Generator-Evaluatorパターン——外部検証が品質を決める
なぜ自己評価ではダメなのか
モデルに「この実装は正しいですか?」と聞くと、多くの場合「はい、正しいと思います」と答える。これは嘘をついているのではなく、自分の出力を評価するバイアスが働いているためだ。
人間でも同じ問題が起きる。自分が書いたコードのバグは見つけにくい。だからコードレビューという外部評価が存在する。AIエージェントでも同じ原理が適用される。
Generator-Evaluatorパターンは、この問題を解決するために設計された。
(仕様定義・スプリント設計)"] P --> G["Generatorエージェント
(実装・コード生成)"] G --> E["Evaluatorエージェント
(外部検証・採点)"] E -->|"スコア不足
(改善フィードバック)"| G E -->|"合格
(閾値クリア)"| D["スプリント完了"] D --> HF["Handoff.md
(次スプリントへ)"] HF --> P G --> L["実装ファイル"] L --> E
採点基準(Grading Criteria)の設計
Evaluatorが「合格」「不合格」を判定するには、主観を排除した採点基準が必要だ。Anthropicのフロントエンド開発事例では、以下のような重み付き基準を使っている。
| 評価軸 | 重み | 測定方法 |
|---|---|---|
| 機能的完成度 | 40% | E2Eテスト合格率 |
| デザイン品質 | 25% | Playwrightスクリーンショット比較 |
| コード品質 | 20% | Lint・型チェック |
| 独創性・UX | 15% | 人間評価(最終のみ) |
機能・品質は自動化でき、独創性のような主観的要素だけ人間が評価する。この分離がEvaluatorを高速かつ信頼性高く動かすポイントだ。
Pythonによる実装例
以下はGenerator-Evaluatorパターンの概念実装だ。実際のプロダクションコードはClaude Code SDKを使うが、パターンの理解には役立つ。
import asyncio
from dataclasses import dataclass
from typing import Optional
@dataclass
class SprintSpec:
"""スプリント仕様(PlannerがGeneratorに渡す契約)"""
description: str
success_criteria: list[str]
threshold: float # 0.0〜1.0(合格最低スコア)
max_iterations: int = 3
@dataclass
class EvaluationResult:
"""Evaluatorが返す評価結果"""
score: float
passed: bool
feedback: str
details: dict
class HarnessOrchestrator:
"""Generator-Evaluatorパターンのオーケストレーター"""
def __init__(self, generator_sdk, evaluator_sdk):
self.generator = generator_sdk
self.evaluator = evaluator_sdk
async def run_sprint(
self,
spec: SprintSpec,
iteration: int = 0
) -> Optional[dict]:
if iteration >= spec.max_iterations:
raise RuntimeError(f"最大反復回数({spec.max_iterations})を超えました")
# Step 1: Generatorが実装
print(f"[Generator] Sprint実装中... (試行 {iteration + 1})")
implementation = await self.generator.implement(spec.description)
# Step 2: Evaluatorが外部から検証
print("[Evaluator] 外部検証中...")
result: EvaluationResult = await self.evaluator.verify(
implementation=implementation,
criteria=spec.success_criteria,
tools=["playwright", "pytest", "eslint"]
)
print(f"[Evaluator] スコア: {result.score:.2f} / 閾値: {spec.threshold}")
# Step 3: スコア判定
if result.score >= spec.threshold:
print("[合格] スプリント完了")
return implementation
# Step 4: フィードバックを含めて再実装
print(f"[不合格] 改善フィードバック: {result.feedback}")
improved_spec = SprintSpec(
description=spec.description + f"\n\n## 前回の改善点\n{result.feedback}",
success_criteria=spec.success_criteria,
threshold=spec.threshold,
max_iterations=spec.max_iterations
)
return await self.run_sprint(improved_spec, iteration + 1)
ポイント: max_iterationsで無限ループを防ぐ。3回失敗したら例外を投げて人間に委ねる設計が安全だ。
Claude Code SDKでのマルチエージェント実装
実際の本番実装ではClaude Code SDKを使う。各エージェントに独立したコンテキストウィンドウと異なるツール権限を与えられるのが重要な点だ。
// Claude Code SDK でのマルチエージェント実装
import { createAgent } from '@anthropic-ai/claude-code-sdk';
import * as fs from 'fs';
// Planner: 仕様定義のみ、書き込み権限なし
const plannerAgent = createAgent({
role: 'planner',
systemPrompt: fs.readFileSync('.claude/agents/planner/CLAUDE.md', 'utf8'),
tools: ['Read', 'Glob', 'Grep', 'WebSearch'], // 調査ツールのみ
maxTokens: 8000
});
// Generator: 実装担当、書き込み権限あり
const generatorAgent = createAgent({
role: 'generator',
systemPrompt: fs.readFileSync('.claude/agents/generator/CLAUDE.md', 'utf8'),
tools: ['Read', 'Write', 'Edit', 'Bash', 'Glob'],
maxTokens: 32000 // 長い実装に対応
});
// Evaluator: 検証のみ、Write/Edit権限なし(重要)
const evaluatorAgent = createAgent({
role: 'evaluator',
systemPrompt: fs.readFileSync('.claude/agents/evaluator/CLAUDE.md', 'utf8'),
tools: ['Read', 'Bash'], // 実行は可、書き込みは不可
maxTokens: 16000
});
async function runSprint(userRequirement) {
// 1. Plannerがスプリント仕様を作成
const plan = await plannerAgent.run(userRequirement);
fs.writeFileSync('sprint-spec.md', plan.content);
// 2. Generator+Evaluatorのループ
const orchestrator = new HarnessOrchestrator(generatorAgent, evaluatorAgent);
const result = await orchestrator.run_sprint(plan);
// 3. Handoff.mdに記録(次スプリントへ)
fs.writeFileSync('handoff.md', generateHandoff(result));
return result;
}
スプリントベース分解パターン——長期タスクの設計
なぜタスクを分割するのか
「Webアプリを作って」という指示を一度に与えると、Claudeはコンテキストが膨らむにつれて品質が落ちる。1つのコンテキストウィンドウでこなせる「適切なサイズ」がある。
スプリントベース分解は、長期タスクを独立した検証可能な単位に切り分ける手法だ。
認証実装"] S1 -->|"Handoff.md"| S2["Sprint 2
DBスキーマ"] S2 -->|"Handoff.md"| S3["Sprint 3
APIエンドポイント"] S3 -->|"Handoff.md"| S4["Sprint 4
フロントエンド"] S4 -->|"Handoff.md"| S5["Sprint 5
E2Eテスト"] S5 --> D["デプロイ"] style S1 fill:#d4edda style S2 fill:#d4edda style S3 fill:#fff3cd style S4 fill:#fff3cd style S5 fill:#f8d7da
スプリント仕様書のフォーマット
スプリントを開始する前に成功基準をコントラクトとして合意するのが最重要ステップだ。曖昧な指示はEvaluatorが評価できない。
# Sprint 1: ユーザー認証の実装
## 概要
JWT認証機能をExpress.jsで実装する。
## 成功基準(Evaluatorが検証する)
- [ ] POST /auth/login が正しい認証情報で200を返す
- [ ] POST /auth/login が誤った認証情報で401を返す
- [ ] JWTトークンの有効期限が30分で正しく動作する
- [ ] パスワードはbcrypt(コスト係数12以上)でハッシュ化される
- [ ] 認証失敗のレート制限(5回/分)が動作する
## 受け入れ基準
- 全ユニットテストがパス(カバレッジ80%以上)
- E2Eテスト(Playwright)が最低3シナリオ通過
- ESLintエラーゼロ
## 除外スコープ(Sprint 3以降)
- リフレッシュトークンのローテーション
- ソーシャルログイン(Google/GitHub OAuth)
## 依存関係
- Node.js 20以上
- PostgreSQL接続設定(.envのDATABASE_URL)
成功基準の書き方のコツ: 「良い実装」ではなく「テストで確認できる具体的な動作」で書く。「セキュアな認証」より「bcryptコスト係数12以上でハッシュ化される」の方がEvaluatorが判定できる。
ハンドオフドキュメント(Handoff.md)
スプリント完了時に次のエージェントへ情報を引き継ぐのがHandoff.mdだ。コンテキストをリセットしても情報が保持される。
# Sprint 1 Handoff
## 完了日時
2026-04-19 14:30 JST
## 完了したこと
- JWTベース認証エンドポイント実装: src/auth/
- POST /auth/login(JWT発行)
- POST /auth/logout(トークン無効化)
- GET /auth/me(認証確認)
- テスト: tests/auth/ (全23本パス, カバレッジ84%)
- bcryptコスト係数: 12 (設定: src/config/security.ts)
- レート制限: express-rate-limit使用 (5回/分)
## Sprint 2の開始条件
- PostgreSQL接続が必要: DATABASE_URL を .env に設定
- マイグレーション未実行: `npm run migrate` を先に実行
## 既知の問題・技術的負債
- リフレッシュトークンのローテーション未実装(Sprint 3予定)
- トークンブラックリストはメモリ保存(Redis化はSprint 4予定)
- エラーメッセージの国際化(i18n)未対応
## 環境変数一覧(Sprint 2で必要なもの)
- JWT_SECRET: 設定済み (.env.example参照)
- JWT_EXPIRES_IN: "30m" (変更不要)
- DATABASE_URL: Sprint 2で設定が必要
コンテキスト管理の実装——コンテキスト不安への対策
コンテキスト不安(Context Anxiety)とは
Anthropicのエンジニアリングブログが命名したこの現象は、実際の運用で深刻な問題を引き起こす。
コンテキスト不安の症状:
- タスクが「完了した」と虚偽報告する
- エラーハンドリングやエッジケースを省略する
- テストを書かずにスキップする
- 「後で実装する」と先送りしてコードを残す
3つの対策戦略
戦略1: コンテキストリセット(最も安全)
スプリント完了のたびに新しいセッションを開始する。
# スプリント1完了後、新しいセッションでスプリント2を開始
claude -p "Sprint 2を開始してください。以下のファイルを読んでから作業を始めてください:
1. sprint-spec.md(スプリント2の仕様)
2. handoff.md(スプリント1の引き継ぎ事項)
3. CLAUDE.md(プロジェクトルール)
まず3ファイルを読んで、理解した内容を要約してください。"
戦略2: /compactコマンドによる圧縮
会話を継続しながらコンテキストを圧縮する。古いメッセージが要約され、直近の会話は保持される。
# 現在のセッション内でコンテキストが膨らんできたら
/compact
# 圧縮後も前のセッションを継続
claude --continue
戦略3: コンテキストチェックポイント
定期的に「今のコンテキスト使用率」を確認し、危険水域に入る前にリセットする。
#!/bin/bash
# .claude/hooks/check-context.sh
# ContextLengthが出力されたらアラート
CONTEXT_RATIO=$(cat | jq -r '.context_length_ratio // 0')
if (( $(echo "$CONTEXT_RATIO > 0.75" | bc -l) )); then
echo "⚠️ コンテキスト使用率が75%を超えました。/compact の実行を検討してください。" >&2
fi
exit 0
スプリント適切サイズの目安
| タスクの複雑さ | 推奨スプリント規模 | コンテキスト消費目安 |
|---|---|---|
| 単一機能(CRUD 1エンドポイント) | 1スプリント | 20〜30% |
| モジュール実装(認証・支払い等) | 2〜3スプリント | 各40〜50% |
| フィーチャー開発(複数モジュール) | 5〜8スプリント | 各30〜40% |
| フルアプリケーション構築 | 10〜20スプリント | 各20〜30% |
Claude Skillsを徹底解説でも解説したように、スキルを使ってスプリント管理の手順書そのものをClaude Codeに「教える」のも効果的だ。スプリント開始時に自動的にHandoff.mdを読み込み、完了時に自動的に記録するスキルを作成できる。
安全ループの実装——Hooksでガードする
Hooksアーキテクチャの全体像
Claude Codeのフックシステムは4つのイベントポイントを持つ。それぞれに実行するスクリプトを紐付けることで、エージェントの行動を外部からコントロールできる。
.claude/settings.jsonでのフック設定
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/security-check.sh",
"timeout": 10
}
]
},
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/path-guard.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/lint-and-test.sh",
"timeout": 60
}
]
}
],
"Stop": [
{
"matcher": ".*",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/log-session.sh",
"timeout": 5
}
]
}
]
}
}
セキュリティチェックフック
危険なコマンドをブロックするPreToolUseフックの実装例だ。
#!/bin/bash
# .claude/hooks/security-check.sh
# stdintからJSON入力を受け取る
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# 危険なコマンドパターンを定義
DANGEROUS_PATTERNS=(
'rm -rf /'
'rm -rf \*'
'DROP TABLE'
'DELETE FROM.*WHERE 1=1'
'git push.*--force'
'git push.*-f origin main'
'chmod 777'
'> /dev/sda'
)
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
if echo "$COMMAND" | grep -qiE "$pattern"; then
# 拒否レスポンスを返す(Claude Codeがブロックする)
jq -n \
--arg reason "危険なコマンド「$pattern」パターンを検出しました。実行をブロックしました。" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": $reason
}
}'
exit 0
fi
done
# 許可(何も出力しない)
exit 0
パス保護フック
書き込みが許可されたディレクトリ以外へのファイル書き込みをブロックする。
#!/bin/bash
# .claude/hooks/path-guard.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path // empty')
# 書き込み禁止ディレクトリ
PROTECTED_PATHS=(
"/etc/"
"/usr/"
"/var/"
"$HOME/.ssh/"
"$HOME/.aws/"
"$HOME/.config/gh/"
)
for protected in "${PROTECTED_PATHS[@]}"; do
if [[ "$FILE_PATH" == $protected* ]]; then
jq -n \
--arg path "$FILE_PATH" \
--arg protected "$protected" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": ("保護ディレクトリへの書き込みをブロック: " + $path)
}
}'
exit 0
fi
done
exit 0
自動Lint・テストフック
ファイル書き込み直後にLintとテストを自動実行し、結果をClaudeにフィードバックする。
#!/bin/bash
# .claude/hooks/lint-and-test.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# 拡張子でLintコマンドを切り替え
FEEDBACK=""
case "${FILE_PATH##*.}" in
ts|tsx)
LINT_RESULT=$(npx eslint "$FILE_PATH" --format compact 2>&1)
TYPE_RESULT=$(npx tsc --noEmit 2>&1)
if [ -n "$LINT_RESULT" ]; then
FEEDBACK="ESLintエラー:\n$LINT_RESULT"
fi
if [ -n "$TYPE_RESULT" ]; then
FEEDBACK="$FEEDBACK\n型エラー:\n$TYPE_RESULT"
fi
;;
py)
LINT_RESULT=$(python -m flake8 "$FILE_PATH" 2>&1)
if [ -n "$LINT_RESULT" ]; then
FEEDBACK="Flake8エラー:\n$LINT_RESULT"
fi
;;
esac
# フィードバックがある場合はClaudeに通知
if [ -n "$FEEDBACK" ]; then
jq -n --arg feedback "$FEEDBACK" '{
"hookSpecificOutput": {
"hookEventName": "PostToolUse",
"additionalContext": $feedback
}
}'
fi
exit 0
PostToolUseの強力な使い方: Lintエラーをフィードバックすると、Claudeは次のツール呼び出し前にエラーを修正しようとする。これにより「書く→Lintエラー→修正→再確認」のループが自動化される。
MCPサーバーの作り方2026完全ガイドでも解説しているように、フックとMCPサーバーを組み合わせると、外部サービスへのログ転送・Slack通知・Jira起票なども自動化できる。
本番運用:可観測性・コスト管理・段階的シンプル化
可観測性の実装——何が起きているかを記録する
本番環境でハーネスを動かすには、何が起きているかを記録する仕組みが不可欠だ。エージェントは人間が見ていないところで動くため、後から追跡できるログが命綱になる。
#!/bin/bash
# .claude/hooks/log-session.sh
# Stop Hookとして設定 — セッション終了時に実行
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // "unknown"')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
LOG_DIR="$HOME/.claude/session_logs"
mkdir -p "$LOG_DIR"
# セッションサマリーをJSONLに追記
jq -n \
--arg session "$SESSION_ID" \
--arg reason "$STOP_REASON" \
--arg time "$TIMESTAMP" \
'{
"session_id": $session,
"stop_reason": $reason,
"timestamp": $time
}' >> "$LOG_DIR/$(date +%Y-%m-%d).jsonl"
ツール呼び出しレベルのログはPreToolUseフックで収集する。
#!/bin/bash
# .claude/hooks/trace-tool-calls.sh
INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
TRACE_DIR="$HOME/.claude/traces"
mkdir -p "$TRACE_DIR"
# ツール呼び出しトレースを記録
echo "$INPUT" | jq --arg ts "$TIMESTAMP" '. + {"timestamp": $ts}' \
>> "$TRACE_DIR/${SESSION_ID}.jsonl"
# 処理は継続(ブロックしない)
exit 0
コスト管理——Anthropicの実例から学ぶ
Anthropicがフロントエンド開発ハーネスで計測した実データが参考になる。
| 構成 | トークンコスト | 品質評価 | 推奨用途 |
|---|---|---|---|
| Generator only | 1x(ベースライン) | 基本水準 | PoC・小機能 |
| Generator + Evaluator | 約5x | 大幅向上 | 機能開発・バグ修正 |
| Planner + Generator + Evaluator | 約20x | 劇的向上 | 長期プロジェクト |
| フル + 人間フィードバックループ | 約50x | 最高水準 | ミッションクリティカル |
モデルキャパシティによる段階的シンプル化
Anthropicのエンジニアが強調しているのが「ハーネスを定期的に見直して複雑さを削る」という原則だ。
モデルが賢くなるにつれて、以前は複雑なフィードバックループが必要だった処理が、シンプルな指示で解決できるようになる。
# ハーネスの複雑さレビューチェックリスト(四半期ごと)
SIMPLIFICATION_CHECKLIST = [
"このEvaluatorのチェックは、Generatorのプロンプトに直接書けないか?",
"このHookが防いでいるエラーは、最近のモデルバージョンで自然に回避されるようになったか?",
"スプリント回数を減らせるようになったか?(モデルが1スプリントでこなせる量が増えた)",
"このHandoff.md項目は、モデルが自動推論できるようになったか?",
"Plannerの介在が不要になった処理はないか?"
]
「まず最も単純な解決策から始め、失敗した部分にのみ複雑さを追加する」——これがAnthropicが推奨するハーネス設計の第一原則だ。
ハーネスパターンの選択指針
要件の複雑さ
│
├── 単一タスク(数時間で完了)
│ └── Generator only → シンプルなCLAUDE.md + Bashフック
│
├── 機能開発(数日)
│ └── Generator + Evaluator → 自動テスト + Lintフック
│
├── 長期プロジェクト(数週間〜)
│ └── Planner + Generator + Evaluator → フルハーネス + ログ収集
│
└── ミッションクリティカル
└── フルハーネス + 人間フィードバックポイント
まとめ——ハーネス設計は「信頼の工学」
ハーネスエンジニアリングの本質はAIを信頼するための仕組みを工学的に作ることだ。
「信頼するが検証する(Trust but verify)」——セキュリティ分野の古い格言がそのままAIエージェント設計に当てはまる。
Generator-Evaluatorパターンはその外部検証の仕組みを、スプリント分解は長期タスクの区切りを、コンテキスト管理は品質の安定を、そしてHooksは安全ガードを担う。
モデルが賢くなるにつれ、ハーネスはシンプルになっていく。今必要な複雑さと、不要になった複雑さを定期的に見直すことで、エージェントとの協働はより洗練されていく。
設計パターンは「決まった答え」ではなく「問題に応じて選ぶ道具」だ。まず最小構成から始め、実際の問題が出てから複雑さを追加する——その姿勢が、長く使えるハーネスを生む。