0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

CrewAIでSQL自動生成AIの精度を劇的に向上させた話

Posted at

1. はじめに:前回の課題について

前回の記事(LangGraph+LLMで自然言語→SQL自動生成を実現した話)では、自然言語でOdooデータを分析する簡易アプリを紹介しました。
ユーザーが「今月の売上トップ5は?」と入力すれば、LangGraph上でSQLが自動生成され、グラフ化まで行う仕組みです。

ただし、当時の課題は明確でした。

  • 複雑なクエリに弱い
    • JOINやサブクエリが必要な場合に的外れなSQLを生成する
  • SQL方言の誤り
    • SQLiteで動かないDATE_TRUNCなど、他DBの関数を平気で出力する

「一つのLLMに全部やらせる構造では精度に限界がある」と痛感しました。


2. 前回のアーキテクチャとその限界

前回はLangGraphを使い、以下のようなシンプルな構造でした。

LangGraph
├─ generate_sql:LLMがSQL生成(スキーマ情報+自然言語)
├─ execute_sql:生成SQLを実行
└─ generate_insights:結果を要約

問題点

  • 1つのLLMが複数の役割を同時に担っていた
    • ユーザー意図の解釈
    • スキーマ理解
    • SQL最適化
    • 方言(SQLite対応)

これは新人1人に会社の全データ管理を任せるようなものです。結果として、正確さ・一貫性ともに不足していました。


3. CrewAIの導入

「一人のAIに頼るのが無理なら、役割分担させればいい」という発想から、CrewAIを導入し、複数エージェントによる「専門家チーム」を組ませるアプローチに変えました。

CrewAIの特徴

CrewAIは、複数のAIエージェントを役割ごとに定義し、チームとして協調作業させるためのフレームワークです。

  • エージェントごとに役割(Role)、目的(Goal)、背景(Backstory)を設定可能
  • タスク分担と内部レビューを自動化(開発→レビュー→修正という自己修正ループが作れる)
  • LangChainやLangGraphなど既存ワークフローとも組み合わせやすい

本記事のSQL生成での活用ポイント

  • 1つのLLMに全タスクを任せず、役割を分けることで精度向上
    • SQL開発者エージェント → ロジック構築に専念
    • DBAエージェント → SQL構文・方言・パフォーマンスをレビュー
  • 人間の「開発者+レビューア」の役割分担をそのままAIに適用できる

役割分担

  • SQL開発者エージェント
    • ユーザー要求とスキーマを基に、最適なSQLロジックを構築
  • DBA(データベース管理者)エージェント
    • 生成SQLをレビューし、構文やパフォーマンス、SQL方言をチェック

4. ハイブリッドアーキテクチャ:LangGraph × CrewAI

LangGraphの「ワークフロー全体管理能力」はそのまま活かし、
generate_sqlノードだけCrewAIのクルーを呼び出すよう置き換えました

LangGraph
├─ generate_sql → ★CrewAIクルー(SQL開発者+DBA)を呼び出す
├─ execute_sql
└─ generate_insights

このように、LangGraphは監督役、CrewAIは専門家チームとして動く構成です。


5. 実装

