← 기사 목록
日本語https://zenn.dev/topics/llm/feed

LLMエージェントの失敗でCADを汚さないロールバック設計

추출된 키워드

27
CAD·5ロールバック設計·5LLMエージェント·5AutoCAD·4AutoCAD .NET API·4操作バッチ·4Transaction·4Verify·3操作計画·3自己修正ループ·3Abort·3Commit·3Act·3Think·3Observe·3C#·3自然言語·2DBObject·2DocumentLock·2Editor·2Database·2BlockTableRecord·2プロンプト·2ステートレス·2決定論的なプログラム·2外部副作用·2リトライ制御·2

원문

12,317
LLMエージェントの失敗でCADを汚さないロールバック設計

LLMエージェントの失敗でCADを汚さないロールバック設計

LLMエージェントに外部ツールを操作させるとき、失敗そのものより厄介なことがあります。
それは失敗が環境に残ることです。

プログラムコードのようなテキスト生成なら、出力が間違っていても捨てれば済みます。
Web API でも、ステートレスな処理であれば「失敗したらもう一回」で済む場面は多いです。

しかし、CAD のような状態を持つアプリケーションを直接操作する場合は話が変わります。

  • 線を引けば、図面上に線が残る。
  • 円を作れば、円が残る。
  • レイヤを変えれば、その状態が残る。
  • 削除すれば、対象は消える。

つまり、エージェントの失敗は単なるエラーではなく、図面そのものを汚す操作になり得ます。
ここが、CADエージェントの難しいところです。

自分は現在、AutoCAD 上で動作する AI エージェントを個人で開発しています。自然言語の指示から LLM が操作計画を生成し、C# / AutoCAD .NET API 側で検証・実行する構成です。

以前、図面情報を全部 LLM に渡すのではなく、図面情報はアプリ内で構造化して保持し、LLM には必要な条件だけ問い合わせさせる設計について書きました。

https://zenn.dev/0xliclog/articles/bdbc550a999dfd

今回はその続きとして、LLM が生成した操作をどう安全に実行するかを書きます。
AutoCAD .NET API の細かい使い方ではなく、複数操作をどの単位で実行し、失敗時にどこまで巻き戻すかという設計の話です。

LLM はどこかで失敗する前提で扱う

まず前提として、LLM の出力は 100% 正しいとは保証できません。

現在の LLM であれば、単純な操作は問題なく動くことも多いです。
ただ、外部ツールを操作するエージェントでは、指示の解釈ミス、座標の取り違え、対象要素の誤認識、手順の抜け漏れなどが起きます。

LLM は曖昧な自然言語を柔軟に扱えます。
この柔軟さがあるからこそ、自然言語で CAD を操作するエージェントを作れます。

一方で、常に同じ条件で同じ正解を返す決定論的なプログラムではありません。
だから、LLMエージェントでは「間違えないようにする」だけでは足りません。
間違えることを前提に、失敗しても環境を壊さない設計にする必要があります。

たとえば AutoCAD エージェントに、次のように指示したとします。

原点に半径50の円を描いて、その外側に長方形を描き、最後に円を赤にしてください。

LLM は内部的に、次のような操作計画を作ります。

{
  "steps": [
    { "op": "create_circle", "x": 0, "y": 0, "radius": 50 },
    { "op": "create_rectangle", "x": -60, "y": -40, "width": 120, "height": 80 },
    { "op": "set_color", "target": "created_circle", "color": "red" }
  ]
}

これは簡略化した例です。
実際には、要素IDの参照、レイヤ指定、色、線種など、もう少し複雑な情報を扱います。

ここで問題になるのは、途中まで成功してから失敗するケースです。
たとえば、次のような状況です。

  • 円の作成は成功
  • 長方形の作成も成功
  • 円の色変更で参照エラーが発生

このとき、何も対策していないと、AutoCAD の図面上には円と長方形が残ります。
しかし、ユーザーの意図したタスク全体としては失敗しています。

図面上に未完成の中間成果物、いわば「ゴミ」が残る。
ここが問題になります。

中途半端な図形が次の Observe を汚染する

図面上にゴミが残るだけでも問題ですが、さらに厄介なのは、それが次の Observe に混ざり、正しい図面状態として扱われてしまうことです。

LLMエージェントは多くの場合、次のようなエージェントループで動きます。

