Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Building Self-Correcting LLM Systems: The Evaluator-Optimizer Pattern

2025年09月24日に「Dev.to」が公開したITニュース「Building Self-Correcting LLM Systems: The Evaluator-Optimizer Pattern」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

LLMが生成するSQLの誤りを自動で修正するシステムを紹介。この「Evaluator-Optimizerパターン」は、SQLエラーを評価し、その場で修正指示を出すことでモデルを「コーチ」する。再学習不要でリアルタイムに間違いから学習させ、信頼性の高いSQL生成を実現し、開発者のデバッグ負担を大幅に削減する。

ITニュース解説

システムエンジニアを目指す皆さんにとって、最近よく耳にする「大規模言語モデル(LLM)」は非常に魅力的な技術だ。LLMは自然な言葉を理解し、コードを生成する能力も持っている。しかし、実際にLLMにシステムを動かすためのSQL(データベースを操作するための命令文)を生成させようとすると、意図は正しくても、構文の間違いやデータベースの仕様に合わない記述が頻繁に発生し、期待通りに動かないことが多い。このような状況は、開発者にとって大きなストレスであり、LLMのせっかくの能力を活かしきれていない課題となっていた。

そこで、今回紹介する技術は、LLMが完璧なSQLを一度で生成することを目指すのではなく、間違いからリアルタイムで学習し、自身で修正する能力を持たせるシステムを構築した事例である。これは「Evaluator-Optimizerパターン」と呼ばれ、まるで「あなたのSQLクエリが失敗しましたね。私がそれを修正しましょう」とLLM自身が語りかけるかのような、賢い振る舞いを実現している。この仕組みにより、これまでフラストレーションの種だったLLM生成SQLが、信頼できるシステムの一部へと変貌を遂げたのだ。

このシステムを構築する上で、複数のLLMサービスプロバイダーを利用する際には、それぞれ異なるAPI呼び出しの回数制限(レートリミット)や、一時的な障害時の再試行(リトライ)のルールがあるという課題に直面した。例えば、あるプロバイダーは「196秒後に再試行してください」と指示するかもしれないし、別のプロバイダーは全く異なるパターンを使うこともある。このような状況に対応するため、システムは「インテリジェントなリトライロジック」を実装している。これは、指定された長い待機時間を適切に尊重し、再試行の間隔を徐々に長くしていく「指数関数的バックオフ」という手法を取り入れている。これにより、同時に多数のリクエストが集中してサーバーに負荷をかける「雷鳴の群れ問題」を防ぎ、API呼び出しの無駄を減らし、システム全体の信頼性を向上させている。また、単なるネットワークエラーではなく、レートリミットのエラー(HTTP 429ステータスコード)が発生した場合にのみ再試行するという、賢い判断も行っている。

このシステムの核心は、LLMを再学習させたり、特定のデータで調整したりする「ファインチューニング」といった、時間もコストもかかる方法に頼らない点にある。なぜなら、LLMの再学習は非常に高価であり、一度特定のバージョンにロックインされてしまうと、新しいモデルの恩恵を受けにくくなるからだ。その代わりに、Anthropicという会社が提唱する「Evaluator-Optimizerパターン」を採用し、LLMが生成したSQLをその場で修正する仕組みを導入した。重要なのは、LLMの元の分析目標(何を調べたかったのか)はそのまま維持しつつ、構文エラーを繰り返し修正していくという考え方である。これにより、LLMの弱点を学習の機会に変えている。

