AIエージェントで作る銘柄評価基盤③—Hermes Agentでの定期実行


AIエージェントで作る銘柄評価基盤③—Hermes Agentでの定期実行

Hermes Agentによる定期起動・実行・記録・通知の概念図

AIエージェントを使った処理が一度成功しても、定期運用できるとは限りません。毎朝の実行では、パソコンの再起動、Gatewayの停止、休日、外部APIの更新遅れ、仮想環境の混線、通知先の障害、同一日の再実行などが起きます。モデルの回答品質だけでなく、「予定どおり起動したか」「どこまで進んだか」「安全にやり直せるか」が重要になります。

このプロジェクトでは、Hermes AgentのcronとGatewayを日次処理の起動基盤として使っています。現在の設定例は平日8:00 JSTの起動です。Hermes Agentが自由に銘柄を選んだり、毎朝処理手順を考えたりするのではありません。Hermesは決められたスクリプトを起動し、スクリプトがPythonの日次パイプラインを実行します。必要な箇所でだけ、評価バックエンドとしてAIを呼び出します。

この記事では、定期ジョブ、Gateway、`no-agent`モード、実行ラッパー、冪等性、ログ、通知、復旧の仕組みを説明します。実際のジョブID、実行ID、PID、通知先ID、ユーザー名、絶対パス、Webhook URLはすべて省略または一般化しています。

> 本記事は自動化システムの運用解説です。自動売買の仕組みではなく、情報収集と評価レポートを作るバッチです。売買判断や注文は自動化していません。

Hermes Agentが担当する範囲

Hermes Agentの役割は、次の3つです。

1. cron定義を保持し、指定時刻にジョブを起動する
2. Gatewayを通じてスケジューラーを継続稼働させる
3. 実行履歴、終了状態、標準出力を後から確認できるようにする

一方、次の処理はプロジェクト側が担当します。

  • 最新営業日の判定
  • データ取得と保存
  • 定量スコアの計算
  • 当日評価対象の選定
  • AI評価の呼び出しと検証
  • レポート生成
  • 通知の内容と失敗時通知
  • 同一営業日の二重実行防止
  • バックアップ

“`mermaid
flowchart LR
C[Hermes cron定義] –> G[Hermes Gateway]
G –> W[no-agent実行ラッパー]
W –> P[Python日次パイプライン]
P –> A[必要な対象だけAI評価]
P –> R[レポート・保存]
P –> N[通知]
W –> L[標準出力・実行履歴]
P –> L
“`

Hermes Agentは運用の外枠です。銘柄評価の業務ロジックはリポジトリに置き、テストとバージョン管理の対象にします。この分離により、ジョブ登録を変えずに評価ロジックを更新でき、評価ロジックを変更せずに起動時刻や配信先を変えられます。

なぜ`no-agent`ジョブにしたのか

Hermes Agentの定期ジョブは、エージェントへ自然言語の仕事を依頼する形と、スクリプトを直接実行する`no-agent`形に分けられます。この銘柄評価では`no-agent`を選びました。

理由は、毎朝の処理順がすでに決まっているからです。

  • 同じ入力なら同じ定量結果にしたい
  • 実行するモジュールと引数を固定したい
  • 自然言語の解釈違いで手順を飛ばしたくない
  • 不要な会話ターンや推論コストを増やしたくない
  • 終了コードと標準出力を監視へ使いたい

`no-agent`という名前でも、AIを一切使わないわけではありません。Hermesのジョブ起動部分ではエージェント推論を使わず、Pythonパイプラインの中で、対象を絞った事業評価にだけAIバックエンドを使います。

“`text
Hermesの起動判断 決定論的
処理順・対象枠 決定論的
定量計算 決定論的
公開資料の読解・採点案 AI
点数検証・保存 決定論的
“`

この構成は、AIを「スケジューラー」ではなく「専門処理の部品」として使う例です。

