3
2

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で学ぶ業務システム開発入門【第8回 Dify API連携編】

3
Last updated at Posted at 2026-06-20

はじめに

前回は、pandasによるExcelインポートとopenpyxlによるExcelエクスポートを実装しました。

前回の記事:Python × Dify × RAGで学ぶ業務システム開発入門【第7回 Excel業務自動化編】

第8回では、いよいよ生成AI(Dify API)との連携を実装します。

ここまで第4回〜第7回で構築してきたのは「従来型の業務システム」です。
本回からはそこにAIの力を組み合わせます。

従来型の業務システム(第4回〜第7回)
  → データの登録・更新・検索・出力
  → 正確だが「判断」や「文章生成」はできない

AIを組み合わせるとできること(第8回〜)
  → 受講者データから評価コメントを自動生成する
  → 社内文書を自然言語で検索・質問回答する
  → よくある質問にチャットボットが自動対応する

本連載は「作って終わり」ではなく、なぜそう設計するのかを重視しています。
設計の意図を理解することで、他のシステムにも応用できる力が身につきます。

本記事で学ぶこと

項目 内容
Dify とは 生成AIアプリケーション構築プラットフォームの概要
プロンプト設計 良い結果を得るためのプロンプトの書き方
API呼び出し Python requests での Dify API 利用方法
エラーハンドリング APIタイムアウト・レート制限への対処
DB保存 生成したコメントを evaluation テーブルに保存する

Dify とは

Dify は、生成AIアプリケーションを構築するためのプラットフォームです。

Difyでできること
  ├── チャットボット作成(会話型AI)
  ├── テキスト生成(文章・要約・翻訳)
  ├── Knowledge(RAG)による文書検索
  └── API経由で外部システムと連携

本システムでは Dify を以下の用途で使います。

用途 Dify機能 実装回
評価コメント自動生成 Chat API 第8回(本記事)
社内文書検索(RAG) Knowledge API 第9回
FAQチャットボット Chat API + Knowledge 第10回

なぜ Dify を選ぶのか

理由 説明
APIキー1つで利用開始 環境構築が最小限で済む
RAGと生成AIを同一プラットフォームで扱える 技術スタックがシンプルになる
GUIでプロンプト調整できる コード変更なしで出力品質を改善できる
無料プランでも学習用途に十分 学習コストが低い

Dify API の基本

エンドポイントとリクエスト形式

POST https://api.dify.ai/v1/chat-messages

Headers:
  Authorization: Bearer {DIFY_API_KEY}
  Content-Type: application/json

Body:
{
  "inputs": {},
  "query": "ユーザーの入力テキスト",
  "response_mode": "blocking",
  "user": "識別子"
}
パラメータ 説明
inputs Difyアプリの変数(今回は空で可)
query 送信するプロンプト・質問文
response_mode "blocking"=レスポンス完了まで待つ / "streaming"=逐次返す
user リクエスト元の識別子(ログ管理用)

レスポンス形式

{
  "answer": "生成されたテキスト",
  "conversation_id": "会話ID",
  "message_id": "メッセージID"
}

プロンプト設計

生成AIの出力品質はプロンプトの書き方で決まります

良いプロンプトの構成要素

1. 役割の定義     → AIに「何者として」振る舞うかを指示
2. タスクの明示   → 何を生成するかを具体的に伝える
3. 入力データ     → 判断材料となる情報を提供
4. 出力形式の指定 → 文字数・形式・トーンを指定
5. 制約条件       → やってはいけないことを明示

評価コメント生成のプロンプト例

EVALUATION_PROMPT = """
あなたは人事評価の専門家です。
以下の研修受講データをもとに、受講者への評価コメントを生成してください。

## 入力データ
- 対象者:{name}
- 研修名:{training_name}
- 出席率:{attendance}%
- 理解度:{understanding}点(100点満点)
- 課題提出スコア:{report_score}点(100点満点)

## 出力条件
- 200字以内で記述すること
- 前向きで建設的な表現を使うこと
- 具体的な改善点または期待を1つ含めること
- 敬体(です・ます調)で記述すること
- 数値データを引用して根拠を示すこと

## 禁止事項
- ネガティブな表現のみで終わらないこと
- 他の受講者との比較をしないこと
"""

良いプロンプト vs 悪いプロンプト

❌ 悪い例
「評価コメントを書いて」
→ 何のデータをもとに、どの程度の品質で書けばよいかわからない

✅ 良い例(上記のプロンプト)
→ 役割・データ・出力形式・制約がすべて明示されている
→ AIが迷わず一貫した品質の出力を返せる

実装:dify_service.py

Dify APIとの通信を services/dify_service.py に実装します。

# services/dify_service.py
import requests
import time
from config import Config


