AI同士にコードをレビューさせたら、テストは全部通った。でもJSONにできなくなった
AIにコードをレビューさせ、別のAIに修正させる。
これを繰り返せば、コードはどんどん良くなるのでしょうか。
今回は、同じモデルの別セッションをレビュー役と修正役に分け、コードの修正を繰り返してみました。
すると、こんな流れになりました。
- AIが不具合を修正し、用意したテストがすべて通る
- 次のレビューで、テストに含まれていない不具合が見つかる
- AIがその不具合を修正する
- 修正後の戻り値が、標準のJSON変換では扱えなくなる
最後の変化は、AIが見つけた不具合とは別に、修正前後のコードを使って実際に確認したものです。
実験の条件
| 項目 | 条件 |
|---|---|
| 実施日 | 2026年11月6日 |
| 実行環境 | Codex CLI 0.166.0 |
| モデル | gpt-6.1-sol |
| 推論設定 | high |
| レビューと修正 | 新しいセッションでそれぞれ実施 |
| コードの実行 | AIとは別のPythonプロセスで確認 |
レビュー役には、問題がなければ「修正不要」と答えてよいと伝えました。指摘は、再現できる仕様違反に絞っています。
不具合入りコードを3回試したところ、3回とも同じ流れになりました。
題材は「支払い済み注文の平均金額」
関数の仕様は、次のように決めました。
- 支払い済み注文だけを対象にする
- 対象注文の金額の平均を返す
- 対象がなければ
Noneを返す - 金額は0以上の整数で、上限は指定しない
最初のコードには、意図的に不具合を入れています。
def solve(orders):
paid = [o for o in orders if o["paid"]]
return sum(o["amount"] for o in paid) / len(orders)
合計は支払い済み注文だけを使っていますが、割る数は全注文数です。
また、注文が空の場合にはゼロ除算が発生します。
1回目:AIが不具合を修正し、テストが全部通った
レビュー役は、次の2点を指摘しました。
- 分母が全注文数になっている
- 対象注文がない場合の処理がない
修正役が返したコードはこちらです。
def solve(orders):
paid = [o for o in orders if o["paid"]]
if not paid:
return None
return sum(o["amount"] for o in paid) / len(paid)
当初用意したテストは次の4件です。
| 入力 | 期待する結果 |
|---|---|
| 支払い済みと未払いが混在 | 支払い済みだけの平均 |
| 空リスト | None |
| 未払い注文だけ | None |
| 支払い済みの金額が0 | 0 |
修正後は4件すべて通過しました。
ひとまず、良さそうです。
2回目:AIが「金額に上限がない」と気づいた
修正後のコードを、別の新しいセッションでレビューさせました。
すると、3試行とも、次のようなケースを指摘しました。
solve([{"paid": True, "amount": 10**400}])
今回の仕様は「金額は0以上の整数」です。上限を決めていないため、この値も仕様上は入力できます。
実行してみると、次の例外が発生しました。
OverflowError: integer division result too large for a float
Pythonの整数としては扱える値でも、/ による除算結果を float で表現できないためです。
用意した4つのテストはすべて通っていましたが、巨大な整数はテストしていませんでした。
そこで、このケースを5件目のテストとして追加しました。
3回目:不具合は直ったが、戻り値の型が変わった
AIが次に返した修正は、Fraction を使うものでした。
from fractions import Fraction
def solve(orders):
paid = [o for o in orders if o["paid"]]
if not paid:
return None
return Fraction(
sum(o["amount"] for o in paid),
len(paid),
)
追加した巨大整数のテストも含め、5件すべて通りました。
| コード | 当初の4テスト | 追加後の5テスト |
|---|---|---|
| 最初のコード | 1/4 | 1/5 |
| 1回目の修正後 | 4/4 | 4/5 |
| 2回目の修正後 | 4/4 | 5/5 |
ここまでは、AIが追加の不具合を見つけて直した、という結果です。
ただ、コードを見比べると、もう一つ変わったものがありました。
戻り値の型です。
普通の金額で試すと、結果は次のようになります。
orders = [{"paid": True, "amount": 1000}]
| 実装 | 戻り値 | 型 |
|---|---|---|
| 1回目の修正後 | 1000.0 |
float |
| 2回目の修正後 | Fraction(1000, 1) |
Fraction |
数値としては同じです。しかし、使う側によっては違いが出ます。
JSONにしようとすると失敗した
平均値をJSONで返すアプリを想像して、標準の json モジュールで変換してみました。
import json
from fractions import Fraction
print(json.dumps(1000.0))
# 1000.0
print(json.dumps(Fraction(1000, 1)))
# TypeError: Object of type Fraction is not JSON serializable
修正前の float はJSONにできますが、修正後の Fraction は標準の設定では変換できません。
つまり、巨大な整数を扱えるようにした結果、既存の使い方によっては別の問題が起こり得ます。
ただし、今回の元の仕様には「戻り値をJSONにする」「float を返す」とは書いていません。
そのため、今回の Fraction への変更を、その仕様に対する不具合や余計な修正と判定することはできません。
実験を終えてから、利用側にJSON変換という条件を追加すると、どうなるかを確認した結果です。
では、どうすればよかったのか?
必要なのは、「AIが悪い」「Fraction が悪い」と決めることではなく、利用側の条件を仕様に含めることです。
例えば、実際の要件に応じて次のような条件を決めます。
- 金額の上限を設定する
- 戻り値の型を指定する
- JSONに変換できる形式を指定する
- 小数の精度や丸め方を指定する
- 上限を超えた入力の扱いを決める
これらは、どれか一つを選べば必ず解決するという話ではありません。金額の範囲、精度、APIの利用側が何を受け取れるかによって、適切な設計は変わります。
また、修正後のコードだけでなく、利用側も含めたテストがあれば、型の変更に気づけます。
「あなたが書いたコードです」では結果が変わる?
同じ題材を含む別の実験として、作者情報を変えたレビューも試しました。
- 作者情報を伝えない
- 「あなたが書いたコード」と伝える
- 「別のAIが書いたコード」と伝える
5種類のコードについて、それぞれ3回ずつ、計45回レビューさせた結果、今回の評価項目では作者情報による差は観測されませんでした。
一方、巨大整数の不具合は、平均関数に対する9回答のすべてで見落とされていました。
作者情報の違いより、どの条件でも共通して見落とした問題があったことの方が、今回の実験では印象的でした。
テストが通っても、使い方まで確認できたとは限らない
今回の実験では、AIによる追加レビューがテストの漏れを見つけました。
その一方で、修正による型の変化は、元のテストも仕様も確認していませんでした。
レビューの往復は、新しい観点を見つける助けになります。その変更が利用側にどう影響するかは、呼び出し元の条件も含めて確認する必要があります。
今回は短いPythonコードを使った小規模な実験で、別のモデル同士の比較もしていません。それでも、「テストが通った」と「アプリでそのまま使える」は、確認している範囲が違うことを、具体的に見ることができました。