はじめに
PS-SLの佐々木です。 アドベントカレンダー14日目になります。 今回はRAGシステムを構築している際にデータの一覧や統計データの取得、集計をしたい場合のTipsを紹介します
セマンティック検索が苦手な質問
RAG(Retrieval-Augmented Generation)システムを構築したことがある方なら、こんな経験はないでしょうか。
ユーザー: 「完了率を教えてください」
RAG: 「完了に関する情報が見つかりました。タスクAは完了しています。タスクBも完了しています...」
ユーザー: 「いや、パーセンテージで知りたいんだけど...」
セマンティック検索(ベクトル検索)は「意味的に関連する情報を取得する」ことは得意ですが、「集計」「統計」「条件フィルタリング」は苦手です。
本記事では、この限界を突破するためにLangChainのSQL Agentを組み合わせるアプローチを紹介します。
セマンティック検索の得意・不得意
得意なこと
| クエリ例 | なぜ得意か |
|---|---|
| 「冷却システムの問題点は?」 | 意味的に関連するドキュメントを取得 |
| 「バッテリーに関する懸念事項」 | 「バッテリー」「電池」「蓄電池」など類似概念も取得 |
| 「納期遅延のリスクについて」 | 文脈を理解して関連情報を取得 |
不得意なこと
| クエリ例 | なぜ不得意か |
|---|---|
| 「完了率は何%?」 | 集計計算ができない |
| 「担当者が田中のタスク一覧」 | 完全一致フィルタリングが苦手 |
| 「期限が来週までのものは何件?」 | 日付比較・カウントができない |
| 「担当者ごとの件数は?」 | GROUP BY相当の処理ができない |
セマンティック検索は「類似度」で検索するため、「田中」で検索すると「田中」だけでなく「山田」「中田」など”なんとなく似ている”ものも返してしまう可能性があります。
解決策:SQL Agentとのハイブリッドアーキテクチャ
全体像
┌─────────────────────────────────────────────────────────────┐
│ ユーザーの質問 │
│ 「冷却システムの問題点は?」「完了率は?」「田中の担当分は?」 │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ LLM(ルーター) │
│ 質問の意図を分析し、適切なツールを選択 │
└─────────────────────────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ セマンティック検索 │ │ SQL Agent │ │ その他ツール │
│ (Azure AI Search │ │ (SQLite) │ │ │
│ / Pinecone等) │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │
▼ ▼
意味的に関連する 正確なフィルタリング
ドキュメント取得 集計・統計計算
データの二重管理
同じデータを2つの形式で保持します:
- ベクトルDB: セマンティック検索用(埋め込みベクトル + メタデータ)
- RDB(SQLite等): 構造化クエリ用(正規化されたテーブル)
元データ(JSON/Excel等)
│
├──→ ベクトルDB(Azure AI Search / Pinecone)
│ - content: テキスト全文
│ - embedding: ベクトル
│ - metadata: 付随情報
│
└──→ SQLite
- record_no: INT
- assignee: TEXT
- status: TEXT
- deadline: DATE
- ...
実装:LangChainのSQL Agent
LangChainには公式のSQLDatabaseToolkitとcreate_sql_agentが用意されており、簡単にSQL Agentを構築できます。
公式のSQL Agentを使う方法
from langchain_community.utilities import SQLDatabase
from langchain_community.agent_toolkits import SQLDatabaseToolkit
from langchain.agents import create_agent
from langchain_openai import ChatOpenAI
# 1. データベース接続
db = SQLDatabase.from_uri("sqlite:///mydata.db")
print(f"利用可能なテーブル: {db.get_usable_table_names()}")
# 2. LLMの準備
llm = ChatOpenAI(model="gpt-4", temperature=0)
# 3. SQLToolkitの作成
toolkit = SQLDatabaseToolkit(db=db, llm=llm)
tools = toolkit.get_tools()
# 4. エージェントの作成
system_prompt = """あなたはSQLデータベースと対話するエージェントです。
質問に対して、正しいSQLクエリを作成し、結果を確認して回答してください。
"""
agent = create_agent(llm, tools, system_prompt=system_prompt)
# 5. 実行
result = agent.invoke({"messages": [{"role": "user", "content": "完了率を教えて"}]})
SQLDatabaseToolkitが提供する4つのツール
| ツール名 | 用途 |
|---|---|
sql_db_query |
SQLクエリを実行し、結果またはエラーを返す |
sql_db_schema |
指定テーブルのスキーマとサンプル行を取得 |
sql_db_list_tables |
利用可能なテーブル一覧を表示 |
sql_db_query_checker |
実行前にクエリの構文をチェック |
エージェントの動作フロー:
sql_db_list_tablesでテーブル一覧を確認sql_db_schemaで関連テーブルの構造を確認sql_db_query_checkerでクエリを検証sql_db_queryで実行
エラーが発生した場合、エラーメッセージがLLMに返され、自動的にクエリを修正して再実行します。
セキュリティ対策(重要)
⚠️ 警告: SQL Q&Aシステムの構築には、モデルが生成したSQLクエリを実行する必要があります。これには固有のリスクがあります。
公式ドキュメントの推奨事項:
- 権限を最小限に: Read-Only接続を使用
- テーブルを制限: 必要なテーブルのみアクセス許可
- Human-in-the-Loop: 重要なクエリは人間が承認
# Read-Only接続の例(SQLite)
uri = "sqlite:///file:mydata.db?mode=ro&uri=true"
db = SQLDatabase.from_uri(
uri,
include_tables=["allowed_table"], # 許可テーブルを制限
sample_rows_in_table_info=3,
)
Human-in-the-Loop(人間による承認)
LangChain 2025では、クエリ実行前に人間の承認を求める機能が組み込まれています。
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
llm, tools, system_prompt=system_prompt,
middleware=[HumanInTheLoopMiddleware(
interrupt_on={"sql_db_query": True} # クエリ実行時に一時停止
)],
checkpointer=InMemorySaver()
)
SQL生成用プロンプトの例
prompt = ChatPromptTemplate.from_messages([
("system", """あなたはSQLの専門家です。
## ルール
1. SELECT文のみ生成
2. SQLのみ返答(説明不要)
## テーブル情報
{table_info}
## クエリ例
完了率:
SELECT
COUNT(*) as total,
SUM(CASE WHEN status = '完了' THEN 1 ELSE 0 END) as completed,
ROUND(SUM(CASE WHEN status = '完了' THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1) as rate
FROM records
担当者別件数:
SELECT assignee, COUNT(*) as count
FROM records GROUP BY assignee ORDER BY count DESC
"""),
("human", "{question}")
])
---
## LLMによるツール選択の実装
### ツールの定義
```python
from langchain_core.tools import tool
@tool
async def semantic_search(query: str, top_k: int = 5) -> str:
"""意味的に関連するドキュメントを検索します。
「〜について」「〜に関する」「〜の問題点」などの質問に使用。
"""
# ベクトル検索の実装
results = await vector_store.search(query, top_k)
return format_results(results)
@tool
async def sql_query(question: str) -> str:
"""データの集計・フィルタリング・統計を取得します。
「何件?」「完了率は?」「担当者が〜」「一覧」などの質問に使用。
"""
result = await sql_agent.execute(question)
return json.dumps(result, ensure_ascii=False)
システムプロンプトで使い分けを指示
ポイント:LLMが適切なツールを選べるよう、判断基準を明示します。
## ツール選択の判断基準
| ユーザーの意図 | 使うツール | キーワード例 |
|--------------|-----------|-------------|
| 意味的に関連する情報 | semantic_search | 「〜について」「〜に関する」 |
| 件数・統計 | sql_query | 「何件」「完了率」「平均」 |
| 条件フィルタ | sql_query | 「担当者が〜」「ステータスが〜」 |
| 一覧取得 | sql_query | 「〜の一覧」「すべての〜」 |
### 具体例
✅ semantic_search を使う:
- 「冷却システムの問題点は?」→ 意味的に関連する情報を取得
✅ sql_query を使う:
- 「担当者が田中のタスク」→ WHERE assignee = '田中'
- 「完了率は?」→ COUNT + CASE WHEN
- 「来週期限のものは?」→ WHERE deadline <= '2025-01-17'
実際のクエリ例と結果
例1: セマンティック検索が適切なケース
ユーザー: 「バッテリー関連のリスクについて教えて」
→ semantic_search("バッテリー リスク")
結果:
- バッテリー劣化による航続距離低下のリスク
- 充電インフラ不足による利便性低下
- 電池廃棄時の環境負荷
(意味的に関連する情報を幅広く取得)
例2: SQLが適切なケース
ユーザー: 「担当者ごとの未完了件数を教えて」
→ sql_query("担当者ごとの未完了件数")
生成されたSQL:
SELECT assignee, COUNT(*) as count
FROM records
WHERE status != '完了'
GROUP BY assignee
ORDER BY count DESC
結果:
| assignee | count |
|----------|-------|
| 田中 | 12 |
| 佐藤 | 8 |
| 鈴木 | 5 |
例3: 組み合わせが必要なケース
ユーザー: 「田中さんの担当分の詳細を教えて」
→ 1. sql_query("担当者が田中のrecord_no一覧")
→ [1, 5, 12, 23, ...]
→ 2. semantic_search("record_no:1 OR record_no:5 OR ...")
→ 各レコードの詳細情報
メリットと考慮点
メリット
| 項目 | セマンティック検索のみ | + SQL Agent |
|---|---|---|
| 意味検索 | ✅ | ✅ |
| 完全一致フィルタ | △(不正確) | ✅ |
| 集計・統計 | ❌ | ✅ |
| 日付範囲検索 | △ | ✅ |
| GROUP BY | ❌ | ✅ |
考慮点
- データの二重管理: ベクトルDBとRDB両方にデータを投入・同期する必要がある
- スキーマ設計: SQLで検索したい項目は正規化してテーブル設計する
- セキュリティ: SQL Injection対策、Read-Only接続、クエリバリデーション
まとめ
セマンティック検索は強力ですが、万能ではありません。
- 「〜について教えて」→ セマンティック検索
- 「何件?」「完了率は?」「担当者が〜」→ SQL
LangChainのSQL Agentを組み合わせることで、RAGシステムの回答可能な範囲が大幅に広がります。
特に業務システムでは「統計」「集計」「正確なフィルタリング」の要求が多いため、この組み合わせは非常に効果的です。
ぜひ試してみてください。