Observe(観測) → Think(思考) → Act(行動) → Observe → Think → Act ...

AutoCAD エージェントでも、図面上の要素を観測し、その状態をもとに次の操作を決めます。

ここで、前回の失敗によって中途半端な図形が残っているとどうなるか。
エージェントは、その図形を「現在の正しい図面状態」として観測してしまいます。

たとえば、本来はタスク全体が失敗しているにもかかわらず、図面上には円と長方形が残っている。次の Observe でそれを見た LLM は、こう判断するかもしれません。

すでに円と長方形は存在している。では円の色だけ赤に変えればよさそうだ。

一見すると、自己修正できているように見えます。
でも、その円や長方形が正しい状態で作られている保証はありません。

中途半端な状態を前提に再計画すると、次の操作もまたズレる可能性があります。
結果として、こうなります。

負の連鎖です。

テキストなら出力を捨てれば終わりです。
一旦捨てて、作り直せばよい。
しかし CAD 操作では、失敗した操作が環境に残ります。

だからこそ、LLMエージェントに状態を持つアプリケーションを操作させる場合は、失敗を前提にした状態管理が必要になります。

発想: 操作バッチを1つの実行単位として扱う

この問題に対して、自分の AutoCAD エージェントでは発想を変えました。
LLM が生成した複数ステップの操作計画を、1つの実行単位として扱います。
ここでは仮に 操作バッチ と呼びます。

ここで登場する概念を整理すると、次のようになります。

概念役割
操作バッチ複数ステップをまとめた論理単位
Transaction図面DB変更を Commit / Abort する仕組み
バッチ実行管理層検証・実行・Commit / Abort・結果返却を統括する層

流れとしては、こうです。

ユーザー指示
    ↓
LLM が操作計画を生成
    ↓
アプリ側が操作計画を検証
    ↓
操作バッチとして実行

ここでいう検証とは、操作バッチを実行する前のチェックです。
操作名が存在するか、必要な項目がそろっているか、参照名が解決できるか、明らかに危険な操作が含まれていないか、といった確認を行います。

ただし、この検証はあくまで実行前に分かる範囲のチェックです。
実際に AutoCAD 上で動かしてみないと分からないエラーもあります。

だから、操作計画内の1コマンドごとに状態を確定しないことが重要になります。
操作バッチ全体が成功して初めて確定する、という単位にします。

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

create_circle    成功 ✅
create_rectangle 成功 ✅
set_color        失敗 ❌

この場合、円と四角形も含めて、操作はなかったことにします。
操作バッチを始める直前まで状態を巻き戻す。

これはデータベースのトランザクションに近い考え方です。
途中で失敗したら、BEGIN 前の状態に戻す。

AutoCAD エージェントでも同じです。

AutoCAD の Transaction で図面状態を守る

AutoCAD .NET API には、図面データベースを操作するための Transaction 機構があります。
図形の作成や既存要素の変更は、Transaction の中で DBObject を開いて行います。

最後に Commit すると変更が確定します。
失敗時は Abort することで、Transaction 内の変更を確定せずに破棄できます。
この記事では、この失敗時の巻き戻しを設計上「ロールバック」と呼びます。

ただし、重要なのは Transaction そのものの使い方ではありません。
LLM が生成した操作バッチ全体を、どの単位で Transaction に載せるかです。

考え方としては、次のような流れです。

これにより、途中まで作成された図形を CAD 上に残さずに済みます。

自分の実装では、Transaction 管理は上位層に寄せています。
各コマンドは個別の処理だけを担当します。

たとえば、かなり単純化すると次のようなイメージです。

Validate(plan)
を Transaction の外側に置くことで、操作名の不正や必須項目の不足など、実行前に分かるエラーは図面DBに触る前に弾きます。
public BatchResult ExecuteBatch(OperationPlan plan)
{
    // 実行前に、操作名や必須項目などを検証する
    Validate(plan);

    // 操作バッチ全体を1つの Transaction として扱う
    using var tr = _database.TransactionManager.StartTransaction();

    try
    {
        foreach (var step in plan.Steps)
        {
            // 各操作は同じ Transaction 内で実行する
            ExecuteStep(step, tr);
        }

        // すべて成功した場合だけ変更を確定する
        tr.Commit();

        return BatchResult.Success();
    }
    catch (Exception ex)
    {
        // 途中で失敗した場合は、Transaction 内の変更を破棄する
        tr.Abort();

        return BatchResult.Failed(
            rolledBack: true,
            // LLM に返せる安全なエラーに変換する
            error: ToSafeError(ex)
        );
    }
}