1. CrewAIエージェント定義(api/crew.py

from crewai import Agent, Crew
from .llm import get_llm

llm = get_llm()

# SQL開発者エージェント
sql_developer = Agent(
    role='シニアSQLデベロッパー',
    goal='スキーマとユーザー要求に基づき、SQLiteで動作する正確なSQLを作成する',
    backstory='データベース設計とクエリ最適化に20年の経験を持つ専門家です',
    llm=llm
)

# レビューア(DBA)エージェント
sql_reviewer = Agent(
    role='データベース管理者',
    goal='SQLの構文、方言、パフォーマンスをレビューし修正する',
    backstory='大規模DB運用を長年担当し、最適化に精通しています',
    llm=llm
)

def create_sql_generation_crew(user_query: str, schema_str: str) -> Crew:
    return Crew(
        agents=[sql_developer, sql_reviewer],
        tasks=[
            f"ユーザー要求: {user_query}\nスキーマ: {schema_str}\nSQLを生成し、レビューを行う"
        ]
    )

2. LangGraphノード(api/graph.py

from .crew import create_sql_generation_crew

def node_generate_sql(state: AgentState):
    print("Executing node: generate_sql_with_crew")

    user_query = state["user_query"]
    schema_str = "..."  # スキーマ情報の取得処理

    sql_crew = create_sql_generation_crew(user_query, schema_str)
    final_sql = sql_crew.kickoff()  # 専門家チームが協調してSQLを確定

    state["sql"] = final_sql.raw
    return state

6. 検証

実際に複雑なSQLになるような指示を行い、検証しました。

テストケース1

テストクエリ:
「1月〜3月の間に少なくとも1回、かつ4月〜6月の間に少なくとも1回売れた製品について、それぞれの製品の2025年上半期(1月〜6月)全体の合計売上金額を教えてください。」

結果
副問い合わせを用いた最適なSQLを生成

Executing SQL: SELECT p.id, p.name, SUM(sol.price_unit * sol.qty_delivered) AS total_sales
FROM product_product p
JOIN sale_order_line sol ON p.id = sol.product_id
JOIN sale_order so ON sol.order_id = so.id
WHERE so.date_order BETWEEN '2025-01-01' AND '2025-06-30'
AND p.id IN (
    SELECT sol.product_id
    FROM sale_order_line sol
    JOIN sale_order so ON sol.order_id = so.id
    WHERE so.date_order BETWEEN '2025-01-01' AND '2025-03-31'
    GROUP BY sol.product_id
    HAVING COUNT(DISTINCT so.id) > 0
)
AND p.id IN (
    SELECT sol.product_id
    FROM sale_order_line sol
    JOIN sale_order so ON sol.order_id = so.id
    WHERE so.date_order BETWEEN '2025-04-01' AND '2025-06-30'
    GROUP BY sol.product_id
    HAVING COUNT(DISTINCT so.id) > 0
)
GROUP BY p.id, p.name;

画面イメージ:
image.png
image.png

テストケース2

テストクエリ:
「2025年の第2四半期(4月〜6月)において、あまり優良ではない顧客(顧客ランクが3未満)によく売れた製品は何 か?販売数量の合計でトップ3の製品名と、その合計数量を教えてください。」

結果:
4つのテーブルの結合を行うSQLを問題なく生成

SELECT pp.name, SUM(sol.qty_delivered) AS total_quantity
FROM sale_order_line sol
JOIN sale_order so ON sol.order_id = so.id
JOIN res_partner rp ON so.partner_id = rp.id
JOIN product_product pp ON sol.product_id = pp.id
WHERE rp.customer_rank < 3
AND so.date_order BETWEEN '2025-04-01' AND '2025-06-30'
GROUP BY pp.name
ORDER BY total_quantity DESC
LIMIT 3;

画面イメージ:
image.png
image.png

テストケース3

テストクエリ:
これまでの全期間で、合計売上金額が$3,000ドル以上あった優良顧客(顧客ランク4以上)は、全部で何人いるか?

結果:
エラー発生:(Data does not have a column named "count". Available columns are good_customers_count)

SELECT COUNT(DISTINCT rp.id) AS good_customers_count
FROM res_partner rp
JOIN sale_order so ON rp.id = so.partner_id
WHERE rp.customer_rank >= 4
GROUP BY rp.id
HAVING SUM(so.amount_total) >= 3000;
Executing node: execute_sql
Executing SQL: SELECT COUNT(DISTINCT rp.id) AS good_customers_count
FROM res_partner rp
JOIN sale_order so ON rp.id = so.partner_id
WHERE rp.customer_rank >= 4
GROUP BY rp.id
HAVING SUM(so.amount_total) >= 3000;

画面イメージ:
image.png
image.png

※画面上はエラー表示されているが、詳細のログを確認すると、データ形式がグラフ描画ロジックに合っていないためのエラーでした。SQLは問題なさそう。

7. なぜ精度が向上したのか?

  • 役割分担による思考の焦点化
    • 開発者はロジック構築に、DBAは品質保証に集中できる
  • 自己修正ループ
    • 「開発→レビュー」の二段構えにより、初期案が誤っていても修正が可能

8. スキーマ定義の改善が鍵

もう1つ重要だったのが、スキーマ情報の精度向上です。

改善前:

"product_id": "製品ID(product_product.id)、外部キー"

改善後:

{
  "name": "product_id",
  "type": "INTEGER",
  "is_foreign_key": true,
  "references": "product_product.id"
}

より構造化されたスキーマを渡すことで、エージェントがテーブル間の関連を確信を持って把握できるようになりました。

9. まとめ:LangGraph×CrewAIは有効か?

CrewAIを組み込むことで、LangGraph単独では実現できなかったSQL生成の精度を大幅に改善できました。

  • LangGraph:ワークフロー全体管理(監督役)
  • CrewAI:専門タスクの協調処理(専門家チーム)
    ただ、Webアプリ全体を見ると一方でグラフの描画のロジックのエラーや、ガードレールの対策がまだ未実施のため、こちらは今後行う予定です。

補足

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?