はじめに
前回は、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構築編】