AIエージェントの「できました」を疑うための実装
AIエージェントに実装を任せ、記録が残っている7月1日から12週間が経ちました。番号をつけて台帳に残した決定は707件あります(2026-09-23 時点)。そこから「機械が成功を名乗ったのに、実際には効いていなかった」ケースだけを抜き出すと、きれいに4つの型に分かれました。
コードの品質の話ではありません。報告と現物のあいだに検査が無い、という話です。
この記事の数字の出どころは、当社サイトに置いた元データです(配信の通し201回を全部数えた記録・期間と数え方つき)。引用するときは、こちらを出典にしてください。
https://braincommunity.jp/jissoku/mihari.html
型1:「配った」と名乗ったが、配れていなかった
顧客向けの資料を、公開期限が来たら自動で消す仕組みを組みました。毎朝9時に起動し、期限切れを探し、ファイルごと消し、コミットし、本番へ配る。
初めて実物が動いた日の記録がこれです。
2026-09-07 09:00:55 下見=21本 / 消した=21本 / git=凍結して上げた
/ 突き合わせ=配ってよい / 配信=失敗 rc=2 / 見張り rc=0 OK
2026-09-12 09:00:54 下見=15本 / 消した=15本 / git=凍結して上げた
/ 突き合わせ=配ってよい / 配信=失敗 rc=2 / 見張り rc=0 OK
2回とも配信が失敗しています。 それなのに行の最後は 見張り rc=0 OK です。気づいたのは15日後でした。
なぜ気づけなかったか。見張りが見ていたのは「ローカルのファイルが消えたか」で、「本番に反映されたか」ではなかったからです。消す処理と配る処理を1本のパイプラインに並べたのに、1行の要約の最後に来るのはローカル側の判定だった。
コードを読み直すと、もう1つ穴がありました。配信が失敗した回は、赤の知らせを置く作りにはなっていました。ところが翌朝、何も消さなかった回が「今日は異常なし」と判断して、その知らせを片づけていたのです。赤は1日で消え、人の目に届きませんでした。
本番を後から測ったら、期限を過ぎた47本は全部「公開期間は終了しました」の1枚を返しました。読めるものは0件(2026-09-23 に測り直しても47本とも同じ)。実害はありませんでした。ただしそれは、別件の配信が後日たまたま巻き取ったからであって、この仕組みが働いたからではありません。
# 悪い: 段ごとの rc を集めずに、最後の見張りだけで名乗る
run_delete() # rc を見ていない
run_commit() # rc を見ていない
run_deploy() # rc=2 だが握りつぶし
if guard() == 0:
log("OK") # ← ここが嘘をつく
# 良い: 段ごとの rc を積み、1つでも非0なら全体を赤にする
rcs = {"delete": run_delete(), "commit": run_commit(), "deploy": run_deploy()}
bad = {k: v for k, v in rcs.items() if v != 0}
log(" / ".join(f"{k}={'できた' if v==0 else f'失敗 rc={v}'}" for k, v in rcs.items()))
if bad:
notify_red(bad) # 人が必ず見る場所へ置く
sys.exit(1)
「消した」と「消えたものが配られた」は別の事実で、別々に数える必要があります。 そして、赤を消す条件を「その回が何もしなかった」にしてはいけません。赤は、直ったと測れたときだけ消します(この穴は記事を書く途中で見つけ、公開と同じ日に直しました。いまは毎朝、消した鍵を本番に1件ずつ訊き、1件でも残っていれば赤を片づけません)。
型2:「書けた」と返ってきたが、中身は古かった
同じ日に、別の形で踏みました。計測用のスクリプトを1行直して端末へ置き直し、「書き込み成功」の応答を受け取って、そのまま実行しました。出たのは、直したはずの行のエラーです。
Traceback (most recent call last):
File "...\check.py", line 20, in <module>
out('1件目のキー', list(recs[0].keys()))
KeyError: 0
recs という変数は、新しい版には1文字も存在しません。つまり端末では古い版が走っていました。サイズを突き合わせたらこうでした。
| バイト | |
|---|---|
| 送った側 | 2,691 |
| 端末に在ったもの | 3,234 |
書き込みAPIは成功を返し、更新時刻も進んでいました。それでも中身は置き換わっていなかった。 直し方は単純で、名前を変えて置き直し、走らせる前にサイズを突き合わせる。
# 置いたあと、走らせる前に必ず突き合わせる
assert remote_size(path) == local_size(path), \
f"反映されていない: remote={remote_size(path)} local={local_size(path)}"
「成功を返したAPI」と「実際に置き換わったファイル」は、別の事実です。
型3:「数えた」が、数え方が違っていた
決定の記録から統治番号の最大値を取ろうとして、こう書きました。
nums = re.findall(r'C-(\d{3,4})', text)
max(int(n) for n in nums) # → 2027
答えは2027。実際の最大は788です。同じ文書には別系統の採番も書かれていて、そちらは年を含む長い形をしています。語境界を入れていなかったので、その年の部分を統治番号として拾っていました。
# 悪い: 語境界が無いので、もっと長い別の採番の一部にも当たる
re.findall(r'C-(\d{3,4})', text)
# 良い: 前後が英数字・ハイフンでないことを確かめる
re.findall(r'(?<![A-Za-z0-9-])C-(\d{3,4})(?![0-9-])', text)
この種の間違いは自分では見つけられません。見つけたのは、同じ数を出す既存の見張りが別の答えを返したからです。
自前の印より、既に在る見張りの判定を正とする。
以前も踏んでいて、そのときは「死んだ絶対パスが62件ある」と自分の走査が言い、正式な見張りの判定は3件でした。残り59件は全部ノイズです。その場で書いた走査は広すぎて、正しいものまで拾います。
同じ日に、この教訓をもう一度使いました。記事の材料として「決定に紐づくファイル」を自動で拾う機械を書いたところ、ドライブ名・フォルダ名・版番号・ブラウザの名乗り文字列まで「ファイルが見つからない」と報告してきました。印を「既知の拡張子で終わるものだけ」に狭めたら、雑音は38%から7%に落ちました。
| 印の広さ | 拾ったパス | 実在 | 見つからない |
|---|---|---|---|
| 広い(最初) | 21 | 13 | 8(38%) |
| 狭い(修正後) | 30 | 28 | 2(7%) |
広い印は、次に本当に見つけたいものを隠します。
型4:引き継いだ数字が、全部ずれていた
前の作業から引き継いだ資産の規模が、実際に数えたらこうでした。
| 引き継ぎ書 | 実測 | |
|---|---|---|
| 見張り(ガード) | 13本 | 48本(+自己検査41本) |
| 作業スクリプト | 490本 | 576本 |
| YAMLパイプライン | 442本 | 1,439本 |
範囲=3リポジトリ、node_modules・退避フォルダ・ビルド生成物を除く、2026-09-22時点。
3つとも少なく出ていました。原因は、引き継ぎ書がファイル名の印象と会話の記憶から起こされていて、一度も全数を数えていなかったことです。少なく出たほうは、1つのフォルダしか見ていませんでした。
引き継いだ数字は、引き継いだ時点で仮説です。
なぜ4つとも同じ形なのか
並べると、全部これです。
成功を名乗る経路と、実際に効いた経路が、別のところを通っている。
- 型1:ローカルの削除は成功した。配信は失敗した。判定はローカルだけを見た
- 型2:APIは成功を返した。ファイルは置き換わらなかった。判定はAPIの応答だけを見た
- 型3:走査は数を返した。数え方が違った。判定は自分の走査だけを見た
- 型4:引き継ぎ書は数を書いた。数えていなかった。判定は文章だけを見た
人が手で作業していた頃は、この差がここまで開きませんでした。手で消せば消えたのが見えるし、手で書けば書けたのが見えるからです。AIエージェントに任せた瞬間、「見える」が「報告を読む」に置き換わります。 そして報告は、書いた本人が書いています。
置いた対策は3つ
1. 段ごとに成否を持たせ、1つでも非0なら全体を赤にする
途中の段が黙って失敗して、最後の段だけ緑になる構造を作らない。外部コマンドは終了コードだけでなく、[FAIL] を含む行を全部と出力の末尾を添えて記録します。失敗の理由はたいてい末尾に出ます。
2. 見張り自身に、3つ揃いの自己検査を付ける
# 素で通る / 故意に壊したら止まる / 戻したらまた通る
assert guard(clean_input) == 0 # 素 = 0
assert guard(broken_input) == 1 # 故意の違反 = 1
assert guard(clean_input) == 0 # 戻して = 0
3つ目がないと、何にでも落ちる見張りが「全違反を検出できている」ように見えます。違反の種は既存データに頼らず自分で作ります。データの状態に依存させると、データが変わった日に検査が黙ります。
この仕組みで配信が実際に何回止まったかは、記録から全部数えて当社サイトに置いています。止めた段20種の内訳と、数え方・「この数字で言えないこと」は元データの方にだけ載せています。人の指示でAIが起動した配信 201回(2026-07-21〜09-22)のうち48回が本番の前に止まり、そのうち14回は見張り自身の自己検査が止めました。
→ https://braincommunity.jp/jissoku/mihari.html
3. 「やった」と名乗る前に、現物を数える
「消した」「置いた」「配った」「無い」は、測ってから名乗る。特に在ることと戻せることは別です。配ったものの中身が一致していても、バージョン管理に載っていなければ届いていません。実際に、中身の照合だけを見ていた見張りが、13本すべて「未コミット」の状態を「13本届いている」と報告したことがあります。
おまけ:この記事も、自分の見張りに止められた
対策2で書いた「3つ揃いの自己検査」を、記事そのものにも当てています。顧客名・内部の固有名・実機の絶対パス・署名の有無・数に範囲が添えてあるかを、出す前に機械が見る検査です。
その検査に、この原稿の初稿を通したらこうなりました。
顧客名の名簿 = 48 件(値は出さない)
[FAIL] 案件ID が本文に在る(BC-…)
[FAIL] 実機の絶対パス が本文に在る(C:/…)
[FAIL] 改修番号 が本文に在る(C-…)
KIJI_GUARD_FAIL 件数=3
3件のうち2件は本物でした。上の型3を説明するために、実物の採番と実機のパスをそのまま書いていたのです。「出してはいけないものを書かない」と決めていた本人が、それを説明する文章の中で書いていました。 残る1件は誤検知で、検査が見出し部を本文として読んでいたのが原因です。そちらは検査のほうを直しました。
人の注意力は、自分が今いちばん集中している対象をすり抜けます。だから検査は、書いた本人ではなく機械が持ちます。
まとめ
AIエージェントに実装させること自体は、うまくいっています。記録が残っている7月からの12週間で、パイプライン1,439本と見張り48本が動くようになりました(2026-09-22 時点・3リポジトリ)。
壊れるのは実装ではなく、実装が自分について語る言葉のほうです。だから私たちは、AIに任せる範囲を広げるのと同じ速さで、AIの報告を信じない仕組みを増やしています。自己検査41本は、そのためにあります。
「AIに全部やらせる」の反対側にあるのは「AIを使わない」ではなく、AIの出力を機械が検算するという設計です。
元データと引用のしかた
見張りが配信を止めた回数・止めた段の全部・数え方・この数字で言えないことは、当社サイトに置いています。数字を引用するときは、記事ではなく元データを出典にしてください。そのまま貼れる形はこうです。
出典:ブレインコミュニティ合同会社「出荷前の見張りは、201回の配信のうち48回を止めた」
https://braincommunity.jp/jissoku/mihari.html (2026-09-23 測定)
当社が自分で測ったほかの数字も、同じ置き場にまとめています。
→ https://braincommunity.jp/jissoku/
業務システムの内製化と、AI活用の受託開発をしています。
ブレインコミュニティ合同会社 — https://braincommunity.jp/