DifyのChatflowでヒアリング型ボット(項目を聞き出して構造化するボット)を作っていると、こんな症状に振り回されます。
- Parameter Extractorが、選択肢に無い独自ラベルを勝手に返してくる
- 全項目が揃っていないのに「揃った」判定になり、初回ターンでWebhookが暴発する
- 会話変数に書き込んだはずの値が、同じターン内では読めない
筆者は不動産の反響対応を想定したヒアリング型デモボット(5項目を聞き出してWebhookで外部に送る構成)をDify Chatflowで構築し、上記すべてを踏みました。この記事では、その実装で確立した**「LLMを信用せず、コードで締める」制御パターン**をまとめます。
この記事で分かること
- ヒアリング型Chatflowの推奨ノード構成と責務分離
- Parameter Extractorの抽出を安定させるモデル選定とdescriptionの書き方
- 「初回完成ターンのみ発火」を実現するガードパターン
- 会話変数のcommit遅延・デフォルト値まわりのクセと回避策
- Make(Webhook)連携時の罠と、デバッグの追い方
⚠️ 本記事のDify・Makeの挙動はすべて執筆時点(2026年7月)に筆者環境で観測したものです。バージョンやモデルにより挙動が変わる可能性があります。
ヒアリング型Chatflowの基本構成
「N項目を聞き出す → 会話変数に蓄積 → 全項目が揃ったら外部送信」という構成の推奨フローです。
ユーザー入力
→ Parameter Extractor ← 項目抽出(モデル: Sonnet推奨)
→ コード実行 ← 正規化・完成判定の中核ロジック
→ 変数代入 ← c_xxx 会話変数にcommit
→ IF/ELSE ← hot/cold等の分岐(必要なら)
→ LLM ← 会話応答の生成のみ(Haikuで十分)
→ IF/ELSE_2 ← 完成判定ゲート
├─ IF(初回完成)→ HTTPリクエスト → 回答
└─ ELSE ──────────────────────────→ 回答
肝は責務分離です。
-
コード実行: 抽出値の正規化、完成判定(
is_complete,is_first_completion) - LLMノード: 会話応答の生成のみ。業務判定をLLMに任せない
- IF/ELSE_2: 「初回完成ターンのみHTTP発火」のゲート
コード実行ノードに別ロジックを混ぜたり、LLMに完成判定をさせたりすると、フローが壊れたときに切り分けできなくなります。
Parameter Extractorのクセと制御
モデル選定が最重要
筆者が試した範囲では、descriptionの遵守度がモデルで大きく違いました。
| モデル | description遵守 | 判断 |
|---|---|---|
| Haiku 4.5 | △(独自ラベル連発) | NG |
| Sonnet 4.6 | ◯ | Parameter Extractor専用に採用 |
| Opus 4.7 | ◎ | 過剰・コスト高 |
応答生成のLLMノードはHaikuで十分なので、「抽出だけSonnet、応答はHaiku」がコスト効率の良い組み合わせでした。
descriptionの書き方
各パラメータのdescriptionには次の3点を必ず入れます。
- カテゴリを
「はい」「検討中」「まだ未定」または空文字の形で列挙して明示する - 「該当しない・言及が無ければ空文字」を必ず書く
- 「確認質問の言い回し(『〜できますか?』など)は抽出しない」を明記する
3つ目はフライング抽出対策です。「青山の1LDKを検討中です、内見できますか?」という入力から、内見意向="検討中" や "はい" を勝手に取ってしまう事故が実際に起きます。
それでも独自ラベルは返ってくる
descriptionを整えても、LLMは「希望」のような選択肢外のラベルを返すことがあります。100%の制御は不可能という前提で、コード実行側に正規化のフェイルセーフを置きます。
valid_viewings = ("はい", "検討中", "まだ未定")
def normalize_viewing(v):
if not v:
return ""
if v in valid_viewings:
return v
if v == "希望": # モデルが返しがちな独自ラベルを吸収
return "はい"
return "" # その他の不正値は「未取得」扱い
不正値を空扱いにすることで、「変なラベルのせいで完成判定が通ってしまう」事故も防げます。
細かい罠
- 入力変数に
sys.queryを設定し忘れると全項目が空で返ってくる(無言で失敗するので気づきにくい) - 抽出値の
〜と~、ヶとヵのような文字ゆれでIF条件にヒットしない → コード側でin演算子の曖昧マッチにする
「初回で揃った扱いになる」暴発を防ぐガード
一番痛かったのがこれです。完成判定をIF/ELSEに直結すると、完成後は毎ターンHTTPリクエストが発火します。かといって「送信済みフラグを会話変数に立てる」方式(c_sentガード)は、筆者環境では会話変数のデフォルト値が反映されないバグに当たって動きませんでした(後述)。
確実に動いたのは、コード実行内で完結する is_first_completion 方式です。
def main(p_name, ..., c_name, ...):
# p_xxx(今ターンの抽出値)優先、空なら c_xxx(会話変数)フォールバック
customer_name = p_name if p_name else (c_name or "")
# ...(他項目も同様)
# 今ターンで完成しているか
complete = bool(customer_name and period and area_pref
and room_type and viewing_intent)
# 前ターンまでに既に完成していたか(c_xxx だけを見る)
prev_complete = bool(c_name and c_period and c_area
and c_room and c_viewing)
return {
"is_complete": "true" if complete else "false",
"is_first_completion":
"true" if (complete and not prev_complete) else "false",
}
ポイントは2つです。
- 「今ターンの状態(p+c)」と「前ターンまでの状態(cのみ)」を別々に判定し、差分が出た瞬間だけ
is_first_completion=trueにする - ゲートのIF/ELSEの条件は
is_first_completion contains trueの1個だけにする
これで初回完成ターンのみ発火し、以降は prev_complete=True になるので発火しません。
なお、業務的に「未定でもリードとして残したい」項目(例: 予算)は、完成判定から外して任意項目にするのが実務的でした。必須にすると、そこで会話が止まったユーザーを丸ごと取りこぼします。
会話変数のクセ
筆者環境で観測した会話変数の挙動です。
-
commit遅延: 変数代入で書き込んだ値は、同じターン内では読めません。同ターン内で使いたい値は、会話変数ではなくコード実行ノードの出力(
コード実行.xxx)を直接参照します。 - デフォルト値が反映されない(少なくともString型で観測): 新規セッションなのに、前セッションの値が残ったように振る舞うケースがありました。会話変数を送信済みフラグに使うガード方式を諦めた直接の理由です。
- IF/ELSEのELSE分岐を未接続にすると前ターンの値が残る: ELSE側も必ずどこかに接続します。
前ターンの残値対策として、prev_complete の判定側でも正規化と同じ厳格チェック(有効値以外は無効扱い)をかけておくと安全です。
Make(Webhook)連携の罠
Dify → Make → 外部サービスの構成で踏んだものです。
- 「Immediately as data arrives」トグルをONにしないと動かない: OFFだとRun onceモード(手動実行時しか処理されない)で、「Difyからは200が返るのに何も起きない」状態になります。
-
Webhookは初回受信時にJSON構造を学習する: テストで
{"test":""}を送ると以後それしか認識しません。本番構造に変えたら Redetermine data structure を1回実行します。 -
Filterの型推論は信用しない: Boolean/Text/Numericのどの比較も単独では安定しませんでした。Filterの値欄を fxモードにして
lower(1.hot_label)のようにテキスト関数で包み、型を強制してから比較するのが確実でした。
デバッグの進め方
「外部にデータが届かない」ときは、上流から順に切り分けます。
- Difyのログ → 実行追跡タブ: HTTPリクエストノードが緑チェックで現れているか。無ければIF分岐に入れていない
-
コード実行の出力:
is_complete/is_first_completionが期待値か。各項目が埋まっているか -
IF/ELSEの入出力: 実際の比較値が
"true"か。違えばコード実行側に戻る -
HTTPリクエストの出力:
status_code: 200ならDify側は正常 → 以降はMake側の問題 - MakeのHistoryタブ: レコードが無ければDify→Makeの通信かSchedulingがOFF
Difyから届いているか自体が怪しいときは、送信先を一時的に webhook.site に変えると、Dify側とMake側のどちらの問題かを機械的に切り分けられます。
チューニングは正常系・異常系(雑談連投、質問への質問、スパム入力等)・コンプラ系(値引き要求、個人情報等)を混ぜたテストシナリオを一括で流し、合格率で判断する方式にしました。また「雑談3回でエスカレーション」のような回数カウント系はLLMが厳密に守らないため、厳格にしたければコード側でカウンタを実装するしかない、というのも実感です。
まとめ
- Parameter Extractorはモデル選定(抽出はSonnet級)とdescriptionの明示(カテゴリ列挙・空文字指定・確認質問の除外)で安定させる。それでも独自ラベルは返るのでコード側の正規化フェイルセーフが必須
- 送信の暴発は、会話変数フラグではなくコード実行内で完結する
is_first_completion方式でガードする - 会話変数は同ターン内で読めない(commit遅延)。同ターンで使う値はコード実行の出力を直接参照する
- Make連携は「Immediately as data arrives」「Redetermine data structure」「Filterのfx型強制」の3点をまず疑う
- デバッグは Difyログ → コード出力 → IF入出力 → HTTPステータス → Make履歴 の順で上流から切り分ける
一言でまとめると、「LLMの出力は信用せず、判定と制御はすべてコード実行ノードに寄せる」。これがヒアリング型Chatflowを安定運用に持ち込む最短ルートでした。冒頭の注記のとおり、挙動は執筆時点の観測ベースなので、実装時はご自身の環境での再確認をおすすめします。