0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIにエラーを直させるだけでは身につかない——失敗を「仮説検証」に変える実装学習法

0
Posted at

はじめに

生成AIを使って開発していると、エラーの解決は驚くほど速くなります。

ログを貼り付けて、

このエラーを直してください

と頼めば、修正案が返ってきます。

そのコードを適用し、テストが通れば、作業としては完了です。

しかし、この進め方を繰り返していると、ある問題に気づきます。

コードは完成しているのに、自分の中に判断基準が残らない。

同じようなエラーが出るたびに、再びログをAIへ渡すことになります。

これは、AIを使っていること自体が問題なのではありません。

問題は、エラーの解決過程が、

失敗
  ↓
AIへ送る
  ↓
修正コードを受け取る
  ↓
成功

という一本の線になっていることです。

この流れでは、途中にあるはずの、

  • 何が観測されたのか
  • どの原因が考えられるのか
  • 何を調べれば原因を区別できるのか
  • なぜその修正が有効だったのか

という思考が省略されます。

そこで現在は、エラーを単なる修正対象ではなく、仮説検証の題材として扱うようにしています。

この記事では、生成AIを使いながらも、自分の実装力を残すために行っている学習方法を紹介します。

エラー解決とデバッグ学習は別の作業

最初に分けて考えたいのは、次の二つです。

エラーを消すこと
デバッグできるようになること

この二つは似ていますが、同じではありません。

例えば、次のようなエラーが出たとします。

AssertionError: expected 9 rows, got 8

このエラーを消すだけなら、テスト側の期待値を8に変更すれば終わるかもしれません。

しかし、本当に確認すべきなのは、

なぜ9件あるはずだと思っていたのか
なぜ実際には8件になったのか
8件が正しいのか、9件が正しいのか

です。

期待値を変更してテストが通っても、処理の途中でデータを1件失っている可能性があります。

反対に、実装を修正して9件へ戻しても、仕様変更によって正しい件数が8件になっている可能性があります。

つまり、テストが通ることと、仕様が正しく実装されていることは同じではありません。

AIは「テストを通す」ことには非常に強い一方で、どちらの状態が本来正しいのかは、与えられた情報から推測するしかありません。

ここが、人間側に残る重要な学習部分です。

まず「事実」と「解釈」を分ける

エラーが出たとき、最初に行うのは原因を考えることではありません。

まず、確認できている事実だけを書き出します。

例えば、次のような状況があったとします。

期待した行数は9件
実際の出力は8件
例外は発生していない
入力ファイルは存在する
処理は終了コード0で完了した

ここまでが観測された事実です。

一方で、

入力データが欠けている
フィルタが厳しすぎる
重複除去で1件消えた
テストが古い

というのは解釈、つまり仮説です。

この二つを混ぜてしまうと、最初に思いついた原因へ引っ張られます。

特にAIは、もっともらしい説明を非常に自然に生成します。

そのため、私はAIへ依頼するときも、次のように分けます。

以下のログから、まず確認できる事実だけを列挙してください。
原因の推測はまだ行わないでください。

次に、

列挙した事実から考えられる原因仮説を、
互いに区別できる形で示してください。

と依頼します。

この一段階を挟むだけで、AIの回答を「答え」ではなく「調査候補」として扱いやすくなります。

良い仮説は、間違っていることを確認できる

原因候補をたくさん出すだけでは、デバッグは進みません。

重要なのは、それぞれの仮説が反証可能になっていることです。

例えば、

何らかの理由でデータが欠けた可能性がある

という仮説は、曖昧すぎて検証できません。

これを次のように分解します。

仮説A:
入力ファイルにもともと8件しか存在しない

仮説B:
入力には9件あるが、フィルタ処理で1件除外される

仮説C:
フィルタ後は9件あるが、重複除去で1件統合される

仮説D:
実装は正しく8件を出力しており、テストの期待値が古い

それぞれについて、確認方法を考えます。

Aの確認:
入力直後の件数を数える

Bの確認:
フィルタ前後の件数と除外理由を出力する

Cの確認:
重複除去前後の件数とキーを比較する

Dの確認:
仕様書や過去の生成物から期待件数の根拠を確認する

ここで重要なのは、仮説が「正しそうか」ではありません。

どの観測結果が出れば、その仮説を捨てられるかです。

AIには次のように依頼できます。

それぞれの仮説について、

- 正しい場合に観測されること
- 間違っている場合に観測されること
- 最小の確認方法

を示してください。

この使い方をすると、AIは修正コードの生成器ではなく、実験計画を作る補助役になります。

最も情報量の多い確認から行う

仮説が複数ある場合、上から順番に試す必要はありません。

できるだけ多くの仮説を一度に区別できる確認から行います。

先ほどの例であれば、いきなりコードを変更するより、処理段階ごとの件数を出力する方が有効です。

入力直後: 9件
フィルタ後: 8件
重複除去後: 8件
出力時: 8件

