「各回ごとにコードを丁寧にレビューし、テストがすべてパスしていれば、段階的なチュートリアル全体としても正しくコードが引き継がれているはずだ」── 技術解説やプログラミング教材の連載を執筆したことのある方なら、誰もがそう信じているのではないでしょうか。

しかし、実際に公開を完了していた全4回の技術連載(Perl によるフォームバリデーション構築チュートリアル)を後日通読したところ、第2回で実装した年齢の必須・数値検証、確認用パスワードの依存関係チェック、そしてエッジケースを検証するテストケースが、第3回の完成コードから丸ごと姿を消していました。

連載コードにおける 回をまたぐ後退(サイレントリグレッション) とは、各回単体のレビューでは要件を満たして合格しているにもかかわらず、前の回で実装した入力検証やテストケースが後続回の完成コードで密かに削ぎ落とされてしまう現象を指します。

各回で閉じた局所的なレビューをいくら厳格に繰り返しても、回と回の間の差分や継承関係を照合する仕組みがなければ、この種の後退は構造的にすり抜けてしまいます。必要なのは、プロンプトでの「落とさないで」という予防指示ではなく、前回のテストスイートを次回の完成コードに機械的に実行する決定論的な検収ゲートです。以下、手元の実務ログで発覚した後退事例を起点に、AI コーディングエージェントが過去の検証を落とす背景と、それを防ぐ照合手順の設計を順に見ていきます。

なぜ各回のレビューをすり抜けたのか:局所的レビューの構造的盲点

問題の連載では、第2回で「複雑な入力検証ルールと依存関係の導入」を扱い、第3回で「Chain of Responsibility パターンによるバリデーション処理のオブジェクト指向リファクタリング」を扱っていました。

第3回は、第2回のコードを土台に新しいデザインパターンを適用する回です。各回のレビューは、その回の主題が満たされているかに焦点を当てて単独で通過し、そのまま公開されていました。

しかし、連載の再告知に向けて通読したところ、第2回で実装されていたはずの以下の機能が、第3回のコードから完全に脱落していたのです。

  • 年齢(age)に対する必須検証および数値形式チェック(空文字列や不正文字列でエラーを出す処理)
  • 確認用パスワード(password_confirm)に対する必須チェックと、「パスワード自体の検証に合格した場合のみ確認欄を照合する」という依存関係制御
  • 不正な年齢入力(age='twenty')を検出するためのテストケース(テスト5)

この欠落を修正するために行われたコミットでは、199行の追加(36行削除)が必要となりました。なぜこれほど大きな欠落が起きたのでしょうか。

AI エージェントの内部の判断は外から観測できませんが、新しい抽象化パターンを適用する回で、その回の主題である新構造に注意が集中し、前回の細かな入力検証やエッジケーステストが本質ではない付随物として扱われた、と推測しています。観測できる事実はもう一方にあり、執筆パイプライン側の品質ゲートは「第3回のテーマを満たしているか」という単一ステップの閉じた視点しか持っておらず、第2回の完成コードとの差分や振る舞いの継承関係を照合する工程が構造的に欠落していました。

解説文にも過小主張がありました。コードを復元した際に書き直した拡張例の解説文は、新しいルールを足すときに「手を入れるのは、チェーンを組み立てる関数に1行足すところだけです」としていました。しかし実際には、新しいバリデータを作ってチェーンにつなぐ数行が要ります。そのため復元の10分後に、この記述を「数行だけ」へ訂正するコミット(2行追加・2行削除)を行いました。

発覚した不整合はこれだけではありません。連載全体を通読したことで、次回予告テキストと実際の次回タイトルの食い違い、目次記事の振り返り表と各回本文の内容ズレ、さらには根拠のない「テストケースは指数的に増えていきます」という誇張表現など、回ごとに閉じたレビューの観点には入っていなかった不整合が次々と見つかりました。

以下は、第3回の記事でチェーンを組み立てる関数 build_user_registration_chain に、復元のコミットで加えた差分の抜粋です(記事に埋め込まれた完成コードのうち、確認用パスワードと年齢の部分)。各バリデータのクラス定義と、パスワード強度ルールの分割は省いています。

