- 事件: AIエージェントに「前回の定期集計バッチ処理(日課の運用タスク)はいつ?」と尋ねたら、今朝も昨夜も正常実行していたのに「1ヶ月前の10月7日です!」と自信満々に誤答するトラブルが発生しました。
- 原因: LLMの能力不足ではなく、①更新停止済みの古いファイル(
maintenance_current.log)が残っていたことと、②スクリプト側が最新の足跡をファイルに残していなかった「書き込み側の設計欠如」による複合要因でした。 - 情報空間の罠: プロンプトに現在時刻が渡されていても、古い情報と現在情報が競合すると、LLMの探索や判断は容易に不安定になります(Context Rotにも通じる現象)。
- 大手の設計思想: Google SRE、Kubernetes、AWSなどの大規模システムに共通する「State(現在の状態)とLog(過去の履歴)の分離」思想を個人開発に適用。「即答用の看板JSON(現在状態のスナップショット)」と「直近ログのローテーション」を自動化することで誤認リスクを根本解決しました。
1. 現場で起きた「そんな前のわけないじゃん」事件
「前回の定期集計タスク(毎日のデータ収集・インサイト集計バッチ)っていつでしたっけ?」
日々の開発と運用の合間、何気なく相棒のAIアシスタントにそう尋ねた時のことです。AIから返ってきたのは、あまりにも堂々とした、しかし耳を疑う回答でした。
AI: 「前回実行された定期バッチ処理の実績は、2026年10月7日 22:35(約1ヶ月前)です」
思わず画面に向かって「え?そんな前のわけないじゃん」と声が出ました。
それもそのはずです。今朝の早朝05:00にも、昨夜の22:00にも、私はAIと一緒にその集計スクリプトを実行し、正常終了の報告を受け取っていたからです。つい数時間前にも目の前で動かしていた処理を、AIは平然と「1ヶ月前」と言い放ったのです。
「AIのハルシネーション(幻覚)だろうか?」
「最近のアップデートで記憶がおかしくなったのか?」
一瞬そう思いましたが、システム内部を冷徹に調査していくと、問題はAIの知能ではなく、私たちが組んでいたシステム環境側の「致命的な構造の死角」にありました。
2. なぜAIは1ヶ月前の過去ログを信じ切ったのか?
調査の結果判明した真相は、人間のオフィスに例えるなら非常にシンプルなものでした。
【人間の感覚】
「さっき一緒にやったんだから、今朝の話に決まっている」
(時間の流れの中で生きている)
【AIの視界】
「フォルダの中に『maintenance_current.log(現在のログ)』というファイルがあるぞ!
ここに10月7日と書いてあるから、これが前回の記録に違いない!」
(静止したファイル空間を検索している)
死角①:「current」という名前が、検索時の判断を誤らせる余地を作っていた
AIエージェントには、システムプロンプトとして「現在時刻(例: 2026-11-09T21:30:36)」が常に渡されています。論理的には、ログの日付と現在時刻を照合すれば「1ヶ月前だからおかしい」と推論できる可能性はあります。
しかし、今回の環境には、かつてデバッグ用に作成した logs/maintenance_current.log という固定名ファイルが残っていました。
AIがどのファイルを参照したかを追っていくと、実際にこのファイルの内容が回答の根拠として使われていました。LLM内部で「ファイル名を優先した」とまでは断定できませんが、少なくとも「current」という現在を意味する名前の古いファイルを残しておくこと自体が、AIによる情報探索を誤らせる余地を作っていたのは確かです。
AIに読ませる情報空間では、古い情報を現在形の名前で残さないという設計原則の重要性を痛感しました。
死角②:最新の足跡を残さなかった「書き込み側の設計欠如」
問題はファイル名だけではありませんでした。より本質的な原因は、スクリプト側の書き込み設計にありました。
直近のスクリプト改修により、集計結果のログは画面(標準出力)へ直接流す仕様に変更されていました。そのため、今朝や昨夜のバッチがどれほど完璧に成功していても、ファイルシステム上には「最新の実行ステータス」が1文字も記録されていなかったのです。
ファイルシステム上には「10月7日の古いログ」しか存在しない。その状態でAIに「前回の実行は?」と聞けば、AIが手元にある唯一の記録(10月7日)を最新と誤認するのは当然の帰結でした。AIが間違えたのではなく、AIが正解を引ける状態台帳を用意していなかった私たちのシステム側の欠如だったのです。
近年、コンテキスト内の情報競合や位置によってLLMの性能が低下する「Context Rot(文脈の腐敗)」が学術的にも議論されていますが、今回の事件も、「古い情報と現在情報が競合すると、LLMの判断が不安定になる」という問題を、極端に小さな環境で体験した出来事でした。
3. 大規模システムに共通する「現在の状態」と「過去の履歴」の区別
では、世間一般のシステム開発や大手クラウドは、この問題にどう向き合っているのでしょうか?
Google、AWS、Kubernetesなどの大規模システムでも、「現在の状態」と「過去に発生したイベント」を区別して扱う必要性は常に存在します。膨大なログが存在する中で、「システムは今どうなっているか?」を調べるために過去ログを全文検索していたら、莫大なコストがかかり、古いログに引きずられて誤判定を起こす恐れがあるからです。
現在の状態と過去のイベントを分けて扱う設計は、特定の製品だけの特殊な方法ではありません。
たとえば Google SRE(Site Reliability Engineering) では、状態把握やアラートには軽量なメトリクスを使い、詳細な原因調査にログを使うという役割分担が推奨されています。Kubernetes でも、Podの現在状態を表す status と、コンテナの過去ログを取得する仕組み(/log)は別々に分離されています。AWS CloudWatch においても、メトリクス・ログ・トレースをそれぞれ異なる観測データ(Observabilityシグナル)として扱う設計になっています。
| 比較項目 | 第1層:State(現在の状態・看板) | 第2層:Log(過去の履歴・詳細ログ) |
|---|---|---|
| 答える問い | 「今どうなってる?」(即答用) | 「なぜ失敗した?」(調査用) |
| データの性質 | 単一の最新スナップショット(上書き型) | 時系列の生テキスト(追記型) |
| サイズ | 数KB(超軽量) | 数MB〜数GB(肥大化しやすい) |
| 生存期間(TTL) | 常に最新1件のみ保持(過去は上書き) | 一定期間保持したのちアーカイブ隔離 |
| 運用の扱い | 日常の状況確認はこれ1つだけを見る | 障害調査の時だけ明示的に開く |
「状態を知りたい」ときに過去ログを読ませてはいけない。「調査したい」ときに状態だけ見ても原因がわからない。
この2つの異なる役割を、多段(2層)に切り離して運用することこそが、大規模システムに共通する堅牢な知恵でした。
4. 036 Factoryに実装した「2層防御パイプライン」の実装例
この知見に基づき、私たちのシステム(run_all_maintenance.py)にも即座に「2層防御」を組み込みました。
大手の巨大な仕組みをそのまま真似るのではなく、「大規模システムに共通する情報分離の考え方を、個人開発の小さな環境に縮小して適用する」というアプローチです。
flowchart TD
Run["集計バッチ実行 (run_all_maintenance.py)"]
Patrol["データ収集&集計処理"]
Success{"全工程が正常完了?"}
subgraph Layer1 ["【第1層】State(即答用の看板JSON)"]
State["last_system_state.json を完全上書き<br>(成功確定時刻・対象ジョブ・成否のみ)"]
end
subgraph Layer2 ["【第2層】Log(詳細調査ログ&隔離)"]
LogNew["日付付き詳細ログの保存"]
ArchiveOld["古い過去ログを archives/ へ退避"]
end
Run --> Patrol --> Success
Success -->|Yes(成功確定)| State
Success -->|記録保存| Layer2
【重要規律】Stateは「成功確定後」に刻む
今回の反省から生まれた最も重要な設計規律は、「State(看板)は、処理の開始時ではなく、全工程の成功が確定した瞬間に記録する」ということです。途中で落ちた場合や未確定な状態でStateを更新してしまうと、またAIに偽りの最新状態を信じ込ませることになるからです。
実装コード:最新看板(State)の完全自動上書き
スクリプトが正常終了した瞬間に、AIが現在状態を参照するための正規化されたスナップショット(last_system_state.json)を自動上書きするフックを配置しました。
万が一の読み込みエラー時にも安全にフォールバックできるよう、例外処理を組み込んでいます。
def update_state_file(job_type):
state_file = PROJECT_ROOT / "logs" / "last_system_state.json"
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 既存の状態を読み込み、常に最新のスナップショットを維持
state_data = {
"last_updated": now_str,
"analytics": {"last_run": None, "status": "pending"},
"backup": {"last_run": None, "status": "pending"}
}
if state_file.exists():
try:
with open(state_file, "r", encoding="utf-8") as f:
state_data = json.load(f)
except Exception as e:
# 破損時は警告を出してデフォルト構造から再構築
print(f"⚠️ 既存State読み込み失敗(初期化して継続): {e}")
# 全工程が成功した後にのみ、最新日時・ステータスを更新
if job_type in ["analytics", "all"]:
state_data["analytics"]["last_run"] = now_str
state_data["analytics"]["status"] = "success"
if job_type in ["backup", "all"]:
state_data["backup"]["last_run"] = now_str
state_data["backup"]["status"] = "success"
# 完全上書き保存(余計な過去履歴を溜め込まず、常に1ファイル)
try:
state_file.parent.mkdir(parents=True, exist_ok=True)
with open(state_file, "w", encoding="utf-8") as f:
json.dump(state_data, f, ensure_ascii=False, indent=2)
print(f"📋 現在状態スナップショット(State)を更新しました: {state_file}")
except Exception as e:
print(f"⚠️ ステータス書き込みエラー: {e}")
生成される看板JSONファイルは、以下の通り極めてシンプルです。
{
"last_updated": "2026-11-09 21:50:00",
"analytics": {
"last_run": "2026-11-09 05:00:00",
"status": "success"
},
"backup": {
"last_run": "2026-11-08 22:00:00",
"status": "success"
}
}
これにより、AIに「前回いつバッチを動かした?」と聞けば、AIはこの軽量なJSONファイルを1枚読むだけで、過去ログの不要な探索を行わずに現在状態を正確に把握できるようになりました。
古いログは「捨てず」に倉庫へ退避(隔離)する
そして、過去の試行錯誤ログや1ヶ月前の生ログは、削除して捨てるのではなく archives/logs_legacy/(倉庫) へ物理移動させました。
036 Factoryの運用サイクル(週次での振り返りやシステム点検)に合わせ、
- 日常の作業フォルダ(引き出し): 直近7日分のログと最新看板JSONのみを配置。
- アーカイブフォルダ(倉庫): 過去の全ログを安全に保存。過去のトラブルシューティングを調べたい時だけ明示的に参照する。
というルールを定めました。
5. まとめ:AIを賢くするとは「机の上を片付けること」である
今回のトラブルを通じて痛感したのは、「AIエージェントの信頼性は、プロンプトの工夫だけでなく、AIが参照・操作する情報環境の設計にも大きく左右される」という冷徹な事実でした。
- AIは静止したファイル空間を探索する: 時間感覚を前提とせず、古い情報と現在情報が競合しない環境を作る。
- 「状態(State)」と「記録(Log)」を切り分ける: 日常の状況把握には最新看板(上書き型JSON)を1枚置き、過去ログを探索させない。
- 古い情報は「捨てずに隔離」する: 直近の運用に必要な分だけを作業机に残し、それ以前の記録はアーカイブ倉庫へ安全に退避させる。
コーヒーの焙煎小屋でも、道具や麻袋が乱雑に散らかった作業場では、どんなに腕の良い焙煎士でもミスを起こします。作業台を常に清潔に保ち、使い終わった道具は定位置に戻すからこそ、毎朝安定した味を再現できます。
AIエージェントも全く同じです。
最新の巨大モデルを導入する前に、まずはAIの足元にあるログフォルダを掃除し、「最新の看板」を1枚立ててあげること。それこそが、自律エージェントと長く心地よく付き合うための、最も確実な現場の知恵だと感じています。
☕ 記事の執筆・開発の合間に「普通のコーヒー」を焼いています
036 Factoryの本業は、ご注文をいただいてから生豆を一粒ずつ手作業で選別(ハンドピック)して焙煎する、小さな自家焙煎珈琲工房です。
PC作業やAIとの対話で加熱した頭をふっとリセットする、雑味のない「毎日飲める普通のコーヒー」を少数精鋭で焼いています。この記事が少しでもお役に立ちましたら、ぜひ一度うちの工房を覗いてみてください。