1. はじめに:前回の課題について
前回の記事(LangGraph+LLMで自然言語→SQL自動生成を実現した話)では、自然言語でOdooデータを分析する簡易アプリを紹介しました。
ユーザーが「今月の売上トップ5は?」と入力すれば、LangGraph上でSQLが自動生成され、グラフ化まで行う仕組みです。
ただし、当時の課題は明確でした。
-
複雑なクエリに弱い
- JOINやサブクエリが必要な場合に的外れなSQLを生成する
-
SQL方言の誤り
- SQLiteで動かない
DATE_TRUNC
など、他DBの関数を平気で出力する
- SQLiteで動かない
「一つの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;
テストケース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;
テストケース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;
※画面上はエラー表示されているが、詳細のログを確認すると、データ形式がグラフ描画ロジックに合っていないためのエラーでした。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アプリ全体を見ると一方でグラフの描画のロジックのエラーや、ガードレールの対策がまだ未実施のため、こちらは今後行う予定です。
補足
- Github リポジトリ