GPT-3でZennの使い方を回答できる問い合わせシステムを作ってみた

Zennチームの五十嵐です。普段、Zennに関する記事はZennのPublicationに書くのですが、今回は「やってみた」系の内容なのでこちらに記事を書きます。

Zennチームでは、ZennのテキストデータとGPTなどのLLMとを組み合わせて、これまでにない新しい体験を作りたいと考えています。その第一歩として、Zenn公式アカウントの記事(使い方ページ)をベクトルデータにしてPineconeに登録し、OpenAIのChat APIを使って対話形式で質問できるようにしてみました。

・この機能を公開する予定はありません。
・サンプルに使用したテキストはZenn公式アカウントのものだけです。

デモ

若干怪しい部分もありますが、関連するページのテキストを要約して回答できていることが分かります。

実装

本記事ではざっくりとした流れだけを説明します。ソースコードをご覧になりたい方は、GitHubのリポジトリを参照してください。(内容としてはよくあるチュートリアルレベルのものです。)

構成

テキストのベクトル化

テキストデータをベクトルデータベースのPineconeに登録します。Pineconeについては過去にいくつか記事を書いたのでそちらを参照してください。

以下はGoogle Colabで実施します。

  • ベクトル化したい記事のHTMLとそのメタ情報をJSON形式で作成する
  • HTMLから不要なタグを取り除き、必要なテキストのみを抽出する
  • 抽出したテキストを、一定の長さに分割する
  • 分割したテキストOpenAIのEmbeddings APIを使い、ベクトル化する
  • ベクトルデータをPineconeに登録する

詳しく知りたい方は以下のページが参考になります。

Chat APIに組み込む

以下はNext.jsのプロジェクトです。

  • ユーザーから入力された質問をOpenAIのEmbeddings APIでベクトル化する
  • Pineconeに対してベクトル検索をして、類似するベクトルのテキストデータを取得する
  • テキストデータをプロンプトのコンテキストに埋め込み、回答を得る

詳しく知りたい方は以下のページが参考になります。

チューニング

定量的な評価が難しい部分もあるのですが、いくつかやってみて体感的に違いがあったことを紹介します。modelはgpt-3.5-turboを利用しています。

以下が、最終的に使用したプロンプトです。

[
    {
        role: 'system',
        content: `あなたはZennの使い方を学習したカスタマーサポートAIです。
        回答は簡潔に分かりやすく答えてください。
        回答は日本語で行ってください。`,
    },
    {
        role: 'user',
        content: '記事とスクラップの使い分けについて教えて下さい'
    },
    {
        role: 'assistant',
        content: `記事は、学んだことをまとめたものや、自分が発信したい情報をまとめたものを
        公開するためのものです。一方、スクラップは、今まさに取り組んでいる物事や、まだ解決方法
        が分かっていない問題、学習ログなど、気軽に残していくのに最適です。また、スクラップはス
        レッド形式で情報をまとめることができ、他のユーザーとの情報共有や意見交換にも利用でき
        ます。記事とスクラップは、それぞれの目的に合わせて使い分けることが大切です。詳しくは
        以下の記事をご覧ください。

        【Zennのスクラップ(Scraps)の使い方】
        https://zenn.dev/zenn/articles/about-zenn-scraps`
    },
    /* 途中に会話の履歴を挟み込み */
    {
        role: 'user',
        content: `
        ### 指示 ###
        以下のコンテキストと会話履歴を元に回答してください。
        質問に関連する内容がコンテキストに含まれない場合は「その質問に関連する情報は提供
        されていません。」とだけ回答してください。
        回答セクションのフォーマットに従って回答してください。
        回答セクションの{title}と{url}にはコンテキストに含まれるtitleとurlを変更せずに
        出力してください。

        ### コンテキスト ###
        ${sentences?.join('\n')}

        ### 質問 ###
        ${question}

        ### 回答 ###
        {Answer}
        【{title}】
        {url}`
    }
]

※読みやすさのため改行を加えています

指示はsystemよりuserに書く

指示はsystemよりuserのプロンプトに書くほうが、指示が通る確率が高くなることがありました。

こちらは公式ドキュメントにも書かれている推奨事項です。

gpt-3.5-turbo-0301 does not always pay strong attention to system messages. Future models will be trained to pay stronger attention to system messages.

https://platform.openai.com/docs/guides/chat/introduction

zero-shotとfew-shot

プロンプトの指示で回答のフォーマットを指定していますが、たまに守られないことがありました。そこでfew-shotとして1往復の質問と回答を加えたところ、フォーマットに従った回答が得られる確率が上がりました。

質問

記事とスクラップの使い分けについて教えて下さい

