— AIエージェント開発 航海(後悔)日誌 —
ある週、社内業務を自動化するAIエージェントを立て続けに4隻建造して運用した。
この記事はその航海日誌である。そして「航海」が「後悔」に変わった瞬間と、そこから持ち帰った海図の記録でもある。
🎵 Glory Glory Man United, Glory Glory Man United...
私事だが、この記事を書いている今、私はすこぶる機嫌がいい。我らがブルーノ・フェルナンデスが、ついにプレミアリーグ最優秀選手(Player of the Season)に選ばれたからだ。ピッチ全体を常にスキャンし、決定機を量産しながら、セットプレーやPKで確実に点も積み上げる。閃きの一本も計算ずくの一本も出せる、創造性と再現性を両立した稀有な中盤だ。中盤から試合を「操作」し、決定的な場面を「生成」し、しかし最後のパスを出すかどうかは常に冷静に「判断」する——理想のプレーメーカーである。
……お気づきだろうか。これはそのまま、私が今回作りたかったAIエージェントの理想像でもある。情報を集め、選択肢を組み立て、しかし不可逆な一手(=シュート、あるいは「申請する」ボタン)を打つ前には必ず確かな判断を挟む。ブルーノならオフサイドに飛び込んだりはしない。
ところが私のエージェントたちは、就航初日からオフサイドに飛び込み、味方のいないスペースにパスを通し、あろうことかオウンゴールまで決めてくれた。その全記録が、以下である。グローリー・グローリー、と歌い出した直後にこの体たらく。だが——United の歴史がそうであるように、本当の物語は失点のあとに始まる。
航路図(建造した船団)
| 船名 | 就航 | 任務 | 主な遭難 |
|---|---|---|---|
| 会議通知エージェント | 1日目 | 社内ポータルの予定→Mac通知+Zoom自動起動 | 会議「開始直前」に通知が来る位相ズレ |
| テレワーク申請エージェント | 1日目 | テレワーク申請の予定提出・実績投稿 | 勝手に申請を出す/実績が空のまま確定(本記事の主役) |
| 日報入力エージェント | 1日目 | 社内日報システム(GeneXus製)へ月次日報入力 | 無音で失敗する「反映」、消える小数 |
| チャット監視エージェント | 4日目 | 社内チャットツール監視→共有Excelに実績記入 | 既存行の上書き、paste散布で443セル汚染 |
共通の前提条件はこうだった。
- 対象は全部レガシー or API無しの社内システム。社内ポータルのワークフロー機能には REST API が無い(それらしいエンドポイントを総当たりしたが全部404)。日報システムはGeneXus製。チャットツールはCloudflare Turnstileでブラウザ自動化を拒む。
- よって武器は Playwright によるブラウザ操作。つまり「人間のふりをして画面を操作する」エージェントである。
- 認証情報はすべて macOS Keychain。コードに平文を書かない。これだけは最初から守れた数少ない美点。
本編:テレワーク申請エージェント遭難記
背景 — 毎日の申請こそエージェントの出番
うちの会社では在宅勤務のたびに、社内ポータルのワークフローでテレワーク実施申請(部門長決裁)を出す。タイトルは「○○部_氏名(M/D)」、実施時間は8:30〜17:30固定、接続方法は社内VPN固定……毎日変わるのは日付と業務内容だけ。これを自動化しない手はない。
さらに承認された申請は申請者に戻ってきて、コメント欄に実績を書いて「確認する」を押すと完了になる。つまりエージェントの仕事は2つ:
- 戻ってきた申請に実績を書いて「確認する」
- 翌日分の予定申請を提出する
API が無いことが判明した時点で、Playwright での画面操作に方針転換。申請フォームの標題から末尾のチェックボックス群まで全フィールドを偵察し、承認経路設定画面の「(省略)」選択の作法(処理者を削除して空にすると「次長は必須項目です」で弾かれる。候補リストの「(省略)」を選んで追加で置換するのが正解)まで解明した。準備は万全——のはずだった。
後悔その1:エージェント、勝手に作文して出航す
初回ライブ運用。エージェントは翌日の申請を出した。問題は業務内容だ。
私はまだ何も伝えていないのに、エージェントは翌日の業務内容をそれらしく作文して、そのまま申請を出してしまった。部門長決裁のワークフローに、AIの創作した予定が乗って流れていく。完璧な書式で、完璧に承認経路に乗って。
ここでの後悔は2つある。
- 内容を生成させる設計にしていたこと。 業務予定は「生成」するものではなく、グループウェアの登録予定と課題管理ツール(Backlog)の担当課題という事実から下敷きを作り、人間が取捨選択するものだった。
- 送信を止める仕組みが何も無かったこと。 「申請する」ボタンは一度押したら戻れないのに、押す前のゲートが運用ルール(プロンプトの指示)しかなかった。
前者は「事実から下敷きを作り、その整形だけを手元のローカルLLMに任せる」形に作り直した(業務内容を社外に出さない設計の話は、後述の「整形はローカルで」で詳しく書く)。後者が、このあと船団全体の憲法になる。
後悔その2:実績、空のまま確定。しかも「失敗しました」と報告してくる
同日、戻ってきた当日分の申請に実績を投稿した。4本立て・計8時間分の実績をコメント欄に流し込み、「確認する」をクリック。スクリプトの報告は——「投稿失敗」。
ところが後でポータルを開くと、申請は確定済み。そしてコメント欄は空。
「失敗した」と報告しながら、裏では成功しており、しかも本文が欠落している。
修理 — 「入れたはず」と「終わったはず」を信じない
修正は2点に集約される。
① 書いた値を信じない。
入力欄に文字を「打ち込む」だけでは、React の内部状態(送信時に実際に読まれる値)に届かないことがある。そこで、入力欄に値をセットしたあと 「入力された」「変更された」という出来事をブラウザに明示的に知らせて 、Reactに「ユーザーが手で打った」と認識させる。
そのうえで、入力欄を読み戻して、書いたはずの文字列と一字一句一致するかを照合する。一致しなければ「確認する」を押す前に処理を中止する。「入れたはず」という思い込みを、送信前に潰すわけだ。
② 画面の見た目で完了を判定しない。
完了判定は「権威ある信号」2点の照合に作り直した:
- 処理画面が編集不可になった(コメント欄が消失した)
- 申請詳細の経路変更履歴に実績文字列が実在する
「送信一覧から消えたか」は表示更新ラグで false negative を起こすため、参考ログに格下げ。描画ラグに備えて最大3回読み直す。
テスト用に標題「テストです」の申請を別途流し(承認者役を引き受けてくれた同僚に感謝)、E2Eで「経路変更履歴にコメント全文が記録される」ことを実証。空確定バグは解決した。
整形はローカルで — 業務内容をクラウドに出さない
後悔その1で触れた「内容を生成させない」設計の、もう一つの宿題がこれだ。グループウェアの予定やBacklogの課題から「その日に何をしたか/翌日に何をするか」を読み取って文章に整形する処理は、ローカルで動かしているLLMに行わせている。業務内容は社外秘を含みうるので、この要約・整形だけはクラウドのAPIに出さず、手元のマシンで閉じて完結させる方針にした。
そして整形後の本文も、クラウド側のエージェントには渡さない。受け渡しはファイルパスだけだ。ローカルLLMが整形した本文は、バージョン管理から除外したローカルのテキストファイルに書き出される。クラウド側のエージェントが受け取るのは「このファイルの中身を入力欄に流し込め」という指示と、ファイルのパスだけ。実際にファイルを開いて中身を読み、ブラウザに打ち込むのは、ローカルで動く投稿スクリプトである。クラウドのLLMはファイルを開かない——だから整形済みの本文すら、一度もクラウドには渡っていない。クラウド側が知るのは「何文字あるか」程度のメタ情報だけだ。
📌 補足(業務情報の取り扱い):業務予定・実績の中身に触れる「整形」「要約」は、すべてローカル実行のLLMで処理し、その出力テキストもクラウドLLMには渡さない。受け渡しは gitignore したローカルファイルへの書き出し+パスの受け渡しで行い、ファイルを開いて画面入力するのはローカルのスクリプト。クラウド側のエージェントが扱うのは、ファイルパス・文字数・処理状態・各種フラグといった非業務情報だけで、業務内容の判断・生成・閲覧のいずれもローカルに閉じている。
生まれた鉄の掟 — 承認ゲートは運用ではなく実装で
この2連発から、船団全体に適用される憲法が生まれた。
予定も実績も、人間の明示承認なしにワークフローを操作・送信することは絶対にしない。
ポイントは、これをプロンプトの約束で終わらせなかったこと。LLMへの指示は破られうる。だから技術ガードを入れた。
仕組みそのものはシンプルだ。送信処理(実績投稿・予定申請)は、「承認済み」を示す合言葉(環境変数)が付いていなければ、最初の一行で自分から止まって終了する。下書き保存や確認画面までの「送信しない操作」は合言葉なしで動くので、偵察やテストの機動力は保ったまま、外の世界へ出る一歩手前にだけ門を置ける。
ただし——ここが大事なところで、しかも私自身しばらく誤解していた——この合言葉を付けるのは、ほかでもないAI自身だ。「もう承認は得たよね」とAIが判断して、コマンドに合言葉を足して実行する。つまり鍵を握っているのはAI。これだけでは「AIが勝手に合言葉を付ければ素通り」という穴が残る。合言葉は鍵ではなく、誤発射を防ぐトリップワイヤーにすぎない、と理解しておくのが正確だ。その役割は、リトライの暴発・コピペ事故・フラグの付けっぱなしといった 事故的な送信をデフォルトで失敗させる(fail-closed) こと。悪意や勘違いをしたAIを止める力は、それ単体には無い。
では本当の人間ゲートはどこにあるか。 送信コマンドを実際に実行する瞬間に、その不可逆コマンドそのものを人間に見せて許可を取る ——ここだ。私の運用では、エージェントの実行基盤(Claude Code)が「これから …投稿する というコマンドを走らせます」と本人に提示し、人間が目視して承認しないと一歩も進まない。合言葉を足すのはAIでも、 その足された一手を世に放つ最終許可は人間が出す 。合言葉のガードは、その人間ゲートをすり抜けた事故をもう一枚内側で止める壁、という二段構えになっている。
「一度OKしたら、以降ずっと素通りになるのでは」という不安もあるが、これは設計で潰してある。合言葉は環境変数としてそのコマンド1回にだけインラインで付ける方式で、export して残したりはしない。次の送信ではまた付け直し=再び人間の許可が要る。承認は1操作1回。状態として持ち越さないのが鉄則だ。
サッカーで言えば、攻撃陣がどれだけ華麗でも優勝するのは守備が堅いチームだ。エージェントの自動化は攻撃、人間の最終許可がGK、合言葉ガードはその前に立つセンターバック。91年のシュマイケル獲得級に、まず最初に補強すべきはこの守備ラインである。一枚で守ろうとしない。
寄港地の日誌 — 他の船の後悔
⚓ 会議通知エージェント — 「5分前通知」が「開始直前通知」になる
社内ポータルの予定を取得して会議5分前にMac通知+Zoom自動起動する、船団最初の一隻。launchdで5分おきにポーリング、通知は開始5分前から発火——この設計、お気づきだろうか。
ポーリング間隔(5分)と発火ウィンドウ(0〜5分前)が同じ幅だと、位相のズレ次第で「開始30秒前にようやく通知」が起きる。チェックが14:56に走れば15:00の会議は「4分前」で鳴るが、14:59:50に走れば10秒前だ。修正は単純で、ポーリングを60秒間隔に詰めた。これでリードタイムは常に4〜5分に安定。
後悔の教訓:周期処理の発火保証は「間隔 ≪ ウィンドウ」が大前提。 ついでに言うと、このポータルのスケジュールAPIは X-Requested-With: XMLHttpRequest ヘッダーが無いと401を返すし、画面は常時通信していて networkidle は永遠に来ない。レガシーの海にはレガシーの海流がある。
⚓ 日報入力エージェント — 無音で失敗するボタンと、消える小数
GeneXus製の社内日報システムに月次の日報(グループウェア予定+Backlog+勤怠クラウドの残業データを下敷きに)を入力する船。ここの後悔は「エラーを出さずに失敗するUI」との戦いだった。
- 「登録」ボタンは「この内容でよろしいですか?」という確認ダイアログを出す。 ところが自動操作ツールは、こうしたダイアログを既定で勝手に「キャンセル」してしまう。あらかじめ「ダイアログが出たらOKを押す」と教えておかないと、登録したつもりで毎回キャンセルされ、何も保存されない。
- プロジェクトには「期限」があり、期限日より後の日付では「反映」が無音で通らない。 エラーもダイアログも無い。ただ何も起きない。あるプロジェクトの期限が月の半ばに切れていたため、その翌週の分だけがどうしても入らず、原因究明にかなり溶かした。反映が無音で失敗したら、まず期限を疑え。
- 工数欄は小数1桁で切り捨てられる(2.67→2.6)。8時間を3件に均等割りして2.67ずつ入れると日計が7.9になる。0.1刻みで配分して合計8.0に揃えるロジックが必要だった。
それでも当月分18日/33明細を本番登録(もちろん全件、内容提示→承認後)。レガシーUIの自動化は「成功した証拠」を必ず画面から取り直すこと。 操作した、は成功した、ではない。テレワーク事件とまったく同じ教訓が、別の海でも待っていた。
⚓ チャット監視エージェント — 正面が塞がれたらデッキから乗り込む、そして共有Excelを2回壊しかける
社内チャットツールのトークルームを監視し、技術調査・情報発信があれば共有のSharePoint Excelに実績記入する船。ここは航路選定からして面白かった。
- 正面突破は不可:チャットツールのWeb版サインインには bot 対策(CAPTCHA)がいて、自動化ブラウザでは認証を通してもらえない。新規にプログラムからログインしに行く経路は、そもそも塞がれている。
- 発想転換:そこで「新しくログインする」のをやめ、自分がすでに正規にログインして使っているアプリ/画面を、自動操作の対象として共有してもらう形にした。認証は自分の手で正規に済ませた状態を使い回すだけなので、bot対策とぶつからず、資格情報を新たにどこかへ渡すこともない。
- 書き込み先のExcel(クラウド上の共有ブック)も、API経由の書き込みは組織のポリシーで許可されていなかった。そこで同じ考え方で、自分がブラウザで正規に開いているExcelの画面を操作する形にした。
APIが無ければ画面操作、それも新規ログインが塞がれていれば“自分が正規に開いている画面”を使う。 いずれも認証はあくまで自分の手で正規に通したセッションの範囲内、というのがミソだ。この「降りていく順番」は今回の航海最大の収穫のひとつ。
🔒 接続方法についての注記:本記事では具体的な接続・アタッチの手順には踏み込まない。要点は「新たに資格情報をプログラムに渡したり、認証を回避したりはしていない/自分が正規にログイン済みの状態を、自分の手元の範囲で操作対象にしているだけ」という一点。社内システムの自動操作は、必ず所属組織のポリシーと利用規約の範囲内で、自分の権限の中で行うこと。
ただし共有Excelの海は浅瀬だらけだった。事故は2回起きた。
- 既存行の上書き:最終行検出が状態ドリフトで誤った行番号を返し、既存データの行を上書き。同僚のデータである。バージョン履歴から復旧できたが、背筋は十分に凍った。
- paste散布:直前の列読み取りで作った数百行の範囲選択が解除しきれないまま paste が走り、1列の400行超に日付が散布された。
対策はどちらも「検証付きの物理ガード」に行き着いた。
- 追記前の検証関数:「直上の行にデータがあり、書き込み先が空」でなければ例外で中止。最終行検出の結果を盲信しない。
- セル移動後に「名前ボックスの値==目標セル」を検証し、NGなら再試行、不成立なら中止。選択状態が残ったままのpasteを物理的に阻止。
なお運用モデルは「夕方に読取+通知のみ→人間立会いで判定・承認→記入」。テレワーク事件で生まれた鉄の掟が、ここでもそのまま憲法になっている。最終日にE2E本番記入が初成功した。
持ち帰った海図 — 横断する7つの知見
1週間で4隻、事故は大小6件。並べてみると、後悔は見事に同じパターンに収束する。
1. 不可逆操作の前には、人間の承認を「実装で」挟む。ただし一枚で守らない
プロンプトの約束は保証ではない。要点は3つ(詳細は本編「生まれた鉄の掟」)。①送信系だけにコードのガードを置く。②AI自身が付ける合言葉は鍵ではなく誤発射防止のトリップワイヤー——本物の人間ゲートは「不可逆コマンドを実行直前に人間へ提示して許可を取る」層に置く。③承認は1操作1回・状態を持ち越さない(「一度OK=以降ずっとOK」を作らない)。
2. 「入れたはず」を信じない
React/Vueのcontrolled componentに fill() は届かないことがある。書いたら読み戻して値一致を検証、不一致なら送信前に中止。これだけで空確定事故は構造的に起きなくなる。
3. 「終わったはず」を信じない
完了判定はUIの遷移や要素の消失ではなく、権威ある信号(データが実在する、編集不可になった、一覧から消えた)の2点照合で。1点だと表示ラグの false negative / false positive に振り回される。
4. 共有データへの書き込みは、位置検証ガードを通す
最終行検出・カーソル位置・選択範囲は全部ドリフトする。書く直前に「ここに書いてよい状態か」を独立に検証し、NGなら例外で止まる。復旧手段(バージョン履歴)の存在も事前に確認しておく。
5. 内容を生成させない。事実の下敷き+人間の取捨選択。業務情報はローカルLLMで閉じる
業務予定・実績のような「事実の記録」は、LLMにゼロから作文させてはいけない。スケジューラ・課題管理・勤怠などの事実ソースから下敷きを組み、人間が削る/足すフローにする。そして業務内容に触れる要約・整形は、クラウドAPIではなくローカル実行のLLMに任せる——社外秘を含みうる情報を手元から出さないためだ。肝は、整形“後”のテキストもクラウドに渡さないこと。受け渡しはローカルファイル+パスで行い、クラウド側のエージェントが触るのはパス・文字数・状態・フラグといった非業務情報だけにする。「ローカルで整形」と言いながら整形結果をクラウドに入力させていたら本末転倒——機微情報の境界はアーキテクチャで引く。
6. dry-run → draft → confirm → submit の段階制
送信系スクリプトには必ず段階フラグを用意する。不可逆の一歩手前(確認画面到達)まで自動で行けて、最後の一押しだけ人間。テストの安全性と本番の信頼性を同時に買える。
7. 自動化は「降りていく順番」で考える。ただし認証は正規のまま
正規APIが最善。無ければ画面操作(Playwright)。それも新規ログインが塞がれているなら、自分がすでに正規にログインして使っている画面を操作対象にする、という最終手段がある。降りるほど壊れやすくなるので上の段から順に検討する。そして全段に共通する大原則は、認証を回避したり資格情報を新たにプログラムへ渡したりはしないこと——あくまで「自分が正規に通したセッションを、自分の権限の範囲で使う」に留める。
おわりに — 後悔日誌は続く
「テレワークのエージェントが勝手に申請を出してしまった件」は、結果的に船団全体の憲法を生んだ。いまでは4隻とも、人間の承認なしに外の世界へ何かを送信することはない。エージェントに渡すべきは自由ではなく、よく設計された不自由だった、というのが1週間の結論である。
事故は全部痛かったが、どれも回収可能なうちに起きてくれた。試合終盤の失点ほど人を成長させるものはない——ただしアディショナルタイムの劇的な展開は、エージェントではなく人間の専売特許であってほしい。エージェントには、90分間きっちり守備をしてもらう。
航海は続く。後悔も、たぶん続く。だからこの日誌も続く。
※ 本記事の自動化はすべて自分自身のアカウント・自分の業務範囲で、所属組織のルールの範囲内で行っています。社内システムの自動操作を行う場合は、所属組織のポリシーを確認のうえ自己責任でどうぞ。