# ============================================================
# 評価コメント生成
# ============================================================

EVALUATION_PROMPT = """
あなたは人事評価の専門家です。
以下の研修受講データをもとに、受講者への評価コメントを生成してください。

## 入力データ
- 対象者:{name}
- 研修名:{training_name}
- 出席率:{attendance}%
- 理解度:{understanding}点(100点満点)
- 課題提出スコア:{report_score}点(100点満点)

## 出力条件
- 200字以内で記述すること
- 前向きで建設的な表現を使うこと
- 具体的な改善点または期待を1つ含めること
- 敬体(です・ます調)で記述すること
- 数値データを引用して根拠を示すこと

## 禁止事項
- ネガティブな表現のみで終わらないこと
- 他の受講者との比較をしないこと
""".strip()


def generate_evaluation_comment(
    name: str,
    training_name: str,
    attendance: float,
    understanding: int,
    report_score: int,
) -> dict:
    """
    Dify APIを使って評価コメントを自動生成する

    Args:
        name:           受講者氏名
        training_name:  研修名
        attendance:     出席率(0〜100)
        understanding:  理解度スコア(0〜100)
        report_score:   課題スコア(0〜100)

    Returns:
        {
            "success": True/False,
            "comment": "生成されたコメント" or "",
            "error":   "" or "エラーメッセージ"
        }
    """
    # プロンプトにデータを埋め込む
    prompt = EVALUATION_PROMPT.format(
        name=name,
        training_name=training_name,
        attendance=attendance,
        understanding=understanding,
        report_score=report_score,
    )

    # Dify API 呼び出し
    return _call_dify_chat(prompt)


def _call_dify_chat(query: str, max_retries: int = 2) -> dict:
    """
    Dify Chat APIを呼び出す(リトライ付き)

    Args:
        query:       送信するプロンプト
        max_retries: 最大リトライ回数

    Returns:
        {"success": bool, "comment": str, "error": str}
    """
    if not Config.DIFY_API_KEY:
        return {
            "success": False,
            "comment": "",
            "error": "DIFY_API_KEY が設定されていません(.env を確認してください)"
        }

    headers = {
        "Authorization": f"Bearer {Config.DIFY_API_KEY}",
        "Content-Type": "application/json",
    }
    payload = {
        "inputs": {},
        "query": query,
        "response_mode": "blocking",
        "user": "hr-system",
    }

    for attempt in range(max_retries + 1):
        try:
            response = requests.post(
                f"{Config.DIFY_API_URL}/chat-messages",
                headers=headers,
                json=payload,
                timeout=30,  # 30秒でタイムアウト
            )

            # HTTPステータスコードのチェック
            if response.status_code == 200:
                data = response.json()
                answer = data.get("answer", "").strip()
                if answer:
                    return {"success": True, "comment": answer, "error": ""}
                else:
                    return {
                        "success": False,
                        "comment": "",
                        "error": "AIからの応答が空でした"
                    }

            elif response.status_code == 429:
                # レート制限:少し待ってリトライ
                if attempt < max_retries:
                    time.sleep(2 ** attempt)  # 指数バックオフ
                    continue
                return {
                    "success": False,
                    "comment": "",
                    "error": "APIリクエストが多すぎます。しばらく待ってから再試行してください"
                }

            else:
                return {
                    "success": False,
                    "comment": "",
                    "error": f"APIエラー(ステータス: {response.status_code}"
                }

        except requests.exceptions.Timeout:
            if attempt < max_retries:
                continue
            return {
                "success": False,
                "comment": "",
                "error": "APIリクエストがタイムアウトしました(30秒)"
            }

        except requests.exceptions.ConnectionError:
            return {
                "success": False,
                "comment": "",
                "error": "Dify APIに接続できません。ネットワーク環境を確認してください"
            }

        except Exception as e:
            return {
                "success": False,
                "comment": "",
                "error": f"予期しないエラー: {str(e)}"
            }

    return {"success": False, "comment": "", "error": "リトライ上限に達しました"}

エラーハンドリングの設計

API連携では必ずエラーが起きる前提で設計します。

エラー種別 原因 対処
Timeout Difyの応答が遅い 30秒でタイムアウト。リトライ2回
429 Rate Limit リクエスト頻度が高すぎる 指数バックオフで待機してリトライ
ConnectionError ネットワーク切断 即時エラー返却(リトライしない)
200 だが空レスポンス プロンプト不備・モデル問題 エラーメッセージを返す
APIキー未設定 .env の設定漏れ 呼び出し前にチェックして即時返却

指数バックオフとは

