💡

【Dify】問い合わせチャットボットを最適化する

に公開

はじめに

本記事では、ローコードAIプラットフォームであるDifyを活用して、問い合わせ対応を効率化するチャットボットの構築と最適化について解説します。特に、ナレッジベースを活用した正確な回答と、LLMによる柔軟な対応を組み合わせることで、ユーザーの満足度向上を目指します。

チャットボットの要件定義

今回構築するチャットボットは、以下の要件を満たすものとします。

  • 質問に対して、事前に登録されたナレッジベースの知識を用いて回答する。
  • ナレッジベースで解決できない場合、LLMが回答できそうな関連性の高い質問リストを出力する。
  • 提示された質問リストの中からユーザーが質問を選択した場合、LLMがその質問に対してナレッジベースの知識から回答を出力する。

質問の流れに関して

今回、質問リストの出力はボタン形式で行いました。背景としてはそのまま質問のリストを出力してもユーザーからすると、"質問内容を選ぶ⇒コピペ⇒回答"という工程を挟まなければいけないです。実用といった面でこれは使い勝手が悪く感じてしまいます。そこでボタン形式を選択することでこの1工程をなくすことで使いやすさを向上させUI/UXの向上を図っています。

オーケストレートの整形

Difyでチャットボットのオーケストレーションを実装するにあたり、2つの会話変数を利用します。

  1. check: ユーザー自身が質問を入力したのか、それともLLMが出力した質問候補を選択したのかを判定するために使用します。
  2. record: ユーザーの質問に対するナレッジベースの検索結果や、LLMによる最初の回答などを記録します。

処理の流れは以下の図の通りです。

これを実装したのが以下のオーケストレートです。

if/elseブロックで回答の処理を分けています。
こちら上の枝から順に次の処理を行っています。

  • 「いいえ」と答えられた場合に質問候補を出力する
  • 「はい」と答えられた場合のテンプレートの回答
  • 選ばれた質問に対して回答を出力する
  • 最初の回答を作成する

このようにして問い合わせチャットボットを最適化していきます。

実際のDSLファイルはこちら
app:
  description:
    問い合わせチャットボット
  icon: 🤖
  icon_background: '#FFEAD5'
  mode: advanced-chat
  name: 質問候補出力_0528
  use_icon_as_answer_icon: false
