[ChatGPT x Zendesk]顧客からの問い合わせに自動的に返信する仕組みを作り、未来のヘルプデスクを体感してみた

2023.03.19

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは、ゲームソリューショングループの入井です。

ChatGPT等のLLMの活用方法の例として、顧客からの問い合わせに対する自動応答がよく挙げられています。

今回は、問い合わせ対応についてのSaaSであるZendeskとChatGPTを組み合わせ、実際に顧客からの問い合わせに自動的に対応する仕組みを作ってみました。

作ったもの

実際に動いている様子をご紹介します。

まず、以下のようにZendeskのフォームを通して問い合わせを送ります。このフォームは、顧客と問い合わせ対応者が対話できる形式になっており、Zendeskの標準機能で提供されるものです。

今回は、パソコンの電源をつけても画面が上手くつかないという、よくあるシチュエーションを試してみました。問い合わせ先は、パソコンメーカーのヘルプデスクという設定です。

すると、1分もしない内に回答が届きました。こういったトラブル時の対応方法の提案としては、十分な内容でしょう。もちろん、これらの内容は全てChatGPTが先ほどの質問内容を元に出力しています。

提示してもらった方法では解決しなかっということを伝えてみます。

すると、パソコンのメーカーサポートへ繋ぐよう提案されました。

一応、設定的には今問い合わせている先がメーカーのサポートなのですが、そのあたりの微妙なズレはさておき、回答の方向性的には正しく、型番やシリアルナンバーといった細かい点もフォローしているので、良い対応ができていると言えるでしょう。

どうすれば良いかという回答は得られたので、ここでこの問い合わせは終了し、お礼を言いました。Chat GPTからは、すっきりとした解決に至れなかった点をフォローした回答が届きました。きちんと文脈を理解した文章が作れていますね。

ここまでの流れだけを見れば、正直人間を相手にしているのとあまり変わりがないサポートが受けられたと思います。また、タイムスタンプを見れば分かりますが、これらの対応は全て質問から1分以内に届いています。内容的にブラッシュアップが必要ではありますが、質と圧倒的なスピードの伴ったこのような自動回答の仕組みを導入すれば、ヘルプデスクの業務効率はかなり上がるだろうと感じます。

全体構成

今回作った仕組みの全体像を紹介します。

  1. 顧客からの問い合わせをZendeskフォームで受け取り
  2. Zendeskのトリガー機能により、チケットの更新を検知したらAWS Lambdaの関数URLへチケットの情報を送信
  3. Lambda関数は、Zendeskから受け取った顧客の問い合わせ内容をOpen AI APIへ送信
  4. Open AI APIは、GPT-3.5モデルを使って質問に対する回答をレスポンス
  5. Lambda関数は、Open AI APIから受け取った回答を使ってZendesk API経由で元チケット更新
  6. Zendeskの顧客との対話画面に回答を表示

Zendesk単独ではOpen AI APIからの問い合わせ内容でチケット更新ができないので、間にAWS Lambda関数を挟んでいます。

Zendeskのトリガー、Webhook設定

Zendeskには、チケットの作成や更新時にあらかじめ決めておいたアクションを実行するトリガーという機能があります。

トリガとそのしくみについて – Zendeskヘルプ

上記構成の通り、ZendeskとLambdaの連携にはこの機能を使用しています。

トリガーは、チケットの状態が設定した条件を満たした時のみ実行されます。今回は、トリガー設定を以下のようにしました。

条件設定内の現在のユーザーという項目は、チケットを更新したユーザーが誰であるかを見ています。無限ループを防ぐためにも、顧客からの問い合わせ時のみAIによる回答を行いたいため、顧客を意味するエンドユーザーという値にしました。また、同時にWebフォームからの回答時のみという条件もつけています。

アクション設定では、Webhookへの通知をするよう設定しています。

Zendeskでは、特定の外部URLへのリクエストをWebhookとして作成することが可能です。今回は、AWS Lambda関数のURLに対して、更新が行われたチケットのID情報を渡して呼び出すよう設定しました。

AWS Lambdaのコード

Lambda関数を非同期に実行する

