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文字も存在しません。つまり端末では古い版が走っていました。書き込みAPIは成功を返し、更新時刻も進んでいました。それでも中身は置き換わっていなかった。
同じことは、この記事を直していた 2026-09-24 にも起きました。書き込みの応答が成功を返した直後に、端末のファイルの大きさを測った記録です。
| バイト | |
|---|---|
| 送った側 | 4,908 |
| 端末に在ったもの | 4,579 |
(最初の件は大きさの記録を残していなかったので、2026-09-24 の件の数を載せています。)
直し方は単純で、置き直し、走らせる前にサイズを突き合わせる。
# 置いたあと、走らせる前に必ず突き合わせる
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) # → 2026(これに1を足して「次は2027番」と名乗った)
答えは2026。これに1を足して、次の番号を2027番と名乗りました。採番の機械は、最初から373番と答えていました(2026-08-21 の件)。同じ文書には別系統の採番も219件書かれていて、そちらは年を含む長い形をしています。語境界を入れていなかったので、その年の部分を統治番号として拾っていました。
# 悪い: 語境界が無いので、もっと長い別の採番の一部にも当たる
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件は全部ノイズです。その場で書いた走査は広すぎて、正しいものまで拾います。
この記事の材料を集めるときも、この教訓を使いました。記事の材料として「決定に紐づくファイル」を自動で拾う機械を書いたところ、ドライブ名・フォルダ名・版番号・ブラウザの名乗り文字列まで「ファイルが見つからない」と報告してきました。印を「既知の拡張子で終わるものだけ」に狭めたら、見つからない割合は74%から8%に落ちました(2026-09-24、同じ決定24件を入力にして、広い印と狭い印の両方で数え直した値)。
| 印の広さ | 拾ったパス | 実在 | 見つからない |
|---|---|---|---|
| 広い | 192 | 49 | 143(74%) |
| 狭い | 49 | 45 | 4(8%) |
広い印は、次に本当に見つけたいものを隠します。
型4:引き継いだ数字が、全部ずれていた
前の作業で書かれ、そのまま当社の別の記事に載っていた数字を、出どころまで辿ったらこうでした(2026-09-23)。
| 引き継いだ数字 | 辿った結果 |
|---|---|
| AIのクローラが読みに来た回数「1,214回」 | 出どころを辿れない。記録から数え直すと267回 |
| アクセス解析の期間「90日」 | 記録は7月9日からで、75日 |
| 運用の長さ(年で丸めた書き方) | 記録が残っているのは7月からの12週間 |
3つともずれていました。原因は、数字が前の作業の文章から写され、一度も記録と突き合わせていなかったことです。3つとも、見つけた日に記事を直しました。
引き継いだ数字は、引き継いだ時点で仮説です。
なぜ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週間で、見張り49本と、その見張りを試す自己検査41本が動くようになりました(2026-09-22 の終わりの時点・4リポジトリの版管理に載っている物を数えた)。
壊れるのは実装ではなく、実装が自分について語る言葉のほうです。だから私たちは、AIに任せる範囲を広げるのと同じ速さで、AIの報告を信じない仕組みを増やしています。自己検査41本は、そのためにあります。
「AIに全部やらせる」の反対側にあるのは「AIを使わない」ではなく、AIの出力を機械が検算するという設計です。
(2026-09-24 訂正:型2・型3・型4と、まとめの数を、出どころと突き合わせて直しました。型3の件は、元の記録どおり「答えは2026、名乗ったのは2027番、正しくは373番」に直しています。記録が残っていなかった数は、測り直した値か、辿れる別の件に差し替えました。)
元データと引用のしかた
見張りが配信を止めた回数・止めた段の全部・数え方・この数字で言えないことは、当社サイトに置いています。数字を引用するときは、記事ではなく元データを出典にしてください。そのまま貼れる形はこうです。
出典:ブレインコミュニティ合同会社「出荷前の見張りは、201回の配信のうち48回を止めた」
https://braincommunity.jp/jissoku/mihari.html (2026-09-23 測定)
当社が自分で測ったほかの数字も、同じ置き場にまとめています。
→ https://braincommunity.jp/jissoku/
業務システムの内製化と、AI活用の受託開発をしています。
ブレインコミュニティ合同会社 — https://braincommunity.jp/