AI エージェント運用のコストを最適化する非同期アーキテクチャ
この記事をシェア
はじめに:「強いモデルは欲しい、でも従量課金は払いたくない」
エージェント運用、コストが本当にバカにならない。
私のクライアント案件でも、Claude Code Max プラン(月額数万円)で済んでいたものが、ちょっと油断すると API 従量課金で数十万円コースになる。OpenRouter に逃げても Qwen 3.5 72B や MiniMax M2.7 あたりを"がっつり"使うと、それなりに積み上がる。
でも、よく観察していると気付くことがある。「重いタスク」と「軽いタスク」の比率は、実は 1:9 くらいだということ。
- 1 割:長文ドキュメント生成、深い推論、複雑なリファクタリング、設計判断
- 9 割:ファイル読み書き、grep、簡単な変換、定型的な調査、結果の整形
この 9 割をHermes + 安い LLM(OpenRouter / Ollama / オンプレ vLLM)で捌き、残り 1 割の「ここぞ」というタイミングだけClaude Code Max を定額サブスクの範囲内でサブエージェントとして呼び出す。これが本記事で提案する設計思想である。
しかもさらに踏み込んで、サブエージェント呼び出しは同期 delegate じゃなく cronjob 経由の非同期にするのが本記事の本命。コンテキストが疎結合になり、タイムアウトも気にしなくていい。これを後半で詳しく扱う。
1. なぜこの設計が効くのか:エージェント運用のコスト構造
1-1. エージェント運用のコスト分布
実際のClaude Code利用ログを観察した実感値だが、トークン消費の偏り方はだいたいこうなる。
| タスク種別 | 全タスクに占める頻度 | トークン消費比率 |
|---|---|---|
| ファイル読み・grep・lint・テスト実行 | 60% | 5% |
| 簡単な変換・要約・整形 | 25% | 10% |
| 長文ドキュメント生成・複雑な設計判断 | 10% | 60% |
| その他(コミット・初期化・確認応答) | 5% | 25% |
つまり、コスト消費の 60% を生む「重いタスク」は全体の 10% しかない。逆に言うと、軽いタスクをすべて高級モデルでやっているから無駄に費用が出ている。
ここを切り分けて、軽い仕事は安いモデル、重い仕事だけ Claude Code(しかもサブスク定額枠内)にすれば、合計コストは劇的に下がる。
1-2. Claude Code Max プランの「使い切れない」枠
私のような個人事業主の場合、Claude Code Max プランを契約すると、月の利用枠が結構余る。インタラクティブに使う時間って、1 日せいぜい 2〜3 時間で、それ以外の時間は枠が眠ったままになる。
この眠った枠を、シェルスクリプト経由のサブエージェント呼び出しで使い切るというのが、この設計の本質。サブスクなので、何回呼んでも追加課金は発生しない(レート制限はあるが)。
1-3. なぜ Hermes が「親エージェント」に向くか
主役は Hermes Agent。理由は、
- バックエンド LLM を自由に切り替えられる(OpenRouter、Ollama、オンプレ vLLM、何でも)
- ローカル/カスタムエンドポイントの実装が安定している
- Bash ツール経由で外部コマンドを叩ける =
claude -pを呼べる - MCP クライアントとしても優秀(Dify との組み合わせが効く)
Hermes を「指揮官」として、安い LLM で日常タスクを回しながら、必要な時だけ Claude Code Max に重い仕事を投げる。
2. アーキテクチャ全体図
┌──────────────────────────────────────────────────┐
│ Hermes Agent (CLI / TUI) │
│ 主モデル:OpenRouter:qwen3.6-72b │
│ or Ollama Cloud (free tier) │
│ or オンプレ vLLM │
│ → 9 割のタスクはここで処理(軽い、安い、速い) │
└────────┬─────────────────────────────────────────┘
│
│ ① 重いタスクを検知
│
├──[A] 同期パターン:claude_subagent.sh を直接呼び出す
│ → 即時応答が必要、軽めのサブタスク
│
└──[B] 非同期パターン:タスクキューに投入して即終了
↓
┌─────────────────────────────────────────────┐
│ タスクキュー(JSON / SQLite / S3 など) │
└─────────────────────────────────────────────┘
↑
┌─────────────────────────────────────────────┐
│ cronjob (毎分起動) │
│ → タスクキューを読む │
│ → claude -p --bare で処理 │
│ → 結果をキュー or Slack/メールに書き戻す │
└─────────────────────────────────────────────┘
ポイントは、Hermes が指揮官、Claude Code が「ちょっと頭脳労働してくれる外注先」という関係性。同期呼び出し [A] も使えるが、メインで推したいのは非同期パターン [B]。理由は後述。
3. 実装の核心:claude -p を使い倒す
3-1. 基本の headless 呼び出し
Claude Code の公式ドキュメントから引いておくと、-p(または--print)フラグで headless モードになる。
claude -p "Find and fix the bug in auth.py" --allowedTools "Read,Edit,Bash"
これでインタラクティブセッションを開かずに、プロンプト → 処理 → 結果を stdout に出力 → 終了、という挙動になる。
3-2. サブエージェント呼び出しに最適な--bareフラグ
ここがポイント。--bareを付けると、フックやスキル、プラグイン、MCP サーバ、CLAUDE.mdの自動読み込みをすべてスキップする。
公式の説明だと「--bareを付けると hooks、skills、plugins、MCP サーバ、auto memory、CLAUDE.mdの自動発見をスキップしてスタートアップ時間を短縮する」とある。CI やスクリプト用途で、毎回同じ結果が欲しい場合に向く、と。
サブエージェント用途ではまさにこれ。毎回サブプロセスを起動するので、起動オーバーヘッドが小さいに越したことはない。
claude --bare -p "Summarize this file" --allowedTools "Read"
3-3. コスト・実行時間の安全装置
claude -p は便利だが、暴走するとサブスク枠を急速に食う。必ず安全装置を付ける。
--max-turns 15:エージェントがツール呼び出しできる最大ターン数--max-budget-usd 1.00:使用額の上限(USD)--allowedTools "Read,Bash(grep *)":使えるツールをホワイトリストで明示
「両方とも無人実行には必須」と公式ドキュメント・関連解説でも明記されている。
claude --bare -p "complex analysis" \
--max-turns 15 \
--max-budget-usd 1.00 \
--allowedTools "Read,Glob,Grep" \
--output-format json
3-4. 構造化出力で結果を渡す
呼び出し元に渡すときは、結果が JSON で返ってきてくれると後処理が楽。
claude --bare -p "List all TODO comments and categorize by priority" \
--output-format json
--output-format jsonで JSON、stream-jsonでストリーミング JSON も選べる。
4. 同期パターン:直接 delegateTask する場合
4-1. シンプルなサブエージェント呼び出しスクリプト
~/bin/claude_subagent.sh:
#!/usr/bin/env bash
set -euo pipefail
PROMPT="${1:-}"
TASK_TYPE="${2:-general}"
case "$TASK_TYPE" in
code) TOOLS="Read,Edit,Bash(git *),Glob,Grep"; MAX_TURNS=20; BUDGET="2.00";;
docs) TOOLS="Read,Glob,Grep"; MAX_TURNS=10; BUDGET="0.50";;
review) TOOLS="Read,Glob,Grep,Bash(git diff *)"; MAX_TURNS=15; BUDGET="1.00";;
*) TOOLS="Read,Glob,Grep"; MAX_TURNS=10; BUDGET="0.50";;
esac
claude --bare -p "$PROMPT" \
--allowedTools "$TOOLS" \
--max-turns "$MAX_TURNS" \
--max-budget-usd "$BUDGET" \
--output-format json 2>/dev/null
これを Hermes から Bash ツール経由で呼ぶ。Hermes 側で「重いタスクを検知したら呼ぶ」と skill に教えればいい。
4-2. 同期パターンの限界
同期呼び出しは即時応答が欲しいケースには向く。だが、重いタスクほど同期呼び出しと相性が悪い。具体的には、
- タイムアウトリスク:
claude -pが深く考え始めると数分〜十数分かかる。Hermes セッションがその間ブロックされる - コンテキスト汚染:サブエージェントの長大な思考過程やツール呼び出しが Hermes セッションに戻ってきて、親側のコンテキストを圧迫する
- エラー伝播:
claude -pが途中で失敗すると Hermes セッションごと止まる - Hermes の判断力依存:Hermes の主モデル(安い LLM の場合、判断ミスが起きやすい)がサブエージェントを呼ぶ判断を誤ると、待ち時間と引き換えにゴミが返ってくる
ここを根本から解決するのが、次に紹介するcronjob 経由の非同期パターン。
5. 【本命】非同期パターン:cronjob 経由で Claude Code に重い仕事を投げる
ここが本記事のメインディッシュ。
5-1. なぜ非同期がおすすめか
同期 delegate でなく、タスクキューに投げて、別プロセスの cronjob がそれを拾って処理する形にすると、いいことしかない。
- コンテキストが完全に疎結合:Hermes セッションとサブエージェントの実行は別プロセス・別セッション。互いのコンテキストが汚染しない
- タイムアウトが消える:Hermes 側はタスクをキューに投げて即終了。Claude Code 側は何分かかってもいい
- 失敗が伝播しない:
claude -pがコケても Hermes は関知しない。リトライは cron 側の責任 - Hermes の判断ミスを後から訂正できる:キューに入ったタスクを人間が確認 → 不要なら消す、というガバナンスが効く
- 並列実行できる:cron が複数タスクをまとめて処理することも、複数の worker で並列処理することもできる
- 長時間タスクに強い:「リファクタリング案を 3000 字でまとめて」のような 10 分かかるタスクが普通に回る
- 失敗してもログが残る:cron が処理した結果はログに残るので、デバッグや監査がやりやすい
特に 1 と 2 が大きい。「重い仕事ほど時間がかかる、時間がかかる仕事ほど同期呼び出しと相性が悪い」という宿命を、非同期にすることで根本解決できる。
5-2. シンプルなタスクキュー実装
凝った仕組みは要らない。JSON ファイル 1 個+ディレクトリで十分。
~/.claude_queue/
├── pending/ # Hermes が投入、cron が拾う
│ ├── 20260503-143022-uuid1.json
│ └── 20260503-143508-uuid2.json
├── in_progress/ # 処理中
├── completed/ # 完了、結果付き
└── failed/ # 失敗、エラー付き
タスク定義ファイル(JSON):
{
"id": "uuid1",
"created_at": "2026-05-03T14:30:22+09:00",
"task_type": "docs",
"prompt": "プロジェクト payment-service のリファクタリング提案を 3000 字でまとめてください",
"working_dir": "/home/noguchi/projects/payment-service",
"max_turns": 30,
"max_budget_usd": 2.0,
"allowed_tools": "Read,Glob,Grep",
"callback": {
"type": "slack",
"webhook": "https://hooks.slack.com/services/..."
}
}
5-3. Hermes → タスクキュー投入スクリプト
~/bin/enqueue_claude_task.sh:
#!/usr/bin/env bash
set -euo pipefail
TASK_TYPE="${1:-docs}"
PROMPT="${2:-}"
WORKING_DIR="${3:-$(pwd)}"
QUEUE_DIR="$HOME/.claude_queue/pending"
mkdir -p "$QUEUE_DIR"
UUID=$(uuidgen)
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
FILENAME="${TIMESTAMP}-${UUID}.json"
cat > "$QUEUE_DIR/$FILENAME" <
これを Hermes から呼ぶだけ。呼んだ瞬間に即終了するので、Hermes セッションは全くブロックされない。
5-4. cronjob worker
~/bin/claude_queue_worker.sh:
#!/usr/bin/env bash
set -euo pipefail
QUEUE_BASE="$HOME/.claude_queue"
PENDING="$QUEUE_BASE/pending"
IN_PROGRESS="$QUEUE_BASE/in_progress"
COMPLETED="$QUEUE_BASE/completed"
FAILED="$QUEUE_BASE/failed"
LOG_FILE="$QUEUE_BASE/worker.log"
mkdir -p "$PENDING" "$IN_PROGRESS" "$COMPLETED" "$FAILED"
# 排他ロック(複数 worker の同時起動防止)
LOCKFILE="/tmp/claude_queue_worker.lock"
exec 200>"$LOCKFILE"
flock -n 200 || { echo "$(date): another worker running, skip" >> "$LOG_FILE"; exit 0; }
# 1 ジョブだけ処理(次の cron 起動で次が回る)
TASK_FILE=$(ls -t "$PENDING"/*.json 2>/dev/null | tail -n 1 || true)
if [ -z "$TASK_FILE" ]; then
exit 0
fi
TASK_NAME=$(basename "$TASK_FILE")
mv "$TASK_FILE" "$IN_PROGRESS/$TASK_NAME"
TASK_FILE="$IN_PROGRESS/$TASK_NAME"
echo "$(date): processing $TASK_NAME" >> "$LOG_FILE"
PROMPT=$(jq -r '.prompt' "$TASK_FILE")
WORKING_DIR=$(jq -r '.working_dir' "$TASK_FILE")
MAX_TURNS=$(jq -r '.max_turns' "$TASK_FILE")
BUDGET=$(jq -r '.max_budget_usd' "$TASK_FILE")
TOOLS=$(jq -r '.allowed_tools' "$TASK_FILE")
cd "$WORKING_DIR"
if RESULT=$(claude --bare -p "$PROMPT" \
--allowedTools "$TOOLS" \
--max-turns "$MAX_TURNS" \
--max-budget-usd "$BUDGET" \
--output-format json 2>"$QUEUE_BASE/error.log"); then
jq --argjson result "$RESULT" \
--arg completed_at "$(date -Iseconds)" \
'. + {result: $result, completed_at: $completed_at}' \
"$TASK_FILE" > "$COMPLETED/$TASK_NAME"
rm "$TASK_FILE"
echo "$(date): completed $TASK_NAME" >> "$LOG_FILE"
# コールバック(Slack に通知など)
CALLBACK=$(jq -r '.callback.type // "none"' "$COMPLETED/$TASK_NAME")
if [ "$CALLBACK" = "slack" ]; then
WEBHOOK=$(jq -r '.callback.webhook' "$COMPLETED/$TASK_NAME")
SUMMARY=$(echo "$RESULT" | jq -r '.result' | head -c 500)
curl -s -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"Claude Code タスク完了:$TASK_NAME\n\`\`\`$SUMMARY\`\`\`\"}" \
"$WEBHOOK" >/dev/null
fi
else
jq --arg failed_at "$(date -Iseconds)" \
--arg error "$(cat "$QUEUE_BASE/error.log")" \
'. + {failed_at: $failed_at, error: $error}' \
"$TASK_FILE" > "$FAILED/$TASK_NAME"
rm "$TASK_FILE"
echo "$(date): FAILED $TASK_NAME" >> "$LOG_FILE"
fi
5-5. cron 登録
crontab -e
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/home/noguchi/.npm-global/bin
# 毎分タスクキューをチェック
* * * * * /home/noguchi/bin/claude_queue_worker.sh
# 古い完了タスクの掃除(毎日午前 3 時、7 日以上前のものを削除)
0 3 * * * find /home/noguchi/.claude_queue/completed -mtime +7 -delete
0 3 * * * find /home/noguchi/.claude_queue/failed -mtime +30 -delete
これで設定終わり。毎分 1 ジョブずつ処理する非同期エージェント基盤の出来上がり。
5-6. Hermes からの使い方
Hermes に以下の skill を教えるだけ:
~/.hermes/skills/queue_heavy_task.md:
# Queue Heavy Task
長時間かかる重いタスク(長文ドキュメント生成、設計レビュー、深い推論)は
自分で処理せず、以下のコマンドでタスクキューに投入してください。
## 投入コマンド
```bash
enqueue_claude_task.sh <task_type> "<prompt>" [working_dir]
```
task_type: docs / code / review
## 投入後の動作
- 即座に `{"status": "queued", "task_id": "..."}` が返る
- 実際の処理はバックグラウンドで進む
- 結果は Slack(設定済みなら)または `~/.claude_queue/completed/` で確認可
ユーザーには「タスクをキューに投入しました。完了したら Slack に通知が来ます」と伝えてください。
5-7. 実運用での効果
私がこの構成にしてから感じる変化:
- Hermes セッションがブロックされなくなった:重い仕事を投げても次の作業に進める
- コンテキストが綺麗:サブエージェントの推論過程が Hermes 側に残らない
- 失敗が見える:
failed/ディレクトリを見れば何がコケたか一目でわかる - 監査ログが自動で取れる:
completed/にすべての処理履歴が残る - 再実行が簡単:失敗したタスクを
mv failed/xxx.json pending/で再投入できる
特に「コンテキストが綺麗」というのは、長期運用では効いてくる。同期 delegate 型だと、Hermes セッションが数時間使ううちに、サブエージェントの残骸でコンテキストが膨れ上がる現象が起きる。非同期だとそれがゼロ。
6. もう一段の応用:Dify を「社内データ MCP ゲートウェイ」として噛ませる
ここまでが基本構成。さらに、社内データ参照が絡むケースではDify を社内データ MCP ゲートウェイとして主役級に組み込むと完成度が上がる。
6-1. なぜ Dify を噛ませるか
claude -p で社内コードベースやドキュメントを読ませたい時、生のファイルアクセスを許すのはセキュリティ的に怖い。Dify を噛ませると、
- ワークフローでアクセス可能なデータをガードレール付きで定義できる
- Multimodal Knowledge Base(v1.11 以降)で画像込みの社内ドキュメントが検索可能
- Agent Node で Agentic RAG が組める(一回限りの retrieval-then-generation より精度が高い)
- Dify ワークフローを MCP サーバとして公開できる(v1.6 以降の双方向 MCP ネイティブ対応)
Dify は 2026 年 3 月に HSG リードのシリーズ Pre-A $30M を調達済み(評価額$180M)、世界 140 万台以上で稼働、Maersk、ETS、Anker Innovations、Novartis などが商用採用、SOC 2 Type II / ISO 27001:2022 / GDPR 準拠も 2 年連続達成と、エンタープライズ案件で説明責任を果たす材料は揃っている。
6-2. データの流れ
ユーザー → Hermes (Qwen 3.5)
│
├─ ① Dify MCP で社内データを取得(Dify ワークフロー経由)
│ → VPC 内に閉じる
│
├─ ② 軽いタスクは自分で処理(OpenRouter 経由)
│
└─ ③ 重いタスクはタスクキューへ投入
↓
cron worker
↓
claude -p (Max サブスク枠内で実行)
↓
結果を Slack/メール通知
タスクキュー経由なら、claude -pに渡すプロンプトに「Dify MCP から取得した社内データの抽象化された要約」だけを乗せる、という運用も可能。生データを直接 Anthropic に送らずに、構造化された問い設定だけを渡す設計ができる。
7. 実際のコスト感(2026 年 5 月時点、月額)
私の検証環境での実測値
- Hermes 主モデル(OpenRouter 経由 Qwen 3.5 72B):日常タスクで月額 2,000〜5,000 円程度
- Claude Code Max プラン:月額数万円固定(追加課金なし)
- Dify Community 版 + AWS 自前運用:月額 3〜5 万円
- その他(Ollama Cloud free tier、auxiliary 用 Gemini Flash など):ほぼゼロ
合計:月額 8〜12 万円程度で、ハイエンドエージェント運用が回る。
この構成にしなかった場合との比較
- Claude Code Max プランだけで API にフォールバック多発した場合:月額 30〜80 万円になる案件もある
- Hermes + OpenRouter だけで重いタスクも Qwen でやらせた場合:品質が落ちる + Qwen の長文生成は意外とトークン食う、月額 15〜25 万円
つまり、月額 20〜70 万円のコスト削減が現実に出る。年間にすると数百万円の差になる。
8. 詰まったポイントと回避策
8-1. cron の PATH が効かない問題
cron ジョブでclaudeコマンドが見つからないエラーが頻発する。これは cron が.bashrc等を読まないから。
回避策:
# crontab 内で明示的に PATH を設定
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/home/noguchi/.npm-global/bin:/home/noguchi/.local/bin
または、worker script の先頭で source ~/.bashrc か source ~/.profile する。
8-2. cron からclaudeの認証が通らない
claude loginはインタラクティブシェルで実行するが、cron から起動するとそのトークンが見えないことがある。
回避策:
~/.claude/ディレクトリの所有権を確認(cron ユーザーと一致しているか)- 必要なら
HOME=/home/noguchiを明示的に設定 - それでもダメなら、
API_KEY方式にして cron worker 側で環境変数から渡す
8-3. 排他ロックを忘れて二重起動
毎分起動する cron で、前回のジョブがまだ走っているのに次が起動して二重実行になる。
回避策:本記事の例のように flock を使う。flock -n 200 || exit 0 で「既に走っているなら静かに終了」。
8-4. 失敗ジョブが滞留する
ネットワーク不安定時など、failed/ディレクトリに溜まることがある。
回避策:
- 1 日 1 回、
failed/を眺めるのを習慣にする(あるいは件数を Slack 通知) - リトライ worker script を別途作って、失敗してから 1 時間以上経ったものを
pending/に戻す - ただし無限リトライしないように、リトライ回数を JSON に記録しておく
8-5. ジョブの優先順位
タスクキューが FIFO だと、急ぎのタスクが後ろのジョブに詰まる。
回避策:
- pending ディレクトリを
pending/high/,pending/normal/に分ける - worker script で high を優先的に拾う
enqueue_claude_task.shに第 3 引数で優先度を取れるようにする
8-6. プロジェクト固有の文脈が伝わらない
--bareを付けると CLAUDE.md 等が読まれないので、プロジェクト固有の前提が失われる。
回避策:
- 必要な前提だけプロンプトに直接埋め込む(タスクキュー投入時に展開)
--system-promptでプロジェクト固有の指示を注入する- 重要な文脈ファイルは
--allowedTools "Read"で明示的に読ませる
9. この設計が成立する前提条件
正直に書いておくと、この設計には前提条件がある。
- Claude Code Max プランを契約していること:API 従量課金だと意味がない
- 個人または小規模チームでの使用:大規模チームで全員がガンガン叩くとレート制限に当たる
- Anthropic に「サブエージェント呼び出し時のプロンプト」を送ってもよいこと:データレジデンシー要件が厳しい案件では、
claude -pの引数に何を渡せるかが制約される - 手元の検証環境や PoC 段階での運用:本番ワークロードに組み込む際は、可用性・SLA の設計が別途必要
逆に言うと、個人事業主や小規模チームで、Anthropic に対しては OK、というクライアント案件にはドンピシャでハマる設計だ。私の場合、複数のクライアント PoC を同時進行で回す自分のワークフローにとって、これは決定打になった。
10. まとめ:「強いモデルを、強い仕事だけに使う」という分業設計
書いてきた通り、Claude Code Max サブスクを「常時使う高級ツール」ではなく「キューに投げれば後で答えてくれる外注専門家」として位置付け直すと、コスト構造が劇的に変わる。
人間のチームに置き換えれば自然な発想だ。ジュニアにできる仕事はジュニアに任せて、シニアには本当に判断が必要なところだけ依頼する──こうした分業を、AI エージェントの世界でも素直に組んだ、というだけの話である。
- 9 割の仕事は Hermes + 安い LLM で処理(ジュニア相当)
- 1 割の重い仕事はタスクキューに投入
- cronjob が拾って
claude -pで処理(シニアに非同期で外注、Max サブスク枠内) - 完了したら Slack 通知 → 追加課金ゼロ
- コンテキストが疎結合、タイムアウトなし、失敗が伝播しない
これで、月額 10 万円台前半で、ハイエンドエージェント体験を維持しながら、コンプライアンス制約のある案件にも対応できる構成が完成する。
「強いモデルは欲しい、でも従量課金は払いたくない」というジレンマに対して、役割分担で解くというのは、ハックでも抜け道でもなく、ごく真っ当なエンジニアリング判断だと私は思っている。特に非同期パターンは、長期運用してみるとほんとうに効く。同期 delegate で運用していた頃に戻るのはもう考えられないくらいだ。
検証 PoC・運用設計のご相談
私は AI コンサル兼 AWS インフラエンジニアとして、こうした「コスト最適化されたエージェント基盤構築」案件を複数手がけている。Hermes / Claude Code / Dify / vLLM / MCP の組み合わせで、要件定義から PoC、本番展開まで一気通貫で対応可能。
「うちもエージェント運用のコストが青天井で困っている」という方、お気軽にお問い合わせください。
本記事は 2026 年 5 月時点の検証に基づきます。Claude Code、Hermes Agent、Dify ともに進化が早いため、最新仕様は各公式ドキュメントを必ず確認してください。--dangerously-skip-permissionsの使用は自己責任でお願いします。