kind: app
version: 0.1.5
workflow:
  conversation_variables:
  - description: 2回目以降の質問か確認する変数
    id: 05132f67-00db-4678-9110-af36329c7933
    name: check
    selector:
    - conversation
    - check
    value: 0
    value_type: number
  - description: 知識取得を保存するための変数
    id: 0522c864-16af-419c-9eb8-edbb4ca19c94
    name: record
    selector:
    - conversation
    - record
    value: []
    value_type: array[object]
  environment_variables: []
  features:
    file_upload:
      allowed_file_extensions:
      - .JPG
      - .JPEG
      - .PNG
      - .GIF
      - .WEBP
      - .SVG
      allowed_file_types:
      - image
      allowed_file_upload_methods:
      - local_file
      - remote_url
      enabled: false
      fileUploadConfig:
        audio_file_size_limit: 50
        batch_count_limit: 5
        file_size_limit: 15
        image_file_size_limit: 10
        video_file_size_limit: 100
        workflow_file_upload_limit: 10
      image:
        enabled: false
        number_limits: 3
        transfer_methods:
        - local_file
        - remote_url
      number_limits: 3
    opening_statement: ''
    retriever_resource:
      enabled: true
    sensitive_word_avoidance:
      enabled: false
    speech_to_text:
      enabled: false
    suggested_questions: []
    suggested_questions_after_answer:
      enabled: false
    text_to_speech:
      enabled: false
      language: ''
      voice: ''
  graph:
    edges:
    - data:
        sourceType: llm
        targetType: answer
      id: llm-answer
      selected: false
      source: llm
      sourceHandle: source
      target: answer
      targetHandle: target
      type: custom
    - data:
        isInIteration: false
        sourceType: start
        targetType: if-else
      id: 1748411157898-source-1748411163886-target
      selected: false
      source: '1748411157898'
      sourceHandle: source
      target: '1748411163886'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: knowledge-retrieval
        targetType: assigner
      id: 1748411314007-source-1748411332733-target
      selected: false
      source: '1748411314007'
      sourceHandle: source
      target: '1748411332733'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: assigner
        targetType: llm
      id: 1748411332733-source-llm-target
      selected: false
      source: '1748411332733'
      sourceHandle: source
      target: llm
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: if-else
        targetType: knowledge-retrieval
      id: 1748411163886-false-1748411314007-target
      selected: false
      source: '1748411163886'
      sourceHandle: 'false'
      target: '1748411314007'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: if-else
        targetType: llm
      id: 1748411163886-true-1748411430003-target
      selected: false
      source: '1748411163886'
      sourceHandle: 'true'
      target: '1748411430003'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: llm
        targetType: assigner
      id: 1748411430003-source-1748412493118-target
      selected: false
      source: '1748411430003'
      sourceHandle: source
      target: '1748412493118'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: assigner
        targetType: answer
      id: 1748412493118-source-1748412415854-target
      selected: false
      source: '1748412493118'
      sourceHandle: source
      target: '1748412415854'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: if-else
        targetType: llm
      id: 1748411163886-0acae8b5-0eda-4d35-87dd-d6b25be585ac-1748412516107-target
      selected: false
      source: '1748411163886'
      sourceHandle: 0acae8b5-0eda-4d35-87dd-d6b25be585ac
      target: '1748412516107'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: llm
        targetType: assigner
      id: 1748412516107-source-1748439251080-target
      source: '1748412516107'
      sourceHandle: source
      target: '1748439251080'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: assigner
        targetType: answer
      id: 1748439251080-source-1748412523292-target
      source: '1748439251080'
      sourceHandle: source
      target: '1748412523292'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: if-else
        targetType: assigner
      id: 1748411163886-e419857a-9ab7-4d98-812a-9955a0926470-1748440430377-target
      source: '1748411163886'
      sourceHandle: e419857a-9ab7-4d98-812a-9955a0926470
      target: '1748440430377'
      targetHandle: target
      type: custom
      zIndex: 0
    - data:
        isInIteration: false
        sourceType: assigner
        targetType: answer
      id: 1748440430377-source-1748440411152-target
      source: '1748440430377'
      sourceHandle: source
      target: '1748440411152'
      targetHandle: target
      type: custom
      zIndex: 0
    nodes:
    - data:
        desc: ''
        selected: false
        title: 開始
        type: start
        variables: []
      height: 54
      id: '1748411157898'
      position:
        x: 173.81479720216078
        y: 282
      positionAbsolute:
        x: 173.81479720216078
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: true
          variable_selector:
          - conversation
          - record
        desc: 最初の質問対応
        model:
          completion_params:
            temperature: 0.7
          mode: chat
          name: anthropic.claude-3-5-sonnet-20240620-v1:0
          provider: bedrock
        prompt_template:
        - id: b6978f38-6208-41f1-be70-586fca987fcf
          role: system
          text: "{{#context#}}の中から{{#sys.query#}}に対応する最適な直接的な回答を書き抜いてください。\n** URLがある場合は続けて書き抜いてください\
            \ **\n** コンプライアンスマニュアルを参照している場合 **は参照ページも出力してください\n\nもし直接的な回答がない場合には情報から回答を予測して結論から回答を作成してください。\n\
            \nまた回答はユーザーに見やすいように形を整えてください\n\n\n出力:\n    - 直接的な回答がある場合\n        - 回答内容:[質問に対する回答]\n\
            \        - コンプライアンスマニュアルを参照した場合\n            -マニュアルページ番号: [参照ページ一覧]\n\
            \    - 直接的な回答がない場合\n        - 結論: [質問に対する回答]を出力\n        - 参考事例など: [回答に対して参考にした参考事例、理由]\n\
            \        - コンプライアンスマニュアル参照ページ\n            -マニュアルページ番号: [参照ページ一覧"
        - id: 2a971da4-bff2-41db-96a3-99d92487fd72
          role: user
          text: '質問内容: {{#sys.query#}}'
        selected: false
        title: LLM
        type: llm
        variables: []
        vision:
          enabled: false
      height: 126
      id: llm
      position:
        x: 1284.5947934199883
        y: 709.6839269295078
      positionAbsolute:
        x: 1284.5947934199883
        y: 709.6839269295078
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: '{{#llm.text#}}

          解決しましたか?

          <button data-message="はい">はい</button>

          <button data-message="いいえ">いいえ: 質問リストを出力</button>

          <button data-message="FB">FB</button>'
        desc: ''
        selected: false
        title: 回答
        type: answer
        variables: []
      height: 182
      id: answer
      position:
        x: 1616.4460950649911
        y: 709.6839269295078
      positionAbsolute:
        x: 1616.4460950649911
        y: 709.6839269295078
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        cases:
        - case_id: 'true'
          conditions:
          - comparison_operator: is
            id: 16980755-f8d2-438f-b7e7-a58f3b6ea3e1
            value: いいえ
            varType: string
            variable_selector:
            - sys
            - query
          id: 'true'
          logical_operator: and
        - case_id: e419857a-9ab7-4d98-812a-9955a0926470
          conditions:
          - comparison_operator: is
            id: ced305db-4b25-4185-a03d-75dbdda49e9d
            value: はい
            varType: string
            variable_selector:
            - sys
            - query
          id: e419857a-9ab7-4d98-812a-9955a0926470
          logical_operator: and
        - case_id: 0acae8b5-0eda-4d35-87dd-d6b25be585ac
          conditions:
          - comparison_operator: '='
            id: c5490bd5-b11a-4fd8-a708-de8bb0ec8498
            value: '1'
            varType: number
            variable_selector:
            - conversation
            - check
          id: 0acae8b5-0eda-4d35-87dd-d6b25be585ac
          logical_operator: and
        desc: ''
        selected: false
        title: IF/ELSE
        type: if-else
      height: 222
      id: '1748411163886'
      position:
        x: 437.16839204506675
        y: 282
      positionAbsolute:
        x: 437.16839204506675
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        dataset_ids:
        - 7c2233da-b668-4c30-b9e3-9f7e886fe3c3
        desc: ナレッジから知識取得
        multiple_retrieval_config:
          reranking_enable: false
          reranking_mode: reranking_model
          reranking_model:
            model: cohere.rerank-v3-5:0
            provider: bedrock
          top_k: 4
        query_variable_selector:
        - sys
        - query
        retrieval_mode: multiple
        selected: false
        title: 知識取得
        type: knowledge-retrieval
      height: 120
      id: '1748411314007'
      position:
        x: 717.6765703899466
        y: 709.6839269295078
      positionAbsolute:
        x: 717.6765703899466
        y: 709.6839269295078
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: 知識取得の内容を記録する
        items:
        - input_type: variable
          operation: over-write
          value:
          - '1748411314007'
          - result
          variable_selector:
          - conversation
          - record
          write_mode: over-write
        selected: false
        title: record
        type: assigner
        version: '2'
      height: 116
      id: '1748411332733'
      position:
        x: 995
        y: 709.6839269295078
      positionAbsolute:
        x: 995
        y: 709.6839269295078
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: true
          variable_selector:
          - conversation
          - record
        desc: '「いいえ」の場合

          質問候補を出力するLLM

          ボタン形式で出力する'
        model:
          completion_params:
            temperature: 0.7
          mode: chat
          name: anthropic.claude-3-5-sonnet-20240620-v1:0
          provider: bedrock
        prompt_template:
        - edition_type: basic
          id: cca3033d-5b1e-4794-a002-351f1d00872e
          role: system
          text: "{{#context#}}の中で答えられる質問の候補をできるだけリスト形式で出力してください。ボタンで入力できるように出力してください。\n\
            \n\n出力形式:\n  - 回答できる質問のリスト:\n    - <button data-message=\"[質問内容]\">[質問内容]</button>"
        - id: e2cf3da8-f34a-495a-84b7-70537da16870
          role: user
          text: '質問内容: コンテキストの中の質問のリストを教えてください。'
        selected: false
        title: LLM_質問出力
        type: llm
        variables: []
        vision:
          enabled: false
      height: 158
      id: '1748411430003'
      position:
        x: 717.6765703899466
        y: 82.29388626463248
      positionAbsolute:
        x: 717.6765703899466
        y: 82.29388626463248
      selected: true
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: '​{{#1748411430003.text#}}

          <br>

          **他の質問をする場合は必ず押してください**

          <button data-message="他の質問へ">他の質問をする</button>'
        desc: 質問候補を出力する
        selected: false
        title: 回答_質問出力
        type: answer
        variables: []
      height: 178
      id: '1748412415854'
      position:
        x: 1268.5130164500297
        y: 82.29388626463248
      positionAbsolute:
        x: 1268.5130164500297
        y: 82.29388626463248
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: check=1
        items:
        - input_type: constant
          operation: set
          value: 1
          variable_selector:
          - conversation
          - check
          write_mode: over-write
        selected: false
        title: check_on
        type: assigner
        version: '2'
      height: 116
      id: '1748412493118'
      position:
        x: 999.594793419988
        y: 82.29388626463248
      positionAbsolute:
        x: 999.594793419988
        y: 82.29388626463248
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        context:
          enabled: true
          variable_selector:
          - conversation
          - record
        desc: 'check=1の場合

          2回目以降の回答を出力する'
        model:
          completion_params:
            temperature: 0.7
          mode: chat
          name: anthropic.claude-3-5-sonnet-20240620-v1:0
          provider: bedrock
        prompt_template:
        - id: 08edbacb-74ee-4a96-abc2-beb651127ac3
          role: system
          text: '{{#context#}}をもとに回答を作成してください'
        - id: 3111a21f-9ecd-4973-bbb4-67f5403686c0
          role: user
          text: '質問内容: {{#sys.query#}}'
        selected: false
        title: LLM_質問回答2
        type: llm
        variables: []
        vision:
          enabled: false
      height: 142
      id: '1748412516107'
      position:
        x: 717.6765703899466
        y: 481.4646289196917
      positionAbsolute:
        x: 717.6765703899466
        y: 481.4646289196917
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: '{{#1748412516107.text#}}

          解決しましたか?

          <button data-message="はい">はい</button>

          <button data-message="いいえ">いいえ: 質問リストを出力</button>

          <button data-message="FB">フィードバックをする</button>'
        desc: ''
        selected: false
        title: 回答_2回目
        type: answer
        variables: []
      height: 182
      id: '1748412523292'
      position:
        x: 1289.1895868399763
        y: 481.4646289196917
      positionAbsolute:
        x: 1289.1895868399763
        y: 481.4646289196917
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: check=0
        items:
        - input_type: constant
          operation: set
          value: 0
          variable_selector:
          - conversation
          - check
          write_mode: over-write
        selected: false
        title: check_off
        type: assigner
        version: '2'
      height: 116
      id: '1748439251080'
      position:
        x: 995
        y: 481.4646289196917
      positionAbsolute:
        x: 995
        y: 481.4646289196917
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        answer: 'ご利用ありがとうございました。

          引き続き入力いただくことでご利用いただけます。



          ※押し間違えてしまった場合はこちら

          <button data-message="いいえ">質問出力</button>'
        desc: はいの場合の回答
        selected: false
        title: 回答_はい
        type: answer
        variables: []
      height: 192
      id: '1748440411152'
      position:
        x: 999.594793419988
        y: 282
      positionAbsolute:
        x: 999.594793419988
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    - data:
        desc: check=0
        items:
        - input_type: constant
          operation: set
          value: 0
          variable_selector:
          - conversation
          - check
          write_mode: over-write
        selected: false
        title: check_off
        type: assigner
        version: '2'
      height: 116
      id: '1748440430377'
      position:
        x: 717.6765703899466
        y: 282
      positionAbsolute:
        x: 717.6765703899466
        y: 282
      selected: false
      sourcePosition: right
      targetPosition: left
      type: custom
      width: 244
    viewport:
      x: 332.71848733935377
      y: 168.19454696909278
      zoom: 0.4575753398790304