+    # パスワードにエラーがないときだけ、確認用を検証する
+    my $confirm_required = RequiredValidator->new(
+        field_name    => 'password_confirm',
+        error_message => '確認用パスワードを入力してください',
+        depends_on    => 'password',
     );
 
     my $password_confirm = ConfirmValidator->new(
         field_name    => 'password_confirm',
         error_message => 'パスワードが一致しません',
         confirm_field => 'password',
+        depends_on    => 'password',
     );
 
     my $agreed_terms = BooleanValidator->new(
@@ -330,6 +365,17 @@ sub build_user_registration_chain () {
         error_message => '利用規約に同意してください',
     );
 
+    my $age_required = RequiredValidator->new(
+        field_name    => 'age',
+        error_message => '年齢を入力してください',
+    );
+
+    my $age_number = PatternValidator->new(
+        field_name    => 'age',
+        error_message => '年齢は数値で入力してください',
+        pattern       => qr/\A[0-9]+\z/,
+    );
+
     my $age_range = RangeValidator->new(
         field_name    => 'age',
         error_message => '18歳以上の方のみ登録できます',
@@ -342,9 +388,14 @@ sub build_user_registration_chain () {
         ->set_next($email_format)
         ->set_next($password_required)
         ->set_next($password_length)
-        ->set_next($password_strength)
+        ->set_next($password_upper)
+        ->set_next($password_lower)
+        ->set_next($password_digit)
+        ->set_next($confirm_required)
         ->set_next($password_confirm)
         ->set_next($agreed_terms)
+        ->set_next($age_required)
+        ->set_next($age_number)
         ->set_next($age_range);

+ の行が復元で戻した検証です。確認用パスワードの2つのバリデータには depends_on => 'password' を渡し、パスワード自体にエラーがあるときは確認欄を検証しない、という第2回の依存関係をチェーンの組み立て側に書いています。年齢には必須チェックと数値チェックを、既存の下限チェックの前に挿入しています。

復元後の第3回の記事は、完成コードの末尾で第2回のテストケースを同じ順に流し、次の出力を載せています。テスト1〜5は第2回の完成コードと同じ結果で、テスト5(age='twenty')の不正な年齢も、期待どおり数値のエラーとして返ります。テスト6・7は、未入力の年齢と未入力の確認用パスワードを確かめるために復元時に足したケースです。

=== テスト1 ===
検証成功: 山田太郎 (yamada@example.com)

=== テスト2 ===
検証失敗:
  - age: 18歳以上の方のみ登録できます
  - agreed_terms: 利用規約に同意してください
  - email: メールアドレスの形式が正しくありません
  - name: 名前を入力してください
  - password: パスワードは8文字以上で入力してください

=== テスト3 ===
検証失敗:
  - password: パスワードには大文字を含めてください

=== テスト4 ===
検証失敗:
  - password_confirm: パスワードが一致しません

=== テスト5 ===
検証失敗:
  - age: 年齢は数値で入力してください

=== テスト6 ===
検証失敗:
  - age: 年齢を入力してください

=== テスト7 ===
検証失敗:
  - password_confirm: 確認用パスワードを入力してください

第2回と同等の振る舞いを維持するためには、単に新しい抽象ハンドラを1つ呼ぶだけでなく、各入力項目ごとの個別ルールや依存関係(パスワードが有効なときのみ確認欄をチェックするなど)を明示的に組み立てる必要があります。局所的なレビューでは「新しいハンドラが動いていること」ばかりに目が行き、こうした前提条件の脱落を簡単に見落としてしまうのです。

「One step forward, two steps back」:回をまたぐ後退の技術的背景

この問題は、プロンプトの書き方が悪かったという個別事例にとどまりません。大規模言語モデル(LLM)を用いて段階的な成果物を連続して生成する際に発生する、構造的な限界に根ざしています。

コンテキストドリフトと制約違反の増加

実際のソフトウェア開発における開発者と AI エージェントの相互作用を分析した大規模調査(1,639リポジトリ・20,574セッションの観測研究)1 によれば、エージェントとの不整合エピソードの 90.50% は致命的なシステム破壊ではなく、追加の修正労力とユーザーの信頼損失をもたらしています。そして解決が確認できた不整合エピソードの 91.49% において、開発者による明示的な修正指示が必要となっていました。

同研究では、コーディングエージェントの失敗の主因として「制約違反(constraint violations)」および「不正確な自己報告」が経時的に増加する傾向も示されています。

外部フィードバックのない自己修正の限界

では、プロンプトで「前回の検証ルールやテストを絶対に落とさないでください」と強く指示すれば防げるのでしょうか。結論から言えば、自然言語による予防指示だけでこの問題を解決することはできません。

Google DeepMind による推論能力の検証研究 2 では、コンパイラや実行環境などの客観的な外部オラクルが存在しない状況下でのモデル単体による自己修正(intrinsic self-correction)は、自律的に誤りを修正できないだけでなく、修正の試行によってかえって推論性能を劣化(degrades)させることが実証されています。この研究の対象は推論タスクですが、同じ構図を連載の事例に当てはめると、過去のコードに存在したエッジケースが「現在も維持すべき必須仕様」なのか「新パターン導入に伴って整理された不要な記述」なのかを、モデルが外部情報なしに判断するのは難しいと考えています。

モデル更新やコード生成における回帰の常態化

コード生成 AI の回帰現象を体系的に評価した ReCatcher 論文 3 のベンチマーク実験では、モデルの更新やマージ、ファインチューニングによって、論理的正しさが最大 18% 回帰し、比較したモデル間でインポート欠落の扱いに最大 50% の回帰が生じたことが報告されています。

また、大企業のエンジニアリングリーダー200名を対象とした調査(Lightrun)4 においても、AI が生成したコード変更の 43% が QA やステージング環境を通過した後に本番環境で手動デバッグを必要としており、そのデバッグを要した事例のうち 88% では 2〜3 回の再修正と再デプロイが繰り返されていました。

局所的なレビューを通過したコードが、後続のステップや本番環境において「以前動いていたものを壊す」というサイレントリグレッションは、ここで挙げた研究や調査が示すとおり、AI コーディングで繰り返し報告されているリスクです。

処方箋:前回のテストを回帰センサーとして引き継ぐ照合ゲートの設計

では、連載やチュートリアル、段階的なドキュメント開発において、回をまたぐコード後退をどのように防げばよいのでしょうか。

回帰センサーとしてのテストスイート

Thoughtworks の Birgitta Böckeler 氏は、コーディングエージェントを活用する開発において、テストスイートを「回帰センサー(The test suite as a regression sensor)」として位置づける重要性を提唱しています 5。同氏は、AI 生成コードベースにおける保守性の劣化の兆候として、小さな調整に対して変更されるファイルが増えることや、以前は動いていたものが壊れ始めることを挙げています。このとき、過去のテストスイートを機械的に実行し続けることこそが、AI に対して「意図せず既存の仕様を壊したのか、それとも仕様変更としてテストを更新すべきなのか」を気づかせる客観的なセンサーとして機能します。

段階的チュートリアルのパイプラインに組み込むべき照合ゲートには、具体的に以下の2つの実践手法があります。

  1. 手法1(テストスイートの累積実行): 第 N 回の完成コードに対して、第 N-1 回のテストスイートをそのまま実行する。前回の要件を壊していないかを決定論的に検証する。
  2. 手法2(公開インターフェース・テスト観点の差分照合): 第 N-1 回でテスト対象となっていたパラメータ(age, password_confirm 等)が、第 N 回のコード内や AST(抽象構文木)に存在し続けているかを差分解析で確認する。

これは、ハーネスエンジニアリング 6 が挙げるハーネスの役割のうち、「制約(Constrain)」と「検証(Verify)」に当たります。Böckeler 氏の対比でいえば、プロンプトによる「落とさないで」という予防(ガイド)に頼るのではなく、自動テストの実行(センサー)のフィードバックで正しさを確かめる形です 5。

各回単体レビューと回間回帰照合ゲートの比較

連載記事や段階的コード例の品質を保つには、単体回の要件確認にとどまらず、回をまたぐ連続性をどう検証するかが決定的な分水嶺となります。

比較軸各回単体レビュー(局所検収)回間回帰照合ゲート(連続検収)
検証スコープその回(ステップ)のテーマ・新規コード前回までの完成コードとの累積・差分
判定基準新しい要件を満たしているか新要件を満たし、かつ前回のテストが通るか
主な死角前回の付帯バリデーション・テストのサイレント脱落テストのアサーションが空洞化している場合(後述)
実行主体人間の目視、または AI へのプロンプトによる確認CI / テストランナーによる決定論的自動実行

表が示す通り、各回単体のレビューが「その回の新機能」に最適化されているのに対し、回間回帰照合ゲートは「前回の動作を壊していないか」を機械的に縛ることで、サイレント後退を構造的に排除します。

以下は、CI やコミット前フックで実行可能な、第 N 回のコードに対して前回のテストスイートを機械的に適用する回帰検証スクリプト(scripts/verify-serial-regression.sh)の概念例です。

#!/usr/bin/env bash
set -euo pipefail

# 第 N 回の完成スクリプトと、第 N-1 回のテストスイートのパスを定義
PREV_EPISODE="02"
CURRENT_EPISODE="03"

TEST_FILE="t/episode_${PREV_EPISODE}_validation.t"
TARGET_SCRIPT="lib/MyApp/Episode${CURRENT_EPISODE}.pm"

echo "=== 回間回帰テストの実行 ==="
echo "検証対象コード: ${TARGET_SCRIPT}"
echo "適用テストスイート: ${TEST_FILE} (第 ${PREV_EPISODE} 回のテスト)"

# 環境変数で対象モジュールを差し替え、前回のテストを実行
if TARGET_MODULE="${TARGET_SCRIPT}" prove -v "${TEST_FILE}"; then
    echo "✓ 合格: 第 ${PREV_EPISODE} 回の検証仕様が第 ${CURRENT_EPISODE} 回でも完全に維持されています。"
    exit 0
else
    echo "✗ 失敗: 第 ${CURRENT_EPISODE} 回のコードで第 ${PREV_EPISODE} 回のテストが失敗しました。" >&2
    echo "前回の仕様やエッジケースが脱落していないか確認してください。" >&2
    exit 1
fi

このスクリプトをパイプラインの品質ゲートに組み込むことで、第2回のテストファイル t/episode_02_validation.t が第3回の完成コードに対して自動実行されます。もし age の数値チェックが脱落していれば、テスト5が失敗してスクリプトは即座に非ゼロ終了し、人間のレビューや公開へ進む前に自動検知できます。

このゲートが働くには、前提が2つあります。1つは、記事の Markdown に埋め込まれたコードを、テストから読み込めるファイルとして取り出しておくことです(上のスクリプトは、前回の .t が環境変数 TARGET_MODULE で読み込む対象を切り替える作りであることを前提にしています)。もう1つは、回ごとに API が変わっても前回のテストが動くことです。第3回のように関数やクラスの構成を作り替える回では、前回のテストがそのままでは読み込めません。今回の復元で第2回との同等性を確かめた方法は、第2回のテストケースの入力を新しいチェーンに同じ順に流し、出力を比べることでした。テストを入力と出力の対応だけで書いておけば、内部の構成が変わっても回をまたいで流用できます。

なお、今回の後退は、このゲートではなく通読で見つかったものです。

メリットの裏にあるトレードオフと適用限界

回間の回帰照合ゲートは強力な仕組みですが、すべての連載やドキュメントに過去のテストを無制限に累積させればよいというわけではありません。

教材としての可読性と過剰テストのジレンマ

第一のトレードオフは、教材としての認知負荷の増大です。技術連載の各回には「読者に何を伝えたいか」という明確な主眼があります。新しい概念(デザインパターンや非同期処理など)を初学者に解説する際、本番運用に必要なすべての付帯バリデーションを毎回コード例に残し続けると、本質的な解説コードが埋もれてしまい、教材としての分かりやすさが著しく損なわれます。

Qiita において y0us91 氏が指摘しているように 7、テスト生成が容易になったからといって「変更された事実」だけに反応して無秩序にテストを増やすと、テストスイート自体の保守コストが跳ね上がります。同氏は updateSchema.safeParse({ status: "paused" }) のような型スキーマによる境界保証など、既存の保証機構とテストの責務を切り分けることの重要性を説いています。

連載教材においても同様の分離が必要です。各回の解説コードにおいて教育目的で意図的にバリデーションを削ぎ落とす場合は、「※本章ではパターンの構造を分かりやすく示すため、前回の入力検証の一部を省略しています」と明記し、AI による無自覚な脱落と明確に区別すべきです。

テストアサーションの形骸化リスク

第二の限界は、テストが存在していてもアサーションが空洞化していれば後退を防げないという点です。Böckeler 氏の記事 5 でも警告されているように、結合テストや受け入れテストを増やして見かけのカバレッジを高めても、重要なアサーションが抜けていれば「有効性の誤った安心感」を生むだけです。実際に同氏が挙げた mappers.ts の事例では、カバレッジが高かったにもかかわらずミューテーションテスト(Stryker)で 13 個の変異体が生き残り、重要な変換ロジックが検証されていなかったことが判明しています。

前回のテストを引き継ぐ際は、単にテストファイルの存在を機械的にチェックするだけでなく、核となる不変条件(「この入力に対してこのエラーメッセージが返ること」)が確実に検証されているかを点検しなければなりません。

よくある質問(FAQ)

Q. AI に「前回の仕様を落とさないで」とプロンプトで指示するだけでは防げないのですか?

A. 指示だけでは保証になりません。プロンプトによる指示は生成の前にかける予防で、守られたかどうかは生成物を確かめるまで分かりません。Google DeepMind の研究でも、外部からのフィードバックがない自己修正では性能がかえって劣化することが示されています。前回のテストを実際に実行して合否を判定する検収の仕組みが必要です。

Q. 連載コードですべての過去テストを実行すると、教材としてのシンプルさが失われませんか?

A. 教材コードを複雑化させる必要はありません。各回の完成コードとは別に、前回の振る舞いを保証するテストコードを外部スクリプト(CI 等)として独立して保持し、教育目的で意図的に削る箇所がある場合は「解説のため簡略化している」と明記してテスト側で例外を定義することで、コードの読みやすさと品質保証を両立できます。

まとめ

  • 各回のレビューや単体テストが通っていても、連載全体としてのコードが後退していない保証にはならない。
  • AI エージェントは新パターンの導入やリファクタリング時に、前回の付帯的バリデーションやエッジケーステストを不要なノイズとして削ぎ落とすことがある。
  • プロンプトでの「落とさないで」という予防指示に頼らず、前回のテストスイートを次回のコードに機械的に実行する回帰センサーを導入する。
  • 意図的な教育的簡略化と無自覚な脱落を区別し、回をまたぐ段階的成果物の連続性を決定論的な仕組みで担保する。

AI エージェントを活用した技術ドキュメント・連載教材の作成パイプラインや、コード品質・回帰テストの自動化設計について、ぜひ コンタクトフォーム よりお気軽にご相談ください。

Footnotes

  1. How Coding Agents Fail Their Users Ningzhi Tang et al.、2026年8月31日 (arXiv:2605.29442 v2)。20,574セッションの実開発データ分析。不整合の90.50%が追加労力・信頼損失を生み、91.49%で明示的なユーザー修正指示が必要となる実態。 ↩

  2. Large Language Models Cannot Self-Correct Reasoning Yet Jie Huang, Xinyun Chen et al. (Google DeepMind)、2023年10月3日 (arXiv:2310.01798)。外部フィードバックを伴わない本質的自己修正(intrinsic self-correction)が推論性能を劣化(degrades)させる実証。 ↩

  3. ReCatcher: Towards LLMs Regression Testing for Code Generation Altaf Allah Abbassi et al.、2025年7月25日 (arXiv:2507.19390)。モデル更新やマージにおける論理的正しさ(最大18%)、インポート欠落(最大50%)の回帰実測データ。 ↩

  4. AIに「このバグ直して」って丸投げすると、なぜ半分ハズすのか akira_papa_AI、2026年7月4日。Lightrun「2026 State of AI-Powered Engineering Report」調査引用(AI生成コード変更の43%が本番デバッグを要し、デバッグ事例の88%が2〜3回修正を反復)。推測ではなく観測に基づく回帰テストファースト原則。 ↩

  5. Maintainability sensors for coding agents Birgitta Böckeler (Thoughtworks)、2026年5月27日。保守性(internal quality)劣化シグナル、テストスイートを回帰センサー(The test suite as a regression sensor)として位置づける設計論。Stryker 事例。 ↩ ↩2 ↩3

  6. ハーネスエンジニアリングで人間のコードレビューをやめる Akatsuki (@theaktky)、2026年3月24日 (2026年3月30日更新)。ハーネスの4役割(Constrain, Inform, Verify, Correct)とオシレーション制御。 ↩

  7. AIがテストを書ける時代、「何をテストしないか」をどう決めるか y0us91、2026年9月19日。仕様変更時の過剰テスト増大リスクと、型スキーマによる境界保証・テスト責務の分離。 ↩