cron定義の内容

記事用に一般化したジョブ定義は次のようになります。

| 項目 | 設定例 | 意味 |
|—|—|—|
| 名前 | 銘柄評価daily | 実行一覧で識別する表示名 |
| スケジュール | `0 8 * * 1-5` | 月〜金の8:00 JST |
| モード | `no-agent` | スクリプトを直接実行 |
| スクリプト | `stock_eval_daily.sh` | Hermes配備用ラッパー |
| 配信 | local | 詳細通知はアプリ側で実施 |
| 繰り返し | 無期限 | 停止するまで継続 |

平日指定でも、祝日をcron式だけで完全に表現するのは現実的ではありません。祝日はジョブを起動し、プロジェクト側が「取得できる最新営業日」を解決します。その日が処理済みなら正常スキップにします。

つまり、カレンダーの正しさをHermesだけへ背負わせません。Hermesは定時に起動し、市場営業日の意味は市場データを知っているアプリケーションが判断します。

「登録済み」と「次回動く」は違う

定期運用では、少なくとも次の4状態を分けて確認します。

| 状態 | 確認したいこと | 主な確認方法 |
|—|—|—|
| Job defined | ジョブ定義が存在する | `hermes cron list` |
| Job active | 一時停止されていない | `hermes cron list` |
| Scheduler alive | Gatewayが動いている | `hermes cron status` |
| Last run succeeded | 直近実行が成功した | `hermes cron runs <job_id>` |

ジョブが`active`でも、Gatewayが止まっていれば次回は起動しません。直近実行が成功していても、その後にGatewayが停止した可能性があります。反対に、Gatewayが動いていても、ジョブが一時停止中なら実行されません。

2026年8月12日の確認時点では、Gatewayの稼働、対象ジョブのactive状態、当日朝の成功履歴を別々に確認しました。公開記事には実ジョブID、PID、実行IDを載せず、確認観点だけを残します。

実行ラッパーを一枚挟む

HermesのcronからPythonモジュールを直接起動せず、薄いシェルラッパーを一枚挟んでいます。一般化すると次の形です。

“`bash
#!/bin/bash
set -uo pipefail

PROJECT="/path/to/project"

notify_failure() {
# 秘密情報を表示せず、設定済みの通知経路へ短い失敗メッセージを送る
:
}

if ! cd "$PROJECT"; then
notify_failure "プロジェクトへ移動できません"
exit 1
fi

親プロセスのPython環境を子へ混ぜない unset PYTHONPATH PYTHONHOME

環境固有の安定化設定が必要ならここへ固定する export ARROW_DEFAULT_MEMORY_POOL=system

if /path/to/uv run — python -m stock_brand_eval_agent.daily; then
exit 0
fi

notify_failure "日次バッチが失敗しました。実行履歴を確認してください"
exit 1
“`

コード例には実際のプロジェクトパス、Hermesのホーム、通知URLを載せていません。

ラッパーの役割は、業務ロジックを書くことではありません。実行環境を整え、Pythonパイプラインの終了コードをHermesへ正しく返し、起動以前の失敗も通知できるようにすることです。

作業ディレクトリを明示する

定期ジョブの作業ディレクトリは、手動実行時のターミナルと同じとは限りません。Hermesの`no-agent`スクリプトはHermes側のスクリプト配置場所から起動されるため、相対パスのままではプロジェクトの`.env`、データベース、出力先が見つからない可能性があります。

そのためラッパーでプロジェクトディレクトリへ移動します。移動に失敗したら、そのままPythonを起動せず停止します。外付けストレージやネットワークボリュームを使う場合、未マウント状態を「Pythonのモジュールエラー」として後から悩むより、最初に明確な原因を出せます。

記事や通知では絶対パス全体を出さず、「プロジェクトフォルダへアクセスできない」と一般化します。詳細パスはローカルログだけに残す方が安全です。

親プロセスのPython環境を切り離す