zero-shotの回答

記事は、一つのトピックについてまとまった情報を提供するためのものであり、スクラップは、スレッド形式で
情報をまとめる機能であり、開発の記録や情報共有、意見交換に利用されます。記事は、学んだことを後か
ら振り返る形で書くことが多く、スクラップは、今まさに取り組んでいる物事や、まだ解決方法が分かってい
ない問題、学習ログについて、気軽に残していくのに最適です。記事とスクラップは、それぞれの目的に合
わせて使い分けることが重要です。

few-shotの回答

記事は、学んだことをまとめたものや、自分が発信したい情報をまとめたものを公開するためのものです。
一方、スクラップは、今まさに取り組んでいる物事や、まだ解決方法が分かっていない問題、学習ログなど、
気軽に残していくのに最適です。また、スクラップはスレッド形式で情報をまとめることができ、他のユーザ
ーとの情報共有や意見交換にも利用できます。記事とスクラップは、それぞれの目的に合わせて使い分け
ることが大切です。詳しくは以下の記事をご覧ください。

【Zennのスクラップ(Scraps)の使い方】
https://zenn.dev/zenn/articles/about-zenn-scraps

※読みやすさのため改行を加えています

パラメーター temperature, top_p の調整

temperaturetop_pは回答のランダム性を調整するパラメーターです。プロンプトの指示でコンテキストの内容を元に回答するように指定しているので、もともとランダム性はあまり高くありませんでしたが、デフォルトの1ではやや不安定な回答がみられました。違いが分かりやすいように、いずれかを0にした結果と比較してみました。

質問

記事のslugとはなんですか

回答

今回のように答えがある質問に回答するようなケースでは、低い値にすると良さそうです。

入力データの質

プロンプトのコンテキストとなるテキストデータをどのように作成するかは、回答の質に大きく影響します。はじめは、元のテキストデータを文字数の上限を決めて分割していました。しかし、その条件だけでは十分な回答が得られないことがありました。そこで、検証として元のテキストデータを意味のある単位で分割しました。

質問

Publicationを削除したとき、関連付けられている記事はどうなりますか?

文字列長でぶつ切りにしたコンテキストの場合

Publicationを削除したとき、関連付けられている記事の紐付けは解除されます。また、Publication
の設定はすべて削除されます。

【Publicationの使い方】
https://zenn.dev/zenn/articles/how-to-use-publication

文章単位で切り出したコンテキストの場合

Publicationを削除すると、関連付けられている記事の紐付けが解除されます。また、Publication
の設定やフォローも削除されます。ただし、記事自体は削除されず、ユーザーの記事として残ります。必要
に応じて、記事を別のPublicationに紐付けることができます。

【Publicationの使い方】
https://zenn.dev/zenn/articles/how-to-use-publication

※読みやすさのため改行を加えています

意味のある文章で切り出したコンテキストのほうが、より具体的な回答が得られています。

ただ、長さが長すぎるとベクトル検索の精度の問題や、APIのトークン数の問題などもありますので、適切な長さを見極める必要があります。また、文章を意味のある単位で切り出すという作業そのもを自動化することが難しく、メンテナンスが大変になる可能性もあります。

サービスに組み込むために考えること

曖昧さをどう扱うか

デモでは上手くできた回答を紹介しましたが、全部が上手くいったわけではありません。質問の意図を正しく理解できなかったり、単語の意味を拡大解釈してしまったりした場合に、誤った回答を返してしまうことがあります。

そもそもLLMは非常に曖昧なものですので、サービスに組み込む場合は、LLMで完結させようとするのではなく、上手くいかなかった場合のフォロー手段も用意しておくと良いと思います。

意図しない使われ方をどう防ぐか

いわゆるプロンプトインジェクションの対策です。現状では、プロンプトによる対策のテクニックやモデレーションAPIを使うなどのオプションがありますが、完全に防ぐことは難しいと思います。

プライベートなシステムではある程度許容されると思いますが、パブリックなサービスではユーザー認証をして文字数や回数の制限をしっかりかけるなどの対策も考えておきたいです。

まとめ

GPTとベクトルデータベースを使って、Zennの使い方を回答できるチャットシステムを作りました。デモを見ていただくと分かる通り、上手く回答できた場合は、回答を自然な文章にまとめ上げかなり具体的な内容を返せていたことが分かるかと思います。一方で難しさとしては、誤った回答を返してしまうことがあったり、プロンプトをハックされてしまったりと、システムとして考えなければならない点があるということです。

Zennチームでは、これからも自然言語という新しいインターフェイスで新しい体験作りを考えていきますので、今後にご期待ください。