Paul Jialiang Wu agentic-portfolio English 中文 Español 한국어✉️ 無料リスト
← ポートフォリオへ戻る

AI-Native シリーズ・信頼性

バグを三回直した。問題はバグではなかった。

著者:Paul Jialiang Wu · agentic-portfolio-lovat.vercel.app · 2026-08-02

1分でわかる要点――ここから持ち帰れること

同じ壊れたページをAIエージェントに三回報告した。三回目は全部大文字だった。返事はどれも真実だった――修正済み、マージ済み、テストは緑――それでもページは死んだままだった。配信チェーンの中で三つの異なるリンクが壊れていて、その上流のすべての層が成功を報告し続けていたからだ。教訓:「完了」とは最後のリンクについての主張にすぎない。ユーザーが体験する高度で検証せよ。さもなければ、自分で自分に領収書を切っているだけだ。

ある午後の検死解剖――配線バグ、丁重に姿を消すデプロイ基盤、そして一度も動いたことのない機能。読了目安:約6分。

A seven-box delivery chain — code, merge, build, deploy, serve, auth, pixels — with ✕ marks breaking three connectors: merge to build (break 2), serve to auth (break 1), and auth to pixels (break 3)
配信チェーン:コード → マージ → ビルド → デプロイ → サーブ → 認証 → ピクセル。一つの午後に三つのリンクが壊れた。壊れた箇所より上流のすべてのリンクは「完了」と言い続けていた。

8月2日、このポートフォリオそのものにある非公開ダッシュボードが「Not Found」と表示されている、とAIエージェントに伝えた。バグを直したと言われた。確認すると――Not Found。もう一度報告した。別のバグを直したと言われた。Not Found。三回目に私はこう打った。WHY???――疑問符三つ、ステークホルダーの忍耐の国際単位だ。

ここからが気まずい。それらの修正はどれも正しかった。テストは緑だった。マージも本物だった。それでもページはずっと死んだままだった。私が戦っていたのは一つのバグではなかったからだ。私が戦っていたのはチェーン ——チェーンは一度に一つのリンクだけが壊れ、最後の壊れを直すことでようやく次の壊れが姿を現す。

メンタルモデル:全員が自分の走りだけを採点するリレー競走。 修正は旅をしなければならない——コード → マージ → ビルド → デプロイ → サーブ → 認証 → ユーザーの画面。このリレーの各走者は「自分は良い区間を走った」と胸を張って言うだろうし、たいていそれは本当だ。だが勝負が決まるのはバトンが最後の線を越えたときであり、その瞬間を途中の誰も見ていない。「完了」とは最後の受け渡しについての主張にすぎない。それより上流のすべては、走者自身による自己採点だ。

第一幕:正しい二つの半分、そして握手のない継ぎ目

ダッシュボードのAPIは一つのHTTPヘッダーで認証する。ダッシュボードのページはそのAPIを呼び出していた——ただし、ヘッダーを一切送らずに。APIは正しかった:設計通り、匿名呼び出しを拒否した。ページも正しかった:設計通り、データを取得しようとした。誰も引き受けていなかったのは、その間にある一文だった。ページはAPIが読み取る認証情報を提示しなければならない.

Googleの『SRE本』には、何年も前に読んで「まあ、いい話だ」くらいに片付けていた一節がある。"Note that in a multilayered system, one person's symptom is another person's cause" [1](訳:多層システムでは、ある人にとっての症状が、別の人にとっては原因である場合があることに留意せよ)。私の症状は404だった。原因は、それぞれが自分のテストには合格していた二つのコンポーネントの継ぎ目に潜んでいた。継ぎ目にテストが存在するのは、自分でそれを書いた場合だけだ——これこそがコントラクトテストの存在理由そのものであり、Fowlerはこう指摘している。"[a] failure in any of these contract tests implies you need to update your test doubles, and probably your code" [2](訳:これらのコントラクトテストのいずれかが失敗したなら、テストダブルを、そしておそらくコードそのものも更新する必要があるということだ)。誰もテストしていなかったもの、それがこの契約だった。

第二幕:静かに姿を消すプラットフォーム

そこで配線を直し、マージし、ユーザー(私だ)に「完了した」と伝えた。Not Found。

修正はmainブランチに入っていた。テストは緑だった。誰も知らなかったのは、このホスティングプラットフォームの無料プランが——制限ページの原文そのままに——"100 times every 86400 seconds" [3](訳:86400秒ごとに100回まで)しか許していないということであり、意気込んで小さなプルリクエストを量産したその日の午後に、その100回をすべて使い切っていたということだった。それ以降のマージはすべて、何も引き起こさなかった。リポジトリにはエラーなし。赤いバツ印もなし。デプロイの連携は、ただ静かに断っていただけだった。一日中手紙を受け取り続ける、底の抜けた郵便受けのように。

私のエージェントは「マージ済み ✓」と告げた。まるで、昼から閉まっているキッチンについて「ただいまお持ちします」と言うウェイターのように。その主張は真実だった。そしてその主張は同時に無意味でもあった。なぜなら、「マージ済み」とは、自分の領収書に自分で書いたサインにすぎないからだ——顧客が本当に知りたい問いは「デプロイされてサーブされているか」であり、その問いは一度も発せられなかった。SRE本はこれをブラックボックス/ホワイトボックスの分断と呼ぶ。ブラックボックス監視は"is symptom-oriented and represents active—not predicted—problems: 'The system isn't working correctly, right now'" [1](訳:症状指向であり、予測ではなく実際に起きている問題を表す——「システムは今まさに正しく動作していない」)。我々が行ったチェックはすべてホワイトボックスだった。当直していたブラックボックス監視者はユーザーただ一人であり、そのユーザーは疑問符で私に請求書を回してきていた。

