はじめに
こんにちは、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%満たすことを保証できません。
特に、複数のオブジェクト間の整合性(例: エッジが参照するノードの存在確認)は、プロンプトで指示しても見落とされがちです。
私たちが採用したアプローチ
この課題に対し、私たちは以下のアプローチを採用しました。
- ビジネスルールをValidationRuleクラスとしてコードで定義する
- バリデーションエラーが発生したら、そのエラー情報をLLMにフィードバックして再生成させる
- 成功するまで繰り返し、上限回数で打ち切る
全体のフローは以下のとおりです。
以降では、このアプローチの各要素について詳しく解説します。
バリデーションを2層に分ける理由
私たちはバリデーションの責務を2層に分離しました。
| レイヤー | 責務 | 実装 | チェック内容 |
|---|---|---|---|
| 型レベル | データ構造の検証 | Zodスキーマ | 型、必須フィールド、値の範囲 |
| ビジネスルールレベル | ドメイン整合性の検証 | ValidationRuleクラス | ID一意性、参照整合性、ビジネスロジック |
分離がもたらす3つのメリット
1. 責務の明確化
Zodスキーマは「このデータは正しい形をしているか」を検証し、ValidationRuleは「このデータはビジネス上正しいか」を検証します。
それぞれの関心事を分離することで、コードの見通しが良くなります。
2. 再利用性の向上
ビジネスルールをクラスとして独立させることで、生成機能以外の機能(編集、インポートなど)でも同じルールを再利用できます。
3. エラーメッセージの制御
AIに修復を依頼する際、どのルールに違反しているかを明確に伝える必要があります。
ルールごとにクラスを分けることで、AI向けの詳細なエラーメッセージを提供できます。
ルールクラスの設計指針
私たちが実装したValidationRuleクラスには、以下の特徴があります。
1. 単一責任の原則
1つのルールクラスは1つのビジネスルールのみを検証します。
「エッジの参照先ノード存在チェック」「セクション親参照の整合性チェック」など、それぞれが独立したクラスとして実装されています。
これにより、ルールの追加・修正・削除が容易になります。
2. 型安全なエラーコードとメタデータ
各ルールは固有のエラーコード(例: EDGE_NODE_NOT_FOUND、SECTION_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による複雑な構造化データ生成において、ビジネスルール違反は避けられない課題です。
本記事で紹介したアプローチは以下のように整理できます。
- ルールは人間がコードで定義する: 厳守すべきビジネスルールは曖昧さを排除し、テスト可能な形で実装
- 修復はAIに委ねる: エラー情報をフィードバックすることで、AIの自己修復能力を活用
- 試行回数に上限を設ける: 無限ループを防ぎつつ、成功率を高める
このハイブリッドアプローチにより、LLMの柔軟性を活かしながら、ビジネスルールの厳密性を担保することができました。
同様の課題を抱えているチームの参考になれば幸いです。
本記事で紹介した仕組みは、Acsim 開発チームのkryota-devと共に設計・実装しました。
この場を借りて感謝を伝えます。
