0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

流星検出を題材に、AIエージェントで既存データ基盤を運用可能へ育てた実践記

0
Posted at

はじめに

前回は、古いUbuntu 24.04 PCにHermes Agentを入れ、Discordから操作しながらrootless Podman環境で使った話を書きました。

前回: 制約環境に強いAIアシスタントとしてのHermes Agent:Ubuntu 24.04 + rootless Podmanで試した実践記

前回の記事では、Hermes Agentの概要、Discord連携、複数LLMプロバイダ、rootless Podman、3つのPodmanプロジェクトを扱っています。この記事ではそれらの説明を繰り返しません。

今回は、その中のmeteor-detect-dashboardを深掘りします。対象は流星検出ですが、主題は天文ではありません。
既存コードと既存データを起点に、AIエージェントと一緒に「検出、蓄積、人手確認、BI集計」までをつなぐデータ基盤をどう作り、どう壊れ、どう直したかです。

この記事で扱うこと

関連作業は2026年6月下旬から始め、約1か月半、要求変更と障害対応を繰り返しました。最終的に動いた処理は次のとおりです。

録画到着
  → 夜間対象の選別
  → Python/OpenCVによる候補検出
  → PostgreSQLへの登録
  → 人手レビュー
  → confirmedだけをApache Supersetで集計

Supersetで構築した流星検出ダッシュボード
この記事では、次の話に絞ります。

  • one-shot workerと既存Python/OpenCVツールの責務分離
  • 録画完了ステータスであるrecordingsテーブルから流星検出イベントであるdetection_eventsテーブルへの粒度変更
  • Supersetと専用Review Appの役割分担
  • メモリ不足、rootless Podman、DB View、時刻表示、Supersetフィルターの実障害
  • WSL2ワーカーへの処理オフロード
  • AIエージェントを「コード生成器」ではなく、調査・変更・検証を回す実行者として使ったときの設計論点

成功条件は「検出できる」ことではなかった

最初はチャットベースで「指定時刻の動画に流星が写っているか確認したい」という単発の相談ができればいいかなと考えていました。
録画を探し、ffmpegでフレームや短い動画を作り、既存のOpenCV検出をかければ目的は達成できます。

しかし、候補が見つかった後に次の問いが残りました。

  • どの録画から得た候補か追跡できるか
  • 誤検知を除外できるか
  • 後から条件を変えて再集計できるか
  • 解析を中断しても再開できるか
  • 流星群のピークが近づいてきているときなど日別・時間別に傾向を見られるか

候補を出すだけでは、分析に使えるデータになりません。今回の成功条件は、検出精度ではなく、次のE2Eが成立することに置きました。

最終構成ではworkerは検出ツールを呼び出す側に置いた

構成上、最も意識したのはworkerと検出ロジックを混ぜないことです。

コンポーネント 責務
one-shot worker 対象日選択、lock、ファイル上限、再実行、DB登録、低負荷制御
既存Python/OpenCV 動画解析、候補フレーム検出、検出画像作成
PostgreSQL 録画、検出イベント、レビュー状態、集計View
Review App 人間による判定、誤検知補正、再レビュー
Superset KPI、時系列、ヒートマップ、一覧、フィルター
media server サムネイル、検出画像、元動画へのリンク

workerから既存Python/OpenCVをsubprocessとして起動し、結果をPostgreSQLとメディア領域へ登録します。
※こちらのPython/OpenCVに関してはこの分野でよく利用されるこちらのスクリプトを最近のPythonで使えるようリファクタリングしています。
https://github.com/kin-hasegawa/meteor-detect

one-shot worker
  ├─ 日付指定
  ├─ lock取得
  ├─ MAX_FILESで件数制限
  ├─ Python/OpenCVをsubprocess起動
  ├─ 検出結果をDBへ登録
  └─ 進捗と失敗状態を記録

