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?

Python × Dify × RAGで学ぶ業務システム開発入門【第9回 RAG構築編】

0
Last updated at Posted at 2026-06-20

はじめに

前回は、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 への文書登録

登録する文書の例

文書名 内容 ファイル形式
就業規則 勤務時間・有給・休暇のルール PDF
人事評価基準書 評価項目・基準・プロセス PDF
社内マニュアル 業務手順・ツール使用方法 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検索の上に「会話形式」のインターフェースを被せることで、より自然な問い合わせ体験を実現します。

参考リンク

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?