具体的に、LLMがSQLを自己修正するワークフローは以下の通りだ。 まず「Step 1: 意図の維持」として、LLMが生成したSQLが失敗した場合でも、元の分析目標(例:「遅いエンドポイントを見つける」)、対象となるサービスや時間範囲、必要なメトリクス(測定値)やグループ化といった、クエリの根本的な目的をしっかりと記憶しておく。 次に「Step 2: 精度の高い評価」を行う。ここでは、SQLの構文が正しいかを高速にチェックするために「EXPLAIN AST」という機能を使う。これはSQLを実際に実行する前に、その構造を分析するもので、数ミリ秒で構文エラーを検出できる。さらに、実際にデータベースからデータを取り出す前に、ごく少量のデータでクエリの実行可能性をテストするために「SELECT ... LIMIT 1」のような軽いクエリを実行する。これにより、最小限のコストで実行上の問題を特定できる。その後、システムはエラーメッセージを解析し、具体的な問題(例:「間違ったテーブル名を使っている」「集計ルールに違反している」など)を特定する「エラー分類器」を活用する。 そして「Step 3: 同じコンテキストでの最適化」に進む。ここでは、最初から全く新しいSQLを生成し直すのではなく、先に特定した具体的なエラー情報を使って、LLMに修正点を「コーチング」する。例えば、「あなたのクエリは失敗しました。エラー215: 'count() * duration_ns'は集計関数ではありません。修正してください: 合計時間を取得するにはsum(duration_ns)に置き換えてください。ただし、サービスフィルターとグループ化は正解です」といった具体的な指示を出す。このように、元の分析目標を維持したまま、ピンポイントで修正点を指示することで、LLMは効率的に間違いを修正できる。 最後に「Step 4: 学習したパターンの適用」がある。LLMが何度か試しても自己修正できない場合、システムはあらかじめ定義されたルールに基づいて自動的に修正を行う。例えば、「count() * 列名」のような記述があったら「sum(列名)」に直すといった、一般的な修正パターンを適用する。これらのルールベースの修正は、将来的にLLMへの指示プロンプトに組み込まれることで、最初から正しいSQLを生成できる確率を高めるための「学習」として活用される。

実際の例を見てみよう。もしユーザーが「フロントエンドとバックエンドサービスの合計リクエスト時間を計算して」と尋ねたとする。LLMが最初に生成したSQLは、合計時間を計算するために「count() * duration_ns」のような誤った式を使ってしまうかもしれない。また、データベース名を含んだ「otel.traces」というテーブル名を指定してしまうこともある。この時、Evaluatorは二つの重要なエラーを発見する。一つは「count() * duration_ns」が集計関数ではないというClickHouseのルール違反(エラー215)、もう一つはデータベースがすでに選択されているので「otel.traces」ではなく単に「traces」とすべきだというエラー(エラー60)だ。Optimizerは、分析目標は変えずに、これらの具体的なエラーと修正方法をLLMに伝える。「合計時間が欲しいならsum(duration_ns)を使うべきです」「データベースは既に指定されているので、テーブル名は'traces'で十分です」と。このコーチングを受けたLLMは、見事に自己修正したSQLを生成する。この際、合計時間の計算を正しく修正するだけでなく、平均時間(avg_duration_ms)も追加するといった、より分析に役立つ改善まで行うこともある。このようにして、同じ分析目標を、正しいClickHouseの構文で達成できるのだ。

このEvaluator-Optimizerパターンがなぜこれほど効果的なのか。それは、開発者が実際のデバッグ作業で行う方法と非常に似ているからだ。まず、SQLが実行されるか、特定の分かりやすいエラーコードを返すかという「明確な評価基準」がある。次に、各修正の繰り返しで「特定された問題を一つずつ修正する」という「具体的な改善」が常に見られる。そして、何よりも重要なのは、分析の「目的は変わらず」に、単に「構文だけが修正される」という「コンテキストの維持」である。さらに、構文エラーの修正は、LLMに全く新しいクエリを再生成させるよりも「はるかにコストが低い」という「コスト効率」も大きな利点だ。LLMが自己修正できない場合でも、「count() * column → sum(column)」のような一般的な修正パターンを自動で適用するルールベースのフォールバックが用意されているため、システム全体の信頼性が担保される。これは、Anthropicが提唱するパターンと同様に、評価する部分(この場合はClickHouseなどのデータベース)と最適化する部分(LLMとルール群)が連携し、成功するまで繰り返し処理を行う仕組みだ。モデルを再学習させる必要はなく、リアルタイムのコーチングで問題を解決している。