この分け方にしておくと、既存の検出ロジックを別環境へ移すときも、workerの運用制御を保ったままにできます。

既存の解析コードを「書き直さずに包む」

既存のmeteor-detectは次のパス形式を前提にしていました。

YYYYMMDD/HH/mm.mp4

実録画は次の形式です。

YYYYMMDD/HHMMSS.mp4

この差を理由に検出ロジックを大規模に書き直すのは避けました。worker側で一時的なシンボリックリンクを作り、既存コードが期待するパスへ変換します。

実録画
  YYYYMMDD/HHMMSS.mp4
          ↓ 一時リンク
/tmp/meteor-detect-input/YYYYMMDD/HH/mm.mp4
          ↓
既存DetectMeteor

最近のPythonで動かない箇所や古い依存の扱いは直しましたが、検出ロジックそのものは再利用しました。AIエージェントを使うと、ゼロから生成するより前に、既存資産の前提を読んでアダプターを作る、という進め方が取りやすいと感じました。

バイブコーディングを探索で終わらせない

今回の作業は次の3段階に分かれました。

探索では、会話から即席ツールを作り、価値があるかを確かめます。固定化では、繰り返したい処理をコード、DB、設定、UIへ移します。運用化では、失敗を前提に状態と再実行方法を設計します。

最初の探索は速く進みました。時間がかかったのは、動いた処理を毎晩回しても壊れにくい形へ変える部分です。AIエージェントは探索の速度を上げますが、運用化に必要な設計判断まで自動で消えるわけではありません。

データ粒度を録画単位から検出イベント単位へ変えた

当初は、recordingsテーブルにレビュー状態を持たせていました。しかし1本の動画に複数の候補が含まれ、その中に本物と誤検知が混ざると、録画単位では表現できません。

そこで、レビュー粒度を検出イベント単位へ変更しました。

状態は3つです。

unreviewed      未確認。BI集計から除外
confirmed       人間が確認済み。BI集計対象
false_positive  誤検知。BI集計から除外

検出イベント単位のReview App

これで、同一動画にある複数候補を個別に判定できるようになりました。複数イベントの一括判定、日付による絞り込み、レビュー理由の記録も追加しています。

ここでのHuman-in-the-loopは、AIを監視するための飾りではありません。自動検出の候補と、BIで数えてよい確定データを切り分けるデータ品質プロセスです。

Apache Supersetを汎用BIとして使った

Supersetは流星専用の画面としてではなく、既存PostgreSQLへ接続する汎用OSS BIとして使いました。Apache Supersetは、SQLデータソースへ接続してデータ探索、可視化、ダッシュボードを構築するプラットフォームです。
https://superset.apache.org/

今回追加したのは次の内容です。

  • 確認済み合計流星数のKPI
  • 日別・時間別集計
  • 1年分で未観測・未分析をゼロ埋めしたヒートマップ
  • 日付フィルター
  • 検出画像と元動画へ移動できる一覧
  • confirmedだけを参照するPostgreSQL View
  • JST表示用の日時列

更新系UIまでSupersetへ押し込むことはしませんでした。Supersetは参照と分析、Review Appは更新と判定です。

この分担は、レビュー画面に複雑なUXが必要になったときにも効きました。Supersetを専用Webアプリと比較して優劣を決めたわけではありません。既存のDBとBI基盤があり、要件がKPI、集計、フィルター、一覧中心なら、再利用する価値があるという判断です。

実際に起きた障害

ここからが、設定やコードを生成しただけでは終わらない部分です。

約6GBメモリのホストで解析プロセスが約5GB使った

Pythonの解析プロセスが約5GB RSSまで増え、OOM Killerの対象になりました。周辺コンテナの状態にも影響します。

対策はシンプルです。

  • workerを1つに限定
  • MAX_FILESで処理件数を制限
  • 日付単位のone-shot実行
  • 新しい日付から順番に処理
  • 日付間にsleepを入れる
  • niceとioniceで優先度を下げる
  • 中断後に再開できる状態をDBへ残す

AIエージェントもCPUとメモリを使います。エージェントを同居させる場合は、対象ワークロードだけでなく、エージェントを含めた資源設計が必要です。

rootless Podmanのruntime状態が壊れた

前回の記事でも触れたrootless Podmanの話ですが、ダッシュボード運用では影響がより具体的に出ました。長時間セッション後に/run/user/$UIDが失われ、次のようなエラーが出ることがあります。

Error: default OCI runtime "crun" not found: invalid argument

別のケースではpause processのPID情報がstaleになり、新しいpodman execがnamespaceへ参加できなくなりました。既存コンテナは動いているのに、新しい操作だけが失敗する状態です。

そこで、アプリ専用のstorageとrunrootを固定し、通常のpodman psではなくwrapperから状態を確認する運用にしました。

障害時には、いきなり全コンテナを再作成せず、稼働中コンテナを保護したままpause PID、namespace、runroot、Podman管理情報を順番に確認しました。

PostgreSQL Viewの依存でworker初期化が失敗した

レビュー機能に合わせてViewを更新したところ、下流Viewが依存しているためDROPできませんでした。

DependentObjectsStillExist

これはSQLの1行だけの問題ではありません。SupersetがViewの列名と型を使っているため、Viewは下流に対するデータ契約になります。

依存順にViewを再作成し、Supersetが参照する列名と型は維持しました。DB Viewを内部実装として気軽に壊すと、BIが壊れます。

DBの時刻は正しいのにSuperset一覧だけUTCだった

PostgreSQLにはTIMESTAMPTZで正しく保存されていましたが、Supersetの一覧だけUTC表示になりました。

保存値を書き換えると別の処理で時刻の意味が壊れます。DB、API、Supersetの表示を個別に確認し、保存時刻の意味は維持したまま、Viewまたは表示境界でJST化しました。

Supersetの日付フィルターが見た目だけ変わった

最も時間を使ったのはこの障害です。

  • 日付フィルターのチップは選択した日付に変わる
  • 外側のApplyボタンは無効のまま
  • 3つのチャートは前日の値から変わらない
  • API payloadを見ると動的な「前日」が送られ続ける

Superset日付フィルター障害の調査画面

調査では、次のレイヤーを順番に追いました。

UI
  → DataMask
  → Redux上の適用済み状態
  → API payload
  → chartのtime_range
  → SQL
  → PostgreSQLの実件数

Superset 6.1.0では、動的な前日をdefaultDataMaskへ設定したとき、UIの選択状態と適用済みDataMaskが分離しました。一時検証用ダッシュボードでdefaultDataMaskを空にするとApplyが有効になり、選択日の件数へ更新されました。

最終的には次の方法で回避しました。

  1. ネイティブフィルターのdefaultDataMaskを空にする
  2. 動的な前日を各チャートのTEMPORAL_RANGEとtime_rangeの初期値へ移す
  3. フィルターIDを更新し、ブラウザに残った旧状態と分離する
  4. UI、API payload、SQL、DB実件数が一致することを認証済みブラウザで確認する
  5. 診断用ログを削除して回帰テストを行う

この障害は設定JSONを生成しただけでは見つかりません。ログイン後の実画面を操作し、内部状態とDBをつないで初めて完了と判断できました。

重い処理をWSL2ワーカーへ逃がす

低メモリサーバーだけで過去動画を処理するのは厳しかったため、Windows 11ワークステーションのWSL2へ解析workerを移せるようにしました。

境界は次のようにしました。

  • 録画と既存検出コードはNFS read-only
  • メディア出力だけNFS write
  • PostgreSQLのデータディレクトリはNFSへ置かない
  • DB接続はlocalhost限定ポートへのSSH tunnel
  • workerは1日付につき1つ
  • デフォルトのメモリ上限は4GiB、swap上限は6GiB
  • サービスを常駐させず、docker compose run --rmで実行