この結果が得られれば、原因はフィルタ処理付近に絞られます。

入力ファイルや重複除去を先に詳しく調査する必要はありません。

私はAIに対して、単に確認手順を出させるのではなく、次のように依頼しています。

原因候補を最も効率よく切り分けるために、
情報量が多い確認から順番に並べてください。

各確認によって、
どの仮説を除外できるかも示してください。

この考え方は、プログラムのデバッグだけではなく、環境構築や外部API、データ処理の調査でも役立ちます。

修正前に「何が起きるはずか」を予測する

AIが修正案を出したとき、すぐに適用しないようにしています。

先に、修正後に何が変わるはずかを予測します。

例えば、AIが次の修正を提案したとします。

一時ファイルへ保存し、
検証成功後に正式ファイル名へ変更する

この場合、修正によって期待される変化は次の通りです。

正常終了時:
正式ファイルが作成される

途中終了時:
正式ファイルは作成されない
.part ファイルだけが残る、または削除される

検証失敗時:
正式ファイルへ昇格しない
失敗理由が記録される

この予測を書いてから修正を実行すると、結果を「動いた・動かなかった」だけで判断しなくなります。

確認すべき対象が明確になります。

私はAIにも次のように質問します。

この修正を適用した場合に、
正常系と異常系で観測されるはずの変化を示してください。

まだコードは変更しないでください。

この「変更前の予測」があることで、実装結果を検証する力が鍛えられます。

成功したテストだけでは、原因は証明できない

修正後にテストが通ると、その修正が正しかったように見えます。

しかし、一つのテストが通っただけでは、原因を正しく理解したとは限りません。

例えば、次のテストが通ったとします。

def test_output_has_eight_rows():
    result = build_table()
    assert len(result) == 8

このテストは、8件の出力が得られることしか確認していません。

なぜ8件なのかは分かりません。

フィルタ処理が正しく1件を除外した場合も、別のバグによって偶然1件失われた場合も通ります。

そのため、結果だけではなく、因果関係を確認するテストを追加します。

def test_invalid_row_is_filtered_out():
    rows = [
        {"id": 1, "valid": True},
        {"id": 2, "valid": False},
    ]

    result = filter_rows(rows)

    assert result == [{"id": 1, "valid": True}]

さらに、境界条件も確認します。

def test_valid_rows_are_not_removed():
    rows = [
        {"id": 1, "valid": True},
        {"id": 2, "valid": True},
    ]

    result = filter_rows(rows)

    assert result == rows

一つ目のテストだけでは、

除外すべきものを除外できる

ことしか分かりません。

二つ目を追加することで、

残すべきものまで除外していない

ことを確認できます。

AIにテストを作らせるときも、単に「テストを追加して」とは頼みません。

修正内容を確認するテストだけでなく、
修正によって壊れる可能性がある反対側の挙動もテストしてください。

と伝えます。

これは、実装に都合のよいテストだけが作られることを防ぐためです。

「反実仮想」を考えると理解が深まる

修正が成功した後に、私は次の問いを考えます。

この修正をしなかった場合、どの条件で再び失敗するか

例えば、一時ファイル方式を導入した場合なら、

直接正式ファイルへ保存したままだと、
どのタイミングで不完全な成果物が残るのか

を考えます。

これは反実仮想、つまり「別の実装だったら何が起きるか」を考える作業です。

AIには次のように依頼できます。

今回採用した実装を使わなかった場合に起こり得る失敗を、
具体的な実行順序で説明してください。

例えば、次のような説明が得られます。

1. 正式ファイル名で書き込みを開始する
2. データの半分まで保存される
3. ネットワークが切断される
4. 処理は失敗する
5. 不完全なファイルだけが正式名で残る
6. 次回処理がそのファイルを正常な入力として誤認する

ここまで理解できると、一時ファイルは単なる定番テクニックではなく、

不完全な状態を正式な状態として公開しないための設計

だと分かります。

コードの記法ではなく、設計原理として理解できるようになります。

エラーを「局所的な失敗」で終わらせない

一つのバグを修正したら、その問題がほかの場所にも存在しないか確認します。

例えば、不完全なファイルが残る問題を発見した場合、問題は一つのダウンロード関数だけとは限りません。

同じ設計上の弱点が、

  • キャッシュ生成
  • 設定ファイル更新
  • データベース書き込み
  • レポート生成
  • モデル保存

にも存在する可能性があります。

そこで、修正後に次のようにAIへ質問します。

今回の問題を、個別のバグではなく設計上のパターンとして一般化してください。

同じ種類の問題が起こり得る箇所を、
このプロジェクト内で探す観点も示してください。

例えば、今回の問題は次のように一般化できます。

複数段階で生成される成果物を、
完了確認前に正式な状態として公開している

この一般化ができると、別のコードでも同じ問題を発見しやすくなります。

実装学習において重要なのは、一つの修正方法を覚えることより、再利用できる問題の見方を獲得することです。