第三幕:一度も動いたことのない機能

割り当てが解放され、デプロイを強制し、ページはようやく読み込まれた——そして中身は空だった。ダッシュボードは、CLIがサイトにプッシュするレポートカードを描画する。そのプッシュは、今になって判明したのだが、その生涯において一度も成功したことがなかった。CLIはある名前のヘッダーで認証情報を送り、サイトは別の名前のヘッダーを読んでいた。この機能はテストされていた——ただし、認証情報を必要としない側の経路に対してだけ。認証を必要とする側の経路は、本番実行がゼロだった。「めったに動かない」ではない。ゼロだ。

本番で一度も動いたことのないコードパスは、機能ではない。それはユニットテスト付きの噂にすぎない。

Left box: where the tests looked — unit tests, merges, green builds, three checkmarks. Right dark box: the user's screen, 'Not Found' times three. Below, a dashed panel titled 'THE GAP' lists the three numbered breaks: the missing credential handshake, the spent deploy quota, and the wrong header name
テストが見ていた場所と、ユーザーが見ていた場所。三つの故障はすべて、その隙間に潜んでいた——そして一つが次を隠していた。

なぜ三度も繰り返されたのか——スイスチーズモデルによる答え

James Reasonのスイスチーズモデルは、ハイテクシステムにおける事故を、複数の防御層に空いた穴を貫通する軌道として描写する——"some are engineered (alarms, physical barriers, automatic shutdowns, etc), others rely on people"(訳:それらの一部は工学的に設計されたもの(警報、物理的障壁、自動停止装置など)であり、他は人に依存している)[4]。通常この理論が引き合いに出されるのは、穴が一直線に並んで惨事がすり抜けてしまう場面でだ。だが私のあの午後はその逆で、現場のエンジニアが日々味わっているバージョンだった。スライスはあまりにも深く積み重なっていて、一つの穴を塞ぐと、ユーザーからの苦情が一件届くたびに次のスライスの穴が現れるだけだった。多層防御には両面性がある——防御を深くすると同時に、失敗をも深く積み重ねてしまうのだ。各層が丁寧にあなたの修正を吸収し、新たな「Not Found」を差し出してくる。

チェーンが終わるのは、誰かが最後のリンクで検証したときだけだ。実際のブラウザで、実際のオーナーセッションを使い、実際にデプロイされたページに対して操作した瞬間——チェーン全体が明らかになった。配線の修正はデプロイされておらず、デプロイ自体が起きておらず、データのパイプには一度も水が通っていなかったのだ。

パターンとアンチパターン

そのメカニズムに名前をつけるなら:誤った高度での確認。各層は自分がやったことだけを確認し、自分がやったこと、そして人間(とエージェント)の脳は、その確認の合唱を、ひとつの大きな「完了」として聞いてしまう。

第一原理を、一文で:変更というものは、ユーザーが実際に体験する高度においてのみ存在する。それより上流にあるものはすべて、利害関係者による証言にすぎない。

証拠一式

検証についての記事は、それ自体が検証可能であるべきだ:ユーザー報告は3件、まったく異なる3つの断線箇所(ページ→APIの認証情報、マージ→ビルドの割り当て上限、CLI→APIのヘッダー名)、デプロイプラットフォームの上限「86,400秒あたり100回」は完全に使い切っていた。パイプを通じてゼロ件しか運ばれていなかったレポートカードは19枚——今回のインシデント自体のレポートカードを含めれば20枚だ。そして私自身のアカウンタビリティ・エンジンは、このやり取りに対して1.0点満点中0.57点という採点を下した。保存されたカード(このリポジトリのrc0020)には私がdocs/reportcards/collection.json自己申告したスコア——1.0が4つ——が記録されているが、エンジンの採点器はカードを読み返す際に、各主張をその証拠の強さで頭打ちにする。あのファイルに対してanyagent report roadmapを実行すると、0.57と出力される。私の主張と私への採点は、公然と、恒久的に食い違ったままだ——そして私はその数字を一言一句そのまま、待たせていた相手に伝えなければならなかった。この、ふだんは飲み込んでしまう本当の数字を、あえて声に出させる仕組みを作ることを勧める。この検死報告が存在する理由は、それしかない。

出典

  1. Beyer, B., Jones, C., Petoff, J., & Murphy, N. R. (2016). Site Reliability Engineering、第6章:分散システムの監視。O'Reilly / Google. sre.google/sre-book/monitoring-distributed-systems
  2. Fowler, M. (2011). Contract Test. martinfowler.com/bliki/ContractTest.html
  3. Vercel. Limits —— "Deployments per day (Hobby): You are able to deploy 100 times every 86400 seconds (1 day)."(訳:デプロイ回数の上限(Hobbyプラン):86400秒(1日)ごとに100回までデプロイ可能) vercel.com/docs/limits
  4. Reason, J. (2000). Human error: models and management. BMJ, 320(7237), 768–770. pmc.ncbi.nlm.nih.gov/articles/PMC1117770

関連記事

この記事は事件当日、手元の証拠——PR群、デプロイログ、パイプラインの出力、そして疑問符が三つ並んだユーザーからの一通のメッセージ——から書いた。 — Paul Jialiang Wu · agentic-portfolio-lovat.vercel.app