実運用で起きた問題の一つが、Hermes Agent自身の仮想環境と、銘柄評価プロジェクトのPython環境の混線です。親プロセスが`PYTHONPATH`を子プロセスへ渡すと、別バージョンのPython向けにインストールされた数値計算ライブラリが先に読み込まれ、起動時に失敗することがあります。

対策として、プロジェクトを起動する直前に`PYTHONPATH`と`PYTHONHOME`を解除し、プロジェクト側のパッケージ管理環境だけで実行します。

この対応は、エージェント運用に特有というより「複数のPython環境を持つ常駐プロセスから別プロジェクトを起動する」時の一般的な注意点です。手動実行が成功しcronだけ失敗する場合、作業ディレクトリ、PATH、PYTHONPATH、HOME、権限、マウント状態の差を確認します。

環境固有の回避策をラッパーへ閉じ込める

データ処理ライブラリのメモリ割り当て実装が実行環境と相性を起こしたため、現行ラッパーではメモリプールを明示する環境変数も設定しています。この設定はライブラリの読み込み時にしか効かないため、Pythonが始まる前に置きます。

重要なのは、この回避策を評価式や日次処理へ散らさないことです。実行環境に依存する設定はラッパー、業務ロジックの設定は`config.py`、秘密情報は`.env`というように置き場所を分けます。

| 種類 | 置き場所 | 例 |
|—|—|—|
| 起動環境の調整 | シェルラッパー | 作業場所、親環境の解除、ライブラリ回避策 |
| 評価ロジック | 設定モジュール | 閾値、重み、Tier周期、機能フラグ |
| 秘密情報 | Git管理外の`.env` | APIキー、Webhook、通知先 |
| 日次状態 | SQLite・Parquet | 処理日、評価履歴、スコア、実行結果 |

同じ営業日を二重処理しない

cronは一度しか起動しないとは限りません。手動確認、Gateway再起動、障害復旧、祝日明け、タイムアウト後の再試行などで、同じ営業日を再び処理する可能性があります。

日次パイプラインは、対象営業日を解決した後、その日付のバッチ選定記録がすでに存在するか確認します。存在し、強制実行ではなければ処理をスキップします。

“`mermaid
flowchart TD
A[cron起動] –> B[取得可能な最新営業日を解決]
B –> C{その営業日は処理済みか}
C — はい –> D[正常スキップを記録・通知]
C — いいえ –> E[日次処理を開始]
E –> F[対象単位で保存]
F –> G[完了要約を出力]
“`

スキップ時も通知を一通送ります。毎朝の通知が来ない場合は、Gateway停止や起動失敗の可能性があります。スキップ通知が来た場合は、実行基盤は動いたが処理対象が新しくなかったと切り分けられます。

強制実行のフラグもありますが、本番データベースで安易に使いません。ローテーターが対象を再選定し、外部AI評価を再度呼び出す可能性があるためです。検証ではデータベースをコピーし、通知を無効にした環境で実行します。

一件ずつ保存して部分成功を残す

AI評価は、当日対象をすべて処理してから一括保存するのではなく、銘柄単位でコミットします。10件目で外部サービスが失敗しても、1〜9件目の成功を失いません。当日評価済みの対象は再実行時にスキップできます。

連続失敗が設定上限に達した場合は、その日のAI評価を打ち切ります。認証障害やサービス停止時に、全対象へ同じ失敗を繰り返して費用と時間を消費しないためです。

それでも定量計算、保存、レポートは継続できます。AI評価ができなかった対象は0点にせず、未評価または前回の有効評価として扱います。「AIが失敗したので日次全体を失敗」としない劣化運転です。

通知は開始・スキップ・完了・失敗を分ける

通知はランキングを届けるだけではなく、実行状態を観測するために使います。

| 通知 | 目的 | 含める内容 |
|—|—|—|
| 開始 | cron起動を確認 | 対象営業日、対象件数の概要 |
| スキップ | 正常な処理不要を確認 | 処理済み営業日であること |
| 完了 | 成果を確認 | レポート要約、件数、警告 |
| 検証 | 新機能の状態確認 | 充足率、整合性エラー数 |
| 失敗 | 復旧を開始 | 失敗段階、ログの確認先 |

通知モジュールは複数経路へファンアウトします。一方の通知が失敗しても、もう一方を試します。通知先が未設定なら、その経路だけを静かに省略します。

失敗通知で秘密情報や完全な例外スタックを送らないことも重要です。通知には短い要約とローカルの確認先だけを載せ、Webhook URL、チャンネルID、API応答本文は載せません。

標準出力を機械的な実行証跡にする

日次処理の最後に、人が読めて機械でも解析しやすい一行要約を標準出力へ出します。記事用の一般化例です。

“`text
OK YYYY-MM-DD: batch=N hard_pass=N events=N evals=N
ontology=N policy=STATE backup=RESULT report=FILE
“`

失敗時は非ゼロの終了コードを返し、ラッパーも失敗として終了します。Hermesの実行履歴から、実行時刻、成功・失敗、標準出力を確認できます。

レポートファイルだけを成功判定に使わない理由は、古いレポートが残っている可能性があるからです。終了コード、今回の標準出力、対象日のデータベース記録、通知を組み合わせて判断します。

Hermes側で確認するコマンド

定期運用では、次の読み取りコマンドを使います。

“`bash
# ジョブ定義、active状態、次回予定、直近結果
hermes cron list

Gatewayとスケジューラーの稼働状態 hermes cron status

対象ジョブの永続的な実行履歴 hermes cron runs <job_id> “`

公開スクリーンショットを作る場合は、ジョブID、実行ID、PID、作業ディレクトリ、配信先、他のジョブ名までマスクします。コマンド出力には、目的外の定期ジョブや個人用の通知先が一緒に表示されることがあります。

よくある障害と切り分け

| 症状 | 確認する順番 | 主な対処 |
|—|—|—|
| 予定時刻に何も来ない | Gateway→ジョブactive→次回予定 | Gateway再起動、ジョブ再開 |
| Hermesでは失敗、手動では成功 | cwd→PATH→Python環境→権限 | ラッパーで環境を固定 |
| 休日に前営業日を再処理しそう | 対象営業日→処理済み記録 | 正常スキップを確認 |
| API取得で停止 | 認証→提供日→回数制限→通信 | 原因別に停止・待機・再認証 |
| AI評価だけ進まない | バックエンド可用性→連続失敗 | AI層を打ち切り本流継続 |
| レポートはあるが通知がない | 通知経路ごとの設定と応答 | 片方ずつテスト、ローカル成果確認 |
| 同じ通知が二重に届く | 同一営業日ガード→外部送信履歴 | 冪等キー、処理済み判定 |
| 外付け保存先へ書けない | マウント→権限→空き容量 | 起動前チェック、ローカル退避 |

タイムアウトは特に注意が必要です。外部サービスへ書き込んだ直後に応答だけ失われると、再試行で二重送信が起こります。このプロジェクトの主要処理はローカル保存と通知ですが、外部書き込みを増やす場合は、再試行前に外部状態を照会します。

手動復旧の基本手順

1. `hermes cron status`でGatewayが生きているか確認する
2. `hermes cron list`で対象ジョブがactiveか確認する
3. 実行履歴で、起動前・Python起動時・処理途中のどこで失敗したか見る
4. 対象営業日のデータベース記録とレポート有無を確認する
5. 認証、通信、保存先、Python環境の原因を修正する
6. 本番DBをバックアップする
7. 必要ならコピー環境で再現し、通知を止めて検証する
8. 未処理なら通常実行、処理済みなら重複影響を確認してから強制実行する
9. 完了要約、レポート、通知を照合する