AIエージェントを障害調査の実行者として使う

前回の記事では、Hermes Agentの機能と安全面を紹介しました。今回実感したのは、エージェントが技術レイヤーをまたぐ障害調査で役に立つことです。

Supersetフィルター障害では、同じ会話の中で次を進めました。

  1. 認証済みブラウザで症状を再現
  2. Supersetの設定JSONとDataMaskを確認
  3. API payloadを確認
  4. 生成SQLを確認
  5. PostgreSQLの実件数と比較
  6. 一時ダッシュボードで分離条件を作る
  7. 最小変更を適用
  8. 3チャートすべてでE2E確認
  9. 自動テストを再実行
  10. 解決手順をドキュメント化

エージェントの仮説を人間が信じる必要はありません。UI、ログ、API、SQL、DBという証拠が一致するかを確認します。

人間が担当したことも明確です。

人間 AIエージェント
目的と優先順位 実環境の探索
技術スタックの指定 実装案と代替案
UXの受入判断 コード、SQL、設定の変更
危険操作の承認 テスト、build、DB、HTTP確認
流星か誤検知かの判断 障害の証拠収集と仮説検証
本番で許容するリスク 手順のドキュメント化・スキル化

測定できたもの

日次ダッシュボード追加時には、実装だけでなく実環境での確認時間も記録しました。

項目 実測値
初回ライブ検証まで 約17分
認証済みフィルターE2E完了まで 109分
新規Python 566行
自動テスト 29件成功
新規依存パッケージ 0
新規運用サービス 0

17分に含まれるのは、既存PostgreSQL、Superset、メディア配信、Review Appがある状態での日次ダッシュボード追加です。基盤全体を17分で作ったわけではありません。

最初に表示するところまでは速く進みました。一方、認証済みブラウザでフィルターを操作し、API、SQL、DB件数まで一致させる作業には109分かかりました。今回の事例では、コード生成より統合とE2E検証が支配的でした。

エンタープライズ環境へ持ち込むなら

個人環境の運用を、そのまま企業本番へ持ち込むつもりはありません。少なくとも次の写像が必要です。

今回のPoC 企業環境での候補
Discord Teams、社内Portal、ServiceNow
個人の承認 RBAC、職務分離、承認ワークフロー
ローカルファイル変更 Git、Pull Request、CI/CD
.env Secrets Manager、Vault
コマンド実行ログ 監査ログ、証跡保管
手動復旧 Runbook、Rollback、SLO
録画とOpenCV 既存ETL、データ品質処理、機械学習・AIモデル
Review App 業務ワークフロー
Superset 既存BI、DWH、Lakehouse

IBM製品には、複数のAIエージェントをアプリケーションや業務ワークフローへ接続し、企業向けに統合・ガバナンスするwatsonx Orchestrateがあります。
今回のHermes Agent事例はwatsonx Orchestrateとの製品比較ではありません。個人環境のOSに近い場所で、権限境界、Human-in-the-loop、監査可能な証拠、再実行、停止条件を試したものです。企業向けAgentOpsを考えるときも、同じ設計論点が現れます。

まとめ

流星検出器そのものは、今回の主役ではありませんでした。

価値があったのは、既存の動画と解析コードを起点に、次のデータライフサイクルを作れたことです。

入力
  → 自動処理
  → 候補イベント
  → 人間のレビュー
  → 確定データ
  → BI集計
  → 次の改善

AIエージェントを使うことで、既存資産の調査、アダプター実装、DBとUIの変更、コンテナ操作、障害の切り分け、E2E確認を同じ作業ループで回せました。

ただし、人間の判断を消したわけではありません。人間が目的とリスクを決め、AIエージェントが実環境で調査・変更・検証を進める分担です。既存データ基盤の変更・検証ループへAIエージェントを参加させることは、コード生成以外でも実用的な選択肢になり得ます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?