1回目の失敗 → 1秒待つ(2^0)
2回目の失敗 → 2秒待つ(2^1)
3回目の失敗 → 4秒待つ(2^2)
→ 待ち時間を徐々に延ばすことで、サーバーへの負荷を抑える
# 指数バックオフの実装
import time
time.sleep(2 ** attempt)  # attempt=0→1秒, attempt=1→2秒

実装:評価コメントのDB保存

生成したコメントを evaluation テーブルに保存します。

# services/evaluation_service.py
from database import get_db
from services.dify_service import generate_evaluation_comment


def get_evaluation_data(training_id: int) -> list[dict]:
    """
    研修の受講者ごとの評価データを取得する
    既存のAIコメントがあればそれも含める
    """
    conn = get_db()
    rows = conn.execute(
        """
        SELECT
            e.id AS employee_id,
            e.employee_no,
            e.name,
            e.department,
            th.attendance_rate,
            th.understanding_level,
            th.report_score,
            ev.ai_comment,
            ev.score,
            ev.created_at AS comment_date
        FROM training_history th
        JOIN employee e ON th.employee_id = e.id
        LEFT JOIN evaluation ev ON ev.employee_id = e.id
        WHERE th.training_id = ?
        ORDER BY e.employee_no
        """,
        (training_id,)
    ).fetchall()
    conn.close()
    return [dict(row) for row in rows]


def generate_and_save_comment(
    employee_id: int,
    name: str,
    training_name: str,
    attendance: float,
    understanding: int,
    report_score: int,
) -> dict:
    """
    AIコメントを生成してDBに保存する

    Returns:
        {"success": bool, "comment": str, "error": str}
    """
    # Dify APIでコメント生成
    result = generate_evaluation_comment(
        name=name,
        training_name=training_name,
        attendance=attendance,
        understanding=understanding,
        report_score=report_score,
    )

    if not result["success"]:
        return result

    # 総合スコア計算
    total_score = round(
        (attendance + understanding + report_score) / 3, 1
    )

    # DB保存(既存があれば更新、なければ新規登録)
    conn = get_db()
    existing = conn.execute(
        "SELECT id FROM evaluation WHERE employee_id = ?",
        (employee_id,)
    ).fetchone()

    if existing:
        conn.execute(
            """
            UPDATE evaluation
            SET score = ?, ai_comment = ?, created_at = CURRENT_TIMESTAMP
            WHERE id = ?
            """,
            (total_score, result["comment"], existing["id"])
        )
    else:
        conn.execute(
            """
            INSERT INTO evaluation (employee_id, score, ai_comment)
            VALUES (?, ?, ?)
            """,
            (employee_id, total_score, result["comment"])
        )

    conn.commit()
    conn.close()
    return result

LEFT JOIN evaluation を使うことで、まだコメントが生成されていない受講者も含めて一覧を取得できます。
JOIN(INNER JOIN)だとコメントが存在する行しか返りません。

実装:app.py(評価管理ルーティング)

# app.py(評価管理部分)
from services.evaluation_service import (
    get_evaluation_data, generate_and_save_comment
)
from services.training_service import get_all_trainings, get_training_by_id


@app.route("/evaluations")
@login_required
@roles_required("admin", "staff")
def evaluation_list():
    """評価一覧(研修を選択する画面)"""
    trainings = get_all_trainings()
    return render_template("evaluation_select.html", trainings=trainings)


@app.route("/evaluations/<int:training_id>")
@login_required
@roles_required("admin", "staff")
def evaluation_detail(training_id):
    """研修ごとの評価一覧"""
    training = get_training_by_id(training_id)
    if training is None:
        flash("研修が見つかりません", "error")
        return redirect(url_for("evaluation_list"))

    evaluations = get_evaluation_data(training_id)
    return render_template(
        "evaluation.html",
        training=training,
        evaluations=evaluations
    )


@app.route("/evaluations/<int:training_id>/generate/<int:employee_id>",
           methods=["POST"])
@login_required
@roles_required("admin", "staff")
def evaluation_generate(training_id, employee_id):
    """AI評価コメント生成"""
    training = get_training_by_id(training_id)
    if training is None:
        flash("研修が見つかりません", "error")
        return redirect(url_for("evaluation_list"))

    # 受講データを取得
    evaluations = get_evaluation_data(training_id)
    target = next(
        (ev for ev in evaluations if ev["employee_id"] == employee_id),
        None
    )
    if target is None:
        flash("受講者が見つかりません", "error")
        return redirect(url_for("evaluation_detail",
                                training_id=training_id))

    # AIコメント生成+保存
    result = generate_and_save_comment(
        employee_id=employee_id,
        name=target["name"],
        training_name=training["training_name"],
        attendance=target["attendance_rate"],
        understanding=target["understanding_level"],
        report_score=target["report_score"],
    )

    if result["success"]:
        flash(f"{target['name']} さんの評価コメントを生成しました", "success")
    else:
        flash(f"コメント生成に失敗しました:{result['error']}", "error")

    return redirect(url_for("evaluation_detail",
                            training_id=training_id))

実装:テンプレート(evaluation.html)

{% extends "base.html" %}
{% block title %}評価管理 - {{ training.training_name }}{% endblock %}

{% block content %}
<div class="page-header">
  <h1>📊 評価管理:{{ training.training_name }}</h1>
  <a href="{{ url_for('evaluation_list') }}" class="btn btn-secondary">← 研修選択</a>
</div>

<table class="table">
  <thead>
    <tr>
      <th>社員番号</th>
      <th>氏名</th>
      <th>出席率</th>
      <th>理解度</th>
      <th>課題</th>
      <th>AIコメント</th>
      <th>操作</th>
    </tr>
  </thead>
  <tbody>
    {% for ev in evaluations %}
    <tr>
      <td>{{ ev.employee_no }}</td>
      <td>{{ ev.name }}</td>
      <td>{{ ev.attendance_rate }}%</td>
      <td>{{ ev.understanding_level }}点</td>
      <td>{{ ev.report_score }}点</td>
      <td class="comment-cell">
        {% if ev.ai_comment %}
          <div class="ai-comment">{{ ev.ai_comment }}</div>
          <small class="text-muted">生成日: {{ ev.comment_date }}</small>
        {% else %}
          <span class="text-muted">(未生成)</span>
        {% endif %}
      </td>
      <td>
        <form method="POST"
              action="{{ url_for('evaluation_generate',
                                 training_id=training.id,
                                 employee_id=ev.employee_id) }}">
          <button type="submit" class="btn btn-small btn-primary"
                  onclick="this.disabled=true; this.innerText='生成中...'; this.form.submit();">
            {% if ev.ai_comment %}再生成{% else %}生成{% endif %}
          </button>
        </form>
      </td>
    </tr>
    {% endfor %}
  </tbody>
</table>

<!-- Excel出力リンク -->
<div class="form-actions">
  <a href="{{ url_for('export_excel', training_id=training.id) }}"
     class="btn btn-secondary">📤 評価シートをExcel出力</a>
</div>
{% endblock %}

Dify アプリの設定手順

Dify 側の設定方法を簡単に紹介します。

1. Dify アカウント作成

https://cloud.dify.ai でアカウントを作成します(無料プランで可)。

2. アプリ作成

Difyダッシュボード
  → 「アプリを作成」
  → 「チャットボット」を選択
  → アプリ名: "HR Evaluation Comment Generator"

3. APIキー取得

アプリ設定 → 「APIアクセス」→ 「APIキーを作成」
  → 生成された文字列を .env の DIFY_API_KEY に設定

4. .env に設定

DIFY_API_KEY=app-xxxxxxxxxxxxxxxxxxxxxxxx
DIFY_API_URL=https://api.dify.ai/v1

APIキーは絶対にGitにコミットしないでください。
.env ファイルで管理し、.gitignore で除外してください。

動作確認

手順

# .env に DIFY_API_KEY を設定してからアプリ起動
python app.py

# ブラウザで http://localhost:5000/evaluations にアクセス

確認ポイント

操作 期待する動作
評価一覧で研修を選択 受講者一覧とスコアが表示される
「生成」ボタンを押す AIコメントが生成され表示される
コメントにデータの引用が含まれる 出席率や理解度の数値がコメント内で言及される
「再生成」ボタンを押す 新しいコメントで上書きされる
APIキー未設定で生成ボタンを押す 「DIFY_API_KEY が設定されていません」と表示される
ネットワーク切断状態で押す 「Dify APIに接続できません」と表示される

今後の連載予定

タイトル 主な内容
第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・テスト設計・デモ・振り返り

おわりに

第8回では、Dify APIを使った評価コメント自動生成を実装しました。

ポイントを振り返ります。

  • プロンプト設計:役割・タスク・データ・出力形式・制約の5要素を含む構造化プロンプトを作成した
  • API呼び出しrequests.post でDify APIにリクエストを送信し、レスポンスからコメントを取得した
  • エラーハンドリング:タイムアウト・レート制限・接続エラーに対して適切な処理を実装した
  • 指数バックオフ:429エラー時に待ち時間を段階的に延ばすリトライ戦略を採用した
  • DB保存:生成したコメントを evaluation テーブルにUPSERT(更新 or 新規登録)した
  • LEFT JOIN:コメント未生成の受講者も含めて一覧を取得する結合方法を使った

次回は「RAG構築編」として、社内文書をDify Knowledgeに登録し、自然言語で検索できる機能を実装します。

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

参考リンク

3
2
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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?