はじめに
前回は、Dify APIを使った評価コメントの自動生成を実装しました。
前回の記事:Python × Dify × RAGで学ぶ業務システム開発入門【第8回 Dify API連携編】
第9回では、Dify Knowledgeを利用した社内文書検索画面を実装します。
本記事の内容は、以下のリポジトリにある最新ソースコードに合わせています。
RAG(Retrieval-Augmented Generation)は、質問に関連する文書を検索し、その情報をもとに生成AIが回答する仕組みです。
利用者が質問を入力
↓
FlaskがDify Chat APIを呼び出す
↓
DifyアプリがKnowledgeから関連情報を検索
↓
検索結果をもとに回答を生成
↓
Flaskの検索結果画面へ表示
現行ソースコードはDify Knowledgeの検索APIを直接呼び出しません。Knowledgeを設定したDifyアプリのChat APIへ質問を送り、Dify側で検索と回答生成を行う構成です。
本記事で学ぶこと
| 項目 | 内容 |
|---|---|
| RAGの基本 | 検索と生成を組み合わせる仕組み |
| Embedding | テキストをベクトル化して類似度を比較する考え方 |
| Dify Knowledge | 社内文書を登録し、Difyアプリから利用する方法 |
| Flask実装 | 検索フォームから質問を受け取る方法 |
| Dify API連携 | Chat APIへ質問を送信して回答を取得する方法 |
| エラー処理 | 空欄・APIキー未設定・通信失敗時の動作 |
RAGの仕組み
通常の生成AIは、学習済みの一般知識をもとに回答します。そのため、就業規則や社内マニュアルなど、組織固有の情報を正確に把握しているとは限りません。
RAGでは、回答を生成する前に関連文書を検索します。
ステップ1:Retrieval(検索)
質問 → ベクトル化 → 関連する文書断片を取得
ステップ2:Generation(生成)
質問 + 関連文書 → LLM → 文書を根拠に回答
たとえば、利用者が「有給休暇の申請方法は?」と質問すると、DifyはKnowledgeから関連する記述を探し、その内容をもとに回答します。
| 方式 | メリット | 注意点 |
|---|---|---|
| LLM単体 | 一般的な質問に幅広く回答できる | 社内固有の情報を知らない |
| キーワード検索 | 一致する語句を高速に探せる | 表現が異なる質問に弱い |
| RAG | 社内文書を根拠として自然言語で回答できる | 文書整備と回答検証が必要 |
Embeddingとは
Embedding(埋め込み)は、文章の意味を数値のベクトルへ変換する技術です。
「有給休暇の申請方法」
↓ Embeddingモデル
[0.12, -0.34, 0.56, 0.89, ...]
質問と文書をそれぞれベクトルへ変換し、距離が近い文書を関連度の高い情報として取得します。そのため、「有休の取り方」と「年次有給休暇の申請手続き」のように表現が異なっていても、意味が近ければ検索候補にできます。
現行システムでは、Embeddingの生成、チャンク管理、類似度検索をDify Knowledge側へ任せています。Flask側でベクトルDBやEmbeddingモデルを直接操作するコードはありません。
Dify側の準備
登録する文書の例
| 文書名 | 内容 | 形式例 |
|---|---|---|
| 就業規則 | 勤務時間・休暇・申請ルール | PDF / Markdown |
| 人事評価基準書 | 評価項目・基準・プロセス | PDF / Markdown |
| 社内マニュアル | 業務手順・ツールの使い方 | Word / Markdown |
| 研修資料 | 研修の目的・カリキュラム | PDF / PowerPoint |
| FAQ集 | よくある質問と回答 | Markdown / CSV |
Knowledgeとアプリを接続する
Dify側では、概ね次の順序で準備します。
- Knowledgeを作成する
- 社内文書を登録する
- チャンク分割と検索設定を確認する
- ChatアプリまたはChatflowを作成する
- アプリからKnowledgeを参照できるようにする
- APIキーを発行する
具体的なDify画面の操作、OpenAIモデルの設定、Knowledgeへの文書登録、チャンク設定、APIキー取得は、次の記事で解説しています。
Dify × OpenAI × RAG入門 ― 社内ナレッジを回答するRAGアプリケーション「Human Resource AI」を作ってみる
Flaskへ設定するAPIキーは、KnowledgeそのもののIDではなく、Knowledgeを利用するDifyアプリのAPIキーです。現行コードでは DIFY_DATASET_ID を使用しません。
検索精度を改善するポイント
検索精度は、登録文書の内容とDify側の設定に大きく影響されます。
1. 1つの見出しに1つのテーマをまとめる
複数の制度を1つの長い段落へ混在させず、休暇、勤務時間、評価、研修などのテーマごとに見出しを分けます。
2. FAQ形式を併用する
Q: 有給休暇は何日前までに申請しますか?
A: 就業規則第12条に基づき、原則として3営業日前までに申請します。
緊急時は直属の上長へ連絡してください。
実際に利用者が入力する表現に近い質問文を含めると、関連情報を取得しやすくなります。
3. 文書の出典と更新日を記載する
文書名:就業規則
対象:全社員
改定日:2026-04-01
管理部署:人事部
4. 古い文書を残さない
同じ制度について新旧の文書が混在すると、誤った回答につながります。規程改定時はDify Knowledgeの文書も更新します。
精度改善チェックリスト
| 項目 | 確認内容 |
|---|---|
| 網羅性 | よくある質問の根拠が文書に含まれているか |
| チャンク | 1つの断片に複数テーマが混在していないか |
| 見出し | 文書構造が明確になっているか |
| FAQ | 頻出質問をQ&A形式で補足しているか |
| 鮮度 | 古い規程や重複文書が残っていないか |
| 検証 | 想定質問で定期的に回答を確認しているか |
環境変数を設定する
プロジェクト直下の .env に、DifyアプリのAPIキーとAPI URLを設定します。
DIFY_API_KEY=取得したDifyアプリのAPIキー
DIFY_API_URL=https://api.dify.ai/v1
config.py では次のように読み込みます。
import os
from dotenv import load_dotenv
load_dotenv()
class Config:
DIFY_API_KEY = os.getenv("DIFY_API_KEY", "")
DIFY_API_URL = os.getenv("DIFY_API_URL", "https://api.dify.ai/v1")
.env は .gitignore に追加し、APIキーをGitHubへコミットしないでください。
実装1:Dify APIを呼び出す
services/dify_service.py の search_knowledge() が、Dify Chat APIへ質問を送ります。
def search_knowledge(question: str, top_k: int = 3) -> dict:
if not Config.DIFY_API_KEY:
return {
"records": [
{
"title": "ローカル確認用サンプル",
"content": (
"DIFY_API_KEY を .env に設定すると、"
"Dify Knowledge / Chat API に問い合わせます。"
),
"score": 1.0,
}
]
}
payload = {
"inputs": {"top_k": top_k},
"query": question,
"response_mode": "blocking",
"user": "human-resource-app",
}
try:
res = requests.post(
f"{Config.DIFY_API_URL}/chat-messages",
headers=_headers(),
json=payload,
timeout=30,
)
res.raise_for_status()
data = res.json()
answer = data.get("answer", "")
return {
"records": [
{
"title": "Dify回答",
"content": answer,
"score": 1.0,
}
] if answer else []
}
except requests.RequestException as exc:
return {"records": [], "error": str(exc)}
リクエスト内容
| 項目 | 値 | 役割 |
|---|---|---|
inputs |
{"top_k": top_k} |
Difyアプリへ追加変数を渡す |
query |
利用者の質問 | Difyへ送信する本文 |
response_mode |
blocking |
回答完了までHTTP接続を待つ |
user |
human-resource-app |
Dify側の固定利用者ID |
timeout |
30秒 | 長時間応答しない通信を終了する |
top_k はDifyアプリ側で同名の入力変数を利用する場合に参照できます。ただし、現行のFlaskコード自体が取得チャンク数を制御するわけではありません。実際の検索件数や検索方式はDify側のKnowledge設定にも依存します。
APIキー未設定時の動作
APIキーが未設定の場合は、エラーで停止せず「ローカル確認用サンプル」を1件返します。この動作により、Dify接続前でも検索画面のレイアウトを確認できます。
Difyから返る回答の扱い
現行実装はDifyレスポンスの answer を取り出し、次の固定情報を付けた1件の検索結果に変換します。
{
"title": "Dify回答",
"content": answer,
"score": 1.0,
}
この score はKnowledge検索から取得した実際の関連度ではなく、画面表示用の固定値です。また、現行実装では参照元文書名やチャンク本文をDifyレスポンスから抽出していません。
実装2:検索サービスを作る
services/search_service.py は、画面とDify連携の間に置く薄いラッパーです。
"""RAG検索・FAQチャットの薄いラッパー"""
from services.dify_service import search_knowledge, chat_with_faq
def search_documents(question: str) -> dict:
if not question.strip():
return {
"records": [],
"error": "検索キーワードを入力してください。",
}
return search_knowledge(question)
strip() 後の文字列が空ならDify APIを呼ばず、入力エラーを返します。HTMLには required も設定していますが、サーバー側でも検証することで直接リクエストされた場合に備えます。
実装3:Flaskの検索ルート
app.py には、GETとPOSTの両方を受け付ける /search を定義します。
from services.search_service import search_documents
@app.route("/search", methods=["GET", "POST"])
@login_required
def search():
result = None
question = ""
if request.method == "POST":
question = request.form.get("question", "")
result = search_documents(question)
return render_template(
"search.html",
result=result,
question=question,
)
GETとPOSTの動作
| リクエスト | 動作 |
|---|---|
GET /search |
入力フォームだけを表示する |
POST /search |
質問を受け取り、Difyの回答またはエラーを表示する |
@login_required が付いているため、未ログインの利用者は検索画面へアクセスできません。ロール制限はないため、admin、staff、user、社員番号でログインした社員のいずれも利用できます。
実装4:検索画面
templates/search.html は、検索フォームと検索結果を同じ画面に表示します。
{% extends "base.html" %}
{% block title %}社内文書検索 - AI人事研修システム{% endblock %}
{% block content %}
<div class="content-header">
<div>
<h1>社内文書検索</h1>
<p>Dify Knowledgeに登録した社内規則・マニュアルを検索します。</p>
</div>
</div>
<section class="page-card rag-page">
<h2>🔍 社内文書検索(RAG)</h2>
<p class="lead">
知りたい内容を自然な文章で入力してください。
関連する社内文書をもとに回答します。
</p>
<form method="POST" class="rag-search-box">
<div class="rag-search-row">
<input
type="text"
name="question"
value="{{ question }}"
placeholder="例:テレワーク申請のルールを教えてください"
required
>
<button class="btn btn-primary" type="submit">検索</button>
</div>
<p class="help-text">
例:評価基準を教えてください / 有給休暇の申請方法は?
/ 研修レポートの提出方法は?
</p>
</form>
{% if result %}
{% if result.error %}
<div class="alert alert-warning">{{ result.error }}</div>
{% endif %}
<h2 class="result-title">検索結果</h2>
<div class="result-list">
{% for rec in result.records %}
<article class="answer-card">
<div class="answer-label">AI回答</div>
<h3>{{ rec.title }}</h3>
<p>{{ rec.content }}</p>
<div class="answer-meta">
関連度スコア:{{ rec.score }}
</div>
</article>
{% else %}
<div class="empty-state">
検索結果がありません。質問を変えてもう一度お試しください。
</div>
{% endfor %}
</div>
{% endif %}
</section>
{% endblock %}
入力した質問は value="{{ question }}" に設定し、検索後もフォームへ残します。回答内の改行はCSSの white-space: pre-wrap で維持します。
画面のスタイル
RAG検索画面の主要なスタイルは static/css/style.css に定義されています。
.rag-search-row {
display: flex;
gap: 10px;
}
.rag-search-row input {
flex: 1;
font-size: 16px;
}
.result-list {
display: grid;
gap: 14px;
}
.answer-card {
border: 1px solid #dbe4f0;
border-left: 5px solid var(--blue);
border-radius: 12px;
padding: 20px;
background: #fff;
}
.answer-card p {
line-height: 1.8;
white-space: pre-wrap;
}
画面幅が700px以下の場合は、検索入力欄とボタンを1列に切り替えます。
処理の流れ
検索ボタンを押してから結果が表示されるまでの流れは次のとおりです。
1. 利用者が質問を入力して「検索」を押す
2. POST /search が質問を受け取る
3. search_documents() が空欄を検証する
4. search_knowledge() がDify Chat APIを呼び出す
5. DifyアプリがKnowledgeを参照して回答する
6. answerを1件のrecordsへ変換する
7. search.htmlが回答またはエラーを表示する
動作確認
1. Dify側を準備する
DifyのKnowledgeへ社内文書を登録し、そのKnowledgeを参照するアプリを作成します。
2. .env を設定する
DIFY_API_KEY=取得したDifyアプリのAPIキー
DIFY_API_URL=https://api.dify.ai/v1
3. Flaskアプリを起動する
Windows PowerShellで次を実行します。
python app.py
4. 検索画面を開く
ログイン後に、次のURLへアクセスします。
http://localhost:5000/search
確認項目
| 操作・状態 | 期待する動作 |
|---|---|
| 社内文書に含まれる質問を送る | Difyが生成した回答を表示する |
| 検索後の入力欄 | 入力した質問が残る |
| 空欄で送信する | ブラウザの required またはサーバー側検証で防止する |
| Difyが空の回答を返す | 「検索結果がありません」と表示する |
| Dify APIとの通信に失敗する | 警告としてエラー内容を表示する |
| APIキーを設定しない | ローカル確認用サンプルを表示する |
未ログインで /search を開く |
ログイン画面へ遷移する |
ハルシネーション対策
RAGを利用しても、誤った回答を完全には防げません。対策の中心はDify側の指示とKnowledgeの品質です。
| 対策 | 内容 |
|---|---|
| 根拠を限定する | Knowledgeの内容を優先して回答するようDify側で指示する |
| 情報がない場合を定義する | 根拠がない場合は「確認できません」と回答させる |
| 文書を最新化する | 古い規程や重複文書を削除する |
| 想定質問で検証する | 正解が分かっている質問を定期的に実行する |
| 人による確認を残す | 重要な手続きは正式文書や担当部署でも確認する |
検索結果は社内規程そのものではなく、生成AIによる回答です。人事・労務・法務に関わる重要な判断では、必ず原文と担当部署を確認してください。
現行実装の注意点
| 項目 | 現行仕様 | 改善例 |
|---|---|---|
| Knowledge検索 | Difyアプリ内で実行 | 検索設定をDify側で明文化する |
| 表示件数 | Difyの回答を1件表示 | 回答と参照チャンクを分けて表示する |
| タイトル |
Dify回答 固定 |
Difyのメタデータから文書名を取得する |
| 関連度 |
1.0 固定 |
実際の検索スコアを取得して表示する |
| 参照元 | 未表示 | Difyレスポンスのメタデータを解析する |
| 会話履歴 | 検索画面では保持しない | 会話形式は第10回のFAQチャットで扱う |
| 応答方式 | blocking |
生成中表示や非同期処理を追加する |
| 通信エラー | 例外文字列を画面表示 | 利用者向け文言とサーバーログを分離する |
記事上の「RAGの仕組み」と、現行Flaskコードが直接実装している範囲は分けて考える必要があります。検索・Embedding・チャンク取得・回答生成の中心はDify側で、Flask側は質問送信と回答表示を担当します。
おわりに
第9回では、Dify Knowledgeを利用した社内文書検索画面を実装しました。
- RAGが検索と生成を組み合わせる仕組みであることを確認した
- Embeddingと類似度検索はDify Knowledge側に任せた
- FlaskからDifyの
/chat-messagesへ質問を送信した -
search_service.pyで空欄入力を検証した - Difyの回答を検索結果カードとして表示した
- APIキー未設定時はローカル確認用サンプルを返した
- 通信失敗時は警告を表示した
- 現行実装のスコア
1.0は実際の関連度ではないことを整理した
次回は「FAQチャットボット編」として、会話IDを利用した文脈保持、チャットUI、回答生成中表示、二重送信防止を実装します。
次回:Python × Dify × RAGで学ぶ業務システム開発入門【第10回 FAQチャットボット編】
本連載について
| 回 | タイトル | 対応画面 | 主な内容 |
|---|---|---|---|
| 第1回 | 業務システム全体設計編 | ― | システム概要・技術選定・アーキテクチャ |
| 第2回 | 要求定義・要件定義編 | ― | 業務分析・機能要件・非機能要件 |
| 第3回 | ER図・画面設計編 | ― | テーブル設計・画面遷移図・ワイヤーフレーム |
| 第4回 | Flaskログイン機能編 | 🔐 ログイン | セッション・ハッシュ・ロール認可・社員番号ログイン |
| 第5回 | 社員管理CRUD編 | 👤 社員管理 | 一覧・検索・登録・更新・論理削除・入力制御 |
| 第6回 | 研修管理編 | 📚 研修管理 | 研修登録・LEFT JOIN・履歴一括入力・UPSERT |
| 第7回 | Excel業務自動化編 | 📥 取込 / 📤 出力 | pandas取込・openpyxl出力 |
| 第8回 | Dify API連携編 | 📊 評価管理 | API・フォールバック・生成中UI・二重送信防止 |
| 第9回 | RAG構築編 | 🔍 文書検索 | Dify Chat API・質問送信・回答表示・エラー処理 |
| 第10回 | FAQチャットボット編 | 💬 FAQチャット | 会話ID・Fetch API・生成中表示・二重送信防止 |
| 公開編 | Ubuntu VPS・Docker公開編 | 🌐 サービス公開 | Ubuntu VPS・Docker・Nginxを使った本番環境構築とWebアプリケーション公開 |