LLMの最適化がうまくいかない場合や、何も結果を返さない場合に備えて、「ルールベースの最適化フォールバック」が用意されている。これは、あらかじめ定義された修正ルール集で、例えば「SELECT count() * duration_ns FROM otel.traces WHERE avg(duration) > 1000」のような入力に対して、自動的に「SELECT sum(duration_ns) FROM traces GROUP BY service_name HAVING avg(duration) > 1000」のように修正してくれる。この例では、集計関数の修正、テーブル名の修正、そしてWHERE句にある集計関数をHAVING句に移動するという、三つの修正を一度に適用している。これにより、LLMが失敗した場合でも、確実に動作するSQLを提供できる。

さらに、このシステムでは生成された全てのSQLクエリに、非常に詳細な「メタデータコメント」を付与している。これには、使用したLLMのモデル名、生成モード、生成日時、分析目標、関連サービス、使用されたトークン数、生成にかかった時間、そしてそのクエリがなぜそのように設計されたのかという「推論」まで含まれる。加えて、検証の試行回数、各試行のステータス、実行時間といった「検証履歴」も記録される。このメタデータは、クエリの生成にかかる時間やAPI利用コストを追跡して最適化したり、問題が発生した際にデバッグを容易にしたり、このクエリがビジネス上のどのような意味を持つのかを理解したり、どのモデルバージョンが使われたかを記録して再現性を確保したり、実際のクエリ実行時間から運用上のインテリジェンスを得たりと、多岐にわたる重要な役割を果たす。

また、システムの設定を一元管理し、効率的にロードするための「スマートキャッシング」も導入されている。Portkeyというゲートウェイクライアントが、設定ファイルのコンテンツハッシュ(内容から計算される固有の識別子)を計算し、設定ファイルの内容が変わっていない場合は、キャッシュされた設定を再利用する仕組みだ。これにより、環境変数を31個も削減できた上に、設定変更時にシステムを再起動せずに即座に反映させる「ホットリロード」が可能になった。

このような一連の改善がもたらした影響は大きい。以前は、開発者がLLM生成SQLの手動デバッグに何時間も費やし、異なるLLMモデルからは予測不能なエラーが返され、同じ間違いが何度も繰り返され、API呼び出しコストと開発者の時間という両面で高い運用コストがかかっていた。しかし、この自己修正システムを導入してからは、ほとんどのエラーがEvaluator-Optimizerパターンによって自動的に修正されるようになり、修正されたクエリを通じてシステムが継続的に学習し、改善されていく。適切なクエリタイプに応じて最適なモデルが自動的に選択され、スマートなリトライとキャッシングによってAPI呼び出し回数が減り、結果としてコスト削減が実現した。特に重要なのは、開発者がもはやSQLの構文デバッグに悩むことなく、生成されたSQLを信頼して、本来の目的である分析作業に集中できるようになったことだ。集計エラー、テーブル参照、WHERE句とHAVING句の使い分け、列名のあいまい一致、異なるデータベースの関数構文の変換といった、これまで頻繁に発生していたエラーパターンも自動で処理できるようになった。

この経験が教えてくれた最も重要な教訓は、LLMの出力品質を向上させるために、必ずしもモデルを再学習させる必要はないということである。インテリジェントなエラーハンドリングと、具体的な文脈に合わせたコーチングを実装することで、信頼性に欠けていたLLM生成SQLを、実際に運用に耐えうるシステムへと変革できた。このアプローチは非常にシンプルだが、強力である。それは、明確な基準(SQLが実行されるか)で評価し、一般的な再試行ではなく、具体的なエラーに基づいて最適化し、元の意図を維持したまま構文を修正し、そして間違いから学習して将来のエラーを防ぐというものだ。このEvaluator-Optimizerパターンは、SQL生成だけでなく、明確な成功基準を持つあらゆるLLMの出力に応用できる、汎用性の高いアプローチなのである。

関連コンテンツ

関連IT用語