Acsim Logo
エンジニアリング

Structured Outputのビジネスルール違反をAIで自動修復する

Structured Outputのビジネスルール違反をAIで自動修復する
#LLM#バリデーション#AI#TypeScript#Zod

はじめに

こんにちは、Acsim 開発チームのyoshida-m-3です。

生成AIを活用して複雑な構造化データを生成する機能を開発していると、ある問題に直面します。
それは「スキーマには準拠しているが、ビジネスルールに違反したデータが生成されてしまう」という問題です。

本記事では、この課題に対して私たちが採用した「ビジネスルールはコードで定義し、修復はAIに委ねる」というハイブリッドアプローチについて紹介します。

LLMが生成するデータの「落とし穴」

型は正しい、でもデータは壊れている

私たちのプロダクトでは、ユーザーがテキストや画像からビジネスフローを自動生成できる機能を提供しています。 このビジネスフローのデータは構造化されてデータベースの複数のテーブルに保存されます。

ビジネスフロー構造
├── nodes(ノード: アクター、アクション、条件分岐など)
├── edges(ノード間の接続)
├── artifacts(成果物)
└── businessPatterns(業務パターン)

データベースレイヤーでの参照整合性や型の制約は、ZodスキーマとORMで担保できていましたが、以下のような大小さまざまなビジネスルール違反が発生していました。

ルール違反例
エッジは存在するノードのみを参照できる存在しないノードIDを参照するエッジが生成される
ノードIDは一意でなければならない複数のノードに同じIDが割り当てられる
アクションが参照するアクターは存在しなければならない削除済み、または未生成のアクターIDがactorIdに設定される
アクションの入出力で参照する成果物は存在しなければならない存在しないartifactIdがinputs/outputsに含まれる
条件分岐のオプションIDは一意でなければならない複数の条件ノードで同じconditionOptionIdが使われる
セクション内に配置されたノードは、そのセクションを親として参照しなければならないノードの座標がセクションエリア内にあるのに、セクションを参照していない、または別のセクションを参照している

プロンプト指示の限界

当初はプロンプトに詳細なルールを記載することで対応しようとしました。
しかし、生成対象の規模が大きくなるほど、ルール遵守率が低下する傾向が顕著でした。

LLMは確率的に出力を生成するため、複雑な制約を100%満たすことを保証できません。
特に、複数のオブジェクト間の整合性(例: エッジが参照するノードの存在確認)は、プロンプトで指示しても見落とされがちです。

私たちが採用したアプローチ

この課題に対し、私たちは以下のアプローチを採用しました。

  1. ビジネスルールをValidationRuleクラスとしてコードで定義する
  2. バリデーションエラーが発生したら、そのエラー情報をLLMにフィードバックして再生成させる
  3. 成功するまで繰り返し、上限回数で打ち切る

全体のフローは以下のとおりです。

以降では、このアプローチの各要素について詳しく解説します。

バリデーションを2層に分ける理由

私たちはバリデーションの責務を2層に分離しました。

レイヤー責務実装チェック内容
型レベルデータ構造の検証Zodスキーマ型、必須フィールド、値の範囲
ビジネスルールレベルドメイン整合性の検証ValidationRuleクラスID一意性、参照整合性、ビジネスロジック

分離がもたらす3つのメリット

1. 責務の明確化

Zodスキーマは「このデータは正しい形をしているか」を検証し、ValidationRuleは「このデータはビジネス上正しいか」を検証します。
それぞれの関心事を分離することで、コードの見通しが良くなります。

2. 再利用性の向上

ビジネスルールをクラスとして独立させることで、生成機能以外の機能(編集、インポートなど)でも同じルールを再利用できます。

3. エラーメッセージの制御

AIに修復を依頼する際、どのルールに違反しているかを明確に伝える必要があります。
ルールごとにクラスを分けることで、AI向けの詳細なエラーメッセージを提供できます。

ルールクラスの設計指針

私たちが実装したValidationRuleクラスには、以下の特徴があります。

1. 単一責任の原則

1つのルールクラスは1つのビジネスルールのみを検証します。
「エッジの参照先ノード存在チェック」「セクション親参照の整合性チェック」など、それぞれが独立したクラスとして実装されています。
これにより、ルールの追加・修正・削除が容易になります。

2. 型安全なエラーコードとメタデータ

各ルールは固有のエラーコード(例: EDGE_NODE_NOT_FOUNDSECTION_PARENT_REFERENCE_INVALID)を持ち、エラーごとに適切なメタデータを返します。
TypeScriptの型システムを活用することで、エラーコードとメタデータの組み合わせが型レベルで保証されます。

3. AI向けの詳細なエラーメッセージ

エラーメッセージは人間向けのログとしてだけでなく、AIが修復時に参照する情報源としても機能します。
「どのノードが」「どのような理由で」違反しているかを具体的に記述することで、AIによる修復精度が向上します。