「失敗したからとりあえず再実行」ではなく、どこまでコミット済みかを確認します。AI評価は銘柄単位、日次処理は営業日単位、通知は経路単位で状態が分かれます。

秘密情報と権限の扱い

HermesのスクリプトへAPIキーやWebhook URLを直接書きません。プロジェクトのGit管理外の環境ファイルから読み込みます。ラッパーは必要な時だけ設定を参照し、値を標準出力へ出しません。

運用時に確認する項目は次の通りです。

  • プロジェクトの書き込み範囲を限定する
  • APIキーをコード、記事、Git、会話へ貼らない
  • 通知先IDを公開ログへ出さない
  • AIへ渡す評価パッケージに秘密情報を含めない
  • 外部検索は公開資料に限定する
  • 削除や売買注文のToolをジョブへ与えない
  • バックアップ先のアクセス権と保持期間を確認する
  • 公開前に絶対パス、実行ID、周辺ジョブ名を検索する

このバッチは情報評価までを対象にし、注文APIや証券口座の操作権限を持たせていません。AIエージェントの権限を目的より広くしないことが基本です。

毎朝の運用チェックリスト

  • [ ] Gatewayが起動している
  • [ ] 対象ジョブがactiveである
  • [ ] 次回予定が期待する時刻とタイムゾーンになっている
  • [ ] 直近実行が成功、または理由の分かる正常スキップである
  • [ ] 開始通知と完了・スキップ通知が対応している
  • [ ] 対象営業日が期待どおりである
  • [ ] 取得件数、フィルター通過件数、評価件数が急変していない
  • [ ] AI連続失敗、認証エラー、保存エラーがない
  • [ ] レポートとバックアップが当日分へ更新されている
  • [ ] 実行履歴に秘密情報が出ていない

自動化の設計で得た教訓

第一に、AIエージェントの定期運用では、エージェントに自然言語で毎回考えさせることが必須ではありません。手順が固定された仕事は`no-agent`で起動し、AIが必要な一部分だけをアプリケーション内部から呼ぶ方が、再現性と監視性を高められます。

第二に、ジョブ定義、active状態、Gateway、直近成功を別々に見る必要があります。「登録した」「昨日成功した」だけでは次回の保証になりません。

第三に、手動環境とcron環境は違います。作業ディレクトリ、Pythonパス、仮想環境、マウント、権限をラッパーで明示すると、環境差による障害を減らせます。

第四に、正常スキップを可視化すると監視が楽になります。休日や処理済み日を無音にすると、Gateway停止と区別できません。

まとめ

このプロジェクトでは、Hermes Agentを日次パイプラインの運用基盤として使っています。cronが平日朝に`no-agent`スクリプトを起動し、Gatewayがスケジューラーを支えます。ラッパーが作業場所とPython環境を固定し、Python側が営業日、二重実行、対象単位の保存、通知、レポートを管理します。

AIはパイプライン全体を自由に動かすのではなく、選定済み対象の公開情報を読む部品です。Hermesはその部品を含む決定論的な処理を、毎朝同じ入口から起動し、成功・失敗・スキップの証跡を残します。

次回は、AI評価を本番へ組み込んだ後に、単体テスト、BDD、E2E、検証運転、shadow運転、事後リターンでどのように品質を確認し続けるかを説明します。

実装確認に使った主なファイルと状態

  • `scripts/hermes/stock_brand_eval_daily.sh`
  • `stock_brand_eval_agent/daily.py`
  • `stock_brand_eval_agent/evaluator/agent.py`
  • `stock_brand_eval_agent/reporting/notify.py`
  • `stock_brand_eval_agent/reporting/daily_report.py`
  • `tests/features/idempotency.feature`
  • `tests/features/notification.feature`
  • Hermes Agentの`cron list`、`cron status`、`cron runs`(2026-08-12確認、識別情報は非掲載)

Back to top