解決できない状態を正しく表現する

AIを使っていると、何とか成功状態へ持っていきたくなります。

しかし、実際の開発では、現在の情報だけでは解決できない問題があります。

例えば、

必要な入力ファイルが存在しない
外部サービスが停止している
ダウンロードしたファイルが破損している
実行環境に必要なツールがない
仕様上の正解が定義されていない

といった場合です。

このとき、無理に処理を成功させるより重要なのは、

どこまで確認できたか
何が不足しているか
何がそろえば再開できるか

を明確にすることです。

例えば、次のような状態を出力します。

classification:
required_input_missing

confirmed:
- 出力先ディレクトリは作成済み
- ダウンロード処理は開始された
- 取得ファイルは検証に失敗した

blocked_by:
- 正常な入力ファイルが存在しない

next_condition:
- 検証を通過した入力ファイルが配置されること

これは失敗を隠すための記録ではありません。

失敗を再現可能で、再開可能な状態にするための成果物です。

AIには、成功を強制するのではなく、次のように依頼します。

現在の情報だけで安全に完了できない場合は、
無理に成功扱いにしないでください。

確認できた事実、未確認事項、ブロック要因、
再開条件を記録してください。

このようにすると、AIがもっともらしい成功報告を作ることを防ぎやすくなります。

AIの回答も仮説として扱う

生成AIから返ってきた説明が分かりやすいと、それを正解として受け入れたくなります。

しかし、AIの説明も一つの仮説です。

そのため、重要な判断では次の三つを分けます。

AIが説明したこと
コードから直接確認できること
実行結果として観測できること

例えばAIが、

このエラーは依存関係のバージョン不一致が原因です

と説明しても、実際にバージョンを確認し、条件を変えたときにエラーが消えるまでは確定していません。

私はAIに次のような形式で回答させることがあります。

以下を区別して回答してください。

- ログから確実に言えること
- 可能性が高い推測
- 追加確認が必要なこと

この形式にすると、断定的な説明へ引っ張られにくくなります。

修正履歴ではなく、判断履歴を残す

Gitにはコードの変更履歴が残ります。

しかし、コードだけでは、

なぜこの実装を選んだのか
どの代替案を捨てたのか
どの失敗を防ぎたかったのか

までは分からないことがあります。

そこで、重要な問題については短い判断記録を残します。

## 発生した問題

途中終了したダウンロードファイルが、
正式なファイル名で残ることがあった。

## 観測した事実

- ファイルは存在した
- サイズは0より大きかった
- 検証ツールでは破損と判定された
- ダウンロード処理は正常終了していなかった

## 原因

完了確認前から正式ファイル名へ直接書き込んでいた。

## 採用した方法

`.part` ファイルへ書き込み、
検証成功後のみ正式名へ変更する。

## 採用しなかった方法

起動時に壊れたファイルを削除する方法。

## 採用しなかった理由

ほかの処理が不完全なファイルを読む時間が残るため。

## 追加した確認

- 正常終了時に正式ファイルが作られる
- 中断時に正式ファイルが作られない
- 検証失敗時に昇格しない

この記録の価値は、未来の自分がコードを理解しやすくなることだけではありません。

自分がどのように判断したかを文章化することで、理解の曖昧な部分が見つかります。

私が実際に使っている流れ

現在は、エラーが発生したときに次のように進めています。

1. ログと再現条件を保存する
2. 観測した事実だけを書き出す
3. 原因仮説を複数作る
4. 各仮説を区別できる確認方法を考える
5. 情報量の多い確認から実行する
6. 修正前に、修正後の挙動を予測する
7. 最小の変更を適用する
8. 正常系と異常系の両方をテストする
9. なぜ直ったかを説明する
10. 同じ設計上の問題がほかにないか探す
11. 判断理由を短く記録する

AIは、この各段階で利用できます。

ただし、

原因を決定する
正しい仕様を決める
成功とみなす条件を決める

ところまで自動的に任せないようにしています。

AIには候補を増やしてもらい、確認方法を考えてもらいます。

最終的な判断は、コード、テスト、ログ、仕様を見て行います。

まとめ

生成AIを使えば、エラーを速く直すことはできます。

しかし、実装力として残るのは、修正コードそのものではありません。

残るのは、

  • 事実と解釈を分ける力
  • 複数の原因仮説を作る力
  • 仮説を区別する確認方法を考える力
  • 修正前に結果を予測する力
  • テストが何を証明しているか考える力
  • 個別のバグを設計上のパターンへ一般化する力
  • 解決不能な状態を正しく表現する力

です。

エラーをAIに渡して終わるのではなく、AIを使って調査過程を明示する。

そうすると、失敗は単なる作業の中断ではなくなります。

自分の理解と、実際のプログラムの挙動がどこでずれているかを教えてくれる教材になります。

生成AI時代の実装学習では、正解を速く得ること以上に、正解へ到達するまでの検証可能な道筋を作ることが重要だと感じています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?