実際には DocumentLock、Editor、Database、BlockTableRecord、ログ出力なども絡みますが、役割分担としてはこの形です。

各コマンドは個別処理に集中し、上位の実行管理層が検証、Transaction の開始、Commit / Abort、LLM へのエラーフィードバックを担当します。

失敗したら、環境を戻してから LLM に返す

ここで大事なのは、失敗したときに LLM へ返す情報です。
単に例外を握りつぶして「失敗しました」と返すだけでは、LLM は再計画できません。
一方で、失敗途中の汚れた CAD 状態をそのまま観測させると、前述したように判断が狂います。

そこで、失敗時には次の順番にします。

1. 操作バッチを実行する
2. 途中でエラーが発生する
3. AutoCAD の状態をロールバックする
4. LLM にはエラー内容だけを返す
5. LLM がクリーンな状態を前提に再計画する

たとえば、円の色変更で参照エラーが出た場合、LLM には次のような情報を返します。

{
  "status": "failed",
  "rolled_back": true,
  "failed_step": 3,
  "operation": "set_color",
  "error": "対象要素 'created_circle' が解決できませんでした。"
}

ここで重要なのは、

rolled_back: true
です。
LLM に対して、

途中まで作った円や長方形は残っていない。実行前の状態に戻してある。

という前提を明示します。
これにより、LLM は「作成済みの円を探して色だけ変える」のではなく、円と長方形の作成から含めて、最初から正しい操作計画を生成し直す方向に寄せられます。

プロンプトは有効だが、保証にはならない

もちろん、プロンプトで失敗率を下げることはできます。

  • 出力例を入れる
  • 危険な操作を禁止する
  • 不確かな場合は確認させる

こうした工夫は有効です。

ただし、外部環境の状態、API の制約、図面内の特殊な要素、座標の取り違えまでは、プロンプトだけで完全には防げません。

だから、自分はこの問題をプロンプトだけで解こうとはしていません。

プロンプトで失敗率を下げ、実行基盤側で失敗時の被害を抑える。
この二重構えにしています。

ロールバックできると自己修正ループが作りやすい

トランザクションで状態を巻き戻せるようになると、自己修正ループも作りやすくなります。
流れとしては、こうです。

ユーザー指示
    ↓
LLM が操作計画を生成
    ↓
アプリが操作計画を検証
    ↓
AutoCAD 上でトランザクション実行
    ↓
失敗したらロールバック
    ↓
エラー内容を LLM に返す
    ↓
LLM が修正版の操作計画を生成
    ↓
修正版を再実行

このとき、毎回 AutoCAD の状態がクリーンに戻るため、LLM は同じ前提で再計画できます。
もしロールバックがなければ、再試行のたびに図面状態が変化し、LLM が観測する環境はどんどん複雑になります。

一方、ロールバックがあれば、失敗は単なる学習材料になります。

この操作はこの理由で失敗した
でも環境は実行前に戻っている
では別の方法で試そう

という形にできる。
この違いはかなり大きいです。

ロールバックできる操作とできない操作を分ける

ただし、何でも一括で巻き戻せばよいわけではありません。

たとえば、ユーザーが次のように指示したとします。

図面全体を調査して、条件に合う要素を赤くしてください。

この場合、図面を調査する Observe、対象を抽出する処理、色変更する Act は性質が違います。

Observe は読み取り専用なので、ロールバック対象ではありません。
一方、色変更は図面状態を変えるため、Transaction 管理の対象になります。

つまり、まず操作を分ける必要があります。

読み取り操作
一時的な計算
CAD 状態を変える操作
外部ファイルを書き換える操作
ユーザー確認を伴う操作

AutoCAD の Transaction で守れるのは、基本的に図面DBに対する変更です。

ファイル出力、外部 API 通知、ログの永続化、別アプリケーションの操作などは、AutoCAD 側をロールバックしても自動では戻りません。

そのため、外部副作用は Transaction 境界の外に置く 方が安全です。
まず AutoCAD 内の操作だけを操作バッチとして実行する。
Commit できた後で、ファイル出力や外部通知を行う。
巻き戻せない副作用を持つ操作は、自動実行の範囲に入れないか、人間の確認を挟む。