ボタンの実装に関して

解決しましたかボタンを実装するときにボタンの機能を実装する必要があります。まず、その実装方法について説明します。Difyでは一部のHTMLの機能を使うことができます。HTMLにはbuttonタグというものがあり、それを用いて解決しましたかボタンを実装します。次の文章を回答ノードに入力してみてください。

解決しましたか?
<button data-message="はい">はい</button>
<button data-message="いいえ">いいえ: 質問リストを出力</button>

出力は次のようになります。

解決しましたかボタン

続いてこれの応用としてLLMで出力した質問候補をボタン形式で出力してみましょう。LLMの出力方法に関してはLLMノードのシステムプロンプトで制御します。出力形式を設定することでボタンの出力を実装することができました。その簡易的なプロンプトと出力のイメージを次に示します。なおLLMの種類によっては出力形式の指示を無視する場合があるのでLLMの選定も重要になってきます。特に、ナレッジにないような質問を出力してしまうと次のLLMが回答できない状況があるので気を付けましょう。

input:
    //ナレッジから取得されたデータの説明//
goal:
    inputのデータから回答できる質問のリストをできる限り挙げボタン形式で出力する
output:
    - 回答できる質問のリスト:
        - <button data-message="[質問内容]">[質問内容]</button>

注: ここでのぞんだ回答がないので他の質問を手打ちしてしまうかもしれないのでLLMからの出力のほかに必ず次の文言を回答ノードに追加しておく。

**他の質問をする場合は必ず押してください**
<button data-message="他の質問へ">他の質問をする</button>


出力のイメージ

このようにして質問候補をLLMによりボタン形式で出力できました。

まとめ

Difyで問い合わせボットを作成する際に、LLMによる質問候補を生成し、それをユーザーインターフェース上でボタン形式で提示することで、ユーザーはより直感的かつストレスなく目的の回答にたどり着けるようになります。LLMからの出力でUIを指定しておけば出力時にも反映されるのでMarkdown形式と組み合わせて使うとさらにUI/UXの向上が見込めます。

UPGRADE tech blog

Discussion