🤖

RAGとフィードバックを活用した、継続学習する返信生成サービスの作成

に公開

はじめに

WED株式会社でMLエンジニアをしています、ishi2kiです。
WEDでは、ユーザーの方々からレシートを買い取るアプリ「ONE」を開発運用しています。

今回、生成AIの力を借りてユーザーからの問い合わせへの返信内容を自動で生成するサービスを作成したので紹介します。

現状の課題

ユーザーからの問い合わせでエスカレーションが発生した場合、ZendeskからSlackに通知が飛びます。
それを確認し、UCが問い合わせへの返信を作成していたのですが、これには多くの工数がかかっていました。

そこで今回、過去の問い合わせデータを活用し、新規問い合わせに対する返信を自動で生成するAIサービスを作成することにしました。
さらに、生成された回答に対してフィードバックを収集し、それを活用して回答の品質を継続的に改善する仕組みも構築しました。

作成したサービスの使用例

Slackのワークフローを実行して、新規問い合わせをサービスに投げます。このとき、返信方針や追加の情報を与えることができます。
ワークフローを実行すると、Slack Appにmentionが飛び、返信生成サービスにWebhookで問い合わせを投げます。

ワークフロー実行

Slack Appのmention

以下のような返信が同一スレッドに返ってきます。

返信

生成された返信に対して、Slackの絵文字リアクションやmention付きでコメントを書くでFBを送ることができます。

絵文字リアクション

mention付きコメント

使用言語・サービス等

  • Python
  • FastAPI (APIサーバー)
  • BigQuery (データベース)
  • BigQuery VECTOR_SEARCH (ベクトル検索)
  • Vertex AI Gemini (AIサービス)
  • Airflow (DAG管理)
  • TROCCO (データ転送サービス)
  • Slack

フロー

フロー図

準備

  1. 定期的に、Zendeskから問い合わせと返信のペアデータをBQに転送
  2. 1のデータをベクトル化

返信生成

  1. SlackからのWebhookで問い合わせを受信
  2. 実際にUCが過去に行った返信と、Geminiが過去に出力した返信およびフィードバック (以下FB) をRAGで検索
  3. 検索結果をプロンプトに追加し、Geminiによる回答を生成
  4. 生成された回答をSlackに投稿
  5. 生成された回答をBQに保存・ベクトル化

FB収集

  1. Slackの絵文字リアクションとコメントをSlack Webhookで受信
  2. 受信したFBのデータをBQに保存

実装

Slackからの問い合わせ・FB受信

Slackからワークフローを実行することで、新規問い合わせをサービスに投げ、同じスレッドに返信を投稿するようにしています。
この実装については、こちらの記事をご覧ください。

今回作成したサービスでは、これに加えて生成した返信に対してのFBを収集できるようにしています。
返信生成と処理の内容が異なるので、別のエンドポイントを用意しました。Slack Appについてもそれぞれ別のものを使用しています。

app = FastAPI()

@app.post("/generate")
async def generate_reply(request: Request):
    pass

@app.post("/fb")
async def feedback_endpoint(request: Request):
    pass

類似問い合わせ検索

類似問い合わせ検索では、BigQueryのVECTOR_SEARCH機能を活用しています。

1. ベクトル化

準備として、過去の問い合わせと返信のペアをベクトル化してBQに保存しておく必要があります。
毎日問い合わせがBQに転送されてくるので、それに合わせてDAGでベクトル化を行っています。

この例では、過去の問い合わせと返信のペアが保存されているzendesk.inquiry_examplesテーブルをベクトル化しています。
なお、ベクトル化に使用するモデルは事前に作成しておく必要があります。(参考)

CREATE OR REPLACE TABLE zendesk.inquiry_example_embeddings
SELECT * except(content)
FROM 
  ML.GENERATE_EMBEDDING(
    # 事前に作成したベクトル化モデルを指定
    MODEL model_dataset.text_embedding,
    # contentカラムがベクトル化される
    (SELECT inquiry AS content, * FROM inquiry_examples), 
    STRUCT(
      'SEMANTIC_SIMILARITY' AS task_type
    )
  )

2. 検索

返信生成の段階で、VECTOR_SEARCHを使用して、新規問い合わせに近い過去の問い合わせを検索します。

SELECT base.inquiry, base.reply
FROM VECTOR_SEARCH(
  TABLE `zendesk.inquiry_example_embeddings`, 'ml_generate_embedding_result',
  (
    SELECT ml_generate_embedding_result, content AS query
    # 新規問い合わせ (query) をベクトル化
    FROM ML.GENERATE_EMBEDDING(
      MODEL `model_dataset.text_embedding`,
      (
        SELECT '{{ query }}' AS content
      ),
      STRUCT(
        'SEMANTIC_SIMILARITY' AS task_type
      )
    )
  ),
  # 検索結果の上限を設定
  top_k => {{ top_n }}
)

過去にこのサービスが生成した返信とFBのデータについても、同様にベクトル化して検索できるようにしています。

回答生成

検索された類似問い合わせを基に、Vertex AI Geminiを使用して回答を生成します:

from textwrap import dedent

import vertexai
from jinja2 import Template
from vertexai.generative_models import GenerationConfig, GenerativeModel

vertexai.init(project=PROJECT_ID, location="global")