戻せない操作は、実行前に止める設計も必要です。

コマンドの成功とタスクの達成は別物

トランザクションで失敗時に巻き戻せるようになると、次に気になるのは Verify です。
つまり、コマンドが例外なく実行できたかだけでなく、タスク達成まで確認するフェーズです。

たとえば、

線を3本作ってください。

であれば、3本の線が作られたかを確認すればよいです。

しかし、

条件に合う要素をすべて赤くしてください。

のような指示では、個々の色変更が成功しても、対象漏れがあればタスクとしては失敗です。

コマンド成功 = タスク達成

とは限りません。
そこで自分の実装では、操作バッチの実行後、Commit 前に状態を確認する層を挟んでいます。

流れとしては、こうです。

操作バッチを実行
    ↓
例外がなければ成功候補
    ↓
状態を確認 (Verify)
    ↓
OK なら Commit
NG ならロールバックして再計画

ここで見ているのは、決定論的に判定できる範囲です。
たとえば、作成系の操作なら結果が返っているか、削除系の操作なら対象が実際に消えているか、といった確認です。

これらは「ユーザーの意図」を解釈しなくても、アプリ側で機械的に判定できます。
確認に失敗した場合は、操作バッチを成功扱いにせず、これまでと同じくロールバックします。

重要なのは、確認をなるべくアプリ側の決定論的な処理に寄せることです。
数や属性で機械的に判定できるものは、LLM に聞かずアプリ側で確定させます。

ただし、今アプリ側で確認できているのは、まだ限られた範囲です。

たとえば「移動後に本当に目的座標へ移動したか」「色変更後に意図どおりの色になったか」までは、まだ確認の対象に入っていません。このあたりをどこまで決定論的に検証し、どこからを LLM の評価に委ねるかは、今後の設計テーマです。

少なくとも現時点で言えるのは、ロールバック可能な実行基盤があると、Verify に失敗しても安全に元の状態へ戻せるということです。

リトライは無限に回さない

ロールバックできると、失敗しても状態を戻したうえで LLM に再計画させられます。

ただし、無限にリトライすればよいわけではありません。

同じエラーが何度も続く場合、LLM が状況を正しく理解できていない可能性があります。
別の操作計画を出していても、根本原因が同じなら、何度実行しても失敗します。

そのため、実装上は次のような制御を入れています。

  • リトライ回数に上限を設ける
  • 同じエラーが連続したら自動修正を止める
  • 対象要素が見つからない場合は再観測する
  • 危険な操作に変化した場合は実行しない
  • 解決できない場合はユーザーに確認を求める

ロールバックは「何度でも雑に試してよい」ための仕組みではありません。
安全に失敗できるようにしたうえで、どこまで自動で再試行し、どこから人間に戻すかを決める。

ここも設計の一部です。

CAD に限らない話

ここまで AutoCAD エージェントを例にしてきましたが、この問題は CAD に限りません。

データベース更新、ファイル編集、Git リポジトリの変更、業務アプリの操作。
状態を持つ外部環境を扱うエージェントは、どれも同じ構造の問題を抱えます。

失敗した操作が環境に残り、その状態を次の Observe が読んでしまう。
だから、LLM を賢くするだけでは実用的なエージェントにはなりません。

LLM が間違える前提で、対象システム側をどう守るか。

プロンプトではなく実行基盤で守るというこの考え方は、ステートフルな環境を扱うエージェント全般に共通すると思っています。

残っている課題

今回の設計で、中途半端な図形が図面上に残る問題はかなり扱いやすくなりました。
ただし、これですべてが解決したわけではありません。

実用的なエージェントには、少なくとも次のような層が必要になります。

実行前の検証
実行中のトランザクション管理
実行後のタスク達成確認 (Verify)
失敗時のリトライ制御

今後さらに詰めたいのは、Verify の粒度、操作バッチの境界、ロールバックできない外部副作用、自動リトライを止める条件、ユーザー確認に切り替える条件です。

プロンプトやモデル選定で失敗率を下げることは重要です。
ただ、それだけでは足りません。

LLMエージェントを実務環境で使うなら、「正しい操作計画を生成できること」だけでなく、「間違えたときに安全に戻せること」も実行基盤の要件になります。

特に CAD のように状態が残る環境では、失敗を消せる設計があるかどうかで、エージェント全体の安定性は大きく変わります。