はじめに
前回は、Dify APIを使った評価コメントの自動生成を実装しました。
前回の記事:Python × Dify × RAGで学ぶ業務システム開発入門【第8回 Dify API連携編】
第9回では、RAG(Retrieval-Augmented Generation) を構築します。
RAGとは「検索で関連情報を取得し、その情報を元にAIが回答を生成する」仕組みです。
従来の生成AI(第8回)
→ AIの知識のみで回答する
→ 社内固有の情報は知らない
→ ハルシネーション(事実と異なる回答)のリスクが高い
RAG(第9回)
→ まず社内文書から関連情報を検索する
→ 検索結果を根拠としてAIが回答を生成する
→ 社内固有の情報に基づいた正確な回答ができる
本連載は「作って終わり」ではなく、なぜそう設計するのかを重視しています。
設計の意図を理解することで、他のシステムにも応用できる力が身につきます。
本記事で学ぶこと
| 項目 | 内容 |
|---|---|
| RAG の仕組み | 検索 → 生成 の2段階アーキテクチャ |
| Embedding とは | テキストをベクトルに変換する技術 |
| Dify Knowledge | 文書登録・チャンク分割・検索の設定方法 |
| 検索精度の改善 | 文書の前処理・FAQ形式・メタデータの活用 |
| Python実装 | Flask からの検索API呼び出し |
RAG の仕組み
RAGは2つのステップで動作します。
ステップ1:Retrieval(検索)
ユーザーの質問 → ベクトル化 → 社内文書DBで類似度検索 → 関連チャンクを取得
ステップ2:Generation(生成)
質問 + 関連チャンク → LLM(大規模言語モデル)→ 根拠付き回答を生成
全体フロー図
利用者: 「有給休暇は何日前までに申請が必要?」
↓
① 質問をベクトル(数値の配列)に変換
↓
② Dify Knowledge で類似度検索
↓
③ 関連チャンクを取得
「就業規則第12条:有給休暇の申請は3営業日前までに...」
↓
④ LLMに「質問 + 検索結果」を入力
↓
⑤ 根拠付き回答を生成
「就業規則第12条に基づき、3営業日前までに申請してください。」
なぜ RAG が必要か
| 方式 | メリット | デメリット |
|---|---|---|
| LLM単体 | 一般知識に強い | 社内固有の情報を知らない |
| キーワード検索 | 高速・確実 | 自然言語での質問に弱い |
| RAG | 社内情報を根拠に回答できる | 文書整備が必要 |
Embedding とは
Embedding(埋め込み)とは、テキストを数値のベクトル(配列)に変換する技術です。
テキスト: 「有給休暇の申請方法」
↓ Embeddingモデル
ベクトル: [0.12, -0.34, 0.56, 0.89, ...] (数百〜数千次元)
ベクトルの類似度で検索する
質問: 「有給休暇は何日前に申請?」
→ ベクトル A: [0.11, -0.33, 0.55, ...]
文書1: 「有給休暇の申請は3営業日前まで」
→ ベクトル B: [0.12, -0.34, 0.56, ...] ← Aと近い(類似度: 0.95)
文書2: 「社員旅行のお知らせ」
→ ベクトル C: [0.78, 0.12, -0.45, ...] ← Aと遠い(類似度: 0.15)
→ ベクトルが近い文書ほど「質問に関連している」と判断できる
Dify Knowledge ではこの Embedding と類似度検索を自動で行ってくれます。
利用者が意識するのは「どの文書を登録するか」「どう分割するか」だけです。
Dify Knowledge への文書登録
登録する文書の例
| 文書名 | 内容 | ファイル形式 |
|---|---|---|
| 就業規則 | 勤務時間・有給・休暇のルール | |
| 人事評価基準書 | 評価項目・基準・プロセス | |
| 社内マニュアル | 業務手順・ツール使用方法 | Word / Markdown |
| 研修資料 | 研修の目的・カリキュラム | PDF / PowerPoint |
| FAQ集 | よくある質問と回答 | テキスト / CSV |
登録手順
1. Difyダッシュボード → 「ナレッジ」→ 「作成」
2. ナレッジ名を入力: "HR-Documents"
3. ファイルをアップロード(PDF / Word / テキスト)
4. チャンク分割の設定:
- 分割方法: 自動(推奨)
- チャンクサイズ: 500〜1000文字
- オーバーラップ: 50文字
5. Embeddingモデルの選択(デフォルトでOK)
6. 「保存して処理」→ インデックス作成完了
チャンク分割とは
文書を適切な大きさに分割することを「チャンク分割」と呼びます。
大きすぎるチャンク(文書全体を1チャンク)
→ 検索結果に不要な情報が含まれすぎる
→ LLMが関連部分を見つけにくい
小さすぎるチャンク(1文ずつ)
→ 文脈が失われる
→ 回答に必要な情報が複数チャンクに分散する
適切なサイズ(500〜1000文字)
→ 1つのトピックが1チャンクに収まる
→ 検索精度と文脈保持のバランスがよい
検索精度を改善するポイント
RAGの検索精度は「登録する文書の品質」で決まります。
1. FAQ形式で登録する
❌ 長い規則文をそのまま登録
「第12条 年次有給休暇の取得に関する手続きについては
所属長の承認を得たうえで人事部に届出をするものとし
原則として3営業日前までに...(以下略)」
✅ FAQ形式に変換して登録
Q: 有給休暇は何日前までに申請が必要ですか?
A: 就業規則第12条に基づき、3営業日前までに申請してください。
緊急の場合は直属の上長への事前口頭報告が必要です。
参照: 就業規則 第12条 第2項
2. メタデータを付与する
文書名: 就業規則
カテゴリ: 勤務ルール
条文番号: 第12条
最終更新日: 2024-04-01
3. 定期的に更新する
就業規則が改定されたら → Dify Knowledgeの文書も更新する
古い情報が残っていると → 誤った回答が生成される
精度改善チェックリスト
| 項目 | 確認内容 |
|---|---|
| 文書の網羅性 | 質問されそうな内容がカバーされているか |
| チャンクサイズ | 1トピック1チャンクになっているか |
| FAQ変換 | 頻出質問を Q&A 形式で登録しているか |
| 情報の鮮度 | 古い情報が残っていないか |
| 回答精度の検証 | 定期的に質問して正確か確認しているか |
実装:dify_service.py(RAG検索)
services/dify_service.py にRAG検索機能を追加します。
# services/dify_service.py に追加
# ============================================================
# RAG 社内文書検索
# ============================================================
def search_knowledge(question: str, top_k: int = 3) -> dict:
"""
Dify Knowledge で社内文書を検索する(RAG)
Args:
question: 利用者が入力した質問文
top_k: 取得する関連チャンク数(デフォルト3件)
Returns:
{
"success": True/False,
"records": [
{
"content": "チャンクの内容",
"score": 0.95,
"document_name": "就業規則.pdf",
}
],
"error": ""
}
"""
if not Config.DIFY_API_KEY:
return {
"success": False,
"records": [],
"error": "DIFY_API_KEY が設定されていません"
}
if not Config.DIFY_DATASET_ID:
return {
"success": False,
"records": [],
"error": "DIFY_DATASET_ID が設定されていません"
}
headers = {
"Authorization": f"Bearer {Config.DIFY_API_KEY}",
"Content-Type": "application/json",
}
payload = {
"query": question,
"retrieval_model": {
"search_method": "semantic_search",
"reranking_enable": False,
},
"top_k": top_k,
}
try:
response = requests.post(
f"{Config.DIFY_API_URL}/datasets/{Config.DIFY_DATASET_ID}/retrieve",
headers=headers,
json=payload,
timeout=15,
)
if response.status_code == 200:
data = response.json()
records = []
for record in data.get("records", []):
records.append({
"content": record.get("segment", {}).get("content", ""),
"score": round(record.get("score", 0), 3),
"document_name": record.get("segment", {}).get(
"document", {}).get("name", "不明"),
})
return {"success": True, "records": records, "error": ""}
else:
return {
"success": False,
"records": [],
"error": f"検索APIエラー(ステータス: {response.status_code})"
}
except requests.exceptions.Timeout:
return {
"success": False,
"records": [],
"error": "検索がタイムアウトしました"
}
except requests.exceptions.ConnectionError:
return {
"success": False,
"records": [],
"error": "Dify APIに接続できません"
}
except Exception as e:
return {
"success": False,
"records": [],
"error": f"予期しないエラー: {str(e)}"
}
def generate_rag_answer(question: str, context_chunks: list[str]) -> dict:
"""
検索結果を元にLLMが回答を生成する
Args:
question: ユーザーの質問
context_chunks: 検索で取得した関連チャンクのリスト
Returns:
{"success": bool, "comment": str, "error": str}
"""
context = "\n---\n".join(context_chunks)
prompt = f"""
あなたは社内文書に基づいて質問に回答するアシスタントです。
## ルール
- 以下の「参考情報」のみを根拠に回答してください
- 参考情報に記載がない場合は「該当する情報が見つかりませんでした」と回答してください
- 回答の末尾に参照元の文書名を記載してください
- 推測や一般論で回答しないでください
## 参考情報
{context}
## 質問
{question}
""".strip()
return _call_dify_chat(prompt)
実装:app.py(検索ルーティング)
# app.py(社内文書検索部分)
from services.dify_service import search_knowledge, generate_rag_answer
@app.route("/search", methods=["GET", "POST"])
@login_required
def search():
"""社内文書検索(RAG)"""
result = None
question = ""
if request.method == "POST":
question = request.form.get("question", "").strip()
if not question:
flash("質問を入力してください", "error")
return render_template("search.html",
result=None, question="")
# Step1: 関連文書を検索
search_result = search_knowledge(question, top_k=3)
if not search_result["success"]:
flash(f"検索エラー:{search_result['error']}", "error")
return render_template("search.html",
result=None, question=question)
if not search_result["records"]:
result = {
"answer": "該当する情報が見つかりませんでした。",
"sources": [],
}
else:
# Step2: 検索結果を元に回答生成
chunks = [r["content"] for r in search_result["records"]]
answer_result = generate_rag_answer(question, chunks)
if answer_result["success"]:
result = {
"answer": answer_result["comment"],
"sources": search_result["records"],
}
else:
flash(f"回答生成エラー:{answer_result['error']}", "error")
result = None
return render_template("search.html",
result=result, question=question)
実装:テンプレート(search.html)
{% extends "base.html" %}
{% block title %}社内文書検索 - AI人事・研修システム{% endblock %}
{% block content %}
<div class="page-header">
<h1>🔍 社内文書検索</h1>
</div>
<p class="text-muted">
就業規則・マニュアル・FAQ などを自然言語で検索できます
</p>
<!-- 検索フォーム -->
<form method="POST" class="search-form-large">
<div class="form-group">
<input type="text" name="question"
placeholder="例: 有給休暇は何日前までに申請が必要ですか?"
value="{{ question }}"
required autofocus>
</div>
<button type="submit" class="btn btn-primary">検索</button>
</form>
<!-- 検索結果 -->
{% if result %}
<div class="search-result-card">
<h3>💡 回答</h3>
<div class="answer-text">{{ result.answer }}</div>
{% if result.sources %}
<h4>📄 参照元</h4>
<ul class="source-list">
{% for src in result.sources %}
<li>
<strong>{{ src.document_name }}</strong>
<span class="score">(関連度: {{ src.score }})</span>
<p class="source-content">{{ src.content[:150] }}{% if src.content|length > 150 %}...{% endif %}</p>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
{% endblock %}
ハルシネーション対策
RAGにおいて最も注意すべきは**ハルシネーション(事実と異なる回答を生成すること)**です。
対策方法
| 対策 | 実装 |
|---|---|
| 根拠のない回答を禁止 | プロンプトに「参考情報のみを根拠に回答」と明記 |
| 参照元を表示 | 回答と一緒に文書名・関連度スコアを表示 |
| 「見つからない」回答を許可 | 情報がない場合は「該当する情報が見つかりません」と返す |
| スコアしきい値 | 関連度が低いチャンクを除外する(score < 0.5 など) |
# スコアしきい値の例(検索精度向上)
MIN_SCORE = 0.5
records = [r for r in search_result["records"] if r["score"] >= MIN_SCORE]
if not records:
result = {"answer": "該当する情報が見つかりませんでした。", "sources": []}
動作確認
手順
# 1. Dify KnowledgeにFAQ文書を登録(Dify GUI上で実施)
# 2. .env に DIFY_DATASET_ID を設定
# 3. Flaskアプリを起動
python app.py
# 4. ブラウザで http://localhost:5000/search にアクセス
確認ポイント
| 操作 | 期待する動作 |
|---|---|
| 「有給休暇は何日前に申請?」と入力 | 就業規則に基づいた回答が表示される |
| 回答の下に参照元が表示される | 文書名・関連度スコアが確認できる |
| 登録していない内容を質問する | 「該当する情報が見つかりません」と表示される |
| DIFY_DATASET_ID 未設定で検索 | 「DIFY_DATASET_ID が設定されていません」と表示される |
| 空欄で検索ボタンを押す | 「質問を入力してください」と表示される |
今後の連載予定
| 回 | タイトル | 主な内容 |
|---|---|---|
| 第1回 | 業務システム全体設計編 | システム概要・技術選定・アーキテクチャ |
| 第2回 | 要求定義・要件定義編 | 業務分析・機能要件・非機能要件 |
| 第3回 | ER図・画面設計編 | テーブル設計・画面遷移図・ワイヤーフレーム |
| 第4回 | Flaskログイン機能編 | セッション・パスワードハッシュ・認証ミドルウェア |
| 第5回 | 社員管理CRUD編 | 一覧・登録・更新・削除・バリデーション |
| 第6回 | 研修管理編 | リレーション・集計・出席率自動計算 |
| 第7回 | Excel業務自動化編 | pandas取込・openpyxl出力・テンプレート活用 |
| 第8回 | Dify API連携編 | プロンプト設計・API呼び出し・エラーハンドリング |
| 第9回 | RAG構築編(本記事) | Knowledgeへの登録・Embedding・検索精度改善 |
| 第10回 | FAQチャットボット編 | チャットUI・会話履歴・ストリーミングレスポンス |
| 第11回 | GitHubチーム開発編 | ブランチ戦略・Pull Request・コードレビュー |
| 第12回 | テスト・発表編 | pytest・テスト設計・デモ・振り返り |
おわりに
第9回では、Dify Knowledgeを使ったRAG社内文書検索を実装しました。
ポイントを振り返ります。
- RAGの仕組み:検索(Retrieval)→ 生成(Generation)の2段階アーキテクチャを理解した
- Embedding:テキストをベクトルに変換し、類似度で検索する仕組みを学んだ
- Dify Knowledge:文書登録・チャンク分割・検索設定の手順を確認した
- 検索精度改善:FAQ形式への変換・チャンクサイズ最適化・メタデータ付与のポイントを整理した
- ハルシネーション対策:プロンプトでの制約指定・参照元表示・スコアしきい値で誤回答を防いだ
- Python実装:検索API呼び出し → 回答生成 → 画面表示の一連のフローを実装した
次回は「FAQチャットボット編」として、チャットUI・会話履歴保持・ストリーミングレスポンスを実装します。
RAG検索の上に「会話形式」のインターフェースを被せることで、より自然な問い合わせ体験を実現します。