// 存在しないノードIDを参照するエッジがある場合のエラーメッセージ例
{
  "code": "EDGE_NODE_NOT_FOUND",
  "message": "Edge 'edge-id' references non-existent source node 'missing-node-id'",
  "meta": {
    "edgeId": "edge-id",
    "missingNodeId": "missing-node-id",
    "nodeType": "source"
  }
}

LLMが見落としやすいルールの実例

現在、私たちのビジネスフローでは13種類のバリデーションルールを実装しています。
プロダクトの成長に伴い、このルール数は今後も増えていくことが予想されます。
単一責任の原則に基づく現在の設計であれば、新しいルールの追加や既存ルールの削除が容易で、ルール数が増えても保守性を保つことができます。

実装しているルールの中でも特徴的なのが、座標と親参照の整合性チェックです。

ビジネスフローでは、ノードは画面上の座標(x, y)を持ち、セクションエリア内に視覚的に配置されます。
LLMがフロー図を生成する際、ノードの座標はセクションエリア内に正しく配置していても、セクションIDの設定を忘れたり、別のセクションのIDを誤って参照することがあります。

補足: このルール違反は座標計算により決定論的に修復することも可能です。
現在の実装では、修復ロジックを汎用化するためにすべてのルール違反をLLMによる修復に統一しています。
将来的には、決定論的に解決可能なルールについては専用の修復ロジックを実装し、LLMの呼び出しを最適化することも検討しています。

この「見た目は正しいが、データ構造として不整合」というパターンは、LLMによる構造化データ生成で頻発する問題です。
プロンプトで「座標がセクション内にあるノードは必ずそのセクションを親として参照してください」と指示しても、規模が大きくなると見落としが発生します。

このようなルールをコードとして明確に定義しておくことで、LLMの出力に依存せず、確実にビジネスルールを担保できます。

AIによる自動修復ループ

ビジネスルールに違反したデータが検出された場合、AIに対してエラー情報を渡し、修復を依頼します。

単純なリトライではなく、検出されたエラーの詳細をAIに渡すことが重要です。
これにより、AIは「何を修正すべきか」を理解した上で修復を試みることができます。

具体的には、修復エージェントへのプロンプトにバリデーションエラーの一覧(エラーコード、エラーメッセージ、問題のあるノードやエッジのID)を含めます。
これにより、AIは「どこを」「どのように」修正すべきかを具体的に把握できます。

何回リトライすべきか

無限ループを防ぐため、最大試行回数を設定しています。
私たちの実装では最大6回(初回バリデーション + AI修復最大5回)としています。

試行回数の設計にはトレードオフがあります。

  • 回数が少なすぎる: 修復可能なエラーでも失敗として扱われる
  • 回数が多すぎる: 修復不可能なエラーに対してリソースを浪費する

実運用では、ほとんどのケースで1〜2回の修復で成功しており、5回を超えることは稀です。
上限に達した場合は、エラー情報を含めて例外をスローし、後続の処理で適切にハンドリングします。

このアプローチが効果的な理由

具体的なエラー情報がAIの修復を助ける

このアプローチの核心は、LLMのフィードバックによる文脈に基づく調整を活用している点です。

LLMは与えられた文脈に基づいて出力を調整し、次の出力を改善します。
単に「エラーがあったので再生成してください」と伝えるのではなく、「このIDが重複しています」「このエッジは存在しないノードを参照しています」といった具体的なエラー情報を渡すことで、AIは適切な修正を行いやすくなります。

ルール定義は人間、修復はAI

担当役割理由
人間(開発者)ビジネスルールの定義ドメイン知識が必要、変更頻度が低い、正確性が求められる
AIルール違反の修復パターン認識が得意、柔軟な対応が可能、試行錯誤が許容される

ビジネスルールは厳密に定義されるべきものであり、曖昧さを許容してはいけません。
一方、修復作業は「正解に近づけばよい」という性質を持つため、AIの試行錯誤が有効に機能します。

まとめ

LLMによる複雑な構造化データ生成において、ビジネスルール違反は避けられない課題です。

本記事で紹介したアプローチは以下のように整理できます。

  1. ルールは人間がコードで定義する: 厳守すべきビジネスルールは曖昧さを排除し、テスト可能な形で実装
  2. 修復はAIに委ねる: エラー情報をフィードバックすることで、AIの自己修復能力を活用
  3. 試行回数に上限を設ける: 無限ループを防ぎつつ、成功率を高める

このハイブリッドアプローチにより、LLMの柔軟性を活かしながら、ビジネスルールの厳密性を担保することができました。

同様の課題を抱えているチームの参考になれば幸いです。


本記事で紹介した仕組みは、Acsim 開発チームのkryota-devと共に設計・実装しました。
この場を借りて感謝を伝えます。

この記事をシェア: