cronで毎日LLMにタスクを投げて自動化する(日次サマリ生成)

ログの要約やアクセス統計へのコメント生成のような「毎日同じパターンでやる定型作業」は、cronとLLM APIを組み合わせて自動化してしまうと地味に楽になる。gitのコミット履歴の要約やニュースまとめにも応用できる発想だが、ここでは前日のnginxアクセスログを集計してLLMに日本語の短いサマリを作らせ、Discordに通知するところまでを実例で組んでみる。

全体の流れ

やることは単純で以下の4ステップ。

  • 集計コマンドで前日分のアクセス数・ステータスコード内訳・上位アクセスパスを数値化する
  • その数値をJSONに組み立ててLLM APIに渡す
  • 返ってきたテキストを抽出する
  • ファイルに保存しつつ、Discord(またはSlack)のWebhookに投稿する

データ集計とAPI呼び出し(bash)

スクリプト全体は次のような形になる。nginxのログパスや通知先は環境に合わせて書き換える。

#!/bin/bash
set -eu

# cron環境はPATHが最小限なので絶対パス指定 & 環境変数を明示的に読み込む
source /home/[ユーザー名]/.config/daily_summary.env

LOG=/var/log/nginx/access.log.1
TOTAL=$(wc -l < "$LOG")
ERR5XX=$(awk '$9 ~ /^5/' "$LOG" | wc -l)
ERR4XX=$(awk '$9 ~ /^4/' "$LOG" | wc -l)
TOP_PATH=$(awk '{print $7}' "$LOG" | sort | uniq -c | sort -rn | head -3)

PROMPT="以下は昨日のアクセスログ集計です。日本語で3行以内の簡潔なサマリを書いてください。
総アクセス数: ${TOTAL}
5xxエラー数: ${ERR5XX}
4xxエラー数: ${ERR4XX}
アクセス上位パス:
${TOP_PATH}"

REQ_BODY=$(jq -n --arg prompt "$PROMPT" '{
  model: "claude-haiku-4-5",
  max_tokens: 300,
  messages: [{role: "user", content: $prompt}]
}')

RESPONSE=$(curl -sS https://api.anthropic.com/v1/messages \
  -H "x-api-key: ${ANTHROPIC_API_KEY}" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d "$REQ_BODY")

SUMMARY=$(echo "$RESPONSE" | jq -r '.content[0].text // empty')

if [ -z "$SUMMARY" ]; then
  curl -sS -X POST -H 'Content-Type: application/json' \
    -d '{"content": "daily_summaryでLLM呼び出しに失敗しました"}' \
    "$DISCORD_WEBHOOK_URL"
  exit 1
fi

echo "$(date '+%Y-%m-%d %H:%M') ${SUMMARY}" >> /home/[ユーザー名]/logs/daily_summary_history.log

curl -sS -X POST -H 'Content-Type: application/json' \
  -d "$(jq -n --arg c "$SUMMARY" '{content: $c}')" \
  "$DISCORD_WEBHOOK_URL"

daily_summary.envにはANTHROPIC_API_KEYDISCORD_WEBHOOK_URLを書いておき、権限はchmod 600にしておく。APIキーをスクリプト本体に直書きしないのは基本中の基本だが、cronから叩くスクリプトほど忘れがちなので改めて意識したい。使うモデル名は執筆時点のものなので、実際に使う際はAPI提供元のドキュメントで現行のモデルIDを確認してから書き換えること。

アクセスログ以外にも、この仕組みはそのまま流用できる。前日のgitコミット履歴をgit log --since=yesterday --onelineで拾って要約させたり、監視ツールのアラート件数を集計して「昨日は静かだった」「〇件のアラートが出ている」とコメントさせたり、素材となる数値やテキストを差し替えるだけで似たようなサマリを量産できる。

cronへの登録

毎朝7時に実行するならcrontab -eでこう書く。

0 7 * * * /home/[ユーザー名]/scripts/daily_summary.sh >> /var/log/daily_summary.log 2>&1

cronのPATHは対話シェルと違って最小限しかセットされないので、スクリプト内でjqcurlをフルパスで呼ぶか、PATHを明示的に設定しておくと事故が減る。「ターミナルで手動実行すると動くのにcronだと動かない」という定番のハマりどころは、大抵このPATHか環境変数の読み込み漏れが原因なので、which jqで確認したフルパスを使うのが確実だ。

Slack Webhookの場合

Discordではなく Slack に投げたい場合はWebhook URLの形式とJSONのキーが違うだけで、投稿自体は似たようなものになる。

curl -sS -X POST -H 'Content-Type: application/json' \
  -d "$(jq -n --arg t "$SUMMARY" '{text: $t}')" \
  "$SLACK_WEBHOOK_URL"

エラー処理とコスト

API呼び出しは失敗することもあるので、上のスクリプトのようにSUMMARYが空ならエラー通知を投げてexit 1するようにしておく。何度かリトライしたい場合はcurl呼び出し部分をループで囲み、失敗したら数秒待って再試行、それでもダメなら諦めて通知、という形にすればいい。通知経路さえ生きていれば「今日は動かなかった」と気づけるので、無言で失敗し続ける事態は避けられる。

ついでにログファイル(daily_summary_history.log)の肥大化にも注意しておきたい。毎日1行ずつとはいえ、放置すれば数年単位でそれなりのサイズになる。logrotateの対象に加えておくか、定期的に古い行を切り詰めるくらいはやっておいて損はない。

コスト面は現実的な話をすると、1日1回・数百トークン程度のやり取りなら月額でも大した金額にはならないことが多い(モデルやトークン数、契約プランによる)。毎日大量に叩くバッチ処理とは違って、こういう「1日1回のちょっとした要約」用途では気にするほどのコストにはならないというのが実際にやってみた感覚だ。

まとめ

cronとLLM APIを組み合わせると、ログ要約に限らずgitのコミット履歴の要約やニュースのまとめなど、「毎日同じことを人間が読んで判断する」作業をかなり機械に任せられる。ポイントはcron環境のPATH・環境変数の制約を意識することと、API失敗時に気づける通知経路を必ず用意しておくこと。この2点さえ押さえておけば、小さな自動化として十分実用に耐える。

読んで頂いて有り難うございます!