はじめに
個人研究として、AIエージェントを使いながら数理最適化のPoCを進めています。
AIに計算方法の改善案を考えさせ、実際に計算して確かめ、結果が悪ければ考え直させる。そうした作業を、どこまで人手を介さずに進められるか試していました。
ところが、そのために開発していた仕組みが、なかなか思うように動きませんでした。
不具合が見つかる。原因を調べて修正する。テストを追加し、独立したレビューでも確認する。しかし、次の実験では別のところで停止してしまう。
実は、以前の実験でも、検査や管理の手続きが複雑になりすぎたのではないかと反省していました。その経験を踏まえ、今回は本来の研究を進めることを優先していたつもりでした。
それでも、途中停止を繰り返しました。
なぜ、個別の不具合を直しているのに、本来の実験を最後まで進められなかったのでしょうか。
最終的には、実行する仕組みを作り直し、必要な一連の処理を最後まで実行できました。
この記事では、その過程で何が起き、実際にプログラムのどこを修正したのかを説明します。
こうしたAIエージェントの実行環境や検証の仕組みを設計する取り組みは、Harness Engineering(ハーネスエンジニアリング)と呼ばれています。
今回は、数理最適化のアルゴリズム自体ではなく、AIエージェントに仕事を進めさせるための実行基盤に焦点を当てます。
1. そもそも、何を実行したかったのか
数理最適化は、与えられた条件を守りながら、できるだけ良い解を計算で探す技術です。
例えば、トラックに積み込む荷物の組合せを考えます。
荷物にはそれぞれ重さと価値があり、トラックに積める重量には上限があります。その上限を守りながら、できるだけ価値の高い荷物の組合せを探す問題です。なお、トラックの例は説明のためのもので、実際の研究対象とは異なります。
計算プログラムに荷物の一覧や重量制限を渡すと、どの荷物を積むかという結果が返ってきます。
このように、数理最適化の計算を行うプログラムをSolverと呼びます。
ただし、今回の研究でAIに期待していたのは、Solverを一度動かすことだけではありません。
例えば、AIが「Solverの設定を変更すると、もっと良い結果が得られるのではないか」と考えたとします。
その場合、実際に設定を変更して計算し、従来の結果と比較する必要があります。
良くなっていれば採用を検討し、悪くなっていれば元の方法を維持する。必要に応じて、AIが別の方法を考え直すこともあります。
つまり、実験には次のような一連の仕事が必要でした。
計算方法を考える → 実際に試す → 結果を確認する → 次の判断をする
ここで問題になったのは、AIの判断を実際の計算につなぎ、その結果を再びAIへ返す仕組みです。
1.1 誰が、何を担当していたのか
今回の実験では、一つのAIがすべての作業を行っていたわけではありません。
複数のAIエージェントと、通常のプログラムが役割を分担していました。
| 登場するもの | 正体と役割 |
|---|---|
| 人間 | 実験の目的や範囲を決め、実行を承認する |
| Orchestrator | Codexによる進行担当。全体の実行順序を管理する |
| Builder | 別のAIエージェント。技術的な改善案や次の作業を考える |
| Python実行器 | 入力や予算を確認し、実際の計算を起動・記録する |
| Solver | 数理最適化の計算を行うプログラム |
| Evaluator | Solverが返した結果を、別途定めた条件で検査するプログラム |
| Independent Oracle | Builderとは別のAIエージェント。実装や実験証拠を独立して監査する |
ここでいうIndependent Oracleは、計算の「正解」を返すプログラムではなく、実装や実験記録を独立して確認する監査役の名称です。
少し登場人物が多いので、実際の処理の流れを図にします。
図1:今回の実験を進める主な役割と処理の流れ。独立監査は、毎回のSolver実行の直後に必ず行われるわけではありません。
1.2 何をきっかけに、次の処理へ進むのか
この図で、全体の進行を管理しているのはOrchestratorです。
まず、人間が実験を承認し、Orchestratorに開始を指示します。
Orchestratorは実験の条件を確認し、必要に応じてBuilderを呼び出します。
Builderが改善案を考えると、その回答はGitHubに記録されます。GitHubは、エージェント間の判断や作業結果を残す場所として使っています。
Orchestratorはその記録を確認し、Pythonの実行器へ計算を要求します。
Pythonの実行器は、入力や実行予算などを確認してからSolverを起動します。Solverが返した結果は保存され、Evaluatorによる検査も行われます。
その結果をOrchestratorが取得し、必要なら再びBuilderを呼び出します。
Builderは、新しい結果を見て、改善案を採用するか、別の方法を試すかといった判断を行います。
ここで重要なのは、Builderが直接Solverを操作しているわけではないことです。
Builderは何を試したいかを考えます。Orchestratorが仕事の順序を管理し、Pythonの実行器がSolverを起動します。
また、Builderが考え続けているわけでもありません。例えば計算結果が出た後、Orchestratorがその結果を基に新しい依頼を発行し、別のAIの処理として再び判断を求めます。
今回の実験では、そうしたAIの判断と実際の計算を組み合わせて、一連の仕事を進めようとしていました。
このように、AIが考えたことを実際の処理につなぎ、結果を次の判断へ戻す仕組みを、この記事ではHarnessと呼びます。
2. 前の実験で、すでに反省していたこと
以前の実験では、開発中には動いていた処理が実際の評価で停止したり、計算結果の保存や読み戻しで問題が見つかったりしました。
こうした問題を防ぐため、入力や実行条件の確認、結果の保存、独立した検査などを重視してきました。
研究では、計算結果が得られればそれでよいわけではありません。
どの条件で計算し、どのような結果が得られたか、後から確認できることも重要です。
そのため、必要な検査を設けることには理由がありました。
一方で、検査の周辺にも、次第に多くの確認作業が必要になっていました。
例えば、記録を確認するだけでなく、確認した証拠を保存し、その証拠についてさらに別の確認を行う、といった具合です。
そこで、以前の実験を振り返った際には、研究結果の信頼性を守る検査と、その周辺で増えた管理作業を分けて考える必要があると整理していました。
手続きを厳密にすることは重要です。しかし、厳密さを高めること自体が目的になってはいけません。
次の実験では、必要な検査を残しながら、本来の研究を進めることを優先したいと考えていました。
ところが、それでも問題が続きました。
3. 実際には、どこで止まっていたのか
ここでは、実際に起きた三つの問題を紹介します。
これらは一度の実験で連続して起きたものではありません。問題を修正して次の実験へ進むと、別の工程で新しい問題が見つかる、という経過でした。
図2:代表的な三つの停止。点線の間には、ほかの修復や実験もありました。
3.1 AIの回答は届いたのに、次の計算を始められなかった
最初に問題になったのは、処理の順番でした。
あるタイミングで、Builderが次に行う作業を回答しました。
Orchestratorはその回答を受け取り、Pythonの実行器に次の計算を要求しようとしました。
しかし、本来はBuilderの回答を正式に記録し、内容を確認してから計算する決まりでした。
その確認が終わる前に、次の計算が要求されてしまったのです。
実行プログラムは必要な記録がないことを検出し、計算を開始する前に停止しました。
この停止自体は正しい動作です。未確認の判断を使って計算してしまえば、後から何を根拠に実行したのか分からなくなります。
そこで、Builderの回答を正式に記録・確認してから、Orchestratorが次の計算を要求するという進行手順に改めました。Python側の安全停止条件を緩めるのではなく、Orchestratorが処理を呼び出す順序を正す対策です。
これは、図1の「Builderからの提案を受け取り、Pythonに計算を要求する」部分の順序を直したことになります。
この問題への対策としては妥当でした。
3.2 テストは通ったのに、実際の起動で止まった
3.1の修正後、実際の計算を使わない54件のテストを実施し、すべて合格しました。
例えば、AIの判断記録がまだそろっていない状態では次の計算へ進ませない、といった点を確認していました。
ところが、別の実験を実際に起動すると、計算を始める前に停止しました。
原因は、WindowsからWSLの実行環境を起動する際に渡される情報と、実行プログラムが想定していた情報の形式が一致していなかったことでした。
今回、SolverをWSL上で動かしていたため、WindowsとWSLをまたぐ起動処理が必要でした。
なぜ、54件のテストでは見つからなかったのでしょうか。
テストでは、正常に起動できた状態を最初から用意していたからです。
実際の処理全体と、テストで確認していた範囲を図にすると、次のようになります。
図3:実際の処理は起動情報の読み取りから始まる。一方、54件のテストでは開始状態を用意し、その後の順序制御を確認していた。
つまり、順序制御の修正が間違っていたわけではありません。
順序制御に入る前の処理を、テストでは確認していなかったという別の問題だったのです。
3.3 今度は計算できたのに、管理情報が足りず停止した
さらに修復を重ねた後、別の実験では実際の計算が進み、Builderが次に試す内容を決めるところまで到達しました。
以前よりも後の工程へ進めたわけです。
しかし、その内容を実行するための記録に、budgetという必須項目がありませんでした。
これは、実行を許可する計算時間や回数に関する管理情報です。
しかも、その記録はすでに変更できない状態で保存されていました。
そのため、不足した項目を正式に訂正できず、次の計算へ進めませんでした。
ここで、二つの疑問が生まれました。
一つは、なぜ必要な項目を確認する前に、記録を変更できない状態にしていたのかということです。
もう一つは、そもそも、実行予算をAIに記入させる必要があったのかということです。
実行できる回数や終了期限は、Pythonの実行器自身が管理している情報です。
それなら、Builderにその値を提出させるのではなく、Pythonが実験の設定から取得して記録すればよいのではないでしょうか。
この問題は、管理情報の作り方と、記録を確定する順序を考え直すきっかけになりました。
4. 実行する仕組みを、小さく作り直した
不具合が見つかるたびに、その問題を防ぐための検査や管理処理を追加してきました。
一つひとつには理由がありましたが、その結果、実験を動かすための仕組み自体が複雑になっていました。
そこで、個別の不具合への対策を積み重ねるのではなく、実行器の設計そのものを見直すことにしました。
特に考え直したのは、AIと通常のプログラムの役割分担です。
まずは、作り直したときに何を変更したのかを説明します。
4.1 AIに書かせる情報を減らした
以前は、Builderが次に試す内容を回答するとき、計算に使える予算などの管理情報も記入する必要がありました。
しかし、実験で許可された計算回数や終了期限は、Python実行器が持っている情報です。
そこで、Builderには「次に何を試したいか」という内容を回答させ、実行に必要な管理情報はPythonが作るように変更しました。
例えば、Builderが「Solverの設定を変えて試したい」と提案したとします。
Orchestratorは、その提案をPython実行器に渡します。
Pythonは、あらかじめ保存された実験の設定を読み取り、許可された計算回数や終了期限、実験を識別する情報などを、正式な提案の記録に付け加えます。
つまり、AIが管理情報の値を考えたり、既知の値を書き写したりする必要をなくしたのです。
これは「AIは正しい形式で回答できない」という話ではありません。
通常のプログラムが確実に把握できる情報を、わざわざAIに提出させないという設計変更です。
4.2 不完全な回答を、確定する前に訂正できるようにした
次に変えたのは、AIから受け取った情報の扱い方です。
以前は、AIの提出物を正式に保存した後で不足が見つかると、訂正できないことがありました。
そこで、Python実行器に、提案を正式に採用する前の検査を設けました。
例えば、BuilderがSolver設定の変更を提案したものの、何を変更するのかを特定する情報が不足していたとします。
Orchestratorがその回答をPythonへ渡すと、Pythonは必要な項目がそろっているか確認します。
不足があれば、その提案を正式に受け付ける前にエラーを返します。
Orchestratorはその結果を受けて、必要ならBuilderに訂正を依頼できます。
Builderが修正した提案を再びPythonに渡し、検査に合格すれば、正式な提案として保存します。
このとき、元の回答も記録として残します。訂正は無制限にできるわけではなく、回数にも制限を設けています。
一方、実行に採用した条件や実際の計算結果は、後から都合よく書き換えられないようにします。
計算前の不備を訂正できることと、実行後の証拠を守ることは、両立できるわけです。
4.3 テストは通った。しかし、まだ完成していなかった
Python実行器を作り直した後、本格的な研究実験を始める前に、動作確認を行いました。
まず、Solverを実際には動かさずに、Python実行器の内部処理をテストしました。45件のテストはすべて合格しました。
次に、実行器を実際の計算環境につなぎ、本物のSolverを起動して計算結果を取得できるか確認しました。
この動作確認では、計算費用を抑えるため、あらかじめ決めた入力データとSolver設定を使い、実際の計算は2回だけ行いました。どちらも成功しました。
ただし、ここで確かめたのは、Python実行器から本物のSolverを起動し、結果を取得できることです。
AIが別の計算方法を提案し、それを試し、結果を見て次の計算へ進むという、本来の研究実験全体を確認したわけではありません。
その後、Independent Oracleが、本格的な実験に進むための機能がそろっているかを独立して確認しました。
すると、実行器に重要な機能が欠けていることが分かりました。
4.4 動作確認用の条件しか扱えなかった
何が不足していたのでしょうか。
ここで、トラックに積む荷物の組合せを決める最適化問題を例に考えます。
Solverには、荷物の一覧や重量制限を渡します。また、Solverには計算の進め方を調整する設定もあります。
今回の動作確認では、同じ荷物の一覧と同じSolver設定を使って、2回計算しました。
ここで、次の二つは別の話です。
- 動作確認で試す条件を限定する:今回は入力Aと設定Aだけで確かめる。しかし、完成した実行器は別の入力や設定も扱える。
- 実行器が扱える条件を限定する:入力Aと設定A以外は受け付けないプログラムを作る。
今回、意図していたのは前者です。
ところが、実際に作られたPython実行器は、後者の構造になっていました。
そのため、AIが「Solverの設定を変えてみよう」と提案しても、その方法を試せません。また、別の荷物の一覧を指定して計算することもできません。
動作確認では一つの条件だけを試すつもりが、実行器そのものも、その条件しか扱えない作りになっていたのです。
なぜそうした実装になったのか、AIがどの指示をどう解釈したかまでは断定できません。
さらに、計算結果を受けて次の計算へ進むための機能も不足していました。
これでは、AIが改善案を試し、その結果を見て次の計算へ進むという、本来の実験ができません。
図にすると、こうなります。
図4:実線は動作確認で成功した処理。点線は本来必要だったが、当時の実行器では使えなかった機能。
4.5 独立レビューが見つけたもの
独立レビューは、動作確認の結果が間違っていると指摘したわけではありません。
レビュー担当は、動作確認とは異なる方法を指定し、実行器がそれを受け付けるか確認しました。
すると、実行器は固定された方法以外を扱えないという理由で、その指定を拒否しました。
これは、単に「別の条件では失敗するかもしれない」という推測ではありません。実際に必要な機能が使えないことを、コードの動作から確認したのです。
しかも、異なる方法や入力を使って計算することは、もともとの仕様に書かれていました。
必要な機能を要求していたことと、その機能が実装されていることは別だったのです。
今回、独立レビューが有効だったのは、実装側のテスト結果を確認するだけで終わらなかったことです。Independent Oracleは、仕様に立ち返って動作確認では使っていない設定を試しました。実装を担当したAIとは別のAIが、同じテストを繰り返すのではなく、仕様から別の確認方法を考えた。 ここに、独立した監査役を設けた意味があったと思います。
この指摘を受け、不足していた機能を修復することにしました。
5. 不足していた機能を修復し、接続をテストした
独立レビューで指摘された機能不足に対して、Python実行器を修復し、接続を改めて確かめました。
前の動作確認では、固定条件で本物のSolverを動かせても、別の設定や入力を実際の計算へ渡す機能が不足していました。
そこで、Python実行器が、受け取ったSolver設定や入力データを、実際の計算処理へ渡せるように修復しました。
例えば、BuilderがSolver設定の変更を提案したとします。
Python実行器は、その設定を読み取り、Solverを呼び出す際の設定に反映する必要があります。
同様に、別の入力データが指定された場合には、動作確認で使った固定データではなく、指定されたデータを読み取って計算に渡す必要があります。
さらに、計算結果を保存し、必要なら次の計算を行う処理までつながっていなければなりません。
これらを確認するたびに本物のSolverを動かすと、時間がかかります。
そこで、Solverが実際に計算する部分だけを、スタブというテスト用の処理に置き換えました。
一方、入力を読み取る処理、指定された設定を計算へ渡す処理、結果を受け取って後続の処理へ進める部分には、実際のコードを使いました。
例えば、異なるSolver設定を指定したとき、その内容が計算を呼び出すところまで正しく届くかを確認します。
また、別の入力データを渡したとき、それが正しいデータとして扱われるかも確認します。
そのうえで、スタブからテスト用の計算結果を返し、後続の処理まで進めるかを確かめました。
ここでの違いを図にすると、次のようになります。
図5:修復前の動作確認と、修復後の接続テストで確かめた範囲の違い。修復後の接続テストは、本物のSolverによる性能評価ではありません。
つまり、計算そのものは省きながら、計算の前後にある実際のプログラムのつながりを検査したのです。
このテストだけで、Solverの計算性能まで確認できるわけではありません。そこは別の実験で実際に計算して確かめます。
今回の修復では、テストを何件行ったかだけでなく、どこを実際のコードで動かし、どこを代替しているのかを明確にすることが重要でした。
6. 見直した後の実験では、何ができたのか
不足していた機能を修復した後、改めて実験を行いました。
今回は、実際のSolverによる計算結果をBuilderが読み、その結果を踏まえて次の方法を考えるところまで進めました。
Builderは改善案を選び、OrchestratorがPython実行器を通じて計算を行いました。
得られた結果を確認すると、その改善案は一部の入力で従来より悪い結果になっていました。
そこで、Builderはその方法を採用しないと判断しました。
その後、別のAIの処理として改めて考え直し、別の改善案を試しました。
しかし、二つ目の改善案にも悪化する入力が見つかり、こちらも採用しませんでした。
最後にBuilderは、追加の改善案を試すより、従来の方法を維持して次の処理へ進むことを選びました。
そして、別の入力について新たにSolverを起動し、計算結果を得るところまで進めました。
今回の実験では、Solverによる計算を合計12回行っています。
6.1 以前の停止と、何が違ったのか
以前は、管理情報の不足などによって、改善案を試す前に実験が停止していました。
そのため、改善案が良かったのか悪かったのか、判断できませんでした。
一方、今回の実験では、改善案を実際に計算し、結果を確認したうえで不採用と判断できました。
| 以前の実験 | 見直し後の実験 |
|---|---|
| 管理情報が不足して、改善案を試せなかった | 実際に改善案を計算できた |
| 改善案の良し悪しを判断できなかった | 結果を比較し、不採用と判断できた |
| 途中で停止し、後続処理へ進めなかった | 結果を受けて再考し、次の計算まで進めた |
ここは、今回の結果を理解するうえで重要な点です。
例えば、ある計算方法を試した結果、従来の方法より悪いことが分かったとします。
それは望んでいた結果ではありません。しかし、実際に試して比較できたのであれば、研究として意味のある結果です。
一方、計算を始める前にプログラムが停止してしまえば、その方法の良し悪しさえ分かりません。
今回前進したのは、より良い計算方法を発見できたことではなく、計算方法の良し悪しを確かめ、結果を使って次の仕事へ進められるようになったことです。
なお、以前にも限定された条件で一連の処理を実行できた記録はありました。今回確認できたのは、実測結果を受けた再考を含む、より広い実験の一巡です。
6.2 実行基盤にも時間がかかっていた
今回の実験全体には、約112分かかりました。
実行記録によると、12回のSolver関連プロセスとEvaluatorのプロセスにかかった時間を足すと、約15分でした。
残る約97分には、AIによる診断や再考のほか、Builderや独立監査担当の応答待ち、GitHubを介した記録の保存・照合、実行前の確認や終了処理などが含まれています。ただし、LLM自体の推論時間と通信・管理にかかった時間は、完全には切り分けられていません。
もちろん、これらがすべて無駄だったわけではありません。
独立レビューは、実際に重要な欠落を見つけていました。また、AIが結果を読んで考え直すこと自体も、今回実現したかった仕事の一部です。
ただし、Solverの計算を動かすことと、それをAIエージェントの一連の仕事へ組み込むことには、異なる費用がかかると分かりました。
今後は、計算そのものの性能だけでなく、実行基盤の準備や管理にかかる時間も観測していきたいと考えています。
7. Harness Engineeringとして、何を学んだのか
今回の経験を、Harness Engineeringという観点から振り返ってみます。
AIエージェントを使って仕事を進めるには、AIに適切な指示を与えるだけでは足りません。
AIが使う道具、実行環境、入力や結果の管理、検査の仕組みなども必要です。
OpenAIのHarness engineeringでは、AIエージェントが開発作業を進めるための環境やフィードバックの整備が論じられています。
また、AnthropicのBuilding effective agentsでは、あらかじめ処理手順を決めるWorkflowと、AIが動的に行動を選ぶAgentを区別し、必要性に応じて複雑さを追加する考え方が紹介されています。
今回の経験を踏まえ、私はHarness Engineeringを、AIを安全に動かすための制約を設けるだけでなく、AIが実際の仕事を進められるように、環境や役割分担を整えることだと理解しています。
7.1 安全に停止できることと、仕事を完了できること
今回の実験では、未確認の判断で計算しない、管理情報が不足していれば停止する、といった仕組みが働いていました。
それ自体は重要なことです。
しかし、安全に停止できることだけでは、本来の仕事を完了できるとは限りません。
一方、実行できることだけを優先して必要な確認を省けば、結果の信頼性が損なわれる可能性があります。
止めるべきときに止まることと、正常な条件では最後まで仕事を進められること。その両方が必要でした。
今回の経験で、特にこの二つを分けて考えるようになりました。
7.2 次に同じ問題が起きたら
今回の経験から、次にAIエージェントの実行基盤を作るときには、次のことを確認したいと思っています。
1. 停止したとき、本当に実験全体を中止する必要があるのか。
例えば、計算結果の出所が分からない場合、その結果を採用してはいけません。
一方、まだ計算を始めていない段階で、AIからの提出物に必要な情報が足りないだけなら、訂正を求めてから実行する方法もあります。
どちらも検査で発見する不備ですが、同じように実験全体を停止する必要があるとは限りません。
2. AIに、プログラムが自分で計算できる情報まで書かせていないか。
今回の実験では、実行器が管理している実行予算などをAIにも提出させていました。
その情報が欠けたことが、停止の原因になりました。
AIが考えるべき内容と、通常のコードで確実に作れる情報を分けられないか、確認したいと思います。
3. テストが通ったとき、本来の仕事のどこまで確認できたのか。
今回は、54件のテストが通っても実際の起動で停止しました。また、45件のテストと2回の実計算が成功しても、別の条件を扱う機能が不足していました。
テストが合格したという結果だけでなく、どの入力を使い、どの処理を実際に通したのかを確認する必要があります。
特に、動作確認用の入力や設定が、実行器本体に固定されていないかは注意したい点です。
4. 試した方法が不採用になったことと、実験自体が失敗したことを混同していないか。
今回の最終的な実験では、AIが選んだ改善案は採用されませんでした。
しかし、それらを実際に試し、結果を比較したうえで不採用と判断し、次の処理まで進めることができました。
改善案が期待した結果を出さないことは、研究では十分に起こり得ます。
一方、管理上の不備で計算を始められなければ、改善案が良いか悪いかも判断できません。
計算結果が期待どおりだったかだけでなく、結果を得て判断するところまで実行できたかを区別する必要があります。
これらは、今回の実験で実際に直面した問題から得た確認事項です。
どれも特別な仕組みを必要とする話ではありません。しかし、問題が発生するたびに目の前の不具合を修正していると、こうした基本的な点を見落とすことがあります。
おわりに
以前の実験で、管理や検査の手続きを複雑にしすぎたのではないかと反省していました。
その経験を踏まえ、本来の研究を進めることを優先していたにもかかわらず、今回も途中停止を繰り返しました。
個々の不具合を修復することには意味がありました。しかし、それだけでは必要な仕事を最後まで進められませんでした。
そこで、AIと通常のコードの役割分担や、記録を確定するタイミングを見直し、実行器を小さく作り直しました。
ところが、今度は動作確認用の条件しか扱えないという問題が見つかりました。
検査を増やせば解決するわけでも、仕組みを小さくすれば解決するわけでもなかったのです。
最終的には、必要な機能と実行経路を修復し、AIが実際の計算結果を受けて判断し、後続の処理まで進められるようになりました。
今回の経験から、私が持ち帰ったのは次のことです。
AIエージェントの実行基盤は、安全に停止できるだけでなく、本来の仕事を最後まで進められるように設計する必要がある。
これは、一般的に最適なHarnessの設計を証明したという話ではありません。
一つの開発経験から、何を見直す必要があったのかを整理したものです。