AIの「完了しました」は当てにならない — 誤完了を証拠で止める品質ゲート

上原正吉(EarthLink Network Co., Ltd.)。Claude Codeを開発の主体に据え、20を超えるプロダクトを1人で同時に開発・運用しています。これは、その現場の実測記です。

AIの「完了しました」は当てにならない — 誤完了を証拠で止める品質ゲート

AIの「完了しました」は当てにならない — 誤完了を証拠で止める品質ゲート

2026 年 6 月 29 日、あるモバイルアプリが「PC 版とのパリティ第 1 弾完了、162 テスト green」と報告されました。実機 (TestFlight) で触ると、主要動線はことごとく壊れていました。SafeArea 未対応でノッチに食い込み、チャットは送信すると即 failed、設定画面は 404、ナレッジのアップロードは failed。162 件のテストが通っていて、実機で動いた主要動線は 0 でした。

AI にコードを書かせると、能力より先に「完了しました」という虚偽報告にぶつかります。本記事では、複数プロダクトで実際に起きた誤完了を解剖し、それを機械強制の品質ゲートに落とし込むまでを記録します。

先に結論を置きます。

  • テストが緑でも、動く証拠にはなりません — 全部モックしたユニットテストの green は「自分のモックが自分の期待どおりに動く」ことしか確かめていません。境界をまたぐ変更は、実機の E2E まで通してはじめて完了とします。
  • 実装したと、本番で動いているは別物です — ライブラリの入れ忘れで、書いたコードが本番では動かない dead code のまま沈黙し、監視には 1 万件を超えるエラーが積み上がりました。パッケージングやデプロイまで契約テストで裏取りします。
  • 完了は主張ではなく証拠で定義します — ビルドが通った・ユニットが緑・デプロイが成功した、といった弱い代理証拠だけで「完了」とは呼ばせません。実機相当の証拠がそろうまで完了にしないゲートを、機械で強制します。
  • デプロイのズレは pull 型で見ます — 本番のプラグインが 12 日間も古い版のまま凍結していたのに、品質ゲートは全部 green でした。コミット時に走る push 型のフックは git の外を観測できないので、配布先で定期的に突き合わせる pull 型のチェックを置きます。

誤完了の典型: 「テストが通る」を「動く」と偽る

162 対 0 の根本原因は 1 つでした。すべてをモックしたユニットテストが通ったことを、「動くアプリ」の証拠にして報告したことです。ふりかえりでは 7 つの根本原因をコード証拠つきで並べました。要点は 3 つです。API の baseURL が配線されていない。認証の結線は「future step」というコメントで先送りされている。実機操作はゼロ。テストは「自分が書いたモックが自分の期待どおりに動く」ことしか確認しておらず、外界とつながっていませんでした。ここから「テストが通る ≠ 機能が動く」を原則にし、完了ゲートの skill を新設しました。

promptflow モバイルの CI 実行結果。ユニットテストは 162 件すべて green(画面は再構成・匿名、数値は実測)。この緑の裏で、実機の主要動線は 1 つも動いていませんでした

別の顔: 「実装した」と「本番で動いている」も別物

誤完了は AI 特有の幻覚だけが原因ではありません。人間でも踏む「実装したのに本番に無い」も、同じ形をしています。

ニュースキュレーション基盤で、壊れた JSON をサルベージする多段フォールバックを実装したのに、本番でエラーが止まらない事象がありました。原因は、フォールバックに使う json-repair ライブラリがワーカー用の requirements ファイルに入っておらず、Docker イメージに存在しなかったことです。requirements ファイルが 2 つある構成の落とし穴でした。コードは書かれ、レビューも通り、しかし本番では dead code。その間、監視には 1 万件を超えるエラーイベントが積み上がっていました。この一件から、パッケージング契約そのものをテストで検証するようにしました。

# 「実装した」を「本番イメージに入っている」で裏取りする契約テスト
def test_fallback_dependency_is_packaged():
    # ワーカーの requirements に json-repair が宣言されていること
    reqs = read_requirements("requirements-worker.txt")
    assert "json-repair" in reqs, "本番イメージにフォールバック依存が無い"
    # 実際に import できること(宣言だけでなく解決可能か)
    import importlib
    assert importlib.util.find_spec("json_repair") is not None

監視ダッシュボードに積み上がった ModuleNotFoundError: No module named 'json_repair'。フォールバックのコードは書かれていたのに、本番イメージに依存が入っておらず dead code のまま、エラーイベントが 1 万件を超えて記録され続けていました(画面は再構成・匿名、数値は実測)

幻覚報告: 空のブランチで「1 行追加しました」

最も AI らしい誤完了もありました。ローカル LLM オーケストレーターに自分自身の改修を投げる dogfooding での出来事です。リポジトリを clone した実行エージェントが、そのリポジトリの設定ファイル(「実装はサブエージェントに委譲せよ」というルール)を読み、委譲しようとしました。ところが headless 環境では孫サブエージェントが起動できず、何も変更していないのに「1 行追加しました」と幻覚報告し、空のブランチを push しました。GitHub は「空の push」を 422 で弾きました。

