Hermes Agent で bot ごとに推論エンジンを設定する — Ollama × OpenRouter 混在運用
はじめに
Mac Studio 1 台で複数の AI bot を動かしていると、すぐにこういう要求にぶつかる。
- 軽い案件はローカル LLM(Ollama)で回してコストゼロにしたい
- 重い案件・本番運用だけクラウド(OpenRouter)に投げたい
- でも会話履歴・記憶・人格・触っていいファイルは bot ごとに完全に分けたい
Hermes Agent はこれを profile という単位で素直に解ける。本記事では、bot A をローカル LLM、bot B を OpenRouter にルーティングする構成を、実際の現場設定の手順としてまとめる。
結論を先に言うと、profile を「LLM provider + bot identity + memory + skill + 制約」のひとまとまりとして扱う設計が、複数 bot 運用の正解だった。bot を増やすことと profile を増やすことを同義にする、という考え方だ。
1. 結論先出し
Hermes Agent では profile という単位で bot を独立稼働でき、それぞれ別の LLM プロバイダにルーティングできる。~/.hermes/<profile>/config.yaml 内の model.provider を書き換えるだけで切り替わる。
最小構成:
-
bot A(default profile)→ Ollama
gemma4:26b-mxfp8(local) -
bot B(qi_bidding profile)→ OpenRouter
openai/gpt-5.4-mini
注: モデル名(
gemma4:26b-mxfp8/openai/gpt-5.4-mini)は環境ごとに読み替えてほしい。ollama listや OpenRouter のモデル一覧で実在するタグに置き換えること。
2. ファイル配置の全体図
profile の独立性を理解する一番の近道は、ディレクトリ構造を眺めることだ。
~/.hermes/
├── config.yaml ← bot A (default profile) のメイン config
├── .env ← bot A の secrets
├── auth.json ← 全 profile 共有の OAuth credential 池
├── webhook_subscriptions.json ← 全 profile 共有 (webhook adapter は default のみ)
├── hermes-agent/ ← Python package 本体 (update で書き換わる)
├── kanban.db (+ boards/*) ← 全 profile 共有 SQLite
├── sessions/ ← bot A の会話履歴
├── memories/ ← bot A の MEMORY.md / USER.md
├── workspace/ ← bot A の作業領域
├── logs/ ← bot A のログ
├── skills/ ← bot A の skill 群
├── profiles/
│ └── qi_bidding/ ← bot B のサンドボックス (default と独立)
│ ├── config.yaml ← bot B のメイン config
│ ├── .env ← bot B の secrets
│ ├── sessions/
│ ├── memories/
│ ├── workspace/
│ ├── logs/
│ ├── skills/
│ └── SOUL.md ← bot B 専用の人格 / 制約 prompt
└── _pre_update_backup-<ts>/ ← 更新前バックアップ (運用上の保険)
~/Library/LaunchAgents/
├── ai.hermes.gateway.plist ← bot A の launchd entry
└── ai.hermes.gateway-qi_bidding.plist ← bot B の launchd entry (別 service)
ポイントは、sessions/ memories/ SOUL.md は profile ごとに独立している一方、auth.json kanban.db は全 profile 共有になっていること。この「何が混ざって何が分かれるか」の線引きが運用設計の肝になる(詳細は §8)。
3. profile を新規作成する
# bot B 用 profile 作成
# (内部で <profile>/ ディレクトリ + デフォルト config を雛形展開)
hermes profile create qi_bidding
# wrapper script が ~/.local/bin/qi_bidding として作られる
# 以降 `hermes -p qi_bidding ...` または `qi_bidding ...` で呼べる
これだけで ~/.hermes/profiles/qi_bidding/ 以下に config の雛形が展開される。
4. config.yaml の model 部だけ書き換える
bot A — Ollama (local LLM) にルーティング
~/.hermes/config.yaml の冒頭:
model:
default: gemma4:26b-mxfp8 # ollama list で出てくる tag そのまま
provider: ollama
base_url: http://127.0.0.1:11434/v1 # Ollama の OpenAI 互換 endpoint
providers:
ollama:
api_key: ollama # 任意の文字列で OK (Ollama は検証しない)
base_url: http://127.0.0.1:11434/v1
事前準備:
brew install --cask ollama
open -a Ollama # daemon 起動
ollama pull gemma4:26b-mxfp8 # モデル取得 (~26GB)
ローカル LLM 並列の罠回避(重要)
launchctl setenv OLLAMA_NUM_PARALLEL 1
launchctl setenv OLLAMA_MAX_LOADED_MODELS 1
launchctl setenv OLLAMA_MAX_QUEUE 10
# Ollama.app を再起動して反映
並列推論時の KV cache 競合で 30B 級モデルが詰まる挙動への対処。OLLAMA_NUM_PARALLEL=1 で推論を順次化する。launchd plist で永続化しておくのが安全(~/Library/LaunchAgents/ai.ollama.envsetter.plist)。
この「ローカル LLM の擬似並列を直列化する」パターンは、別途 Hermes Agent + Ollama の並列処理アーキテクチャの記事で深掘りしているので、そちらも参照してほしい。本記事では「並列の罠があるので潰しておく」程度に留める。
bot B — OpenRouter にルーティング
~/.hermes/profiles/qi_bidding/config.yaml の冒頭:
model:
default: openai/gpt-5.4-mini # OpenRouter のモデル ID 形式
provider: openrouter
base_url: https://openrouter.ai/api/v1
providers:
openrouter:
api_key: ${OPENROUTER_API_KEY} # .env から展開される
base_url: https://openrouter.ai/api/v1
~/.hermes/profiles/qi_bidding/.env に:
OPENROUTER_API_KEY=sk-or-v1-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
default の .env にも同名キーを置けば共有できるが、profile ごとに別契約・別 key にする方が安全。コスト管理も切り分けやすくなる。
5. Discord bot token を profile ごとに分ける
Discord は 1 bot token = 1 WebSocket session なので、bot 2 つ = bot token 2 つ必要。Discord Developer Portal で 2 つの Application を作り、それぞれの token を割り当てる。
# ~/.hermes/.env (bot A 用)
DISCORD_BOT_TOKEN=MT...bot-A-token...
DISCORD_HOME_CHANNEL=<CHANNELID_1>
DISCORD_ALLOW_ALL_USERS=true
# ~/.hermes/profiles/qi_bidding/.env (bot B 用)
DISCORD_BOT_TOKEN=MT...bot-B-token...
DISCORD_HOME_CHANNEL=<CHANNELID_2>
DISCORD_ALLOWED_USERS=<USERID_1>
注意点:
-
Privileged Gateway Intents で Message Content Intent を ON にしないと、bot 起動時に
PrivilegedIntentsRequiredで死ぬ。 -
DISCORD_ALLOWED_USERS=<username>のように ユーザ名で書くと Hermes が name resolution のため Server Members Intent を要求し、admin 承認待ちで詰む。数値 User ID(Discord 開発者モードでコピー)を入れること。
6. 各 bot を launchd で常駐化
~/Library/LaunchAgents/ai.hermes.gateway.plist(bot A 用、雛形):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>ai.hermes.gateway</string>
<key>ProgramArguments</key>
<array>
<string>/Users/llm/.hermes/hermes-agent/venv/bin/python</string>
<string>-m</string><string>hermes_cli.main</string>
<string>gateway</string><string>run</string>
<string>--replace</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>HERMES_HOME</key><string>/Users/llm/.hermes</string>
</dict>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
<key>StandardOutPath</key><string>/Users/llm/.hermes/logs/gateway.log</string>
<key>StandardErrorPath</key><string>/Users/llm/.hermes/logs/gateway.error.log</string>
<key>SoftResourceLimits</key>
<dict><key>NumberOfFiles</key><integer>8192</integer></dict>
<key>HardResourceLimits</key>
<dict><key>NumberOfFiles</key><integer>16384</integer></dict>
</dict>
</plist>
bot B 用は --profile qi_bidding を ProgramArguments に挟むだけ:
<array>
<string>/Users/llm/.hermes/hermes-agent/venv/bin/python</string>
<string>-m</string><string>hermes_cli.main</string>
<string>--profile</string><string>qi_bidding</string> <!-- ここだけ追加 -->
<string>gateway</string><string>run</string>
<string>--replace</string>
</array>
Label は別名(ai.hermes.gateway-qi_bidding)にすること。
FD 上限は両 plist とも 8192 に上げるのが鉄則。 macOS LaunchAgent default の 256 のままだと、長時間稼働で file descriptor が枯渇し、bot がサイレント切断される。
起動:
launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.hermes.gateway.plist
launchctl bootstrap gui/501 ~/Library/LaunchAgents/ai.hermes.gateway-qi_bidding.plist
7. 動作確認
# 両 gateway が独立プロセスで動いていることを確認
ps -ax | grep "hermes_cli.main" | grep -v grep
# 出力イメージ:
# PID args
# AAAAA hermes_cli.main gateway run --replace ← bot A (default)
# BBBBB hermes_cli.main --profile qi_bidding gateway run ← bot B
それぞれの model 設定確認:
hermes status | grep -iE "Model|Provider"
hermes -p qi_bidding status | grep -iE "Model|Provider"
# 出力例:
# [bot A] Model: gemma4:26b-mxfp8 / Provider: Custom endpoint (Ollama)
# [bot B] Model: openai/gpt-5.4-mini / Provider: OpenRouter
smoke test:
hermes -z "Say pong" --yolo # bot A の Ollama 経由
hermes -p qi_bidding -z "Say pong" --yolo # bot B の OpenRouter 経由
両 bot が独立して別エンジンで応答すれば完成。
8. 共有リソースと分離リソース(運用注意)
profile 設計でいちばん事故りやすいのが、「何が共有で何が分離か」の取り違えだ。
| リソース | 範囲 | コメント |
|---|---|---|
~/.hermes/auth.json(OAuth pool) |
全 profile 共有 | API key も OAuth も 1 個の pool。profile-specific にしたいなら別アカウントで key 発行 |
~/.hermes/kanban.db + boards/*
|
全 profile 共有 | bot 間でタスク受け渡しが自然にできる設計。完全分離したいなら HERMES_KANBAN_DB env で別パスへ |
sessions/ / memories/ / SOUL.md
|
profile ごとに独立 | 会話履歴・記憶・人格は混じらない |
| Discord/Teams credentials | profile ごとに独立 | 別 bot token 必要 |
| LLM provider | profile ごとに独立 | この記事の主題 |
| webhook adapter(:8644) | port 競合するので片方だけ enable | default に置き、qi_bidding は platforms.webhook.enabled: false で抑制 |
| Teams adapter(:3978) | 同じく片方 | qi_bidding profile に置く運用例 |
| Cloudflare tunnel(named) | 全 profile が同じ tunnel を共有 | hostname 別 ingress で振り分け |
9. SOUL.md で bot 個性 / 制約を分離
profile の独立性がいちばん効くのが、bot ごとの「触っていい領域」の制約だ。~/.hermes/profiles/qi_bidding/SOUL.md に bot ごとのシステムプロンプトと境界を書く。
You are Hermes_For_QI_Bidding, the QI Bidding agent.
You assist bid/proposal preparation.
## Workspace boundaries (STRICT)
- Allowed Obsidian vault: /Users/llm/Documents/obsidian/bidding/
- Forbidden: /Users/llm/Documents/obsidian/yoshiya/ (個人 vault、別 profile 担当)
## Tool conduct
- Use `read_file` / `write_file` over shell `cat` / `find`.
- Never commit code without explicit user instruction.
これで bot B は bidding vault しか触らない。bot A は default の SOUL.md で別境界(または制限なし)にする。業務 bot から個人 vault を物理的に守れるのは、profile を分けた副次効果として大きい。
10. ハマりどころ(失敗事例)
| 症状 | 原因 | 対処 |
|---|---|---|
| Bot 1 つだけ Discord 反応しない |
DISCORD_BOT_TOKEN を間違って同じ値にして 2 つ目が kick される |
bot 単位に別 token |
PrivilegedIntentsRequired で起動失敗 |
Developer Portal で Message Content Intent OFF | Portal で ON |
| Server Members Intent privileged… |
DISCORD_ALLOWED_USERS にユーザ名(非数値) |
数値 User ID に置換 |
| ローカル LLM が 11 分も応答しない |
OLLAMA_NUM_PARALLEL > 1 で KV cache 競合 |
NUM_PARALLEL=1 で順次化 |
| 30 時間稼働後 bot サイレント切断 | macOS LaunchAgent FD 上限 256 枯渇 | plist の SoftResourceLimits=8192
|
| OpenRouter で 504 タイムアウト |
provider_routing.only で限定したら全滅 |
provider_routing: {} で OpenRouter デフォルトに |
| 片方の bot だけ webhook 立ち上がらない | 両 profile で :8644 取り合い | qi_bidding 側で platforms.webhook.enabled: false
|
| update 後 DB schema mismatch | kanban DB に新カラム未追加 | 手動 ALTER TABLE tasks ADD COLUMN session_id TEXT
|
11. on-the-fly でモデル切替する
Discord 内で /model <name> slash command を打てば、session 単位で切り替わる(gateway restart 不要)。
/model openrouter/anthropic/claude-sonnet-4
/model ollama/qwen3.6:35b-a3b-coding-mxfp8
session のスコープなので他 thread / 他 bot には影響しない。「重い案件だけ強いモデルに切替」「軽い雑談は Ollama で」という運用ができる。
12. fallback_model で provider 不調時の自動 failover
~/.hermes/config.yaml に追記:
fallback_model:
provider: openrouter
model: google/gemma-4-31b-it
primary が rate limit(429)/ overload(529)/ 5xx / 接続失敗で落ちた時、自動で fallback に切り替わる。クラウド側の動的枠超過にも有効。
13. 記事の核(主張)
profile =「LLM provider + bot identity + memory + skill + 制約」のまとまった単位として扱う設計が、複数 bot 運用の正解。bot を増やすことと profile を増やすことを同義にする。
「bot ごとに違う engine」という機能を実現するために、Hermes は config を profile スコープに切る。その結果として会話履歴も人格も制約も独立になる。これが副次効果として、
- 個人 vault を業務 bot から守る
- 片方の provider 不調時にもう片方は無傷
- 片方を Ollama 検証用、もう片方を本番運用
といった運用に効いてくる。
関連記事
本記事では profile 単位で LLM provider を分ける設計を扱ったが、bot を Discord ではなく Microsoft Teams で動かす場合、「プライベートチャンネルで bot が反応しない」という Teams 固有の落とし穴がある。これは Hermes の問題ではなく Teams の設計仕様で、profile 設計とは別レイヤーの話になる。
詳細は別記事「Microsoft Teams のプライベートチャンネルで bot が反応しない問題 — 設計仕様と回避策」にまとめた。Teams adapter の chat_id 形式制約(semicolon 不可)もそちらで扱っている。
タグ
HermesAgent Ollama OpenRouter Discord LLM AIエージェント macOS LaunchAgent