私は今、AIエージェントに実装・テスト・レビューのかなりの部分を任せながら、GitHubに仕様や実行結果を残して、数理最適化のPoCを進めています。
その開発で、開発用データでは動いていた仕組みを評価用データへ持っていったところ、
48試行中48試行が、ソルバーに届く前に停止
しました。
原因は、AIが作った数理最適化モデルの性能ではありませんでした。
入力に足りない情報を、開発用リポジトリの中から暗黙に補っていたことが原因でした。
今回のバグ自体は、AIエージェント特有のものではありません。人間だけで開発していても起こり得ます。
ただ、AIエージェントにリポジトリ単位の実装やテストを広く任せるなら、こうした「人間や開発環境だけが知っている前提」は減らしたい。
この記事では、その具体例を書きます。
30秒でいうと
- 開発に使っていない16件の評価用問題データを、3種類の評価条件で実行した
- 合計48試行すべてが、モデル構築より前で停止した
- 開発時は、JSONに足りない設備の初期状態をファイル名から暗黙に探して補っていた
- 同じJSONでも、ファイル名を変えると壊れた
- 修正後は、入力データと補助情報の対応を明示し、内容や出どころも検証するようにした
- 最初の48試行は失敗記録として残した
- ただし、修正できたことと、評価が成功したことは別
一番の教訓は、
足りない情報を「親切に」暗黙補完すると、開発では便利でも、評価や再現性を壊すことがある
でした。
何を作っていたのか
題材は、公開されている Oven Scheduling Problem(OSP)というベンチマークです。
ざっくり言えば、
- いくつかの仕事を
- 複数のオーブンや設備へ割り当て
- 容量や時間、段取りなどの制約を守りながら
- 良いスケジュールを探す
という数理最適化問題です。
PoCでは、AIを使って数理最適化モデルの開発・修正を進めつつ、実際の計算やテストはローカル環境で実行します。
大まかな流れはこうです。
入力ファイル
↓
parser
入力をモデルが使える形へ変換
↓
数理最適化モデル
↓
solver
↓
スケジュール
↓
独立した検証プログラム
parser は入力ファイルを読み込んで、モデルが扱える形に変換する入口です。
solver は、数理最適化問題を実際に解く計算器です。
最後の検証プログラムは、solverが返したスケジュールが制約を守っているかを、モデルとは別に確認します。
16件 × 3条件 = 48試行
評価には、開発中には使わないよう分けておいた16件の問題データを使いました。
それぞれについて、OSPで定義されている3種類の評価条件を実行します。
この3条件は、単なる同じ処理の3回繰り返しではありません。
主に、
- 処理時間
- 納期遅れ
- 段取りコスト
の重みづけが異なる3種類の目的です。
16件 × 3条件なので、合計48試行です。
48/48で、スタートラインから進めなかった
結果は、48試行すべて
PARSE_ERROR / EXCLUDE_UNOBSERVABLE
でした。
重要なのは、solverが失敗したのではないことです。
入力を読む
↓
エラーで停止
↓
モデル構築に到達しない
↓
solverにも到達しない
↓
検証プログラムにも到達しない
つまり、
「AIが作った数理最適化モデルは、未知の問題に弱かった」
とは言えません。
モデルの良し悪しを試すところまで行っていないからです。
EXCLUDE_UNOBSERVABLE は、この試行では性能比較に必要な結果が観測できず、比較対象から外す、という扱いです。
なので今回の48試行は、性能が悪かった48件ではなく、性能を評価できる状態にすら入れなかった48件でした。
なぜ開発用データでは動いていたのか
原因を追うと、設備の初期状態 InitialState に行き着きました。
開発用の問題データの一つでは、この情報がメインのJSON本体に含まれていませんでした。
ところが以前のparserは、JSONに InitialState がなければ、
「このファイル名なら、リポジトリ内の対応表に補助情報があるはず」
と、別の場所を暗黙に探して補っていました。
実際には、この対応表は manifest.json に保存していました。
実装をかなり単純化すると、こんなイメージです。
# 実際の実装を単純化した疑似コードです
def load_data(json_path):
data = read_json(json_path)
if "InitialState" not in data:
# ファイル名を手がかりに、開発用の対応表から暗黙に補う
data["InitialState"] = dev_settings[json_path.stem]
return data
開発用データには、そのファイル名に対応する補助情報がありました。
だから動いていた。
一方、評価用データでは、その対応関係が実行時の入力として明示されていませんでした。
問題は単に「別ファイルが必要だった」ことではありません。
必要な情報が入力にないのに、その場で止まらず、開発用リポジトリの状態から黙って補っていた
ことでした。
同じJSONなのに、名前を変えると壊れた
原因を確かめるため、失敗した評価データを触り続けるのではなく、開発側で見えている公開データを使って再現しました。
確認したのは3つです。
-
開発用のJSONを、対応する補助情報と一緒に読む
→ 動く -
JSONの中身は1バイトも変えず、別のローカルファイル名へコピーし、補助情報の対応表を明示しない
→ モデル構築前で止まる -
同じJSONに、対応する補助情報の対応表を明示的に渡す
→ 再び動く
変わったのはJSONの中身ではありません。
入力データと補助情報を、どう結び付けるか
だけでした。
これで、開発時にはコード以外のリポジトリ状態に助けられていたことを確認できました。
修正:足りない情報を黙って補わない
修正方針はシンプルです。
裏で勝手に探すのをやめ、
問題データのJSON + 検証済みの補助情報の対応表
を、実行に必要な入力として明示的に扱います。
ここでいう入力契約とは、
実行に必要なファイル・補助情報・それらをどう対応付けるかを明示したルール
くらいの意味です。
考え方を単純化すると、こうです。
# 実際の実装を単純化した疑似コードです
def load_data(json_path, metadata_path):
data = read_json(json_path)
metadata = load_metadata(metadata_path)
verify_identity(data, metadata)
verify_hash(data, metadata)
verify_source(metadata)
return combine(data, metadata)
設備の初期状態は、同じ公開OSPデータセットに含まれる対応するDAT / DZNファイルから取得します。
ただし、何でも好きな補助情報を渡せるようにはしていません。
- JSONがどの公開問題データなのか
- JSON本体のハッシュが一致するか
- 対応するDAT / DZNが正しいか
- DAT / DZNから得た初期状態が一致するか
- 設備IDと初期状態が正しく対応するか
- 補助情報がどの公開データから来たか
を検証します。
既知の解、目的値、下界など、評価結果を漏らし得る情報は、この入力契約には含めません。
そして、必要な情報が足りなければ、
「たぶんこれだろう」
と補わず、その場で止めます。
暗黙のfallbackを、明示的で検証可能な入力契約へ変えた、というのが今回の修正です。
補足:失敗した48試行は、そのまま残した
修正後、最初の48試行を再実行して「成功した評価」に上書きはしませんでした。
48/48は、その時点では実験の入口が壊れていた、という記録として残しました。
修正の確認は、評価には使わない別の確認用データで行っています。
代表ケースでは、
parser
↓
モデル構築
↓
数理最適化ソルバー
↓
スケジュール生成
↓
変更していない検証プログラム
まで一周し、スケジュールが制約を守っていることを確認できました。
ただし、これで評価用48試行が成功したことにはなりません。
それでも、なぜAIエージェントの話として書くのか
今回の原因そのものは古典的です。
AIエージェントを使ったから起きた、と言える証拠はありません。
では、なぜAIエージェントの文脈で書くのか。
私が気にしているのは、原因ではなく、開発をどう運用するかです。
今回、開発用データでは暗黙の補完込みで処理が通っていました。
これは、AIエージェントを使うかどうかに関係なく、
テストが通ったことと、そのテストが開発環境の暗黙前提から独立していることは別
という問題です。
AIエージェントにリポジトリ単位の実装やテストを広く任せるなら、なおさら、
「この前提は人間が覚えている」
「リポジトリを読めば分かる」
に依存する運用にはしたくありません。
人間でも、AIエージェントでも、テストでも、
実行に何が必要なのかを同じ形で確認できる
方が扱いやすい。
最近は、コーディング用のAIエージェントも、一つの関数を書く道具から、リポジトリ全体を長い工程で作る方向へ広がっています。
たとえば NL2Repo-Bench では、自然言語の要件文書と空の作業環境だけをAIエージェントへ渡し、設計、依存関係の管理、複数ファイルの実装を経て、インストール可能なPythonライブラリを作らせています。開発中にはテストケースを見せず、完成したリポジトリを元プロジェクトのpytestで評価します。論文では、早すぎる作業の打ち切り、全体の一貫性の崩れ、ファイル間の依存関係の脆さ、長期的な計画不足などが主な失敗モードとして分析されています。
- NL2Repo-Bench: Towards Long-Horizon Repository Generation Evaluation of Coding Agents
https://proceedings.mlr.press/v306/ding26j.html
また、別の最近の研究では、AIが生成したコードについて、「機能的に正しく動くこと」と「実行に必要な依存関係を正しく指定できること」は別の評価軸だと指摘されています。
今回の失敗はライブラリ依存の問題ではありません。しかし、
コードそのものだけでなく、実行に必要な前提まで明示できて初めて再現可能になる
という問題意識には、近いものを感じました。
- Code That Works, Environments That Don't: Measuring Environment Reproducibility in AI-Generated Software(arXiv preprint)
https://arxiv.org/abs/2610.00425
今なら、評価前に一つ追加する
今回の経験を踏まえると、次に同じような実験をするなら、本番の評価へ入る前に一つ確認を足します。
開発用リポジトリの便利な状態から切り離し、評価時と同じ形の入力だけで、入口から最後まで一度通す。
これは今回実施した手順そのものではなく、今回の失敗から得た予防策です。
開発用テストが緑でも、評価時と同じ入力境界で動くとは限らない。
その確認を一度入れるだけでも、今回のような48/48の失敗は、もっと手前で見つけられた可能性があります。
持ち帰ったこと
今回の経験から、私は次の3点を強く意識するようになりました。
-
足りない情報を黙って補わない。
便利なfallbackが、開発環境への暗黙依存を隠すことがある。 -
評価前に、評価と同じ入力境界で一度通す。
モデルだけでなく、入力の組み立て方も評価対象になる。 -
前提を、人間・AI・テストが共通に確認できる形へ出す。
リポジトリを読めば分かる、ではなく、入力契約として検証できるようにする。
何をまだ言えないか
今回の修正から直接言えるのは、
開発時だけ成立していた暗黙の依存を取り除き、明示的な入力契約で再実験できる技術状態へ戻した
ところまでです。
これだけでは、
- 修正後の評価用データで良い結果が出た
- AIが人手の数理最適化モデル開発を上回った
- この方法が実工場でもそのまま使える
- AIエージェントは一般に暗黙の依存を増やす
とは言えません。
まとめ
今回の48/48の失敗は、ソルバーや最適化アルゴリズムの問題ではありませんでした。
足りない入力情報を、開発用リポジトリの状態から暗黙に補っていたことが、評価で露出した問題でした。
修正したのは、モデルではなく入力の境界です。
AIエージェントに任せる範囲が広がるほど、こうした前提を人間の記憶やリポジトリの偶然に残さず、
人間・AI・テストの誰でも同じように確認できる形にする
ことが重要になるのではないかと思っています。
参考
- Benchmark instances and models for the Oven Scheduling Problem
https://doi.org/10.5281/zenodo.14007438 - OSP formal specification
https://doi.org/10.1007/s10601-023-09347-2 - Code That Works, Environments That Don't: Measuring Environment Reproducibility in AI-Generated Software
https://arxiv.org/abs/2610.00425 - NL2Repo-Bench: Towards Long-Horizon Repository Generation Evaluation of Coding Agents
https://proceedings.mlr.press/v306/ding26j.html
AI利用について
この記事の構成・文章化にはAIを利用しています。
技術的な事実、数値、時系列、実施済み/未実施の区別については、元のGitHub Issue、実行記録、対象コミットを固定した監査、現在のソースコードへ戻って照合しています。