学びは 2 つです。

  • 自己開発のときだけ、自リポの設定が実行エージェントにとって毒になる
  • 報告ではなく成果物(実 diff)を見る

仕組み: なぜ AI は「完了しました」と言ってしまうのか

誤完了には、共通する仕組みがありました。

  • AI は「タスクの意図を満たしたか」ではなく「もっともらしい終端状態に到達したか」で完了を判断しがち。テストが通る、コミット作成、PR 作成といった中間成果は、それ単体では意図の達成を意味しません。
  • 完了の分母に「デプロイ」や「実機」が入っていない。「リポジトリ内では 100% できた」を「本番で動く」と同一視すると、前述の json-repair や後述のプラグイン凍結が起きます。
  • 検証対象を取り違える。あるゲートは working tree の diff だけを見ていて、「コミットすると tree がクリーンになり、永遠に未完了扱いになる」というバグがありました。

つまり「完了」の定義そのものが、外界と切れていました。

対策: 完了を「主張」ではなく「証拠」で定義する

対処は一貫して「完了の定義を、AI の自己申告から機械検証可能な証拠へ移す」ことでした。ゲートを段階的に重ねました。

ひとつめは、証拠なき完了主張をブロックするゲートです。境界をまたぐ変更(外部 API・決済・DB 書き込み・SaaS 間連携)を「ビルドが通った」「ユニットが通った」といった弱い代理証拠だけで完了と呼ぶことを禁じ、実機相当の E2E 証拠を要求します。稼働監視 SaaS では、この実機ゲート(社内で MUST 25 と呼ぶ原則)に一度不合格を出したことで、ユニットがすべて通った裏で 5 件のバグ(DB create が落ちる、スケジューラがペイロードに必要データを含めない、Dockerfile のインストール順ズレ等)が一気に見つかりました。

ふたつめは、モバイルのように境界の多い領域へ専用の完了チェックリストを skill として持たせることです。次のような内容です。

# feature-done ゲート(実機で満たされるまで「完了」と呼ばない)
- [ ] API baseURL が実環境に配線され、モック無しで 1 往復した証拠がある
- [ ] 認証フローを実機で通過した("future step" コメントが残っていない)
- [ ] 主要動線(作成・送信・アップロード)を実機で 1 回ずつ手で通した
- [ ] SafeArea / ナビゲーションが実機スクリーンショットで確認済み

みっつめは、clean 環境でゲートを回すことです。手元では通るのに、clean にチェックアウトした環境ではテストが落ちる。これは実ディレクトリに依存した非 hermetic なテストを炙り出します。実際、あるゲートは clean worktree でテストを回し、「ローカルの npm run check では気づかない欠陥」を検出しました。ゲートの狙いどおりの成果でした。

配布のズレ(デプロイドリフト): push 型フックでは見えない凍結

最後に、完了の分母から「デプロイ」が抜けると何が起きるかの例です。自動運転中の本番アカウント群で、配布しているプラグインが 12 日間も古い版のまま凍結していました。それでも品質ゲートはすべて通過しており、誰も気づきませんでした。5 つの構造穴のうち核心は 2 つです。「リポジトリ内 100%」と「本番反映済み」を同一視していたこと、そして検出層が push 型のフックしか無かったことです。git のコミット時に走るフックは、git の外の状態(どの版が実際に配布先で動いているか)を構造的に観測できません。対策は、配布先で cron が「インストール済みの版」と「配布すべき版」を突き合わせるpull 型のパリティチェックを持つことでした。「配布のズレの検出は pull 型の定期突合だけ」という原則を、ここで文書に昇格させました。

配布先で走らせる pull 型のパリティチェック。インストール済みの版と配布すべき版を突き合わせ、12 日分のズレを検出しています。この間、品質ゲートはずっと全部 green のままでした(画面は再構成・匿名、期間は実測)

まとめ — 転用できる教訓

  • 「テストが通る ≠ 機能が動く」。全モックのユニットテストが通ることは「自分の期待が自分の期待どおり」を確認しているだけです。境界は実機 E2E で完了判定します。
  • 「実装した ≠ 本番で動いている」。パッケージング・デプロイまで含めて契約テストで裏取りします。dead code は本番で沈黙します。
  • 完了は主張でなく証拠で定義する。ビルド OK・ユニット通過・デプロイ SUCCEED という弱い代理証拠だけで「完了」と言わせません。
  • デプロイドリフトは pull 型で見る。push 型フックは git の外を観測できません。「リポジトリ内 100%」は「本番反映済み」ではありません。

筆者について

上原正吉。EarthLink Network Co., Ltd. でAI開発をしています。2025年からClaude Codeを開発の主体に据え、今は20を超えるプロダクトを1人で同時に開発・運用しています。この連載では、その現場で実際に起きたこと(うまくいったことも、失敗も)を、数字と一緒に書いていきます。

また、AIで業務や開発を組み替えたい会社・チーム向けに、AI活用のコンサルティングも受け付けています。ご相談は www.eln.ne.jp からどうぞ。

AIの「完了しました」は当てにならない — 誤完了を証拠で止める品質ゲート