今回、AWS Lambdaの関数は二つ用意しました。Zendeskのトリガーからの受け口となる関数と、OpenAI APIやZendesk APIとのやりとりをする関数です。

受け口となるLambda関数は以下のようにしました。

import {LambdaClient, InvokeCommand} from "@aws-sdk/client-lambda";

export const handler =  async(event) => {
  const body = JSON.parse(event.body);

  const lambdaClient = new LambdaClient({ region: "ap-northeast-1" });
  const params = {
    FunctionName: 'chatGptZendeskTest2',
    InvocationType: 'Event',
    Payload: JSON.stringify({ "id": body.id })
  };

  const command = new InvokeCommand(params);
  const response = await lambdaClient.send(command);
  console.log(response);

  return {
      statusCode: 200,
      body: null,
  };
};

chatGPTZendeskTest2というLambda関数にZendeskのチケットID情報を渡して実行した後、すぐにレスポンスを返しています。InvocationTypeとしてEventを指定している=非同期呼び出しをしているので、Lambda関数の呼び出し後は、その終了を待たずにすぐにレスポンスを返します。

このようにわざわざ関数を二つに分けて非同期実行をしている理由は、Zendeskトリガーの仕様に対応するためです。

初めは非同期実行を使用せず1つの関数でZendesk APIを使ってのチケット更新まで行っていましたが、そうするとトリガーの起動がループしてChatGPTからの問い合わせへの回答が延々続いてしまう現象が起きました。

色々とトリガーの動作を試したところ、どうやらWebhook実行のアクションを設定したZendeskトリガーは、Webhook先からのレスポンスが返ってくるまで、チケットの更新が行われてもその内容が反映されない仕様があるようでした。また、内容の更新はされなくてもチケットが更新されたというイベントだけは検知する仕様もあるようでした。

つまり、以下のような動きをしてしまいます。

  1. Zendeskトリガーを受けてLambda関数実行
  2. Lambda関数内でZendeskチケット更新
  3. 2.の更新をトリガーに、再度Lambda関数実行
    • トリガーの実行条件に顧客からの更新時のみと指定してあるが、上述の仕様が原因で実行されてしまう
  4. 以降、1.から3.が無限ループ

この無限ループ現象は、上述の非同期実行の仕組みを取り入れてレスポンスをすぐに返すことで解消されました。

Zendesk APIからチケットのコメントを取得する処理

ChatGPTに自然な会話をさせるためには、それまでの会話のログも渡す必要があります。そのため、以下のコードで対象チケットの全てのコメントを取得しています。

トリガーでチケットの様々な情報をPOSTリクエストに含めることは可能ですが、チケットのコメントには対応していないため改めてZendesk APIから取得しています。

const getZendeskTichketComment = async (tichketId) => {
  const API_ENDPOINT = `https://xxxxxx.zendesk.com/api/v2/tickets/${tichketId}/comments.json`;
  const API_KEY = 'XXXXXXX';

  const options = {
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Basic ${API_KEY}`
    }
  };

  // HTTPS リクエスト
  const data = await new Promise((resolve, reject) => {
    const req = https.request(API_ENDPOINT, options, (res) => {
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        resolve(data);
      });
    });
    req.on('error', (e) => {
      reject(e);
    });

    req.end();
  });

    return JSON.parse(data);
}

Open AI APIでGPT-3.5モデルへ質問する

顧客からあった質問とそれまでの会話の流れをOpenAI APIへ送信し、その回答を得るコードは以下の通りです。

AIからのコメントが連続してしまった場合、途中で処理を中断するようにしています。この判定には、ZendeskでAI Botとして回答するエージェントのIDを使っています。

また、OpenAI APIへ一連の会話の記録を配列にまとめて送る際、どの発言が顧客のものでどの発言がAIによるものなのか設定する必要があります。チケットのコメントの書き手を見て、AIエージェントからのものであれば会話オブジェクトのroleにassistant、それ以外の場合はuserをセットしています。

もちろん、実際のチケットのコメントにはそれ以外にも色々な人が書き込みますが、今回は単純化しています。

export const requestChatGpt = async (comments) => {
  const API_ENDPOINT = 'https://api.openai.com/v1/chat/completions';
  const API_KEY = 'XXXXXXXXX';
    const BOT_AUTHOR_ID = YYYYY

  const requestData = {
    model: "gpt-3.5-turbo",
    messages: [
      {
        role: "system",
        content: "あなたはパソコンメーカーのカスタマーサポートセンターのオペレーターです。ユーザーからの質問に対し、適切な返答をしてください。"
      }
    ]
  };

  if(comments[comments.length - 1].author_id === BOT_AUTHOR_ID) {
    throw new Error(`最後のコメントがAIによるもののため処理中断`);
  }

  comments.forEach((comment) => {
    if(comment.author_id === BOT_AUTHOR_ID) {
      requestData.messages.push(
      {
        role: "assistant",
        content: comment.body
      })
    } else  {
      requestData.messages.push(
      {
        role: "user",
        content: comment.body
      })
    }

  })

  const options = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`
    }
  };

  const data = await new Promise((resolve, reject) => {
    const req = https.request(API_ENDPOINT, options, (res) => {
      let data = '';

      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        resolve(data);
      });
    });

    req.on('error', (e) => {
      reject(e);
    });

    req.write(JSON.stringify(requestData));
    req.end();
  });

  return JSON.parse(data);
}