class GeminiService:
    def __init__(self, model_name: str):
        self.model = GenerativeModel(model_name)
        self.config = GenerationConfig(
            temperature=0,
            max_output_tokens=4096,
            response_mime_type="text/plain",
        )
        template_path = "prompt.txt"
        with open(template_path, encoding="utf-8") as f:
            self.prompt_template = Template(f.read())

    # プロンプトを生成して回答を生成
    def generate_response(self, **kwargs):
        prompt = self.build_prompt(**kwargs)
        response = self.model.generate_content(prompt, generation_config=self.config)
        return response.text

    # Jinjaテンプレートを使用してプロンプトを生成
    def build_prompt(
        self,
        **kwargs,
    ):
        prompt = self.prompt_template.render(**kwargs)
        prompt = dedent(prompt).strip()
        return prompt

prompt.txtは以下のようなテンプレートです。
RAGで取得した例だけでなく、ワークフロー実行時に与えた返信方針・追加情報も入れるようにしています。
また、過去の生成例は、フィードバックが良いものと悪いものに分け、Geminiが明確に区別できるようにしています。

prompt.txt
あなたは「レシート買取アプリONE」のUC(ONE Support)です。
新規の問い合わせに対する返信を作成してください。

追加プロンプトが与えられる場合があります。その場合は、以下の<制約>よりも追加プロンプトを優先して従ってください。
過去の問い合わせと返信履歴の例を参照できます。
また、あなたが以前に生成した返信と、それに対して与えられたフィードバックも参照できます。
- feedback_emoji はoneからfiveの範囲で、数値が大きいほど良い返信を意味します。
- oneは全く使えない、fiveは問題なしを意味します。
- **フィードバックコメントがある場合は必ず参照して活用して下さい。**

<制約>
- 初回回答ではなく、最終回答を作成してください。そのため「現在調査中です」などの回答は避けてください。
- 返信にはテンプレートを使用してください。
(中略)
</制約>

<追加プロンプト>
{{ additional_prompt }}
</追加プロンプト>

<実際の問い合わせ・返信履歴の例>
{{ similar_inquiries }}
</実際の問い合わせ・返信履歴の例>

<あなたの返信例 (Good)>
<指示>
- feedback_emoji がfour〜fiveの場合は、前回の回答に問題がなかったことを意味します。
- 類似の問い合わせであればそのまま使っても構いません。
- ただし、feedback_comment ある場合は、それを元に改善を行って下さい。
</指示>
<例>
{{ similar_replies_with_good_fb }}
</例>
</あなたの返信例 (Good)>

<あなたの返信例 (Bad)>
<指示>
- feedback_emoji がone〜threeの場合は、前回の回答に改善の余地があったことを意味します。その場合は feedback_comment を分析し、以下の観点で不足を補ったより良い返信を作成してください:
  - ユーザーの具体的な問題に対して、より効果的な解決策を提示する
  (中略)
</指示>
<例>
{{ similar_replies_with_bad_fb }}
</例>
</あなたの返信例 (Bad)>

<テンプレート>
ご返信にお時間をいただき恐れ入ります。
ONE Supportです。

[返信内容]

今後ともONEをよろしくお願いいたします。
</テンプレート>

新しい問い合わせ: {{ inquiry }}

返信: 

生成した返信・FBの保存

返信を生成した際、および、FBを収集した際に、以下のようなSQLを実行してUPSERTを行います。
生成した返信に対してユニークなcomment_idを付与し、それをキーにしています。

MERGE `zendesk.generated_responses` T
USING (
    SELECT
        '{{ comment_id }}' as comment_id,
        '{{ inquiry }}' as inquiry,
        '{{ generated_reply }}' as generated_reply,
        '{{ feedback_emoji }}' as feedback_emoji,
        '{{ feedback_comment }}' as feedback_comment,
        TIMESTAMP('{{ timestamp }}') as created_at,
        TIMESTAMP('{{ timestamp }}') as updated_at
) S
ON T.comment_id = S.comment_id
# 生成した返信がある場合は、FBを更新
WHEN MATCHED THEN
    UPDATE SET
        feedback_emoji = CASE 
            WHEN S.feedback_emoji != '' THEN S.feedback_emoji 
            ELSE T.feedback_emoji 
        END,
        feedback_comment = CASE 
            WHEN S.feedback_comment != '' THEN S.feedback_comment 
            ELSE T.feedback_comment 
        END,
        updated_at = S.updated_at
# 生成した返信がない場合は、新規に追加
WHEN NOT MATCHED THEN
    INSERT (
        comment_id, 
        inquiry, 
        generated_reply, 
        feedback_emoji, 
        feedback_comment,
        created_at, 
        updated_at
    )
    VALUES (
        S.comment_id, 
        S.inquiry, 
        S.generated_reply, 
        S.feedback_emoji, 
        S.feedback_comment,
        S.created_at, 
        S.updated_at
    ) 

まとめ

今回、RAGとフィードバック機能を活用した返信生成サービスを作成しました。

RAGで過去の問い合わせデータを参照してプロンプトを動的に生成することで、プロンプトが冗長になることを防ぎつつ、回答の品質を向上させることができます。
また、フィードバックにより生成された回答の品質を継続的に改善する仕組みを構築し、システムの長期的な品質向上を図っています。

今後は、返信方針や制約を集めたナレッジベースを作成し、本サービス利用者が参照・編集できるような仕組みを作成しようと考えています。


参考

WED Engineering Blog

Discussion