Zendesk APIで新しくチケットコメント追加

ZendeskのチケットにOpen AI APIから取得したChatGPTの回答でコメントを追加するコードは、以下の通りです。

const postZendeskTichketComment = async (tichketId, message) => {
  const API_ENDPOINT = `https://xxxxxx.zendesk.com/api/v2/tickets/${tichketId}/comments.json`;
  const API_KEY = 'XXXXXXX';
    const BOT_AUTHOR_ID = YYYYY

  const requestData = {
    ticket: {
      comment: {
        body : message,
        public: true,
        author_id: BOT_AUTHOR_ID
      }
    }
  };

  const options = {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Basic ${API_KEY}`
    }
  };

  const data = await new Promise((resolve, reject) => {
    const req = https.request(API_ENDPOINT, options, (res) => {
      let data = '';

      res.on('data', (chunk) => {
        data += chunk;
      });
      res.on('end', () => {
        resolve(data);
      });
    });

    req.on('error', (e) => {
      reject(e);
    });

    req.write(JSON.stringify(requestData));
    req.end();
  });

    return JSON.parse(data);
}

現時点での現実的な利用方法

顧客からの問い合わせに対し、ChatGPTの生成したテキストで回答する仕組みを作ってみましたが、これをそのまま使うことは難しいでしょう。

ヘルプデスクは、ある特定の会社の製品についてのサポート窓口として設置するものですが、素のChatGPTではそういった独自の情報を元にした対話はできません。また、今回の例にもあるように微妙にズレた内容の回答をしてしまうこともあるので、そのまま顧客への回答として使うと誤解を与えてしまう可能性があります。

ただ、上記のような問題点については、使い方の工夫をすることである程度の改善が可能です。

特定の知識を与える方法については、既に色々なツールが公開され始めていますので、これらを組み合わせて調整することでヘルプデスクAIとして実践導入する余地が出てくると思います。

LangChain の DynamicTool を使って DevelopersIO を参考に回答してもらった | DevelopersIO

LlamaIndex(GPT Index)にDevelopersIOの記事を100件読み込ませて質問してみた | DevelopersIO

また、そのまま顧客への回答には使えなくても、回答の下書きを作成するためのものとして使うだけでも十分にメリットを得られると思います。

例えばZendeskの場合では、顧客からの問い合わせが入ったのを検知して、まず社内メモとしてChatGPTとしての回答をチケットに自動コメントし、他のスタッフたちはそれを参考に実際の回答内容を検討する、というようなワークフローにすることで、文章を1から作らなくて良くなるため、対応までの時間がだいぶ短縮されると思われます。

まとめ

ZendeskとChatGPTを連携させる仕組みを作ってみました。

いくつかの問題点はありますが、上手に導入をすることで現時点でもヘルプデスクの十分な効率化が可能と思われます。

LLMの発展と共に今後のヘルプデスク業務はどんどん変革が進むと思われるので、今後もこの分野は注視していきたいです。